Skip to content

[Simple Analytics] Add Data Layer & Form Event Tracking#204

Open
alaca wants to merge 1 commit intofeature/analytics-admin-pagefrom
feature/data-layer-and-form-event-tracking
Open

[Simple Analytics] Add Data Layer & Form Event Tracking#204
alaca wants to merge 1 commit intofeature/analytics-admin-pagefrom
feature/data-layer-and-form-event-tracking

Conversation

@alaca
Copy link
Copy Markdown
Collaborator

@alaca alaca commented Mar 24, 2026

Description of the Change

  • Created a custom database table ({prefix}_mailchimp_sf_form_analytics) for daily aggregated form analytics, with columns for list_id, form_id, event_date, views, and submissions (unique key on list_id, form_id, event_date)
  • Implement client-side form view tracking that adds data-list-id attribute to all form types (block, widget, shortcode) and
    fires an AJAX POST on page load, deduplicated per list_id
  • Hook into successful form submissions via a new mailchimp_sf_form_submission_success action to increment daily submission
  • Table creation runs on plugin activation and via the upgrade routine for existing installs

Closes #198

How to test the Change

  • Activate the plugin on a fresh install and verify the wp_mailchimp_sf_form_analytics table is created
  • Visit a page with a Mailchimp signup form and verify a view is recorded in the analytics table for the correct list_id and
    today's date
  • Place multiple forms (different lists) on one page and verify each list gets its own view count
  • Submit a form successfully and verify the submission count increments
  • Submit a form with invalid data (e.g., missing email) and verify no submission is tracked
  • Navigate to Mailchimp > Analytics in wp-admin and verify that test data is displayed

Changelog Entry

Created a custom database table (mailchimp_sf_form_analytics) for daily aggregated form analytics
Implemented client-side form view tracking via AJAX

Credits

Props @alaca

Checklist:

  • I agree to follow this project's Code of Conduct.
  • I have updated the documentation accordingly.
  • I have added tests to cover my change.
  • All new and existing tests pass.

@github-actions github-actions bot added this to the 2.1.0 milestone Mar 24, 2026
@github-actions github-actions bot added the needs:code-review This requires code review. label Mar 24, 2026
@alaca alaca requested a review from iamdharmesh March 24, 2026 15:51
@iamdharmesh iamdharmesh changed the base branch from develop to feature/analytics-admin-page March 28, 2026 03:45
@iamdharmesh iamdharmesh requested a review from Copilot March 28, 2026 03:45
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_success action on successful submissions and hooks it to increment submission counts.
  • Adds data-list-id to 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.

Comment on lines +88 to +90
$analytics_data = new Mailchimp_Analytics_Data();
$end_date = current_time( 'Y-m-d' );
$start_date = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$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).

Suggested change
$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 ) );

Copilot uses AI. Check for mistakes.
Comment on lines +203 to +208

$this->increment_views( $list_id );
wp_send_json_success();
}

/**
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
$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 );
}
/**

Copilot uses AI. Check for mistakes.
Comment on lines 179 to 184
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' ),
)
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +138
/* 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',
});
}
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +32
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' ) );
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
<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>
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
<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; ?>

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

@iamdharmesh iamdharmesh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @alaca. Overall this looks good and added few feedback/comment. Could you please check?

*
* @var string
*/
const DB_VERSION = '1.0';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const DB_VERSION = '1.0';
const DB_VERSION = '1.0.0';

Comment on lines +82 to +91
$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' )
)
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better to add some error handling here and log the failures.

Comment on lines +107 to +116
$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' )
)
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

const formData = new FormData();
formData.append('action', 'mailchimp_sf_track_form_view');
formData.append('list_id', listId);
formData.append('nonce', window.mailchimpSF.analytics_nonce);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use something like mailchimp_sf_nonce to prevent potential conflict with other plugins.

method: 'POST',
body: formData,
credentials: 'same-origin',
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
});
}).catch(() => {});

To prevent unhandled promise rejections in the browser console

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs:code-review This requires code review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Simple Analytics] Add Data Layer & Form Event Tracking

3 participants