diff --git a/packages/sdk/browser/__tests__/BrowserClient.test.ts b/packages/sdk/browser/__tests__/BrowserClient.test.ts new file mode 100644 index 0000000000..b32bbac169 --- /dev/null +++ b/packages/sdk/browser/__tests__/BrowserClient.test.ts @@ -0,0 +1,272 @@ +import { jest } from '@jest/globals'; + +import { + AutoEnvAttributes, + EventSourceCapabilities, + EventSourceInitDict, + Hasher, + LDLogger, + PlatformData, + Requests, + SdkData, +} from '@launchdarkly/js-client-sdk-common'; + +import { BrowserClient } from '../src/BrowserClient'; + +function mockResponse(value: string, statusCode: number) { + const response: Response = { + headers: { + // @ts-ignore + get: jest.fn(), + // @ts-ignore + keys: jest.fn(), + // @ts-ignore + values: jest.fn(), + // @ts-ignore + entries: jest.fn(), + // @ts-ignore + has: jest.fn(), + }, + status: statusCode, + text: () => Promise.resolve(value), + json: () => Promise.resolve(JSON.parse(value)), + }; + return Promise.resolve(response); +} + +function mockFetch(value: string, statusCode: number = 200) { + const f = jest.fn(); + // @ts-ignore + f.mockResolvedValue(mockResponse(value, statusCode)); + return f; +} + +function makeRequests(): Requests { + return { + // @ts-ignore + fetch: jest.fn((url: string, _options: any) => { + if (url.includes('/sdk/goals/')) { + return mockFetch( + JSON.stringify([ + { + key: 'pageview', + kind: 'pageview', + urls: [{ kind: 'exact', url: 'http://browserclientintegration.com' }], + }, + { + key: 'click', + kind: 'click', + selector: '.button', + urls: [{ kind: 'exact', url: 'http://browserclientintegration.com' }], + }, + ]), + 200, + )(); + } + return mockFetch('{ "flagA": true }', 200)(); + }), + // @ts-ignore + createEventSource(_url: string, _eventSourceInitDict: EventSourceInitDict): EventSource { + throw new Error('Function not implemented.'); + }, + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: false, + headers: false, + customMethod: false, + }; + }, + }; +} + +class MockHasher implements Hasher { + update(_data: string): Hasher { + return this; + } + digest?(_encoding: string): string { + return 'hashed'; + } + async asyncDigest?(_encoding: string): Promise { + return 'hashed'; + } +} + +describe('given a mock platform for a BrowserClient', () => { + const logger: LDLogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + let platform: any; + beforeEach(() => { + Object.defineProperty(window, 'location', { + value: { href: 'http://browserclientintegration.com' }, + writable: true, + }); + jest.useFakeTimers().setSystemTime(new Date('2024-09-19')); + platform = { + requests: makeRequests(), + info: { + platformData(): PlatformData { + return { + name: 'node', + }; + }, + sdkData(): SdkData { + return { + name: 'browser-sdk', + version: '1.0.0', + }; + }, + }, + crypto: { + createHash: () => new MockHasher(), + randomUUID: () => '123', + }, + storage: { + get: async (_key: string) => null, + set: async (_key: string, _value: string) => {}, + clear: async (_key: string) => {}, + }, + encoding: { + btoa: (str: string) => str, + }, + }; + }); + + it('includes urls in custom events', async () => { + const client = new BrowserClient( + 'client-side-id', + AutoEnvAttributes.Disabled, + { + initialConnectionMode: 'polling', + logger, + diagnosticOptOut: true, + }, + platform, + ); + await client.identify({ key: 'user-key', kind: 'user' }); + await client.flush(); + client.track('user-key', undefined, 1); + await client.flush(); + + expect(JSON.parse(platform.requests.fetch.mock.calls[3][1].body)[0]).toMatchObject({ + kind: 'custom', + creationDate: 1726704000000, + key: 'user-key', + contextKeys: { + user: 'user-key', + }, + metricValue: 1, + url: 'http://browserclientintegration.com', + }); + }); + + it('can filter URLs in custom events', async () => { + const client = new BrowserClient( + 'client-side-id', + AutoEnvAttributes.Disabled, + { + initialConnectionMode: 'polling', + logger, + diagnosticOptOut: true, + eventUrlTransformer: (url: string) => + url.replace('http://browserclientintegration.com', 'http://filtered.org'), + }, + platform, + ); + await client.identify({ key: 'user-key', kind: 'user' }); + await client.flush(); + client.track('user-key', undefined, 1); + await client.flush(); + + const events = JSON.parse(platform.requests.fetch.mock.calls[3][1].body); + const customEvent = events.find((e: any) => e.kind === 'custom'); + + expect(customEvent).toMatchObject({ + kind: 'custom', + creationDate: 1726704000000, + key: 'user-key', + contextKeys: { + user: 'user-key', + }, + metricValue: 1, + url: 'http://filtered.org', + }); + }); + + it('can filter URLs in click events', async () => { + const client = new BrowserClient( + 'client-side-id', + AutoEnvAttributes.Disabled, + { + initialConnectionMode: 'polling', + logger, + diagnosticOptOut: true, + eventUrlTransformer: (url: string) => + url.replace('http://browserclientintegration.com', 'http://filtered.org'), + }, + platform, + ); + await client.identify({ key: 'user-key', kind: 'user' }); + await client.flush(); + + // Simulate a click event + const button = document.createElement('button'); + button.className = 'button'; + document.body.appendChild(button); + button.click(); + + while (platform.requests.fetch.mock.calls.length < 4) { + // eslint-disable-next-line no-await-in-loop + await client.flush(); + jest.runAllTicks(); + } + + const events = JSON.parse(platform.requests.fetch.mock.calls[3][1].body); + const clickEvent = events.find((e: any) => e.kind === 'click'); + expect(clickEvent).toMatchObject({ + kind: 'click', + creationDate: 1726704000000, + key: 'click', + contextKeys: { + user: 'user-key', + }, + url: 'http://filtered.org', + }); + + document.body.removeChild(button); + }); + + it('can filter URLs in pageview events', async () => { + const client = new BrowserClient( + 'client-side-id', + AutoEnvAttributes.Disabled, + { + initialConnectionMode: 'polling', + logger, + diagnosticOptOut: true, + eventUrlTransformer: (url: string) => + url.replace('http://browserclientintegration.com', 'http://filtered.com'), + }, + platform, + ); + + await client.identify({ key: 'user-key', kind: 'user' }); + await client.flush(); + + const events = JSON.parse(platform.requests.fetch.mock.calls[2][1].body); + const pageviewEvent = events.find((e: any) => e.kind === 'pageview'); + expect(pageviewEvent).toMatchObject({ + kind: 'pageview', + creationDate: 1726704000000, + key: 'pageview', + contextKeys: { + user: 'user-key', + }, + url: 'http://filtered.com', + }); + }); +}); diff --git a/packages/sdk/browser/__tests__/goals/GoalManager.test.ts b/packages/sdk/browser/__tests__/goals/GoalManager.test.ts index 819528aa69..acdd6e5b1f 100644 --- a/packages/sdk/browser/__tests__/goals/GoalManager.test.ts +++ b/packages/sdk/browser/__tests__/goals/GoalManager.test.ts @@ -47,6 +47,7 @@ describe('given a GoalManager with mocked dependencies', () => { } as any); await goalManager.initialize(); + goalManager.startTracking(); expect(mockRequests.fetch).toHaveBeenCalledWith('polling/sdk/goals/test-credential'); expect(mockLocationWatcherFactory).toHaveBeenCalled(); @@ -58,6 +59,7 @@ describe('given a GoalManager with mocked dependencies', () => { mockRequests.fetch.mockRejectedValue(error); await goalManager.initialize(); + goalManager.startTracking(); expect(mockReportError).toHaveBeenCalledWith(expect.any(LDUnexpectedResponseError)); }); @@ -91,6 +93,7 @@ describe('given a GoalManager with mocked dependencies', () => { json: () => Promise.resolve(mockGoals), } as any); await goalManager.initialize(); + goalManager.startTracking(); // Check that no goal was emitted on initial load expect(mockReportGoal).not.toHaveBeenCalled(); diff --git a/packages/sdk/browser/__tests__/goals/LocationWatcher.test.ts b/packages/sdk/browser/__tests__/goals/LocationWatcher.test.ts index 561419d07a..d278402ac1 100644 --- a/packages/sdk/browser/__tests__/goals/LocationWatcher.test.ts +++ b/packages/sdk/browser/__tests__/goals/LocationWatcher.test.ts @@ -1,6 +1,9 @@ import { jest } from '@jest/globals'; -import { DefaultLocationWatcher, LOCATION_WATCHER_INTERVAL } from '../../src/goals/LocationWatcher'; +import { + DefaultLocationWatcher, + LOCATION_WATCHER_INTERVAL_MS, +} from '../../src/goals/LocationWatcher'; let mockCallback: jest.Mock; @@ -25,7 +28,7 @@ it('should call callback when URL changes', () => { value: { href: 'https://example.com/new-page' }, writable: true, }); - jest.advanceTimersByTime(LOCATION_WATCHER_INTERVAL); + jest.advanceTimersByTime(LOCATION_WATCHER_INTERVAL_MS); expect(mockCallback).toHaveBeenCalledTimes(1); @@ -40,7 +43,7 @@ it('should not call callback when URL remains the same', () => { const watcher = new DefaultLocationWatcher(mockCallback); - jest.advanceTimersByTime(LOCATION_WATCHER_INTERVAL * 2); + jest.advanceTimersByTime(LOCATION_WATCHER_INTERVAL_MS * 2); expect(mockCallback).not.toHaveBeenCalled(); @@ -80,7 +83,7 @@ it('should stop watching when close is called', () => { value: { href: 'https://example.com/new-page' }, writable: true, }); - jest.advanceTimersByTime(LOCATION_WATCHER_INTERVAL); + jest.advanceTimersByTime(LOCATION_WATCHER_INTERVAL_MS); window.dispatchEvent(new Event('popstate')); expect(mockCallback).not.toHaveBeenCalled(); diff --git a/packages/sdk/browser/__tests__/options.test.ts b/packages/sdk/browser/__tests__/options.test.ts index bbeee2fde8..cb1d84a67f 100644 --- a/packages/sdk/browser/__tests__/options.test.ts +++ b/packages/sdk/browser/__tests__/options.test.ts @@ -54,7 +54,7 @@ it('applies default options', () => { const opts = validateOptions({}, logger); expect(opts.fetchGoals).toBe(true); - expect(opts.eventUrlTransformer).toBeUndefined(); + expect(opts.eventUrlTransformer).toBeDefined(); expect(logger.debug).not.toHaveBeenCalled(); expect(logger.info).not.toHaveBeenCalled(); diff --git a/packages/sdk/browser/jest.config.js b/packages/sdk/browser/jest.config.js index 523d4a99d5..21105621a4 100644 --- a/packages/sdk/browser/jest.config.js +++ b/packages/sdk/browser/jest.config.js @@ -4,7 +4,7 @@ export default { preset: 'ts-jest/presets/default-esm', testEnvironment: 'jest-environment-jsdom', transform: { - '^.+\\.tsx?$': ['ts-jest', { useESM: true }], + '^.+\\.tsx?$': ['ts-jest', { useESM: true, tsconfig: 'tsconfig.json' }], }, testPathIgnorePatterns: ['./dist'], }; diff --git a/packages/sdk/browser/package.json b/packages/sdk/browser/package.json index 367528e2b4..c03638f278 100644 --- a/packages/sdk/browser/package.json +++ b/packages/sdk/browser/package.json @@ -30,7 +30,7 @@ "build": "rollup -c rollup.config.js", "lint": "eslint . --ext .ts,.tsx", "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore", - "test": "NODE_OPTIONS=--experimental-vm-modules npx jest", + "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --runInBand", "coverage": "yarn test --coverage", "check": "yarn prettier && yarn lint && yarn build && yarn test" }, diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index f248119012..a275fddccd 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -5,8 +5,10 @@ import { LDClient as CommonClient, DataSourcePaths, Encoding, + internal, LDClientImpl, LDContext, + Platform, } from '@launchdarkly/js-client-sdk-common'; import GoalManager from './goals/GoalManager'; @@ -25,6 +27,7 @@ export class BrowserClient extends LDClientImpl { private readonly clientSideId: string, autoEnvAttributes: AutoEnvAttributes, options: BrowserOptions = {}, + overridePlatform?: Platform, ) { const { logger: customLogger, debug } = options; const logger = @@ -38,14 +41,24 @@ export class BrowserClient extends LDClientImpl { // TODO: Use the already-configured baseUri from the SDK config. SDK-560 const baseUrl = options.baseUri ?? 'https://clientsdk.launchdarkly.com'; - const platform = new BrowserPlatform(options); + const platform = overridePlatform ?? new BrowserPlatform(logger); const ValidatedBrowserOptions = validateOptions(options, logger); + const { eventUrlTransformer } = ValidatedBrowserOptions; super(clientSideId, autoEnvAttributes, platform, filterToBaseOptions(options), { analyticsEventPath: `/events/bulk/${clientSideId}`, diagnosticEventPath: `/events/diagnostic/${clientSideId}`, includeAuthorizationHeader: false, highTimeoutThreshold: 5, userAgentHeaderName: 'x-launchdarkly-user-agent', + trackEventModifier: (event: internal.InputCustomEvent) => + new internal.InputCustomEvent( + event.context, + event.key, + event.data, + event.metricValue, + event.samplingRatio, + eventUrlTransformer(window.location.href), + ), }); if (ValidatedBrowserOptions.fetchGoals) { @@ -62,10 +75,11 @@ export class BrowserClient extends LDClientImpl { if (!context) { return; } + const transformedUrl = eventUrlTransformer(url); if (isClick(goal)) { this.sendEvent({ kind: 'click', - url, + url: transformedUrl, samplingRatio: 1, key: goal.key, creationDate: Date.now(), @@ -75,7 +89,7 @@ export class BrowserClient extends LDClientImpl { } else { this.sendEvent({ kind: 'pageview', - url, + url: transformedUrl, samplingRatio: 1, key: goal.key, creationDate: Date.now(), @@ -120,4 +134,9 @@ export class BrowserClient extends LDClientImpl { }, }; } + + override async identify(context: LDContext): Promise { + await super.identify(context); + this.goalManager?.startTracking(); + } } diff --git a/packages/sdk/browser/src/goals/GoalManager.ts b/packages/sdk/browser/src/goals/GoalManager.ts index eecd21f920..50932dd0cd 100644 --- a/packages/sdk/browser/src/goals/GoalManager.ts +++ b/packages/sdk/browser/src/goals/GoalManager.ts @@ -9,6 +9,7 @@ export default class GoalManager { private url: string; private watcher?: LocationWatcher; private tracker?: GoalTracker; + private isTracking = false; constructor( credential: string, @@ -29,10 +30,20 @@ export default class GoalManager { public async initialize(): Promise { await this.fetchGoals(); + // If tracking has been started before goal fetching completes, we need to + // create the tracker so it can start watching for events. + this.createTracker(); + } + + public startTracking() { + this.isTracking = true; this.createTracker(); } private createTracker() { + if (!this.isTracking) { + return; + } this.tracker?.close(); if (this.goals && this.goals.length) { this.tracker = new GoalTracker(this.goals, (goal) => { diff --git a/packages/sdk/browser/src/goals/LocationWatcher.ts b/packages/sdk/browser/src/goals/LocationWatcher.ts index 4343f644e3..7b6c5e3de1 100644 --- a/packages/sdk/browser/src/goals/LocationWatcher.ts +++ b/packages/sdk/browser/src/goals/LocationWatcher.ts @@ -1,4 +1,4 @@ -export const LOCATION_WATCHER_INTERVAL = 300; +export const LOCATION_WATCHER_INTERVAL_MS = 300; // Using any for the timer handle because the type is not the same for all // platforms and we only need to use it opaquely. @@ -39,7 +39,7 @@ export class DefaultLocationWatcher { * Details on when popstate is called: * https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#when_popstate_is_sent */ - this.watcherHandle = setInterval(checkUrl, LOCATION_WATCHER_INTERVAL); + this.watcherHandle = setInterval(checkUrl, LOCATION_WATCHER_INTERVAL_MS); window.addEventListener('popstate', checkUrl); diff --git a/packages/sdk/browser/src/index.ts b/packages/sdk/browser/src/index.ts index 26015a6744..26f5e703b9 100644 --- a/packages/sdk/browser/src/index.ts +++ b/packages/sdk/browser/src/index.ts @@ -18,10 +18,8 @@ import { import { BrowserClient, LDClient } from './BrowserClient'; import { BrowserOptions as LDOptions } from './options'; -// TODO: Export and use browser specific options. export { LDClient, - AutoEnvAttributes, LDFlagSet, LDContext, LDContextCommon, @@ -36,10 +34,7 @@ export { LDEvaluationReason, }; -export function init( - clientSideId: string, - autoEnvAttributes: AutoEnvAttributes, - options?: LDOptions, -): LDClient { - return new BrowserClient(clientSideId, autoEnvAttributes, options); +export function init(clientSideId: string, options?: LDOptions): LDClient { + // AutoEnvAttributes are not supported yet in the browser SDK. + return new BrowserClient(clientSideId, AutoEnvAttributes.Disabled, options); } diff --git a/packages/sdk/browser/src/options.ts b/packages/sdk/browser/src/options.ts index c0d62c549c..d08eb53b2a 100644 --- a/packages/sdk/browser/src/options.ts +++ b/packages/sdk/browser/src/options.ts @@ -28,12 +28,12 @@ export interface BrowserOptions extends LDOptionsBase { export interface ValidatedOptions { fetchGoals: boolean; - eventUrlTransformer?: (url: string) => string; + eventUrlTransformer: (url: string) => string; } const optDefaults = { fetchGoals: true, - eventUrlTransformer: undefined, + eventUrlTransformer: (url: string) => url, }; const validators: { [Property in keyof BrowserOptions]: TypeValidator | undefined } = { diff --git a/packages/sdk/browser/src/platform/BrowserInfo.ts b/packages/sdk/browser/src/platform/BrowserInfo.ts index 637ebad50b..3a2c064fba 100644 --- a/packages/sdk/browser/src/platform/BrowserInfo.ts +++ b/packages/sdk/browser/src/platform/BrowserInfo.ts @@ -1,7 +1,5 @@ import { Info, PlatformData, SdkData } from '@launchdarkly/js-client-sdk-common'; -import { name, version } from '../../package.json'; - export default class BrowserInfo implements Info { platformData(): PlatformData { return { @@ -10,8 +8,8 @@ export default class BrowserInfo implements Info { } sdkData(): SdkData { return { - name, - version, + name: '@launchdarkly/js-client-sdk', + version: '0.0.0', // x-release-please-version userAgentBase: 'JSClient', }; } diff --git a/packages/sdk/browser/src/platform/BrowserPlatform.ts b/packages/sdk/browser/src/platform/BrowserPlatform.ts index 34dfccaefa..33b5b10248 100644 --- a/packages/sdk/browser/src/platform/BrowserPlatform.ts +++ b/packages/sdk/browser/src/platform/BrowserPlatform.ts @@ -2,7 +2,7 @@ import { Crypto, Encoding, Info, - LDOptions, + LDLogger, Platform, Requests, Storage, @@ -22,9 +22,9 @@ export default class BrowserPlatform implements Platform { requests: Requests = new BrowserRequests(); storage?: Storage; - constructor(options: LDOptions) { + constructor(logger: LDLogger) { if (isLocalStorageSupported()) { - this.storage = new LocalStorage(options.logger); + this.storage = new LocalStorage(logger); } } } diff --git a/packages/sdk/browser/tsconfig.json b/packages/sdk/browser/tsconfig.json index 0a219e1516..2f2d034948 100644 --- a/packages/sdk/browser/tsconfig.json +++ b/packages/sdk/browser/tsconfig.json @@ -30,7 +30,7 @@ "node_modules", "contract-tests", "babel.config.js", - "jest.config.ts", + "jest.config.js", "jestSetupFile.ts", "**/*.test.ts*" ] diff --git a/packages/sdk/browser/tsconfig.test.json b/packages/sdk/browser/tsconfig.test.json deleted file mode 100644 index 2c617dcaa7..0000000000 --- a/packages/sdk/browser/tsconfig.test.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "jsx": "react-jsx", - "lib": ["es6", "dom"], - "module": "ES6", - "moduleResolution": "node", - "resolveJsonModule": true, - "rootDir": ".", - "strict": true, - "types": ["jest", "node"] - }, - "exclude": ["dist", "node_modules", "__tests__", "example"] -} diff --git a/packages/shared/common/src/internal/events/EventProcessor.ts b/packages/shared/common/src/internal/events/EventProcessor.ts index ef4a6e490a..4e6acd128e 100644 --- a/packages/shared/common/src/internal/events/EventProcessor.ts +++ b/packages/shared/common/src/internal/events/EventProcessor.ts @@ -36,6 +36,7 @@ interface CustomOutputEvent { data?: any; metricValue?: number; samplingRatio?: number; + url?: string; } interface FeatureOutputEvent { @@ -344,6 +345,10 @@ export default class EventProcessor implements LDEventProcessor { out.metricValue = event.metricValue; } + if (event.url !== undefined) { + out.url = event.url; + } + return out; } case 'click': { diff --git a/packages/shared/common/src/internal/events/InputCustomEvent.ts b/packages/shared/common/src/internal/events/InputCustomEvent.ts index 1c0c4a2b39..a8e29364f6 100644 --- a/packages/shared/common/src/internal/events/InputCustomEvent.ts +++ b/packages/shared/common/src/internal/events/InputCustomEvent.ts @@ -13,6 +13,8 @@ export default class InputCustomEvent { // Currently custom events are not sampled, but this is here to make the handling // code more uniform. public readonly samplingRatio: number = 1, + // Browser SDKs can include a URL for custom events. + public readonly url?: string, ) { this.creationDate = Date.now(); this.context = context; diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 1392db206a..8ce5538590 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -23,6 +23,7 @@ import { ConnectionMode, LDClient, type LDOptions } from './api'; import { LDEvaluationDetail, LDEvaluationDetailTyped } from './api/LDEvaluationDetail'; import { LDIdentifyOptions } from './api/LDIdentifyOptions'; import Configuration from './configuration'; +import { LDClientInternalOptions } from './configuration/Configuration'; import { addAutoEnv } from './context/addAutoEnv'; import { ensureKey } from './context/ensureKey'; import createDiagnosticsManager from './diagnostics/createDiagnosticsManager'; @@ -72,7 +73,7 @@ export default class LDClientImpl implements LDClient { public readonly autoEnvAttributes: AutoEnvAttributes, public readonly platform: Platform, options: LDOptions, - internalOptions?: internal.LDInternalOptions, + internalOptions?: LDClientInternalOptions, ) { if (!sdkKey) { throw new Error('You must configure the client with a client-side SDK key'); @@ -502,7 +503,9 @@ export default class LDClientImpl implements LDClient { } this.eventProcessor?.sendEvent( - this.eventFactoryDefault.customEvent(key, this.checkedContext!, data, metricValue), + this.config.trackEventModifier( + this.eventFactoryDefault.customEvent(key, this.checkedContext!, data, metricValue), + ), ); } diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index e68dd9f746..64bb9867e2 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -14,6 +14,10 @@ import validators from './validators'; const DEFAULT_POLLING_INTERVAL: number = 60 * 5; +export interface LDClientInternalOptions extends internal.LDInternalOptions { + trackEventModifier?: (event: internal.InputCustomEvent) => internal.InputCustomEvent; +} + export default class Configuration { public static DEFAULT_POLLING = 'https://clientsdk.launchdarkly.com'; public static DEFAULT_STREAM = 'https://clientstream.launchdarkly.com'; @@ -66,10 +70,14 @@ export default class Configuration { public readonly userAgentHeaderName: 'user-agent' | 'x-launchdarkly-user-agent'; + public readonly trackEventModifier: ( + event: internal.InputCustomEvent, + ) => internal.InputCustomEvent; + // Allow indexing Configuration by a string [index: string]: any; - constructor(pristineOptions: LDOptions = {}, internalOptions: internal.LDInternalOptions = {}) { + constructor(pristineOptions: LDOptions = {}, internalOptions: LDClientInternalOptions = {}) { const errors = this.validateTypesAndNames(pristineOptions); errors.forEach((e: string) => this.logger.warn(e)); @@ -86,6 +94,7 @@ export default class Configuration { this.tags = new ApplicationTags({ application: this.applicationInfo, logger: this.logger }); this.userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent'; + this.trackEventModifier = internalOptions.trackEventModifier ?? ((event) => event); } validateTypesAndNames(pristineOptions: LDOptions): string[] { diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index a4186185d4..d5b3e293d4 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -1,3 +1,4 @@ +import { LDClientInternalOptions } from './configuration/Configuration'; import LDClientImpl from './LDClientImpl'; export * from '@launchdarkly/js-sdk-common'; @@ -18,4 +19,4 @@ export type { export { DataSourcePaths } from './streaming'; -export { LDClientImpl }; +export { LDClientImpl, LDClientInternalOptions };