diff --git a/core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.spec.ts b/core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.spec.ts index 218a10c33da7..e6f00e0ec849 100644 --- a/core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.spec.ts +++ b/core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.spec.ts @@ -800,14 +800,17 @@ describe('createAnalyticsQueue', () => { queue.enqueue(mockEvent, mockContext); + // Clear previous calls from initialize/enqueue + sessionStorageRemoveItem.mockClear(); + // Simulate normal flush (keepalive=false) sendBatchCallback([mockEvent], []); - // Should clear storage after successful send - expect(sessionStorageRemoveItem).toHaveBeenCalled(); + // Should clear storage after dispatching send + expect(sessionStorageRemoveItem).toHaveBeenCalledTimes(1); }); - it('should NOT clear storage on keepalive flush', () => { + it('should clear storage on keepalive flush to prevent duplicate sends', () => { const queue = createAnalyticsQueue(mockConfig); queue.initialize(); @@ -829,8 +832,10 @@ describe('createAnalyticsQueue', () => { // Simulate flush with keepalive sendBatchCallback([mockEvent], []); - // Should NOT clear storage (keepalive flush leaves backup) - expect(sessionStorageRemoveItem).not.toHaveBeenCalled(); + // Storage should be cleared even for keepalive flushes. + // sessionStorage writes are synchronous and complete before unload, + // so leaving stale events causes the next page to re-send them. + expect(sessionStorageRemoveItem).toHaveBeenCalledTimes(1); }); it('should handle corrupted storage gracefully', () => { diff --git a/core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.ts b/core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.ts index 4e1e9bc6a350..394dc49f2904 100644 --- a/core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.ts +++ b/core-web/libs/sdk/analytics/src/lib/core/shared/queue/dot-analytics.queue.utils.ts @@ -237,15 +237,17 @@ export const createAnalyticsQueue = (config: DotCMSAnalyticsConfig) => { (e) => !events.some((sent) => sent === e) ); - // Clear storage after normal flush (not keepalive) - // For keepalive flushes (page unload), keep events in storage as backup - if (!keepalive) { - if (eventsForPersistence.length === 0) { - clearStorage(); - } else { - // Update storage with remaining events - persistToStorage(); - } + // Always update storage after dispatching the send — even for keepalive flushes. + // Note: sendAnalyticsEvent is fire-and-forget (the returned promise is not awaited), + // so this runs regardless of whether the HTTP request succeeds. + // sessionStorage writes are synchronous and complete before page unload, + // so the persistence state stays consistent. Previously, keepalive flushes + // skipped the storage update, which caused the next page load to re-send + // the same events from persistence, producing duplicates. + if (eventsForPersistence.length === 0) { + clearStorage(); + } else { + persistToStorage(); } };