Skip to content

Commit 3576ef8

Browse files
committed
Update Fireperf logging to use sendBeacon only if the payload is under the 64KB limit for most browsers.
- For the flush, attempt to use sendBeacon with a low number of events incase sendBeacon is also used by other libraries.
1 parent 86155b3 commit 3576ef8

File tree

2 files changed

+220
-30
lines changed

2 files changed

+220
-30
lines changed

packages/performance/src/services/transport_service.test.ts

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import sinonChai from 'sinon-chai';
2121
import {
2222
transportHandler,
2323
setupTransportService,
24-
resetTransportService
24+
resetTransportService, flushQueuedEvents
2525
} from './transport_service';
2626
import { SettingsService } from './settings_service';
2727

@@ -88,7 +88,7 @@ describe('Firebase Performance > transport_service', () => {
8888
expect(fetchStub).to.not.have.been.called;
8989
});
9090

91-
it('sends up to the maximum event limit in one request', async () => {
91+
it('sends up to the maximum event limit in one request if payload is under 64 KB', async () => {
9292
// Arrange
9393
const setting = SettingsService.getInstance();
9494
const flTransportFullUrl =
@@ -134,6 +134,61 @@ describe('Firebase Performance > transport_service', () => {
134134
expect(fetchStub).to.not.have.been.called;
135135
});
136136

137+
it('sends fetch if payload is above 64 KB', async () => {
138+
// Arrange
139+
const setting = SettingsService.getInstance();
140+
const flTransportFullUrl =
141+
setting.flTransportEndpointUrl + '?key=' + setting.transportKey;
142+
fetchStub.resolves(
143+
new Response('{}', {
144+
status: 200,
145+
headers: { 'Content-type': 'application/json' }
146+
})
147+
);
148+
149+
const payload = 'a'.repeat(300);
150+
// Act
151+
// Generate 1020 events
152+
for (let i = 0; i < 1020; i++) {
153+
testTransportHandler(payload + i);
154+
}
155+
// Wait for first and second event dispatch to happen.
156+
clock.tick(INITIAL_SEND_TIME_DELAY_MS);
157+
// This is to resolve the floating promise chain in transport service.
158+
await Promise.resolve().then().then().then();
159+
clock.tick(DEFAULT_SEND_INTERVAL_MS);
160+
161+
// Assert
162+
// Expects the first logRequest which contains first 1000 events.
163+
const firstLogRequest = generateLogRequest('5501');
164+
for (let i = 0; i < MAX_EVENT_COUNT_PER_REQUEST; i++) {
165+
firstLogRequest['log_event'].push({
166+
'source_extension_json_proto3': payload + i,
167+
'event_time_ms': '1'
168+
});
169+
}
170+
expect(fetchStub).calledWith(
171+
flTransportFullUrl,
172+
{
173+
method: 'POST',
174+
body: JSON.stringify(firstLogRequest),
175+
}
176+
);
177+
// Expects the second logRequest which contains remaining 20 events;
178+
const secondLogRequest = generateLogRequest('15501');
179+
for (let i = 0; i < 20; i++) {
180+
secondLogRequest['log_event'].push({
181+
'source_extension_json_proto3':
182+
payload + (MAX_EVENT_COUNT_PER_REQUEST + i),
183+
'event_time_ms': '1'
184+
});
185+
}
186+
expect(sendBeaconStub).calledWith(
187+
flTransportFullUrl,
188+
JSON.stringify(secondLogRequest)
189+
);
190+
});
191+
137192
it('falls back to fetch if sendBeacon fails.', async () => {
138193
sendBeaconStub.returns(false);
139194
fetchStub.resolves(
@@ -147,6 +202,102 @@ describe('Firebase Performance > transport_service', () => {
147202
expect(fetchStub).to.have.been.calledOnce;
148203
});
149204

205+
it('flushes the queue with multiple sendBeacons in batches of 40', async () => {
206+
// Arrange
207+
const setting = SettingsService.getInstance();
208+
const flTransportFullUrl =
209+
setting.flTransportEndpointUrl + '?key=' + setting.transportKey;
210+
fetchStub.resolves(
211+
new Response('{}', {
212+
status: 200,
213+
headers: { 'Content-type': 'application/json' }
214+
})
215+
);
216+
217+
const payload = 'a'.repeat(300);
218+
// Act
219+
// Generate 80 events
220+
for (let i = 0; i < 80; i++) {
221+
testTransportHandler(payload + i);
222+
}
223+
224+
flushQueuedEvents();
225+
226+
// Assert
227+
const firstLogRequest = generateLogRequest('1');
228+
const secondLogRequest = generateLogRequest('1');
229+
for (let i = 0; i < 40; i++) {
230+
firstLogRequest['log_event'].push({
231+
'source_extension_json_proto3': payload + (i + 40),
232+
'event_time_ms': '1'
233+
});
234+
secondLogRequest['log_event'].push({
235+
'source_extension_json_proto3': payload + i,
236+
'event_time_ms': '1'
237+
});
238+
}
239+
expect(sendBeaconStub).calledWith(
240+
flTransportFullUrl,
241+
JSON.stringify(firstLogRequest)
242+
);
243+
expect(sendBeaconStub).calledWith(
244+
flTransportFullUrl,
245+
JSON.stringify(secondLogRequest)
246+
);
247+
expect(fetchStub).to.not.have.been.called;
248+
});
249+
250+
it('flushes the queue with fetch for sendBeacons that failed', async () => {
251+
// Arrange
252+
const setting = SettingsService.getInstance();
253+
const flTransportFullUrl =
254+
setting.flTransportEndpointUrl + '?key=' + setting.transportKey;
255+
fetchStub.resolves(
256+
new Response('{}', {
257+
status: 200,
258+
headers: { 'Content-type': 'application/json' }
259+
})
260+
);
261+
262+
const payload = 'a'.repeat(300);
263+
// Act
264+
// Generate 80 events
265+
for (let i = 0; i < 80; i++) {
266+
testTransportHandler(payload + i);
267+
}
268+
sendBeaconStub.onCall(0).returns(true);
269+
sendBeaconStub.onCall(1).returns(false);
270+
flushQueuedEvents();
271+
272+
273+
// Assert
274+
const firstLogRequest = generateLogRequest('1');
275+
const secondLogRequest = generateLogRequest('1');
276+
for (let i = 40; i < 80; i++) {
277+
firstLogRequest['log_event'].push({
278+
'source_extension_json_proto3': payload + i,
279+
'event_time_ms': '1'
280+
});
281+
}
282+
for (let i = 0; i < 40; i++) {
283+
secondLogRequest['log_event'].push({
284+
'source_extension_json_proto3': payload + i,
285+
'event_time_ms': '1'
286+
});
287+
}
288+
expect(sendBeaconStub).calledWith(
289+
flTransportFullUrl,
290+
JSON.stringify(firstLogRequest)
291+
);
292+
expect(fetchStub).calledWith(
293+
flTransportFullUrl,
294+
{
295+
method: 'POST',
296+
body: JSON.stringify(secondLogRequest),
297+
}
298+
);
299+
});
300+
150301
function generateLogRequest(requestTimeMs: string): any {
151302
return {
152303
'request_time_ms': requestTimeMs,

packages/performance/src/services/transport_service.ts

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ const INITIAL_SEND_TIME_DELAY_MS = 5.5 * 1000;
2424
const MAX_EVENT_COUNT_PER_REQUEST = 1000;
2525
const DEFAULT_REMAINING_TRIES = 3;
2626

27+
// Most browsers have a max payload of 64KB for sendbeacon/keep alive payload.
28+
const MAX_SEND_BEACON_PAYLOAD_SIZE = 65536;
29+
// The max number of events to send during a flush. This number is kept low to since Chrome has a
30+
// shared payload limit for all sendBeacon calls in the same nav context.
31+
const MAX_FLUSH_SIZE = 40;
32+
33+
const TEXT_ENCODER = new TextEncoder();
34+
2735
let remainingTries = DEFAULT_REMAINING_TRIES;
2836

2937
interface BatchEvent {
@@ -90,14 +98,31 @@ function dispatchQueueEvents(): void {
9098
// for next attempt.
9199
const staged = queue.splice(0, MAX_EVENT_COUNT_PER_REQUEST);
92100

101+
const data = buildPayload(staged);
102+
103+
postToFlEndpoint(data)
104+
.then(() => {
105+
remainingTries = DEFAULT_REMAINING_TRIES;
106+
})
107+
.catch(() => {
108+
// If the request fails for some reason, add the events that were attempted
109+
// back to the primary queue to retry later.
110+
queue = [...staged, ...queue];
111+
remainingTries--;
112+
consoleLogger.info(`Tries left: ${remainingTries}.`);
113+
processQueue(DEFAULT_SEND_INTERVAL_MS);
114+
});
115+
}
116+
117+
function buildPayload(events: BatchEvent[]): string {
93118
/* eslint-disable camelcase */
94119
// We will pass the JSON serialized event to the backend.
95-
const log_event: Log[] = staged.map(evt => ({
120+
const log_event: Log[] = events.map(evt => ({
96121
source_extension_json_proto3: evt.message,
97122
event_time_ms: String(evt.eventTime)
98123
}));
99124

100-
const data: TransportBatchLogFormat = {
125+
const transportBatchLog: TransportBatchLogFormat = {
101126
request_time_ms: String(Date.now()),
102127
client_info: {
103128
client_type: 1, // 1 is JS
@@ -108,32 +133,24 @@ function dispatchQueueEvents(): void {
108133
};
109134
/* eslint-enable camelcase */
110135

111-
postToFlEndpoint(data)
112-
.then(() => {
113-
remainingTries = DEFAULT_REMAINING_TRIES;
114-
})
115-
.catch(() => {
116-
// If the request fails for some reason, add the events that were attempted
117-
// back to the primary queue to retry later.
118-
queue = [...staged, ...queue];
119-
remainingTries--;
120-
consoleLogger.info(`Tries left: ${remainingTries}.`);
121-
processQueue(DEFAULT_SEND_INTERVAL_MS);
122-
});
136+
return JSON.stringify(transportBatchLog);
123137
}
124138

125-
function postToFlEndpoint(data: TransportBatchLogFormat): Promise<void> {
139+
/** Sends to Firelog. Atempts to use sendBeacon otherwsise uses fetch. */
140+
function postToFlEndpoint(body: string): Promise<void | Response> {
126141
const flTransportFullUrl =
127142
SettingsService.getInstance().getFlTransportFullUrl();
128-
const body = JSON.stringify(data);
129-
130-
return navigator.sendBeacon && navigator.sendBeacon(flTransportFullUrl, body)
131-
? Promise.resolve()
132-
: fetch(flTransportFullUrl, {
133-
method: 'POST',
134-
body,
135-
keepalive: true
136-
}).then();
143+
const size = TEXT_ENCODER.encode(body).length;
144+
145+
if (size <= MAX_SEND_BEACON_PAYLOAD_SIZE && navigator.sendBeacon &&
146+
navigator.sendBeacon(flTransportFullUrl, body)) {
147+
return Promise.resolve();
148+
} else {
149+
return fetch(flTransportFullUrl, {
150+
method: 'POST',
151+
body,
152+
});
153+
}
137154
}
138155

139156
function addToQueue(evt: BatchEvent): void {
@@ -153,17 +170,39 @@ export function transportHandler(
153170
const message = serializer(...args);
154171
addToQueue({
155172
message,
156-
eventTime: Date.now()
173+
eventTime: Date.now(),
157174
});
158175
};
159176
}
160177

161178
/**
162-
* Force flush the queued events. Useful at page unload time to ensure all
163-
* events are uploaded.
179+
* Force flush the queued events. Useful at page unload time to ensure all events are uploaded.
180+
* Flush will attempt to use sendBeacon to send events async and defaults back to fetch as soon as a
181+
* sendBeacon fails. Firefox
164182
*/
165183
export function flushQueuedEvents(): void {
184+
const flTransportFullUrl =
185+
SettingsService.getInstance().getFlTransportFullUrl();
186+
166187
while (queue.length > 0) {
167-
dispatchQueueEvents();
188+
// Send the last events first to prioritize page load traces
189+
const staged = queue.splice(-MAX_FLUSH_SIZE);
190+
const body = buildPayload(staged);
191+
192+
if (navigator.sendBeacon && navigator.sendBeacon(flTransportFullUrl, body)) {
193+
continue;
194+
} else {
195+
queue = [...queue, ...staged];
196+
break;
197+
}
198+
}
199+
if (queue.length > 0) {
200+
const body = buildPayload(queue);
201+
fetch(flTransportFullUrl, {
202+
method: 'POST',
203+
body,
204+
}).catch(() => {
205+
consoleLogger.info(`Failed flushing queued events.`);
206+
});
168207
}
169208
}

0 commit comments

Comments
 (0)