Skip to content

Commit b5970cd

Browse files
committed
Create session eagerly in registerTelemetry
1 parent 668485a commit b5970cd

File tree

4 files changed

+79
-33
lines changed

4 files changed

+79
-33
lines changed

packages/telemetry/src/api.test.ts

Lines changed: 29 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,32 @@ 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+
// We need a real-ish app to test registration, but we can mock the components
167+
const app = getFakeApp();
168+
getTelemetry(app);
169+
// getFakeApp calls registerTelemetry, but we want to test it specifically.
170+
// However, registerTelemetry is called during getFakeApp.
171+
172+
// Check if session ID was created in storage
173+
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal(MOCK_SESSION_ID);
174+
});
175+
176+
it('should not create a new session if one exists', () => {
177+
storage[TELEMETRY_SESSION_ID_KEY] = 'existing-session';
178+
emittedLogs.length = 0;
179+
180+
getFakeApp();
181+
182+
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal('existing-session');
183+
});
184+
});
185+
157186
describe('captureError()', () => {
158187
it('should capture an Error object correctly', () => {
159188
const error = new Error('This is a test error');
@@ -312,17 +341,6 @@ describe('Top level API', () => {
312341
});
313342

314343
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-
326344
it('should retrieve existing session ID from sessionStorage', () => {
327345
storage[TELEMETRY_SESSION_ID_KEY] = 'existing-session-id';
328346

packages/telemetry/src/helpers.ts

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

18+
import { LoggerProvider, 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,39 @@ 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 {
33-
if (
34-
typeof sessionStorage !== 'undefined' &&
35-
typeof crypto?.randomUUID === 'function'
36-
) {
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(loggerProvider: LoggerProvider): void {
58+
if (typeof sessionStorage !== 'undefined') {
3759
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;
60+
const sessionId = crypto.randomUUID();
61+
sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId);
62+
63+
// Emit session creation log
64+
const logger = loggerProvider.getLogger('session-logger');
65+
logger.emit({
66+
severityNumber: SeverityNumber.DEBUG,
67+
body: 'Session created',
68+
attributes: {
69+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: sessionId
70+
}
71+
});
4472
} catch (e) {
4573
// Ignore errors accessing sessionStorage (e.g. security restrictions)
4674
}

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: 7 additions & 1 deletion
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,6 +58,11 @@ export function registerTelemetry(): void {
5758
dynamicLogAttributeProviders
5859
);
5960

61+
// Immediately track this as a new client session (if one doesn't exist yet)
62+
if (!getSessionId()) {
63+
startNewSession(loggerProvider);
64+
}
65+
6066
return new TelemetryService(app, loggerProvider);
6167
},
6268
ComponentType.PUBLIC

0 commit comments

Comments
 (0)