diff --git a/packages/sdk/react-native/src/RNOptions.ts b/packages/sdk/react-native/src/RNOptions.ts new file mode 100644 index 0000000000..4d41227719 --- /dev/null +++ b/packages/sdk/react-native/src/RNOptions.ts @@ -0,0 +1,28 @@ +import { LDOptions } from '@launchdarkly/js-client-sdk-common'; + +export default interface RNOptions extends LDOptions { + /** + * Some platforms (windows, web, mac, linux) can continue executing code + * in the background. + * + * Defaults to false. + */ + readonly runInBackground?: boolean; + + /** + * Enable handling of network availability. When this is true the + * connection state will automatically change when network + * availability changes. + * + * Defaults to true. + */ + readonly automaticNetworkHandling?: boolean; + + /** + * Enable handling associated with transitioning between the foreground + * and background. + * + * Defaults to true. + */ + readonly automaticBackgroundHandling?: boolean; +} diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts index aafe5b1553..a829d16a1b 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.test.ts @@ -1,32 +1,325 @@ -import { AutoEnvAttributes, type LDContext } from '@launchdarkly/js-client-sdk-common'; +import { AutoEnvAttributes, LDLogger, Response } from '@launchdarkly/js-client-sdk-common'; +import createPlatform from './platform'; +import PlatformCrypto from './platform/crypto'; +import PlatformEncoding from './platform/PlatformEncoding'; +import PlatformInfo from './platform/PlatformInfo'; +import PlatformStorage from './platform/PlatformStorage'; import ReactNativeLDClient from './ReactNativeLDClient'; -describe('ReactNativeLDClient', () => { - let ldc: ReactNativeLDClient; +function mockResponse(value: string, statusCode: number) { + const response: Response = { + headers: { + get: jest.fn(), + keys: jest.fn(), + values: jest.fn(), + entries: jest.fn(), + has: jest.fn(), + }, + status: statusCode, + text: () => Promise.resolve(value), + json: () => Promise.resolve(JSON.parse(value)), + }; + return Promise.resolve(response); +} - beforeEach(() => { - ldc = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { sendEvents: false }); +/** + * Mocks basicPlatform fetch. Returns the fetch jest.Mock object. + * @param remoteJson + * @param statusCode + */ +function mockFetch(value: string, statusCode: number = 200) { + const f = jest.fn(); + f.mockResolvedValue(mockResponse(value, statusCode)); + return f; +} + +jest.mock('./platform', () => ({ + __esModule: true, + default: jest.fn((logger: LDLogger) => ({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger), + requests: { + createEventSource: jest.fn(), + fetch: jest.fn(), + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + })), +})); + +const createMockEventSource = (streamUri: string = '', options: any = {}) => ({ + streamUri, + options, + onclose: jest.fn(), + addEventListener: jest.fn(), + close: jest.fn(), +}); + +it('uses correct default diagnostic url', () => { + const mockedFetch = jest.fn(); + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + (createPlatform as jest.Mock).mockReturnValue({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger), + requests: { + createEventSource: jest.fn(), + fetch: mockedFetch, + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + }); + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled); + + expect(mockedFetch).toHaveBeenCalledWith( + 'https://events.launchdarkly.com/mobile/events/diagnostic', + expect.anything(), + ); + client.close(); +}); + +it('uses correct default analytics event url', async () => { + const mockedFetch = mockFetch('{"flagA": true}', 200); + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + (createPlatform as jest.Mock).mockReturnValue({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger), + requests: { + createEventSource: createMockEventSource, + fetch: mockedFetch, + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + }); + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + diagnosticOptOut: true, + initialConnectionMode: 'polling', + }); + await client.identify({ kind: 'user', key: 'bob' }); + await client.flush(); + + expect(mockedFetch).toHaveBeenCalledWith( + 'https://events.launchdarkly.com/mobile', + expect.anything(), + ); +}); + +it('uses correct default polling url', async () => { + const mockedFetch = mockFetch('{"flagA": true}', 200); + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + (createPlatform as jest.Mock).mockReturnValue({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger), + requests: { + createEventSource: jest.fn(), + fetch: mockedFetch, + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + }); + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + diagnosticOptOut: true, + sendEvents: false, + initialConnectionMode: 'polling', + automaticBackgroundHandling: false, + }); + await client.identify({ kind: 'user', key: 'bob' }); + + const regex = /https:\/\/clientsdk\.launchdarkly\.com\/msdk\/evalx\/contexts\/.*/; + expect(mockedFetch).toHaveBeenCalledWith(expect.stringMatching(regex), expect.anything()); +}); + +it('uses correct default streaming url', (done) => { + const mockedCreateEventSource = jest.fn(); + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + (createPlatform as jest.Mock).mockReturnValue({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger), + requests: { + createEventSource: mockedCreateEventSource, + fetch: jest.fn(), + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + }); + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + diagnosticOptOut: true, + sendEvents: false, + initialConnectionMode: 'streaming', + automaticBackgroundHandling: false, }); - test('constructing a new client', () => { - expect(ldc.highTimeoutThreshold).toEqual(15); - expect(ldc.sdkKey).toEqual('mobile-key'); - expect(ldc.config.serviceEndpoints).toEqual({ - analyticsEventPath: '/mobile', - diagnosticEventPath: '/mobile/events/diagnostic', - events: 'https://events.launchdarkly.com', - includeAuthorizationHeader: true, - polling: 'https://clientsdk.launchdarkly.com', - streaming: 'https://clientstream.launchdarkly.com', + client + .identify({ kind: 'user', key: 'bob' }, { timeout: 0 }) + .then(() => {}) + .catch(() => {}) + .then(() => { + const regex = /https:\/\/clientstream\.launchdarkly\.com\/meval\/.*/; + expect(mockedCreateEventSource).toHaveBeenCalledWith( + expect.stringMatching(regex), + expect.anything(), + ); + done(); }); +}); + +it('includes authorization header for polling', async () => { + const mockedFetch = mockFetch('{"flagA": true}', 200); + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + (createPlatform as jest.Mock).mockReturnValue({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger), + requests: { + createEventSource: jest.fn(), + fetch: mockedFetch, + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + }); + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + diagnosticOptOut: true, + sendEvents: false, + initialConnectionMode: 'polling', + automaticBackgroundHandling: false, + }); + await client.identify({ kind: 'user', key: 'bob' }); + + expect(mockedFetch).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + headers: expect.objectContaining({ authorization: 'mobile-key' }), + }), + ); +}); + +it('includes authorization header for streaming', (done) => { + const mockedCreateEventSource = jest.fn(); + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + (createPlatform as jest.Mock).mockReturnValue({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger), + requests: { + createEventSource: mockedCreateEventSource, + fetch: jest.fn(), + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + }); + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + diagnosticOptOut: true, + sendEvents: false, + initialConnectionMode: 'streaming', + automaticBackgroundHandling: false, + }); + + client + .identify({ kind: 'user', key: 'bob' }, { timeout: 0 }) + .then(() => {}) + .catch(() => {}) + .then(() => { + expect(mockedCreateEventSource).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + headers: expect.objectContaining({ authorization: 'mobile-key' }), + }), + ); + done(); + }); +}); + +it('includes authorization header for events', async () => { + const mockedFetch = mockFetch('{"flagA": true}', 200); + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + (createPlatform as jest.Mock).mockReturnValue({ + crypto: new PlatformCrypto(), + info: new PlatformInfo(logger), + requests: { + createEventSource: jest.fn(), + fetch: mockedFetch, + }, + encoding: new PlatformEncoding(), + storage: new PlatformStorage(logger), + }); + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + diagnosticOptOut: true, + initialConnectionMode: 'polling', }); + await client.identify({ kind: 'user', key: 'bob' }); + await client.flush(); - test('createStreamUriPath', () => { - const context: LDContext = { kind: 'user', key: 'test-user-key-1' }; + expect(mockedFetch).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + headers: expect.objectContaining({ authorization: 'mobile-key' }), + }), + ); +}); + +it('identify with too high of a timeout', () => { + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + sendEvents: false, + initialConnectionMode: 'offline', + logger, + }); + client.identify({ key: 'potato', kind: 'user' }, { timeout: 16 }); + expect(logger.warn).toHaveBeenCalledWith( + 'The identify function was called with a timeout greater than 15 seconds. We recommend a timeout of less than 15 seconds.', + ); +}); - expect(ldc.createStreamUriPath(context)).toEqual( - '/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlci1rZXktMSJ9', - ); +it('identify timeout equal to threshold', () => { + const logger: LDLogger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, { + sendEvents: false, + initialConnectionMode: 'offline', + logger, }); + client.identify({ key: 'potato', kind: 'user' }, { timeout: 15 }); + expect(logger.warn).not.toHaveBeenCalled(); }); diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index 430ef92093..c52a9492f9 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -7,11 +7,11 @@ import { internal, LDClientImpl, type LDContext, - type LDOptions, } from '@launchdarkly/js-client-sdk-common'; import createPlatform from './platform'; import { ConnectionDestination, ConnectionManager } from './platform/ConnectionManager'; +import LDOptions from './RNOptions'; import RNStateDetector from './RNStateDetector'; /** @@ -84,9 +84,10 @@ export default class ReactNativeLDClient extends LDClientImpl { initialConnectionMode, // TODO: Add the ability to configure connection management. // This is not yet added as the RN SDK needs package specific configuration added. - automaticNetworkHandling: true, - automaticBackgroundHandling: true, - runInBackground: false, + // TODO: Add RN config option validation beyond base options. + automaticNetworkHandling: options.automaticNetworkHandling ?? true, + automaticBackgroundHandling: options.automaticBackgroundHandling ?? true, + runInBackground: options.runInBackground ?? true, }, destination, new RNStateDetector(), diff --git a/packages/sdk/react-native/src/index.ts b/packages/sdk/react-native/src/index.ts index 78b49da152..bcfda9922e 100644 --- a/packages/sdk/react-native/src/index.ts +++ b/packages/sdk/react-native/src/index.ts @@ -7,6 +7,7 @@ */ import { setupPolyfill } from './polyfills'; import ReactNativeLDClient from './ReactNativeLDClient'; +import RNOptions from './RNOptions'; setupPolyfill(); @@ -14,4 +15,4 @@ export * from '@launchdarkly/js-client-sdk-common'; export * from './hooks'; export * from './provider'; -export { ReactNativeLDClient }; +export { ReactNativeLDClient, RNOptions as LDOptions }; diff --git a/packages/sdk/react-native/src/platform/ConnectionManager.ts b/packages/sdk/react-native/src/platform/ConnectionManager.ts index 7c04ced3c7..8f39f76f04 100644 --- a/packages/sdk/react-native/src/platform/ConnectionManager.ts +++ b/packages/sdk/react-native/src/platform/ConnectionManager.ts @@ -52,17 +52,23 @@ export interface ConnectionManagerConfig { /// The initial connection mode the SDK should use. readonly initialConnectionMode: ConnectionMode; - /// Some platforms (windows, web, mac, linux) can continue executing code - /// in the background. + /** + * Some platforms (windows, web, mac, linux) can continue executing code + * in the background. + */ readonly runInBackground: boolean; - /// Enable handling of network availability. When this is true the - /// connection state will automatically change when network - /// availability changes. + /** + * Enable handling of network availability. When this is true the + * connection state will automatically change when network + * availability changes. + */ readonly automaticNetworkHandling: boolean; - /// Enable handling associated with transitioning between the foreground - /// and background. + /** + * Enable handling associated with transitioning between the foreground + * and background. + */ readonly automaticBackgroundHandling: boolean; } diff --git a/packages/sdk/react-native/src/platform/PlatformEncoding.ts b/packages/sdk/react-native/src/platform/PlatformEncoding.ts new file mode 100644 index 0000000000..a90159282d --- /dev/null +++ b/packages/sdk/react-native/src/platform/PlatformEncoding.ts @@ -0,0 +1,9 @@ +import type { Encoding } from '@launchdarkly/js-client-sdk-common'; + +import { btoa } from '../polyfills'; + +export default class PlatformEncoding implements Encoding { + btoa(data: string): string { + return btoa(data); + } +} diff --git a/packages/sdk/react-native/src/platform/PlatformInfo.ts b/packages/sdk/react-native/src/platform/PlatformInfo.ts new file mode 100644 index 0000000000..ef68b44807 --- /dev/null +++ b/packages/sdk/react-native/src/platform/PlatformInfo.ts @@ -0,0 +1,30 @@ +import type { Info, LDLogger, PlatformData, SdkData } from '@launchdarkly/js-client-sdk-common'; + +import { name, version } from '../../package.json'; +import { ldApplication, ldDevice } from './autoEnv'; + +export default class PlatformInfo implements Info { + constructor(private readonly logger: LDLogger) {} + + platformData(): PlatformData { + const data = { + name: 'React Native', + ld_application: ldApplication, + ld_device: ldDevice, + }; + + this.logger.debug(`platformData: ${JSON.stringify(data, null, 2)}`); + return data; + } + + sdkData(): SdkData { + const data = { + name, + version, + userAgentBase: 'ReactNativeClient', + }; + + this.logger.debug(`sdkData: ${JSON.stringify(data, null, 2)}`); + return data; + } +} diff --git a/packages/sdk/react-native/src/platform/PlatformRequests.ts b/packages/sdk/react-native/src/platform/PlatformRequests.ts new file mode 100644 index 0000000000..5b345d2950 --- /dev/null +++ b/packages/sdk/react-native/src/platform/PlatformRequests.ts @@ -0,0 +1,28 @@ +import type { + EventName, + EventSource, + EventSourceInitDict, + LDLogger, + Options, + Requests, + Response, +} from '@launchdarkly/js-client-sdk-common'; + +import RNEventSource from '../fromExternal/react-native-sse'; + +export default class PlatformRequests implements Requests { + constructor(private readonly logger: LDLogger) {} + + createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { + return new RNEventSource(url, { + headers: eventSourceInitDict.headers, + retryAndHandleError: eventSourceInitDict.errorFilter, + logger: this.logger, + }); + } + + fetch(url: string, options?: Options): Promise { + // @ts-ignore + return fetch(url, options); + } +} diff --git a/packages/sdk/react-native/src/platform/PlatformStorage.ts b/packages/sdk/react-native/src/platform/PlatformStorage.ts new file mode 100644 index 0000000000..33e003c82f --- /dev/null +++ b/packages/sdk/react-native/src/platform/PlatformStorage.ts @@ -0,0 +1,28 @@ +import type { LDLogger, Storage } from '@launchdarkly/js-client-sdk-common'; + +import AsyncStorage from './ConditionalAsyncStorage'; + +export default class PlatformStorage implements Storage { + constructor(private readonly logger: LDLogger) {} + async clear(key: string): Promise { + await AsyncStorage.removeItem(key); + } + + async get(key: string): Promise { + try { + const value = await AsyncStorage.getItem(key); + return value ?? null; + } catch (error) { + this.logger.debug(`Error getting AsyncStorage key: ${key}, error: ${error}`); + return null; + } + } + + async set(key: string, value: string): Promise { + try { + await AsyncStorage.setItem(key, value); + } catch (error) { + this.logger.debug(`Error saving AsyncStorage key: ${key}, value: ${value}, error: ${error}`); + } + } +} diff --git a/packages/sdk/react-native/src/platform/index.ts b/packages/sdk/react-native/src/platform/index.ts index df1f103ded..e4b29b25d5 100644 --- a/packages/sdk/react-native/src/platform/index.ts +++ b/packages/sdk/react-native/src/platform/index.ts @@ -1,100 +1,10 @@ -/* eslint-disable max-classes-per-file */ -import type { - Encoding, - EventName, - EventSource, - EventSourceInitDict, - Info, - LDLogger, - Options, - Platform, - PlatformData, - Requests, - Response, - SdkData, - Storage, -} from '@launchdarkly/js-client-sdk-common'; +import { LDLogger, Platform } from '@launchdarkly/js-client-sdk-common'; -import { name, version } from '../../package.json'; -import RNEventSource from '../fromExternal/react-native-sse'; -import { btoa } from '../polyfills'; -import { ldApplication, ldDevice } from './autoEnv'; -import AsyncStorage from './ConditionalAsyncStorage'; import PlatformCrypto from './crypto'; - -export class PlatformRequests implements Requests { - constructor(private readonly logger: LDLogger) {} - - createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { - return new RNEventSource(url, { - headers: eventSourceInitDict.headers, - retryAndHandleError: eventSourceInitDict.errorFilter, - logger: this.logger, - }); - } - - fetch(url: string, options?: Options): Promise { - // @ts-ignore - return fetch(url, options); - } -} - -class PlatformEncoding implements Encoding { - btoa(data: string): string { - return btoa(data); - } -} - -class PlatformInfo implements Info { - constructor(private readonly logger: LDLogger) {} - - platformData(): PlatformData { - const data = { - name: 'React Native', - ld_application: ldApplication, - ld_device: ldDevice, - }; - - this.logger.debug(`platformData: ${JSON.stringify(data, null, 2)}`); - return data; - } - - sdkData(): SdkData { - const data = { - name, - version, - userAgentBase: 'ReactNativeClient', - }; - - this.logger.debug(`sdkData: ${JSON.stringify(data, null, 2)}`); - return data; - } -} - -class PlatformStorage implements Storage { - constructor(private readonly logger: LDLogger) {} - async clear(key: string): Promise { - await AsyncStorage.removeItem(key); - } - - async get(key: string): Promise { - try { - const value = await AsyncStorage.getItem(key); - return value ?? null; - } catch (error) { - this.logger.debug(`Error getting AsyncStorage key: ${key}, error: ${error}`); - return null; - } - } - - async set(key: string, value: string): Promise { - try { - await AsyncStorage.setItem(key, value); - } catch (error) { - this.logger.debug(`Error saving AsyncStorage key: ${key}, value: ${value}, error: ${error}`); - } - } -} +import PlatformEncoding from './PlatformEncoding'; +import PlatformInfo from './PlatformInfo'; +import PlatformRequests from './PlatformRequests'; +import PlatformStorage from './PlatformStorage'; const createPlatform = (logger: LDLogger): Platform => ({ crypto: new PlatformCrypto(), diff --git a/packages/shared/sdk-client/src/LDClientImpl.events.test.ts b/packages/shared/sdk-client/src/LDClientImpl.events.test.ts index eeb43a1d77..5d2ba57cef 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.events.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.events.test.ts @@ -1,11 +1,17 @@ -import { AutoEnvAttributes, clone, LDContext } from '@launchdarkly/js-sdk-common'; +import { + AutoEnvAttributes, + ClientContext, + clone, + internal, + LDContext, + subsystem, +} from '@launchdarkly/js-sdk-common'; import { InputCustomEvent, InputIdentifyEvent } from '@launchdarkly/js-sdk-common/dist/internal'; import { basicPlatform, hasher, logger, MockEventProcessor, - setupMockEventProcessor, setupMockStreamingProcessor, } from '@launchdarkly/private-js-mocks'; @@ -34,9 +40,23 @@ let defaultPutResponse: Flags; const carContext: LDContext = { kind: 'car', key: 'test-car' }; describe('sdk-client object', () => { + const mockedSendEvent: jest.Mock = jest.fn(); beforeEach(() => { defaultPutResponse = clone(mockResponseJson); - setupMockEventProcessor(); + mockedSendEvent.mockReset(); + MockEventProcessor.mockImplementation( + ( + _config: internal.EventProcessorOptions, + _clientContext: ClientContext, + _contextDeduplicator?: subsystem.LDContextDeduplicator, + _diagnosticsManager?: internal.DiagnosticsManager, + _start: boolean = true, + ) => ({ + close: jest.fn(), + flush: jest.fn(), + sendEvent: mockedSendEvent, + }), + ); setupMockStreamingProcessor(false, defaultPutResponse); basicPlatform.crypto.randomUUID.mockReturnValue('random1'); hasher.digest.mockReturnValue('digested1'); @@ -59,7 +79,7 @@ describe('sdk-client object', () => { await ldc.identify(carContext); expect(MockEventProcessor).toHaveBeenCalled(); - expect(ldc.eventProcessor!.sendEvent).toHaveBeenNthCalledWith( + expect(mockedSendEvent).toHaveBeenNthCalledWith( 1, expect.objectContaining({ kind: 'identify', @@ -79,7 +99,7 @@ describe('sdk-client object', () => { ldc.track('the-event', { the: 'data' }, undefined); expect(MockEventProcessor).toHaveBeenCalled(); - expect(ldc.eventProcessor!.sendEvent).toHaveBeenNthCalledWith( + expect(mockedSendEvent).toHaveBeenNthCalledWith( 2, expect.objectContaining({ kind: 'custom', @@ -102,7 +122,7 @@ describe('sdk-client object', () => { ldc.track('the-event', undefined, 12); expect(MockEventProcessor).toHaveBeenCalled(); - expect(ldc.eventProcessor!.sendEvent).toHaveBeenNthCalledWith( + expect(mockedSendEvent).toHaveBeenNthCalledWith( 2, expect.objectContaining({ kind: 'custom', @@ -125,7 +145,7 @@ describe('sdk-client object', () => { ldc.track('the-event', { the: 'data' }, 12); expect(MockEventProcessor).toHaveBeenCalled(); - expect(ldc.eventProcessor!.sendEvent).toHaveBeenNthCalledWith( + expect(mockedSendEvent).toHaveBeenNthCalledWith( 2, expect.objectContaining({ kind: 'custom', diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index e803d86122..3a2a0df944 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -65,40 +65,6 @@ describe('sdk-client object', () => { jest.resetAllMocks(); }); - test('instantiate with blank options', () => { - ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, basicPlatform, {}); - - expect(ldc.config).toMatchObject({ - allAttributesPrivate: false, - baseUri: 'https://clientsdk.launchdarkly.com', - capacity: 100, - diagnosticOptOut: false, - diagnosticRecordingInterval: 900, - eventsUri: 'https://events.launchdarkly.com', - flushInterval: 30, - inspectors: [], - logger: { - destination: expect.any(Function), - formatter: expect.any(Function), - logLevel: 1, - name: 'LaunchDarkly', - }, - privateAttributes: [], - sendEvents: true, - sendLDHeaders: true, - serviceEndpoints: { - events: 'https://events.launchdarkly.com', - polling: 'https://clientsdk.launchdarkly.com', - streaming: 'https://clientstream.launchdarkly.com', - }, - streamInitialReconnectDelay: 1, - streamUri: 'https://clientstream.launchdarkly.com', - tags: {}, - useReport: false, - withReasons: false, - }); - }); - test('all flags', async () => { await ldc.identify(context); const all = ldc.allFlags(); diff --git a/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts b/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts index 69356c11b8..2cf4f79b63 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts @@ -26,6 +26,8 @@ const carContext: LDContext = { kind: 'car', key: 'test-car' }; let ldc: LDClientImpl; let defaultPutResponse: Flags; +const DEFAULT_IDENTIFY_TIMEOUT = 5; + describe('sdk-client identify timeout', () => { beforeAll(() => { jest.useFakeTimers(); @@ -52,7 +54,7 @@ describe('sdk-client identify timeout', () => { // streaming is setup to error in beforeEach to cause a timeout test('rejects with default timeout of 5s', async () => { - jest.advanceTimersByTimeAsync(ldc.identifyTimeout * 1000).then(); + jest.advanceTimersByTimeAsync(DEFAULT_IDENTIFY_TIMEOUT * 1000).then(); await expect(ldc.identify(carContext)).rejects.toThrow(/identify timed out/); expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/identify timed out/)); }); @@ -66,7 +68,7 @@ describe('sdk-client identify timeout', () => { test('resolves with default timeout', async () => { setupMockStreamingProcessor(false, defaultPutResponse); - jest.advanceTimersByTimeAsync(ldc.identifyTimeout * 1000).then(); + jest.advanceTimersByTimeAsync(DEFAULT_IDENTIFY_TIMEOUT * 1000).then(); await expect(ldc.identify(carContext)).resolves.toBeUndefined(); @@ -120,7 +122,6 @@ describe('sdk-client identify timeout', () => { jest.advanceTimersByTimeAsync(customTimeout * 1000).then(); await ldc.identify(carContext, { timeout: customTimeout }); - expect(ldc.identifyTimeout).toEqual(10); expect(logger.warn).not.toHaveBeenCalledWith(expect.stringMatching(/timeout greater/)); }); @@ -135,12 +136,10 @@ describe('sdk-client identify timeout', () => { }); test('safe timeout should not warn', async () => { - const { identifyTimeout } = ldc; - const safeTimeout = identifyTimeout; setupMockStreamingProcessor(false, defaultPutResponse); - jest.advanceTimersByTimeAsync(identifyTimeout * 1000).then(); + jest.advanceTimersByTimeAsync(DEFAULT_IDENTIFY_TIMEOUT * 1000).then(); - await ldc.identify(carContext, { timeout: safeTimeout }); + await ldc.identify(carContext, { timeout: DEFAULT_IDENTIFY_TIMEOUT }); expect(logger.warn).not.toHaveBeenCalledWith(expect.stringMatching(/timeout greater/)); }); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 9b34577650..5fefa7e6db 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -34,15 +34,15 @@ const { createErrorEvaluationDetail, createSuccessEvaluationDetail, ClientMessag internal; export default class LDClientImpl implements LDClient { - config: Configuration; - context?: LDContext; - diagnosticsManager?: internal.DiagnosticsManager; - eventProcessor?: internal.EventProcessor; - identifyTimeout: number = 5; - logger: LDLogger; - updateProcessor?: LDStreamProcessor; + private readonly config: Configuration; + private context?: LDContext; + private readonly diagnosticsManager?: internal.DiagnosticsManager; + private eventProcessor?: internal.EventProcessor; + private identifyTimeout: number = 5; + readonly logger: LDLogger; + private updateProcessor?: LDStreamProcessor; - readonly highTimeoutThreshold: number = 15; + private readonly highTimeoutThreshold: number = 15; private eventFactoryDefault = new EventFactory(false); private eventFactoryWithReasons = new EventFactory(true); @@ -309,8 +309,8 @@ export default class LDClientImpl implements LDClient { if (this.identifyTimeout > this.highTimeoutThreshold) { this.logger.warn( - 'The identify function was called with a timeout greater than' + - `${this.highTimeoutThreshold} seconds. We recommend a timeout of less than` + + 'The identify function was called with a timeout greater than ' + + `${this.highTimeoutThreshold} seconds. We recommend a timeout of less than ` + `${this.highTimeoutThreshold} seconds.`, ); } @@ -351,7 +351,6 @@ export default class LDClientImpl implements LDClient { } } else { this.updateProcessor?.close(); - switch (this.getConnectionMode()) { case 'streaming': this.createStreamingProcessor(context, checkedContext, identifyResolve, identifyReject); diff --git a/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts b/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts index 06ca449c0d..9066c02396 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts @@ -50,8 +50,7 @@ describe('sdk-client object', () => { }); test('variation flag not found', async () => { - // set context manually to pass validation - ldc.context = { kind: 'user', key: 'test-user' }; + await ldc.identify({ kind: 'user', key: 'test-user' }); const errorListener = jest.fn().mockName('errorListener'); ldc.on('error', errorListener); diff --git a/packages/shared/sdk-client/src/api/LDClient.ts b/packages/shared/sdk-client/src/api/LDClient.ts index d5b45d4961..7902bc6914 100644 --- a/packages/shared/sdk-client/src/api/LDClient.ts +++ b/packages/shared/sdk-client/src/api/LDClient.ts @@ -167,7 +167,7 @@ export interface LDClient { * * @returns The configured {@link LDLogger}. */ - logger: LDLogger; + readonly logger: LDLogger; /** * Determines the numeric variation of a feature flag.