Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/app/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { name as storageCompatName } from '../../../packages/storage-compat/pack
import { name as firestoreName } from '../../../packages/firestore/package.json';
import { name as aiName } from '../../../packages/ai/package.json';
import { name as firestoreCompatName } from '../../../packages/firestore-compat/package.json';
import { name as telemetryName } from '../../../packages/telemetry/package.json';
import { name as packageName } from '../../../packages/firebase/package.json';

/**
Expand Down Expand Up @@ -74,6 +75,7 @@ export const PLATFORM_LOG_STRING = {
[remoteConfigCompatName]: 'fire-rc-compat',
[storageName]: 'fire-gcs',
[storageCompatName]: 'fire-gcs-compat',
[telemetryName]: 'fire-telemetry',
[firestoreName]: 'fire-fst',
[firestoreCompatName]: 'fire-fst-compat',
[aiName]: 'fire-vertex',
Expand Down
9 changes: 7 additions & 2 deletions packages/telemetry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@
"@firebase/app-types": "0.x"
},
"dependencies": {
"tslib": "^2.1.0",
"@firebase/component": "0.7.0"
"@firebase/component": "0.7.0",
"@opentelemetry/api-logs": "0.203.0",
"@opentelemetry/exporter-logs-otlp-http": "0.203.0",
"@opentelemetry/resources": "2.0.1",
"@opentelemetry/sdk-logs": "0.203.0",
"@opentelemetry/semantic-conventions": "1.36.0",
"tslib": "^2.1.0"
},
"license": "Apache-2.0",
"devDependencies": {
Expand Down
126 changes: 126 additions & 0 deletions packages/telemetry/src/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* @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 { expect } from 'chai';
import { LoggerProvider } from '@opentelemetry/sdk-logs';
import { Telemetry } from './public-types';
import { Logger, LogRecord, SeverityNumber } from '@opentelemetry/api-logs';
import { captureError, flush } from './api';

const emittedLogs: LogRecord[] = [];

const fakeLoggerProvider = {
getLogger: (): Logger => {
return {
emit: (logRecord: LogRecord) => {
emittedLogs.push(logRecord);
}
};
},
forceFlush: () => {
emittedLogs.length = 0;
return Promise.resolve();
},
shutdown: () => Promise.resolve()
} as unknown as LoggerProvider;

const fakeTelemetry: Telemetry = {
app: {
name: 'DEFAULT',
automaticDataCollectionEnabled: true,
options: {
projectId: 'my-project',
appId: 'my-appid'
}
},
loggerProvider: fakeLoggerProvider
};

describe('Top level API', () => {
beforeEach(() => {
// Clear the logs before each test.
emittedLogs.length = 0;
});

describe('captureError()', () => {
it('should capture an Error object correctly', () => {
const error = new Error('This is a test error');
error.stack = '...stack trace...';
error.name = 'TestError';

captureError(fakeTelemetry, error);

expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.severityNumber).to.equal(SeverityNumber.ERROR);
expect(log.body).to.equal('This is a test error');
expect(log.attributes).to.deep.equal({
'error.type': 'TestError',
'error.stack': '...stack trace...'
});
});

it('should handle an Error object with no stack trace', () => {
const error = new Error('error with no stack');
error.stack = undefined;

captureError(fakeTelemetry, error);

expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.severityNumber).to.equal(SeverityNumber.ERROR);
expect(log.body).to.equal('error with no stack');
expect(log.attributes).to.deep.equal({
'error.type': 'Error',
'error.stack': 'No stack trace available'
});
});

it('should capture a string error correctly', () => {
captureError(fakeTelemetry, 'a string error');

expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.severityNumber).to.equal(SeverityNumber.ERROR);
expect(log.body).to.equal('a string error');
expect(log.attributes).to.be.undefined;
});

it('should capture an unknown error type correctly', () => {
captureError(fakeTelemetry, 12345);

expect(emittedLogs.length).to.equal(1);
const log = emittedLogs[0];
expect(log.severityNumber).to.equal(SeverityNumber.ERROR);
expect(log.body).to.equal('Unknown error type: number');
expect(log.attributes).to.be.undefined;
});
});

describe('flush()', () => {
it('should flush logs correctly', async () => {
captureError(fakeTelemetry, 'error1');
captureError(fakeTelemetry, 'error2');

expect(emittedLogs.length).to.equal(2);

await flush(fakeTelemetry);

expect(emittedLogs.length).to.equal(0);
});
});
});
54 changes: 54 additions & 0 deletions packages/telemetry/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import { _getProvider, FirebaseApp, getApp } from '@firebase/app';
import { TELEMETRY_TYPE } from './constants';
import { Telemetry } from './public-types';
import { Provider } from '@firebase/component';
import { SeverityNumber } from '@opentelemetry/api-logs';
import { TelemetryService } from './service';

declare module '@firebase/component' {
interface NameServiceMapping {
[TELEMETRY_TYPE]: TelemetryService;
}
}

/**
* Returns the default {@link Telemetry} instance that is associated with the provided
Expand All @@ -44,3 +52,49 @@ export function getTelemetry(app: FirebaseApp = getApp()): Telemetry {

return telemetryProvider.getImmediate();
}

/**
* Enqueues an error to be uploaded to the Firebase Telemetry API.
*
* @public
*
* @param telemetry - The {@link Telemetry} instance.
* @param error - the caught exception, typically an {@link Error}
*/
export function captureError(telemetry: Telemetry, error: unknown): void {
const logger = telemetry.loggerProvider.getLogger('error-logger');
if (error instanceof Error) {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: error.message,
attributes: {
'error.type': error.name || 'Error',
'error.stack': error.stack || 'No stack trace available'
}
});
} else if (typeof error === 'string') {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: error
});
} else {
logger.emit({
severityNumber: SeverityNumber.ERROR,
body: `Unknown error type: ${typeof error}`
});
}
}

