Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
119 changes: 112 additions & 7 deletions packages/telemetry/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import { Component, ComponentType } from '@firebase/component';
import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types';
import { captureError, flush, getTelemetry } from './api';
import { LOG_ENTRY_ATTRIBUTE_KEYS, TELEMETRY_SESSION_ID_KEY } from './constants';
import { TelemetryService } from './service';
import { registerTelemetry } from './register';
import { _FirebaseInstallationsInternal } from '@firebase/installations';
Expand Down Expand Up @@ -127,7 +128,7 @@ describe('Top level API', () => {
expect(log.attributes).to.deep.equal({
'error.type': 'TestError',
'error.stack': '...stack trace...',
'app.version': 'unset'
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset'
});
});

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

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

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

Expand All @@ -195,7 +196,7 @@ describe('Top level API', () => {
expect(emittedLogs[0].attributes).to.deep.equal({
'error.type': 'TestError',
'error.stack': '...stack trace...',
'app.version': 'unset',
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
'logging.googleapis.com/trace': `projects/${PROJECT_ID}/traces/my-trace`,
'logging.googleapis.com/spanId': `my-span`
});
Expand All @@ -220,7 +221,7 @@ describe('Top level API', () => {
expect(log.attributes).to.deep.equal({
'error.type': 'TestError',
'error.stack': '...stack trace...',
'app.version': 'unset',
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
strAttr: 'string attribute',
mapAttr: {
boolAttr: true,
Expand All @@ -244,7 +245,111 @@ describe('Top level API', () => {
expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.attributes).to.deep.equal({
'app.version': '1.0.0'
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: '1.0.0'
});
});

describe('Session Metadata', () => {
let originalSessionStorage: Storage | undefined;
let originalCrypto: Crypto | undefined;

beforeEach(() => {
// @ts-ignore
originalSessionStorage = global.sessionStorage;
// @ts-ignore
originalCrypto = global.crypto;
});

afterEach(() => {
Object.defineProperty(global, 'sessionStorage', {
value: originalSessionStorage,
writable: true
});
Object.defineProperty(global, 'crypto', {
value: originalCrypto,
writable: true
});
});

it('should generate and store a new session ID if none exists', () => {
const sessionStorageMock = {
getItem: () => null,
setItem: (_: string, __: string) => { }
};
sessionStorageMock.setItem = (
key: string,
value: string
) => {
// @ts-ignore
sessionStorageMock[key] = value;
};
const cryptoMock = {
randomUUID: () => 'new-session-id'
};

Object.defineProperty(global, 'sessionStorage', {
value: sessionStorageMock,
writable: true
});
Object.defineProperty(global, 'crypto', {
value: cryptoMock,
writable: true
});

captureError(fakeTelemetry, 'error');

expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.equal('new-session-id');
// @ts-ignore
expect(sessionStorageMock[TELEMETRY_SESSION_ID_KEY]).to.equal(
'new-session-id'
);
});

it('should retrieve existing session ID from sessionStorage', () => {
const sessionStorageMock = {
getItem: () => 'existing-session-id',
setItem: () => { }
};
const cryptoMock = {
randomUUID: () => 'new-session-id'
};

Object.defineProperty(global, 'sessionStorage', {
value: sessionStorageMock,
writable: true
});
Object.defineProperty(global, 'crypto', {
value: cryptoMock,
writable: true
});

captureError(fakeTelemetry, 'error');

expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.equal('existing-session-id');
});

it('should handle errors when accessing sessionStorage', () => {
const sessionStorageMock = {
getItem: () => {
throw new Error('SecurityError');
},
setItem: () => { }
};

Object.defineProperty(global, 'sessionStorage', {
value: sessionStorageMock,
writable: true
});

captureError(fakeTelemetry, 'error');

expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.attributes![LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]).to.be.undefined;
});
});
});
Expand Down
18 changes: 16 additions & 2 deletions packages/telemetry/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import { _getProvider, FirebaseApp, getApp } from '@firebase/app';
import { TELEMETRY_TYPE } from './constants';
import { LOG_ENTRY_ATTRIBUTE_KEYS, TELEMETRY_SESSION_ID_KEY, TELEMETRY_TYPE } from './constants';
import { Telemetry, TelemetryOptions } from './public-types';
import { Provider } from '@firebase/component';
import { AnyValueMap, SeverityNumber } from '@opentelemetry/api-logs';
Expand Down Expand Up @@ -98,7 +98,21 @@ export function captureError(
if ((telemetry as TelemetryService).options?.appVersion) {
appVersion = (telemetry as TelemetryService).options!.appVersion!;
}
customAttributes['app.version'] = appVersion;
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION] = appVersion;

// Add session ID metadata
if (typeof sessionStorage !== 'undefined') {
try {
let sessionId = sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY);
if (!sessionId) {
sessionId = crypto.randomUUID();
sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId);
}
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID] = sessionId;
} catch (e) {
// Ignore errors accessing sessionStorage (e.g. security restrictions)
}
}

if (error instanceof Error) {
logger.emit({
Expand Down
10 changes: 10 additions & 0 deletions packages/telemetry/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@

/** Type constant for Firebase Telemetry. */
export const TELEMETRY_TYPE = 'telemetry';

/** Key for storing the session ID in sessionStorage. */
export const TELEMETRY_SESSION_ID_KEY = 'firebasetelemetry.sessionid';

/** Keys for attributes in log entries. */
export const LOG_ENTRY_ATTRIBUTE_KEYS = {
USER_ID: 'user.id',
SESSION_ID: 'session.id',
APP_VERSION: 'app.version',
};
5 changes: 3 additions & 2 deletions packages/telemetry/src/logging/installation-id-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { Provider } from '@firebase/component';
import { DynamicLogAttributeProvider, LogEntryAttribute } from '../types';
import { _FirebaseInstallationsInternal } from '@firebase/installations';
import { LOG_ENTRY_ATTRIBUTE_KEYS } from '../constants';

/**
* Allows logging to include the client's installation ID.
Expand Down Expand Up @@ -45,7 +46,7 @@ export class InstallationIdProvider implements DynamicLogAttributeProvider {
return null;
}
if (this._iid) {
return ['user.id', this._iid];
return [LOG_ENTRY_ATTRIBUTE_KEYS.USER_ID, this._iid];
}

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

this._iid = iid;
return ['user.id', iid];
return [LOG_ENTRY_ATTRIBUTE_KEYS.USER_ID, iid];
}
}
Loading