From e180de68669602706d5a59febe1c9e3c7f1fa5e0 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 6 Sep 2024 16:54:29 -0500 Subject: [PATCH 01/15] tests now passing --- .../react-native/src/ReactNativeLDClient.ts | 13 +- .../src/platform/PlatformRequests.ts | 2 + .../common/src/api/platform/EventSource.ts | 4 +- .../src/LDClientImpl.events.test.ts | 29 +- .../sdk-client/src/LDClientImpl.mocks.ts | 50 +++ .../src/LDClientImpl.storage.test.ts | 377 +++++++++++------- .../sdk-client/src/LDClientImpl.test.ts | 99 +++-- .../src/LDClientImpl.timeout.test.ts | 70 ++-- .../shared/sdk-client/src/LDClientImpl.ts | 49 ++- .../src/LDClientImpl.variation.test.ts | 59 +-- .../shared/sdk-client/src/api/LDOptions.ts | 8 + .../src/configuration/Configuration.ts | 16 +- packages/shared/sdk-client/src/index.ts | 1 + .../src/streaming/DataSourceConfig.ts | 20 + .../src/streaming/StreamingProcessor.test.ts | 283 +++++++++++++ .../src/streaming/StreamingProcessor.ts | 166 ++++++++ .../shared/sdk-client/src/streaming/index.ts | 5 + 17 files changed, 969 insertions(+), 282 deletions(-) create mode 100644 packages/shared/sdk-client/src/LDClientImpl.mocks.ts create mode 100644 packages/shared/sdk-client/src/streaming/DataSourceConfig.ts create mode 100644 packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts create mode 100644 packages/shared/sdk-client/src/streaming/StreamingProcessor.ts create mode 100644 packages/shared/sdk-client/src/streaming/index.ts diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index 44558a5cb..9e98c560b 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -4,9 +4,11 @@ import { base64UrlEncode, BasicLogger, ConnectionMode, + Encoding, internal, LDClientImpl, type LDContext, + StreamingPaths, } from '@launchdarkly/js-client-sdk-common'; import validateOptions, { filterToBaseOptions } from './options'; @@ -103,8 +105,15 @@ export default class ReactNativeLDClient extends LDClientImpl { return base64UrlEncode(JSON.stringify(context), this.platform.encoding!); } - override createStreamUriPath(context: LDContext) { - return `/meval/${this.encodeContext(context)}`; + override getStreamingPaths(): StreamingPaths { + return { + pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { + return `/meval/${base64UrlEncode(_plainContextString, encoding)}`; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return `/meval`; + }, + }; } override createPollUriPath(context: LDContext): string { diff --git a/packages/sdk/react-native/src/platform/PlatformRequests.ts b/packages/sdk/react-native/src/platform/PlatformRequests.ts index 5b345d295..5d4ef3b0b 100644 --- a/packages/sdk/react-native/src/platform/PlatformRequests.ts +++ b/packages/sdk/react-native/src/platform/PlatformRequests.ts @@ -15,7 +15,9 @@ export default class PlatformRequests implements Requests { createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { return new RNEventSource(url, { + method: eventSourceInitDict.method, headers: eventSourceInitDict.headers, + body: eventSourceInitDict.body, retryAndHandleError: eventSourceInitDict.errorFilter, logger: this.logger, }); diff --git a/packages/shared/common/src/api/platform/EventSource.ts b/packages/shared/common/src/api/platform/EventSource.ts index a9214b015..55bef9bfe 100644 --- a/packages/shared/common/src/api/platform/EventSource.ts +++ b/packages/shared/common/src/api/platform/EventSource.ts @@ -18,8 +18,10 @@ export interface EventSource { } export interface EventSourceInitDict { - errorFilter: (err: HttpErrorResponse) => boolean; + method?: string; headers: { [key: string]: string | string[] }; + body?: string; + errorFilter: (err: HttpErrorResponse) => boolean; initialRetryDelayMillis: number; readTimeoutMillis: number; retryResetIntervalMillis: number; diff --git a/packages/shared/sdk-client/src/LDClientImpl.events.test.ts b/packages/shared/sdk-client/src/LDClientImpl.events.test.ts index b62971b3b..0e0aef2c4 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.events.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.events.test.ts @@ -2,6 +2,7 @@ import { AutoEnvAttributes, ClientContext, clone, + Encoding, internal, LDContext, subsystem, @@ -10,11 +11,11 @@ import { createBasicPlatform, createLogger, MockEventProcessor, - setupMockStreamingProcessor, } from '@launchdarkly/private-js-mocks'; import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; +import { MockEventSource } from './LDClientImpl.mocks'; import { Flags } from './types'; type InputCustomEvent = internal.InputCustomEvent; @@ -36,7 +37,6 @@ jest.mock('@launchdarkly/js-sdk-common', () => { ...{ internal: { ...actual.internal, - StreamingProcessor: m.MockStreamingProcessor, EventProcessor: m.MockEventProcessor, }, }, @@ -45,6 +45,7 @@ jest.mock('@launchdarkly/js-sdk-common', () => { const testSdkKey = 'test-sdk-key'; let ldc: LDClientImpl; +let mockEventSource: MockEventSource; let defaultPutResponse: Flags; const carContext: LDContext = { kind: 'car', key: 'test-car' }; @@ -66,15 +67,31 @@ describe('sdk-client object', () => { sendEvent: mockedSendEvent, }), ); - setupMockStreamingProcessor(false, defaultPutResponse); + + const simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + mockPlatform.storage.get.mockImplementation(() => undefined); + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); + mockPlatform.crypto.randomUUID.mockReturnValue('random1'); ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, mockPlatform, { logger, }); - jest - .spyOn(LDClientImpl.prototype as any, 'createStreamUriPath') - .mockReturnValue('/stream/path'); + + jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + }); }); afterEach(() => { diff --git a/packages/shared/sdk-client/src/LDClientImpl.mocks.ts b/packages/shared/sdk-client/src/LDClientImpl.mocks.ts new file mode 100644 index 000000000..c8e04abf6 --- /dev/null +++ b/packages/shared/sdk-client/src/LDClientImpl.mocks.ts @@ -0,0 +1,50 @@ +import { EventSource, EventSourceInitDict } from '@launchdarkly/js-sdk-common'; + +export class MockEventSource implements EventSource { + eventsByType: Map = new Map(); + + handlers: Record void> = {}; + + closed = false; + + url: string; + + options: EventSourceInitDict; + + constructor(url: string, options: EventSourceInitDict) { + this.url = url; + this.options = options; + } + + onclose: (() => void) | undefined; + + onerror: (() => void) | undefined; + + onopen: (() => void) | undefined; + + onretrying: ((e: { delayMillis: number }) => void) | undefined; + + addEventListener(type: string, listener: (event?: { data?: any }) => void): void { + this.handlers[type] = listener; + + // replay events to listener + (this.eventsByType.get(type) ?? []).forEach((event) => { + listener(event); + }); + } + + close(): void { + this.closed = true; + } + + simulateEvents(type: string, events: { data?: any }[]) { + this.eventsByType.set(type, events); + } + + simulateError(error: { status: number; message: string }) { + const shouldRetry = this.options.errorFilter(error); + if (!shouldRetry) { + this.closed = true; + } + } +} diff --git a/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts b/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts index 1b4ffcb15..864cb813b 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts @@ -1,15 +1,12 @@ -import { AutoEnvAttributes, clone, type LDContext, noop } from '@launchdarkly/js-sdk-common'; -import { - createBasicPlatform, - createLogger, - setupMockStreamingProcessor, -} from '@launchdarkly/private-js-mocks'; +import { AutoEnvAttributes, clone, Encoding, type LDContext } from '@launchdarkly/js-sdk-common'; +import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import LDEmitter from './api/LDEmitter'; import { toMulti } from './context/addAutoEnv'; import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; -import { DeleteFlag, Flags, PatchFlag } from './types'; +import { MockEventSource } from './LDClientImpl.mocks'; +import { Flags, PatchFlag } from './types'; let mockPlatform: ReturnType; let logger: ReturnType; @@ -19,25 +16,12 @@ beforeEach(() => { logger = createLogger(); }); -jest.mock('@launchdarkly/js-sdk-common', () => { - const actual = jest.requireActual('@launchdarkly/js-sdk-common'); - const { MockStreamingProcessor } = jest.requireActual('@launchdarkly/private-js-mocks'); - return { - ...actual, - ...{ - internal: { - ...actual.internal, - StreamingProcessor: MockStreamingProcessor, - }, - }, - }; -}); - const testSdkKey = 'test-sdk-key'; const context: LDContext = { kind: 'org', key: 'Testy Pizza' }; const flagStorageKey = 'LaunchDarkly_1234567890123456_1234567890123456'; const indexStorageKey = 'LaunchDarkly_1234567890123456_ContextIndex'; let ldc: LDClientImpl; +let mockEventSource: MockEventSource; let emitter: LDEmitter; let defaultPutResponse: Flags; let defaultFlagKeys: string[]; @@ -50,36 +34,6 @@ const onChangePromise = () => }); }); -// Common setup code for all tests -// 1. Sets up streaming -// 2. Sets up the change listener -// 3. Runs identify -// 4. Get all flags -const identifyGetAllFlags = async ( - shouldError: boolean = false, - putResponse = defaultPutResponse, - patchResponse?: PatchFlag, - deleteResponse?: DeleteFlag, - waitForChange: boolean = true, -) => { - setupMockStreamingProcessor(shouldError, putResponse, patchResponse, deleteResponse); - const changePromise = onChangePromise(); - - try { - await ldc.identify(context); - } catch (e) { - /* empty */ - } - jest.runAllTimers(); - - // if streaming errors, don't wait for 'change' because it will not be sent. - if (waitForChange && !shouldError) { - await changePromise; - } - - return ldc.allFlags(); -}; - describe('sdk-client storage', () => { beforeEach(() => { jest.useFakeTimers(); @@ -97,9 +51,14 @@ describe('sdk-client storage', () => { } }); - jest - .spyOn(LDClientImpl.prototype as any, 'createStreamUriPath') - .mockReturnValue('/stream/path'); + jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + }); ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Disabled, mockPlatform, { logger, @@ -116,8 +75,17 @@ describe('sdk-client storage', () => { }); test('initialize from storage succeeds without streaming', async () => { - // make sure streaming errors - const allFlags = await identifyGetAllFlags(true, defaultPutResponse); + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateError({ status: 404, message: 'test-error' }); + return mockEventSource; + }, + ); + + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; expect(mockPlatform.storage.get).toHaveBeenCalledWith(flagStorageKey); @@ -130,7 +98,8 @@ describe('sdk-client storage', () => { context, expect.objectContaining({ message: 'test-error' }), ); - expect(allFlags).toEqual({ + + expect(ldc.allFlags()).toEqual({ 'dev-test-flag': true, 'easter-i-tunes-special': false, 'easter-specials': 'no specials', @@ -143,6 +112,14 @@ describe('sdk-client storage', () => { }); test('initialize from storage succeeds with auto env', async () => { + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateError({ status: 404, message: 'test-error' }); + return mockEventSource; + }, + ); + ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, mockPlatform, { logger, sendEvents: false, @@ -151,7 +128,7 @@ describe('sdk-client storage', () => { emitter = ldc.emitter; jest.spyOn(emitter as LDEmitter, 'emit'); - const allFlags = await identifyGetAllFlags(true, defaultPutResponse); + await ldc.identify(context); expect(mockPlatform.storage.get).toHaveBeenLastCalledWith( expect.stringMatching('LaunchDarkly_1234567890123456_1234567890123456'), @@ -171,7 +148,7 @@ describe('sdk-client storage', () => { expect.objectContaining(toMulti(context)), expect.objectContaining({ message: 'test-error' }), ); - expect(allFlags).toEqual({ + expect(ldc.allFlags()).toEqual({ 'dev-test-flag': true, 'easter-i-tunes-special': false, 'easter-specials': 'no specials', @@ -184,38 +161,40 @@ describe('sdk-client storage', () => { }); test('not emitting change event when changed keys is empty', async () => { - let LDClientImplTestNoChange; - jest.isolateModules(async () => { - LDClientImplTestNoChange = jest.requireActual('./LDClientImpl').default; - ldc = new LDClientImplTestNoChange(testSdkKey, AutoEnvAttributes.Enabled, mockPlatform, { - logger, - sendEvents: false, - }); - }); + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + return mockEventSource; + }, + ); // @ts-ignore emitter = ldc.emitter; jest.spyOn(emitter as LDEmitter, 'emit'); // expect emission - await identifyGetAllFlags(true, defaultPutResponse); + await ldc.identify(context); // expit no emission - await identifyGetAllFlags(true, defaultPutResponse); + await ldc.identify(context); expect(emitter.emit).toHaveBeenCalledTimes(1); }); test('no storage, cold start from streaming', async () => { - // fake previously cached flags even though there's no storage for this context - // @ts-ignore - ldc.flags = defaultPutResponse; + const simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; mockPlatform.storage.get.mockImplementation(() => undefined); - setupMockStreamingProcessor(false, defaultPutResponse); + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); - ldc.identify(context).then(noop); - await jest.runAllTimersAsync(); + await ldc.identify(context); + expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); expect(mockPlatform.storage.set).toHaveBeenNthCalledWith( 1, indexStorageKey, @@ -244,12 +223,21 @@ describe('sdk-client storage', () => { test('syncing storage when a flag is deleted', async () => { const putResponse = clone(defaultPutResponse); delete putResponse['dev-test-flag']; - const allFlags = await identifyGetAllFlags(false, putResponse); - // wait for async code to resolve promises - await jest.runAllTimersAsync(); + const simulatedEvents = [{ data: JSON.stringify(putResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); - expect(allFlags).not.toHaveProperty('dev-test-flag'); + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; + + expect(ldc.allFlags()).not.toHaveProperty('dev-test-flag'); expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); expect(mockPlatform.storage.set).toHaveBeenNthCalledWith( 1, @@ -275,12 +263,21 @@ describe('sdk-client storage', () => { variation: 1, trackEvents: false, }; - const allFlags = await identifyGetAllFlags(false, putResponse); - // wait for async code to resolve promises - await jest.runAllTimersAsync(); + const simulatedEvents = [{ data: JSON.stringify(putResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); + + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; - expect(allFlags).toMatchObject({ 'another-dev-test-flag': false }); + expect(ldc.allFlags()).toMatchObject({ 'another-dev-test-flag': false }); expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); expect(mockPlatform.storage.set).toHaveBeenNthCalledWith( 1, @@ -299,9 +296,21 @@ describe('sdk-client storage', () => { const putResponse = clone(defaultPutResponse); putResponse['dev-test-flag'].version = 999; putResponse['dev-test-flag'].value = false; - const allFlags = await identifyGetAllFlags(false, putResponse); - expect(allFlags).toMatchObject({ 'dev-test-flag': false }); + const simulatedEvents = [{ data: JSON.stringify(putResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); + + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; + + expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': false }); expect(emitter.emit).toHaveBeenNthCalledWith(2, 'change', context, ['dev-test-flag']); }); @@ -313,13 +322,22 @@ describe('sdk-client storage', () => { putResponse['dev-test-flag'].value = false; putResponse['another-dev-test-flag'] = newFlag; delete putResponse['moonshot-demo']; - const allFlags = await identifyGetAllFlags(false, putResponse); - // wait for async code to resolve promises - await jest.runAllTimersAsync(); + const simulatedEvents = [{ data: JSON.stringify(putResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); + + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; - expect(allFlags).toMatchObject({ 'dev-test-flag': false, 'another-dev-test-flag': true }); - expect(allFlags).not.toHaveProperty('moonshot-demo'); + expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': false, 'another-dev-test-flag': true }); + expect(ldc.allFlags()).not.toHaveProperty('moonshot-demo'); expect(emitter.emit).toHaveBeenNthCalledWith(2, 'change', context, [ 'moonshot-demo', 'dev-test-flag', @@ -328,15 +346,16 @@ describe('sdk-client storage', () => { }); test('syncing storage when PUT is consistent so no change', async () => { - const allFlags = await identifyGetAllFlags( - false, - defaultPutResponse, - undefined, - undefined, - false, + const simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, ); - // wait for async code to resolve promises + await ldc.identify(context); await jest.runAllTimersAsync(); expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); @@ -356,7 +375,7 @@ describe('sdk-client storage', () => { expect(emitter.emit).toHaveBeenNthCalledWith(1, 'change', context, defaultFlagKeys); // this is defaultPutResponse - expect(allFlags).toEqual({ + expect(ldc.allFlags()).toEqual({ 'dev-test-flag': true, 'easter-i-tunes-special': false, 'easter-specials': 'no specials', @@ -372,13 +391,22 @@ describe('sdk-client storage', () => { const putResponse = clone(defaultPutResponse); putResponse['dev-test-flag'].reason = { kind: 'RULE_MATCH', inExperiment: true }; - const allFlags = await identifyGetAllFlags(false, putResponse); + const simulatedEvents = [{ data: JSON.stringify(putResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); + + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; - // wait for async code to resolve promises - await jest.runAllTimersAsync(); const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; - expect(allFlags).toMatchObject({ 'dev-test-flag': true }); + expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': true }); expect(flagsInStorage['dev-test-flag'].reason).toEqual({ kind: 'RULE_MATCH', inExperiment: true, @@ -395,13 +423,23 @@ describe('sdk-client storage', () => { patchResponse.value = false; patchResponse.version += 1; - const allFlags = await identifyGetAllFlags(false, defaultPutResponse, patchResponse); + const putEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const patchEvents = [{ data: JSON.stringify(patchResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', putEvents); + mockEventSource.simulateEvents('patch', patchEvents); + return mockEventSource; + }, + ); - // wait for async code to resolve promises - await jest.runAllTimersAsync(); + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; - expect(allFlags).toMatchObject({ 'dev-test-flag': false }); + expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': false }); expect(mockPlatform.storage.set).toHaveBeenCalledTimes(4); expect(flagsInStorage['dev-test-flag'].version).toEqual(patchResponse.version); expect(emitter.emit).toHaveBeenCalledTimes(2); @@ -412,13 +450,23 @@ describe('sdk-client storage', () => { const patchResponse = clone(defaultPutResponse['dev-test-flag']); patchResponse.key = 'another-dev-test-flag'; - const allFlags = await identifyGetAllFlags(false, defaultPutResponse, patchResponse); + const putEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const patchEvents = [{ data: JSON.stringify(patchResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', putEvents); + mockEventSource.simulateEvents('patch', patchEvents); + return mockEventSource; + }, + ); - // wait for async code to resolve promises - await jest.runAllTimersAsync(); + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; - expect(allFlags).toHaveProperty('another-dev-test-flag'); + expect(ldc.allFlags()).toHaveProperty('another-dev-test-flag'); expect(mockPlatform.storage.set).toHaveBeenNthCalledWith( 4, flagStorageKey, @@ -435,19 +483,27 @@ describe('sdk-client storage', () => { patchResponse.value = false; patchResponse.version -= 1; - const allFlags = await identifyGetAllFlags( - false, - defaultPutResponse, - patchResponse, - undefined, - false, + const putEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const patchEvents = [{ data: JSON.stringify(patchResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', putEvents); + mockEventSource.simulateEvents('patch', patchEvents); + return mockEventSource; + }, ); - expect(mockPlatform.storage.set).toHaveBeenCalledTimes(0); + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; + + // the initial put is resulting in two sets, one for the index and one for the flag data + expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); expect(emitter.emit).not.toHaveBeenCalledWith('change'); // this is defaultPutResponse - expect(allFlags).toEqual({ + expect(ldc.allFlags()).toEqual({ 'dev-test-flag': true, 'easter-i-tunes-special': false, 'easter-specials': 'no specials', @@ -465,18 +521,23 @@ describe('sdk-client storage', () => { version: defaultPutResponse['dev-test-flag'].version + 1, }; - const allFlags = await identifyGetAllFlags( - false, - defaultPutResponse, - undefined, - deleteResponse, + const putEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const deleteEvents = [{ data: JSON.stringify(deleteResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', putEvents); + mockEventSource.simulateEvents('delete', deleteEvents); + return mockEventSource; + }, ); - // wait for async code to resolve promises - await jest.runAllTimersAsync(); + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; - expect(allFlags).not.toHaveProperty('dev-test-flag'); + expect(ldc.allFlags()).not.toHaveProperty('dev-test-flag'); expect(mockPlatform.storage.set).toHaveBeenNthCalledWith( 4, flagStorageKey, @@ -493,16 +554,24 @@ describe('sdk-client storage', () => { version: defaultPutResponse['dev-test-flag'].version, }; - const allFlags = await identifyGetAllFlags( - false, - defaultPutResponse, - undefined, - deleteResponse, - false, + const putEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const deleteEvents = [{ data: JSON.stringify(deleteResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', putEvents); + mockEventSource.simulateEvents('delete', deleteEvents); + return mockEventSource; + }, ); - expect(allFlags).toHaveProperty('dev-test-flag'); - expect(mockPlatform.storage.set).toHaveBeenCalledTimes(0); + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; + + expect(ldc.allFlags()).toHaveProperty('dev-test-flag'); + // the initial put is resulting in two sets, one for the index and one for the flag data + expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); expect(emitter.emit).not.toHaveBeenCalledWith('change'); }); @@ -512,16 +581,24 @@ describe('sdk-client storage', () => { version: defaultPutResponse['dev-test-flag'].version - 1, }; - const allFlags = await identifyGetAllFlags( - false, - defaultPutResponse, - undefined, - deleteResponse, - false, + const putEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const deleteEvents = [{ data: JSON.stringify(deleteResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', putEvents); + mockEventSource.simulateEvents('delete', deleteEvents); + return mockEventSource; + }, ); - expect(allFlags).toHaveProperty('dev-test-flag'); - expect(mockPlatform.storage.set).toHaveBeenCalledTimes(0); + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; + + expect(ldc.allFlags()).toHaveProperty('dev-test-flag'); + // the initial put is resulting in two sets, one for the index and one for the flag data + expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); expect(emitter.emit).not.toHaveBeenCalledWith('change'); }); @@ -531,10 +608,20 @@ describe('sdk-client storage', () => { version: 1, }; - await identifyGetAllFlags(false, defaultPutResponse, undefined, deleteResponse, false); + const putEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const deleteEvents = [{ data: JSON.stringify(deleteResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', putEvents); + mockEventSource.simulateEvents('delete', deleteEvents); + return mockEventSource; + }, + ); - // wait for async code to resolve promises - await jest.runAllTimersAsync(); + const changePromise = onChangePromise(); + await ldc.identify(context); + await changePromise; const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index 6fd145f61..ad9df36b3 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -1,29 +1,11 @@ -import { AutoEnvAttributes, clone, Hasher, LDContext } from '@launchdarkly/js-sdk-common'; -import { - createBasicPlatform, - createLogger, - MockStreamingProcessor, - setupMockStreamingProcessor, -} from '@launchdarkly/private-js-mocks'; +import { AutoEnvAttributes, clone, Encoding, Hasher, LDContext } from '@launchdarkly/js-sdk-common'; +import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; +import { MockEventSource } from './LDClientImpl.mocks'; import { Flags } from './types'; -jest.mock('@launchdarkly/js-sdk-common', () => { - const actual = jest.requireActual('@launchdarkly/js-sdk-common'); - const actualMock = jest.requireActual('@launchdarkly/private-js-mocks'); - return { - ...actual, - ...{ - internal: { - ...actual.internal, - StreamingProcessor: actualMock.MockStreamingProcessor, - }, - }, - }; -}); - const testSdkKey = 'test-sdk-key'; const context: LDContext = { kind: 'org', key: 'Testy Pizza' }; const autoEnv = { @@ -41,8 +23,11 @@ const autoEnv = { os: { name: 'An OS', version: '1.0.1', family: 'orange' }, }, }; + describe('sdk-client object', () => { let ldc: LDClientImpl; + let mockEventSource: MockEventSource; + let simulatedEvents: { data?: any }[] = []; let defaultPutResponse: Flags; let mockPlatform: ReturnType; let logger: ReturnType; @@ -51,7 +36,6 @@ describe('sdk-client object', () => { mockPlatform = createBasicPlatform(); logger = createLogger(); defaultPutResponse = clone(mockResponseJson); - setupMockStreamingProcessor(false, defaultPutResponse); mockPlatform.crypto.randomUUID.mockReturnValue('random1'); const hasher = { update: jest.fn((): Hasher => hasher), @@ -59,13 +43,28 @@ describe('sdk-client object', () => { }; mockPlatform.crypto.createHash.mockReturnValue(hasher); + jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + }); + + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); + ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, mockPlatform, { logger, sendEvents: false, }); - jest - .spyOn(LDClientImpl.prototype as any, 'createStreamUriPath') - .mockReturnValue('/stream/path'); }); afterEach(async () => { @@ -90,11 +89,18 @@ describe('sdk-client object', () => { }); test('identify success', async () => { - defaultPutResponse['dev-test-flag'].value = false; const carContext: LDContext = { kind: 'car', key: 'test-car' }; mockPlatform.crypto.randomUUID.mockReturnValue('random1'); + // need reference within test to run assertions against + const mockCreateEventSource = jest.fn((streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', [{ data: JSON.stringify(defaultPutResponse) }]); + return mockEventSource; + }); + mockPlatform.requests.createEventSource = mockCreateEventSource; + await ldc.identify(carContext); const c = ldc.getContext(); const all = ldc.allFlags(); @@ -105,20 +111,26 @@ describe('sdk-client object', () => { ...autoEnv, }); expect(all).toMatchObject({ - 'dev-test-flag': false, + 'dev-test-flag': true, }); - expect(MockStreamingProcessor).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - '/stream/path', - expect.anything(), - undefined, + + expect(mockCreateEventSource).toHaveBeenCalledWith( + expect.stringContaining('/stream/path'), expect.anything(), ); }); test('identify success withReasons', async () => { const carContext: LDContext = { kind: 'car', key: 'test-car' }; + + // need reference within test to run assertions against + const mockCreateEventSource = jest.fn((streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', [{ data: JSON.stringify(defaultPutResponse) }]); + return mockEventSource; + }); + mockPlatform.requests.createEventSource = mockCreateEventSource; + ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, mockPlatform, { logger, sendEvents: false, @@ -127,18 +139,16 @@ describe('sdk-client object', () => { await ldc.identify(carContext); - expect(MockStreamingProcessor).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - '/stream/path?withReasons=true', - expect.anything(), - undefined, + expect(mockCreateEventSource).toHaveBeenCalledWith( + expect.stringContaining('?withReasons=true'), expect.anything(), ); }); test('identify success without auto env', async () => { defaultPutResponse['dev-test-flag'].value = false; + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const carContext: LDContext = { kind: 'car', key: 'test-car' }; ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Disabled, mockPlatform, { logger, @@ -157,6 +167,8 @@ describe('sdk-client object', () => { test('identify anonymous', async () => { defaultPutResponse['dev-test-flag'].value = false; + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + const carContext: LDContext = { kind: 'car', anonymous: true, key: '' }; mockPlatform.crypto.randomUUID.mockReturnValue('random1'); @@ -184,7 +196,14 @@ describe('sdk-client object', () => { }); test('identify error stream error', async () => { - setupMockStreamingProcessor(true); + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateError({ status: 404, message: 'test-error' }); + return mockEventSource; + }, + ); + const carContext: LDContext = { kind: 'car', key: 'test-car' }; await expect(ldc.identify(carContext)).rejects.toThrow('test-error'); diff --git a/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts b/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts index 9ed81aca5..5ca99b65e 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts @@ -1,13 +1,10 @@ -import { AutoEnvAttributes, clone, LDContext } from '@launchdarkly/js-sdk-common'; -import { - createBasicPlatform, - createLogger, - setupMockStreamingProcessor, -} from '@launchdarkly/private-js-mocks'; +import { AutoEnvAttributes, clone, Encoding, LDContext } from '@launchdarkly/js-sdk-common'; +import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import { toMulti } from './context/addAutoEnv'; import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; +import { MockEventSource } from './LDClientImpl.mocks'; import { Flags } from './types'; let mockPlatform: ReturnType; @@ -18,24 +15,12 @@ beforeEach(() => { logger = createLogger(); }); -jest.mock('@launchdarkly/js-sdk-common', () => { - const actual = jest.requireActual('@launchdarkly/js-sdk-common'); - const m = jest.requireActual('@launchdarkly/private-js-mocks'); - return { - ...actual, - ...{ - internal: { - ...actual.internal, - StreamingProcessor: m.MockStreamingProcessor, - }, - }, - }; -}); - const testSdkKey = 'test-sdk-key'; const carContext: LDContext = { kind: 'car', key: 'test-car' }; let ldc: LDClientImpl; +let mockEventSource: MockEventSource; +let simulatedEvents: { data?: any }[] = []; let defaultPutResponse: Flags; const DEFAULT_IDENTIFY_TIMEOUT = 5; @@ -48,30 +33,38 @@ describe('sdk-client identify timeout', () => { beforeEach(() => { defaultPutResponse = clone(mockResponseJson); - // simulate streaming error after a long timeout - setupMockStreamingProcessor(true, defaultPutResponse, undefined, undefined, 30); + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, mockPlatform, { logger, sendEvents: false, }); - jest - .spyOn(LDClientImpl.prototype as any, 'createStreamUriPath') - .mockReturnValue('/stream/path'); + jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + }); }); afterEach(() => { jest.resetAllMocks(); }); - // streaming is setup to error in beforeEach to cause a timeout test('rejects with default timeout of 5s', async () => { 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/)); }); - // streaming is setup to error in beforeEach to cause a timeout test('rejects with custom timeout', async () => { const timeout = 15; jest.advanceTimersByTimeAsync(timeout * 1000).then(); @@ -79,7 +72,9 @@ describe('sdk-client identify timeout', () => { }); test('resolves with default timeout', async () => { - setupMockStreamingProcessor(false, defaultPutResponse); + // set simulated events to be default response + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + jest.advanceTimersByTimeAsync(DEFAULT_IDENTIFY_TIMEOUT * 1000).then(); await expect(ldc.identify(carContext)).resolves.toBeUndefined(); @@ -99,7 +94,10 @@ describe('sdk-client identify timeout', () => { test('resolves with custom timeout', async () => { const timeout = 15; - setupMockStreamingProcessor(false, defaultPutResponse); + + // set simulated events to be default response + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + jest.advanceTimersByTimeAsync(timeout).then(); await expect(ldc.identify(carContext, { timeout })).resolves.toBeUndefined(); @@ -119,7 +117,10 @@ describe('sdk-client identify timeout', () => { test('setting high timeout threshold with internalOptions', async () => { const highTimeoutThreshold = 20; - setupMockStreamingProcessor(false, defaultPutResponse); + + // set simulated events to be default response + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + ldc = new LDClientImpl( testSdkKey, AutoEnvAttributes.Enabled, @@ -139,7 +140,10 @@ describe('sdk-client identify timeout', () => { test('warning when timeout is too high', async () => { const highTimeout = 60; - setupMockStreamingProcessor(false, defaultPutResponse); + + // set simulated events to be default response + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + jest.advanceTimersByTimeAsync(highTimeout * 1000).then(); await ldc.identify(carContext, { timeout: highTimeout }); @@ -148,7 +152,9 @@ describe('sdk-client identify timeout', () => { }); test('safe timeout should not warn', async () => { - setupMockStreamingProcessor(false, defaultPutResponse); + // set simulated events to be default response + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + jest.advanceTimersByTimeAsync(DEFAULT_IDENTIFY_TIMEOUT * 1000).then(); await ldc.identify(carContext, { timeout: DEFAULT_IDENTIFY_TIMEOUT }); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 78d8b3de8..428c87af2 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -3,6 +3,7 @@ import { ClientContext, clone, Context, + Encoding, internal, LDClientError, LDContext, @@ -31,6 +32,7 @@ import EventFactory from './events/EventFactory'; import FlagManager from './flag-manager/FlagManager'; import { ItemDescriptor } from './flag-manager/ItemDescriptor'; import PollingProcessor from './polling/PollingProcessor'; +import { StreamingPaths, StreamingProcessor } from './streaming'; import { DeleteFlag, Flags, PatchFlag } from './types'; const { createErrorEvaluationDetail, createSuccessEvaluationDetail, ClientMessages, ErrorKinds } = @@ -256,17 +258,19 @@ export default class LDClientImpl implements LDClient { return listeners; } - /** - * Generates the url path for streaming. - * - * @protected This function must be overridden in subclasses for streaming - * to work. - * @param _context The LDContext object - */ - protected createStreamUriPath(_context: LDContext): string { - throw new Error( - 'createStreamUriPath not implemented. Client sdks must implement createStreamUriPath for streaming to work.', - ); + protected getStreamingPaths(): StreamingPaths { + return { + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + throw new Error( + 'getStreamingPaths not implemented. Client sdks must implement getStreamingPaths for streaming with GET to work.', + ); + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + throw new Error( + 'getStreamingPaths not implemented. Client sdks must implement getStreamingPaths for streaming with REPORT to work.', + ); + }, + }; } /** @@ -436,16 +440,21 @@ export default class LDClientImpl implements LDClient { identifyResolve: any, identifyReject: any, ) { - let streamingPath = this.createStreamUriPath(context); - if (this.config.withReasons) { - streamingPath = `${streamingPath}?withReasons=true`; - } - - this.updateProcessor = new internal.StreamingProcessor( - this.sdkKey, - this.clientContext, - streamingPath, + this.updateProcessor = new StreamingProcessor( + JSON.stringify(context), + { + credential: this.sdkKey, + streamingEndpoint: this.config.serviceEndpoints.streaming, + paths: this.getStreamingPaths(), + tags: this.clientContext.basicConfiguration.tags, + info: this.platform.info, + initialRetryDelayMillis: 1000, + withReasons: this.config.withReasons, + useReport: this.config.useReport, + }, this.createStreamListeners(checkedContext, identifyResolve), + this.platform.requests, + this.platform.encoding!, this.diagnosticsManager, (e) => { identifyReject(e); diff --git a/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts b/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts index 39d63065d..8caa4db0c 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts @@ -1,12 +1,15 @@ -import { AutoEnvAttributes, clone, Context, LDContext } from '@launchdarkly/js-sdk-common'; import { - createBasicPlatform, - createLogger, - setupMockStreamingProcessor, -} from '@launchdarkly/private-js-mocks'; + AutoEnvAttributes, + clone, + Context, + Encoding, + LDContext, +} from '@launchdarkly/js-sdk-common'; +import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; +import { MockEventSource } from './LDClientImpl.mocks'; import { Flags } from './types'; let mockPlatform: ReturnType; @@ -17,37 +20,39 @@ beforeEach(() => { logger = createLogger(); }); -jest.mock('@launchdarkly/js-sdk-common', () => { - const actual = jest.requireActual('@launchdarkly/js-sdk-common'); - const actualMock = jest.requireActual('@launchdarkly/private-js-mocks'); - return { - ...actual, - ...{ - internal: { - ...actual.internal, - StreamingProcessor: actualMock.MockStreamingProcessor, - }, - }, - }; -}); - const testSdkKey = 'test-sdk-key'; const context: LDContext = { kind: 'org', key: 'Testy Pizza' }; let ldc: LDClientImpl; +let mockEventSource: MockEventSource; +let simulatedEvents: { data?: any }[] = []; let defaultPutResponse: Flags; describe('sdk-client object', () => { beforeEach(() => { defaultPutResponse = clone(mockResponseJson); - setupMockStreamingProcessor(false, defaultPutResponse); + jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + }); + + simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + mockPlatform.requests.createEventSource.mockImplementation( + (streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', simulatedEvents); + return mockEventSource; + }, + ); + ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Disabled, mockPlatform, { logger, sendEvents: false, }); - jest - .spyOn(LDClientImpl.prototype as any, 'createStreamUriPath') - .mockReturnValue('/stream/path'); }); afterEach(() => { @@ -57,7 +62,6 @@ describe('sdk-client object', () => { test('variation', async () => { await ldc.identify(context); const devTestFlag = ldc.variation('dev-test-flag'); - expect(devTestFlag).toBe(true); }); @@ -67,14 +71,11 @@ describe('sdk-client object', () => { ldc.on('error', errorListener); const p = ldc.identify(context); - setTimeout(() => { - // call variation in the next tick to give ldc a chance to hook up event emitter - ldc.variation('does-not-exist', 'not-found'); - }); + ldc.variation('does-not-exist', 'not-found'); await expect(p).resolves.toBeUndefined(); - const error = errorListener.mock.calls[0][1]; expect(errorListener).toHaveBeenCalledTimes(1); + const error = errorListener.mock.calls[0][1]; expect(error.message).toMatch(/unknown feature/i); }); diff --git a/packages/shared/sdk-client/src/api/LDOptions.ts b/packages/shared/sdk-client/src/api/LDOptions.ts index 97de4f8b1..e3c8b2488 100644 --- a/packages/shared/sdk-client/src/api/LDOptions.ts +++ b/packages/shared/sdk-client/src/api/LDOptions.ts @@ -198,6 +198,14 @@ export interface LDOptions { */ pollInterval?: number; + /** + * Directs the SDK to use the REPORT method for HTTP requests instead of GET. (Default: `false`) + * + * This setting applies both to requests to the streaming service, as well as flag requests when the SDK is in polling + * mode. + */ + useReport?: boolean; + /** * Whether LaunchDarkly should provide additional information about how flag values were * calculated. diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index c8d7f9aa7..a34243f47 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -32,14 +32,14 @@ export default class Configuration { public readonly flushInterval = 30; public readonly streamInitialReconnectDelay = 1; - public readonly allAttributesPrivate = false; - public readonly debug = false; - public readonly diagnosticOptOut = false; - public readonly sendEvents = true; - public readonly sendLDHeaders = true; + public readonly allAttributesPrivate: boolean = false; + public readonly debug: boolean = false; + public readonly diagnosticOptOut: boolean = false; + public readonly sendEvents: boolean = true; + public readonly sendLDHeaders: boolean = true; - public readonly useReport = false; - public readonly withReasons = false; + public readonly useReport: boolean = false; + public readonly withReasons: boolean = false; public readonly inspectors: LDInspection[] = []; public readonly privateAttributes: string[] = []; @@ -81,6 +81,8 @@ export default class Configuration { internalOptions.diagnosticEventPath, internalOptions.includeAuthorizationHeader, ); + this.useReport = pristineOptions.useReport ?? false; + this.tags = new ApplicationTags({ application: this.applicationInfo, logger: this.logger }); } diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index db960e981..4099b9ec7 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -2,6 +2,7 @@ import LDClientImpl from './LDClientImpl'; export * as platform from '@launchdarkly/js-sdk-common'; export * from './api'; +export { StreamingPaths } from './streaming'; export * from '@launchdarkly/js-sdk-common'; diff --git a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts new file mode 100644 index 000000000..931b9e587 --- /dev/null +++ b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts @@ -0,0 +1,20 @@ +import { ApplicationTags, Encoding, Info } from '@launchdarkly/js-sdk-common'; + +export interface DataSourceConfig { + credential: string; + info: Info; + tags?: ApplicationTags; + withReasons: boolean; + useReport: boolean; +} + +export interface StreamingDataSourceConfig extends DataSourceConfig { + initialRetryDelayMillis: number; + streamingEndpoint: string; + paths: StreamingPaths; +} + +export interface StreamingPaths { + pathGet(encoding: Encoding, credential: string, plainContextString: string): string; + pathReport(encoding: Encoding, credential: string, plainContextString: string): string; +} diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts new file mode 100644 index 000000000..a7d74f6c8 --- /dev/null +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts @@ -0,0 +1,283 @@ +import { + defaultHeaders, + Encoding, + EventName, + Info, + internal, + LDStreamingError, + Platform, + ProcessStreamResponse, + Requests, + subsystem, +} from '@launchdarkly/js-sdk-common'; +import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; + +import { StreamingDataSourceConfig, StreamingProcessor } from '.'; + +let logger: ReturnType; + +const serviceEndpoints = { + events: '', + polling: '', + streaming: 'https://mockstream.ld.com', + diagnosticEventPath: '/diagnostic', + analyticsEventPath: '/bulk', + includeAuthorizationHeader: true, +}; + +const dateNowString = '2023-08-10'; +const sdkKey = 'my-sdk-key'; +const event = { + data: { + flags: { + flagkey: { key: 'flagkey', version: 1 }, + }, + segments: { + segkey: { key: 'segkey', version: 2 }, + }, + }, +}; + +let basicPlatform: Platform; + +function getStreamingDataSourceConfig(): StreamingDataSourceConfig { + return { + credential: sdkKey, + streamingEndpoint: serviceEndpoints.streaming, + paths: { + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/stream/path'; + }, + }, + tags: undefined, + info: basicPlatform.info, + initialRetryDelayMillis: 1000, + withReasons: false, + useReport: false, + }; +} + +beforeEach(() => { + basicPlatform = createBasicPlatform(); + logger = createLogger(); +}); + +const createMockEventSource = (streamUri: string = '', options: any = {}) => ({ + streamUri, + options, + onclose: jest.fn(), + addEventListener: jest.fn(), + close: jest.fn(), +}); + +describe('given a stream processor with mock event source', () => { + let info: Info; + let streamingProcessor: subsystem.LDStreamProcessor; + let diagnosticsManager: internal.DiagnosticsManager; + let listeners: Map; + let mockEventSource: any; + let mockListener: ProcessStreamResponse; + let mockErrorHandler: jest.Mock; + let simulatePutEvent: (e?: any) => void; + let simulateError: (e: { status: number; message: string }) => boolean; + + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date(dateNowString)); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + mockErrorHandler = jest.fn(); + + info = basicPlatform.info; + + basicPlatform.requests = { + createEventSource: jest.fn((streamUri: string, options: any) => { + mockEventSource = createMockEventSource(streamUri, options); + return mockEventSource; + }), + } as any; + simulatePutEvent = (e: any = event) => { + mockEventSource.addEventListener.mock.calls[0][1](e); + }; + simulateError = (e: { status: number; message: string }): boolean => + mockEventSource.options.errorFilter(e); + + listeners = new Map(); + mockListener = { + deserializeData: jest.fn((data) => data), + processJson: jest.fn(), + }; + listeners.set('put', mockListener); + listeners.set('patch', mockListener); + + diagnosticsManager = new internal.DiagnosticsManager(sdkKey, basicPlatform, {}); + + streamingProcessor = new StreamingProcessor( + 'mockContextString', + getStreamingDataSourceConfig(), + listeners, + basicPlatform.requests, + basicPlatform.encoding!, + diagnosticsManager, + mockErrorHandler, + logger, + ); + + jest.spyOn(streamingProcessor, 'stop'); + streamingProcessor.start(); + }); + + afterEach(() => { + streamingProcessor.close(); + jest.resetAllMocks(); + }); + + it('uses expected uri and eventSource init args', () => { + expect(basicPlatform.requests.createEventSource).toBeCalledWith( + `${serviceEndpoints.streaming}/stream/path`, + { + errorFilter: expect.any(Function), + headers: defaultHeaders(sdkKey, info, undefined), + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, + }, + ); + }); + + it('sets streamInitialReconnectDelay correctly', () => { + streamingProcessor = new StreamingProcessor( + 'mockContextString', + getStreamingDataSourceConfig(), + listeners, + basicPlatform.requests, + basicPlatform.encoding!, + diagnosticsManager, + mockErrorHandler, + ); + streamingProcessor.start(); + + expect(basicPlatform.requests.createEventSource).toHaveBeenLastCalledWith( + `${serviceEndpoints.streaming}/stream/path`, + { + errorFilter: expect.any(Function), + headers: defaultHeaders(sdkKey, info, undefined), + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, + }, + ); + }); + + it('adds listeners', () => { + expect(mockEventSource.addEventListener).toHaveBeenNthCalledWith( + 1, + 'put', + expect.any(Function), + ); + expect(mockEventSource.addEventListener).toHaveBeenNthCalledWith( + 2, + 'patch', + expect.any(Function), + ); + }); + + it('executes listeners', () => { + simulatePutEvent(); + const patchHandler = mockEventSource.addEventListener.mock.calls[1][1]; + patchHandler(event); + + expect(mockListener.deserializeData).toBeCalledTimes(2); + expect(mockListener.processJson).toBeCalledTimes(2); + }); + + it('passes error to callback if json data is malformed', async () => { + (mockListener.deserializeData as jest.Mock).mockReturnValue(false); + simulatePutEvent(); + + expect(logger.error).toBeCalledWith(expect.stringMatching(/invalid data in "put"/)); + expect(logger.debug).toBeCalledWith(expect.stringMatching(/invalid json/i)); + expect(mockErrorHandler.mock.lastCall[0].message).toMatch(/malformed json/i); + }); + + it('calls error handler if event.data prop is missing', async () => { + simulatePutEvent({ flags: {} }); + + expect(mockListener.deserializeData).not.toBeCalled(); + expect(mockListener.processJson).not.toBeCalled(); + expect(mockErrorHandler.mock.lastCall[0].message).toMatch(/unexpected payload/i); + }); + + it('closes and stops', async () => { + streamingProcessor.close(); + + expect(streamingProcessor.stop).toBeCalled(); + expect(mockEventSource.close).toBeCalled(); + // @ts-ignore + expect(streamingProcessor.eventSource).toBeUndefined(); + }); + + it('creates a stream init event', async () => { + const startTime = Date.now(); + simulatePutEvent(); + + const diagnosticEvent = diagnosticsManager.createStatsEventAndReset(0, 0, 0); + expect(diagnosticEvent.streamInits.length).toEqual(1); + const si = diagnosticEvent.streamInits[0]; + expect(si.timestamp).toEqual(startTime); + expect(si.failed).toBeFalsy(); + expect(si.durationMillis).toBeGreaterThanOrEqual(0); + }); + + describe.each([400, 408, 429, 500, 503])('given recoverable http errors', (status) => { + it(`continues retrying after error: ${status}`, () => { + const startTime = Date.now(); + const testError = { status, message: 'retry. recoverable.' }; + const willRetry = simulateError(testError); + + expect(willRetry).toBeTruthy(); + expect(mockErrorHandler).not.toBeCalled(); + expect(logger.warn).toBeCalledWith( + expect.stringMatching(new RegExp(`${status}.*will retry`)), + ); + + const diagnosticEvent = diagnosticsManager.createStatsEventAndReset(0, 0, 0); + expect(diagnosticEvent.streamInits.length).toEqual(1); + const si = diagnosticEvent.streamInits[0]; + expect(si.timestamp).toEqual(startTime); + expect(si.failed).toBeTruthy(); + expect(si.durationMillis).toBeGreaterThanOrEqual(0); + }); + }); + + describe.each([401, 403])('given irrecoverable http errors', (status) => { + it(`stops retrying after error: ${status}`, () => { + const startTime = Date.now(); + const testError = { status, message: 'stopping. irrecoverable.' }; + const willRetry = simulateError(testError); + + expect(willRetry).toBeFalsy(); + expect(mockErrorHandler).toBeCalledWith( + new LDStreamingError(testError.message, testError.status), + ); + expect(logger.error).toBeCalledWith( + expect.stringMatching(new RegExp(`${status}.*permanently`)), + ); + + const diagnosticEvent = diagnosticsManager.createStatsEventAndReset(0, 0, 0); + expect(diagnosticEvent.streamInits.length).toEqual(1); + const si = diagnosticEvent.streamInits[0]; + expect(si.timestamp).toEqual(startTime); + expect(si.failed).toBeTruthy(); + expect(si.durationMillis).toBeGreaterThanOrEqual(0); + }); + }); +}); diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts new file mode 100644 index 000000000..5e1f00d56 --- /dev/null +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts @@ -0,0 +1,166 @@ +import { + defaultHeaders, + Encoding, + EventName, + EventSource, + httpErrorMessage, + HttpErrorResponse, + internal, + LDLogger, + LDStreamingError, + ProcessStreamResponse, + Requests, + shouldRetry, + subsystem, +} from '@launchdarkly/js-sdk-common'; + +import { StreamingDataSourceConfig } from './DataSourceConfig'; + +const reportJsonError = ( + type: string, + data: string, + logger?: LDLogger, + errorHandler?: internal.StreamingErrorHandler, +) => { + logger?.error(`Stream received invalid data in "${type}" message`); + logger?.debug(`Invalid JSON follows: ${data}`); + errorHandler?.(new LDStreamingError('Malformed JSON data in event stream')); +}; + +class StreamingProcessor implements subsystem.LDStreamProcessor { + private readonly headers: { [key: string]: string | string[] }; + private readonly streamUri: string; + + private eventSource?: EventSource; + private connectionAttemptStartTime?: number; + + constructor( + private readonly plainContextString: string, + private readonly dataSourceConfig: StreamingDataSourceConfig, + private readonly listeners: Map, + private readonly requests: Requests, + encoding: Encoding, + private readonly diagnosticsManager?: internal.DiagnosticsManager, + private readonly errorHandler?: internal.StreamingErrorHandler, + private readonly logger?: LDLogger, + ) { + let path = dataSourceConfig.useReport + ? dataSourceConfig.paths.pathReport(encoding, dataSourceConfig.credential, plainContextString) + : dataSourceConfig.paths.pathGet(encoding, dataSourceConfig.credential, plainContextString); + + if (dataSourceConfig.withReasons) { + path = `${path}?withReasons=true`; + } + + this.headers = defaultHeaders( + dataSourceConfig.credential, + dataSourceConfig.info, + dataSourceConfig.tags, + ); + + this.logger = logger; + this.requests = requests; + this.streamUri = `${dataSourceConfig.streamingEndpoint}${path}`; + } + + private logConnectionStarted() { + this.connectionAttemptStartTime = Date.now(); + } + + private logConnectionResult(success: boolean) { + if (this.connectionAttemptStartTime && this.diagnosticsManager) { + this.diagnosticsManager.recordStreamInit( + this.connectionAttemptStartTime, + !success, + Date.now() - this.connectionAttemptStartTime, + ); + } + + this.connectionAttemptStartTime = undefined; + } + + /** + * This is a wrapper around the passed errorHandler which adds additional + * diagnostics and logging logic. + * + * @param err The error to be logged and handled. + * @return boolean whether to retry the connection. + * + * @private + */ + private retryAndHandleError(err: HttpErrorResponse) { + if (!shouldRetry(err)) { + this.logConnectionResult(false); + this.errorHandler?.(new LDStreamingError(err.message, err.status)); + this.logger?.error(httpErrorMessage(err, 'streaming request')); + return false; + } + + this.logger?.warn(httpErrorMessage(err, 'streaming request', 'will retry')); + this.logConnectionResult(false); + this.logConnectionStarted(); + return true; + } + + start() { + this.logConnectionStarted(); + + // TLS is handled by the platform implementation. + const eventSource = this.requests.createEventSource(this.streamUri, { + headers: this.headers, + ...(this.dataSourceConfig.useReport && { method: 'REPORT', body: this.plainContextString }), + errorFilter: (error: HttpErrorResponse) => this.retryAndHandleError(error), + initialRetryDelayMillis: this.dataSourceConfig.initialRetryDelayMillis, + readTimeoutMillis: 5 * 60 * 1000, + retryResetIntervalMillis: 60 * 1000, + }); + this.eventSource = eventSource; + + eventSource.onclose = () => { + this.logger?.info('Closed LaunchDarkly stream connection'); + }; + + eventSource.onerror = () => { + // The work is done by `errorFilter`. + }; + + eventSource.onopen = () => { + this.logger?.info('Opened LaunchDarkly stream connection'); + }; + + eventSource.onretrying = (e) => { + this.logger?.info(`Will retry stream connection in ${e.delayMillis} milliseconds`); + }; + + this.listeners.forEach(({ deserializeData, processJson }, eventName) => { + eventSource.addEventListener(eventName, (event) => { + this.logger?.debug(`Received ${eventName} event`); + + if (event?.data) { + this.logConnectionResult(true); + const { data } = event; + const dataJson = deserializeData(data); + + if (!dataJson) { + reportJsonError(eventName, data, this.logger, this.errorHandler); + return; + } + processJson(dataJson); + } else { + this.errorHandler?.(new LDStreamingError('Unexpected payload from event stream')); + } + }); + }); + } + + stop() { + this.eventSource?.close(); + this.eventSource = undefined; + } + + close() { + this.stop(); + } +} + +export default StreamingProcessor; diff --git a/packages/shared/sdk-client/src/streaming/index.ts b/packages/shared/sdk-client/src/streaming/index.ts new file mode 100644 index 000000000..c8fc9ad30 --- /dev/null +++ b/packages/shared/sdk-client/src/streaming/index.ts @@ -0,0 +1,5 @@ +import { StreamingDataSourceConfig, StreamingPaths } from './DataSourceConfig'; +import StreamingProcessor from './StreamingProcessor'; + +export { StreamingPaths }; +export { StreamingProcessor, StreamingDataSourceConfig }; From d35efdcd9be9c2ebc9b44a404af1d4cf681f2d08 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 9 Sep 2024 15:31:07 -0500 Subject: [PATCH 02/15] moving EventSourceMock to __tests__ folder. --- .../shared/sdk-client/__tests__/LDClientImpl.events.test.ts | 2 +- .../shared/sdk-client/__tests__/LDClientImpl.storage.test.ts | 4 ++-- packages/shared/sdk-client/__tests__/LDClientImpl.test.ts | 2 +- .../shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts | 2 +- .../sdk-client/__tests__/LDClientImpl.variation.test.ts | 2 +- .../{src => __tests__/streaming}/LDClientImpl.mocks.ts | 0 .../{src => __tests__}/streaming/StreamingProcessor.test.ts | 2 +- packages/shared/sdk-client/__tests__/streaming/index.ts | 1 + packages/shared/sdk-client/src/index.ts | 1 - 9 files changed, 8 insertions(+), 8 deletions(-) rename packages/shared/sdk-client/{src => __tests__/streaming}/LDClientImpl.mocks.ts (100%) rename packages/shared/sdk-client/{src => __tests__}/streaming/StreamingProcessor.test.ts (99%) create mode 100644 packages/shared/sdk-client/__tests__/streaming/index.ts diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts index e9d4677ac..523ef2029 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts @@ -14,9 +14,9 @@ import { } from '@launchdarkly/private-js-mocks'; import LDClientImpl from '../src/LDClientImpl'; -import { MockEventSource } from '../src/LDClientImpl.mocks'; import { Flags } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; +import { MockEventSource } from './streaming/LDClientImpl.mocks'; type InputCustomEvent = internal.InputCustomEvent; type InputIdentifyEvent = internal.InputIdentifyEvent; diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts index 218841f66..b92713426 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts @@ -2,11 +2,11 @@ import { AutoEnvAttributes, clone, Encoding, type LDContext } from '@launchdarkl import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import { toMulti } from '../src/context/addAutoEnv'; -import { MockEventSource } from '../src/LDClientImpl.mocks'; +import LDClientImpl from '../src/LDClientImpl'; import LDEmitter from '../src/LDEmitter'; import { Flags, PatchFlag } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; -import LDClientImpl from '../src/LDClientImpl'; +import { MockEventSource } from './streaming/LDClientImpl.mocks'; let mockPlatform: ReturnType; let logger: ReturnType; diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index e096d4f3c..a63c4fc42 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -2,9 +2,9 @@ import { AutoEnvAttributes, clone, Encoding, Hasher, LDContext } from '@launchda import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import LDClientImpl from '../src/LDClientImpl'; -import { MockEventSource } from '../src/LDClientImpl.mocks'; import { Flags } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; +import { MockEventSource } from './streaming/LDClientImpl.mocks'; const testSdkKey = 'test-sdk-key'; const context: LDContext = { kind: 'org', key: 'Testy Pizza' }; diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts index d562c8353..592a4f2a0 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts @@ -3,9 +3,9 @@ import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mock import { toMulti } from '../src/context/addAutoEnv'; import LDClientImpl from '../src/LDClientImpl'; -import { MockEventSource } from '../src/LDClientImpl.mocks'; import { Flags } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; +import { MockEventSource } from './streaming/LDClientImpl.mocks'; let mockPlatform: ReturnType; let logger: ReturnType; diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts index 0b708738a..6dc80729f 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts @@ -8,9 +8,9 @@ import { import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import LDClientImpl from '../src/LDClientImpl'; -import { MockEventSource } from '../src/LDClientImpl.mocks'; import { Flags } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; +import { MockEventSource } from './streaming/LDClientImpl.mocks'; let mockPlatform: ReturnType; let logger: ReturnType; diff --git a/packages/shared/sdk-client/src/LDClientImpl.mocks.ts b/packages/shared/sdk-client/__tests__/streaming/LDClientImpl.mocks.ts similarity index 100% rename from packages/shared/sdk-client/src/LDClientImpl.mocks.ts rename to packages/shared/sdk-client/__tests__/streaming/LDClientImpl.mocks.ts diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts similarity index 99% rename from packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts rename to packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index 73356787b..d9f26e181 100644 --- a/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -12,7 +12,7 @@ import { } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; -import { StreamingDataSourceConfig, StreamingProcessor } from '.'; +import { StreamingDataSourceConfig, StreamingProcessor } from '../../src/streaming'; let logger: ReturnType; diff --git a/packages/shared/sdk-client/__tests__/streaming/index.ts b/packages/shared/sdk-client/__tests__/streaming/index.ts new file mode 100644 index 000000000..80375ed68 --- /dev/null +++ b/packages/shared/sdk-client/__tests__/streaming/index.ts @@ -0,0 +1 @@ +export { MockEventSource } from './LDClientImpl.mocks'; \ No newline at end of file diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index 32751c193..293904474 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -18,5 +18,4 @@ export type { export { StreamingPaths } from './streaming'; - export { LDClientImpl }; From 646f1db7bf4122c6398962d17d12c4081a8ceb73 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 9 Sep 2024 15:52:11 -0500 Subject: [PATCH 03/15] fixing some lint issues --- .../sdk-client/__tests__/streaming/StreamingProcessor.test.ts | 1 - packages/shared/sdk-client/__tests__/streaming/index.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index d9f26e181..e2c48f417 100644 --- a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -7,7 +7,6 @@ import { LDStreamingError, Platform, ProcessStreamResponse, - Requests, subsystem, } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; diff --git a/packages/shared/sdk-client/__tests__/streaming/index.ts b/packages/shared/sdk-client/__tests__/streaming/index.ts index 80375ed68..a29d015e8 100644 --- a/packages/shared/sdk-client/__tests__/streaming/index.ts +++ b/packages/shared/sdk-client/__tests__/streaming/index.ts @@ -1 +1 @@ -export { MockEventSource } from './LDClientImpl.mocks'; \ No newline at end of file +export { MockEventSource } from './LDClientImpl.mocks'; From 338c6397e0ff997b1d375c1b8e3e6196e249f612 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 9 Sep 2024 16:22:24 -0500 Subject: [PATCH 04/15] adding StreamingProcessor useReport tests --- .../streaming/StreamingProcessor.test.ts | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index e2c48f417..b6339aac5 100644 --- a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -22,6 +22,7 @@ const serviceEndpoints = { diagnosticEventPath: '/diagnostic', analyticsEventPath: '/bulk', includeAuthorizationHeader: true, + payloadFilterKey: 'testPayloadFilterKey' }; const dateNowString = '2023-08-10'; @@ -39,24 +40,27 @@ const event = { let basicPlatform: Platform; -function getStreamingDataSourceConfig(): StreamingDataSourceConfig { +function getStreamingDataSourceConfig( + withReasons: boolean = false, + useReport: boolean = false, +): StreamingDataSourceConfig { return { credential: sdkKey, // eslint-disable-next-line object-shorthand serviceEndpoints: serviceEndpoints, paths: { pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { - return '/stream/path'; + return '/stream/path/get'; }, pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { - return '/stream/path'; + return '/stream/path/report'; }, }, tags: undefined, info: basicPlatform.info, initialRetryDelayMillis: 1000, - withReasons: false, - useReport: false, + withReasons, + useReport, }; } @@ -73,9 +77,9 @@ const createMockEventSource = (streamUri: string = '', options: any = {}) => ({ close: jest.fn(), }); -describe('given a stream processor with mock event source', () => { +describe('given a stream processor', () => { let info: Info; - let streamingProcessor: subsystem.LDStreamProcessor; + let streamingProcessor: StreamingProcessor; let diagnosticsManager: internal.DiagnosticsManager; let listeners: Map; let mockEventSource: any; @@ -142,7 +146,7 @@ describe('given a stream processor with mock event source', () => { it('uses expected uri and eventSource init args', () => { expect(basicPlatform.requests.createEventSource).toBeCalledWith( - `${serviceEndpoints.streaming}/stream/path`, + `${serviceEndpoints.streaming}/stream/path/get?filter=testPayloadFilterKey`, { errorFilter: expect.any(Function), headers: defaultHeaders(sdkKey, info, undefined), @@ -166,7 +170,57 @@ describe('given a stream processor with mock event source', () => { streamingProcessor.start(); expect(basicPlatform.requests.createEventSource).toHaveBeenLastCalledWith( - `${serviceEndpoints.streaming}/stream/path`, + `${serviceEndpoints.streaming}/stream/path/get?filter=testPayloadFilterKey`, + { + errorFilter: expect.any(Function), + headers: defaultHeaders(sdkKey, info, undefined), + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, + }, + ); + }); + + it('uses the report path and modifies init dict when useReport is true ', () => { + streamingProcessor = new StreamingProcessor( + 'mockContextString', + getStreamingDataSourceConfig(true, true), + listeners, + basicPlatform.requests, + basicPlatform.encoding!, + diagnosticsManager, + mockErrorHandler, + ); + streamingProcessor.start(); + + expect(basicPlatform.requests.createEventSource).toHaveBeenLastCalledWith( + `${serviceEndpoints.streaming}/stream/path/report?withReasons=true&filter=testPayloadFilterKey`, + { + method: 'REPORT', + body: 'mockContextString', + errorFilter: expect.any(Function), + headers: defaultHeaders(sdkKey, info, undefined), + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, + }, + ); + }); + + it('withReasons and payload filter coexist', () => { + streamingProcessor = new StreamingProcessor( + 'mockContextString', + getStreamingDataSourceConfig(true, false), + listeners, + basicPlatform.requests, + basicPlatform.encoding!, + diagnosticsManager, + mockErrorHandler, + ); + streamingProcessor.start(); + + expect(basicPlatform.requests.createEventSource).toHaveBeenLastCalledWith( + `${serviceEndpoints.streaming}/stream/path/get?withReasons=true&filter=testPayloadFilterKey`, { errorFilter: expect.any(Function), headers: defaultHeaders(sdkKey, info, undefined), From 73335bacdba11d6a50c0b919730d63a12d8c760e Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Mon, 9 Sep 2024 16:28:50 -0500 Subject: [PATCH 05/15] lint fixes --- .../sdk-client/__tests__/streaming/StreamingProcessor.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index b6339aac5..e32c9fa04 100644 --- a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -7,7 +7,6 @@ import { LDStreamingError, Platform, ProcessStreamResponse, - subsystem, } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; @@ -22,7 +21,7 @@ const serviceEndpoints = { diagnosticEventPath: '/diagnostic', analyticsEventPath: '/bulk', includeAuthorizationHeader: true, - payloadFilterKey: 'testPayloadFilterKey' + payloadFilterKey: 'testPayloadFilterKey', }; const dateNowString = '2023-08-10'; From 80af9f557b3393b679d5f3574fa30a9e33b51b7e Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Tue, 10 Sep 2024 15:39:53 -0500 Subject: [PATCH 06/15] updating PollingProcessor constructor to be consistent with StreamingProcessor and using the context in the body of the request. --- .../react-native/src/ReactNativeLDClient.ts | 15 +- .../sdk-client/__tests__/LDClientImpl.test.ts | 29 ++- .../polling/PollingProcessot.test.ts | 208 ++++++++++-------- .../streaming/StreamingProcessor.test.ts | 2 +- .../shared/sdk-client/src/LDClientImpl.ts | 62 +++--- packages/shared/sdk-client/src/index.ts | 2 +- .../src/polling/PollingProcessor.ts | 53 +++-- .../sdk-client/src/polling/Requestor.ts | 8 +- .../src/streaming/DataSourceConfig.ts | 10 +- .../shared/sdk-client/src/streaming/index.ts | 9 +- 10 files changed, 229 insertions(+), 169 deletions(-) diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index 9e98c560b..cc1e4f05d 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -4,11 +4,11 @@ import { base64UrlEncode, BasicLogger, ConnectionMode, + DataSourcePaths, Encoding, internal, LDClientImpl, type LDContext, - StreamingPaths, } from '@launchdarkly/js-client-sdk-common'; import validateOptions, { filterToBaseOptions } from './options'; @@ -105,7 +105,7 @@ export default class ReactNativeLDClient extends LDClientImpl { return base64UrlEncode(JSON.stringify(context), this.platform.encoding!); } - override getStreamingPaths(): StreamingPaths { + override getStreamingPaths(): DataSourcePaths { return { pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { return `/meval/${base64UrlEncode(_plainContextString, encoding)}`; @@ -116,8 +116,15 @@ export default class ReactNativeLDClient extends LDClientImpl { }; } - override createPollUriPath(context: LDContext): string { - return `/msdk/evalx/contexts/${this.encodeContext(context)}`; + override getPollingPaths(): DataSourcePaths { + return { + pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { + return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return `/msdk/evalx/contexts`; + }, + }; } override async setConnectionMode(mode: ConnectionMode): Promise { diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index a63c4fc42..8b666a307 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -45,10 +45,10 @@ describe('sdk-client object', () => { jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { - return '/stream/path'; + return '/stream/path/get'; }, pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { - return '/stream/path'; + return '/stream/path/report'; }, }); @@ -144,6 +144,31 @@ describe('sdk-client object', () => { ); }); + test('identify success useReport', async () => { + const carContext: LDContext = { kind: 'car', key: 'test-car' }; + + // need reference within test to run assertions against + const mockCreateEventSource = jest.fn((streamUri: string = '', options: any = {}) => { + mockEventSource = new MockEventSource(streamUri, options); + mockEventSource.simulateEvents('put', [{ data: JSON.stringify(defaultPutResponse) }]); + return mockEventSource; + }); + mockPlatform.requests.createEventSource = mockCreateEventSource; + + ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, mockPlatform, { + logger, + sendEvents: false, + useReport: true, + }); + + await ldc.identify(carContext); + + expect(mockCreateEventSource).toHaveBeenCalledWith( + expect.stringContaining('/stream/path/report'), + expect.anything(), + ); + }); + test('identify success without auto env', async () => { defaultPutResponse['dev-test-flag'].value = false; simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; diff --git a/packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts index b6a98ae3e..328b3a5bc 100644 --- a/packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts @@ -1,6 +1,7 @@ import { waitFor } from '@testing-library/dom'; import { + Encoding, EventSource, EventSourceInitDict, Info, @@ -10,7 +11,8 @@ import { SdkData, } from '@launchdarkly/js-sdk-common'; -import PollingProcessor, { PollingConfig } from '../../src/polling/PollingProcessor'; +import PollingProcessor from '../../src/polling/PollingProcessor'; +import { PollingDataSourceConfig } from '../../src/streaming'; function mockResponse(value: string, statusCode: number) { const response: Response = { @@ -48,6 +50,12 @@ function makeRequests(): Requests { }; } +function makeEncoding(): Encoding { + return { + btoa: jest.fn(), + }; +} + function makeInfo(sdkData: SdkData = {}, platformData: PlatformData = {}): Info { return { sdkData: () => sdkData, @@ -55,26 +63,37 @@ function makeInfo(sdkData: SdkData = {}, platformData: PlatformData = {}): Info }; } -function makeConfig(config?: { pollInterval?: number; useReport?: boolean }): PollingConfig { +const serviceEndpoints = { + events: 'mockEventsEndpoint', + polling: 'mockPollingEndpoint', + streaming: 'mockStreamingEndpoint', + diagnosticEventPath: '/diagnostic', + analyticsEventPath: '/bulk', + includeAuthorizationHeader: true, + payloadFilterKey: 'testPayloadFilterKey', +}; + +function makeConfig( + pollInterval: number, + withReasons: boolean, + useReport: boolean, +): PollingDataSourceConfig { return { - pollInterval: config?.pollInterval ?? 60 * 5, - // eslint-disable-next-line no-console - logger: { - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), + credential: 'the-sdk-key', + serviceEndpoints, + paths: { + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/poll/path/get'; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return '/poll/path/report'; + }, }, tags: {}, - useReport: config?.useReport ?? false, - serviceEndpoints: { - streaming: '', - polling: 'http://example.example.example', - events: '', - analyticsEventPath: '', - diagnosticEventPath: '', - includeAuthorizationHeader: false, - }, + info: makeInfo(), + withReasons, + useReport, + pollInterval, }; } @@ -82,12 +101,10 @@ it('makes no requests until it is started', () => { const requests = makeRequests(); // eslint-disable-next-line no-new new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(1, true, false), requests, - makeInfo(), - '/polling', - [], - makeConfig(), + makeEncoding(), (_flags) => {}, (_error) => {}, ); @@ -99,12 +116,10 @@ it('polls immediately when started', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(1, true, false), requests, - makeInfo(), - '/polling', - [], - makeConfig(), + makeEncoding(), (_flags) => {}, (_error) => {}, ); @@ -120,12 +135,10 @@ it('calls callback on success', async () => { const errorCallback = jest.fn(); const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(1000, true, false), requests, - makeInfo(), - '/polling', - [], - makeConfig(), + makeEncoding(), dataCallback, errorCallback, ); @@ -142,12 +155,10 @@ it('polls repeatedly', async () => { requests.fetch = mockFetch('{ "flagA": true }', 200); const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(0.1, true, false), requests, - makeInfo(), - '/polling', - [], - makeConfig({ pollInterval: 0.1 }), + makeEncoding(), dataCallback, errorCallback, ); @@ -174,12 +185,10 @@ it('stops polling when stopped', (done) => { const errorCallback = jest.fn(); const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(0.01, true, false), requests, - makeInfo(), - '/stops', - [], - makeConfig({ pollInterval: 0.01 }), + makeEncoding(), dataCallback, errorCallback, ); @@ -196,16 +205,17 @@ it('stops polling when stopped', (done) => { it('includes the correct headers on requests', () => { const requests = makeRequests(); + const config = makeConfig(1, true, false); + config.info = makeInfo({ + userAgentBase: 'AnSDK', + version: '42', + }); + const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + config, requests, - makeInfo({ - userAgentBase: 'AnSDK', - version: '42', - }), - '/polling', - [], - makeConfig(), + makeEncoding(), (_flags) => {}, (_error) => {}, ); @@ -223,16 +233,14 @@ it('includes the correct headers on requests', () => { polling.stop(); }); -it('defaults to using the "GET" verb', () => { +it('defaults to using the "GET" method', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(1000, true, false), requests, - makeInfo(), - '/polling', - [], - makeConfig(), + makeEncoding(), (_flags) => {}, (_error) => {}, ); @@ -242,21 +250,20 @@ it('defaults to using the "GET" verb', () => { expect.anything(), expect.objectContaining({ method: 'GET', + body: undefined, }), ); polling.stop(); }); -it('can be configured to use the "REPORT" verb', () => { +it('can be configured to use the "REPORT" method', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(1000, true, true), requests, - makeInfo(), - '/polling', - [], - makeConfig({ useReport: true }), + makeEncoding(), (_flags) => {}, (_error) => {}, ); @@ -266,6 +273,7 @@ it('can be configured to use the "REPORT" verb', () => { expect.anything(), expect.objectContaining({ method: 'REPORT', + body: 'mockContextString', }), ); polling.stop(); @@ -275,17 +283,21 @@ it('continues polling after receiving bad JSON', async () => { const requests = makeRequests(); const dataCallback = jest.fn(); const errorCallback = jest.fn(); - const config = makeConfig({ pollInterval: 0.1 }); + const logger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(0.1, true, false), requests, - makeInfo(), - '/polling', - [], - config, + makeEncoding(), dataCallback, errorCallback, + logger, ); polling.start(); @@ -296,7 +308,7 @@ it('continues polling after receiving bad JSON', async () => { requests.fetch = mockFetch('{ham', 200); await waitFor(() => expect(requests.fetch).toHaveBeenCalled()); await waitFor(() => expect(errorCallback).toHaveBeenCalled()); - expect(config.logger.error).toHaveBeenCalledWith('Polling received invalid data'); + expect(logger.error).toHaveBeenCalledWith('Polling received invalid data'); polling.stop(); }); @@ -304,17 +316,21 @@ it('continues polling after an exception thrown during a request', async () => { const requests = makeRequests(); const dataCallback = jest.fn(); const errorCallback = jest.fn(); - const config = makeConfig({ pollInterval: 0.1 }); + const logger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(0.1, true, false), requests, - makeInfo(), - '/polling', - [], - config, + makeEncoding(), dataCallback, errorCallback, + logger, ); polling.start(); @@ -327,7 +343,7 @@ it('continues polling after an exception thrown during a request', async () => { }); await waitFor(() => expect(requests.fetch).toHaveBeenCalled()); polling.stop(); - expect(config.logger.error).toHaveBeenCalledWith( + expect(logger.error).toHaveBeenCalledWith( 'Received I/O error (bad) for polling request - will retry', ); }); @@ -336,17 +352,21 @@ it('can handle recoverable http errors', async () => { const requests = makeRequests(); const dataCallback = jest.fn(); const errorCallback = jest.fn(); - const config = makeConfig({ pollInterval: 0.1 }); + const logger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(0.1, true, false), requests, - makeInfo(), - '/polling', - [], - config, + makeEncoding(), dataCallback, errorCallback, + logger, ); polling.start(); @@ -357,26 +377,28 @@ it('can handle recoverable http errors', async () => { requests.fetch = mockFetch('', 408); await waitFor(() => expect(requests.fetch).toHaveBeenCalled()); polling.stop(); - expect(config.logger.error).toHaveBeenCalledWith( - 'Received error 408 for polling request - will retry', - ); + expect(logger.error).toHaveBeenCalledWith('Received error 408 for polling request - will retry'); }); it('stops polling on unrecoverable error codes', (done) => { const requests = makeRequests(); const dataCallback = jest.fn(); const errorCallback = jest.fn(); - const config = makeConfig({ pollInterval: 0.01 }); + const logger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; const polling = new PollingProcessor( - 'the-sdk-key', + 'mockContextString', + makeConfig(0.01, true, false), requests, - makeInfo(), - '/polling', - [], - config, + makeEncoding(), dataCallback, errorCallback, + logger, ); polling.start(); @@ -385,7 +407,7 @@ it('stops polling on unrecoverable error codes', (done) => { // Polling should stop on the 401, but we need to give some time for more // polls to be done. setTimeout(() => { - expect(config.logger.error).toHaveBeenCalledWith( + expect(logger.error).toHaveBeenCalledWith( 'Received error 401 (invalid SDK key) for polling request - giving up permanently', ); expect(requests.fetch).toHaveBeenCalledTimes(1); diff --git a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index e32c9fa04..d0119da74 100644 --- a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -55,7 +55,7 @@ function getStreamingDataSourceConfig( return '/stream/path/report'; }, }, - tags: undefined, + tags: {}, info: basicPlatform.info, initialRetryDelayMillis: 1000, withReasons, diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 301532537..13c389997 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -1,6 +1,5 @@ import { AutoEnvAttributes, - ClientContext, clone, Context, Encoding, @@ -35,7 +34,8 @@ import FlagManager from './flag-manager/FlagManager'; import { ItemDescriptor } from './flag-manager/ItemDescriptor'; import LDEmitter, { EventName } from './LDEmitter'; import PollingProcessor from './polling/PollingProcessor'; -import { StreamingPaths, StreamingProcessor } from './streaming'; +import { StreamingProcessor } from './streaming'; +import { DataSourcePaths } from './streaming/DataSourceConfig'; import { DeleteFlag, Flags, PatchFlag } from './types'; const { ClientMessages, ErrorKinds } = internal; @@ -57,8 +57,6 @@ export default class LDClientImpl implements LDClient { private emitter: LDEmitter; private flagManager: FlagManager; - private readonly clientContext: ClientContext; - private eventSendingEnabled: boolean = true; private networkAvailable: boolean = true; private connectionMode: ConnectionMode; @@ -83,7 +81,6 @@ export default class LDClientImpl implements LDClient { this.config = new Configuration(options, internalOptions); this.connectionMode = this.config.initialConnectionMode; - this.clientContext = new ClientContext(sdkKey, this.config, platform); this.logger = this.config.logger; this.flagManager = new FlagManager( this.platform, @@ -260,7 +257,7 @@ export default class LDClientImpl implements LDClient { return listeners; } - protected getStreamingPaths(): StreamingPaths { + protected getStreamingPaths(): DataSourcePaths { return { pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { throw new Error( @@ -275,18 +272,19 @@ export default class LDClientImpl implements LDClient { }; } - /** - * Generates the url path for polling. - * @param _context - * - * @protected This function must be overridden in subclasses for polling - * to work. - * @param _context The LDContext object - */ - protected createPollUriPath(_context: LDContext): string { - throw new Error( - 'createPollUriPath not implemented. Client sdks must implement createPollUriPath for polling to work.', - ); + protected getPollingPaths(): DataSourcePaths { + return { + pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + throw new Error( + 'getPollingPaths not implemented. Client sdks must implement getPollingPaths for polling with GET to work.', + ); + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + throw new Error( + 'getPollingPaths not implemented. Client sdks must implement getPollingPaths for polling with REPORT to work.', + ); + }, + }; } private createIdentifyPromise(timeout: number) { @@ -405,18 +403,20 @@ export default class LDClientImpl implements LDClient { identifyResolve: any, identifyReject: any, ) { - const parameters: { key: string; value: string }[] = []; - if (this.config.withReasons) { - parameters.push({ key: 'withReasons', value: 'true' }); - } - this.updateProcessor = new PollingProcessor( - this.sdkKey, - this.clientContext.platform.requests, - this.clientContext.platform.info, - this.createPollUriPath(context), - parameters, - this.config, + JSON.stringify(context), + { + credential: this.sdkKey, + serviceEndpoints: this.config.serviceEndpoints, + paths: this.getPollingPaths(), + tags: this.config.tags, + info: this.platform.info, + pollInterval: this.config.pollInterval, + withReasons: this.config.withReasons, + useReport: this.config.useReport, + }, + this.platform.requests, + this.platform.encoding!, async (flags) => { this.logger.debug(`Handling polling result: ${Object.keys(flags)}`); @@ -450,9 +450,9 @@ export default class LDClientImpl implements LDClient { credential: this.sdkKey, serviceEndpoints: this.config.serviceEndpoints, paths: this.getStreamingPaths(), - tags: this.clientContext.basicConfiguration.tags, + tags: this.config.tags, info: this.platform.info, - initialRetryDelayMillis: 1000, + initialRetryDelayMillis: this.config.streamInitialReconnectDelay * 1000, withReasons: this.config.withReasons, useReport: this.config.useReport, }, diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index 293904474..a4186185d 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -16,6 +16,6 @@ export type { ConnectionMode, } from './api'; -export { StreamingPaths } from './streaming'; +export { DataSourcePaths } from './streaming'; export { LDClientImpl }; diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index 2b95f27fd..c29ad6d5b 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -1,43 +1,27 @@ import { - ApplicationTags, + Encoding, getPollingUri, httpErrorMessage, HttpErrorResponse, - Info, isHttpRecoverable, LDLogger, LDPollingError, Requests, - ServiceEndpoints, subsystem, } from '@launchdarkly/js-sdk-common'; +import { PollingDataSourceConfig } from '../streaming/DataSourceConfig'; import { Flags } from '../types'; import Requestor, { LDRequestError } from './Requestor'; export type PollingErrorHandler = (err: LDPollingError) => void; -/** - * Subset of configuration required for polling. - * - * @internal - */ -export type PollingConfig = { - logger: LDLogger; - pollInterval: number; - tags: ApplicationTags; - useReport: boolean; - serviceEndpoints: ServiceEndpoints; -}; - /** * @internal */ export default class PollingProcessor implements subsystem.LDStreamProcessor { private stopped = false; - private logger?: LDLogger; - private pollInterval: number; private timeoutHandle: any; @@ -45,20 +29,35 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { private requestor: Requestor; constructor( - sdkKey: string, + private readonly plainContextString: string, + private readonly dataSourceConfig: PollingDataSourceConfig, requests: Requests, - info: Info, - uriPath: string, - parameters: { key: string; value: string }[], - config: PollingConfig, + encoding: Encoding, private readonly dataHandler: (flags: Flags) => void, private readonly errorHandler?: PollingErrorHandler, + private readonly logger?: LDLogger, ) { - const uri = getPollingUri(config.serviceEndpoints, uriPath, parameters); - this.logger = config.logger; - this.pollInterval = config.pollInterval; + const path = dataSourceConfig.useReport + ? dataSourceConfig.paths.pathReport(encoding, dataSourceConfig.credential, plainContextString) + : dataSourceConfig.paths.pathGet(encoding, dataSourceConfig.credential, plainContextString); + + const parameters: { key: string; value: string }[] = []; + if (this.dataSourceConfig.withReasons) { + parameters.push({ key: 'withReasons', value: 'true' }); + } - this.requestor = new Requestor(sdkKey, requests, info, uri, config.useReport, config.tags); + const uri = getPollingUri(dataSourceConfig.serviceEndpoints, path, parameters); + this.pollInterval = dataSourceConfig.pollInterval; + + this.requestor = new Requestor( + this.dataSourceConfig.credential, + requests, + this.dataSourceConfig.info, + uri, + this.dataSourceConfig.tags, + this.dataSourceConfig.useReport ? 'REPORT' : 'GET', + this.dataSourceConfig.useReport ? plainContextString : undefined, // context is in body for REPORT + ); } private async poll() { diff --git a/packages/shared/sdk-client/src/polling/Requestor.ts b/packages/shared/sdk-client/src/polling/Requestor.ts index 6a46dfcff..cfc051b35 100644 --- a/packages/shared/sdk-client/src/polling/Requestor.ts +++ b/packages/shared/sdk-client/src/polling/Requestor.ts @@ -29,26 +29,26 @@ export class LDRequestError extends Error implements HttpErrorResponse { */ export default class Requestor { private readonly headers: { [key: string]: string }; - private verb: string; constructor( sdkKey: string, private requests: Requests, info: Info, private readonly uri: string, - useReport: boolean, tags: ApplicationTags, + private readonly method: string, + private readonly body?: string, ) { this.headers = defaultHeaders(sdkKey, info, tags); - this.verb = useReport ? 'REPORT' : 'GET'; } async requestPayload(): Promise { let status: number | undefined; try { const res = await this.requests.fetch(this.uri, { - method: this.verb, + method: this.method, headers: this.headers, + body: this.body, }); if (isOk(res.status)) { return await res.text(); diff --git a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts index 9de16d541..a77543c41 100644 --- a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts +++ b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts @@ -4,17 +4,21 @@ export interface DataSourceConfig { credential: string; serviceEndpoints: ServiceEndpoints; info: Info; - tags?: ApplicationTags; + tags: ApplicationTags; withReasons: boolean; useReport: boolean; + paths: DataSourcePaths; +} + +export interface PollingDataSourceConfig extends DataSourceConfig { + pollInterval: number; } export interface StreamingDataSourceConfig extends DataSourceConfig { initialRetryDelayMillis: number; - paths: StreamingPaths; } -export interface StreamingPaths { +export interface DataSourcePaths { pathGet(encoding: Encoding, credential: string, plainContextString: string): string; pathReport(encoding: Encoding, credential: string, plainContextString: string): string; } diff --git a/packages/shared/sdk-client/src/streaming/index.ts b/packages/shared/sdk-client/src/streaming/index.ts index c8fc9ad30..172e6c5f7 100644 --- a/packages/shared/sdk-client/src/streaming/index.ts +++ b/packages/shared/sdk-client/src/streaming/index.ts @@ -1,5 +1,8 @@ -import { StreamingDataSourceConfig, StreamingPaths } from './DataSourceConfig'; +import { + DataSourcePaths, + PollingDataSourceConfig, + StreamingDataSourceConfig, +} from './DataSourceConfig'; import StreamingProcessor from './StreamingProcessor'; -export { StreamingPaths }; -export { StreamingProcessor, StreamingDataSourceConfig }; +export { DataSourcePaths, PollingDataSourceConfig, StreamingProcessor, StreamingDataSourceConfig }; From 66f418fa339c3b076aadd90c20743dc9eea0b6fa Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Tue, 10 Sep 2024 16:21:02 -0500 Subject: [PATCH 07/15] readding runAllTimersAsync calls --- .../__tests__/LDClientImpl.storage.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts index b92713426..ad63d84b5 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts @@ -129,6 +129,7 @@ describe('sdk-client storage', () => { jest.spyOn(emitter as LDEmitter, 'emit'); await ldc.identify(context); + await jest.runAllTimersAsync(); expect(mockPlatform.storage.get).toHaveBeenLastCalledWith( expect.stringMatching('LaunchDarkly_1234567890123456_1234567890123456'), @@ -193,8 +194,8 @@ describe('sdk-client storage', () => { ); await ldc.identify(context); + await jest.runAllTimersAsync(); - expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); expect(mockPlatform.storage.set).toHaveBeenNthCalledWith( 1, indexStorageKey, @@ -236,6 +237,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); expect(ldc.allFlags()).not.toHaveProperty('dev-test-flag'); expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); @@ -276,6 +278,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); expect(ldc.allFlags()).toMatchObject({ 'another-dev-test-flag': false }); expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); @@ -309,6 +312,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': false }); expect(emitter.emit).toHaveBeenNthCalledWith(2, 'change', context, ['dev-test-flag']); @@ -335,6 +339,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': false, 'another-dev-test-flag': true }); expect(ldc.allFlags()).not.toHaveProperty('moonshot-demo'); @@ -403,9 +408,9 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; - expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': true }); expect(flagsInStorage['dev-test-flag'].reason).toEqual({ kind: 'RULE_MATCH', @@ -437,6 +442,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': false }); @@ -464,6 +470,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; expect(ldc.allFlags()).toHaveProperty('another-dev-test-flag'); @@ -497,6 +504,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); // the initial put is resulting in two sets, one for the index and one for the flag data expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2); @@ -535,6 +543,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; expect(ldc.allFlags()).not.toHaveProperty('dev-test-flag'); @@ -568,6 +577,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); expect(ldc.allFlags()).toHaveProperty('dev-test-flag'); // the initial put is resulting in two sets, one for the index and one for the flag data @@ -595,6 +605,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); expect(ldc.allFlags()).toHaveProperty('dev-test-flag'); // the initial put is resulting in two sets, one for the index and one for the flag data @@ -622,6 +633,7 @@ describe('sdk-client storage', () => { const changePromise = onChangePromise(); await ldc.identify(context); await changePromise; + await jest.runAllTimersAsync(); const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags; From 7bac549dc65c04a3acc8d2f1dc225cc22f82cfc1 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Thu, 12 Sep 2024 15:28:15 -0500 Subject: [PATCH 08/15] adding log when platform doesn't support REPORT, but config option useReport is true --- packages/shared/sdk-client/__tests__/LDClientImpl.test.ts | 7 ++++++- .../__tests__/streaming/StreamingProcessor.test.ts | 5 +++++ .../shared/sdk-client/src/streaming/StreamingProcessor.ts | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index 8b666a307..dc77114e8 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -1,4 +1,4 @@ -import { AutoEnvAttributes, clone, Encoding, Hasher, LDContext } from '@launchdarkly/js-sdk-common'; +import { AutoEnvAttributes, clone, Encoding, EventSourceCapabilities, Hasher, LDContext } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import LDClientImpl from '../src/LDClientImpl'; @@ -53,6 +53,11 @@ describe('sdk-client object', () => { }); simulatedEvents = [{ data: JSON.stringify(defaultPutResponse) }]; + mockPlatform.requests.getEventSourceCapabilities.mockImplementation(() => ({ + readTimeout: true, + headers: true, + customMethod: true, + })); mockPlatform.requests.createEventSource.mockImplementation( (streamUri: string = '', options: any = {}) => { mockEventSource = new MockEventSource(streamUri, options); diff --git a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index d0119da74..daf61a7cb 100644 --- a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -106,6 +106,11 @@ describe('given a stream processor', () => { mockEventSource = createMockEventSource(streamUri, options); return mockEventSource; }), + getEventSourceCapabilities: jest.fn(() => ({ + readTimeout: true, + headers: true, + customMethod: true, + })), } as any; simulatePutEvent = (e: any = event) => { mockEventSource.addEventListener.mock.calls[0][1](e); diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts index 7f3e6e11b..4e9d664ac 100644 --- a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts @@ -45,6 +45,13 @@ class StreamingProcessor implements subsystem.LDStreamProcessor { private readonly errorHandler?: internal.StreamingErrorHandler, private readonly logger?: LDLogger, ) { + // TODO: SC-255969 Implement better REPORT fallback logic + if (dataSourceConfig.useReport && !requests.getEventSourceCapabilities().customMethod) { + logger?.warn( + "Configuration option useReport is true, but platform's EventSource does not support custom methods. Streaming may not work.", + ); + } + const path = dataSourceConfig.useReport ? dataSourceConfig.paths.pathReport(encoding, dataSourceConfig.credential, plainContextString) : dataSourceConfig.paths.pathGet(encoding, dataSourceConfig.credential, plainContextString); From 669ae0239db957878e48604d7e6595382146534b Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 13 Sep 2024 14:26:55 -0500 Subject: [PATCH 09/15] adding content header for streaming --- packages/sdk/react-native/package.json | 2 +- .../streaming/StreamingProcessor.test.ts | 6 +++--- .../src/streaming/StreamingProcessor.ts | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index d7e91dd5f..ee7de1450 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -35,7 +35,7 @@ "test": "jest", "coverage": "yarn test --coverage", "check": "yarn prettier && yarn lint && yarn build && yarn test", - "android": "yarn && yarn ./example && yarn build && (cd example/ && yarn android-release)", + "android": "yarn && yarn ./example && yarn build && (cd example/ && yarn android)", "ios": "yarn && yarn ./example && yarn build && (cd example/ && yarn ios-go)" }, "peerDependencies": { diff --git a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index daf61a7cb..936bfff24 100644 --- a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -199,15 +199,15 @@ describe('given a stream processor', () => { expect(basicPlatform.requests.createEventSource).toHaveBeenLastCalledWith( `${serviceEndpoints.streaming}/stream/path/report?withReasons=true&filter=testPayloadFilterKey`, - { + expect.objectContaining({ method: 'REPORT', body: 'mockContextString', errorFilter: expect.any(Function), - headers: defaultHeaders(sdkKey, info, undefined), + headers: expect.objectContaining({ 'content-type': 'application/json' }), initialRetryDelayMillis: 1000, readTimeoutMillis: 300000, retryResetIntervalMillis: 60000, - }, + }), ); }); diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts index 4e9d664ac..6032576d5 100644 --- a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts @@ -114,10 +114,22 @@ class StreamingProcessor implements subsystem.LDStreamProcessor { start() { this.logConnectionStarted(); + let methodAndBodyOverrides; + if (this.dataSourceConfig.useReport) { + // REPORT will include a body, so content type is required. + this.headers['content-type'] = 'application/json'; + + // orverrides default method with REPORT and adds body. + methodAndBodyOverrides = { method: 'REPORT', body: this.plainContextString }; + } else { + // no method or body override + methodAndBodyOverrides = {}; + } + // TLS is handled by the platform implementation. const eventSource = this.requests.createEventSource(this.streamUri, { - headers: this.headers, - ...(this.dataSourceConfig.useReport && { method: 'REPORT', body: this.plainContextString }), + headers: this.headers, // adds content-type header required when body will be present + ...methodAndBodyOverrides, errorFilter: (error: HttpErrorResponse) => this.retryAndHandleError(error), initialRetryDelayMillis: this.dataSourceConfig.initialRetryDelayMillis, readTimeoutMillis: 5 * 60 * 1000, From 610d47a9504999069a2d9f48c891b89eb4cdc03f Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 13 Sep 2024 15:51:23 -0500 Subject: [PATCH 10/15] fixing lint issues --- packages/shared/sdk-client/__tests__/LDClientImpl.test.ts | 2 +- packages/shared/sdk-client/src/polling/PollingProcessor.ts | 1 - packages/shared/sdk-client/src/streaming/StreamingProcessor.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index dc77114e8..9709b70d5 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -1,4 +1,4 @@ -import { AutoEnvAttributes, clone, Encoding, EventSourceCapabilities, Hasher, LDContext } from '@launchdarkly/js-sdk-common'; +import { AutoEnvAttributes, clone, Encoding, Hasher, LDContext } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; import LDClientImpl from '../src/LDClientImpl'; diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index 7c5b4622b..995705f63 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -4,7 +4,6 @@ import { httpErrorMessage, HttpErrorResponse, isHttpRecoverable, - LDHeaders, LDLogger, LDPollingError, Requests, diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts index 956ee99d0..bd43ec592 100644 --- a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts @@ -1,5 +1,4 @@ import { - defaultHeaders, Encoding, EventName, EventSource, From 1ab46d87164a977b6a66685500222e166c0f202a Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 13 Sep 2024 16:21:23 -0500 Subject: [PATCH 11/15] Adding Polling report body support. --- packages/sdk/browser/src/BrowserClient.ts | 26 ++++++++++++++++--- .../polling/PollingProcessor.test.ts | 3 +++ .../src/polling/PollingProcessor.ts | 17 +++++++----- .../sdk-client/src/polling/Requestor.ts | 8 ++---- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 6bb99c8b1..065fecba4 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -2,6 +2,8 @@ import { AutoEnvAttributes, base64UrlEncode, LDClient as CommonClient, + DataSourcePaths, + Encoding, LDClientImpl, LDContext, LDOptions, @@ -33,11 +35,27 @@ export class BrowserClient extends LDClientImpl { return base64UrlEncode(JSON.stringify(context), this.platform.encoding!); } - override createStreamUriPath(context: LDContext) { - return `/eval/${this.clientSideId}/${this.encodeContext(context)}`; + override getStreamingPaths(): DataSourcePaths { + const parentThis = this; + return { + pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { + return `/eval/${parentThis.clientSideId}/${base64UrlEncode(_plainContextString, encoding)}`; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return `/eval/${parentThis.clientSideId}`; + }, + }; } - override createPollUriPath(context: LDContext): string { - return `/sdk/evalx/${this.clientSideId}/contexts/${this.encodeContext(context)}`; + override getPollingPaths(): DataSourcePaths { + const parentThis = this; + return { + pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { + return `/sdk/evalx/${parentThis.clientSideId}/contexts/${base64UrlEncode(_plainContextString, encoding)}`; + }, + pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + return `/sdk/evalx/${parentThis.clientSideId}/contexts`; + }, + }; } } diff --git a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts index 925e22b8f..c3108667b 100644 --- a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts @@ -277,6 +277,9 @@ it('can be configured to use the "REPORT" method', () => { expect.anything(), expect.objectContaining({ method: 'REPORT', + headers: expect.objectContaining({ + 'content-type': 'application/json', + }), body: 'mockContextString', }), ); diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index 995705f63..b88c1fca5 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -49,13 +49,16 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { const uri = getPollingUri(dataSourceConfig.serviceEndpoints, path, parameters); this.pollInterval = dataSourceConfig.pollInterval; - this.requestor = new Requestor( - requests, - uri, - dataSourceConfig.baseHeaders, - dataSourceConfig.useReport ? 'REPORT' : 'GET', - dataSourceConfig.useReport ? plainContextString : undefined, // context is in body for REPORT - ); + let method = 'GET'; + const headers: { [key: string]: string } = { ...dataSourceConfig.baseHeaders }; + let body; + if (dataSourceConfig.useReport) { + method = 'REPORT'; + headers['content-type'] = 'application/json'; + body = plainContextString; // context is in body for REPORT + } + + this.requestor = new Requestor(requests, uri, headers, method, body); } private async poll() { diff --git a/packages/shared/sdk-client/src/polling/Requestor.ts b/packages/shared/sdk-client/src/polling/Requestor.ts index b685baf14..f35e1d4c1 100644 --- a/packages/shared/sdk-client/src/polling/Requestor.ts +++ b/packages/shared/sdk-client/src/polling/Requestor.ts @@ -22,17 +22,13 @@ export class LDRequestError extends Error implements HttpErrorResponse { * @internal */ export default class Requestor { - private readonly headers: { [key: string]: string }; - constructor( private requests: Requests, private readonly uri: string, - baseHeaders: LDHeaders, + private readonly headers: { [key: string]: string }, private readonly method: string, private readonly body?: string, - ) { - this.headers = { ...baseHeaders }; - } + ) {} async requestPayload(): Promise { let status: number | undefined; From ee8bdb0263b537edf1169b1fcf2d6adb886cfd56 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 13 Sep 2024 16:32:59 -0500 Subject: [PATCH 12/15] Correcting polling report path --- packages/sdk/browser/src/BrowserClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 065fecba4..92f05fb5f 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -54,7 +54,7 @@ export class BrowserClient extends LDClientImpl { return `/sdk/evalx/${parentThis.clientSideId}/contexts/${base64UrlEncode(_plainContextString, encoding)}`; }, pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { - return `/sdk/evalx/${parentThis.clientSideId}/contexts`; + return `/sdk/evalx/${parentThis.clientSideId}/context`; }, }; } From a2465e9446a6c0150820d521623273559c2c8964 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 13 Sep 2024 16:37:28 -0500 Subject: [PATCH 13/15] more minor tweaks --- packages/sdk/react-native/src/ReactNativeLDClient.ts | 2 +- packages/shared/sdk-client/src/polling/Requestor.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index cc1e4f05d..6f7a746cd 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -122,7 +122,7 @@ export default class ReactNativeLDClient extends LDClientImpl { return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`; }, pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { - return `/msdk/evalx/contexts`; + return `/msdk/evalx/context`; }, }; } diff --git a/packages/shared/sdk-client/src/polling/Requestor.ts b/packages/shared/sdk-client/src/polling/Requestor.ts index f35e1d4c1..48ba95b26 100644 --- a/packages/shared/sdk-client/src/polling/Requestor.ts +++ b/packages/shared/sdk-client/src/polling/Requestor.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line max-classes-per-file -import { HttpErrorResponse, LDHeaders, Requests } from '@launchdarkly/js-sdk-common'; +import { HttpErrorResponse, Requests } from '@launchdarkly/js-sdk-common'; function isOk(status: number) { return status >= 200 && status <= 299; From a61bc2d25ebefae547144c7d16b05039bf6e4d75 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:04:14 -0700 Subject: [PATCH 14/15] Fix method default. --- packages/sdk/react-native/src/platform/PlatformRequests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/react-native/src/platform/PlatformRequests.ts b/packages/sdk/react-native/src/platform/PlatformRequests.ts index 2bf9e1c10..0fe8698f7 100644 --- a/packages/sdk/react-native/src/platform/PlatformRequests.ts +++ b/packages/sdk/react-native/src/platform/PlatformRequests.ts @@ -16,7 +16,7 @@ export default class PlatformRequests implements Requests { createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { return new RNEventSource(url, { - method: eventSourceInitDict.method, + method: eventSourceInitDict.method ?? 'GET', headers: eventSourceInitDict.headers, body: eventSourceInitDict.body, retryAndHandleError: eventSourceInitDict.errorFilter, From fc54863141d41ffbc8dca8dd59f027fbc05871fe Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Tue, 17 Sep 2024 11:55:51 -0500 Subject: [PATCH 15/15] Adressing review comments --- packages/sdk/browser/src/BrowserClient.ts | 8 ++++---- packages/sdk/react-native/package.json | 4 +--- packages/sdk/react-native/src/ReactNativeLDClient.ts | 8 ++++---- .../common/src/internal/stream/StreamingProcessor.ts | 1 + .../sdk-client/__tests__/LDClientImpl.events.test.ts | 4 ++-- .../sdk-client/__tests__/LDClientImpl.storage.test.ts | 4 ++-- packages/shared/sdk-client/__tests__/LDClientImpl.test.ts | 4 ++-- .../sdk-client/__tests__/LDClientImpl.timeout.test.ts | 4 ++-- .../sdk-client/__tests__/LDClientImpl.variation.test.ts | 4 ++-- .../sdk-client/__tests__/polling/PollingProcessor.test.ts | 4 ++-- .../__tests__/streaming/StreamingProcessor.test.ts | 4 ++-- packages/shared/sdk-client/src/LDClientImpl.ts | 8 ++++---- .../shared/sdk-client/src/polling/PollingProcessor.ts | 4 ++-- .../shared/sdk-client/src/streaming/DataSourceConfig.ts | 4 ++-- .../shared/sdk-client/src/streaming/StreamingProcessor.ts | 8 ++++---- 15 files changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 92f05fb5f..79153a220 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -38,10 +38,10 @@ export class BrowserClient extends LDClientImpl { override getStreamingPaths(): DataSourcePaths { const parentThis = this; return { - pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(encoding: Encoding, _plainContextString: string): string { return `/eval/${parentThis.clientSideId}/${base64UrlEncode(_plainContextString, encoding)}`; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return `/eval/${parentThis.clientSideId}`; }, }; @@ -50,10 +50,10 @@ export class BrowserClient extends LDClientImpl { override getPollingPaths(): DataSourcePaths { const parentThis = this; return { - pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(encoding: Encoding, _plainContextString: string): string { return `/sdk/evalx/${parentThis.clientSideId}/contexts/${base64UrlEncode(_plainContextString, encoding)}`; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return `/sdk/evalx/${parentThis.clientSideId}/context`; }, }; diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index ee7de1450..9abc8441e 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -34,9 +34,7 @@ "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore", "test": "jest", "coverage": "yarn test --coverage", - "check": "yarn prettier && yarn lint && yarn build && yarn test", - "android": "yarn && yarn ./example && yarn build && (cd example/ && yarn android)", - "ios": "yarn && yarn ./example && yarn build && (cd example/ && yarn ios-go)" + "check": "yarn prettier && yarn lint && yarn build && yarn test" }, "peerDependencies": { "react": "*", diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index 6f7a746cd..a0ade06ed 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -107,10 +107,10 @@ export default class ReactNativeLDClient extends LDClientImpl { override getStreamingPaths(): DataSourcePaths { return { - pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(encoding: Encoding, _plainContextString: string): string { return `/meval/${base64UrlEncode(_plainContextString, encoding)}`; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return `/meval`; }, }; @@ -118,10 +118,10 @@ export default class ReactNativeLDClient extends LDClientImpl { override getPollingPaths(): DataSourcePaths { return { - pathGet(encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(encoding: Encoding, _plainContextString: string): string { return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return `/msdk/evalx/context`; }, }; diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.ts index a4d115e40..b91e103a3 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.ts @@ -25,6 +25,7 @@ const reportJsonError = ( errorHandler?.(new LDStreamingError('Malformed JSON data in event stream')); }; +// TODO: SDK-156 - Move to Server SDK specific location class StreamingProcessor implements LDStreamProcessor { private readonly headers: { [key: string]: string | string[] }; private readonly streamUri: string; diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts index 523ef2029..093fcaef3 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts @@ -85,10 +85,10 @@ describe('sdk-client object', () => { }); jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { return '/stream/path'; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return '/stream/path'; }, }); diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts index ad63d84b5..976dd2092 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts @@ -52,10 +52,10 @@ describe('sdk-client storage', () => { }); jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { return '/stream/path'; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return '/stream/path'; }, }); diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index 9709b70d5..4e732d114 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -44,10 +44,10 @@ describe('sdk-client object', () => { mockPlatform.crypto.createHash.mockReturnValue(hasher); jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { return '/stream/path/get'; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return '/stream/path/report'; }, }); diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts index 592a4f2a0..bfe2a5df7 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts @@ -46,10 +46,10 @@ describe('sdk-client identify timeout', () => { sendEvents: false, }); jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { return '/stream/path'; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return '/stream/path'; }, }); diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts index 6dc80729f..664a54ca2 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts @@ -32,10 +32,10 @@ describe('sdk-client object', () => { beforeEach(() => { defaultPutResponse = clone(mockResponseJson); jest.spyOn(LDClientImpl.prototype as any, 'getStreamingPaths').mockReturnValue({ - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { return '/stream/path'; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return '/stream/path'; }, }); diff --git a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts index c3108667b..593372d83 100644 --- a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts @@ -80,10 +80,10 @@ function makeConfig( credential: 'the-sdk-key', serviceEndpoints, paths: { - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { return '/poll/path/get'; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return '/poll/path/report'; }, }, diff --git a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index 5ab4a9066..fcd4a96d6 100644 --- a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -48,10 +48,10 @@ function getStreamingDataSourceConfig( // eslint-disable-next-line object-shorthand serviceEndpoints: serviceEndpoints, paths: { - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { return '/stream/path/get'; }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { return '/stream/path/report'; }, }, diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 71de4cf36..984eda484 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -272,12 +272,12 @@ export default class LDClientImpl implements LDClient { protected getStreamingPaths(): DataSourcePaths { return { - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { throw new Error( 'getStreamingPaths not implemented. Client sdks must implement getStreamingPaths for streaming with GET to work.', ); }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { throw new Error( 'getStreamingPaths not implemented. Client sdks must implement getStreamingPaths for streaming with REPORT to work.', ); @@ -287,12 +287,12 @@ export default class LDClientImpl implements LDClient { protected getPollingPaths(): DataSourcePaths { return { - pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathGet(_encoding: Encoding, _plainContextString: string): string { throw new Error( 'getPollingPaths not implemented. Client sdks must implement getPollingPaths for polling with GET to work.', ); }, - pathReport(_encoding: Encoding, _credential: string, _plainContextString: string): string { + pathReport(_encoding: Encoding, _plainContextString: string): string { throw new Error( 'getPollingPaths not implemented. Client sdks must implement getPollingPaths for polling with REPORT to work.', ); diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index b88c1fca5..1e4f229dd 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -38,8 +38,8 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { private readonly logger?: LDLogger, ) { const path = dataSourceConfig.useReport - ? dataSourceConfig.paths.pathReport(encoding, dataSourceConfig.credential, plainContextString) - : dataSourceConfig.paths.pathGet(encoding, dataSourceConfig.credential, plainContextString); + ? dataSourceConfig.paths.pathReport(encoding, plainContextString) + : dataSourceConfig.paths.pathGet(encoding, plainContextString); const parameters: { key: string; value: string }[] = []; if (this.dataSourceConfig.withReasons) { diff --git a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts index 3ae9f1315..41ce87b40 100644 --- a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts +++ b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts @@ -18,6 +18,6 @@ export interface StreamingDataSourceConfig extends DataSourceConfig { } export interface DataSourcePaths { - pathGet(encoding: Encoding, credential: string, plainContextString: string): string; - pathReport(encoding: Encoding, credential: string, plainContextString: string): string; + pathGet(encoding: Encoding, plainContextString: string): string; + pathReport(encoding: Encoding, plainContextString: string): string; } diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts index bd43ec592..7c7a083a1 100644 --- a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts @@ -46,14 +46,14 @@ class StreamingProcessor implements subsystem.LDStreamProcessor { ) { // TODO: SC-255969 Implement better REPORT fallback logic if (dataSourceConfig.useReport && !requests.getEventSourceCapabilities().customMethod) { - logger?.warn( - "Configuration option useReport is true, but platform's EventSource does not support custom methods. Streaming may not work.", + logger?.error( + "Configuration option useReport is true, but platform's EventSource does not support custom HTTP methods. Streaming may not work.", ); } const path = dataSourceConfig.useReport - ? dataSourceConfig.paths.pathReport(encoding, dataSourceConfig.credential, plainContextString) - : dataSourceConfig.paths.pathGet(encoding, dataSourceConfig.credential, plainContextString); + ? dataSourceConfig.paths.pathReport(encoding, plainContextString) + : dataSourceConfig.paths.pathGet(encoding, plainContextString); const parameters: { key: string; value: string }[] = []; if (this.dataSourceConfig.withReasons) {