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