Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions packages/telemetry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 30 additions & 4 deletions packages/telemetry/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];

Expand Down Expand Up @@ -67,7 +70,8 @@ const fakeTelemetry: Telemetry = {
appId: APP_ID
}
},
loggerProvider: fakeLoggerProvider
loggerProvider: fakeLoggerProvider,
fid: FID
};

describe('Top level API', () => {
Expand Down Expand Up @@ -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...'
});
Expand All @@ -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'
});
Expand All @@ -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', () => {
Expand All @@ -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 () => {
Expand All @@ -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`,
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/telemetry/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions packages/telemetry/src/public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export interface Telemetry {

/** The {@link LoggerProvider} this {@link Telemetry} instance uses. */
loggerProvider: LoggerProvider;

/**
* The Firebase Installation ID.
*
* @internal
*/
fid?: string;
}

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/telemetry/src/register.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion packages/telemetry/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -38,14 +41,15 @@ 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,
endpointUrl,
dynamicHeaderProviders
);

return new TelemetryService(app, loggerProvider);
return new TelemetryService(app, installationsProvider, loggerProvider);
},
ComponentType.PUBLIC
).setMultipleInstances(true)
Expand Down
16 changes: 15 additions & 1 deletion packages/telemetry/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hsubox76 Due to the nature of our API, the captureError method needs to stay synchronous, but retrieving the fid is async. This leads to the fid being set here on subsequent invocations of captureError, not the first. Any ideas on how to have the fid in place in time for the first captureError call while keeping that method sync? cc @andrewbrook

Copy link

@andrewbrook andrewbrook Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed this. We could use the same model we have for dynamic headers, and only retrieve fid when doing the background request?

}

private async _getFid(): Promise<void> {
try {
const fid = await this.installationsProvider.getId();
this.fid = fid;
} catch (err) {
console.error('Failed to get FID for telemetry:', err);
}
}

_delete(): Promise<void> {
return Promise.resolve();
Expand Down
Loading