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: 1 addition & 1 deletion docs/reference/functions/FirebaseAppProvider.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/reference/functions/preloadObservable.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/reference/functions/useFirebaseApp.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/reference/functions/useIsSuspenseEnabled.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/reference/functions/useObservable.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions docs/reference/interfaces/FirebaseAppProviderProps.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions docs/reference/interfaces/ObservableStatus.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/reference/variables/version.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 5 additions & 8 deletions src/firebaseApp.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { getApps, initializeApp, registerVersion } from 'firebase/app';
import { getApps, initializeApp } from 'firebase/app';
import { registerReactFireVersion, version } from './telemetry.js';

import type { FirebaseApp, FirebaseOptions } from 'firebase/app';

declare const process: { env: Record<string, string | undefined> };
export { version };

// INVESTIGATE I don't like magic strings, can we have export this in js-sdk?
const DEFAULT_APP_NAME = '[DEFAULT]';
Expand All @@ -18,15 +19,14 @@ export interface FirebaseAppProviderProps {
suspense?: boolean;
}

// REACTFIRE_VERSION is automatically pulled in from `package.json` by Vite
export const version = process.env.REACTFIRE_VERSION as string;

const shallowEq = (a: { [key: string]: any }, b: { [key: string]: any }) => a === b || [...Object.keys(a), ...Object.keys(b)].every((key) => a[key] === b[key]);

export function FirebaseAppProvider(props: React.PropsWithChildren<FirebaseAppProviderProps>) {
const { firebaseConfig, appName, suspense } = props;

const firebaseApp: FirebaseApp = React.useMemo(() => {
registerReactFireVersion();

if (props.firebaseApp) {
return props.firebaseApp;
}
Expand All @@ -45,9 +45,6 @@ export function FirebaseAppProvider(props: React.PropsWithChildren<FirebaseAppPr
throw new Error('No firebaseConfig provided');
}

const reactVersion = React.version || 'unknown';
registerVersion('react', reactVersion);
registerVersion('reactfire', version);
return initializeApp(firebaseConfig, appName);
}
}, [props.firebaseApp, firebaseConfig, appName]);
Expand Down
48 changes: 48 additions & 0 deletions src/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from 'react';
import * as firebaseAppModule from 'firebase/app';
import { registerVersion } from 'firebase/app';

declare const process: { env: Record<string, string | undefined> };

// Vite injects REACTFIRE_VERSION from package.json at build time.
export const version = process.env.REACTFIRE_VERSION as string;

let hasRegisteredFallback = false;

/**
* Registers the active React and ReactFire versions with `@firebase/app` for platform logging.
*
* Checks `_components` before calling `registerVersion` so repeated renders do not emit
* duplicate-registration debug logs under `setLogLevel('debug')`.
*
* Never throws: telemetry is best effort, so a partially mocked or mis-bundled `firebase/app`
* cannot break importing or rendering ReactFire.
*/
export function registerReactFireVersion(): void {
try {
const components = (firebaseAppModule as unknown as { _components?: unknown })._components;
const reactVersion = React.version || 'unknown';

if (components instanceof Map) {
if (!components.has('react-version')) {
registerVersion('react', reactVersion);
}
if (!components.has('reactfire-version')) {
registerVersion('reactfire', version);
}
return;
}

if (!hasRegisteredFallback) {
hasRegisteredFallback = true;
registerVersion('react', reactVersion);
registerVersion('reactfire', version);
}
} catch {
// Best effort: drop the error rather than fail the import or render.
}
}

// Register at module load so apps that initialize Firebase SDKs at module scope
// record React and ReactFire in the first daily heartbeat before React renders.
registerReactFireVersion();
3 changes: 3 additions & 0 deletions src/useObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
import { SuspenseSubject } from './SuspenseSubject.js';
import { useSuspenseEnabledFromConfigAndContext } from './firebaseApp.js';
import { ReactFireGlobals, ReactFireOptions } from './index.js';
import { registerReactFireVersion } from './telemetry.js';

const DEFAULT_TIMEOUT = 30_000;

Expand Down Expand Up @@ -69,6 +70,8 @@ export function useObservable<T = unknown>(observableId: string, source: Observa
throw new Error('cannot call useObservable without an observableId');
}

registerReactFireVersion();

const suspenseEnabled = useSuspenseEnabledFromConfigAndContext(config.suspense);

