Skip to content

Commit

Permalink
Provide option not to retry on event send failure (close #1248)
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-el committed Oct 10, 2023
1 parent de97c3b commit 01a6d80
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 14 deletions.
3 changes: 2 additions & 1 deletion libraries/browser-tracker-core/src/tracker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
23 changes: 17 additions & 6 deletions libraries/browser-tracker-core/src/tracker/out_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface OutQueue {
* @param retryStatusCodes – Failure HTTP response status codes from Collector for which sending events should be retried (they can override the `dontRetryStatusCodes`)
* @param dontRetryStatusCodes – Failure HTTP response status codes from Collector for which sending events should not be retried
* @param idService - Id service full URL. This URL will be added to the queue and will be called using a GET method.
* @param retryFailures - Whether to retry failed requests
* @returns object OutQueueManager instance
*/
export function OutQueueManager(
Expand All @@ -83,7 +84,8 @@ export function OutQueueManager(
withCredentials: boolean,
retryStatusCodes: number[],
dontRetryStatusCodes: number[],
idService?: string
idService?: string,
retryFailures: boolean = true
): OutQueue {
type PostEvent = {
evt: Record<string, unknown>;
Expand Down Expand Up @@ -349,10 +351,7 @@ export function OutQueueManager(
}

// Time out POST requests after connectionTimeout
const xhrTimeout = setTimeout(function () {
xhr.abort();
executingQueue = false;
}, connectionTimeout);
xhr.timeout = connectionTimeout;

const removeEventsFromQueue = (numberToSend: number): void => {
for (let deleteCount = 0; deleteCount < numberToSend; deleteCount++) {
Expand All @@ -370,9 +369,21 @@ export function OutQueueManager(
executeQueue();
};

const failureHandler = (e: ProgressEvent<EventTarget>) => {
// `e.type` will be "timeout", "abort", or "error"
if (!retryFailures) {
LOG.error(`Failed to send events due to: ${e.type}, retry failed is disabled.`);
removeEventsFromQueue(numberToSend);
executingQueue = false;
}
};

xhr.onerror = failureHandler;
xhr.ontimeout = failureHandler;
xhr.onabort = failureHandler;

xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status >= 200) {
clearTimeout(xhrTimeout);
if (xhr.status < 300) {
onPostSuccess(numberToSend);
} else {
Expand Down
12 changes: 12 additions & 0 deletions libraries/browser-tracker-core/src/tracker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
144 changes: 137 additions & 7 deletions libraries/browser-tracker-core/test/out_queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,30 @@
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;

var xhrMock: Partial<XMLHttpRequest>;
var xhrMock: XMLHttpRequest;
var xhrOpenMock: jest.Mock;
beforeEach(() => {
localStorage.clear();

xhrOpenMock = jest.fn();
xhrMock = {
...new XMLHttpRequest(),
open: xhrOpenMock,
send: jest.fn(),
setRequestHeader: jest.fn(),
withCredentials: true,
};

jest.spyOn(window, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest);
jest.spyOn(window, 'XMLHttpRequest').mockImplementation(() => xhrMock);
});

const respondMockRequest = (status: number) => {
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -337,4 +339,132 @@ describe('OutQueueManager', () => {
});
});
});

describe('retry failed events when 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 retry after abort', () => {
let outQueue = createOutQueue();
outQueue.enqueueRequest(request, 'http://example.com');

let retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);

xhrMock.onabort?.(new ProgressEvent('abort'));

retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);
});

it('should retry after timeout', () => {
let outQueue = createOutQueue();
outQueue.enqueueRequest(request, 'http://example.com');

let retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);

xhrMock.ontimeout?.(new ProgressEvent('timeout'));

retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);
});

it('should retry after error', () => {
let outQueue = createOutQueue();
outQueue.enqueueRequest(request, 'http://example.com');

let retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);

xhrMock.onerror?.(new ProgressEvent('error'));

retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);
});
});

describe('not retry failed events when 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,
10,
false,
{},
true,
[],
[],
'',
false
);

it('should not retry after abort', () => {
let outQueue = createOutQueue();
outQueue.enqueueRequest(request, 'http://example.com');

let retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);

xhrMock.onabort?.(new ProgressEvent('abort'));

retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(0);
});

it('should not retry after timeout', () => {
let outQueue = createOutQueue();
outQueue.enqueueRequest(request, 'http://example.com');

let retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);

xhrMock.ontimeout?.(new ProgressEvent('timeout'));

retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(0);
});

it('should not retry after error', () => {
let outQueue = createOutQueue();
outQueue.enqueueRequest(request, 'http://example.com');

let retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(1);

xhrMock.onerror?.(new ProgressEvent('error'));

retrievedQueue = readPostQueue();
expect(retrievedQueue).toHaveLength(0);
});
});
});

0 comments on commit 01a6d80

Please sign in to comment.