Skip to content

Commit 7163adf

Browse files
authored
feat!: Only support identify with result. (#1000)
Originally the SDK API had an `identify` method which didn't return any result. We added an `identifyResult` which is a safer API. We have decided that we shouldn't support the less-safe API and instead should have a single identify method. So the regular identify method will now have the behavior of `identifyResult`. This moves the browser client to use a PIMPL implementation to decouple its interface of the inheritance hierarchy. Which allows for identify to have a completely different signature than the base implementation. It also allows us to retain identify in RN until we are ready to do a major version. For the PIMPL implementation I decided to use a factory instead of a class. So tests needed updated to use that factory. I also re-organized plugin registration so the better encapsulate PIMPL implementation would be passed to register. The "compat" adapter needed to be updated to use the new API. For now it supports using the old interface, but we may want to consider removing it if we determine it isn't safe. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Replaces the BrowserClient class with a makeClient factory using a PIMPL impl and changes identify to return LDIdentifyResult (replacing identifyResult); updates plugin registration, compat, initialization, and tests. > > - **API** > - `identify` now returns `LDIdentifyResult` (with statuses like `completed/shed/timeout/error`) and accepts browser-specific options; `identifyResult` is removed from the public interface. > - Introduce factory `makeClient` wrapping an internal `BrowserClientImpl` (PIMPL); `initialize` now uses `makeClient`. > - `LDClient` type updated to expose the new `identify` signature and omit the old one. > - **Plugins** > - Refactor plugin registration to register against the public PIMPL client; hook retrieval unchanged; environment metadata still forwarded. > - **Compat** > - `LDClientCompatImpl` updated to use `makeClient` and new `identify` result handling; passes `sheddable: false`, propagates `bootstrap/hash`, and maps failures/timeouts. > - **Tests** > - Migrate tests to `makeClient`; adjust expectations for `identify` return values and shedding behavior; verify plugin registration and environment metadata. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4df7885. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent af138c1 commit 7163adf

File tree

7 files changed

+152
-110
lines changed

7 files changed

+152
-110
lines changed

packages/sdk/browser/__tests__/BrowserClient.plugins.test.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
LDLogger,
77
} from '@launchdarkly/js-client-sdk-common';
88

