Skip to content

Commit b84f02d

Browse files
chihsuanclaude
andauthored
WooCommerce Analytics: Use Beacon API for reliable event tracking with fallback (#45891)
* Analytics: Use Beacon API for reliable event tracking with keepalive fallback Implements navigator.sendBeacon() as the primary method for sending analytics events, with graceful fallback to fetch() with keepalive option. This ensures reliable event delivery, especially during page unload scenarios where traditional async requests may be cancelled by the browser. Changes: - Add sendEventsViaBeacon() method using navigator.sendBeacon API - Replace @wordpress/api-fetch with native fetch() + keepalive option - Pass REST API endpoint URL from server to handle plain permalinks correctly - Add trackEndpoint to wcAnalytics config in PHP (using rest_url()) - Remove @wordpress/api-fetch dependency from package.json - Remove unused API_NAMESPACE and API_ENDPOINT constants Benefits: - Guaranteed event delivery on page unload (browser queues beacon requests) - Better mobile browser support (especially iOS Safari) - Works correctly with plain permalinks - Filters out sophisticated bots that disable beaconing APIs - Simpler codebase with native Web APIs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * changelog * Enhance event tracking reliability by checking for trackEndpoint availability This update adds a check for the existence of the trackEndpoint in the wcAnalytics configuration before attempting to send events. If the trackEndpoint is not available, a debug message is logged, and the event sending process is halted. The fallback mechanism to fetch with keepalive is retained for cases where the Beacon API fails, ensuring robust event delivery. Changes: - Added a conditional check for window.wcAnalytics?.trackEndpoint - Updated debug logging for better clarity on event sending status * Update pnpm-lock.yaml --------- Co-authored-by: Claude <[email protected]>
1 parent 616d794 commit b84f02d

File tree

7 files changed

+61
-15
lines changed

7 files changed

+61
-15
lines changed

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: changed
3+
4+
Added Beacon API support and replaced @wordpress/api-fetch with native fetch

projects/packages/woocommerce-analytics/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"watch": "pnpm build --watch"
2626
},
2727
"dependencies": {
28-
"@wordpress/api-fetch": "7.35.0",
2928
"debug": "4.4.3"
3029
},
3130
"devDependencies": {

projects/packages/woocommerce-analytics/src/class-universal.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ public function inject_analytics_data() {
8686
// Set the assets URL for webpack to find the split assets.
8787
wcAnalytics.assets_url = '<?php echo esc_url( plugins_url( '../build/', __DIR__ . '/class-woocommerce-analytics.php' ) ); ?>';
8888

89+
// Set the REST API tracking endpoint URL.
90+
wcAnalytics.trackEndpoint = '<?php echo esc_url( rest_url( 'woocommerce-analytics/v1/track' ) ); ?>';
91+
8992
// Set common properties for all events.
9093
wcAnalytics.commonProps = <?php echo wp_json_encode( $common_properties, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ); ?>;
9194

projects/packages/woocommerce-analytics/src/client/api-client.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
/**
22
* External dependencies
33
*/
4-
import apiFetch from '@wordpress/api-fetch';
54
import debugFactory from 'debug';
65
/**
76
* Internal dependencies
87
*/
9-
import { API_NAMESPACE, API_ENDPOINT, BATCH_SIZE, DEBOUNCE_DELAY } from './constants';
8+
import { BATCH_SIZE, DEBOUNCE_DELAY } from './constants';
109
import type { ApiEvent, ApiFetchResponse } from './types/shared';
1110

1211
const debug = debugFactory( 'wc-analytics:api-client' );
@@ -77,6 +76,32 @@ export class ApiClient {
7776
}, DEBOUNCE_DELAY );
7877
};
7978

79+
/**
80+
* Send events using the Beacon API for guaranteed delivery
81+
*
82+
* @param events - The events to send.
83+
* @return True if beacon was successfully queued, false otherwise.
84+
*/
85+
private sendEventsViaBeacon = ( events: ApiEvent[] ): boolean => {
86+
// Check if beacon API is available
87+
if ( typeof navigator === 'undefined' || ! navigator.sendBeacon ) {
88+
debug( 'Beacon API not available' );
89+
return false;
90+
}
91+
92+
try {
93+
// Convert events to JSON and create a Blob with correct content type
94+
const data = JSON.stringify( events );
95+
const blob = new Blob( [ data ], { type: 'application/json' } );
96+
97+
// Send via beacon - returns true if successfully queued
98+
return navigator.sendBeacon( window.wcAnalytics.trackEndpoint, blob );
99+
} catch ( error ) {
100+
debug( 'Beacon API failed: %o', error );
101+
return false;
102+
}
103+
};
104+
80105
/**
81106
* Flush all pending events immediately
82107
*/
@@ -93,7 +118,20 @@ export class ApiClient {
93118
const eventsToSend = [ ...this.eventQueue ];
94119
this.eventQueue = [];
95120

96-
this.sendEvents( eventsToSend );
121+
if ( ! window.wcAnalytics?.trackEndpoint ) {
122+
debug( 'Track endpoint not available' );
123+
return;
124+
}
125+
126+
// Try sending via Beacon API first for guaranteed delivery
127+
const beaconSuccess = this.sendEventsViaBeacon( eventsToSend );
128+
129+
if ( beaconSuccess ) {
130+
debug( 'Sent %d events via Beacon API', eventsToSend.length );
131+
} else {
132+
debug( 'Failed to send events via Beacon API, falling back to fetch with keepalive' );
133+
this.sendEvents( eventsToSend );
134+
}
97135
};
98136

99137
/**
@@ -109,16 +147,22 @@ export class ApiClient {
109147
try {
110148
debug( 'Sending %d events to API', events.length );
111149

112-
const response = await apiFetch< ApiFetchResponse >( {
113-
path: `/${ API_NAMESPACE }/${ API_ENDPOINT }`,
150+
const response = await fetch( window.wcAnalytics.trackEndpoint, {
114151
method: 'POST',
115-
data: events,
152+
headers: {
153+
'Content-Type': 'application/json',
154+
},
155+
body: JSON.stringify( events ),
156+
keepalive: true,
157+
credentials: 'same-origin',
116158
} );
117-
debug( 'API response received: %o', response );
118159

119-
if ( ! response.success ) {
120-
debug( 'Some events failed to send: %o', response.results );
160+
if ( ! response.ok ) {
161+
throw new Error( `HTTP error! status: ${ response.status }` );
121162
}
163+
164+
const data: ApiFetchResponse = await response.json();
165+
debug( 'API response received: %o', data );
122166
} catch ( error ) {
123167
debug( 'Failed to send events to API: %o', error );
124168
// Re-add events to queue for potential retry on next event

projects/packages/woocommerce-analytics/src/client/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,5 @@ export const EVENT_PREFIX = 'woocommerceanalytics_';
33
export const EVENT_NAME_REGEX = /^[a-z_][a-z0-9_]*$/;
44

55
// API Configuration
6-
export const API_NAMESPACE = 'woocommerce-analytics/v1';
7-
export const API_ENDPOINT = 'track';
86
export const BATCH_SIZE = 10;
97
export const DEBOUNCE_DELAY = 1000; // 1 second

projects/packages/woocommerce-analytics/src/client/types/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
declare global {
66
interface Window {
77
wcAnalytics?: {
8+
trackEndpoint: string;
89
eventQueue: Array< { eventName: string; props?: Record< string, unknown > } >;
910
commonProps: Record< string, unknown >;
1011
features: Record< string, boolean >;

0 commit comments

Comments
 (0)