Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Comment thread
oidacra marked this conversation as resolved.
const queue = createAnalyticsQueue(mockConfig);
queue.initialize();

Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
};

Expand Down
Loading