Bug Report
Replacing the original write-up. The previous diagnosis ("duplicate requests") was wrong — duplicates are deduplicated server-side in heartbeat processing. The real bug is that some /analytics/ POSTs never reach the server when the user navigates away quickly.
Current Behavior
When the user triggers a type=2 (video heatmap) event and then immediately navigates to another page, the corresponding POST /analytics/ request is initiated in the browser but is cancelled by the browser as the page tears down — before it ever reaches the server. In DevTools → Network, these show up alongside normal {} JSON responses but with the blank-document icon (no response body / cancelled), e.g. the two highlighted rows in the screenshot below:
)
The request payload is well-formed (and keepalive: true is set), but the request never lands. Reproducible on palak-godam.rt.gw by interacting with a Reel Pops modal and clicking a nav link before the modal fully closes / before the next tick of the analytics async pipeline.
Sample payload of a cancelled request (from the Reel Pops release page):
{
"site_url": "https://palak-godam.rt.gw",
"user_token": "83c1aa4a-7b06-4bef-b087-f83b463c1c03",
"wp_user_id": 0,
"account_token": "cuunl3cre4",
"visitor_timestamp": 1779795970929,
"visit_entry_action_url": "https://palak-godam.rt.gw/reel-pops-release/",
"visit_entry_action_name": "Reel Pops release – palak-godam.rt.gw",
"referer_url": "https://palak-godam.rt.gw/2026/05/15/shoppable-video-godam-video-product-gallery-block/",
"config_resolution": "799x836",
"config_os": "Macintosh",
"config_browser_name": "Google Chrome",
"is_page": true,
"post_id": 462,
"post_title": "Reel Pops release",
"type": 2,
"video_id": 457,
"ranges": [[0, 0]],
"video_length": 0,
"job_id": "50uc96k012"
}
(Side note: ranges: [[0, 0]] + video_length: 0 is also worth tightening — a heatmap with zero-duration ranges and an unloaded duration is not a meaningful play event. See "Secondary cleanup" below. The primary bug is the request loss, not this payload shape.)
Expected Behavior
For every analytics.track('video_heatmap', …) call that survives the property-validation checks in trackVideoEvent (player exists, ranges non-empty, video id present), the POST /analytics/ request should reach the server — even if the user navigates away in the very next click.
Root cause
The analytics library path is async all the way down:
window.analytics.track('video_heatmap', …) calls Analytics from the analytics npm package (v0.8.18, @analytics/core v0.13.1).
- The library dispatches a
trackStart action through its Redux store. Each middleware (le in @analytics/core) wraps the call in Promise.resolve(…).then(…) chains — meaning the call to videoAnalyticsPlugin.track happens at least one microtask later.
- The plugin's
track is itself async, so the fetch() call is at least one more microtask after that.
- Only at that point does
fetch() get called, and only then does keepalive: true start protecting the request.
If the user clicks a link before step 4 runs, the browser begins unloading the page and the Promise chain is silently abandoned — fetch() is never called, so there is no in-flight keepalive request to preserve.
The plugin author was already aware of this exact failure mode and worked around it for the unload path. See the comment in sendPlayerHeatmap():
window.analytics.track() dispatches async work (Promises, microtasks). The browser does NOT block page unload waiting for async work to settle — the async chain can be silently abandoned mid-flight. The correct fix is to call fetch() with keepalive: true SYNCHRONOUSLY inside the handler.
…and sendPlayerHeatmap accordingly bypasses window.analytics.track() to call fetch() directly from beforeunload/pagehide/visibilitychange/dispose. But the same bypass is not applied to trackVideoEvent callers, even though those callers (notably the Reel Pops modal) are extremely likely to be followed by a navigation. Example caller — godam-for-woo/assets/src/js/reel-pops/reel-pops-frontend.js:338-346:
beforeRestore: ( { item } ) => {
item.classList.remove( 'is-in-modal' );
const video = item.querySelector( 'video' );
if ( video && window.analytics?.trackVideoEvent ) {
window.analytics.trackVideoEvent( { // ← async pipeline, can be lost
type: 2,
videoId: parseInt( video.getAttribute( 'data-id' ), 10 ),
root: item,
reelPopId: this.reelPopId,
sendPageLoad: false,
} );
}
},
If the user closes the modal (which triggers beforeRestore) and clicks a nav link in the same gesture, the type=2 fetch never gets initiated.
Steps to Reproduce
- Open palak-godam.rt.gw/reel-pops-release/ in Chrome.
- Open DevTools → Network tab, filter by
ana, enable Preserve log.
- Click the Reel Pops widget to open the modal, optionally swipe between reels.
- Close the modal and immediately click a link to navigate to another page (do both within ~500 ms).
- In the Network tab, observe one or more
/analytics/ rows with the blank-document icon (cancelled — no response body). Inspect their Timing → "Stalled" or "(canceled)" status.
Possible Solution
The cleanest fix is to mirror the sendPlayerHeatmap approach for the trackVideoEvent path: bypass the analytics library and issue fetch( … , { keepalive: true } ) synchronously inside the caller's gesture.
Concretely:
- Extract the shared body construction into a synchronous helper (it already is — see
buildAnalyticsRequestBody in analytics-helpers.js).
- Have
trackVideoEvent call fetch() directly with keepalive: true for type=2, instead of going through window.analytics.track(). The videoAnalyticsPlugin would then only handle non-critical events (or be removed entirely if heatmap is the only thing it serves).
- Or, expose a
window.GoDAM.sendHeatmapNow( player, video ) synchronous API the Reel Pops modal can call from beforeRestore — same as sendPlayerHeatmap does internally.
Either way the win is the same: fetch() is in the call stack of the user gesture, so the request is queued at the network layer before unload begins, and keepalive: true is honored.
Secondary cleanup (related, low priority)
collectPlayedRanges() (analytics.js:28-37) emits [[0, 0]] when videojs.player.played() reports a single zero-length range (happens when play() was called but no frames rendered). trackVideoEvent then proceeds because ranges.length === 1, not 0. Tightening the guard to ranges.every(([s, e]) => e > s) (or a min-duration threshold) would drop these phantom heatmap events at the source — and would also reduce the number of cancellable in-flight requests during quick navigation.
Environment
- Site: https://palak-godam.rt.gw
- Browser: Google Chrome 148.0.0.0 on macOS (per
config_token)
- Account:
cuunl3cre4
- Visitor: anonymous (
wp_user_id: 0)
- Reel Pops widget enabled (godam-for-woo).
Reported & investigated with help from Claude (via Claude Code).
Bug Report
Current Behavior
When the user triggers a type=2 (video heatmap) event and then immediately navigates to another page, the corresponding
POST /analytics/request is initiated in the browser but is cancelled by the browser as the page tears down — before it ever reaches the server. In DevTools → Network, these show up alongside normal{}JSON responses but with the blank-document icon (no response body / cancelled), e.g. the two highlighted rows in the screenshot below:The request payload is well-formed (and
keepalive: trueis set), but the request never lands. Reproducible on palak-godam.rt.gw by interacting with a Reel Pops modal and clicking a nav link before the modal fully closes / before the next tick of the analytics async pipeline.Sample payload of a cancelled request (from the Reel Pops release page):
{ "site_url": "https://palak-godam.rt.gw", "user_token": "83c1aa4a-7b06-4bef-b087-f83b463c1c03", "wp_user_id": 0, "account_token": "cuunl3cre4", "visitor_timestamp": 1779795970929, "visit_entry_action_url": "https://palak-godam.rt.gw/reel-pops-release/", "visit_entry_action_name": "Reel Pops release – palak-godam.rt.gw", "referer_url": "https://palak-godam.rt.gw/2026/05/15/shoppable-video-godam-video-product-gallery-block/", "config_resolution": "799x836", "config_os": "Macintosh", "config_browser_name": "Google Chrome", "is_page": true, "post_id": 462, "post_title": "Reel Pops release", "type": 2, "video_id": 457, "ranges": [[0, 0]], "video_length": 0, "job_id": "50uc96k012" }(Side note:
ranges: [[0, 0]]+video_length: 0is also worth tightening — a heatmap with zero-duration ranges and an unloaded duration is not a meaningful play event. See "Secondary cleanup" below. The primary bug is the request loss, not this payload shape.)Expected Behavior
For every
analytics.track('video_heatmap', …)call that survives the property-validation checks intrackVideoEvent(player exists, ranges non-empty, video id present), thePOST /analytics/request should reach the server — even if the user navigates away in the very next click.Root cause
The analytics library path is async all the way down:
window.analytics.track('video_heatmap', …)callsAnalyticsfrom theanalyticsnpm package (v0.8.18,@analytics/corev0.13.1).trackStartaction through its Redux store. Each middleware (lein@analytics/core) wraps the call inPromise.resolve(…).then(…)chains — meaning the call tovideoAnalyticsPlugin.trackhappens at least one microtask later.trackis itselfasync, so thefetch()call is at least one more microtask after that.fetch()get called, and only then doeskeepalive: truestart protecting the request.If the user clicks a link before step 4 runs, the browser begins unloading the page and the Promise chain is silently abandoned —
fetch()is never called, so there is no in-flight keepalive request to preserve.The plugin author was already aware of this exact failure mode and worked around it for the unload path. See the comment in
sendPlayerHeatmap():…and
sendPlayerHeatmapaccordingly bypasseswindow.analytics.track()to callfetch()directly frombeforeunload/pagehide/visibilitychange/dispose. But the same bypass is not applied totrackVideoEventcallers, even though those callers (notably the Reel Pops modal) are extremely likely to be followed by a navigation. Example caller —godam-for-woo/assets/src/js/reel-pops/reel-pops-frontend.js:338-346:If the user closes the modal (which triggers
beforeRestore) and clicks a nav link in the same gesture, the type=2 fetch never gets initiated.Steps to Reproduce
ana, enable Preserve log./analytics/rows with the blank-document icon (cancelled — no response body). Inspect their Timing → "Stalled" or "(canceled)" status.Possible Solution
The cleanest fix is to mirror the
sendPlayerHeatmapapproach for thetrackVideoEventpath: bypass the analytics library and issuefetch( … , { keepalive: true } )synchronously inside the caller's gesture.Concretely:
buildAnalyticsRequestBodyinanalytics-helpers.js).trackVideoEventcallfetch()directly withkeepalive: truefor type=2, instead of going throughwindow.analytics.track(). ThevideoAnalyticsPluginwould then only handle non-critical events (or be removed entirely if heatmap is the only thing it serves).window.GoDAM.sendHeatmapNow( player, video )synchronous API the Reel Pops modal can call frombeforeRestore— same assendPlayerHeatmapdoes internally.Either way the win is the same:
fetch()is in the call stack of the user gesture, so the request is queued at the network layer before unload begins, andkeepalive: trueis honored.Secondary cleanup (related, low priority)
collectPlayedRanges()(analytics.js:28-37) emits[[0, 0]]whenvideojs.player.played()reports a single zero-length range (happens whenplay()was called but no frames rendered).trackVideoEventthen proceeds becauseranges.length === 1, not 0. Tightening the guard toranges.every(([s, e]) => e > s)(or a min-duration threshold) would drop these phantom heatmap events at the source — and would also reduce the number of cancellable in-flight requests during quick navigation.Environment
config_token)cuunl3cre4wp_user_id: 0)