Skip to content

Commit 3d44792

Browse files
authored
Update Fireperf logging to use sendBeacon only if the payload is under the 64KB limit (#9120)
* 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. * Add changeset and fix format * Add additional comments * Put max flush size behind remote config flag
1 parent 86155b3 commit 3d44792

File tree

6 files changed

+243
-31
lines changed

6 files changed

+243
-31
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/remote_config_service.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ describe('Performance Monitoring > remote_config_service', () => {
4040
"fpr_log_endpoint_url":"https://firebaselogging.test.com",\
4141
"fpr_log_transport_key":"pseudo-transport-key",\
4242
"fpr_log_source":"2","fpr_vc_network_request_sampling_rate":"0.250000",\
43-
"fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000"},\
43+
"fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000",
44+
"fpr_log_max_flush_size":"10"},\
4445
"state":"UPDATE"}`;
4546
const PROJECT_ID = 'project1';
4647
const APP_ID = '1:23r:web:fewq';
@@ -80,6 +81,7 @@ describe('Performance Monitoring > remote_config_service', () => {
8081
settingsService.loggingEnabled = false;
8182
settingsService.networkRequestsSamplingRate = 1;
8283
settingsService.tracesSamplingRate = 1;
84+
settingsService.logMaxFlushSize = 40;
8385
}
8486

8587
// parameterized beforeEach. Should be called at beginning of each test.
@@ -150,6 +152,7 @@ describe('Performance Monitoring > remote_config_service', () => {
150152
expect(SettingsService.getInstance().tracesSamplingRate).to.equal(
151153
TRACE_SAMPLING_RATE
152154
);
155+
expect(SettingsService.getInstance().logMaxFlushSize).to.equal(10);
153156
});
154157

155158
it('does not call remote config if a valid config is in local storage', async () => {
@@ -190,6 +193,7 @@ describe('Performance Monitoring > remote_config_service', () => {
190193
expect(SettingsService.getInstance().tracesSamplingRate).to.equal(
191194
TRACE_SAMPLING_RATE
192195
);
196+
expect(SettingsService.getInstance().logMaxFlushSize).to.equal(10);
193197
});
194198

195199
it('does not change the default config if call to RC fails', async () => {
@@ -207,6 +211,7 @@ describe('Performance Monitoring > remote_config_service', () => {
207211
await getConfig(performanceController, IID);
208212

209213
expect(SettingsService.getInstance().loggingEnabled).to.equal(false);
214+
expect(SettingsService.getInstance().logMaxFlushSize).to.equal(40);
210215
});
211216

212217
it('uses secondary configs if the response does not have all the fields', async () => {

packages/performance/src/services/remote_config_service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface SecondaryConfig {
3838
transportKey?: string;
3939
tracesSamplingRate?: number;
4040
networkRequestsSamplingRate?: number;
41+
logMaxFlushSize?: number;
4142
}
4243

4344
// These values will be used if the remote config object is successfully
@@ -56,6 +57,7 @@ interface RemoteConfigTemplate {
5657
fpr_vc_network_request_sampling_rate?: string;
5758
fpr_vc_trace_sampling_rate?: string;
5859
fpr_vc_session_sampling_rate?: string;
60+
fpr_log_max_flush_size?: string;
5961
}
6062
/* eslint-enable camelcase */
6163

@@ -221,6 +223,14 @@ function processConfig(
221223
settingsServiceInstance.tracesSamplingRate =
222224
DEFAULT_CONFIGS.tracesSamplingRate;
223225
}
226+
227+
if (entries.fpr_log_max_flush_size) {
228+
settingsServiceInstance.logMaxFlushSize = Number(
229+
entries.fpr_log_max_flush_size
230+
);
231+
} else if (DEFAULT_CONFIGS.logMaxFlushSize) {
232+
settingsServiceInstance.logMaxFlushSize = DEFAULT_CONFIGS.logMaxFlushSize;
233+
}
224234
// Set the per session trace and network logging flags.
225235
settingsServiceInstance.logTraceAfterSampling = shouldLogAfterSampling(
226236
settingsServiceInstance.tracesSamplingRate

packages/performance/src/services/settings_service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export class SettingsService {
5454
// TTL of config retrieved from remote config in hours.
5555
configTimeToLive = 12;
5656

57+
// The max number of events to send during a flush. This number is kept low to since Chrome has a
58+
// shared payload limit for all sendBeacon calls in the same nav context.
59+
logMaxFlushSize = 40;
60+
5761
getFlTransportFullUrl(): string {
5862
return this.flTransportEndpointUrl.concat('?key=', this.transportKey);
5963
}

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

Lines changed: 149 additions & 3 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,14 +89,15 @@ 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 =
9596
setting.flTransportEndpointUrl + '?key=' + setting.transportKey;
9697

9798
// Act
98-
// Generate 1020 events, which should be dispatched in two batches (1000 events and 20 events).
99+
// Generate 1020 events with small payloads, which should be dispatched in two batches
100+
// (1000 events and 20 events).
99101
for (let i = 0; i < 1020; i++) {
100102
testTransportHandler('event' + i);
101103
}
@@ -134,6 +136,58 @@ describe('Firebase Performance > transport_service', () => {
134136
expect(fetchStub).to.not.have.been.called;
135137
});
136138

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

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

0 commit comments

Comments
 (0)