Skip to content

Commit 3bf0319

Browse files
authored
fix(analytics): update storage handling during keepalive flush to prevent duplicate event sends (#34877)
PR Description: ## Summary Fixes duplicate analytics event sends caused by keepalive flushes (page unload) skipping the storage update. The stale events left in `sessionStorage` were re-sent on the next page load. ## Changes Made ### Frontend (SDK) - **`dot-analytics.queue.utils.ts`**: Removed the `if (!keepalive)` guard around storage cleanup after batch sends. Storage is now always updated after flushing — both for normal and keepalive flushes — since `sessionStorage` writes are synchronous and complete before page unload. - **`dot-analytics.queue.utils.spec.ts`**: Updated the keepalive flush test to assert that storage **is** cleared (previously asserted it was not), with comments explaining why. ## Technical Details The previous implementation treated keepalive flushes (triggered during `visibilitychange`/`beforeunload`) as a special case: it intentionally left sent events in `sessionStorage` as a "backup" in case the beacon request failed. However, because `sessionStorage.removeItem()` is synchronous and guaranteed to complete before the page unloads, this backup was unnecessary. Worse, it caused the next page load to pick up the stale persisted events and re-send them, resulting in duplicate analytics data. The fix removes the keepalive branch so that `clearStorage()` / `persistToStorage()` always runs after a successful batch send, regardless of the flush trigger. ## Breaking Changes None ## Testing - [x] Unit tests updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] E2E tests added/updated ## Related Issues Closes #34357 ## Additional Notes None
1 parent d0b89fa commit 3bf0319

2 files changed

Lines changed: 21 additions & 14 deletions

File tree

core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.spec.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -800,14 +800,17 @@ describe('createAnalyticsQueue', () => {
800800

801801
queue.enqueue(mockEvent, mockContext);
802802

803+
// Clear previous calls from initialize/enqueue
804+
sessionStorageRemoveItem.mockClear();
805+
803806
// Simulate normal flush (keepalive=false)
804807
sendBatchCallback([mockEvent], []);
805808

806-
// Should clear storage after successful send
807-
expect(sessionStorageRemoveItem).toHaveBeenCalled();
809+
// Should clear storage after dispatching send
810+
expect(sessionStorageRemoveItem).toHaveBeenCalledTimes(1);
808811
});
809812

810-
it('should NOT clear storage on keepalive flush', () => {
813+
it('should clear storage on keepalive flush to prevent duplicate sends', () => {
811814
const queue = createAnalyticsQueue(mockConfig);
812815
queue.initialize();
813816

@@ -829,8 +832,10 @@ describe('createAnalyticsQueue', () => {
829832
// Simulate flush with keepalive
830833
sendBatchCallback([mockEvent], []);
831834

832-
// Should NOT clear storage (keepalive flush leaves backup)
833-
expect(sessionStorageRemoveItem).not.toHaveBeenCalled();
835+
// Storage should be cleared even for keepalive flushes.
836+
// sessionStorage writes are synchronous and complete before unload,
837+
// so leaving stale events causes the next page to re-send them.
838+
expect(sessionStorageRemoveItem).toHaveBeenCalledTimes(1);
834839
});
835840

836841
it('should handle corrupted storage gracefully', () => {

core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,15 +237,17 @@ export const createAnalyticsQueue = (config: DotCMSAnalyticsConfig) => {
237237
(e) => !events.some((sent) => sent === e)
238238
);
239239

240-
// Clear storage after normal flush (not keepalive)
241-
// For keepalive flushes (page unload), keep events in storage as backup
242-
if (!keepalive) {
243-
if (eventsForPersistence.length === 0) {
244-
clearStorage();
245-
} else {
246-
// Update storage with remaining events
247-
persistToStorage();
248-
}
240+
// Always update storage after dispatching the send — even for keepalive flushes.
241+
// Note: sendAnalyticsEvent is fire-and-forget (the returned promise is not awaited),
242+
// so this runs regardless of whether the HTTP request succeeds.
243+
// sessionStorage writes are synchronous and complete before page unload,
244+
// so the persistence state stays consistent. Previously, keepalive flushes
245+
// skipped the storage update, which caused the next page load to re-send
246+
// the same events from persistence, producing duplicates.
247+
if (eventsForPersistence.length === 0) {
248+
clearStorage();
249+
} else {
250+
persistToStorage();
249251
}
250252
};
251253

0 commit comments

Comments
 (0)