diff --git a/libraries/browser-tracker-core/src/tracker/index.ts b/libraries/browser-tracker-core/src/tracker/index.ts index a6443a932..af536aa2a 100755 --- a/libraries/browser-tracker-core/src/tracker/index.ts +++ b/libraries/browser-tracker-core/src/tracker/index.ts @@ -303,7 +303,8 @@ export function Tracker( trackerConfiguration.withCredentials ?? true, trackerConfiguration.retryStatusCodes ?? [], (trackerConfiguration.dontRetryStatusCodes ?? []).concat([400, 401, 403, 410, 422]), - trackerConfiguration.idService + trackerConfiguration.idService, + trackerConfiguration.retryFailures ), // Whether pageViewId should be regenerated after each trackPageView. Affect web_page context preservePageViewId = false, diff --git a/libraries/browser-tracker-core/src/tracker/out_queue.ts b/libraries/browser-tracker-core/src/tracker/out_queue.ts index 92e3ad968..f5418261b 100644 --- a/libraries/browser-tracker-core/src/tracker/out_queue.ts +++ b/libraries/browser-tracker-core/src/tracker/out_queue.ts @@ -83,7 +83,8 @@ export function OutQueueManager( withCredentials: boolean, retryStatusCodes: number[], dontRetryStatusCodes: number[], - idService?: string + idService?: string, + retryFailures: boolean = true ): OutQueue { type PostEvent = { evt: Record; @@ -351,7 +352,14 @@ export function OutQueueManager( // Time out POST requests after connectionTimeout const xhrTimeout = setTimeout(function () { xhr.abort(); - executingQueue = false; + + if (retryFailures) { + LOG.warn(`Request failed, will retry.`); + } else { + LOG.error(`Request failed, will not retry.`); + removeEventsFromQueue(numberToSend); + executingQueue = false; + } }, connectionTimeout); const removeEventsFromQueue = (numberToSend: number): void => { @@ -371,17 +379,17 @@ export function OutQueueManager( }; xhr.onreadystatechange = function () { - if (xhr.readyState === 4 && xhr.status >= 200) { - clearTimeout(xhrTimeout); - if (xhr.status < 300) { + if (xhr.readyState === 4) { + if (xhr.status >= 200 && xhr.status < 300) { + clearTimeout(xhrTimeout); onPostSuccess(numberToSend); } else { if (!shouldRetryForStatusCode(xhr.status)) { LOG.error(`Status ${xhr.status}, will not retry.`); removeEventsFromQueue(numberToSend); } - executingQueue = false; } + executingQueue = false; } }; diff --git a/libraries/browser-tracker-core/src/tracker/types.ts b/libraries/browser-tracker-core/src/tracker/types.ts index 0f5801c9d..6bc42a3fe 100755 --- a/libraries/browser-tracker-core/src/tracker/types.ts +++ b/libraries/browser-tracker-core/src/tracker/types.ts @@ -249,6 +249,18 @@ export type TrackerConfiguration = { * The request respects the `anonymousTracking` option, including the SP-Anonymous header if needed, and any additional custom headers from the customHeaders option. */ idService?: string; + + /** + * Whether to retry failed requests to the collector. + * + * Failed requests are requests that failed due to + * [timeouts](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout_event), + * [network errors](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/error_event), + * and [abort events](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort_event). + * + * @defaultValue true + */ + retryFailures?: boolean; }; /** diff --git a/libraries/browser-tracker-core/test/out_queue.test.ts b/libraries/browser-tracker-core/test/out_queue.test.ts index bab53486b..98f8b6be2 100644 --- a/libraries/browser-tracker-core/test/out_queue.test.ts +++ b/libraries/browser-tracker-core/test/out_queue.test.ts @@ -31,6 +31,12 @@ import { OutQueueManager, OutQueue } from '../src/tracker/out_queue'; import { SharedState } from '../src/state'; +const readPostQueue = () => { + return JSON.parse( + window.localStorage.getItem('snowplowOutQueue_sp_post2') ?? fail('Unable to find local storage queue') + ); +}; + describe('OutQueueManager', () => { const maxQueueSize = 2; @@ -45,6 +51,7 @@ describe('OutQueueManager', () => { send: jest.fn(), setRequestHeader: jest.fn(), withCredentials: true, + abort: jest.fn(), }; jest.spyOn(window, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest); @@ -219,11 +226,6 @@ describe('OutQueueManager', () => { describe('idService requests', () => { const idServiceEndpoint = 'http://example.com/id'; - const readPostQueue = () => { - return JSON.parse( - window.localStorage.getItem('snowplowOutQueue_sp_post2') ?? fail('Unable to find local storage queue') - ); - }; const readGetQueue = () => JSON.parse(window.localStorage.getItem('snowplowOutQueue_sp_get') ?? fail('Unable to find local storage queue')); @@ -337,4 +339,86 @@ describe('OutQueueManager', () => { }); }); }); + + describe('retryFailures = true', () => { + const request = { e: 'pv', eid: '65cb78de-470c-4764-8c10-02bd79477a3a' }; + let createOutQueue = () => + OutQueueManager( + 'sp', + new SharedState(), + true, + 'post', + '/com.snowplowanalytics.snowplow/tp2', + 1, + 40000, + 0, + false, + maxQueueSize, + 10, + false, + {}, + true, + [], + [], + '', + true + ); + + it('should remain in queue on failure', (done) => { + let outQueue = createOutQueue(); + outQueue.enqueueRequest(request, 'http://example.com'); + + let retrievedQueue = readPostQueue(); + expect(retrievedQueue).toHaveLength(1); + + respondMockRequest(0); + + setTimeout(() => { + retrievedQueue = readPostQueue(); + expect(retrievedQueue).toHaveLength(1); + done(); + }, 20); + }); + }); + + describe('retryFailures = false', () => { + const request = { e: 'pv', eid: '65cb78de-470c-4764-8c10-02bd79477a3a' }; + let createOutQueue = () => + OutQueueManager( + 'sp', + new SharedState(), + true, + 'post', + '/com.snowplowanalytics.snowplow/tp2', + 1, + 40000, + 0, + false, + maxQueueSize, + 0, + false, + {}, + true, + [], + [], + '', + false + ); + + it('should remove from queue on failure', (done) => { + let outQueue = createOutQueue(); + outQueue.enqueueRequest(request, 'http://example.com'); + + let retrievedQueue = readPostQueue(); + expect(retrievedQueue).toHaveLength(1); + + respondMockRequest(0); + + setTimeout(() => { + retrievedQueue = readPostQueue(); + expect(retrievedQueue).toHaveLength(0); + done(); + }, 20); + }); + }); });