Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add singleton support for browser-telemetry. #739

Merged
merged 2 commits into from
Jan 21, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { fallbackLogger } from '../../src/logging';
import { getTelemetryInstance, initTelemetry, resetTelemetryInstance } from '../../src/singleton';

beforeEach(() => {
resetTelemetryInstance();
jest.resetAllMocks();
});

it('warns and keeps existing instance when initialized multiple times', () => {
const mockLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};

initTelemetry({ logger: mockLogger });
const instanceA = getTelemetryInstance();
initTelemetry({ logger: mockLogger });
const instanceB = getTelemetryInstance();

expect(mockLogger.warn).toHaveBeenCalledWith(
expect.stringMatching(/Telemetry has already been initialized/),
);

expect(instanceA).toBe(instanceB);
});

it('warns when getting telemetry instance before initialization', () => {
const spy = jest.spyOn(fallbackLogger, 'warn');

getTelemetryInstance();

expect(spy).toHaveBeenCalledWith(expect.stringMatching(/Telemetry has not been initialized/));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { LDInspection } from '@launchdarkly/js-client-sdk';

import { Breadcrumb, LDClientTracking } from '../../src/api';
import { BrowserTelemetry } from '../../src/api/BrowserTelemetry';
import { getTelemetryInstance } from '../../src/singleton/singletonInstance';
import {
addBreadcrumb,
captureError,
captureErrorEvent,
close,
inspectors,
register,
} from '../../src/singleton/singletonMethods';

jest.mock('../../src/singleton/singletonInstance');

const mockTelemetry: jest.Mocked<BrowserTelemetry> = {
inspectors: jest.fn(),
captureError: jest.fn(),
captureErrorEvent: jest.fn(),
addBreadcrumb: jest.fn(),
register: jest.fn(),
close: jest.fn(),
};

const mockGetTelemetryInstance = getTelemetryInstance as jest.Mock;

beforeEach(() => {
jest.resetAllMocks();
});

it('returns empty array when telemetry is not initialized for inspectors', () => {
mockGetTelemetryInstance.mockReturnValue(undefined);
expect(() => inspectors()).not.toThrow();
expect(inspectors()).toEqual([]);
});

it('returns inspectors when telemetry is initialized', () => {
const mockInspectors: LDInspection[] = [
{ name: 'test-inspector', type: 'flag-used', method: () => {} },
];
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
mockTelemetry.inspectors.mockReturnValue(mockInspectors);

expect(inspectors()).toBe(mockInspectors);
});

it('does not crash when calling captureError with no telemetry instance', () => {
mockGetTelemetryInstance.mockReturnValue(undefined);
const error = new Error('test error');

expect(() => captureError(error)).not.toThrow();

expect(mockTelemetry.captureError).not.toHaveBeenCalled();
});

it('captures errors when telemetry is initialized', () => {
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
const error = new Error('test error');

captureError(error);

expect(mockTelemetry.captureError).toHaveBeenCalledWith(error);
});

it('it does not crash when calling captureErrorEvent with no telemetry instance', () => {
mockGetTelemetryInstance.mockReturnValue(undefined);
const errorEvent = new ErrorEvent('error', { error: new Error('test error') });

expect(() => captureErrorEvent(errorEvent)).not.toThrow();

expect(mockTelemetry.captureErrorEvent).not.toHaveBeenCalled();
});

it('captures error event when telemetry is initialized', () => {
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
const errorEvent = new ErrorEvent('error', { error: new Error('test error') });

captureErrorEvent(errorEvent);

expect(mockTelemetry.captureErrorEvent).toHaveBeenCalledWith(errorEvent);
});

it('does not crash when calling addBreadcrumb with no telemetry instance', () => {
mockGetTelemetryInstance.mockReturnValue(undefined);
const breadcrumb: Breadcrumb = {
type: 'custom',
data: { test: 'data' },
timestamp: Date.now(),
class: 'custom',
level: 'info',
};

expect(() => addBreadcrumb(breadcrumb)).not.toThrow();

expect(mockTelemetry.addBreadcrumb).not.toHaveBeenCalled();
});

it('adds breadcrumb when telemetry is initialized', () => {
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
const breadcrumb: Breadcrumb = {
type: 'custom',
data: { test: 'data' },
timestamp: Date.now(),
class: 'custom',
level: 'info',
};

addBreadcrumb(breadcrumb);

expect(mockTelemetry.addBreadcrumb).toHaveBeenCalledWith(breadcrumb);
});

it('does not crash when calling register with no telemetry instance', () => {
mockGetTelemetryInstance.mockReturnValue(undefined);
const mockClient: jest.Mocked<LDClientTracking> = {
track: jest.fn(),
};

expect(() => register(mockClient)).not.toThrow();

expect(mockTelemetry.register).not.toHaveBeenCalled();
});

it('registers client when telemetry is initialized', () => {
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);
const mockClient: jest.Mocked<LDClientTracking> = {
track: jest.fn(),
};

register(mockClient);

expect(mockTelemetry.register).toHaveBeenCalledWith(mockClient);
});

it('does not crash when calling close with no telemetry instance', () => {
mockGetTelemetryInstance.mockReturnValue(undefined);

expect(() => close()).not.toThrow();

expect(mockTelemetry.close).not.toHaveBeenCalled();
});

it('closes when telemetry is initialized', () => {
mockGetTelemetryInstance.mockReturnValue(mockTelemetry);

close();

expect(mockTelemetry.close).toHaveBeenCalled();
});
15 changes: 14 additions & 1 deletion packages/telemetry/browser-telemetry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,20 @@ import parse from './options';

export * from './api';

export function initializeTelemetry(options?: Options): BrowserTelemetry {
export * from './singleton';

/**
* Initialize a new telemetry instance.
*
* This instance is not global. Generally developers should use {@link initializeTelemetry} instead.
*
* If for some reason multiple telemetry instances are needed, this method can be used to create a new instance.
* Instances are not aware of each other and may send duplicate data from automatically captured events.
*
* @param options The options to use for the telemetry instance.
* @returns A telemetry instance.
*/
export function initializeTelemetryInstance(options?: Options): BrowserTelemetry {
const parsedOptions = parse(options || {}, safeMinLogger(options?.logger));
return new BrowserTelemetryImpl(parsedOptions);
}
2 changes: 2 additions & 0 deletions packages/telemetry/browser-telemetry/src/singleton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './singletonInstance';
export * from './singletonMethods';
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Options } from '../api';
import { BrowserTelemetry } from '../api/BrowserTelemetry';
import BrowserTelemetryImpl from '../BrowserTelemetryImpl';
import { fallbackLogger, prefixLog, safeMinLogger } from '../logging';
import parse from '../options';

let telemetryInstance: BrowserTelemetry | undefined;
let warnedClientNotInitialized: boolean = false;

/**
* Initialize the LaunchDarkly telemetry client
*
* This method should be called one time as early as possible in the application lifecycle.
*
* @example
* ```
* import { initTelemetry } from '@launchdarkly/browser-telemetry';
*
* initTelemetry();
* ```
*
* After initialization the telemetry client must be registered with the LaunchDarkly SDK client.
*
* @example
* ```
* import { initTelemetry, register } from '@launchdarkly/browser-telemetry';
*
* initTelemetry();
*
* // Create your LaunchDarkly client following the LaunchDarkly SDK documentation.
*
* register(ldClient);
* ```
*
* If using the 3.x version of the LaunchDarkly SDK, then you must also add inspectors when intializing your LaunchDarkly client.
kinyoklion marked this conversation as resolved.
Show resolved Hide resolved
* This allows for integration with feature flag data.
*
* @example
* ```
* import { initTelemetry, register, inspectors } from '@launchdarkly/browser-telemetry';
* import { init } from 'launchdarkly-js-client-sdk';
*
* initTelemetry();
*
* const ldClient = init('YOUR_CLIENT_SIDE_ID', {
* inspectors: inspectors()
* });
*
* register(ldClient);
* ```
*
* @param options The options to use for the telemetry instance. Refer to {@link Options} for more information.
*/
export function initTelemetry(options?: Options) {
const logger = safeMinLogger(options?.logger);

if (telemetryInstance) {
logger.warn(prefixLog('Telemetry has already been initialized. Ignoring new options.'));
return;
}

const parsedOptions = parse(options || {}, logger);
telemetryInstance = new BrowserTelemetryImpl(parsedOptions);
}

/**
* Get the telemetry instance.
*
* In typical operation this method doesn't need to be called. Instead the functions exported by this package directly
* use the telemetry instance.
*
* This function can be used when the telemetry instance needs to be injected into code instead of accessed globally.
*
* @returns The telemetry instance, or undefined if it has not been initialized.
*/
export function getTelemetryInstance(): BrowserTelemetry | undefined {
if (!telemetryInstance) {
if (warnedClientNotInitialized) {
return undefined;
}

fallbackLogger.warn(prefixLog('Telemetry has not been initialized'));
warnedClientNotInitialized = true;
return undefined;
}

return telemetryInstance;
}

/**
* Reset the telemetry instance to its initial state.
*
* This method is intended to be used in tests.
*
* @internal
*/
export function resetTelemetryInstance() {
telemetryInstance = undefined;
warnedClientNotInitialized = false;
}
Loading
Loading