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
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.
*
* @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