diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 155acb0c22..fce4d95d5f 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -112,6 +112,7 @@ "@firebase/app": "0.14.6", "@opentelemetry/sdk-trace-web": "2.1.0", "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-replace": "6.0.2", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.0", "@types/react": "19.1.13", diff --git a/packages/telemetry/rollup.config.js b/packages/telemetry/rollup.config.js index 5ffe384414..5408855d19 100644 --- a/packages/telemetry/rollup.config.js +++ b/packages/telemetry/rollup.config.js @@ -17,14 +17,25 @@ import json from '@rollup/plugin-json'; import copy from 'rollup-plugin-copy'; +import replacePlugin from '@rollup/plugin-replace'; import typescriptPlugin from 'rollup-plugin-typescript2'; import typescript from 'typescript'; import pkg from './package.json'; import { emitModulePackageFile } from '../../scripts/build/rollup_emit_module_package_file'; -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); +const deps = [ + ...Object.keys(Object.assign({}, pkg.peerDependencies, pkg.dependencies)), + './auto-constants' +]; + +function replaceSource(path) { + return replacePlugin({ + './src/auto-constants': `'${path}'`, + '../auto-constants': `'${path}'`, + delimiters: ["'", "'"], + preventAssignment: true + }); +} const buildPlugins = [typescriptPlugin({ typescript }), json()]; @@ -36,7 +47,7 @@ const browserBuilds = [ format: 'es', sourcemap: true }, - plugins: buildPlugins, + plugins: [...buildPlugins, replaceSource('./auto-constants.mjs')], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }, { @@ -46,7 +57,7 @@ const browserBuilds = [ format: 'cjs', sourcemap: true }, - plugins: buildPlugins, + plugins: [...buildPlugins, replaceSource('./auto-constants.js')], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } ]; @@ -59,7 +70,7 @@ const nodeBuilds = [ format: 'cjs', sourcemap: true }, - plugins: buildPlugins, + plugins: [...buildPlugins, replaceSource('./auto-constants.js')], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }, { @@ -69,7 +80,11 @@ const nodeBuilds = [ format: 'es', sourcemap: true }, - plugins: [...buildPlugins, emitModulePackageFile()], + plugins: [ + ...buildPlugins, + emitModulePackageFile(), + replaceSource('../auto-constants.mjs') + ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } ]; @@ -100,7 +115,8 @@ const reactBuilds = [ dest: 'dist' } ] - }) + }), + replaceSource('../auto-constants.mjs') ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }, @@ -117,7 +133,8 @@ const reactBuilds = [ typescript, tsconfig: 'tsconfig.react.json' }), - json() + json(), + replaceSource('../auto-constants.js') ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } @@ -148,7 +165,8 @@ const angularBuilds = [ dest: 'dist' } ] - }) + }), + replaceSource('../auto-constants.mjs') ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) }, @@ -164,15 +182,36 @@ const angularBuilds = [ typescript, tsconfig: 'tsconfig.angular.json' }), - json() + json(), + replaceSource('../auto-constants.js') ], external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } ]; +const autoinitBuild = [ + { + input: './src/auto-constants.ts', + output: { + file: './dist/auto-constants.js', + format: 'cjs' + }, + plugins: buildPlugins + }, + { + input: './src/auto-constants.ts', + output: { + file: './dist/auto-constants.mjs', + format: 'es' + }, + plugins: buildPlugins + } +]; + export default [ ...browserBuilds, ...nodeBuilds, ...reactBuilds, - ...angularBuilds + ...angularBuilds, + ...autoinitBuild ]; diff --git a/packages/telemetry/src/api.test.ts b/packages/telemetry/src/api.test.ts index b954987b69..e953da94d3 100644 --- a/packages/telemetry/src/api.test.ts +++ b/packages/telemetry/src/api.test.ts @@ -42,6 +42,7 @@ import { import { TelemetryService } from './service'; import { registerTelemetry } from './register'; import { _FirebaseInstallationsInternal } from '@firebase/installations'; +import { AUTO_CONSTANTS } from './auto-constants'; const PROJECT_ID = 'my-project'; const APP_ID = 'my-appid'; @@ -278,6 +279,7 @@ describe('Top level API', () => { }); it('should use explicit app version when provided', () => { + AUTO_CONSTANTS.appVersion = '1.2.3'; // Unused const telemetry = new TelemetryService( fakeTelemetry.app, fakeTelemetry.loggerProvider @@ -296,6 +298,19 @@ describe('Top level API', () => { }); }); + it('should use auto constants if available', () => { + AUTO_CONSTANTS.appVersion = '1.2.3'; + + captureError(fakeTelemetry, 'a string error'); + + expect(emittedLogs.length).to.equal(1); + const log = emittedLogs[0]; + expect(log.attributes).to.deep.equal({ + [LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: '1.2.3', + [LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID + }); + }); + describe('Session Metadata', () => { it('should generate and store a new session ID if none exists', () => { captureError(fakeTelemetry, 'error'); diff --git a/packages/telemetry/src/api.ts b/packages/telemetry/src/api.ts index f1f16896d8..e65039d5b5 100644 --- a/packages/telemetry/src/api.ts +++ b/packages/telemetry/src/api.ts @@ -16,16 +16,13 @@ */ import { _getProvider, FirebaseApp, getApp } from '@firebase/app'; -import { - LOG_ENTRY_ATTRIBUTE_KEYS, - TELEMETRY_SESSION_ID_KEY, - TELEMETRY_TYPE -} from './constants'; +import { LOG_ENTRY_ATTRIBUTE_KEYS, TELEMETRY_TYPE } from './constants'; import { Telemetry, TelemetryOptions } from './public-types'; import { Provider } from '@firebase/component'; import { AnyValueMap, SeverityNumber } from '@opentelemetry/api-logs'; import { trace } from '@opentelemetry/api'; import { TelemetryService } from './service'; +import { getAppVersion, getSessionId } from './helpers'; declare module '@firebase/component' { interface NameServiceMapping { @@ -97,28 +94,12 @@ export function captureError( } // Add app version metadata - let appVersion = 'unset'; - // TODO: implement app version fallback logic - if ((telemetry as TelemetryService).options?.appVersion) { - appVersion = (telemetry as TelemetryService).options!.appVersion!; - } - customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION] = appVersion; + customAttributes['app.version'] = getAppVersion(telemetry); // Add session ID metadata - if ( - typeof sessionStorage !== 'undefined' && - typeof crypto?.randomUUID === 'function' - ) { - try { - let sessionId = sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY); - if (!sessionId) { - sessionId = crypto.randomUUID(); - sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId); - } - customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID] = sessionId; - } catch (e) { - // Ignore errors accessing sessionStorage (e.g. security restrictions) - } + const sessionId = getSessionId(); + if (sessionId) { + customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID] = sessionId; } if (error instanceof Error) { diff --git a/packages/telemetry/src/auto-constants.ts b/packages/telemetry/src/auto-constants.ts new file mode 100644 index 0000000000..daff9b0d5d --- /dev/null +++ b/packages/telemetry/src/auto-constants.ts @@ -0,0 +1,24 @@ +/** + * @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. + */ + +/** + * A map of constants intended to be optionally overwritten during the application build process. + * The supported keys are: + * - appVersion: string indicating the version of source code being deployed (eg. git commit hash) + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const AUTO_CONSTANTS: any = {}; diff --git a/packages/telemetry/src/helpers.ts b/packages/telemetry/src/helpers.ts new file mode 100644 index 0000000000..64593816d1 --- /dev/null +++ b/packages/telemetry/src/helpers.ts @@ -0,0 +1,48 @@ +/** + * @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 * as constants from './auto-constants'; +import { TELEMETRY_SESSION_ID_KEY } from './constants'; +import { Telemetry } from './public-types'; +import { TelemetryService } from './service'; + +export function getAppVersion(telemetry: Telemetry): string { + if ((telemetry as TelemetryService).options?.appVersion) { + return (telemetry as TelemetryService).options!.appVersion!; + } else if (constants.AUTO_CONSTANTS?.appVersion) { + return constants.AUTO_CONSTANTS.appVersion; + } + return 'unset'; +} + +export function getSessionId(): string | undefined { + if ( + typeof sessionStorage !== 'undefined' && + typeof crypto?.randomUUID === 'function' + ) { + try { + let sessionId = sessionStorage.getItem(TELEMETRY_SESSION_ID_KEY); + if (!sessionId) { + sessionId = crypto.randomUUID(); + sessionStorage.setItem(TELEMETRY_SESSION_ID_KEY, sessionId); + } + return sessionId; + } catch (e) { + // Ignore errors accessing sessionStorage (e.g. security restrictions) + } + } +}