diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 0393651a3a4..1568bd395bc 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -90,6 +90,7 @@ }, "dependencies": { "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.203.0", "@opentelemetry/exporter-logs-otlp-http": "0.203.0", diff --git a/packages/telemetry/src/api.test.ts b/packages/telemetry/src/api.test.ts index 6a6c68deebf..04b0fe7cc5d 100644 --- a/packages/telemetry/src/api.test.ts +++ b/packages/telemetry/src/api.test.ts @@ -37,9 +37,12 @@ import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types'; import { captureError, flush, getTelemetry } from './api'; import { TelemetryService } from './service'; import { registerTelemetry } from './register'; +import { _FirebaseInstallationsInternal } from '@firebase/installations'; const PROJECT_ID = 'my-project'; const APP_ID = 'my-appid'; +const API_KEY = 'my-api-key'; +const FID = 'fid-1234'; const emittedLogs: LogRecord[] = []; @@ -67,7 +70,8 @@ const fakeTelemetry: Telemetry = { appId: APP_ID } }, - loggerProvider: fakeLoggerProvider + loggerProvider: fakeLoggerProvider, + fid: FID }; describe('Top level API', () => { @@ -123,6 +127,7 @@ describe('Top level API', () => { expect(log.severityNumber).to.equal(SeverityNumber.ERROR); expect(log.body).to.equal('This is a test error'); expect(log.attributes).to.deep.equal({ + 'user.id': FID, 'error.type': 'TestError', 'error.stack': '...stack trace...' }); @@ -139,6 +144,7 @@ describe('Top level API', () => { expect(log.severityNumber).to.equal(SeverityNumber.ERROR); expect(log.body).to.equal('error with no stack'); expect(log.attributes).to.deep.equal({ + 'user.id': FID, 'error.type': 'Error', 'error.stack': 'No stack trace available' }); @@ -151,7 +157,9 @@ describe('Top level API', () => { const log = emittedLogs[0]; expect(log.severityNumber).to.equal(SeverityNumber.ERROR); expect(log.body).to.equal('a string error'); - expect(log.attributes).to.deep.equal({}); + expect(log.attributes).to.deep.equal({ + "user.id": "fid-1234" + }); }); it('should capture an unknown error type correctly', () => { @@ -161,7 +169,9 @@ describe('Top level API', () => { const log = emittedLogs[0]; expect(log.severityNumber).to.equal(SeverityNumber.ERROR); expect(log.body).to.equal('Unknown error type: number'); - expect(log.attributes).to.deep.equal({}); + expect(log.attributes).to.deep.equal({ + "user.id": "fid-1234" + }); }); it('should propagate trace context', async () => { @@ -185,6 +195,7 @@ describe('Top level API', () => { await provider.shutdown(); expect(emittedLogs[0].attributes).to.deep.equal({ + 'user.id': FID, 'error.type': 'TestError', 'error.stack': '...stack trace...', 'logging.googleapis.com/trace': `projects/${PROJECT_ID}/traces/my-trace`, @@ -209,6 +220,7 @@ describe('Top level API', () => { expect(emittedLogs.length).to.equal(1); const log = emittedLogs[0]; expect(log.attributes).to.deep.equal({ + 'user.id': FID, 'error.type': 'TestError', 'error.stack': '...stack trace...', strAttr: 'string attribute', @@ -237,6 +249,16 @@ describe('Top level API', () => { function getFakeApp(): FirebaseApp { registerTelemetry(); + _registerComponent( + new Component( + 'installations-internal', + () => ({ + getId: async () => 'FID', + getToken: async () => 'authToken' + }) as _FirebaseInstallationsInternal, + ComponentType.PUBLIC + ) + ); _registerComponent( new Component( 'app-check-internal', @@ -246,7 +268,11 @@ function getFakeApp(): FirebaseApp { ComponentType.PUBLIC ) ); - const app = initializeApp({}); + const app = initializeApp({ + projectId: PROJECT_ID, + appId: APP_ID, + apiKey: API_KEY + }); _addOrOverwriteComponent( app, //@ts-ignore diff --git a/packages/telemetry/src/api.ts b/packages/telemetry/src/api.ts index 0b93fe555fe..aec514458a5 100644 --- a/packages/telemetry/src/api.ts +++ b/packages/telemetry/src/api.ts @@ -87,6 +87,12 @@ export function captureError( const customAttributes = attributes || {}; + // Set firebase installation ID ("FID") if available, which + // represents the "user" who experienced the error. + if (telemetry.fid) { + customAttributes['user.id'] = telemetry.fid; + } + if (error instanceof Error) { logger.emit({ severityNumber: SeverityNumber.ERROR, diff --git a/packages/telemetry/src/public-types.ts b/packages/telemetry/src/public-types.ts index 852c72221fd..04047868849 100644 --- a/packages/telemetry/src/public-types.ts +++ b/packages/telemetry/src/public-types.ts @@ -33,6 +33,13 @@ export interface Telemetry { /** The {@link LoggerProvider} this {@link Telemetry} instance uses. */ loggerProvider: LoggerProvider; + + /** + * The Firebase Installation ID. + * + * @internal + */ + fid?: string; } /** diff --git a/packages/telemetry/src/register.node.ts b/packages/telemetry/src/register.node.ts index 52e27ff8f81..93ea2e01c03 100644 --- a/packages/telemetry/src/register.node.ts +++ b/packages/telemetry/src/register.node.ts @@ -21,6 +21,9 @@ import { TELEMETRY_TYPE } from './constants'; import { name, version } from '../package.json'; import { TelemetryService } from './service'; import { createLoggerProvider } from './logging/logger-provider'; +// This needs to be in the same file that calls `getProvider()` on the component +// or it will get tree-shaken out. +import '@firebase/installations'; export function registerTelemetry(): void { _registerComponent( @@ -36,9 +39,10 @@ export function registerTelemetry(): void { // getImmediate for FirebaseApp will always succeed const app = container.getProvider('app').getImmediate(); + const installationsProvider = container.getProvider('installations-internal').getImmediate(); const loggerProvider = createLoggerProvider(app, endpointUrl); - return new TelemetryService(app, loggerProvider); + return new TelemetryService(app, installationsProvider, loggerProvider); }, ComponentType.PUBLIC ).setMultipleInstances(true) diff --git a/packages/telemetry/src/register.ts b/packages/telemetry/src/register.ts index 9cf0d885a0b..b0b0c94a469 100644 --- a/packages/telemetry/src/register.ts +++ b/packages/telemetry/src/register.ts @@ -22,6 +22,9 @@ import { name, version } from '../package.json'; import { TelemetryService } from './service'; import { createLoggerProvider } from './logging/logger-provider'; import { AppCheckProvider } from './logging/appcheck-provider'; +// This needs to be in the same file that calls `getProvider()` on the component +// or it will get tree-shaken out. +import '@firebase/installations'; export function registerTelemetry(): void { _registerComponent( @@ -38,6 +41,7 @@ export function registerTelemetry(): void { // getImmediate for FirebaseApp will always succeed const app = container.getProvider('app').getImmediate(); const appCheckProvider = container.getProvider('app-check-internal'); + const installationsProvider = container.getProvider('installations-internal').getImmediate(); const dynamicHeaderProviders = [new AppCheckProvider(appCheckProvider)]; const loggerProvider = createLoggerProvider( app, @@ -45,7 +49,7 @@ export function registerTelemetry(): void { dynamicHeaderProviders ); - return new TelemetryService(app, loggerProvider); + return new TelemetryService(app, installationsProvider, loggerProvider); }, ComponentType.PUBLIC ).setMultipleInstances(true) diff --git a/packages/telemetry/src/service.ts b/packages/telemetry/src/service.ts index e09e861d3dc..b0d434ae6db 100644 --- a/packages/telemetry/src/service.ts +++ b/packages/telemetry/src/service.ts @@ -18,9 +18,23 @@ import { _FirebaseService, FirebaseApp } from '@firebase/app'; import { Telemetry } from './public-types'; import { LoggerProvider } from '@opentelemetry/sdk-logs'; +import { _FirebaseInstallationsInternal } from '@firebase/installations'; export class TelemetryService implements Telemetry, _FirebaseService { - constructor(public app: FirebaseApp, public loggerProvider: LoggerProvider) {} + fid?: string; + + constructor(public app: FirebaseApp, public installationsProvider: _FirebaseInstallationsInternal, public loggerProvider: LoggerProvider) { + void this._getFid(); + } + + private async _getFid(): Promise { + try { + const fid = await this.installationsProvider.getId(); + this.fid = fid; + } catch (err) { + console.error('Failed to get FID for telemetry:', err); + } + } _delete(): Promise { return Promise.resolve();