9-
import { BrowserClient } from '../src/BrowserClient';
9+
import { makeClient } from '../src/BrowserClient';
1010
import { LDPlugin } from '../src/LDPlugin';
1111
import { BrowserOptions } from '../src/options';
1212
import { makeBasicPlatform } from './BrowserClient.mocks';
@@ -41,7 +41,7 @@ it('registers plugins and executes hooks during initialization', async () => {
4141

4242
const platform = makeBasicPlatform();
4343

44-
const client = new BrowserClient(
44+
const client = makeClient(
4545
'client-side-id',
4646
AutoEnvAttributes.Disabled,
4747
{
@@ -132,7 +132,7 @@ it('registers multiple plugins and executes all hooks', async () => {
132132

133133
const platform = makeBasicPlatform();
134134

135-
const client = new BrowserClient(
135+
const client = makeClient(
136136
'client-side-id',
137137
AutoEnvAttributes.Disabled,
138138
{
@@ -198,8 +198,7 @@ it('passes correct environmentMetadata to plugin getHooks and register functions
198198

199199
const platform = makeBasicPlatform(options);
200200

201-
// eslint-disable-next-line no-new
202-
new BrowserClient(
201+
makeClient(
203202
'client-side-id',
204203
AutoEnvAttributes.Disabled,
205204
{
@@ -280,8 +279,7 @@ it('passes correct environmentMetadata without optional fields', async () => {
280279

281280
const platform = makeBasicPlatform();
282281

283-
// eslint-disable-next-line no-new
284-
new BrowserClient(
282+
makeClient(
285283
'client-side-id',
286284
AutoEnvAttributes.Disabled,
287285
{

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

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
LDSingleKindContext,
55
} from '@launchdarkly/js-client-sdk-common';
66

7-
import { BrowserClient } from '../src/BrowserClient';
7+
import { makeClient } from '../src/BrowserClient';
88
import { makeBasicPlatform } from './BrowserClient.mocks';
99
import { goodBootstrapDataWithReasons } from './testBootstrapData';
1010

@@ -27,7 +27,7 @@ describe('given a mock platform for a BrowserClient', () => {
2727
});
2828

2929
it('includes urls in custom events', async () => {
30-
const client = new BrowserClient(
30+
const client = makeClient(
3131
'client-side-id',
3232
AutoEnvAttributes.Disabled,
3333
{
@@ -56,7 +56,7 @@ describe('given a mock platform for a BrowserClient', () => {
5656
});
5757

5858
it('can filter URLs in custom events', async () => {
59-
const client = new BrowserClient(
59+
const client = makeClient(
6060
'client-side-id',
6161
AutoEnvAttributes.Disabled,
6262
{
@@ -90,7 +90,7 @@ describe('given a mock platform for a BrowserClient', () => {
9090
});
9191

9292
it('can filter URLs in click events', async () => {
93-
const client = new BrowserClient(
93+
const client = makeClient(
9494
'client-side-id',
9595
AutoEnvAttributes.Disabled,
9696
{
@@ -133,7 +133,7 @@ describe('given a mock platform for a BrowserClient', () => {
133133
});
134134

135135
it('can filter URLs in pageview events', async () => {
136-
const client = new BrowserClient(
136+
const client = makeClient(
137137
'client-side-id',
138138
AutoEnvAttributes.Disabled,
139139
{
@@ -163,7 +163,7 @@ describe('given a mock platform for a BrowserClient', () => {
163163
});
164164

165165
it('can use bootstrap data', async () => {
166-
const client = new BrowserClient(
166+
const client = makeClient(
167167
'client-side-id',
168168
AutoEnvAttributes.Disabled,
169169
{
@@ -189,17 +189,17 @@ describe('given a mock platform for a BrowserClient', () => {
189189
});
190190
});
191191

192-
it('can shed intermediate identifyResult calls', async () => {
193-
const client = new BrowserClient(
192+
it('can shed intermediate identify calls', async () => {
193+
const client = makeClient(
194194
'client-side-id',
195195
AutoEnvAttributes.Disabled,
196196
{ streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false },
197197
platform,
198198
);
199199

200-
const promise1 = client.identifyResult({ key: 'user-key-1', kind: 'user' });
201-
const promise2 = client.identifyResult({ key: 'user-key-2', kind: 'user' });
202-
const promise3 = client.identifyResult({ key: 'user-key-3', kind: 'user' });
200+
const promise1 = client.identify({ key: 'user-key-1', kind: 'user' });
201+
const promise2 = client.identify({ key: 'user-key-2', kind: 'user' });
202+
const promise3 = client.identify({ key: 'user-key-3', kind: 'user' });
203203

204204
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
205205

@@ -212,7 +212,7 @@ describe('given a mock platform for a BrowserClient', () => {
212212

213213
it('calls beforeIdentify in order', async () => {
214214
const order: string[] = [];
215-
const client = new BrowserClient(
215+
const client = makeClient(
216216
'client-side-id',
217217
AutoEnvAttributes.Disabled,
218218
{
@@ -250,7 +250,7 @@ describe('given a mock platform for a BrowserClient', () => {
250250

251251
it('completes identify calls in order', async () => {
252252
const order: string[] = [];
253-
const client = new BrowserClient(
253+
const client = makeClient(
254254
'client-side-id',
255255
AutoEnvAttributes.Disabled,
256256
{
@@ -292,7 +292,7 @@ describe('given a mock platform for a BrowserClient', () => {
292292

293293
it('completes awaited identify calls in order without shedding', async () => {
294294
const order: string[] = [];
295-
const client = new BrowserClient(
295+
const client = makeClient(
296296
'client-side-id',
297297
AutoEnvAttributes.Disabled,
298298
{
@@ -323,9 +323,9 @@ describe('given a mock platform for a BrowserClient', () => {
323323
platform,
324324
);
325325

326-
const result1 = await client.identifyResult({ key: 'user-key-1', kind: 'user' });
327-
const result2 = await client.identifyResult({ key: 'user-key-2', kind: 'user' });
328-
const result3 = await client.identifyResult({ key: 'user-key-3', kind: 'user' });
326+
const result1 = await client.identify({ key: 'user-key-1', kind: 'user' });
327+
const result2 = await client.identify({ key: 'user-key-2', kind: 'user' });
328+
const result3 = await client.identify({ key: 'user-key-3', kind: 'user' });
329329

330330
expect(result1.status).toEqual('completed');
331331
expect(result2.status).toEqual('completed');
@@ -335,8 +335,8 @@ describe('given a mock platform for a BrowserClient', () => {
335335
expect(order).toEqual(['user-key-1', 'user-key-2', 'user-key-3']);
336336
});
337337

338-
it('can shed intermediate identify calls', async () => {
339-
const client = new BrowserClient(
338+
it('can shed intermediate identify calls without waiting for results', async () => {
339+
const client = makeClient(
340340
'client-side-id',
341341
AutoEnvAttributes.Disabled,
342342
{ streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false },
@@ -354,25 +354,16 @@ describe('given a mock platform for a BrowserClient', () => {
354354
});
355355

356356
it('it does not shed non-shedable identify calls', async () => {
357-
const client = new BrowserClient(
357+
const client = makeClient(
358358
'client-side-id',
359359
AutoEnvAttributes.Disabled,
360360
{ streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false },
361361
platform,
362362
);
363363

364-
const promise1 = client.identifyResult(
365-
{ key: 'user-key-1', kind: 'user' },
366-
{ sheddable: false },
367-
);
368-
const promise2 = client.identifyResult(
369-
{ key: 'user-key-2', kind: 'user' },
370-
{ sheddable: false },
371-
);
372-
const promise3 = client.identifyResult(
373-
{ key: 'user-key-3', kind: 'user' },
374-
{ sheddable: false },
375-
);
364+
const promise1 = client.identify({ key: 'user-key-1', kind: 'user' }, { sheddable: false });
365+
const promise2 = client.identify({ key: 'user-key-2', kind: 'user' }, { sheddable: false });
366+
const promise3 = client.identify({ key: 'user-key-3', kind: 'user' }, { sheddable: false });
376367

377368
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
378369

packages/sdk/browser/__tests__/compat/LDClientCompatImpl.test.ts

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@ import { jest } from '@jest/globals';
22

33
import { LDContext, LDFlagSet } from '@launchdarkly/js-client-sdk-common';
44

5-
import { BrowserClient } from '../../src/BrowserClient';
5+
// Import after mocking
6+
import { makeClient } from '../../src/BrowserClient';
67
import LDClientCompatImpl from '../../src/compat/LDClientCompatImpl';
78
import { LDOptions } from '../../src/compat/LDCompatOptions';
9+
import { LDClient } from '../../src/LDClient';
810

911
// @ts-ignore
10-
const mockBrowserClient: jest.MockedObject<BrowserClient> = {
12+
const mockBrowserClient: jest.MockedObject<LDClient> = {
1113
identify: jest.fn(),
1214
allFlags: jest.fn(),
1315
close: jest.fn(),
1416
flush: jest.fn(),
1517
setStreaming: jest.fn(),
1618
on: jest.fn(),
1719
off: jest.fn(),
18-
sdkKey: 'test-sdk-key',
1920
variation: jest.fn(),
2021
variationDetail: jest.fn(),
2122
boolVariation: jest.fn(),
@@ -39,36 +40,37 @@ const mockBrowserClient: jest.MockedObject<BrowserClient> = {
3940

4041
jest.mock('../../src/BrowserClient', () => ({
4142
__esModule: true,
42-
BrowserClient: jest.fn(() => mockBrowserClient),
43+
makeClient: jest.fn(),
4344
}));
4445

46+
const mockMakeClient = makeClient as jest.MockedFunction<typeof makeClient>;
47+
4548
afterEach(() => {
46-
jest.resetAllMocks();
49+
jest.clearAllMocks();
4750
});
4851

4952
beforeEach(() => {
50-
// TypesScript doesn't understand that the BrowserClient is a mock.
51-
// @ts-ignore
52-
BrowserClient.mockImplementation(() => mockBrowserClient);
53+
// Restore the mock implementation after clearing
54+
mockMakeClient.mockReturnValue(mockBrowserClient);
5355
});
5456

5557
describe('given a LDClientCompatImpl client with mocked browser client that is immediately ready', () => {
5658
let client: LDClientCompatImpl;
5759

5860
beforeEach(() => {
5961
jest.useFakeTimers();
60-
mockBrowserClient.identify.mockImplementation(() => Promise.resolve());
62+
mockBrowserClient.identify.mockImplementation(() => Promise.resolve({ status: 'completed' }));
6163
client = new LDClientCompatImpl('env-key', { kind: 'user', key: 'user-key' });
6264
});
6365

6466
it('should resolve waitForInitialization when the client is already initialized', async () => {
6567
jest.advanceTimersToNextTimer();
66-
mockBrowserClient.identify.mockResolvedValue(undefined);
68+
mockBrowserClient.identify.mockResolvedValue({ status: 'completed' });
6769

6870
await expect(client.waitForInitialization()).resolves.toBeUndefined();
6971
expect(mockBrowserClient.identify).toHaveBeenCalledWith(
7072
{ kind: 'user', key: 'user-key' },
71-
{ bootstrap: undefined, hash: undefined, noTimeout: true },
73+
{ bootstrap: undefined, hash: undefined, noTimeout: true, sheddable: false },
7274
);
7375
});
7476
});
@@ -81,7 +83,7 @@ describe('given a LDClientCompatImpl client with mocked browser client that init
8183
mockBrowserClient.identify.mockImplementation(
8284
() =>
8385
new Promise((r) => {
84-
setTimeout(r, 100);
86+
setTimeout(() => r({ status: 'completed' }), 100);
8587
}),
8688
);
8789
client = new LDClientCompatImpl('env-key', { kind: 'user', key: 'user-key' });
@@ -92,12 +94,15 @@ describe('given a LDClientCompatImpl client with mocked browser client that init
9294
const context: LDContext = { kind: 'user', key: 'new-user' };
9395
const mockFlags: LDFlagSet = { flag1: true, flag2: false };
9496

95-
mockBrowserClient.identify.mockResolvedValue();
97+
mockBrowserClient.identify.mockResolvedValue({ status: 'completed' });
9698
mockBrowserClient.allFlags.mockReturnValue(mockFlags);
9799

98100
const result = await client.identify(context);
99101

100-
expect(mockBrowserClient.identify).toHaveBeenCalledWith(context, { hash: undefined });
102+
expect(mockBrowserClient.identify).toHaveBeenCalledWith(context, {
103+
hash: undefined,
104+
sheddable: false,
105+
});
101106
expect(result).toEqual(mockFlags);
102107
});
103108

@@ -107,7 +112,7 @@ describe('given a LDClientCompatImpl client with mocked browser client that init
107112
const mockFlags: LDFlagSet = { flag1: true, flag2: false };
108113

109114
mockBrowserClient.allFlags.mockReturnValue(mockFlags);
110-
mockBrowserClient.identify.mockImplementation(() => Promise.resolve());
115+
mockBrowserClient.identify.mockImplementation(() => Promise.resolve({ status: 'completed' }));
111116
// Starting advancing the timers for the nest call. The wrapped promises
112117
// do not resolve sychronously.
113118
jest.advanceTimersToNextTimerAsync();
@@ -161,35 +166,35 @@ describe('given a LDClientCompatImpl client with mocked browser client that init
161166

162167
it('should resolve waitForInitialization when the client is initialized', async () => {
163168
jest.advanceTimersToNextTimer();
164-
mockBrowserClient.identify.mockResolvedValue(undefined);
169+
mockBrowserClient.identify.mockResolvedValue({ status: 'completed' });
165170

166171
await expect(client.waitForInitialization()).resolves.toBeUndefined();
167172
expect(mockBrowserClient.identify).toHaveBeenCalledWith(
168173
{ kind: 'user', key: 'user-key' },
169-
{ bootstrap: undefined, hash: undefined, noTimeout: true },
174+
{ bootstrap: undefined, hash: undefined, noTimeout: true, sheddable: false },
170175
);
171176
});
172177

173178
it('should resolve second waitForInitialization immediately', async () => {
174179
jest.advanceTimersToNextTimer();
175-
mockBrowserClient.identify.mockResolvedValue(undefined);
180+
mockBrowserClient.identify.mockResolvedValue({ status: 'completed' });
176181

177182
await expect(client.waitForInitialization()).resolves.toBeUndefined();
178183
await expect(client.waitForInitialization()).resolves.toBeUndefined();
179184
expect(mockBrowserClient.identify).toHaveBeenCalledWith(
180185
{ kind: 'user', key: 'user-key' },
181-
{ bootstrap: undefined, hash: undefined, noTimeout: true },
186+
{ bootstrap: undefined, hash: undefined, noTimeout: true, sheddable: false },
182187
);
183188
});
184189

185190
it('should resolve waitUntilReady immediately if the client is already initialized', async () => {
186191
jest.advanceTimersToNextTimer();
187-
mockBrowserClient.identify.mockResolvedValue(undefined);
192+
mockBrowserClient.identify.mockResolvedValue({ status: 'completed' });
188193

189194
await expect(client.waitUntilReady()).resolves.toBeUndefined();
190195
expect(mockBrowserClient.identify).toHaveBeenCalledWith(
191196
{ kind: 'user', key: 'user-key' },
192-
{ bootstrap: undefined, hash: undefined, noTimeout: true },
197+
{ bootstrap: undefined, hash: undefined, noTimeout: true, sheddable: false },
193198
);
194199
});
195200

@@ -427,6 +432,7 @@ it('forwards bootstrap and hash to BrowserClient identify call', async () => {
427432
bootstrap: bootstrapData,
428433
hash: 'someHash',
429434
noTimeout: true,
435+
sheddable: false,
430436
});
431437
});
432438

@@ -451,7 +457,7 @@ describe('given a LDClientCompatImpl client with mocked browser client which fai
451457

452458
expect(mockBrowserClient.identify).toHaveBeenCalledWith(
453459
{ kind: 'user', key: 'user-key' },
454-
{ bootstrap: undefined, hash: undefined, noTimeout: true },
460+
{ bootstrap: undefined, hash: undefined, noTimeout: true, sheddable: false },
455461
);
456462
});
457463

@@ -464,7 +470,7 @@ describe('given a LDClientCompatImpl client with mocked browser client which fai
464470

465471
expect(mockBrowserClient.identify).toHaveBeenCalledWith(
466472
{ kind: 'user', key: 'user-key' },
467-
{ bootstrap: undefined, hash: undefined, noTimeout: true },
473+
{ bootstrap: undefined, hash: undefined, noTimeout: true, sheddable: false },
468474
);
469475
});
470476

@@ -475,7 +481,7 @@ describe('given a LDClientCompatImpl client with mocked browser client which fai
475481

476482
expect(mockBrowserClient.identify).toHaveBeenCalledWith(
477483
{ kind: 'user', key: 'user-key' },
478-
{ bootstrap: undefined, hash: undefined, noTimeout: true },
484+
{ bootstrap: undefined, hash: undefined, noTimeout: true, sheddable: false },
479485
);
480486
});
481487

@@ -487,7 +493,7 @@ describe('given a LDClientCompatImpl client with mocked browser client which fai
487493

488494
expect(mockBrowserClient.identify).toHaveBeenCalledWith(
489495
{ kind: 'user', key: 'user-key' },
490-
{ bootstrap: undefined, hash: undefined, noTimeout: true },
496+
{ bootstrap: undefined, hash: undefined, noTimeout: true, sheddable: false },
491497
);
492498
});
493499

0 commit comments

Comments
 (0)