Skip to content

Commit 4f40c67

Browse files
authored
Merge branch 'main' into rlamb/sdk-166/esm-common-2
2 parents 3511a4b + 3389983 commit 4f40c67

File tree

12 files changed

+278
-19
lines changed

12 files changed

+278
-19
lines changed

packages/sdk/browser/__tests__/BrowserDataManager.test.ts

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {
1111
internal,
1212
LDEmitter,
1313
LDHeaders,
14-
LDIdentifyOptions,
1514
LDLogger,
1615
Platform,
1716
Response,
1817
ServiceEndpoints,
1918
} from '@launchdarkly/js-client-sdk-common';
2019

2120
import BrowserDataManager from '../src/BrowserDataManager';
21+
import { BrowserIdentifyOptions } from '../src/BrowserIdentifyOptions';
2222
import validateOptions, { ValidatedOptions } from '../src/options';
2323
import BrowserEncoding from '../src/platform/BrowserEncoding';
2424
import BrowserInfo from '../src/platform/BrowserInfo';
@@ -193,7 +193,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
193193
);
194194

195195
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
196-
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
196+
const identifyOptions: BrowserIdentifyOptions = {};
197197
const identifyResolve = jest.fn();
198198
const identifyReject = jest.fn();
199199

@@ -202,9 +202,91 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
202202
expect(platform.requests.createEventSource).toHaveBeenCalled();
203203
});
204204

205+
it('includes the secure mode hash for streaming requests', async () => {
206+
dataManager = new BrowserDataManager(
207+
platform,
208+
flagManager,
209+
'test-credential',
210+
config,
211+
validateOptions({ streaming: true }, logger),
212+
() => ({
213+
pathGet(encoding: Encoding, _plainContextString: string): string {
214+
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
215+
},
216+
pathReport(_encoding: Encoding, _plainContextString: string): string {
217+
return `/msdk/evalx/context`;
218+
},
219+
}),
220+
() => ({
221+
pathGet(encoding: Encoding, _plainContextString: string): string {
222+
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
223+
},
224+
pathReport(_encoding: Encoding, _plainContextString: string): string {
225+
return `/meval`;
226+
},
227+
}),
228+
baseHeaders,
229+
emitter,
230+
diagnosticsManager,
231+
);
232+
233+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
234+
const identifyOptions: BrowserIdentifyOptions = { hash: 'potato' };
235+
const identifyResolve = jest.fn();
236+
const identifyReject = jest.fn();
237+
238+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
239+
240+
expect(platform.requests.createEventSource).toHaveBeenCalledWith(
241+
'/meval/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlciJ9?h=potato&withReasons=true',
242+
expect.anything(),
243+
);
244+
});
245+
246+
it('includes secure mode hash for initial poll request', async () => {
247+
dataManager = new BrowserDataManager(
248+
platform,
249+
flagManager,
250+
'test-credential',
251+
config,
252+
validateOptions({ streaming: false }, logger),
253+
() => ({
254+
pathGet(encoding: Encoding, _plainContextString: string): string {
255+
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
256+
},
257+
pathReport(_encoding: Encoding, _plainContextString: string): string {
258+
return `/msdk/evalx/context`;
259+
},
260+
}),
261+
() => ({
262+
pathGet(encoding: Encoding, _plainContextString: string): string {
263+
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
264+
},
265+
pathReport(_encoding: Encoding, _plainContextString: string): string {
266+
return `/meval`;
267+
},
268+
}),
269+
baseHeaders,
270+
emitter,
271+
diagnosticsManager,
272+
);
273+
274+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
275+
const identifyOptions: BrowserIdentifyOptions = { hash: 'potato' };
276+
const identifyResolve = jest.fn();
277+
const identifyReject = jest.fn();
278+
279+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
280+
281+
expect(platform.requests.fetch).toHaveBeenCalledWith(
282+
'/msdk/evalx/contexts/eyJraW5kIjoidXNlciIsImtleSI6InRlc3QtdXNlciJ9?withReasons=true&h=potato',
283+
expect.anything(),
284+
);
285+
});
286+
205287
it('should load cached flags and continue to poll to complete identify', async () => {
206288
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
207-
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
289+
208290
flagManager.loadCached.mockResolvedValue(true);
209291

210292
let identifyResolve: () => void;
@@ -216,7 +298,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
216298
identifyReject = jest.fn();
217299

218300
// this is the function under test
219-
dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
301+
dataManager.identify(identifyResolve, identifyReject, context, {});
220302
});
221303

