From 27f5d9190e297df35780059edd59766e39d53332 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Sep 2024 09:49:16 -0700 Subject: [PATCH 01/10] feat: Add basic secure mode support for browser SDK. --- .../sdk/browser/BrowserIdentifyOptions.ts | 9 ++ .../__tests__/BrowserDataManager.test.ts | 100 ++++++++++++++++-- packages/sdk/browser/src/BrowserClient.ts | 46 +++++++- .../sdk/browser/src/BrowserDataManager.ts | 16 ++- .../polling/PollingProcessor.test.ts | 45 ++++++++ .../streaming/StreamingProcessor.test.ts | 50 +++++++++ packages/shared/sdk-client/src/DataManager.ts | 14 +++ .../src/polling/PollingProcessor.ts | 4 +- .../src/streaming/DataSourceConfig.ts | 1 + .../src/streaming/StreamingProcessor.ts | 4 +- 10 files changed, 272 insertions(+), 17 deletions(-) create mode 100644 packages/sdk/browser/BrowserIdentifyOptions.ts diff --git a/packages/sdk/browser/BrowserIdentifyOptions.ts b/packages/sdk/browser/BrowserIdentifyOptions.ts new file mode 100644 index 0000000000..9600719ef6 --- /dev/null +++ b/packages/sdk/browser/BrowserIdentifyOptions.ts @@ -0,0 +1,9 @@ +import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; + +export interface BrowserIdentifyOptions extends Omit { + /** + * The signed context key if you are using [Secure Mode] + * (https://docs.launchdarkly.com/sdk/features/secure-mode#configuring-secure-mode-in-the-javascript-client-side-sdk). + */ + hash?: string; +} diff --git a/packages/sdk/browser/__tests__/BrowserDataManager.test.ts b/packages/sdk/browser/__tests__/BrowserDataManager.test.ts index fee37b29b5..5518801971 100644 --- a/packages/sdk/browser/__tests__/BrowserDataManager.test.ts +++ b/packages/sdk/browser/__tests__/BrowserDataManager.test.ts @@ -11,13 +11,13 @@ import { internal, LDEmitter, LDHeaders, - LDIdentifyOptions, LDLogger, Platform, Response, ServiceEndpoints, } from '@launchdarkly/js-client-sdk-common'; +import { BrowserIdentifyOptions } from '../BrowserIdentifyOptions'; import BrowserDataManager from '../src/BrowserDataManager'; import validateOptions, { ValidatedOptions } from '../src/options'; import BrowserEncoding from '../src/platform/BrowserEncoding'; @@ -196,7 +196,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => { ); const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); - const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false }; + const identifyOptions: BrowserIdentifyOptions = {}; const identifyResolve = jest.fn(); const identifyReject = jest.fn(); @@ -205,9 +205,91 @@ describe('given a BrowserDataManager with mocked dependencies', () => { expect(platform.requests.createEventSource).toHaveBeenCalled(); }); + it('includes the secure mode hash for streaming requests', async () => { + dataManager = new BrowserDataManager( + platform, + flagManager, + 'test-credential', + config, + validateOptions({ streaming: true }, logger), + () => ({ + pathGet(encoding: Encoding, _plainContextString: string): string { + return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`; + }, + pathReport(_encoding: Encoding, _plainContextString: string): string { + return `/msdk/evalx/context`; + }, + }), + () => ({ + pathGet(encoding: Encoding, _plainContextString: string): string { + return `/meval/${base64UrlEncode(_plainContextString, encoding)}`; + }, + pathReport(_encoding: Encoding, _plainContextString: string): string { + return `/meval`; + }, + }), + baseHeaders, + emitter, + diagnosticsManager, + ); + + const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); + const identifyOptions: BrowserIdentifyOptions = { hash: 'potato' }; + const identifyResolve = jest.fn(); + const identifyReject = jest.fn(); + + await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions); + + expect(platform.requests.createEventSource).toHaveBeenCalledWith( + '/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlciJ9?h=potato&withReasons=true', + expect.anything(), + ); + }); + + it('includes secure mode hash for initial poll request', async () => { + dataManager = new BrowserDataManager( + platform, + flagManager, + 'test-credential', + config, + validateOptions({ streaming: false }, logger), + () => ({ + pathGet(encoding: Encoding, _plainContextString: string): string { + return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`; + }, + pathReport(_encoding: Encoding, _plainContextString: string): string { + return `/msdk/evalx/context`; + }, + }), + () => ({ + pathGet(encoding: Encoding, _plainContextString: string): string { + return `/meval/${base64UrlEncode(_plainContextString, encoding)}`; + }, + pathReport(_encoding: Encoding, _plainContextString: string): string { + return `/meval`; + }, + }), + baseHeaders, + emitter, + diagnosticsManager, + ); + + const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); + const identifyOptions: BrowserIdentifyOptions = { hash: 'potato' }; + const identifyResolve = jest.fn(); + const identifyReject = jest.fn(); + + await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions); + + expect(platform.requests.fetch).toHaveBeenCalledWith( + '/msdk/evalx/contexts/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlciJ9?withReasons=true&h=potato', + expect.anything(), + ); + }); + it('should load cached flags and continue to poll to complete identify', async () => { const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); - const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false }; + const identifyOptions: BrowserIdentifyOptions = {}; const identifyResolve = jest.fn(); const identifyReject = jest.fn(); @@ -230,7 +312,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => { it('should identify from polling when there are no cached flags', async () => { const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); - const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false }; + const identifyOptions: BrowserIdentifyOptions = {}; const identifyResolve = jest.fn(); const identifyReject = jest.fn(); @@ -253,7 +335,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => { it('creates a stream when streaming is enabled after construction', async () => { const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); - const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false }; + const identifyOptions: BrowserIdentifyOptions = {}; const identifyResolve = jest.fn(); const identifyReject = jest.fn(); @@ -268,7 +350,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => { it('does not re-create the stream if it already running', async () => { const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); - const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false }; + const identifyOptions: BrowserIdentifyOptions = {}; const identifyResolve = jest.fn(); const identifyReject = jest.fn(); @@ -296,7 +378,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => { it('starts a stream on demand when not forced on/off', async () => { const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); - const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false }; + const identifyOptions: BrowserIdentifyOptions = {}; const identifyResolve = jest.fn(); const identifyReject = jest.fn(); @@ -315,7 +397,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => { it('does not start a stream when forced off', async () => { const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); - const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false }; + const identifyOptions: BrowserIdentifyOptions = {}; const identifyResolve = jest.fn(); const identifyReject = jest.fn(); @@ -335,7 +417,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => { it('starts streaming on identify if the automatic state is true', async () => { const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); - const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false }; + const identifyOptions: BrowserIdentifyOptions = {}; const identifyResolve = jest.fn(); const identifyReject = jest.fn(); diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 7e76bd2f7f..9e7410c575 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -13,9 +13,9 @@ import { LDHeaders, Platform, } from '@launchdarkly/js-client-sdk-common'; -import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common/dist/api/LDIdentifyOptions'; import { EventName } from '@launchdarkly/js-client-sdk-common/dist/LDEmitter'; +import { BrowserIdentifyOptions as LDIdentifyOptions } from '../BrowserIdentifyOptions'; import BrowserDataManager from './BrowserDataManager'; import GoalManager from './goals/GoalManager'; import { Goal, isClick } from './goals/Goals'; @@ -23,12 +23,19 @@ import validateOptions, { BrowserOptions, filterToBaseOptions } from './options' import BrowserPlatform from './platform/BrowserPlatform'; /** - * We are not supporting dynamically setting the connection mode on the LDClient. - * The SDK does not support offline mode. Instead bootstrap data can be used. + * + * The LaunchDarkly SDK client object. + * + * Applications should configure the client at page load time and reuse the same instance. + * + * For more information, see the [SDK Reference Guide](https://docs.launchdarkly.com/sdk/client-side/javascript). + * + * @ignore Implementation Note: We are not supporting dynamically setting the connection mode on the LDClient. + * @ignore Implementation Note: The SDK does not support offline mode. Instead bootstrap data can be used. */ export type LDClient = Omit< CommonClient, - 'setConnectionMode' | 'getConnectionMode' | 'getOffline' + 'setConnectionMode' | 'getConnectionMode' | 'getOffline' | 'identify' > & { /** * Specifies whether or not to open a streaming connection to LaunchDarkly for live flag updates. @@ -40,9 +47,38 @@ export type LDClient = Omit< * This can also be set as the `streaming` property of {@link LDOptions}. */ setStreaming(streaming?: boolean): void; + + /** + * Identifies a context to LaunchDarkly. + * + * Unlike the server-side SDKs, the client-side JavaScript SDKs maintain a current context state, + * which is set when you call `identify()`. + * + * Changing the current context also causes all feature flag values to be reloaded. Until that has + * finished, calls to {@link variation} will still return flag values for the previous context. You can + * await the Promise to determine when the new flag values are available. + * + * @param context + * The LDContext object. + * @param identifyOptions + * Optional configuration. Please see {@link LDIdentifyOptions}. + * @returns + * A Promise which resolves when the flag values for the specified + * context are available. It rejects when: + * + * 1. The context is unspecified or has no key. + * + * 2. The identify timeout is exceeded. In client SDKs this defaults to 5s. + * You can customize this timeout with {@link LDIdentifyOptions | identifyOptions}. + * + * 3. A network error is encountered during initialization. + * + * @ignore Implementation Note: Browser implementation has different options. + */ + identify(context: LDContext, identifyOptions?: LDIdentifyOptions): Promise; }; -export class BrowserClient extends LDClientImpl { +export class BrowserClient extends LDClientImpl implements LDClient { private readonly goalManager?: GoalManager; constructor( diff --git a/packages/sdk/browser/src/BrowserDataManager.ts b/packages/sdk/browser/src/BrowserDataManager.ts index f1d121baf6..b6ba7ff7a6 100644 --- a/packages/sdk/browser/src/BrowserDataManager.ts +++ b/packages/sdk/browser/src/BrowserDataManager.ts @@ -13,6 +13,7 @@ import { Requestor, } from '@launchdarkly/js-client-sdk-common'; +import { BrowserIdentifyOptions } from '../BrowserIdentifyOptions'; import { ValidatedOptions } from './options'; const logTag = '[BrowserDataManager]'; @@ -22,6 +23,7 @@ export default class BrowserDataManager extends BaseDataManager { // Otherwise we automatically manage streaming state. private forcedStreaming?: boolean = undefined; private automaticStreamingState: boolean = false; + private secureModeHash?: string; // +-----------+-----------+---------------+ // | forced | automatic | state | @@ -68,9 +70,18 @@ export default class BrowserDataManager extends BaseDataManager { identifyResolve: () => void, identifyReject: (err: Error) => void, context: Context, - _identifyOptions?: LDIdentifyOptions, + identifyOptions?: LDIdentifyOptions, ): Promise { this.context = context; + const browserIdentifyOptions = identifyOptions as BrowserIdentifyOptions; + if (browserIdentifyOptions.hash) { + this.setConnectionParams({ + queryParameters: [{ key: 'h', value: browserIdentifyOptions.hash }], + }); + } else { + this.setConnectionParams(); + } + this.secureModeHash = browserIdentifyOptions.hash; if (await this.flagManager.loadCached(context)) { this.debugLog('Identify - Flags loaded from cache. Continuing to initialize via a poll.'); } @@ -162,6 +173,9 @@ export default class BrowserDataManager extends BaseDataManager { if (this.config.withReasons) { parameters.push({ key: 'withReasons', value: 'true' }); } + if (this.secureModeHash) { + parameters.push({ key: 'h', value: this.secureModeHash }); + } const headers: { [key: string]: string } = { ...this.baseHeaders }; let body; diff --git a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts index 593372d837..52ea9f188c 100644 --- a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts @@ -75,6 +75,7 @@ function makeConfig( pollInterval: number, withReasons: boolean, useReport: boolean, + queryParameters?: { key: string; value: string }[], ): PollingDataSourceConfig { return { credential: 'the-sdk-key', @@ -91,6 +92,7 @@ function makeConfig( withReasons, useReport, pollInterval, + queryParameters, }; } @@ -109,6 +111,49 @@ it('makes no requests until it is started', () => { expect(requests.fetch).toHaveBeenCalledTimes(0); }); +it('includes custom query parameters when specified', () => { + const requests = makeRequests(); + + const polling = new PollingProcessor( + 'mockContextString', + makeConfig(1, true, false, [ + { key: 'custom', value: 'value' }, + { key: 'custom2', value: 'value2' }, + ]), + requests, + makeEncoding(), + (_flags) => {}, + (_error) => {}, + ); + polling.start(); + + expect(requests.fetch).toHaveBeenCalledWith( + 'mockPollingEndpoint/poll/path/get?custom=value&custom2=value2&withReasons=true&filter=testPayloadFilterKey', + expect.anything(), + ); + polling.stop(); +}); + +it('works without any custom query parameters', () => { + const requests = makeRequests(); + + const polling = new PollingProcessor( + 'mockContextString', + makeConfig(1, true, false), + requests, + makeEncoding(), + (_flags) => {}, + (_error) => {}, + ); + polling.start(); + + expect(requests.fetch).toHaveBeenCalledWith( + 'mockPollingEndpoint/poll/path/get?withReasons=true&filter=testPayloadFilterKey', + expect.anything(), + ); + polling.stop(); +}); + it('polls immediately when started', () => { const requests = makeRequests(); diff --git a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts index fcd4a96d6c..3b52daf679 100644 --- a/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/streaming/StreamingProcessor.test.ts @@ -42,6 +42,7 @@ let basicPlatform: Platform; function getStreamingDataSourceConfig( withReasons: boolean = false, useReport: boolean = false, + queryParameters?: [{ key: string; value: string }], ): StreamingDataSourceConfig { return { credential: sdkKey, @@ -63,6 +64,7 @@ function getStreamingDataSourceConfig( initialRetryDelayMillis: 1000, withReasons, useReport, + queryParameters, }; } @@ -342,3 +344,51 @@ describe('given a stream processor', () => { }); }); }); + +it('includes custom query parameters', () => { + const { info } = basicPlatform; + const listeners = new Map(); + const mockListener = { + deserializeData: jest.fn((data) => data), + processJson: jest.fn(), + }; + listeners.set('put', mockListener); + listeners.set('patch', mockListener); + const diagnosticsManager = new internal.DiagnosticsManager(sdkKey, basicPlatform, {}); + + basicPlatform.requests = { + createEventSource: jest.fn((streamUri: string, options: any) => { + const mockEventSource = createMockEventSource(streamUri, options); + return mockEventSource; + }), + getEventSourceCapabilities: jest.fn(() => ({ + readTimeout: true, + headers: true, + customMethod: true, + })), + } as any; + + const streamingProcessor = new StreamingProcessor( + 'mockContextString', + getStreamingDataSourceConfig(undefined, undefined, [{ key: 'custom', value: 'value' }]), + listeners, + basicPlatform.requests, + basicPlatform.encoding!, + diagnosticsManager, + () => {}, + logger, + ); + + streamingProcessor.start(); + + expect(basicPlatform.requests.createEventSource).toHaveBeenCalledWith( + `${serviceEndpoints.streaming}/stream/path/get?custom=value&filter=testPayloadFilterKey`, + { + errorFilter: expect.any(Function), + headers: defaultHeaders(sdkKey, info, undefined), + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, + }, + ); +}); diff --git a/packages/shared/sdk-client/src/DataManager.ts b/packages/shared/sdk-client/src/DataManager.ts index 1fb0e9c1c2..252ec44014 100644 --- a/packages/shared/sdk-client/src/DataManager.ts +++ b/packages/shared/sdk-client/src/DataManager.ts @@ -55,10 +55,15 @@ export interface DataManagerFactory { ): DataManager; } +export interface ConnectionParams { + queryParameters?: { key: string; value: string }[]; +} + export abstract class BaseDataManager implements DataManager { protected updateProcessor?: subsystem.LDStreamProcessor; protected readonly logger: LDLogger; protected context?: Context; + private connectionParams?: ConnectionParams; constructor( protected readonly platform: Platform, @@ -74,6 +79,13 @@ export abstract class BaseDataManager implements DataManager { this.logger = config.logger; } + /** + * Set additional connection parameters for requests polling/streaming. + */ + protected setConnectionParams(connectionParams?: ConnectionParams) { + this.connectionParams = connectionParams; + } + abstract identify( identifyResolve: () => void, identifyReject: (err: Error) => void, @@ -97,6 +109,7 @@ export abstract class BaseDataManager implements DataManager { pollInterval: this.config.pollInterval, withReasons: this.config.withReasons, useReport: this.config.useReport, + queryParameters: this.connectionParams?.queryParameters, }, this.platform.requests, this.platform.encoding!, @@ -138,6 +151,7 @@ export abstract class BaseDataManager implements DataManager { initialRetryDelayMillis: this.config.streamInitialReconnectDelay * 1000, withReasons: this.config.withReasons, useReport: this.config.useReport, + queryParameters: this.connectionParams?.queryParameters, }, this.createStreamListeners(checkedContext, identifyResolve), this.platform.requests, diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index 1e4f229dd3..2b7fb524cc 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -41,7 +41,9 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { ? dataSourceConfig.paths.pathReport(encoding, plainContextString) : dataSourceConfig.paths.pathGet(encoding, plainContextString); - const parameters: { key: string; value: string }[] = []; + const parameters: { key: string; value: string }[] = [ + ...(dataSourceConfig.queryParameters ?? []), + ]; if (this.dataSourceConfig.withReasons) { parameters.push({ key: 'withReasons', value: 'true' }); } diff --git a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts index 41ce87b402..01fc6f9038 100644 --- a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts +++ b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts @@ -7,6 +7,7 @@ export interface DataSourceConfig { withReasons: boolean; useReport: boolean; paths: DataSourcePaths; + queryParameters?: { key: string; value: string }[]; } export interface PollingDataSourceConfig extends DataSourceConfig { diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts index 7c7a083a10..ae963f9bd1 100644 --- a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts @@ -55,7 +55,9 @@ class StreamingProcessor implements subsystem.LDStreamProcessor { ? dataSourceConfig.paths.pathReport(encoding, plainContextString) : dataSourceConfig.paths.pathGet(encoding, plainContextString); - const parameters: { key: string; value: string }[] = []; + const parameters: { key: string; value: string }[] = [ + ...(dataSourceConfig.queryParameters ?? []), + ]; if (this.dataSourceConfig.withReasons) { parameters.push({ key: 'withReasons', value: 'true' }); } From 292109612efce1d39fa66e37803848312214ebac Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:21:21 -0700 Subject: [PATCH 02/10] Additional comment. --- packages/sdk/browser/src/BrowserClient.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 9e7410c575..932cde0c5a 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -32,6 +32,8 @@ import BrowserPlatform from './platform/BrowserPlatform'; * * @ignore Implementation Note: We are not supporting dynamically setting the connection mode on the LDClient. * @ignore Implementation Note: The SDK does not support offline mode. Instead bootstrap data can be used. + * @ignore Implementation Note: The browser SDK has different identify options, so omits the base implementation + * @ignore from the interface. */ export type LDClient = Omit< CommonClient, From 156045a17b7a6a49f8d59401eb610218840b0ec1 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:24:44 -0700 Subject: [PATCH 03/10] Fix optionality. --- packages/sdk/browser/src/BrowserDataManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk/browser/src/BrowserDataManager.ts b/packages/sdk/browser/src/BrowserDataManager.ts index b6ba7ff7a6..115981e9d6 100644 --- a/packages/sdk/browser/src/BrowserDataManager.ts +++ b/packages/sdk/browser/src/BrowserDataManager.ts @@ -73,15 +73,15 @@ export default class BrowserDataManager extends BaseDataManager { identifyOptions?: LDIdentifyOptions, ): Promise { this.context = context; - const browserIdentifyOptions = identifyOptions as BrowserIdentifyOptions; - if (browserIdentifyOptions.hash) { + const browserIdentifyOptions = identifyOptions as BrowserIdentifyOptions | undefined; + if (browserIdentifyOptions?.hash) { this.setConnectionParams({ queryParameters: [{ key: 'h', value: browserIdentifyOptions.hash }], }); } else { this.setConnectionParams(); } - this.secureModeHash = browserIdentifyOptions.hash; + this.secureModeHash = browserIdentifyOptions?.hash; if (await this.flagManager.loadCached(context)) { this.debugLog('Identify - Flags loaded from cache. Continuing to initialize via a poll.'); } From ed4a62f0b0097bd855c58d34acb839864c689b97 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:26:00 -0700 Subject: [PATCH 04/10] Export connection params. --- packages/shared/sdk-client/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index 038221ba5d..6904475acf 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -20,7 +20,7 @@ export type { LDIdentifyOptions, } from './api'; -export type { DataManager, DataManagerFactory } from './DataManager'; +export type { DataManager, DataManagerFactory, ConnectionParams } from './DataManager'; export type { FlagManager } from './flag-manager/FlagManager'; export type { Configuration } from './configuration/Configuration'; From 1fea68fc11ea22dc917d410b7b52f76036fda289 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:30:54 -0700 Subject: [PATCH 05/10] Move code and fix exports. --- packages/sdk/browser/__tests__/BrowserDataManager.test.ts | 2 +- packages/sdk/browser/src/BrowserClient.ts | 2 +- packages/sdk/browser/src/BrowserDataManager.ts | 2 +- packages/sdk/browser/{ => src}/BrowserIdentifyOptions.ts | 0 packages/sdk/browser/src/index.ts | 2 ++ 5 files changed, 5 insertions(+), 3 deletions(-) rename packages/sdk/browser/{ => src}/BrowserIdentifyOptions.ts (100%) diff --git a/packages/sdk/browser/__tests__/BrowserDataManager.test.ts b/packages/sdk/browser/__tests__/BrowserDataManager.test.ts index 5518801971..bcbd95e2f1 100644 --- a/packages/sdk/browser/__tests__/BrowserDataManager.test.ts +++ b/packages/sdk/browser/__tests__/BrowserDataManager.test.ts @@ -17,7 +17,7 @@ import { ServiceEndpoints, } from '@launchdarkly/js-client-sdk-common'; -import { BrowserIdentifyOptions } from '../BrowserIdentifyOptions'; +import { BrowserIdentifyOptions } from '../src/BrowserIdentifyOptions'; import BrowserDataManager from '../src/BrowserDataManager'; import validateOptions, { ValidatedOptions } from '../src/options'; import BrowserEncoding from '../src/platform/BrowserEncoding'; diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 932cde0c5a..abf47e0b5d 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -15,7 +15,7 @@ import { } from '@launchdarkly/js-client-sdk-common'; import { EventName } from '@launchdarkly/js-client-sdk-common/dist/LDEmitter'; -import { BrowserIdentifyOptions as LDIdentifyOptions } from '../BrowserIdentifyOptions'; +import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions'; import BrowserDataManager from './BrowserDataManager'; import GoalManager from './goals/GoalManager'; import { Goal, isClick } from './goals/Goals'; diff --git a/packages/sdk/browser/src/BrowserDataManager.ts b/packages/sdk/browser/src/BrowserDataManager.ts index 115981e9d6..b61ff9800c 100644 --- a/packages/sdk/browser/src/BrowserDataManager.ts +++ b/packages/sdk/browser/src/BrowserDataManager.ts @@ -13,7 +13,7 @@ import { Requestor, } from '@launchdarkly/js-client-sdk-common'; -import { BrowserIdentifyOptions } from '../BrowserIdentifyOptions'; +import { BrowserIdentifyOptions } from './BrowserIdentifyOptions'; import { ValidatedOptions } from './options'; const logTag = '[BrowserDataManager]'; diff --git a/packages/sdk/browser/BrowserIdentifyOptions.ts b/packages/sdk/browser/src/BrowserIdentifyOptions.ts similarity index 100% rename from packages/sdk/browser/BrowserIdentifyOptions.ts rename to packages/sdk/browser/src/BrowserIdentifyOptions.ts diff --git a/packages/sdk/browser/src/index.ts b/packages/sdk/browser/src/index.ts index 26f5e703b9..d171664d68 100644 --- a/packages/sdk/browser/src/index.ts +++ b/packages/sdk/browser/src/index.ts @@ -16,6 +16,7 @@ import { // The exported LDClient and LDOptions are the browser specific implementations. // These shadow the common implementations. import { BrowserClient, LDClient } from './BrowserClient'; +import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions'; import { BrowserOptions as LDOptions } from './options'; export { @@ -32,6 +33,7 @@ export { LDEvaluationDetail, LDEvaluationDetailTyped, LDEvaluationReason, + LDIdentifyOptions, }; export function init(clientSideId: string, options?: LDOptions): LDClient { From 1d01a435cc41929a607bc4073a08692b59759bb8 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:37:19 -0700 Subject: [PATCH 06/10] Lint --- packages/sdk/browser/__tests__/BrowserDataManager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/browser/__tests__/BrowserDataManager.test.ts b/packages/sdk/browser/__tests__/BrowserDataManager.test.ts index bcbd95e2f1..74ca999979 100644 --- a/packages/sdk/browser/__tests__/BrowserDataManager.test.ts +++ b/packages/sdk/browser/__tests__/BrowserDataManager.test.ts @@ -17,8 +17,8 @@ import { ServiceEndpoints, } from '@launchdarkly/js-client-sdk-common'; -import { BrowserIdentifyOptions } from '../src/BrowserIdentifyOptions'; import BrowserDataManager from '../src/BrowserDataManager'; +import { BrowserIdentifyOptions } from '../src/BrowserIdentifyOptions'; import validateOptions, { ValidatedOptions } from '../src/options'; import BrowserEncoding from '../src/platform/BrowserEncoding'; import BrowserInfo from '../src/platform/BrowserInfo'; From bf64ea7727e80aea36be8e618c643634900bea2d Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:41:00 -0700 Subject: [PATCH 07/10] Lint. --- 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 abf47e0b5d..47a1c4a28f 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -15,8 +15,8 @@ import { } from '@launchdarkly/js-client-sdk-common'; import { EventName } from '@launchdarkly/js-client-sdk-common/dist/LDEmitter'; -import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions'; import BrowserDataManager from './BrowserDataManager'; +import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions'; import GoalManager from './goals/GoalManager'; import { Goal, isClick } from './goals/Goals'; import validateOptions, { BrowserOptions, filterToBaseOptions } from './options'; From 30c495e2fdbe2dcde1fda150bebc8d98a2383fff Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:20:30 -0700 Subject: [PATCH 08/10] feat: Add bootstrap support. --- .../browser/__tests__/BrowserClient.test.ts | 28 ++++ .../__tests__/BrowserDataManager.test.ts | 32 ++++ .../sdk/browser/__tests__/bootstrap.test.ts | 149 ++++++++++++++++++ .../browser/__tests__/testBootstrapData.ts | 76 +++++++++ .../sdk/browser/src/BrowserDataManager.ts | 36 ++++- .../sdk/browser/src/BrowserIdentifyOptions.ts | 15 ++ packages/sdk/browser/src/bootstrap.ts | 47 ++++++ .../sdk-client/src/events/EventFactory.ts | 2 +- .../src/flag-manager/FlagManager.ts | 12 ++ packages/shared/sdk-client/src/index.ts | 2 + packages/shared/sdk-client/src/types/index.ts | 6 +- 11 files changed, 395 insertions(+), 10 deletions(-) create mode 100644 packages/sdk/browser/__tests__/bootstrap.test.ts create mode 100644 packages/sdk/browser/__tests__/testBootstrapData.ts create mode 100644 packages/sdk/browser/src/bootstrap.ts diff --git a/packages/sdk/browser/__tests__/BrowserClient.test.ts b/packages/sdk/browser/__tests__/BrowserClient.test.ts index 7a8dc3ff24..4cba088a16 100644 --- a/packages/sdk/browser/__tests__/BrowserClient.test.ts +++ b/packages/sdk/browser/__tests__/BrowserClient.test.ts @@ -12,6 +12,7 @@ import { import { BrowserClient } from '../src/BrowserClient'; import { MockHasher } from './MockHasher'; +import { goodBootstrapDataWithReasons } from './testBootstrapData'; function mockResponse(value: string, statusCode: number) { const response: Response = { @@ -257,4 +258,31 @@ describe('given a mock platform for a BrowserClient', () => { url: 'http://filtered.com', }); }); + + it('can use bootstrap data', async () => { + const client = new BrowserClient( + 'client-side-id', + AutoEnvAttributes.Disabled, + { + streaming: false, + logger, + diagnosticOptOut: true, + }, + platform, + ); + await client.identify( + { kind: 'user', key: 'bob' }, + { + bootstrap: goodBootstrapDataWithReasons, + }, + ); + + expect(client.jsonVariationDetail('json', undefined)).toEqual({ + reason: { + kind: 'OFF', + }, + value: ['a', 'b', 'c', 'd'], + variationIndex: 1, + }); + }); }); diff --git a/packages/sdk/browser/__tests__/BrowserDataManager.test.ts b/packages/sdk/browser/__tests__/BrowserDataManager.test.ts index 74ca999979..2ce0682bf7 100644 --- a/packages/sdk/browser/__tests__/BrowserDataManager.test.ts +++ b/packages/sdk/browser/__tests__/BrowserDataManager.test.ts @@ -24,6 +24,7 @@ import BrowserEncoding from '../src/platform/BrowserEncoding'; import BrowserInfo from '../src/platform/BrowserInfo'; import LocalStorage from '../src/platform/LocalStorage'; import { MockHasher } from './MockHasher'; +import { goodBootstrapData } from './testBootstrapData'; global.TextEncoder = TextEncoder; @@ -126,6 +127,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => { upsert: jest.fn(), on: jest.fn(), off: jest.fn(), + setBootstrap: jest.fn(), } as unknown as jest.Mocked; browserConfig = validateOptions({}, logger); @@ -310,6 +312,36 @@ describe('given a BrowserDataManager with mocked dependencies', () => { expect(platform.requests.createEventSource).not.toHaveBeenCalled(); }); + it('uses data from bootstrap and does not make an initial poll', async () => { + const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); + const identifyOptions: BrowserIdentifyOptions = { + bootstrap: goodBootstrapData, + }; + const identifyResolve = jest.fn(); + const identifyReject = jest.fn(); + + flagManager.loadCached.mockResolvedValue(true); + + await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions); + + expect(logger.debug).toHaveBeenCalledWith( + '[BrowserDataManager] Identify - Initialization completed from bootstrap', + ); + + expect(flagManager.loadCached).not.toHaveBeenCalledWith(context); + expect(identifyResolve).toHaveBeenCalled(); + expect(flagManager.init).not.toHaveBeenCalled(); + expect(flagManager.setBootstrap).toHaveBeenCalledWith(expect.anything(), { + cat: { version: 2, flag: { version: 2, variation: 1, value: false } }, + json: { version: 3, flag: { version: 3, variation: 1, value: ['a', 'b', 'c', 'd'] } }, + killswitch: { version: 5, flag: { version: 5, variation: 0, value: true } }, + 'my-boolean-flag': { version: 11, flag: { version: 11, variation: 1, value: false } }, + 'string-flag': { version: 3, flag: { version: 3, variation: 1, value: 'is bob' } }, + }); + expect(platform.requests.createEventSource).not.toHaveBeenCalled(); + expect(platform.requests.fetch).not.toHaveBeenCalled(); + }); + it('should identify from polling when there are no cached flags', async () => { const context = Context.fromLDContext({ kind: 'user', key: 'test-user' }); const identifyOptions: BrowserIdentifyOptions = {}; diff --git a/packages/sdk/browser/__tests__/bootstrap.test.ts b/packages/sdk/browser/__tests__/bootstrap.test.ts new file mode 100644 index 0000000000..b7e9a2e436 --- /dev/null +++ b/packages/sdk/browser/__tests__/bootstrap.test.ts @@ -0,0 +1,149 @@ +import { jest } from '@jest/globals'; + +import { readFlagsFromBootstrap } from '../src/bootstrap'; +import { goodBootstrapData, goodBootstrapDataWithReasons } from './testBootstrapData'; + +it('can read valid bootstrap data', () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const readData = readFlagsFromBootstrap(logger, goodBootstrapData); + expect(readData).toEqual({ + cat: { version: 2, flag: { version: 2, variation: 1, value: false } }, + json: { version: 3, flag: { version: 3, variation: 1, value: ['a', 'b', 'c', 'd'] } }, + killswitch: { version: 5, flag: { version: 5, variation: 0, value: true } }, + 'my-boolean-flag': { version: 11, flag: { version: 11, variation: 1, value: false } }, + 'string-flag': { version: 3, flag: { version: 3, variation: 1, value: 'is bob' } }, + }); + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); +}); + +it('can read valid bootstrap data with reasons', () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const readData = readFlagsFromBootstrap(logger, goodBootstrapDataWithReasons); + expect(readData).toEqual({ + cat: { + version: 2, + flag: { + version: 2, + variation: 1, + value: false, + reason: { + kind: 'OFF', + }, + }, + }, + json: { + version: 3, + flag: { + version: 3, + variation: 1, + value: ['a', 'b', 'c', 'd'], + reason: { + kind: 'OFF', + }, + }, + }, + killswitch: { + version: 5, + flag: { + version: 5, + variation: 0, + value: true, + reason: { + kind: 'FALLTHROUGH', + }, + }, + }, + 'my-boolean-flag': { + version: 11, + flag: { + version: 11, + variation: 1, + value: false, + reason: { + kind: 'OFF', + }, + }, + }, + 'string-flag': { + version: 3, + flag: { + version: 3, + variation: 1, + value: 'is bob', + reason: { + kind: 'OFF', + }, + }, + }, + }); + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); +}); + +it('can read old bootstrap data', () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const oldData: any = { ...goodBootstrapData }; + delete oldData.$flagsState; + + const readData = readFlagsFromBootstrap(logger, oldData); + expect(readData).toEqual({ + cat: { version: 0, flag: { version: 0, value: false } }, + json: { version: 0, flag: { version: 0, value: ['a', 'b', 'c', 'd'] } }, + killswitch: { version: 0, flag: { version: 0, value: true } }, + 'my-boolean-flag': { version: 0, flag: { version: 0, value: false } }, + 'string-flag': { version: 0, flag: { version: 0, value: 'is bob' } }, + }); + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalledWith( + 'LaunchDarkly client was initialized with bootstrap data that did not' + + ' include flag metadata. Events may not be sent correctly.', + ); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.error).not.toHaveBeenCalled(); +}); + +it('can handle invalid bootstrap data', () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const invalid: any = { $valid: false, $flagsState: {} }; + + const readData = readFlagsFromBootstrap(logger, invalid); + expect(readData).toEqual({}); + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalledWith( + 'LaunchDarkly bootstrap data is not available because the back end' + + ' could not read the flags.', + ); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.error).not.toHaveBeenCalled(); +}); diff --git a/packages/sdk/browser/__tests__/testBootstrapData.ts b/packages/sdk/browser/__tests__/testBootstrapData.ts new file mode 100644 index 0000000000..bb36d55d95 --- /dev/null +++ b/packages/sdk/browser/__tests__/testBootstrapData.ts @@ -0,0 +1,76 @@ +export const goodBootstrapData = { + cat: false, + json: ['a', 'b', 'c', 'd'], + killswitch: true, + 'my-boolean-flag': false, + 'string-flag': 'is bob', + $flagsState: { + cat: { + variation: 1, + version: 2, + }, + json: { + variation: 1, + version: 3, + }, + killswitch: { + variation: 0, + version: 5, + }, + 'my-boolean-flag': { + variation: 1, + version: 11, + }, + 'string-flag': { + variation: 1, + version: 3, + }, + }, + $valid: true, +}; + +export const goodBootstrapDataWithReasons = { + cat: false, + json: ['a', 'b', 'c', 'd'], + killswitch: true, + 'my-boolean-flag': false, + 'string-flag': 'is bob', + $flagsState: { + cat: { + variation: 1, + version: 2, + reason: { + kind: 'OFF', + }, + }, + json: { + variation: 1, + version: 3, + reason: { + kind: 'OFF', + }, + }, + killswitch: { + variation: 0, + version: 5, + reason: { + kind: 'FALLTHROUGH', + }, + }, + 'my-boolean-flag': { + variation: 1, + version: 11, + reason: { + kind: 'OFF', + }, + }, + 'string-flag': { + variation: 1, + version: 3, + reason: { + kind: 'OFF', + }, + }, + }, + $valid: true, +}; diff --git a/packages/sdk/browser/src/BrowserDataManager.ts b/packages/sdk/browser/src/BrowserDataManager.ts index b61ff9800c..c308c65239 100644 --- a/packages/sdk/browser/src/BrowserDataManager.ts +++ b/packages/sdk/browser/src/BrowserDataManager.ts @@ -13,6 +13,7 @@ import { Requestor, } from '@launchdarkly/js-client-sdk-common'; +import { readFlagsFromBootstrap } from './bootstrap'; import { BrowserIdentifyOptions } from './BrowserIdentifyOptions'; import { ValidatedOptions } from './options'; @@ -82,14 +83,29 @@ export default class BrowserDataManager extends BaseDataManager { this.setConnectionParams(); } this.secureModeHash = browserIdentifyOptions?.hash; - if (await this.flagManager.loadCached(context)) { - this.debugLog('Identify - Flags loaded from cache. Continuing to initialize via a poll.'); - } - const plainContextString = JSON.stringify(Context.toLDContext(context)); - const requestor = this.getRequestor(plainContextString); // TODO: Handle wait for network results in a meaningful way. SDK-707 + if (browserIdentifyOptions?.bootstrap) { + this.finishIdentifyFromBootstrap(context, browserIdentifyOptions.bootstrap, identifyResolve); + } else { + if (await this.flagManager.loadCached(context)) { + this.debugLog('Identify - Flags loaded from cache. Continuing to initialize via a poll.'); + } + const plainContextString = JSON.stringify(Context.toLDContext(context)); + const requestor = this.getRequestor(plainContextString); + await this.finishIdentifyFromPoll(requestor, context, identifyResolve, identifyReject); + } + + this.updateStreamingState(); + } + + private async finishIdentifyFromPoll( + requestor: Requestor, + context: Context, + identifyResolve: () => void, + identifyReject: (err: Error) => void, + ) { try { const payload = await requestor.requestPayload(); const listeners = this.createStreamListeners(context, identifyResolve); @@ -98,8 +114,16 @@ export default class BrowserDataManager extends BaseDataManager { } catch (e: any) { identifyReject(e); } + } - this.updateStreamingState(); + private finishIdentifyFromBootstrap( + context: Context, + bootstrap: unknown, + identifyResolve: () => void, + ) { + this.flagManager.setBootstrap(context, readFlagsFromBootstrap(this.logger, bootstrap)); + this.debugLog('Identify - Initialization completed from bootstrap'); + identifyResolve(); } setForcedStreaming(streaming?: boolean) { diff --git a/packages/sdk/browser/src/BrowserIdentifyOptions.ts b/packages/sdk/browser/src/BrowserIdentifyOptions.ts index 9600719ef6..da3cd7795a 100644 --- a/packages/sdk/browser/src/BrowserIdentifyOptions.ts +++ b/packages/sdk/browser/src/BrowserIdentifyOptions.ts @@ -6,4 +6,19 @@ export interface BrowserIdentifyOptions extends Omit { + if (key !== metadataKey && key !== validKey) { + let flag: Flag; + if (metadata && metadata[key]) { + flag = { + value: data[key], + ...metadata[key], + }; + } else { + flag = { + value: data[key], + version: 0, + }; + } + ret[key] = { + version: flag.version, + flag, + }; + } + }); + return ret; +} diff --git a/packages/shared/sdk-client/src/events/EventFactory.ts b/packages/shared/sdk-client/src/events/EventFactory.ts index 25ef0e0550..b0d64a53bf 100644 --- a/packages/shared/sdk-client/src/events/EventFactory.ts +++ b/packages/shared/sdk-client/src/events/EventFactory.ts @@ -23,7 +23,7 @@ export default class EventFactory extends internal.EventFactoryBase { defaultVal, flagKey, reason, - trackEvents, + trackEvents: !!trackEvents, value, variation, version, diff --git a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts index 915a81407e..4dcc35a3e3 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagManager.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagManager.ts @@ -40,6 +40,12 @@ export interface FlagManager { */ loadCached(context: Context): Promise; + /** + * Update in-memory storage with the specified flags, but do not persistent them to cache + * storage. + */ + setBootstrap(context: Context, newFlags: { [key: string]: ItemDescriptor }): void; + /** * Register a flag change callback. */ @@ -108,6 +114,12 @@ export default class DefaultFlagManager implements FlagManager { return this.flagStore.getAll(); } + setBootstrap(context: Context, newFlags: { [key: string]: ItemDescriptor }): void { + // Bypasses the persistence as we do not want to put these flags into any cache. + // Generally speaking persistence likely *SHOULD* be disabled when using bootstrap. + this.flagUpdater.init(context, newFlags); + } + async init(context: Context, newFlags: { [key: string]: ItemDescriptor }): Promise { return (await this.flagPersistencePromise).init(context, newFlags); } diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index 6904475acf..8a8e92499f 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -25,6 +25,8 @@ export type { FlagManager } from './flag-manager/FlagManager'; export type { Configuration } from './configuration/Configuration'; export type { LDEmitter }; +export type { ItemDescriptor } from './flag-manager/ItemDescriptor'; +export type { Flag } from './types'; export { DataSourcePaths } from './streaming'; export { BaseDataManager } from './DataManager'; diff --git a/packages/shared/sdk-client/src/types/index.ts b/packages/shared/sdk-client/src/types/index.ts index 18b24736d6..f79ffc01f7 100644 --- a/packages/shared/sdk-client/src/types/index.ts +++ b/packages/shared/sdk-client/src/types/index.ts @@ -2,10 +2,10 @@ import { LDEvaluationReason, LDFlagValue } from '@launchdarkly/js-sdk-common'; export interface Flag { version: number; - flagVersion: number; + flagVersion?: number; value: LDFlagValue; - variation: number; - trackEvents: boolean; + variation?: number; + trackEvents?: boolean; trackReason?: boolean; reason?: LDEvaluationReason; debugEventsUntilDate?: number; From 459eaa831cad3292aa001e8205d62a1d61001d8a Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:18:37 -0700 Subject: [PATCH 09/10] Fix typo --- packages/sdk/browser/src/BrowserIdentifyOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/browser/src/BrowserIdentifyOptions.ts b/packages/sdk/browser/src/BrowserIdentifyOptions.ts index da3cd7795a..231b499056 100644 --- a/packages/sdk/browser/src/BrowserIdentifyOptions.ts +++ b/packages/sdk/browser/src/BrowserIdentifyOptions.ts @@ -1,6 +1,6 @@ import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; -export interface BrowserIdentifyOptions extends Omit { +export interface BrowserIdentifyOptions extends Omit { /** * The signed context key if you are using [Secure Mode] * (https://docs.launchdarkly.com/sdk/features/secure-mode#configuring-secure-mode-in-the-javascript-client-side-sdk). From 9972b67fde9340e29b21a5a8a99c78e68aafc146 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:43:12 -0700 Subject: [PATCH 10/10] merge better --- packages/sdk/browser/src/BrowserDataManager.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/sdk/browser/src/BrowserDataManager.ts b/packages/sdk/browser/src/BrowserDataManager.ts index 6c653631f9..a540f3beac 100644 --- a/packages/sdk/browser/src/BrowserDataManager.ts +++ b/packages/sdk/browser/src/BrowserDataManager.ts @@ -85,12 +85,6 @@ export default class BrowserDataManager extends BaseDataManager { this.setConnectionParams(); } this.secureModeHash = browserIdentifyOptions?.hash; - if (await this.flagManager.loadCached(context)) { - this.debugLog('Identify - Flags loaded from cache. Continuing to initialize via a poll.'); - } - this.secureModeHash = browserIdentifyOptions?.hash; - - // TODO: Handle wait for network results in a meaningful way. SDK-707 if (browserIdentifyOptions?.bootstrap) { this.finishIdentifyFromBootstrap(context, browserIdentifyOptions.bootstrap, identifyResolve);