Skip to content

Commit 572afb5

Browse files
authored
Create session eagerly in registerTelemetry (#9415)
* Create session eagerly in registerTelemetry * Address feedback * Add app version to session created log entry * Use log entry attribute constant in api * Remove unused var
1 parent 668485a commit 572afb5

File tree

6 files changed

+220
-31
lines changed

6 files changed

+220
-31
lines changed

packages/telemetry/src/api.test.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ describe('Top level API', () => {
113113
value: cryptoMock,
114114
writable: true
115115
});
116+
117+
// Simulate session creation that now happens in registerTelemetry
118+
storage[TELEMETRY_SESSION_ID_KEY] = MOCK_SESSION_ID;
116119
});
117120

118121
afterEach(async () => {
@@ -154,6 +157,28 @@ describe('Top level API', () => {
154157
});
155158
});
156159

160+
describe('registerTelemetry()', () => {
161+
it('should create a session and emit a log entry if none exists', () => {
162+
// Clear storage to simulate no session
163+
storage = {};
164+
emittedLogs.length = 0;
165+
166+
getTelemetry(getFakeApp());
167+
168+
// Check if session ID was created in storage
169+
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal(MOCK_SESSION_ID);
170+
});
171+
172+
it('should not create a new session if one exists', () => {
173+
storage[TELEMETRY_SESSION_ID_KEY] = 'existing-session';
174+
emittedLogs.length = 0;
175+
176+
getTelemetry(getFakeApp());
177+
178+
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal('existing-session');
179+
});
180+
});
181+
157182
describe('captureError()', () => {
158183
it('should capture an Error object correctly', () => {
159184
const error = new Error('This is a test error');
@@ -312,17 +337,6 @@ describe('Top level API', () => {
312337
});
313338

314339
describe('Session Metadata', () => {
315-
it('should generate and store a new session ID if none exists', () => {
316-
captureError(fakeTelemetry, 'error');
317-
318-
expect(emittedLogs.length).to.equal(1);
319-
const log = emittedLogs[0];
320-
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.equal(
321-
MOCK_SESSION_ID
322-
);
323-
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal(MOCK_SESSION_ID);
324-
});
325-
326340
it('should retrieve existing session ID from sessionStorage', () => {
327341
storage[TELEMETRY_SESSION_ID_KEY] = 'existing-session-id';
328342

packages/telemetry/src/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ export function captureError(
9494
}
9595

9696
// Add app version metadata
97-
customAttributes['app.version'] = getAppVersion(telemetry);
97+
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION] =
98+
getAppVersion(telemetry);
9899

99100
// Add session ID metadata
100101
const sessionId = getSessionId();
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
import { LoggerProvider } from '@opentelemetry/sdk-logs';
20+
import { Logger, LogRecord } from '@opentelemetry/api-logs';
21+
import { Telemetry } from './public-types';
22+
import { startNewSession } from './helpers';
23+
import {
24+
LOG_ENTRY_ATTRIBUTE_KEYS,
25+
TELEMETRY_SESSION_ID_KEY
26+
} from './constants';
27+
import { AUTO_CONSTANTS } from './auto-constants';
28+
import { TelemetryService } from './service';
29+
30+
const MOCK_SESSION_ID = '00000000-0000-0000-0000-000000000000';
31+
32+
describe('helpers', () => {
33+
let originalSessionStorage: Storage | undefined;
34+
let originalCrypto: Crypto | undefined;
35+
let storage: Record<string, string> = {};
36+
let emittedLogs: LogRecord[] = [];
37+
38+
const fakeLoggerProvider = {
39+
getLogger: (): Logger => {
40+
return {
41+
emit: (logRecord: LogRecord) => {
42+
emittedLogs.push(logRecord);
43+
}
44+
};
45+
},
46+
forceFlush: () => Promise.resolve(),
47+
shutdown: () => Promise.resolve()
48+
} as unknown as LoggerProvider;
49+
50+
const fakeTelemetry: Telemetry = {
51+
app: {
52+
name: 'DEFAULT',
53+
automaticDataCollectionEnabled: true,
54+
options: {
55+
projectId: 'my-project',
56+
appId: 'my-appid'
57+
}
58+
},
59+
loggerProvider: fakeLoggerProvider
60+
};
61+
62+
beforeEach(() => {
63+
emittedLogs = [];
64+
storage = {};
65+
// @ts-ignore
66+
originalSessionStorage = global.sessionStorage;
67+
// @ts-ignore
68+
originalCrypto = global.crypto;
69+
70+
const sessionStorageMock: Partial<Storage> = {
71+
getItem: (key: string) => storage[key] || null,
72+
setItem: (key: string, value: string) => {
73+
storage[key] = value;
74+
}
75+
};
76+
const cryptoMock: Partial<Crypto> = {
77+
randomUUID: () => MOCK_SESSION_ID
78+
};
79+
80+
Object.defineProperty(global, 'sessionStorage', {
81+
value: sessionStorageMock,
82+
writable: true
83+
});
84+
Object.defineProperty(global, 'crypto', {
85+
value: cryptoMock,
86+
writable: true
87+
});
88+
});
89+
90+
afterEach(() => {
91+
Object.defineProperty(global, 'sessionStorage', {
92+
value: originalSessionStorage,
93+
writable: true
94+
});
95+
Object.defineProperty(global, 'crypto', {
96+
value: originalCrypto,
97+
writable: true
98+
});
99+
delete AUTO_CONSTANTS.appVersion;
100+
});
101+
102+
describe('startNewSession', () => {
103+
it('should create a new session and log it with app version (unset)', () => {
104+
startNewSession(fakeTelemetry);
105+
106+
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal(MOCK_SESSION_ID);
107+
expect(emittedLogs.length).to.equal(1);
108+
expect(emittedLogs[0].attributes).to.deep.equal({
109+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID,
110+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset'
111+
});
112+
});
113+
114+
it('should log app version from AUTO_CONSTANTS', () => {
115+
AUTO_CONSTANTS.appVersion = '1.2.3';
116+
startNewSession(fakeTelemetry);
117+
118+
expect(emittedLogs[0].attributes).to.deep.equal({
119+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID,
120+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: '1.2.3'
121+
});
122+
});
123+
124+
it('should log app version from telemetry options', () => {
125+
const telemetryWithVersion = new TelemetryService(
126+
fakeTelemetry.app,
127+
fakeTelemetry.loggerProvider
128+
);
129+
telemetryWithVersion.options = { appVersion: '9.9.9' };
130+
131+
startNewSession(telemetryWithVersion);
132+
133+
expect(emittedLogs[0].attributes).to.deep.equal({
134+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID,
135+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: '9.9.9'
136+
});
137+
});
138+
});
139+
});

packages/telemetry/src/helpers.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { SeverityNumber } from '@opentelemetry/api-logs';
1819
import * as constants from './auto-constants';
19-
import { TELEMETRY_SESSION_ID_KEY } from './constants';
20+
import {
21+
LOG_ENTRY_ATTRIBUTE_KEYS,
22+
TELEMETRY_SESSION_ID_KEY
23+
} from './constants';
2024
import { Telemetry } from './public-types';
2125
import { TelemetryService } from './service';
2226

27+
/**
28+
* Returns the app version from the provided Telemetry instance, if available.
29+
*/
2330
export function getAppVersion(telemetry: Telemetry): string {
2431
if ((telemetry as TelemetryService).options?.appVersion) {
2532
return (telemetry as TelemetryService).options!.appVersion!;
@@ -29,18 +36,44 @@ export function getAppVersion(telemetry: Telemetry): string {
2936
return 'unset';
3037
}
3138

39+
/**
40+
* Returns the session ID stored in sessionStorage, if available.
41+
*/
3242
export function getSessionId(): string | undefined {
43+
if (typeof sessionStorage !== 'undefined') {
44+
try {
45+
return sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY) || undefined;
46+
} catch (e) {
47+
// Ignore errors accessing sessionStorage (e.g. security restrictions)
48+
}
49+
}
50+
}
51+
52+
/**
53+
* Generate a new session UUID. We record it in two places:
54+
* 1. The client browser's sessionStorage (if available)
55+
* 2. In Cloud Logging as its own log entry
56+
*/
57+
export function startNewSession(telemetry: Telemetry): void {
58+
const { loggerProvider } = telemetry;
3359
if (
3460
typeof sessionStorage !== 'undefined' &&
3561
typeof crypto?.randomUUID === 'function'
3662
) {
3763
try {
38-
let sessionId = sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY);
39-
if (!sessionId) {
40-
sessionId = crypto.randomUUID();
41-
sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId);
42-
}
43-
return sessionId;
64+
const sessionId = crypto.randomUUID();
65+
sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId);
66+
67+
// Emit session creation log
68+
const logger = loggerProvider.getLogger('session-logger');
69+
logger.emit({
70+
severityNumber: SeverityNumber.DEBUG,
71+
body: 'Session created',
72+
attributes: {
73+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: sessionId,
74+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: getAppVersion(telemetry)
75+
}
76+
});
4477
} catch (e) {
4578
// Ignore errors accessing sessionStorage (e.g. security restrictions)
4679
}

packages/telemetry/src/react/index.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,18 @@ export function FirebaseTelemetry({
5353
telemetryOptions?: TelemetryOptions;
5454
}): null {
5555
useEffect(() => {
56+
const telemetry = getTelemetry(firebaseApp, telemetryOptions);
57+
5658
if (typeof window === 'undefined') {
5759
return;
5860
}
5961

6062
const errorListener = (event: ErrorEvent): void => {
61-
captureError(
62-
getTelemetry(firebaseApp, telemetryOptions),
63-
event.error,
64-
{}
65-
);
63+
captureError(telemetry, event.error, {});
6664
};
6765

6866
const unhandledRejectionListener = (event: PromiseRejectionEvent): void => {
69-
captureError(
70-
getTelemetry(firebaseApp, telemetryOptions),
71-
event.reason,
72-
{}
73-
);
67+
captureError(telemetry, event.reason, {});
7468
};
7569

7670
try {

packages/telemetry/src/register.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@
1717

1818
import { _registerComponent, registerVersion } from '@firebase/app';
1919
import { Component, ComponentType } from '@firebase/component';
20-
import { TELEMETRY_TYPE } from './constants';
2120
import { name, version } from '../package.json';
2221
import { TelemetryService } from './service';
2322
import { createLoggerProvider } from './logging/logger-provider';
2423
import { AppCheckProvider } from './logging/appcheck-provider';
2524
import { InstallationIdProvider } from './logging/installation-id-provider';
25+
import { TELEMETRY_TYPE } from './constants';
2626

2727
// We only import types from this package elsewhere in the `telemetry` package, so this
2828
// explicit import is needed here to prevent this module from being tree-shaken out.
2929
import '@firebase/installations';
30+
import { getSessionId, startNewSession } from './helpers';
3031

3132
export function registerTelemetry(): void {
3233
_registerComponent(
@@ -57,7 +58,14 @@ export function registerTelemetry(): void {
5758
dynamicLogAttributeProviders
5859
);
5960

60-
return new TelemetryService(app, loggerProvider);
61+
const telemetryService = new TelemetryService(app, loggerProvider);
62+
63+
// Immediately track this as a new client session (if one doesn't exist yet)
64+
if (!getSessionId()) {
65+
startNewSession(telemetryService);
66+
}
67+
68+
return telemetryService;
6169
},
6270
ComponentType.PUBLIC
6371
).setMultipleInstances(true)

0 commit comments

Comments
 (0)