222304
expect(logger.debug).toHaveBeenCalledWith(
@@ -234,7 +316,6 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
234316

235317
it('should identify from polling when there are no cached flags', async () => {
236318
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
237-
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
238319

239320
let identifyResolve: () => void;
240321
let identifyReject: (err: Error) => void;
@@ -245,7 +326,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
245326
identifyReject = jest.fn();
246327

247328
// this is the function under test
248-
dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
329+
dataManager.identify(identifyResolve, identifyReject, context, {});
249330
});
250331

251332
expect(logger.debug).not.toHaveBeenCalledWith(
@@ -263,7 +344,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
263344

264345
it('creates a stream when streaming is enabled after construction', async () => {
265346
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
266-
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
347+
const identifyOptions: BrowserIdentifyOptions = {};
267348
const identifyResolve = jest.fn();
268349
const identifyReject = jest.fn();
269350

@@ -278,7 +359,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
278359

279360
it('does not re-create the stream if it already running', async () => {
280361
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
281-
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
362+
const identifyOptions: BrowserIdentifyOptions = {};
282363
const identifyResolve = jest.fn();
283364
const identifyReject = jest.fn();
284365

@@ -306,7 +387,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
306387

307388
it('starts a stream on demand when not forced on/off', async () => {
308389
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
309-
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
390+
const identifyOptions: BrowserIdentifyOptions = {};
310391
const identifyResolve = jest.fn();
311392
const identifyReject = jest.fn();
312393

@@ -325,7 +406,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
325406

326407
it('does not start a stream when forced off', async () => {
327408
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
328-
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
409+
const identifyOptions: BrowserIdentifyOptions = {};
329410
const identifyResolve = jest.fn();
330411
const identifyReject = jest.fn();
331412

@@ -345,7 +426,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
345426

346427
it('starts streaming on identify if the automatic state is true', async () => {
347428
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
348-
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
429+
const identifyOptions: BrowserIdentifyOptions = {};
349430
const identifyResolve = jest.fn();
350431
const identifyReject = jest.fn();
351432

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@ import {
1717
} from '@launchdarkly/js-client-sdk-common';
1818

1919
import BrowserDataManager from './BrowserDataManager';
20+
import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions';
2021
import GoalManager from './goals/GoalManager';
2122
import { Goal, isClick } from './goals/Goals';
2223
import validateOptions, { BrowserOptions, filterToBaseOptions } from './options';
2324
import BrowserPlatform from './platform/BrowserPlatform';
2425

2526
/**
26-
* We are not supporting dynamically setting the connection mode on the LDClient.
27-
* The SDK does not support offline mode. Instead bootstrap data can be used.
27+
*
28+
* The LaunchDarkly SDK client object.
29+
*
30+
* Applications should configure the client at page load time and reuse the same instance.
31+
*
32+
* For more information, see the [SDK Reference Guide](https://docs.launchdarkly.com/sdk/client-side/javascript).
33+
*
34+
* @ignore Implementation Note: We are not supporting dynamically setting the connection mode on the LDClient.
35+
* @ignore Implementation Note: The SDK does not support offline mode. Instead bootstrap data can be used.
36+
* @ignore Implementation Note: The browser SDK has different identify options, so omits the base implementation
37+
* @ignore from the interface.
2838
*/
2939
export type LDClient = Omit<
3040
CommonClient,
31-
'setConnectionMode' | 'getConnectionMode' | 'getOffline'
41+
'setConnectionMode' | 'getConnectionMode' | 'getOffline' | 'identify'
3242
> & {
3343
/**
3444
* Specifies whether or not to open a streaming connection to LaunchDarkly for live flag updates.
@@ -40,9 +50,38 @@ export type LDClient = Omit<
4050
* This can also be set as the `streaming` property of {@link LDOptions}.
4151
*/
4252
setStreaming(streaming?: boolean): void;
53+
54+
/**
55+
* Identifies a context to LaunchDarkly.
56+
*
57+
* Unlike the server-side SDKs, the client-side JavaScript SDKs maintain a current context state,
58+
* which is set when you call `identify()`.
59+
*
60+
* Changing the current context also causes all feature flag values to be reloaded. Until that has
61+
* finished, calls to {@link variation} will still return flag values for the previous context. You can
62+
* await the Promise to determine when the new flag values are available.
63+
*
64+
* @param context
65+
* The LDContext object.
66+
* @param identifyOptions
67+
* Optional configuration. Please see {@link LDIdentifyOptions}.
68+
* @returns
69+
* A Promise which resolves when the flag values for the specified
70+
* context are available. It rejects when:
71+
*
72+
* 1. The context is unspecified or has no key.
73+
*
74+
* 2. The identify timeout is exceeded. In client SDKs this defaults to 5s.
75+
* You can customize this timeout with {@link LDIdentifyOptions | identifyOptions}.
76+
*
77+
* 3. A network error is encountered during initialization.
78+
*
79+
* @ignore Implementation Note: Browser implementation has different options.
80+
*/
81+
identify(context: LDContext, identifyOptions?: LDIdentifyOptions): Promise<void>;
4382
};
4483

45-
export class BrowserClient extends LDClientImpl {
84+
export class BrowserClient extends LDClientImpl implements LDClient {
4685
private readonly goalManager?: GoalManager;
4786

4887
constructor(

packages/sdk/browser/src/BrowserDataManager.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Requestor,
1616
} from '@launchdarkly/js-client-sdk-common';
1717

18+
import { BrowserIdentifyOptions } from './BrowserIdentifyOptions';
1819
import { ValidatedOptions } from './options';
1920

2021
const logTag = '[BrowserDataManager]';
@@ -24,6 +25,7 @@ export default class BrowserDataManager extends BaseDataManager {
2425
// Otherwise we automatically manage streaming state.
2526
private forcedStreaming?: boolean = undefined;
2627
private automaticStreamingState: boolean = false;
28+
private secureModeHash?: string;
2729

2830
// +-----------+-----------+---------------+
2931
// | forced | automatic | state |
@@ -70,9 +72,18 @@ export default class BrowserDataManager extends BaseDataManager {
7072
identifyResolve: () => void,
7173
identifyReject: (err: Error) => void,
7274
context: Context,
73-
_identifyOptions?: LDIdentifyOptions,
75+
identifyOptions?: LDIdentifyOptions,
7476
): Promise<void> {
7577
this.context = context;
78+
const browserIdentifyOptions = identifyOptions as BrowserIdentifyOptions | undefined;
79+
if (browserIdentifyOptions?.hash) {
80+
this.setConnectionParams({
81+
queryParameters: [{ key: 'h', value: browserIdentifyOptions.hash }],
82+
});
83+
} else {
84+
this.setConnectionParams();
85+
}
86+
this.secureModeHash = browserIdentifyOptions?.hash;
7687
if (await this.flagManager.loadCached(context)) {
7788
this.debugLog('Identify - Flags loaded from cache. Continuing to initialize via a poll.');
7889
}
@@ -177,6 +188,9 @@ export default class BrowserDataManager extends BaseDataManager {
177188
if (this.config.withReasons) {
178189
parameters.push({ key: 'withReasons', value: 'true' });
179190
}
191+
if (this.secureModeHash) {
192+
parameters.push({ key: 'h', value: this.secureModeHash });
193+
}
180194

181195
const headers: { [key: string]: string } = { ...this.baseHeaders };
182196
let body;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common';
2+
3+
export interface BrowserIdentifyOptions extends Omit<LDIdentifyOptions, 'waitForNetworkResults'> {
4+
/**
5+
* The signed context key if you are using [Secure Mode]
6+
* (https://docs.launchdarkly.com/sdk/features/secure-mode#configuring-secure-mode-in-the-javascript-client-side-sdk).
7+
*/
8+
hash?: string;
9+
}

packages/sdk/browser/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
// The exported LDClient and LDOptions are the browser specific implementations.
1717
// These shadow the common implementations.
1818
import { BrowserClient, LDClient } from './BrowserClient';
19+
import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions';
1920
import { BrowserOptions as LDOptions } from './options';
2021

2122
export {
@@ -32,6 +33,7 @@ export {
3233
LDEvaluationDetail,
3334
LDEvaluationDetailTyped,
3435
LDEvaluationReason,
36+
LDIdentifyOptions,
3537
};
3638

3739
export function init(clientSideId: string, options?: LDOptions): LDClient {

packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ function makeConfig(
7575
pollInterval: number,
7676
withReasons: boolean,
7777
useReport: boolean,
78+
queryParameters?: { key: string; value: string }[],
7879
): PollingDataSourceConfig {
7980
return {
8081
credential: 'the-sdk-key',
@@ -91,6 +92,7 @@ function makeConfig(
9192
withReasons,
9293
useReport,
9394
pollInterval,
95+
queryParameters,
9496
};
9597
}
9698

@@ -109,6 +111,49 @@ it('makes no requests until it is started', () => {
109111
expect(requests.fetch).toHaveBeenCalledTimes(0);
110112
});
111113

114+
it('includes custom query parameters when specified', () => {
115+
const requests = makeRequests();
116+
117+
const polling = new PollingProcessor(
118+
'mockContextString',
119+
makeConfig(1, true, false, [
120+
{ key: 'custom', value: 'value' },
121+
{ key: 'custom2', value: 'value2' },
122+
]),
123+
requests,
124+
makeEncoding(),
125+
(_flags) => {},
126+
(_error) => {},
127+
);
128+
polling.start();
129+
130+
expect(requests.fetch).toHaveBeenCalledWith(
131+
'mockPollingEndpoint/poll/path/get?custom=value&custom2=value2&withReasons=true&filter=testPayloadFilterKey',
132+
expect.anything(),
133+
);
134+
polling.stop();
135+
});
136+
137+
it('works without any custom query parameters', () => {
138+
const requests = makeRequests();
139+
140+
const polling = new PollingProcessor(
141+
'mockContextString',
142+
makeConfig(1, true, false),
143+
requests,
144+
makeEncoding(),
145+
(_flags) => {},
146+
(_error) => {},
147+
);
148+
polling.start();
149+
150+
expect(requests.fetch).toHaveBeenCalledWith(
151+
'mockPollingEndpoint/poll/path/get?withReasons=true&filter=testPayloadFilterKey',
152+
expect.anything(),
153+
);
154+
polling.stop();
155+
});
156+
112157
it('polls immediately when started', () => {
113158
const requests = makeRequests();
114159

0 commit comments

Comments
 (0)