Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions packages/telemetry/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ describe('Top level API', () => {
value: cryptoMock,
writable: true
});

// Simulate session creation that now happens in registerTelemetry
storage[TELEMETRY_SESSION_ID_KEY] = MOCK_SESSION_ID;
});

afterEach(async () => {
Expand Down Expand Up @@ -154,6 +157,28 @@ describe('Top level API', () => {
});
});

describe('registerTelemetry()', () => {
it('should create a session and emit a log entry if none exists', () => {
// Clear storage to simulate no session
storage = {};
emittedLogs.length = 0;

getTelemetry(getFakeApp());

// Check if session ID was created in storage
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal(MOCK_SESSION_ID);
});

it('should not create a new session if one exists', () => {
storage[TELEMETRY_SESSION_ID_KEY] = 'existing-session';
emittedLogs.length = 0;

getTelemetry(getFakeApp());

expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal('existing-session');
});
});

describe('captureError()', () => {
it('should capture an Error object correctly', () => {
const error = new Error('This is a test error');
Expand Down Expand Up @@ -312,17 +337,6 @@ describe('Top level API', () => {
});

describe('Session Metadata', () => {
it('should generate and store a new session ID if none exists', () => {
captureError(fakeTelemetry, 'error');

expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.equal(
MOCK_SESSION_ID
);
expect(storage[TELEMETRY_SESSION_ID_KEY]).to.equal(MOCK_SESSION_ID);
});

it('should retrieve existing session ID from sessionStorage', () => {
storage[TELEMETRY_SESSION_ID_KEY] = 'existing-session-id';

Expand Down
45 changes: 38 additions & 7 deletions packages/telemetry/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@
* limitations under the License.
*/

import { LoggerProvider, SeverityNumber } from '@opentelemetry/api-logs';
import * as constants from './auto-constants';
import { TELEMETRY_SESSION_ID_KEY } from './constants';
import {
LOG_ENTRY_ATTRIBUTE_KEYS,
TELEMETRY_SESSION_ID_KEY
} from './constants';
import { Telemetry } from './public-types';
import { TelemetryService } from './service';

/**
* Returns the app version from the provided Telemetry instance, if available.
*/
export function getAppVersion(telemetry: Telemetry): string {
if ((telemetry as TelemetryService).options?.appVersion) {
return (telemetry as TelemetryService).options!.appVersion!;
Expand All @@ -29,18 +36,42 @@ export function getAppVersion(telemetry: Telemetry): string {
return 'unset';
}

/**
* Returns the session ID stored in sessionStorage, if available.
*/
export function getSessionId(): string | undefined {
if (typeof sessionStorage !== 'undefined') {
try {
return sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY) || undefined;
} catch (e) {
// Ignore errors accessing sessionStorage (e.g. security restrictions)
}
}
}

/**
* Generate a new session UUID. We record it in two places:
* 1. The client browser's sessionStorage (if available)
* 2. In Cloud Logging as its own log entry
*/
export function startNewSession(loggerProvider: LoggerProvider): void {
if (
typeof sessionStorage !== 'undefined' &&
typeof crypto?.randomUUID === 'function'
) {
try {
let sessionId = sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY);
if (!sessionId) {
sessionId = crypto.randomUUID();
sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId);
}
return sessionId;
const sessionId = crypto.randomUUID();
sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId);

// Emit session creation log
const logger = loggerProvider.getLogger('session-logger');
logger.emit({
severityNumber: SeverityNumber.DEBUG,
body: 'Session created',
attributes: {
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: sessionId
}
});
} catch (e) {
// Ignore errors accessing sessionStorage (e.g. security restrictions)
}
Expand Down
14 changes: 4 additions & 10 deletions packages/telemetry/src/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,18 @@ export function FirebaseTelemetry({
telemetryOptions?: TelemetryOptions;
}): null {
useEffect(() => {
const telemetry = getTelemetry(firebaseApp, telemetryOptions);

if (typeof window === 'undefined') {
return;
}

const errorListener = (event: ErrorEvent): void => {
captureError(
getTelemetry(firebaseApp, telemetryOptions),
event.error,
{}
);
captureError(telemetry, event.error, {});
};

const unhandledRejectionListener = (event: PromiseRejectionEvent): void => {
captureError(
getTelemetry(firebaseApp, telemetryOptions),
event.reason,
{}
);
captureError(telemetry, event.reason, {});
};

try {
Expand Down
8 changes: 7 additions & 1 deletion packages/telemetry/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@

import { _registerComponent, registerVersion } from '@firebase/app';
import { Component, ComponentType } from '@firebase/component';
import { TELEMETRY_TYPE } from './constants';
import { name, version } from '../package.json';
import { TelemetryService } from './service';
import { createLoggerProvider } from './logging/logger-provider';
import { AppCheckProvider } from './logging/appcheck-provider';
import { InstallationIdProvider } from './logging/installation-id-provider';
import { TELEMETRY_TYPE } from './constants';

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

export function registerTelemetry(): void {
_registerComponent(
Expand Down Expand Up @@ -57,6 +58,11 @@ export function registerTelemetry(): void {
dynamicLogAttributeProviders
);

// Immediately track this as a new client session (if one doesn't exist yet)
if (!getSessionId()) {
startNewSession(loggerProvider);
}

return new TelemetryService(app, loggerProvider);
},
ComponentType.PUBLIC
Expand Down
Loading