/**
* Flushes all enqueued telemetry data immediately, instead of waiting for default batching.
*
* @public
*
* @param telemetry - The {@link Telemetry} instance.
* @returns a promise which is resolved when all flushes are complete
*/
export function flush(telemetry: Telemetry): Promise<void> {
return telemetry.loggerProvider.forceFlush().catch(err => {
console.error('Error flushing logs from Firebase Telemetry:', err);
});
}
45 changes: 45 additions & 0 deletions packages/telemetry/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @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 {
LoggerProvider,
BatchLogRecordProcessor
} from '@opentelemetry/sdk-logs';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';

/**
* Create default logger provider.
*
* @internal
*/
export function createLoggerProvider(): LoggerProvider {
const resource = resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'firebase_telemetry_service'
});

const otlpEndpoint = process.env.OTEL_ENDPOINT;

const logExporter = new OTLPLogExporter({
url: `${otlpEndpoint}/api/v1/logs`
});
return new LoggerProvider({
resource,
processors: [new BatchLogRecordProcessor(logExporter)]
});
}
18 changes: 4 additions & 14 deletions packages/telemetry/src/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,11 @@
*/

import { _registerComponent, registerVersion } from '@firebase/app';
import { TestType } from './types/index';
import { Component, ComponentType } from '@firebase/component';
import { TELEMETRY_TYPE } from './constants';
import { name, version } from '../package.json';
import { TelemetryService } from './service';

export function testFxn(): number {
const _thing: TestType = {};
console.log('hi');
return 42;
}

declare module '@firebase/component' {
interface NameServiceMapping {
[TELEMETRY_TYPE]: TelemetryService;
}
}
import { createLoggerProvider } from './helpers';

export function registerTelemetry(): void {
_registerComponent(
Expand All @@ -41,7 +29,9 @@ export function registerTelemetry(): void {
container => {
// getImmediate for FirebaseApp will always succeed
const app = container.getProvider('app').getImmediate();
return new TelemetryService(app);
const loggerProvider = createLoggerProvider();

return new TelemetryService(app, loggerProvider);
},
ComponentType.PUBLIC
)
Expand Down
31 changes: 0 additions & 31 deletions packages/telemetry/src/index.test.ts

This file was deleted.

18 changes: 4 additions & 14 deletions packages/telemetry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,11 @@
*/

import { _registerComponent, registerVersion } from '@firebase/app';
import { TestType } from './types/index';
import { Component, ComponentType } from '@firebase/component';
import { TELEMETRY_TYPE } from './constants';
import { name, version } from '../package.json';
import { TelemetryService } from './service';

export function testFxn(): number {
const _thing: TestType = {};
console.log('hi');
return 42;
}

declare module '@firebase/component' {
interface NameServiceMapping {
[TELEMETRY_TYPE]: TelemetryService;
}
}
import { createLoggerProvider } from './helpers';

export function registerTelemetry(): void {
_registerComponent(
Expand All @@ -41,7 +29,9 @@ export function registerTelemetry(): void {
(container, {}) => {
// getImmediate for FirebaseApp will always succeed
const app = container.getProvider('app').getImmediate();
return new TelemetryService(app);
const loggerProvider = createLoggerProvider();

return new TelemetryService(app, loggerProvider);
},
ComponentType.PUBLIC
)
Expand Down
4 changes: 4 additions & 0 deletions packages/telemetry/src/public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { FirebaseApp } from '@firebase/app';
import { LoggerProvider } from '@opentelemetry/sdk-logs';

/**
* An instance of the Firebase Telemetry SDK.
Expand All @@ -29,4 +30,7 @@ export interface Telemetry {
* The {@link @firebase/app#FirebaseApp} this {@link Telemetry} instance is associated with.
*/
app: FirebaseApp;

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