Skip to content

Commit 25ebd49

Browse files
Add support for sending session ID for telemetry (#9392)
* Add support for sending session ID for telemetry * Simplify test * Check for existence of crypto.randomUUID Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Format * Refactor and cleanup api test * Add test * Formatting --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent d24986a commit 25ebd49

File tree

4 files changed

+160
-13
lines changed

4 files changed

+160
-13
lines changed

packages/telemetry/src/api.test.ts

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@ import {
3535
import { Component, ComponentType } from '@firebase/component';
3636
import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types';
3737
import { captureError, flush, getTelemetry } from './api';
38+
import {
39+
LOG_ENTRY_ATTRIBUTE_KEYS,
40+
TELEMETRY_SESSION_ID_KEY
41+
} from './constants';
3842
import { TelemetryService } from './service';
3943
import { registerTelemetry } from './register';
4044
import { _FirebaseInstallationsInternal } from '@firebase/installations';
4145

4246
const PROJECT_ID = 'my-project';
4347
const APP_ID = 'my-appid';
4448
const API_KEY = 'my-api-key';
49+
const MOCK_SESSION_ID = '00000000-0000-0000-0000-000000000000';
4550

4651
const emittedLogs: LogRecord[] = [];
4752

@@ -74,15 +79,51 @@ const fakeTelemetry: Telemetry = {
7479

7580
describe('Top level API', () => {
7681
let app: FirebaseApp;
82+
let originalSessionStorage: Storage | undefined;
83+
let originalCrypto: Crypto | undefined;
84+
let storage: Record<string, string> = {};
7785

7886
beforeEach(() => {
7987
// Clear the logs before each test.
8088
emittedLogs.length = 0;
8189
app = getFakeApp();
90+
storage = {};
91+
92+
// @ts-ignore
93+
originalSessionStorage = global.sessionStorage;
94+
// @ts-ignore
95+
originalCrypto = global.crypto;
96+
97+
const sessionStorageMock: Partial<Storage> = {
98+
getItem: (key: string) => storage[key] || null,
99+
setItem: (key: string, value: string) => {
100+
storage[key] = value;
101+
}
102+
};
103+
const cryptoMock: Partial<Crypto> = {
104+
randomUUID: () => MOCK_SESSION_ID
105+
};
106+
107+
Object.defineProperty(global, 'sessionStorage', {
108+
value: sessionStorageMock,
109+
writable: true
110+
});
111+
Object.defineProperty(global, 'crypto', {
112+
value: cryptoMock,
113+
writable: true
114+
});
82115
});
83116

84117
afterEach(async () => {
85118
await deleteApp(app);
119+
Object.defineProperty(global, 'sessionStorage', {
120+
value: originalSessionStorage,
121+
writable: true
122+
});
123+
Object.defineProperty(global, 'crypto', {
124+
value: originalCrypto,
125+
writable: true
126+
});
86127
});
87128

88129
describe('getTelemetry()', () => {
@@ -127,7 +168,8 @@ describe('Top level API', () => {
127168
expect(log.attributes).to.deep.equal({
128169
'error.type': 'TestError',
129170
'error.stack': '...stack trace...',
130-
'app.version': 'unset'
171+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
172+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID
131173
});
132174
});
133175

@@ -144,7 +186,8 @@ describe('Top level API', () => {
144186
expect(log.attributes).to.deep.equal({
145187
'error.type': 'Error',
146188
'error.stack': 'No stack trace available',
147-
'app.version': 'unset'
189+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
190+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID
148191
});
149192
});
150193

@@ -156,7 +199,8 @@ describe('Top level API', () => {
156199
expect(log.severityNumber).to.equal(SeverityNumber.ERROR);
157200
expect(log.body).to.equal('a string error');
158201
expect(log.attributes).to.deep.equal({
159-
'app.version': 'unset'
202+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
203+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID
160204
});
161205
});
162206

@@ -168,7 +212,8 @@ describe('Top level API', () => {
168212
expect(log.severityNumber).to.equal(SeverityNumber.ERROR);
169213
expect(log.body).to.equal('Unknown error type: number');
170214
expect(log.attributes).to.deep.equal({
171-
'app.version': 'unset'
215+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
216+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID
172217
});
173218
});
174219

@@ -195,9 +240,10 @@ describe('Top level API', () => {
195240
expect(emittedLogs[0].attributes).to.deep.equal({
196241
'error.type': 'TestError',
197242
'error.stack': '...stack trace...',
198-
'app.version': 'unset',
243+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
199244
'logging.googleapis.com/trace': `projects/${PROJECT_ID}/traces/my-trace`,
200-
'logging.googleapis.com/spanId': `my-span`
245+
'logging.googleapis.com/spanId': `my-span`,
246+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID
201247
});
202248
});
203249

@@ -220,13 +266,14 @@ describe('Top level API', () => {
220266
expect(log.attributes).to.deep.equal({
221267
'error.type': 'TestError',
222268
'error.stack': '...stack trace...',
223-
'app.version': 'unset',
269+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
224270
strAttr: 'string attribute',
225271
mapAttr: {
226272
boolAttr: true,
227273
numAttr: 2
228274
},
229-
arrAttr: [1, 2, 3]
275+
arrAttr: [1, 2, 3],
276+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID
230277
});
231278
});
232279

@@ -244,7 +291,75 @@ describe('Top level API', () => {
244291
expect(emittedLogs.length).to.equal(1);
245292
const log = emittedLogs[0];
246293
expect(log.attributes).to.deep.equal({
247-
'app.version': '1.0.0'
294+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: '1.0.0',
295+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID
296+
});
297+
});
298+
299+
describe('Session Metadata', () => {
300+
it('should generate and store a new session ID if none exists', () => {
301+
captureError(fakeTelemetry, 'error');
302+
303+
expect(emittedLogs.length).to.equal(1);
304+
const log = emittedLogs[0];
305+
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.equal(
306+
MOCK_SESSION_ID
307+
);
308+
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal(MOCK_SESSION_ID);
309+
});
310+
311+
it('should retrieve existing session ID from sessionStorage', () => {
312+
storage[TELEMETRY_SESSION_ID_KEY] = 'existing-session-id';
313+
314+
captureError(fakeTelemetry, 'error');
315+
316+
expect(emittedLogs.length).to.equal(1);
317+
const log = emittedLogs[0];
318+
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.equal(
319+
'existing-session-id'
320+
);
321+
});
322+
323+
it('should handle errors when sessionStorage.getItem throws', () => {
324+
const sessionStorageMock: Partial<Storage> = {
325+
getItem: () => {
326+
throw new Error('SecurityError');
327+
},
328+
setItem: () => {}
329+
};
330+
331+
Object.defineProperty(global, 'sessionStorage', {
332+
value: sessionStorageMock,
333+
writable: true
334+
});
335+
336+
captureError(fakeTelemetry, 'error');
337+
338+
expect(emittedLogs.length).to.equal(1);
339+
const log = emittedLogs[0];
340+
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.be
341+
.undefined;
342+
});
343+
344+
it('should handle errors when sessionStorage.setItem throws', () => {
345+
const sessionStorageMock: Partial<Storage> = {
346+
getItem: () => null, // Emulate no existing session ID
347+
setItem: () => {
348+
throw new Error('SecurityError');
349+
}
350+
};
351+
352+
Object.defineProperty(global, 'sessionStorage', {
353+
value: sessionStorageMock,
354+
writable: true
355+
});
356+
357+
captureError(fakeTelemetry, 'error');
358+
359+
expect(emittedLogs.length).to.equal(1);
360+
const log = emittedLogs[0];
361+
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.be
362+
.undefined;
248363
});
249364
});
250365
});

packages/telemetry/src/api.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
*/
1717

1818
import { _getProvider, FirebaseApp, getApp } from '@firebase/app';
19-
import { TELEMETRY_TYPE } from './constants';
19+
import {
20+
LOG_ENTRY_ATTRIBUTE_KEYS,
21+
TELEMETRY_SESSION_ID_KEY,
22+
TELEMETRY_TYPE
23+
} from './constants';
2024
import { Telemetry, TelemetryOptions } from './public-types';
2125
import { Provider } from '@firebase/component';
2226
import { AnyValueMap, SeverityNumber } from '@opentelemetry/api-logs';
@@ -98,7 +102,24 @@ export function captureError(
98102
if ((telemetry as TelemetryService).options?.appVersion) {
99103
appVersion = (telemetry as TelemetryService).options!.appVersion!;
100104
}
101-
customAttributes['app.version'] = appVersion;
105+
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION] = appVersion;
106+
107+
// Add session ID metadata
108+
if (
109+
typeof sessionStorage !== 'undefined' &&
110+
typeof crypto?.randomUUID === 'function'
111+
) {
112+
try {
113+
let sessionId = sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY);
114+
if (!sessionId) {
115+
sessionId = crypto.randomUUID();
116+
sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId);
117+
}
118+
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID] = sessionId;
119+
} catch (e) {
120+
// Ignore errors accessing sessionStorage (e.g. security restrictions)
121+
}
122+
}
102123

103124
if (error instanceof Error) {
104125
logger.emit({

packages/telemetry/src/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@
1717

1818
/** Type constant for Firebase Telemetry. */
1919
export const TELEMETRY_TYPE = 'telemetry';
20+
21+
/** Key for storing the session ID in sessionStorage. */
22+
export const TELEMETRY_SESSION_ID_KEY = 'firebasetelemetry.sessionid';
23+
24+
/** Keys for attributes in log entries. */
25+
export const LOG_ENTRY_ATTRIBUTE_KEYS = {
26+
USER_ID: 'user.id',
27+
SESSION_ID: 'session.id',
28+
APP_VERSION: 'app.version'
29+
};

packages/telemetry/src/logging/installation-id-provider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import { Provider } from '@firebase/component';
1919
import { DynamicLogAttributeProvider, LogEntryAttribute } from '../types';
2020
import { _FirebaseInstallationsInternal } from '@firebase/installations';
21+
import { LOG_ENTRY_ATTRIBUTE_KEYS } from '../constants';
2122

2223
/**
2324
* Allows logging to include the client's installation ID.
@@ -45,7 +46,7 @@ export class InstallationIdProvider implements DynamicLogAttributeProvider {
4546
return null;
4647
}
4748
if (this._iid) {
48-
return ['user.id', this._iid];
49+
return [LOG_ENTRY_ATTRIBUTE_KEYS.USER_ID, this._iid];
4950
}
5051

5152
const iid = await this.installations.getId();
@@ -54,6 +55,6 @@ export class InstallationIdProvider implements DynamicLogAttributeProvider {
5455
}
5556

5657
this._iid = iid;
57-
return ['user.id', iid];
58+
return [LOG_ENTRY_ATTRIBUTE_KEYS.USER_ID, iid];
5859
}
5960
}

0 commit comments

Comments
 (0)