Skip to content

Commit 449fd18

Browse files
authored
Cache heartbeats one per date (#5945)
1 parent 7fffc57 commit 449fd18

File tree

3 files changed

+246
-55
lines changed

3 files changed

+246
-55
lines changed

packages/app/src/heartbeatService.test.ts

Lines changed: 123 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717

1818
import { expect } from 'chai';
1919
import '../test/setup';
20-
import { HeartbeatServiceImpl } from './heartbeatService';
20+
import {
21+
countBytes,
22+
HeartbeatServiceImpl,
23+
extractHeartbeatsForHeader
24+
} from './heartbeatService';
2125
import {
2226
Component,
2327
ComponentType,
@@ -28,7 +32,7 @@ import { FirebaseApp } from './public-types';
2832
import * as firebaseUtil from '@firebase/util';
2933
import { SinonStub, stub, useFakeTimers } from 'sinon';
3034
import * as indexedDb from './indexeddb';
31-
import { isIndexedDBAvailable } from '@firebase/util';
35+
import { base64Encode, isIndexedDBAvailable } from '@firebase/util';
3236

3337
declare module '@firebase/component' {
3438
interface NameServiceMapping {
@@ -39,6 +43,24 @@ declare module '@firebase/component' {
3943
const USER_AGENT_STRING_1 = 'vs1/1.2.3 vs2/2.3.4';
4044
const USER_AGENT_STRING_2 = 'different/1.2.3';
4145

46+
function generateUserAgentString(pairs: number): string {
47+
let uaString = '';
48+
for (let i = 0; i < pairs; i++) {
49+
uaString += `test-platform/${i % 10}.${i % 10}.${i % 10}`;
50+
}
51+
return uaString;
52+
}
53+
54+
function generateDates(count: number): string[] {
55+
let currentTimestamp = Date.now();
56+
const dates = [];
57+
for (let i = 0; i < count; i++) {
58+
dates.push(new Date(currentTimestamp).toISOString().slice(0, 10));
59+
currentTimestamp += 24 * 60 * 60 * 1000;
60+
}
61+
return dates;
62+
}
63+
4264
describe('HeartbeatServiceImpl', () => {
4365
describe('If IndexedDB has no entries', () => {
4466
let heartbeatService: HeartbeatServiceImpl;
@@ -80,29 +102,32 @@ describe('HeartbeatServiceImpl', () => {
80102
expect(heartbeatService._heartbeatsCache?.length).to.equal(1);
81103
const heartbeat1 = heartbeatService._heartbeatsCache?.[0];
82104
expect(heartbeat1?.userAgent).to.equal(USER_AGENT_STRING_1);
83-
expect(heartbeat1?.dates[0]).to.equal('1970-01-01');
105+
expect(heartbeat1?.date).to.equal('1970-01-01');
84106
expect(writeStub).to.be.calledWith([heartbeat1]);
85107
});
86108
it(`triggerHeartbeat() doesn't store another heartbeat on the same day`, async () => {
109+
expect(heartbeatService._heartbeatsCache?.length).to.equal(1);
87110
await heartbeatService.triggerHeartbeat();
88-
const heartbeat1 = heartbeatService._heartbeatsCache?.[0];
89-
expect(heartbeat1?.dates.length).to.equal(1);
111+
expect(heartbeatService._heartbeatsCache?.length).to.equal(1);
90112
});
91113
it(`triggerHeartbeat() does store another heartbeat on a different day`, async () => {
114+
expect(heartbeatService._heartbeatsCache?.length).to.equal(1);
92115
clock.tick(24 * 60 * 60 * 1000);
93116
await heartbeatService.triggerHeartbeat();
94-
const heartbeat1 = heartbeatService._heartbeatsCache?.[0];
95-
expect(heartbeat1?.dates.length).to.equal(2);
96-
expect(heartbeat1?.dates[1]).to.equal('1970-01-02');
117+
expect(heartbeatService._heartbeatsCache?.length).to.equal(2);
118+
expect(heartbeatService._heartbeatsCache?.[1].date).to.equal(
119+
'1970-01-02'
120+
);
97121
});
98122
it(`triggerHeartbeat() stores another entry for a different user agent`, async () => {
99123
userAgentString = USER_AGENT_STRING_2;
124+
expect(heartbeatService._heartbeatsCache?.length).to.equal(2);
100125
clock.tick(2 * 24 * 60 * 60 * 1000);
101126
await heartbeatService.triggerHeartbeat();
102-
expect(heartbeatService._heartbeatsCache?.length).to.equal(2);
103-
const heartbeat2 = heartbeatService._heartbeatsCache?.[1];
104-
expect(heartbeat2?.dates.length).to.equal(1);
105-
expect(heartbeat2?.dates[0]).to.equal('1970-01-03');
127+
expect(heartbeatService._heartbeatsCache?.length).to.equal(3);
128+
expect(heartbeatService._heartbeatsCache?.[2].date).to.equal(
129+
'1970-01-03'
130+
);
106131
});
107132
it('getHeartbeatHeaders() gets stored heartbeats and clears heartbeats', async () => {
108133
const deleteStub = stub(heartbeatService._storage, 'deleteAll');
@@ -127,9 +152,14 @@ describe('HeartbeatServiceImpl', () => {
127152
let writeStub: SinonStub;
128153
let userAgentString = USER_AGENT_STRING_1;
129154
const mockIndexedDBHeartbeats = [
155+
// Chosen so one will exceed 30 day limit and one will not.
130156
{
131157
userAgent: 'old-user-agent',
132-
dates: ['1969-01-01', '1969-01-02']
158+
date: '1969-12-01'
159+
},
160+
{
161+
userAgent: 'old-user-agent',
162+
date: '1969-12-31'
133163
}
134164
];
135165
before(() => {
@@ -179,18 +209,19 @@ describe('HeartbeatServiceImpl', () => {
179209
expect(heartbeatService._heartbeatsCache).to.deep.equal([]);
180210
}
181211
});
182-
it(`triggerHeartbeat() writes new heartbeats without removing old ones`, async () => {
212+
it(`triggerHeartbeat() writes new heartbeats and retains old ones newer than 30 days`, async () => {
183213
userAgentString = USER_AGENT_STRING_2;
184214
clock.tick(3 * 24 * 60 * 60 * 1000);
185215
await heartbeatService.triggerHeartbeat();
186216
if (isIndexedDBAvailable()) {
187217
expect(writeStub).to.be.calledWith([
188-
...mockIndexedDBHeartbeats,
189-
{ userAgent: USER_AGENT_STRING_2, dates: ['1970-01-04'] }
218+
// The first entry exceeds the 30 day retention limit.
219+
mockIndexedDBHeartbeats[1],
220+
{ userAgent: USER_AGENT_STRING_2, date: '1970-01-04' }
190221
]);
191222
} else {
192223
expect(writeStub).to.be.calledWith([
193-
{ userAgent: USER_AGENT_STRING_2, dates: ['1970-01-04'] }
224+
{ userAgent: USER_AGENT_STRING_2, date: '1970-01-04' }
194225
]);
195226
}
196227
});
@@ -201,8 +232,7 @@ describe('HeartbeatServiceImpl', () => {
201232
);
202233
if (isIndexedDBAvailable()) {
203234
expect(heartbeatHeaders).to.include('old-user-agent');
204-
expect(heartbeatHeaders).to.include('1969-01-01');
205-
expect(heartbeatHeaders).to.include('1969-01-02');
235+
expect(heartbeatHeaders).to.include('1969-12-31');
206236
}
207237
expect(heartbeatHeaders).to.include(USER_AGENT_STRING_2);
208238
expect(heartbeatHeaders).to.include('1970-01-04');
@@ -213,4 +243,78 @@ describe('HeartbeatServiceImpl', () => {
213243
expect(deleteStub).to.be.called;
214244
});
215245
});
246+
247+
describe('countBytes()', () => {
248+
it('counts how many bytes there will be in a stringified, encoded header', () => {
249+
const heartbeats = [
250+
{ userAgent: generateUserAgentString(1), dates: generateDates(1) },
251+
{ userAgent: generateUserAgentString(3), dates: generateDates(2) }
252+
];
253+
let size: number = 0;
254+
const headerString = base64Encode(
255+
JSON.stringify({ version: 2, heartbeats })
256+
);
257+
// Use independent methods to validate our byte count method matches.
258+
// We don't use this measurement method in the app because user
259+
// environments are much more unpredictable while we know the
260+
// tests will run in either a standard headless browser or Node.
261+
if (typeof Blob !== 'undefined') {
262+
const blob = new Blob([headerString]);
263+
size = blob.size;
264+
} else if (typeof Buffer !== 'undefined') {
265+
const buffer = Buffer.from(headerString);
266+
size = buffer.byteLength;
267+
}
268+
expect(countBytes(heartbeats)).to.equal(size);
269+
});
270+
});
271+
272+
describe('_extractHeartbeatsForHeader()', () => {
273+
it('returns empty heartbeatsToKeep if it cannot get under maxSize', () => {
274+
const heartbeats = [
275+
{ userAgent: generateUserAgentString(1), date: '2022-01-01' }
276+
];
277+
const { unsentEntries, heartbeatsToSend } = extractHeartbeatsForHeader(
278+
heartbeats,
279+
5
280+
);
281+
expect(heartbeatsToSend.length).to.equal(0);
282+
expect(unsentEntries).to.deep.equal(heartbeats);
283+
});
284+
it('splits heartbeats array', () => {
285+
const heartbeats = [
286+
{ userAgent: generateUserAgentString(20), date: '2022-01-01' },
287+
{ userAgent: generateUserAgentString(4), date: '2022-01-02' }
288+
];
289+
const sizeWithHeartbeat0Only = countBytes([
290+
{ userAgent: heartbeats[0].userAgent, dates: [heartbeats[0].date] }
291+
]);
292+
const { unsentEntries, heartbeatsToSend } = extractHeartbeatsForHeader(
293+
heartbeats,
294+
sizeWithHeartbeat0Only + 1
295+
);
296+
expect(heartbeatsToSend.length).to.equal(1);
297+
expect(unsentEntries.length).to.equal(1);
298+
});
299+
it('splits the first heartbeat if needed', () => {
300+
const uaString = generateUserAgentString(20);
301+
const heartbeats = [
302+
{ userAgent: uaString, date: '2022-01-01' },
303+
{ userAgent: uaString, date: '2022-01-02' },
304+
{ userAgent: uaString, date: '2022-01-03' }
305+
];
306+
const sizeWithHeartbeat0Only = countBytes([
307+
{ userAgent: heartbeats[0].userAgent, dates: [heartbeats[0].date] }
308+
]);
309+
const { unsentEntries, heartbeatsToSend } = extractHeartbeatsForHeader(
310+
heartbeats,
311+
sizeWithHeartbeat0Only + 1
312+
);
313+
expect(heartbeatsToSend.length).to.equal(1);
314+
expect(unsentEntries.length).to.equal(2);
315+
expect(heartbeatsToSend[0].dates.length + unsentEntries.length).to.equal(
316+
heartbeats.length
317+
);
318+
});
319+
});
216320
});

0 commit comments

Comments
 (0)