// Register the observable with the cache
Expand Down
101 changes: 100 additions & 1 deletion test/firebaseApp.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import { cleanup, render, waitFor, renderHook } from '@testing-library/react';
import { initializeApp, deleteApp, getApps } from 'firebase/app';
import * as firebaseAppModule from 'firebase/app';
import { initializeApp, deleteApp, getApps, FirebaseApp } from 'firebase/app';
import '@testing-library/jest-dom/extend-expect';
import * as React from 'react';
import { useFirebaseApp, FirebaseAppProvider, version } from '../src/index';
import pkg from '../package.json';

const _components = (firebaseAppModule as unknown as { _components: Map<string, unknown> })._components;

interface AppWithContainer extends FirebaseApp {
container: {
getProvider(name: 'platform-logger'): {
getImmediate(): { getPlatformInfoString(): string };
};
};
}

function getPlatformInfo(app = getApps()[0]): string {
return (app as AppWithContainer).container.getProvider('platform-logger').getImmediate().getPlatformInfoString();
}

// reactfire registers versions at import time, so start every test from a clean slate
// instead of relying on a previous test's afterEach.
beforeEach(() => {
_components.delete('react-version');
_components.delete('reactfire-version');
});

afterEach(() => {
cleanup();

getApps().forEach(deleteApp);
_components.delete('react-version');
_components.delete('reactfire-version');
});

const DEFAULT_APP_CONFIG = { appId: '12345' };
Expand Down Expand Up @@ -99,4 +123,79 @@ describe('reactfire version', () => {
it(`should match version ${pkg.version}`, () => {
expect(pkg.version).toEqual(version);
});

it('registers react and reactfire versions at module import time before any component renders', async () => {
_components.delete('react-version');
_components.delete('reactfire-version');
expect(_components.has('react-version')).toBe(false);
expect(_components.has('reactfire-version')).toBe(false);

vi.resetModules();
await import('../src/telemetry');

const app = initializeApp(DEFAULT_APP_CONFIG, 'module-load-test');
const info = getPlatformInfo(app);
expect(info).toContain(`react/${React.version}`);
expect(info).toContain(`reactfire/${version}`);
});

it('never throws when firebase/app fails to register versions', async () => {
const registerVersion = vi.fn(() => {
throw new Error('registerVersion failed');
});
vi.resetModules();
vi.doMock('firebase/app', () => ({ _components: new Map(), registerVersion }));

try {
// Importing runs the module-level registration, which must not throw either.
const { registerReactFireVersion } = await import('../src/telemetry');
expect(() => registerReactFireVersion()).not.toThrow();
expect(registerVersion).toHaveBeenCalled();
} finally {
vi.doUnmock('firebase/app');
vi.resetModules();
}
});

it('registers react and reactfire versions when initializing an app via firebaseConfig', () => {
expect(_components.has('react-version')).toBe(false);
expect(_components.has('reactfire-version')).toBe(false);

render(<FirebaseAppProvider firebaseConfig={DEFAULT_APP_CONFIG} />);

const info = getPlatformInfo();
expect(info).toContain(`react/${React.version}`);
expect(info).toContain(`reactfire/${version}`);
});

it('registers react and reactfire versions when passed an existing firebaseApp prop', () => {
const app = initializeApp(DEFAULT_APP_CONFIG, 'test');
expect(getPlatformInfo(app)).not.toContain(`reactfire/${version}`);

render(<FirebaseAppProvider firebaseApp={app} />);

const info = getPlatformInfo(app);
expect(info).toContain(`react/${React.version}`);
expect(info).toContain(`reactfire/${version}`);
});

it('registers react and reactfire versions when reusing an already-initialized app via firebaseConfig', () => {
const app = initializeApp(DEFAULT_APP_CONFIG);
expect(getPlatformInfo(app)).not.toContain(`reactfire/${version}`);

render(<FirebaseAppProvider firebaseConfig={DEFAULT_APP_CONFIG} />);

const info = getPlatformInfo(app);
expect(info).toContain(`react/${React.version}`);
expect(info).toContain(`reactfire/${version}`);
});

it('registers versions idempotently across multiple FirebaseAppProvider renders', () => {
const { rerender } = render(<FirebaseAppProvider firebaseConfig={DEFAULT_APP_CONFIG} />);
rerender(<FirebaseAppProvider firebaseApp={getApps()[0]} />);

const tokens = getPlatformInfo().split(' ');
expect(tokens.filter((t) => t === `react/${React.version}`)).toHaveLength(1);
expect(tokens.filter((t) => t === `reactfire/${version}`)).toHaveLength(1);
});
});
Loading
Loading