Skip to content

Commit d853dad

Browse files
committed
Add support for sending session ID for telemetry
1 parent d24986a commit d853dad

File tree

4 files changed

+141
-11
lines changed

4 files changed

+141
-11
lines changed

packages/telemetry/src/api.test.ts

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ 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 { LOG_ENTRY_ATTRIBUTE_KEYS, TELEMETRY_SESSION_ID_KEY } from './constants';
3839
import { TelemetryService } from './service';
3940
import { registerTelemetry } from './register';
4041
import { _FirebaseInstallationsInternal } from '@firebase/installations';
@@ -127,7 +128,7 @@ describe('Top level API', () => {
127128
expect(log.attributes).to.deep.equal({
128129
'error.type': 'TestError',
129130
'error.stack': '...stack trace...',
130-
'app.version': 'unset'
131+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset'
131132
});
132133
});
133134

@@ -144,7 +145,7 @@ describe('Top level API', () => {
144145
expect(log.attributes).to.deep.equal({
145146
'error.type': 'Error',
146147
'error.stack': 'No stack trace available',
147-
'app.version': 'unset'
148+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset'
148149
});
149150
});
150151

@@ -156,7 +157,7 @@ describe('Top level API', () => {
156157
expect(log.severityNumber).to.equal(SeverityNumber.ERROR);
157158
expect(log.body).to.equal('a string error');
158159
expect(log.attributes).to.deep.equal({
159-
'app.version': 'unset'
160+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset'
160161
});
161162
});
162163

@@ -168,7 +169,7 @@ describe('Top level API', () => {
168169
expect(log.severityNumber).to.equal(SeverityNumber.ERROR);
169170
expect(log.body).to.equal('Unknown error type: number');
170171
expect(log.attributes).to.deep.equal({
171-
'app.version': 'unset'
172+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset'
172173
});
173174
});
174175

@@ -195,7 +196,7 @@ describe('Top level API', () => {
195196
expect(emittedLogs[0].attributes).to.deep.equal({
196197
'error.type': 'TestError',
197198
'error.stack': '...stack trace...',
198-
'app.version': 'unset',
199+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
199200
'logging.googleapis.com/trace': `projects/${PROJECT_ID}/traces/my-trace`,
200201
'logging.googleapis.com/spanId': `my-span`
201202
});
@@ -220,7 +221,7 @@ describe('Top level API', () => {
220221
expect(log.attributes).to.deep.equal({
221222
'error.type': 'TestError',
222223
'error.stack': '...stack trace...',
223-
'app.version': 'unset',
224+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
224225
strAttr: 'string attribute',
225226
mapAttr: {
226227
boolAttr: true,
@@ -244,7 +245,111 @@ describe('Top level API', () => {
244245
expect(emittedLogs.length).to.equal(1);
245246
const log = emittedLogs[0];
246247
expect(log.attributes).to.deep.equal({
247-
'app.version': '1.0.0'
248+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: '1.0.0'
249+
});
250+
});
251+
252+
describe('Session Metadata', () => {
253+
let originalSessionStorage: Storage | undefined;
254+
let originalCrypto: Crypto | undefined;
255+
256+
beforeEach(() => {
257+
// @ts-ignore
258+
originalSessionStorage = global.sessionStorage;
259+
// @ts-ignore
260+
originalCrypto = global.crypto;
261+
});
262+
263+
afterEach(() => {
264+
Object.defineProperty(global, 'sessionStorage', {
265+
value: originalSessionStorage,
266+
writable: true
267+
});
268+
Object.defineProperty(global, 'crypto', {
269+
value: originalCrypto,
270+
writable: true
271+
});
272+
});
273+
274+
it('should generate and store a new session ID if none exists', () => {
275+
const sessionStorageMock = {
276+
getItem: () => null,
277+
setItem: (_: string, __: string) => { }
278+
};
279+
sessionStorageMock.setItem = (
280+
key: string,
281+
value: string
282+
) => {
283+
// @ts-ignore
284+
sessionStorageMock[key] = value;
285+
};
286+
const cryptoMock = {
287+
randomUUID: () => 'new-session-id'
288+
};
289+
290+
Object.defineProperty(global, 'sessionStorage', {
291+
value: sessionStorageMock,
292+
writable: true
293+
});
294+
Object.defineProperty(global, 'crypto', {
295+
value: cryptoMock,
296+
writable: true
297+
});
298+
299+
captureError(fakeTelemetry, 'error');
300+
301+
expect(emittedLogs.length).to.equal(1);
302+
const log = emittedLogs[0];
303+
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.equal('new-session-id');
304+
// @ts-ignore
305+
expect(sessionStorageMock[TELEMETRY_SESSION_ID_KEY]).to.equal(
306+
'new-session-id'
307+
);
308+
});
309+
310+
it('should retrieve existing session ID from sessionStorage', () => {
311+
const sessionStorageMock = {
312+
getItem: () => 'existing-session-id',
313+
setItem: () => { }
314+
};
315+
const cryptoMock = {
316+
randomUUID: () => 'new-session-id'
317+
};
318+
319+
Object.defineProperty(global, 'sessionStorage', {
320+
value: sessionStorageMock,
321+
writable: true
322+
});
323+
Object.defineProperty(global, 'crypto', {
324+
value: cryptoMock,
325+
writable: true
326+
});
327+
328+
captureError(fakeTelemetry, 'error');
329+
330+
expect(emittedLogs.length).to.equal(1);
331+
const log = emittedLogs[0];
332+
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.equal('existing-session-id');
333+
});
334+
335+
it('should handle errors when accessing sessionStorage', () => {
336+
const sessionStorageMock = {
337+
getItem: () => {
338+
throw new Error('SecurityError');
339+
},
340+
setItem: () => { }
341+
};
342+
343+
Object.defineProperty(global, 'sessionStorage', {
344+
value: sessionStorageMock,
345+
writable: true
346+
});
347+
348+
captureError(fakeTelemetry, 'error');
349+
350+
expect(emittedLogs.length).to.equal(1);
351+
const log = emittedLogs[0];
352+
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.be.undefined;
248353
});
249354
});
250355
});

packages/telemetry/src/api.ts

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

1818
import { _getProvider, FirebaseApp, getApp } from '@firebase/app';
19-
import { TELEMETRY_TYPE } from './constants';
19+
import { LOG_ENTRY_ATTRIBUTE_KEYS, TELEMETRY_SESSION_ID_KEY, TELEMETRY_TYPE } from './constants';
2020
import { Telemetry, TelemetryOptions } from './public-types';
2121
import { Provider } from '@firebase/component';
2222
import { AnyValueMap, SeverityNumber } from '@opentelemetry/api-logs';
@@ -98,7 +98,21 @@ export function captureError(
9898
if ((telemetry as TelemetryService).options?.appVersion) {
9999
appVersion = (telemetry as TelemetryService).options!.appVersion!;
100100
}
101-
customAttributes['app.version'] = appVersion;
101+
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION] = appVersion;
102+
103+
// Add session ID metadata
104+
if (typeof sessionStorage !== 'undefined') {
105+
try {
106+
let sessionId = sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY);
107+
if (!sessionId) {
108+
sessionId = crypto.randomUUID();
109+
sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId);
110+
}
111+
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID] = sessionId;
112+
} catch (e) {
113+
// Ignore errors accessing sessionStorage (e.g. security restrictions)
114+
}
115+
}
102116

103117
if (error instanceof Error) {
104118
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)