From 6b3125d3b5fabbb80998adc584ec00ccf17180fb Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Tue, 11 Nov 2025 19:03:24 +0000 Subject: [PATCH 01/12] Attach fid to `captureError` in telemetry --- packages/telemetry/package.json | 1 + packages/telemetry/src/api.test.ts | 19 +++++++++++++++++-- packages/telemetry/src/api.ts | 6 ++++++ packages/telemetry/src/public-types.ts | 7 +++++++ packages/telemetry/src/service.ts | 3 ++- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index e18a2c8d2e6..3699004adaa 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -71,6 +71,7 @@ "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-types": "0.x", + "@firebase/installations": "0.x", "@types/react": "17.x || 18.x || 19.x", "react": "17.x || 18.x || 19.x" }, diff --git a/packages/telemetry/src/api.test.ts b/packages/telemetry/src/api.test.ts index ece52760e84..7baf8bed38e 100644 --- a/packages/telemetry/src/api.test.ts +++ b/packages/telemetry/src/api.test.ts @@ -67,7 +67,8 @@ const fakeTelemetry: Telemetry = { appId: APP_ID } }, - loggerProvider: fakeLoggerProvider + loggerProvider: fakeLoggerProvider, + fid: 'fid-1234' }; describe('Top level API', () => { @@ -123,6 +124,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-1234', 'error.type': 'TestError', 'error.stack': '...stack trace...', 'app.version': 'unset' @@ -140,6 +142,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-1234', 'error.type': 'Error', 'error.stack': 'No stack trace available', 'app.version': 'unset' @@ -191,6 +194,7 @@ describe('Top level API', () => { await provider.shutdown(); expect(emittedLogs[0].attributes).to.deep.equal({ + 'user.id': 'fid-1234', 'error.type': 'TestError', 'error.stack': '...stack trace...', 'app.version': 'unset', @@ -216,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-1234', 'error.type': 'TestError', 'error.stack': '...stack trace...', 'app.version': 'unset', @@ -263,6 +268,16 @@ describe('Top level API', () => { function getFakeApp(): FirebaseApp { registerTelemetry(); + _registerComponent( + new Component( + 'installations-internal', + () => ({ + getId: () => Promise.resolve('fid-1234'), + getToken: () => Promise.resolve('token-5678') + }), + ComponentType.PRIVATE + ) + ); _registerComponent( new Component( 'app-check-internal', @@ -281,7 +296,7 @@ function getFakeApp(): FirebaseApp { // @ts-ignore () => { return { - triggerHeartbeat: () => {} + triggerHeartbeat: () => { } }; }, ComponentType.PUBLIC diff --git a/packages/telemetry/src/api.ts b/packages/telemetry/src/api.ts index 72b6c123e23..03a239e22e8 100644 --- a/packages/telemetry/src/api.ts +++ b/packages/telemetry/src/api.ts @@ -100,6 +100,12 @@ export function captureError( } customAttributes['app.version'] = appVersion; + // 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 0d807e84086..2d6175d7fbb 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/service.ts b/packages/telemetry/src/service.ts index c5848ac65cb..68e40ab754e 100644 --- a/packages/telemetry/src/service.ts +++ b/packages/telemetry/src/service.ts @@ -18,11 +18,12 @@ import { _FirebaseService, FirebaseApp } from '@firebase/app'; import { Telemetry, TelemetryOptions } from './public-types'; import { LoggerProvider } from '@opentelemetry/sdk-logs'; +import { getInstallations, getId } from '@firebase/installations'; export class TelemetryService implements Telemetry, _FirebaseService { private _options?: TelemetryOptions; - constructor(public app: FirebaseApp, public loggerProvider: LoggerProvider) {} + constructor(public app: FirebaseApp, public loggerProvider: LoggerProvider) { } _delete(): Promise { return Promise.resolve(); From b53ab4c5703f735edd4a0a8e5929f69ab5926b2c Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Tue, 11 Nov 2025 19:28:49 +0000 Subject: [PATCH 02/12] Update error message log in getFid() fetch --- packages/telemetry/src/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/telemetry/src/service.ts b/packages/telemetry/src/service.ts index 68e40ab754e..cc79fcf210d 100644 --- a/packages/telemetry/src/service.ts +++ b/packages/telemetry/src/service.ts @@ -18,7 +18,6 @@ import { _FirebaseService, FirebaseApp } from '@firebase/app'; import { Telemetry, TelemetryOptions } from './public-types'; import { LoggerProvider } from '@opentelemetry/sdk-logs'; -import { getInstallations, getId } from '@firebase/installations'; export class TelemetryService implements Telemetry, _FirebaseService { private _options?: TelemetryOptions; From f476c0583e3332a844c94bf5bd81171e6ea424e1 Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Tue, 11 Nov 2025 22:05:09 +0000 Subject: [PATCH 03/12] Provide installations package via getProvider --- packages/telemetry/package.json | 2 +- packages/telemetry/src/api.test.ts | 27 ++++++++++++++++--------- packages/telemetry/src/register.node.ts | 6 +++++- packages/telemetry/src/register.ts | 6 +++++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 3699004adaa..9cef88e7d13 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -71,7 +71,6 @@ "peerDependencies": { "@firebase/app": "0.x", "@firebase/app-types": "0.x", - "@firebase/installations": "0.x", "@types/react": "17.x || 18.x || 19.x", "react": "17.x || 18.x || 19.x" }, @@ -91,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 7baf8bed38e..7fc4e7f6e55 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[] = []; @@ -68,7 +71,7 @@ const fakeTelemetry: Telemetry = { } }, loggerProvider: fakeLoggerProvider, - fid: 'fid-1234' + fid: FID }; describe('Top level API', () => { @@ -124,7 +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-1234', + 'user.id': FID, 'error.type': 'TestError', 'error.stack': '...stack trace...', 'app.version': 'unset' @@ -142,7 +145,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-1234', + 'user.id': FID, 'error.type': 'Error', 'error.stack': 'No stack trace available', 'app.version': 'unset' @@ -194,7 +197,7 @@ describe('Top level API', () => { await provider.shutdown(); expect(emittedLogs[0].attributes).to.deep.equal({ - 'user.id': 'fid-1234', + 'user.id': FID, 'error.type': 'TestError', 'error.stack': '...stack trace...', 'app.version': 'unset', @@ -220,7 +223,7 @@ describe('Top level API', () => { expect(emittedLogs.length).to.equal(1); const log = emittedLogs[0]; expect(log.attributes).to.deep.equal({ - 'user.id': 'fid-1234', + 'user.id': FID, 'error.type': 'TestError', 'error.stack': '...stack trace...', 'app.version': 'unset', @@ -272,10 +275,10 @@ function getFakeApp(): FirebaseApp { new Component( 'installations-internal', () => ({ - getId: () => Promise.resolve('fid-1234'), - getToken: () => Promise.resolve('token-5678') - }), - ComponentType.PRIVATE + getId: async () => 'FID', + getToken: async () => 'authToken' + }) as _FirebaseInstallationsInternal, + ComponentType.PUBLIC ) ); _registerComponent( @@ -287,7 +290,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/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) From 31ebe3f2baebb4f05c06894794d79b45d5106fb8 Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 19:48:39 +0000 Subject: [PATCH 04/12] Refactor to create and use a DynamicLogAttributeProvider --- packages/telemetry/package.json | 1 + packages/telemetry/src/api.test.ts | 5 +- .../logging/installation-id-provider.test.ts | 69 +++++++++++++++++++ .../src/logging/installation-id-provider.ts | 44 ++++++++++++ .../telemetry/src/logging/logger-provider.ts | 46 ++++++++++++- packages/telemetry/src/register.node.ts | 6 +- packages/telemetry/src/register.ts | 13 +++- packages/telemetry/src/types.ts | 32 ++++++++- yarn.lock | 7 ++ 9 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 packages/telemetry/src/logging/installation-id-provider.test.ts create mode 100644 packages/telemetry/src/logging/installation-id-provider.ts diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 9cef88e7d13..155acb0c223 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -93,6 +93,7 @@ "@firebase/installations": "0.6.19", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.203.0", + "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-logs-otlp-http": "0.203.0", "@opentelemetry/otlp-exporter-base": "0.205.0", "@opentelemetry/otlp-transformer": "0.205.0", diff --git a/packages/telemetry/src/api.test.ts b/packages/telemetry/src/api.test.ts index 7fc4e7f6e55..148884bda6e 100644 --- a/packages/telemetry/src/api.test.ts +++ b/packages/telemetry/src/api.test.ts @@ -274,10 +274,11 @@ function getFakeApp(): FirebaseApp { _registerComponent( new Component( 'installations-internal', - () => ({ + () => + ({ getId: async () => 'FID', getToken: async () => 'authToken' - }) as _FirebaseInstallationsInternal, + } as _FirebaseInstallationsInternal), ComponentType.PUBLIC ) ); diff --git a/packages/telemetry/src/logging/installation-id-provider.test.ts b/packages/telemetry/src/logging/installation-id-provider.test.ts new file mode 100644 index 00000000000..73113fd4992 --- /dev/null +++ b/packages/telemetry/src/logging/installation-id-provider.test.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InstallationIdProvider } from './installation-id-provider'; +import { _FirebaseInstallationsInternal } from '@firebase/installations'; +import { expect } from 'chai'; + +describe('InstallationIdProvider', () => { + it('should cache the FID after the first call', async () => { + let callCount = 0; + const mockInstallations = { + getId: async () => { + callCount++; + return 'fid-123'; + } + } as unknown as _FirebaseInstallationsInternal; + + const provider = new InstallationIdProvider(mockInstallations); + + const attr1 = await provider.getAttribute(); + expect(attr1).to.deep.equal(['user.id', 'fid-123']); + expect(callCount).to.equal(1); + + const attr2 = await provider.getAttribute(); + expect(attr2).to.deep.equal(['user.id', 'fid-123']); + expect(callCount).to.equal(1); // Should still be 1 + }); + + it('should not cache if FID is null', async () => { + let callCount = 0; + let returnValue: string | null = null; + const mockInstallations = { + getId: async () => { + callCount++; + return returnValue; + } + } as unknown as _FirebaseInstallationsInternal; + + const provider = new InstallationIdProvider(mockInstallations); + + const attr1 = await provider.getAttribute(); + expect(attr1).to.be.null; + expect(callCount).to.equal(1); + + returnValue = 'fid-456'; + const attr2 = await provider.getAttribute(); + expect(attr2).to.deep.equal(['user.id', 'fid-456']); + expect(callCount).to.equal(2); + + // Should cache now + const attr3 = await provider.getAttribute(); + expect(attr3).to.deep.equal(['user.id', 'fid-456']); + expect(callCount).to.equal(2); + }); +}); diff --git a/packages/telemetry/src/logging/installation-id-provider.ts b/packages/telemetry/src/logging/installation-id-provider.ts new file mode 100644 index 00000000000..a16073e9dac --- /dev/null +++ b/packages/telemetry/src/logging/installation-id-provider.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DynamicLogAttributeProvider, LogEntryAttribute } from '../types'; +import { _FirebaseInstallationsInternal } from '@firebase/installations'; + +/** + * An implementation of DynamicHeaderProvider that can be used to provide App Check token headers. + * + * @internal + */ +export class InstallationIdProvider implements DynamicLogAttributeProvider { + private _fid: string | undefined; + + constructor(private installationsProvider: _FirebaseInstallationsInternal) {} + + async getAttribute(): Promise { + if (this._fid) { + return ['user.id', this._fid]; + } + + const fid = await this.installationsProvider.getId(); + if (!fid) { + return null; + } + + this._fid = fid; + return ['user.id', fid]; + } +} diff --git a/packages/telemetry/src/logging/logger-provider.ts b/packages/telemetry/src/logging/logger-provider.ts index 3cb4e0e947c..0d0042b3325 100644 --- a/packages/telemetry/src/logging/logger-provider.ts +++ b/packages/telemetry/src/logging/logger-provider.ts @@ -30,8 +30,10 @@ import { createOtlpNetworkExportDelegate } from '@opentelemetry/otlp-exporter-base'; import { FetchTransport } from './fetch-transport'; -import { DynamicHeaderProvider } from '../types'; +import { DynamicHeaderProvider, DynamicLogAttributeProvider } from '../types'; import { FirebaseApp } from '@firebase/app'; +import { ExportResult } from '@opentelemetry/core'; +import { _FirebaseInstallationsInternal } from '@firebase/installations'; /** * Create a logger provider for the current execution environment. @@ -41,7 +43,8 @@ import { FirebaseApp } from '@firebase/app'; export function createLoggerProvider( app: FirebaseApp, endpointUrl: string, - dynamicHeaderProviders: DynamicHeaderProvider[] = [] + dynamicHeaderProviders: DynamicHeaderProvider[] = [], + dynamicLogAttributeProviders: DynamicLogAttributeProvider[] = [] ): LoggerProvider { const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'firebase_telemetry_service' @@ -64,11 +67,48 @@ export function createLoggerProvider( return new LoggerProvider({ resource, - processors: [new BatchLogRecordProcessor(logExporter)], + processors: [ + new BatchLogRecordProcessor( + new AsyncAttributeLogExporter(logExporter, dynamicLogAttributeProviders) + ) + ], logRecordLimits: {} }); } +/** A log exporter that appends log entries with resolved async attributes before exporting. */ +class AsyncAttributeLogExporter implements LogRecordExporter { + private readonly _delegate: LogRecordExporter; + + constructor( + exporter: OTLPLogExporter, + private dynamicLogAttributeProviders: DynamicLogAttributeProvider[] + ) { + this._delegate = exporter; + } + + async export( + logs: ReadableLogRecord[], + resultCallback: (result: ExportResult) => void + ): Promise { + void Promise.all( + this.dynamicLogAttributeProviders.map(async provider => { + const attribute = await provider.getAttribute(); + if (attribute) { + logs.forEach(log => { + log.attributes[attribute[0]] = attribute[1]; + }); + } + }) + ); + void this._delegate.export(logs, resultCallback); + } + + shutdown(): Promise { + return this._delegate.shutdown(); + } +} + /** OTLP exporter that uses custom FetchTransport. */ class OTLPLogExporter extends OTLPExporterBase diff --git a/packages/telemetry/src/register.node.ts b/packages/telemetry/src/register.node.ts index 93ea2e01c03..52e27ff8f81 100644 --- a/packages/telemetry/src/register.node.ts +++ b/packages/telemetry/src/register.node.ts @@ -21,9 +21,6 @@ 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( @@ -39,10 +36,9 @@ 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, installationsProvider, loggerProvider); + return new TelemetryService(app, loggerProvider); }, ComponentType.PUBLIC ).setMultipleInstances(true) diff --git a/packages/telemetry/src/register.ts b/packages/telemetry/src/register.ts index b0b0c94a469..5395471d9d6 100644 --- a/packages/telemetry/src/register.ts +++ b/packages/telemetry/src/register.ts @@ -25,6 +25,7 @@ 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'; +import { InstallationIdProvider } from './logging/installation-id-provider'; export function registerTelemetry(): void { _registerComponent( @@ -41,15 +42,21 @@ 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 installationsProvider = container + .getProvider('installations-internal') + .getImmediate(); const dynamicHeaderProviders = [new AppCheckProvider(appCheckProvider)]; + const dynamicLogAttributeProviders = [ + new InstallationIdProvider(installationsProvider) + ]; const loggerProvider = createLoggerProvider( app, endpointUrl, - dynamicHeaderProviders + dynamicHeaderProviders, + dynamicLogAttributeProviders ); - return new TelemetryService(app, installationsProvider, loggerProvider); + return new TelemetryService(app, loggerProvider); }, ComponentType.PUBLIC ).setMultipleInstances(true) diff --git a/packages/telemetry/src/types.ts b/packages/telemetry/src/types.ts index 95c517148cb..3c9d915a0ed 100644 --- a/packages/telemetry/src/types.ts +++ b/packages/telemetry/src/types.ts @@ -15,12 +15,38 @@ * limitations under the License. */ +type KeyValuePair = [key: string, value: string]; + +/** + * A type for Cloud Logging log entry attributes + * + * @internal + */ +export type LogEntryAttribute = KeyValuePair; + +/** + * An interface for classes that provide dynamic log entry attributes. + * + * Classes that implement this interface can be used to supply custom headers for logging. + * + * @internal + */ +export interface DynamicLogAttributeProvider { + /** + * Returns a record of attributes to be added to a log entry. + * + * @returns A {@link Promise} that resolves to a {@link LogEntryAttribute} key-value pair, + * or null if no attribute is to be added. + */ + getAttribute(): Promise; +} + /** * A type for HTTP Headers * * @internal */ -export type HttpHeader = [key: string, value: string]; +export type HttpHeader = KeyValuePair; /** * An interface for classes that provide dynamic headers. @@ -33,8 +59,8 @@ export interface DynamicHeaderProvider { /** * Returns a record of headers to be added to a request. * - * @returns A {@link Promise} that resolves to a {@link Record} of header - * key-value pairs, or null if no headers are to be added. + * @returns A {@link Promise} that resolves to a {@link HttpHeader} key-value pair, + * or null if no header is to be added. */ getHeader(): Promise; } diff --git a/yarn.lock b/yarn.lock index a5e54f121ef..2128373edfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2777,6 +2777,13 @@ dependencies: "@opentelemetry/semantic-conventions" "^1.29.0" +"@opentelemetry/core@2.2.0": + version "2.2.0" + resolved "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz#2f857d7790ff160a97db3820889b5f4cade6eaee" + integrity sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw== + dependencies: + "@opentelemetry/semantic-conventions" "^1.29.0" + "@opentelemetry/exporter-logs-otlp-http@0.203.0": version "0.203.0" resolved "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.203.0.tgz#cdecb5c5b39561aa8520c8bb78347c6e11c91a81" From 4377815b9d9d1020243ba090133ecbb75265c4cf Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 20:01:53 +0000 Subject: [PATCH 05/12] Fix tests --- packages/telemetry/src/api.test.ts | 15 ++++++++------- packages/telemetry/src/logging/logger-provider.ts | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/telemetry/src/api.test.ts b/packages/telemetry/src/api.test.ts index 148884bda6e..425c420d86d 100644 --- a/packages/telemetry/src/api.test.ts +++ b/packages/telemetry/src/api.test.ts @@ -42,7 +42,6 @@ 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[] = []; @@ -70,8 +69,7 @@ const fakeTelemetry: Telemetry = { appId: APP_ID } }, - loggerProvider: fakeLoggerProvider, - fid: FID + loggerProvider: fakeLoggerProvider }; describe('Top level API', () => { @@ -127,7 +125,6 @@ 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...', 'app.version': 'unset' @@ -145,7 +142,6 @@ 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', 'app.version': 'unset' @@ -197,7 +193,6 @@ describe('Top level API', () => { await provider.shutdown(); expect(emittedLogs[0].attributes).to.deep.equal({ - 'user.id': FID, 'error.type': 'TestError', 'error.stack': '...stack trace...', 'app.version': 'unset', @@ -223,7 +218,6 @@ 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...', 'app.version': 'unset', @@ -275,10 +269,17 @@ function getFakeApp(): FirebaseApp { new Component( 'installations-internal', () => +<<<<<<< HEAD ({ getId: async () => 'FID', getToken: async () => 'authToken' } as _FirebaseInstallationsInternal), +======= + ({ + getId: async () => 'iid', + getToken: async () => 'authToken' + } as _FirebaseInstallationsInternal), +>>>>>>> e66c193d1 (Fix tests) ComponentType.PUBLIC ) ); diff --git a/packages/telemetry/src/logging/logger-provider.ts b/packages/telemetry/src/logging/logger-provider.ts index 0d0042b3325..96da9523bf3 100644 --- a/packages/telemetry/src/logging/logger-provider.ts +++ b/packages/telemetry/src/logging/logger-provider.ts @@ -33,7 +33,6 @@ import { FetchTransport } from './fetch-transport'; import { DynamicHeaderProvider, DynamicLogAttributeProvider } from '../types'; import { FirebaseApp } from '@firebase/app'; import { ExportResult } from '@opentelemetry/core'; -import { _FirebaseInstallationsInternal } from '@firebase/installations'; /** * Create a logger provider for the current execution environment. From 16fdf3e577fd0e209e1dcf87cde778ad21ed39e5 Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 20:10:17 +0000 Subject: [PATCH 06/12] Use await for dynamic attribute resolution --- packages/telemetry/src/logging/logger-provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/telemetry/src/logging/logger-provider.ts b/packages/telemetry/src/logging/logger-provider.ts index 96da9523bf3..7c87b76719f 100644 --- a/packages/telemetry/src/logging/logger-provider.ts +++ b/packages/telemetry/src/logging/logger-provider.ts @@ -90,7 +90,7 @@ class AsyncAttributeLogExporter implements LogRecordExporter { logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void ): Promise { - void Promise.all( + await Promise.all( this.dynamicLogAttributeProviders.map(async provider => { const attribute = await provider.getAttribute(); if (attribute) { @@ -100,7 +100,7 @@ class AsyncAttributeLogExporter implements LogRecordExporter { } }) ); - void this._delegate.export(logs, resultCallback); + this._delegate.export(logs, resultCallback); } shutdown(): Promise { From 50ab9ad6aecd1664d48962c5621b3786ceef30b7 Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 20:22:18 +0000 Subject: [PATCH 07/12] Cleaned up var naming --- .../src/logging/installation-id-provider.test.ts | 16 ++++++++-------- .../src/logging/installation-id-provider.ts | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/telemetry/src/logging/installation-id-provider.test.ts b/packages/telemetry/src/logging/installation-id-provider.test.ts index 73113fd4992..77cc1a85bd4 100644 --- a/packages/telemetry/src/logging/installation-id-provider.test.ts +++ b/packages/telemetry/src/logging/installation-id-provider.test.ts @@ -20,27 +20,27 @@ import { _FirebaseInstallationsInternal } from '@firebase/installations'; import { expect } from 'chai'; describe('InstallationIdProvider', () => { - it('should cache the FID after the first call', async () => { + it('should cache the installation id after the first call', async () => { let callCount = 0; const mockInstallations = { getId: async () => { callCount++; - return 'fid-123'; + return 'iid-123'; } } as unknown as _FirebaseInstallationsInternal; const provider = new InstallationIdProvider(mockInstallations); const attr1 = await provider.getAttribute(); - expect(attr1).to.deep.equal(['user.id', 'fid-123']); + expect(attr1).to.deep.equal(['user.id', 'iid-123']); expect(callCount).to.equal(1); const attr2 = await provider.getAttribute(); - expect(attr2).to.deep.equal(['user.id', 'fid-123']); + expect(attr2).to.deep.equal(['user.id', 'iid-123']); expect(callCount).to.equal(1); // Should still be 1 }); - it('should not cache if FID is null', async () => { + it('should not cache if installation id is null', async () => { let callCount = 0; let returnValue: string | null = null; const mockInstallations = { @@ -56,14 +56,14 @@ describe('InstallationIdProvider', () => { expect(attr1).to.be.null; expect(callCount).to.equal(1); - returnValue = 'fid-456'; + returnValue = 'iid-456'; const attr2 = await provider.getAttribute(); - expect(attr2).to.deep.equal(['user.id', 'fid-456']); + expect(attr2).to.deep.equal(['user.id', 'iid-456']); expect(callCount).to.equal(2); // Should cache now const attr3 = await provider.getAttribute(); - expect(attr3).to.deep.equal(['user.id', 'fid-456']); + expect(attr3).to.deep.equal(['user.id', 'iid-456']); expect(callCount).to.equal(2); }); }); diff --git a/packages/telemetry/src/logging/installation-id-provider.ts b/packages/telemetry/src/logging/installation-id-provider.ts index a16073e9dac..c05c9412c27 100644 --- a/packages/telemetry/src/logging/installation-id-provider.ts +++ b/packages/telemetry/src/logging/installation-id-provider.ts @@ -19,26 +19,26 @@ import { DynamicLogAttributeProvider, LogEntryAttribute } from '../types'; import { _FirebaseInstallationsInternal } from '@firebase/installations'; /** - * An implementation of DynamicHeaderProvider that can be used to provide App Check token headers. + * Allows logging to include the client's installation ID. * * @internal */ export class InstallationIdProvider implements DynamicLogAttributeProvider { - private _fid: string | undefined; + private _iid: string | undefined; constructor(private installationsProvider: _FirebaseInstallationsInternal) {} async getAttribute(): Promise { - if (this._fid) { - return ['user.id', this._fid]; + if (this._iid) { + return ['user.id', this._iid]; } - const fid = await this.installationsProvider.getId(); - if (!fid) { + const iid = await this.installationsProvider.getId(); + if (!iid) { return null; } - this._fid = fid; - return ['user.id', fid]; + this._iid = iid; + return ['user.id', iid]; } } From 9e455d8a6e6f4014c9b7d2f22974679c940ff2f3 Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 20:24:19 +0000 Subject: [PATCH 08/12] Remove old code --- packages/telemetry/src/api.ts | 6 ------ packages/telemetry/src/public-types.ts | 7 ------- 2 files changed, 13 deletions(-) diff --git a/packages/telemetry/src/api.ts b/packages/telemetry/src/api.ts index 03a239e22e8..72b6c123e23 100644 --- a/packages/telemetry/src/api.ts +++ b/packages/telemetry/src/api.ts @@ -100,12 +100,6 @@ export function captureError( } customAttributes['app.version'] = appVersion; - // 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 2d6175d7fbb..0d807e84086 100644 --- a/packages/telemetry/src/public-types.ts +++ b/packages/telemetry/src/public-types.ts @@ -33,13 +33,6 @@ export interface Telemetry { /** The {@link LoggerProvider} this {@link Telemetry} instance uses. */ loggerProvider: LoggerProvider; - - /** - * The Firebase Installation ID. - * - * @internal - */ - fid?: string; } /** From 91f8bbb32c5979112e61afe1b22b737afd84f077 Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 21:06:08 +0000 Subject: [PATCH 09/12] Address pr feedback --- packages/telemetry/src/register.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/telemetry/src/register.ts b/packages/telemetry/src/register.ts index 5395471d9d6..1717fb91bbb 100644 --- a/packages/telemetry/src/register.ts +++ b/packages/telemetry/src/register.ts @@ -22,11 +22,12 @@ 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'; import { InstallationIdProvider } from './logging/installation-id-provider'; +// 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'; + export function registerTelemetry(): void { _registerComponent( new Component( From 8aa5d9fad6e1463dbb8102f5d1b1d8033a9b826a Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 21:21:48 +0000 Subject: [PATCH 10/12] Make installation provider optional --- .../logging/installation-id-provider.test.ts | 14 ++++++++++++-- .../src/logging/installation-id-provider.ts | 19 +++++++++++++++++-- packages/telemetry/src/register.ts | 6 +++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/telemetry/src/logging/installation-id-provider.test.ts b/packages/telemetry/src/logging/installation-id-provider.test.ts index 77cc1a85bd4..0de1143a753 100644 --- a/packages/telemetry/src/logging/installation-id-provider.test.ts +++ b/packages/telemetry/src/logging/installation-id-provider.test.ts @@ -29,7 +29,12 @@ describe('InstallationIdProvider', () => { } } as unknown as _FirebaseInstallationsInternal; - const provider = new InstallationIdProvider(mockInstallations); + const mockProvider = { + getImmediate: () => mockInstallations, + get: async () => mockInstallations + } as any; + + const provider = new InstallationIdProvider(mockProvider); const attr1 = await provider.getAttribute(); expect(attr1).to.deep.equal(['user.id', 'iid-123']); @@ -50,7 +55,12 @@ describe('InstallationIdProvider', () => { } } as unknown as _FirebaseInstallationsInternal; - const provider = new InstallationIdProvider(mockInstallations); + const mockProvider = { + getImmediate: () => mockInstallations, + get: async () => mockInstallations + } as any; + + const provider = new InstallationIdProvider(mockProvider); const attr1 = await provider.getAttribute(); expect(attr1).to.be.null; diff --git a/packages/telemetry/src/logging/installation-id-provider.ts b/packages/telemetry/src/logging/installation-id-provider.ts index c05c9412c27..3e507a33856 100644 --- a/packages/telemetry/src/logging/installation-id-provider.ts +++ b/packages/telemetry/src/logging/installation-id-provider.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { Provider } from '@firebase/component'; import { DynamicLogAttributeProvider, LogEntryAttribute } from '../types'; import { _FirebaseInstallationsInternal } from '@firebase/installations'; @@ -24,16 +25,30 @@ import { _FirebaseInstallationsInternal } from '@firebase/installations'; * @internal */ export class InstallationIdProvider implements DynamicLogAttributeProvider { + private installations: _FirebaseInstallationsInternal | null; private _iid: string | undefined; - constructor(private installationsProvider: _FirebaseInstallationsInternal) {} + constructor(installationsProvider: Provider<'installations-internal'>) { + this.installations = installationsProvider?.getImmediate({ + optional: true + }); + if (!this.installations) { + void installationsProvider + ?.get() + .then(installations => (this.installations = installations)) + .catch(); + } + } async getAttribute(): Promise { + if (!this.installations) { + return null; + } if (this._iid) { return ['user.id', this._iid]; } - const iid = await this.installationsProvider.getId(); + const iid = await this.installations.getId(); if (!iid) { return null; } diff --git a/packages/telemetry/src/register.ts b/packages/telemetry/src/register.ts index 1717fb91bbb..ae1e5855929 100644 --- a/packages/telemetry/src/register.ts +++ b/packages/telemetry/src/register.ts @@ -43,9 +43,9 @@ 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 installationsProvider = container.getProvider( + 'installations-internal' + ); const dynamicHeaderProviders = [new AppCheckProvider(appCheckProvider)]; const dynamicLogAttributeProviders = [ new InstallationIdProvider(installationsProvider) From 693d3ff6e22971020cbda3cd84ea38f96672ce43 Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 21:26:41 +0000 Subject: [PATCH 11/12] Fix test --- packages/telemetry/src/api.test.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/telemetry/src/api.test.ts b/packages/telemetry/src/api.test.ts index 425c420d86d..9a64961a349 100644 --- a/packages/telemetry/src/api.test.ts +++ b/packages/telemetry/src/api.test.ts @@ -269,17 +269,10 @@ function getFakeApp(): FirebaseApp { new Component( 'installations-internal', () => -<<<<<<< HEAD - ({ - getId: async () => 'FID', - getToken: async () => 'authToken' - } as _FirebaseInstallationsInternal), -======= ({ getId: async () => 'iid', getToken: async () => 'authToken' - } as _FirebaseInstallationsInternal), ->>>>>>> e66c193d1 (Fix tests) + } as _FirebaseInstallationsInternal), ComponentType.PUBLIC ) ); From 305d3ea746895c9ad4fce5a27ced62666c9569c6 Mon Sep 17 00:00:00 2001 From: Anthony Barone Date: Mon, 17 Nov 2025 21:49:27 +0000 Subject: [PATCH 12/12] Formatting --- packages/telemetry/src/api.test.ts | 4 ++-- packages/telemetry/src/service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/telemetry/src/api.test.ts b/packages/telemetry/src/api.test.ts index 9a64961a349..62529a166e2 100644 --- a/packages/telemetry/src/api.test.ts +++ b/packages/telemetry/src/api.test.ts @@ -272,7 +272,7 @@ function getFakeApp(): FirebaseApp { ({ getId: async () => 'iid', getToken: async () => 'authToken' - } as _FirebaseInstallationsInternal), + } as _FirebaseInstallationsInternal), ComponentType.PUBLIC ) ); @@ -298,7 +298,7 @@ function getFakeApp(): FirebaseApp { // @ts-ignore () => { return { - triggerHeartbeat: () => { } + triggerHeartbeat: () => {} }; }, ComponentType.PUBLIC diff --git a/packages/telemetry/src/service.ts b/packages/telemetry/src/service.ts index cc79fcf210d..c5848ac65cb 100644 --- a/packages/telemetry/src/service.ts +++ b/packages/telemetry/src/service.ts @@ -22,7 +22,7 @@ import { LoggerProvider } from '@opentelemetry/sdk-logs'; export class TelemetryService implements Telemetry, _FirebaseService { private _options?: TelemetryOptions; - constructor(public app: FirebaseApp, public loggerProvider: LoggerProvider) { } + constructor(public app: FirebaseApp, public loggerProvider: LoggerProvider) {} _delete(): Promise { return Promise.resolve();