Skip to content

Commit 4a680d1

Browse files
authored
Merge 74db5f8 into 86155b3
2 parents 86155b3 + 74db5f8 commit 4a680d1

File tree

3 files changed

+224
-29
lines changed

3 files changed

+224
-29
lines changed

.changeset/nervous-needles-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/performance': patch
3+
---
4+
5+
Fix bug where events are not sent if they exceed sendBeacon payload limit

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

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

@@ -88,7 +89,7 @@ describe('Firebase Performance > transport_service', () => {
8889
expect(fetchStub).to.not.have.been.called;
8990
});
9091

91-
it('sends up to the maximum event limit in one request', async () => {
92+
it('sends up to the maximum event limit in one request if payload is under 64 KB', async () => {
9293
// Arrange
9394
const setting = SettingsService.getInstance();
9495
const flTransportFullUrl =
@@ -134,6 +135,58 @@ describe('Firebase Performance > transport_service', () => {
134135
expect(fetchStub).to.not.have.been.called;
135136
});
136137

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

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

packages/performance/src/services/transport_service.ts

Lines changed: 72 additions & 27 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,27 @@ 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 (
146+
size <= MAX_SEND_BEACON_PAYLOAD_SIZE &&
147+
navigator.sendBeacon &&
148+
navigator.sendBeacon(flTransportFullUrl, body)
149+
) {
150+
return Promise.resolve();
151+
} else {
152+
return fetch(flTransportFullUrl, {
153+
method: 'POST',
154+
body
155+
});
156+
}
137157
}
138158

139159
function addToQueue(evt: BatchEvent): void {
@@ -159,11 +179,36 @@ export function transportHandler(
159179
}
160180

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

0 commit comments

Comments
 (0)