diff --git a/common/api-review/telemetry-angular.api.md b/common/api-review/telemetry-angular.api.md index 0b3198be1d..e171823ba8 100644 --- a/common/api-review/telemetry-angular.api.md +++ b/common/api-review/telemetry-angular.api.md @@ -9,7 +9,7 @@ import { FirebaseApp } from '@firebase/app'; // @public export class FirebaseErrorHandler implements ErrorHandler { - constructor(app: FirebaseApp, telemetryOptions?: TelemetryOptions | undefined); + constructor(app: FirebaseApp, telemetryOptions?: TelemetryOptions); // (undocumented) handleError(error: unknown): void; } diff --git a/docs-devsite/telemetry_angular.firebaseerrorhandler.md b/docs-devsite/telemetry_angular.firebaseerrorhandler.md index 370fc1af6b..5a80b49ac5 100644 --- a/docs-devsite/telemetry_angular.firebaseerrorhandler.md +++ b/docs-devsite/telemetry_angular.firebaseerrorhandler.md @@ -40,7 +40,7 @@ Constructs a new instance of the `FirebaseErrorHandler` class Signature: ```typescript -constructor(app: FirebaseApp, telemetryOptions?: TelemetryOptions | undefined); +constructor(app: FirebaseApp, telemetryOptions?: TelemetryOptions); ``` #### Parameters @@ -48,7 +48,7 @@ constructor(app: FirebaseApp, telemetryOptions?: TelemetryOptions | undefined); | Parameter | Type | Description | | --- | --- | --- | | app | [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | | -| telemetryOptions | [TelemetryOptions](./telemetry_.telemetryoptions.md#telemetryoptions_interface) \| undefined | | +| telemetryOptions | [TelemetryOptions](./telemetry_.telemetryoptions.md#telemetryoptions_interface) | | ## FirebaseErrorHandler.handleError() diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index a3cf827210..5e7d53b261 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -111,6 +111,7 @@ "@angular/platform-browser": "19.2.15", "@angular/router": "19.2.15", "@firebase/app": "0.14.6", + "@firebase/util": "1.13.0", "@opentelemetry/sdk-trace-web": "2.1.0", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", diff --git a/packages/telemetry/src/api.ts b/packages/telemetry/src/api.ts index 1a80a32505..8b80ac76ec 100644 --- a/packages/telemetry/src/api.ts +++ b/packages/telemetry/src/api.ts @@ -22,7 +22,7 @@ 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'; +import { flush, getAppVersion, getSessionId } from './helpers'; import { TelemetryInternal } from './types'; declare module '@firebase/component' { @@ -132,19 +132,4 @@ export function captureError( } } -/** - * 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 { - // Cast to TelemetryInternal to access internal loggerProvider - return (telemetry as TelemetryInternal).loggerProvider - .forceFlush() - .catch(err => { - console.error('Error flushing logs from Firebase Telemetry:', err); - }); -} +export { flush }; diff --git a/packages/telemetry/src/helpers.test.ts b/packages/telemetry/src/helpers.test.ts index dbd450e89a..de87404cdc 100644 --- a/packages/telemetry/src/helpers.test.ts +++ b/packages/telemetry/src/helpers.test.ts @@ -18,7 +18,8 @@ import { expect } from 'chai'; import { LoggerProvider } from '@opentelemetry/sdk-logs'; import { Logger, LogRecord } from '@opentelemetry/api-logs'; -import { startNewSession } from './helpers'; +import { isNode } from '@firebase/util'; +import { registerListeners, startNewSession } from './helpers'; import { LOG_ENTRY_ATTRIBUTE_KEYS, TELEMETRY_SESSION_ID_KEY @@ -34,6 +35,7 @@ describe('helpers', () => { let originalCrypto: Crypto | undefined; let storage: Record = {}; let emittedLogs: LogRecord[] = []; + let flushed = false; const fakeLoggerProvider = { getLogger: (): Logger => { @@ -43,7 +45,10 @@ describe('helpers', () => { } }; }, - forceFlush: () => Promise.resolve(), + forceFlush: () => { + flushed = true; + return Promise.resolve(); + }, shutdown: () => Promise.resolve() } as unknown as LoggerProvider; @@ -61,6 +66,7 @@ describe('helpers', () => { beforeEach(() => { emittedLogs = []; + flushed = false; storage = {}; // @ts-ignore originalSessionStorage = global.sessionStorage; @@ -96,6 +102,12 @@ describe('helpers', () => { value: originalCrypto, writable: true }); + if (!isNode()) { + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + writable: true + }); + } delete AUTO_CONSTANTS.appVersion; }); @@ -136,4 +148,36 @@ describe('helpers', () => { }); }); }); + + describe('registerListeners', () => { + if (isNode()) { + it('should do nothing in node', () => { + registerListeners(fakeTelemetry); + }); + } else { + it('should flush logs when the visibility changes to hidden', () => { + registerListeners(fakeTelemetry); + + expect(flushed).to.be.false; + + Object.defineProperty(document, 'visibilityState', { + value: 'hidden', + writable: true + }); + window.dispatchEvent(new Event('visibilitychange')); + + expect(flushed).to.be.true; + }); + + it('should flush logs when the pagehide event fires', () => { + registerListeners(fakeTelemetry); + + expect(flushed).to.be.false; + + window.dispatchEvent(new Event('pagehide')); + + expect(flushed).to.be.true; + }); + } + }); }); diff --git a/packages/telemetry/src/helpers.ts b/packages/telemetry/src/helpers.ts index 0603f8dcec..be9a3662c1 100644 --- a/packages/telemetry/src/helpers.ts +++ b/packages/telemetry/src/helpers.ts @@ -81,3 +81,37 @@ export function startNewSession(telemetry: Telemetry): void { } } } + +/** + * Registers event listeners to flush logs when the page is hidden. In some cases multiple listeners + * may trigger at the same time, but flushing only occurs once per batch. + */ +export function registerListeners(telemetry: Telemetry): void { + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + window.addEventListener('visibilitychange', async () => { + if (document.visibilityState === 'hidden') { + await flush(telemetry); + } + }); + window.addEventListener('pagehide', async () => { + await flush(telemetry); + }); + } +} + +/** + * 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 { + // Cast to TelemetryInternal to access internal loggerProvider + return (telemetry as TelemetryInternal).loggerProvider + .forceFlush() + .catch(err => { + console.error('Error flushing logs from Firebase Telemetry:', err); + }); +} diff --git a/packages/telemetry/src/register.ts b/packages/telemetry/src/register.ts index 7bfeb05b91..1ed2b7d036 100644 --- a/packages/telemetry/src/register.ts +++ b/packages/telemetry/src/register.ts @@ -23,11 +23,11 @@ import { createLoggerProvider } from './logging/logger-provider'; import { AppCheckProvider } from './logging/appcheck-provider'; import { InstallationIdProvider } from './logging/installation-id-provider'; import { TELEMETRY_TYPE } from './constants'; +import { getSessionId, registerListeners, startNewSession } from './helpers'; // 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'; -import { getSessionId, startNewSession } from './helpers'; export function registerTelemetry(): void { _registerComponent( @@ -65,6 +65,9 @@ export function registerTelemetry(): void { startNewSession(telemetryService); } + // Register relevant event listeners + registerListeners(telemetryService); + return telemetryService; }, ComponentType.PUBLIC