[Simple Analytics] Add Data Layer & Form Event Tracking#204
[Simple Analytics] Add Data Layer & Form Event Tracking#204alaca wants to merge 1 commit intofeature/analytics-admin-pagefrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new “Simple Analytics” data layer to track Mailchimp signup form views/submissions and surfaces the data in the WP Admin Analytics page.
Changes:
- Adds a custom DB table (
{prefix}_mailchimp_sf_form_analytics) plus query/increment helpers and an AJAX handler for form-view tracking. - Emits a new
mailchimp_sf_form_submission_successaction on successful submissions and hooks it to increment submission counts. - Adds
data-list-idto rendered forms and adds frontend JS to POST form-view events.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
mailchimp_widget.php |
Adds data-list-id attribute to widget/shortcode form markup for JS tracking. |
includes/blocks/mailchimp/markup.php |
Adds data-list-id attribute to block form markup for JS tracking. |
assets/js/mailchimp.js |
Adds client-side view tracking that POSTs to admin-ajax.php. |
includes/class-mailchimp-form-submission.php |
Fires a new action on successful form submission for analytics hooks. |
includes/class-mailchimp-analytics-data.php |
New analytics storage/query class, table creation, AJAX handler, and submission tracking hook. |
mailchimp.php |
Loads the analytics data class, registers activation hook, localizes AJAX URL + nonce. |
mailchimp_upgrade.php |
Runs table creation in upgrade routine based on a stored DB version option. |
includes/class-mailchimp-analytics.php |
Removes trailing whitespace only. |
includes/admin/templates/analytics.php |
Fetches analytics data and currently outputs it via print_r() for display. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $analytics_data = new Mailchimp_Analytics_Data(); | ||
| $end_date = current_time( 'Y-m-d' ); | ||
| $start_date = gmdate( 'Y-m-d', strtotime( '-30 days' ) ); |
There was a problem hiding this comment.
$end_date uses current_time('Y-m-d') (site timezone) while $start_date uses gmdate() (UTC). Since event_date is stored using current_time('Y-m-d'), mixing timezones can shift the queried range by a day on some sites. Use a consistent timezone for both (e.g., compute $start_date from current_time('timestamp') or wp_date() in site timezone).
| $analytics_data = new Mailchimp_Analytics_Data(); | |
| $end_date = current_time( 'Y-m-d' ); | |
| $start_date = gmdate( 'Y-m-d', strtotime( '-30 days' ) ); | |
| $analytics_data = new Mailchimp_Analytics_Data(); | |
| $current_timestamp = current_time( 'timestamp' ); | |
| $end_date = wp_date( 'Y-m-d', $current_timestamp ); | |
| $start_date = wp_date( 'Y-m-d', $current_timestamp - ( 30 * DAY_IN_SECONDS ) ); |
|
|
||
| $this->increment_views( $list_id ); | ||
| wp_send_json_success(); | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
handle_form_view() only checks that list_id is non-empty. As written, an unauthenticated request can create rows for arbitrary list_id values, which can bloat the analytics table and pollute reporting. Validate that list_id is one of the configured/known lists (e.g., from the mailchimp_sf_lists option or the active mc_list_id) and reject/ignore unknown IDs (also consider enforcing the expected length/format).
| $this->increment_views( $list_id ); | |
| wp_send_json_success(); | |
| } | |
| /** | |
| // Ensure the list ID is one of the configured/known lists to prevent | |
| // arbitrary IDs from polluting analytics data. | |
| if ( ! $this->is_valid_list_id( $list_id ) ) { | |
| wp_send_json_error( 'Invalid list_id.', 400 ); | |
| } | |
| $this->increment_views( $list_id ); | |
| wp_send_json_success(); | |
| } | |
| /** | |
| * Determine whether a list ID is one of the configured/known lists. | |
| * | |
| * This helps ensure that analytics data is only recorded for legitimate lists | |
| * configured within the plugin options. | |
| * | |
| * @param string $list_id The list ID to validate. | |
| * @return bool True if the list ID is known/configured, false otherwise. | |
| */ | |
| private function is_valid_list_id( $list_id ) { | |
| if ( empty( $list_id ) ) { | |
| return false; | |
| } | |
| $valid_ids = array(); | |
| // Collect list IDs from the stored Mailchimp lists option, if present. | |
| $mailchimp_lists = get_option( 'mailchimp_sf_lists' ); | |
| if ( is_array( $mailchimp_lists ) ) { | |
| foreach ( $mailchimp_lists as $list ) { | |
| // Handle both scalar IDs and associative array structures. | |
| if ( is_string( $list ) || is_int( $list ) ) { | |
| $valid_ids[] = (string) $list; | |
| } elseif ( is_array( $list ) ) { | |
| // Common keys used to store list IDs. | |
| foreach ( array( 'id', 'list_id', 'mc_list_id' ) as $key ) { | |
| if ( isset( $list[ $key ] ) && ! empty( $list[ $key ] ) ) { | |
| $valid_ids[] = (string) $list[ $key ]; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Include the active Mailchimp list ID option, if set. | |
| $active_list_id = get_option( 'mc_list_id' ); | |
| if ( ! empty( $active_list_id ) ) { | |
| $valid_ids[] = (string) $active_list_id; | |
| } | |
| // If we have no configured IDs, fail closed and do not treat arbitrary IDs as valid. | |
| if ( empty( $valid_ids ) ) { | |
| return false; | |
| } | |
| $valid_ids = array_unique( $valid_ids ); | |
| return in_array( (string) $list_id, $valid_ids, true ); | |
| } | |
| /** |
| array( | ||
| 'ajax_url' => trailingslashit( home_url() ), | ||
| 'phone_validation_error' => esc_html__( 'Please enter a valid phone number.', 'mailchimp' ), | ||
| 'analytics_ajax_url' => admin_url( 'admin-ajax.php' ), | ||
| 'analytics_nonce' => wp_create_nonce( 'mailchimp_sf_analytics_nonce' ), | ||
| ) |
There was a problem hiding this comment.
mailchimp_sf_load_resources() is called from admin screens for the form preview (e.g., setup page), and these localized analytics values will cause the frontend tracking code to fire in wp-admin and record “views” from administrators previewing the form. Consider only localizing analytics_ajax_url / analytics_nonce on the frontend (e.g., if ( ! is_admin() )) or otherwise gating tracking so admin previews don’t affect analytics.
| /* Form view tracking for analytics */ | ||
| (function () { | ||
| if (!window.mailchimpSF || !window.mailchimpSF.analytics_ajax_url) { | ||
| return; | ||
| } | ||
|
|
||
| const forms = document.querySelectorAll('.mc_signup_form[data-list-id]'); | ||
| const tracked = {}; | ||
|
|
||
| for (let i = 0; i < forms.length; i++) { | ||
| const listId = forms[i].getAttribute('data-list-id'); | ||
| if (listId && !tracked[listId]) { | ||
| tracked[listId] = true; | ||
|
|
||
| const formData = new FormData(); | ||
| formData.append('action', 'mailchimp_sf_track_form_view'); | ||
| formData.append('list_id', listId); | ||
| formData.append('nonce', window.mailchimpSF.analytics_nonce); | ||
|
|
||
| fetch(window.mailchimpSF.analytics_ajax_url, { | ||
| method: 'POST', | ||
| body: formData, | ||
| credentials: 'same-origin', | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
The new form-view tracking runs immediately when mailchimp.js is loaded, which can also execute in wp-admin contexts (the setup preview explicitly enqueues this script) and inflate view counts. Add an explicit guard to disable tracking in admin/preview contexts (or only enable when the localized analytics config indicates a real frontend render).
| public function init() { | ||
| add_action( 'wp_ajax_mailchimp_sf_track_form_view', array( $this, 'handle_form_view' ) ); | ||
| add_action( 'wp_ajax_nopriv_mailchimp_sf_track_form_view', array( $this, 'handle_form_view' ) ); | ||
| add_action( 'mailchimp_sf_form_submission_success', array( $this, 'track_submission' ) ); | ||
| } |
There was a problem hiding this comment.
This PR introduces new analytics behaviors (AJAX endpoint for view tracking, DB writes, and a new submission-success hook) but doesn’t add/extend automated coverage. There is an existing Cypress suite under tests/cypress; adding at least one e2e test that verifies view tracking + submission tracking increments the expected counts would help prevent regressions.
| <pre><?php print_r( $totals ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r ?></pre> | ||
|
|
||
| <h3><?php esc_html_e( 'Daily Breakdown', 'mailchimp' ); ?></h3> | ||
| <pre><?php print_r( $daily ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r ?></pre> |
There was a problem hiding this comment.
The Analytics admin template is currently outputting raw data via print_r() inside <pre> blocks. This looks like debug output and will ship to production (and bypasses any intended UI/Chart rendering), making the page noisy and harder to maintain. Replace this with proper, escaped rendering (e.g., table/chart markup) or restore the placeholder until the UI is implemented.
| <pre><?php print_r( $totals ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r ?></pre> | |
| <h3><?php esc_html_e( 'Daily Breakdown', 'mailchimp' ); ?></h3> | |
| <pre><?php print_r( $daily ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r ?></pre> | |
| <?php if ( ! empty( $totals ) && is_array( $totals ) ) : ?> | |
| <table class="widefat striped"> | |
| <thead> | |
| <tr> | |
| <th><?php esc_html_e( 'Metric', 'mailchimp' ); ?></th> | |
| <th><?php esc_html_e( 'Value', 'mailchimp' ); ?></th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <?php foreach ( $totals as $metric_key => $metric_value ) : ?> | |
| <tr> | |
| <td><?php echo esc_html( $metric_key ); ?></td> | |
| <td> | |
| <?php | |
| if ( is_array( $metric_value ) || is_object( $metric_value ) ) { | |
| echo esc_html( wp_json_encode( $metric_value ) ); | |
| } else { | |
| echo esc_html( (string) $metric_value ); | |
| } | |
| ?> | |
| </td> | |
| </tr> | |
| <?php endforeach; ?> | |
| </tbody> | |
| </table> | |
| <?php else : ?> | |
| <p><?php esc_html_e( 'No totals data is currently available for this period.', 'mailchimp' ); ?></p> | |
| <?php endif; ?> | |
| <h3><?php esc_html_e( 'Daily Breakdown', 'mailchimp' ); ?></h3> | |
| <?php if ( ! empty( $daily ) && is_array( $daily ) ) : ?> | |
| <?php | |
| $first_row = reset( $daily ); | |
| $has_header = is_array( $first_row ) && ! empty( $first_row ); | |
| ?> | |
| <table class="widefat striped"> | |
| <?php if ( $has_header ) : ?> | |
| <thead> | |
| <tr> | |
| <?php foreach ( array_keys( $first_row ) as $header_key ) : ?> | |
| <th><?php echo esc_html( $header_key ); ?></th> | |
| <?php endforeach; ?> | |
| </tr> | |
| </thead> | |
| <?php endif; ?> | |
| <tbody> | |
| <?php foreach ( $daily as $row ) : ?> | |
| <tr> | |
| <?php | |
| if ( is_array( $row ) ) { | |
| foreach ( $row as $cell_value ) { | |
| if ( is_array( $cell_value ) || is_object( $cell_value ) ) { | |
| $cell_output = wp_json_encode( $cell_value ); | |
| } else { | |
| $cell_output = (string) $cell_value; | |
| } | |
| ?> | |
| <td><?php echo esc_html( $cell_output ); ?></td> | |
| <?php | |
| } | |
| } else { | |
| if ( is_array( $row ) || is_object( $row ) ) { | |
| $row_output = wp_json_encode( $row ); | |
| } else { | |
| $row_output = (string) $row; | |
| } | |
| ?> | |
| <td><?php echo esc_html( $row_output ); ?></td> | |
| <?php } ?> | |
| </tr> | |
| <?php endforeach; ?> | |
| </tbody> | |
| </table> | |
| <?php else : ?> | |
| <p><?php esc_html_e( 'No daily analytics data is currently available for this period.', 'mailchimp' ); ?></p> | |
| <?php endif; ?> |
iamdharmesh
left a comment
There was a problem hiding this comment.
Thanks @alaca. Overall this looks good and added few feedback/comment. Could you please check?
| * | ||
| * @var string | ||
| */ | ||
| const DB_VERSION = '1.0'; |
There was a problem hiding this comment.
| const DB_VERSION = '1.0'; | |
| const DB_VERSION = '1.0.0'; |
| $wpdb->query( | ||
| $wpdb->prepare( | ||
| "INSERT INTO {$table_name} (list_id, form_id, event_date, views, submissions) | ||
| VALUES (%s, %s, %s, 1, 0) | ||
| ON DUPLICATE KEY UPDATE views = views + 1", | ||
| $list_id, | ||
| $form_id, | ||
| current_time( 'Y-m-d' ) | ||
| ) | ||
| ); |
There was a problem hiding this comment.
Maybe better to add some error handling here and log the failures.
| $wpdb->query( | ||
| $wpdb->prepare( | ||
| "INSERT INTO {$table_name} (list_id, form_id, event_date, views, submissions) | ||
| VALUES (%s, %s, %s, 0, 1) | ||
| ON DUPLICATE KEY UPDATE submissions = submissions + 1", | ||
| $list_id, | ||
| $form_id, | ||
| current_time( 'Y-m-d' ) | ||
| ) | ||
| ); |
| const formData = new FormData(); | ||
| formData.append('action', 'mailchimp_sf_track_form_view'); | ||
| formData.append('list_id', listId); | ||
| formData.append('nonce', window.mailchimpSF.analytics_nonce); |
There was a problem hiding this comment.
Can use something like mailchimp_sf_nonce to prevent potential conflict with other plugins.
| method: 'POST', | ||
| body: formData, | ||
| credentials: 'same-origin', | ||
| }); |
There was a problem hiding this comment.
| }); | |
| }).catch(() => {}); |
To prevent unhandled promise rejections in the browser console
Description of the Change
fires an AJAX POST on page load, deduplicated per list_id
Closes #198
How to test the Change
wp_mailchimp_sf_form_analyticstable is createdlist_idandtoday's date
Changelog Entry
Credits
Props @alaca
Checklist: