Skip to content

Commit 8ae28c8

Browse files
committed
Enable batching multiple events into a single POST request over buffer size if maxPostBytes setting is followed (#1356)
1 parent ffa44ff commit 8ae28c8

File tree

11 files changed

+59
-39
lines changed

11 files changed

+59
-39
lines changed

api-docs/docs/browser-tracker/markdown/browser-tracker.emitterconfigurationbase.maxpostbytes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
## EmitterConfigurationBase.maxPostBytes property
66

7-
The max size a POST request can be before the tracker will force send it
7+
The max size a POST request can be before the tracker will force send it Also dictates the max size of a POST request before a batch of events is split into multiple requests
88

99
<b>Signature:</b>
1010

api-docs/docs/browser-tracker/markdown/browser-tracker.emitterconfigurationbase.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface EmitterConfigurationBase
2525
| [idService?](./browser-tracker.emitterconfigurationbase.idservice.md) | string | <i>(Optional)</i> Id service full URL. This URL will be added to the queue and will be called using a GET method. This option is there to allow the service URL to be called in order to set any required identifiers e.g. extra cookies.<!-- -->The request respects the <code>anonymousTracking</code> option, including the SP-Anonymous header if needed, and any additional custom headers from the customHeaders option. |
2626
| [keepalive?](./browser-tracker.emitterconfigurationbase.keepalive.md) | boolean | <i>(Optional)</i> Indicates that the request should be allowed to outlive the webpage that initiated it. Enables collector requests to complete even if the page is closed or navigated away from. |
2727
| [maxGetBytes?](./browser-tracker.emitterconfigurationbase.maxgetbytes.md) | number | <i>(Optional)</i> The max size a GET request (its complete URL) can be. Requests over this size will be tried as a POST request. |
28-
| [maxPostBytes?](./browser-tracker.emitterconfigurationbase.maxpostbytes.md) | number | <i>(Optional)</i> The max size a POST request can be before the tracker will force send it |
28+
| [maxPostBytes?](./browser-tracker.emitterconfigurationbase.maxpostbytes.md) | number | <i>(Optional)</i> The max size a POST request can be before the tracker will force send it Also dictates the max size of a POST request before a batch of events is split into multiple requests |
2929
| [onRequestFailure?](./browser-tracker.emitterconfigurationbase.onrequestfailure.md) | (data: RequestFailure, response?: Response) =&gt; void | <i>(Optional)</i> A callback function to be executed whenever a request fails to be sent to the collector. This is the inverse of the onRequestSuccess callback, so any non 2xx status code will trigger this callback. |
3030
| [onRequestSuccess?](./browser-tracker.emitterconfigurationbase.onrequestsuccess.md) | (data: EventBatch, response: Response) =&gt; void | <i>(Optional)</i> A callback function to be executed whenever a request is successfully sent to the collector. In practice this means any request which returns a 2xx status code will trigger this callback. |
3131
| [postPath?](./browser-tracker.emitterconfigurationbase.postpath.md) | string | <i>(Optional)</i> The post path which events will be sent to. Ensure your collector is configured to accept events on this post path |

api-docs/docs/node-tracker/markdown/node-tracker.emitterconfigurationbase.maxpostbytes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
## EmitterConfigurationBase.maxPostBytes property
66

7-
The max size a POST request can be before the tracker will force send it
7+
The max size a POST request can be before the tracker will force send it Also dictates the max size of a POST request before a batch of events is split into multiple requests
88

99
<b>Signature:</b>
1010

api-docs/docs/node-tracker/markdown/node-tracker.emitterconfigurationbase.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface EmitterConfigurationBase
2525
| [idService?](./node-tracker.emitterconfigurationbase.idservice.md) | string | <i>(Optional)</i> Id service full URL. This URL will be added to the queue and will be called using a GET method. This option is there to allow the service URL to be called in order to set any required identifiers e.g. extra cookies.<!-- -->The request respects the <code>anonymousTracking</code> option, including the SP-Anonymous header if needed, and any additional custom headers from the customHeaders option. |
2626
| [keepalive?](./node-tracker.emitterconfigurationbase.keepalive.md) | boolean | <i>(Optional)</i> Indicates that the request should be allowed to outlive the webpage that initiated it. Enables collector requests to complete even if the page is closed or navigated away from. |
2727
| [maxGetBytes?](./node-tracker.emitterconfigurationbase.maxgetbytes.md) | number | <i>(Optional)</i> The max size a GET request (its complete URL) can be. Requests over this size will be tried as a POST request. |
28-
| [maxPostBytes?](./node-tracker.emitterconfigurationbase.maxpostbytes.md) | number | <i>(Optional)</i> The max size a POST request can be before the tracker will force send it |
28+
| [maxPostBytes?](./node-tracker.emitterconfigurationbase.maxpostbytes.md) | number | <i>(Optional)</i> The max size a POST request can be before the tracker will force send it Also dictates the max size of a POST request before a batch of events is split into multiple requests |
2929
| [onRequestFailure?](./node-tracker.emitterconfigurationbase.onrequestfailure.md) | (data: RequestFailure, response?: Response) =&gt; void | <i>(Optional)</i> A callback function to be executed whenever a request fails to be sent to the collector. This is the inverse of the onRequestSuccess callback, so any non 2xx status code will trigger this callback. |
3030
| [onRequestSuccess?](./node-tracker.emitterconfigurationbase.onrequestsuccess.md) | (data: EventBatch, response: Response) =&gt; void | <i>(Optional)</i> A callback function to be executed whenever a request is successfully sent to the collector. In practice this means any request which returns a 2xx status code will trigger this callback. |
3131
| [postPath?](./node-tracker.emitterconfigurationbase.postpath.md) | string | <i>(Optional)</i> The post path which events will be sent to. Ensure your collector is configured to accept events on this post path |

libraries/tracker-core/src/emitter/emitter_request.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ export interface EmitterRequest {
3333
*/
3434
countEvents: () => number;
3535
/**
36-
* Cancel the timeout timer, should be called when the request is sent
36+
* Cancel timeout timer if it is still pending.
37+
* If not successful, the request will be aborted.
38+
* @param successful - Whether the request was successful
39+
* @param reason - Reason for aborting the request
3740
*/
38-
cancelTimeoutTimer: () => void;
41+
closeRequest: (successful: boolean, reason?: string) => void;
3942
}
4043

4144
export interface EmitterRequestConfiguration {
@@ -48,7 +51,6 @@ export interface EmitterRequestConfiguration {
4851
keepalive?: boolean;
4952
postPath?: string;
5053
useStm?: boolean;
51-
bufferSize?: number;
5254
maxPostBytes?: number,
5355
credentials?: 'omit' | 'same-origin' | 'include';
5456
}
@@ -89,19 +91,23 @@ export function newEmitterRequest({
8991
keepalive = true,
9092
postPath = '/com.snowplowanalytics.snowplow/tp2',
9193
useStm = true,
92-
bufferSize,
9394
maxPostBytes = 40000,
9495
credentials = 'include',
9596
}: EmitterRequestConfiguration): EmitterRequest {
9697
let events: EmitterEvent[] = [];
9798
let usePost = eventMethod.toLowerCase() === 'post';
9899
let timer: ReturnType<typeof setTimeout> | undefined;
100+
let abortController: AbortController | undefined;
99101

100102
function countBytes(): number {
101-
return events.reduce(
103+
let count = events.reduce(
102104
(acc, event) => acc + (usePost ? event.getPOSTRequestBytesCount() : event.getGETRequestBytesCount()),
103105
0
104106
);
107+
if (usePost) {
108+
count += 88; // 88 bytes for the payload_data envelope
109+
}
110+
return count;
105111
}
106112

107113
function countEvents(): number {
@@ -112,7 +118,6 @@ export function newEmitterRequest({
112118
return events.length > 0 ? events[0].getServerAnonymization() : undefined;
113119
}
114120

115-
116121
function addEvent(event: EmitterEvent) {
117122
if (events.length > 0 && getServerAnonymizationOfExistingEvents() !== event.getServerAnonymization()) {
118123
return false;
@@ -128,9 +133,6 @@ export function newEmitterRequest({
128133

129134
function isFull(): boolean {
130135
if (usePost) {
131-
if (bufferSize !== undefined && countEvents() >= Math.max(1, bufferSize)) {
132-
return true;
133-
}
134136
return countBytes() >= maxPostBytes;
135137
} else {
136138
return events.length >= 1;
@@ -167,15 +169,19 @@ export function newEmitterRequest({
167169
}
168170

169171
function makeRequest(url: string, options: RequestInit): Request {
170-
const controller = new AbortController();
172+
closeRequest(false);
173+
174+
abortController = new AbortController();
171175
timer = setTimeout(() => {
172-
console.error('Request timed out');
173-
controller.abort()
176+
const reason = 'Request timed out';
177+
console.error(reason);
178+
timer = undefined;
179+
closeRequest(false, reason);
174180
}, connectionTimeout ?? 5000);
175181

176182
const requestOptions: RequestInit = {
177183
headers: createHeaders(),
178-
signal: controller.signal,
184+
signal: abortController.signal,
179185
keepalive,
180186
credentials,
181187
...options,
@@ -218,11 +224,19 @@ export function newEmitterRequest({
218224
}
219225
}
220226

221-
function cancelTimeoutTimer() {
222-
if (timer) {
227+
function closeRequest(successful: boolean, reason?: string) {
228+
if (timer !== undefined) {
223229
clearTimeout(timer);
224230
timer = undefined;
225231
}
232+
233+
if (abortController !== undefined) {
234+
const controller = abortController;
235+
abortController = undefined;
236+
if (!successful) {
237+
controller.abort(reason);
238+
}
239+
}
226240
}
227241

228242
return {
@@ -232,6 +246,6 @@ export function newEmitterRequest({
232246
countBytes,
233247
countEvents,
234248
isFull,
235-
cancelTimeoutTimer,
249+
closeRequest,
236250
};
237251
}

libraries/tracker-core/src/emitter/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface EmitterConfigurationBase {
4747
bufferSize?: number;
4848
/**
4949
* The max size a POST request can be before the tracker will force send it
50+
* Also dictates the max size of a POST request before a batch of events is split into multiple requests
5051
* @defaultValue 40000
5152
*/
5253
maxPostBytes?: number;
@@ -274,7 +275,7 @@ export function newEmitter({
274275
try {
275276
const response = await customFetch(fetchRequest);
276277

277-
request.cancelTimeoutTimer();
278+
request.closeRequest(true);
278279

279280
if (response.ok) {
280281
callOnRequestSuccess(payloads, response);
@@ -293,6 +294,8 @@ export function newEmitter({
293294
return { success: false, retry: willRetry, status: response.status };
294295
}
295296
} catch (e) {
297+
request.closeRequest(false);
298+
296299
const message = typeof e === 'string' ? e : e ? (e as Error).message : 'Unknown error';
297300
callOnRequestFailure({
298301
events: payloads,
@@ -312,7 +315,6 @@ export function newEmitter({
312315
customHeaders,
313316
connectionTimeout,
314317
keepalive,
315-
bufferSize,
316318
maxPostBytes,
317319
useStm,
318320
credentials,
@@ -324,7 +326,7 @@ export function newEmitter({
324326
LOG.warn('Event (' + bytes + 'B) too big, max is ' + maxBytes);
325327

326328
if (usePost) {
327-
const bytes = emitterEvent.getPOSTRequestBytesCount();
329+
const bytes = emitterEvent.getPOSTRequestBytesCount() + 88; // 88 bytes for the payload_data envelope
328330
const tooBig = bytes > maxPostBytes;
329331
if (tooBig) {
330332
eventTooBigWarning(bytes, maxPostBytes);

libraries/tracker-core/test/emitter/emitter_request.test.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { newEventStorePayload } from '../../src/event_store_payload';
66

77
const newEmitterEventFromPayload = (payload: Record<string, unknown>) => {
88
return newEmitterEvent(newEventStorePayload({ payload }));
9-
}
9+
};
1010

1111
// MARK: - addEvent
1212

@@ -71,7 +71,7 @@ test('countBytes returns the correct byte count', (t) => {
7171

7272
t.true(request.addEvent(newEmitterEventFromPayload({ e: 'pv', p: 'web' })));
7373
t.true(request.addEvent(newEmitterEventFromPayload({ e: 'pv', p: 'mob' })));
74-
t.is(request.countBytes(), 40);
74+
t.is(request.countBytes(), 40 + 88); // 40 bytes for each event, 88 bytes for the payload_data envelope
7575
});
7676

7777
// MARK: - countEvents
@@ -89,34 +89,31 @@ test('countEvents returns the correct event count', (t) => {
8989

9090
// MARK: - isFull
9191

92-
test('isFull returns false when not reached buffer size', (t) => {
92+
test('isFull returns false when not reached max post bytes', (t) => {
9393
const request = newEmitterRequest({
9494
endpoint: 'https://example.com',
9595
maxPostBytes: 1000,
96-
bufferSize: 2,
9796
});
9897

9998
t.true(request.addEvent(newEmitterEventFromPayload({ e: 'pv', p: 'web' })));
10099
t.false(request.isFull());
101100
});
102101

103-
test('isFull returns true when reached buffer size', (t) => {
102+
test('isFull returns false when reached buffer size and not max post bytes', (t) => {
104103
const request = newEmitterRequest({
105104
endpoint: 'https://example.com',
106105
maxPostBytes: 1000,
107-
bufferSize: 2,
108106
});
109107

110108
t.true(request.addEvent(newEmitterEventFromPayload({ e: 'pv', p: 'web' })));
111109
t.true(request.addEvent(newEmitterEventFromPayload({ e: 'pv', p: 'mob' })));
112-
t.true(request.isFull());
110+
t.false(request.isFull());
113111
});
114112

115113
test('isFull returns true when reached max post bytes', (t) => {
116114
const request = newEmitterRequest({
117115
endpoint: 'https://example.com',
118116
maxPostBytes: 10,
119-
bufferSize: 2,
120117
});
121118

122119
t.true(request.addEvent(newEmitterEventFromPayload({ e: 'pv', p: 'web' })));

libraries/tracker-core/test/emitter/index.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,9 @@ test('adds a timeout to the request', async (t) => {
311311

312312
return new Promise((resolve, reject) => {
313313
let timer = setTimeout(() => {
314+
t.fail('Request should have timed out');
314315
resolve(new Response(null, { status: 200 }));
315-
}, 1000);
316+
}, 500);
316317

317318
input.signal?.addEventListener('abort', () => {
318319
clearTimeout(timer);

plugins/browser-plugin-media-tracking/tests/test.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ describe('MediaTrackingPlugin', () => {
6262
},
6363
],
6464
contexts: { webPage: false },
65+
customFetch: async () => new Response(null, { status: 200 }),
6566
});
6667
id = `media-${idx}`;
6768
});

plugins/browser-plugin-media/test/api.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('Media Tracking API', () => {
5757
},
5858
],
5959
contexts: { webPage: false },
60+
customFetch: async () => new Response(null, { status: 200 }),
6061
});
6162
id = `media-${idx}`;
6263
});

0 commit comments

Comments
 (0)