Skip to content

Commit 1e0cf6a

Browse files
authored
fix: Client SDKs should use wrapper information. (#836)
Wrappers are accessed through the platform, but react-native and the browser implementation were not propagating these to the platform. The first implementation was the node platform, which handled this correctly. The Cloudlare and RN platform didn't, and then the RN platform was used as reference for implementing the browser platform. So, this also affects edge and a similar update should be considered for those SDKs.
1 parent a3b816b commit 1e0cf6a

File tree

10 files changed

+173
-19
lines changed

10 files changed

+173
-19
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
102102
createHash: () => new MockHasher(),
103103
randomUUID: () => '123',
104104
},
105-
info: new BrowserInfo(),
105+
info: new BrowserInfo({}),
106106
requests: {
107107
createEventSource: jest.fn((streamUri: string = '', options: any = {}) => ({
108108
streamUri,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import BrowserInfo from '../../src/platform/BrowserInfo';
2+
3+
it('returns correct platform data', () => {
4+
const browserInfo = new BrowserInfo({});
5+
expect(browserInfo.platformData()).toEqual({
6+
name: 'JS',
7+
});
8+
});
9+
10+
it('returns correct SDK data without wrapper info', () => {
11+
const browserInfo = new BrowserInfo({});
12+
expect(browserInfo.sdkData()).toEqual({
13+
name: '@launchdarkly/js-client-sdk',
14+
version: '0.0.0',
15+
userAgentBase: 'JSClient',
16+
});
17+
});
18+
19+
it('returns correct SDK data with wrapper name', () => {
20+
const browserInfo = new BrowserInfo({ wrapperName: 'test-wrapper' });
21+
expect(browserInfo.sdkData()).toEqual({
22+
name: '@launchdarkly/js-client-sdk',
23+
version: '0.0.0',
24+
userAgentBase: 'JSClient',
25+
wrapperName: 'test-wrapper',
26+
});
27+
});
28+
29+
it('returns correct SDK data with wrapper version', () => {
30+
const browserInfo = new BrowserInfo({ wrapperVersion: '1.0.0' });
31+
expect(browserInfo.sdkData()).toEqual({
32+
name: '@launchdarkly/js-client-sdk',
33+
version: '0.0.0',
34+
userAgentBase: 'JSClient',
35+
wrapperVersion: '1.0.0',
36+
});
37+
});
38+
39+
it('returns correct SDK data with both wrapper name and version', () => {
40+
const browserInfo = new BrowserInfo({
41+
wrapperName: 'test-wrapper',
42+
wrapperVersion: '1.0.0',
43+
});
44+
expect(browserInfo.sdkData()).toEqual({
45+
name: '@launchdarkly/js-client-sdk',
46+
version: '0.0.0',
47+
userAgentBase: 'JSClient',
48+
wrapperName: 'test-wrapper',
49+
wrapperVersion: '1.0.0',
50+
});
51+
});

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class BrowserClient extends LDClientImpl implements LDClient {
115115
// TODO: Use the already-configured baseUri from the SDK config. SDK-560
116116
const baseUrl = options.baseUri ?? 'https://clientsdk.launchdarkly.com';
117117

118-
const platform = overridePlatform ?? new BrowserPlatform(logger);
118+
const platform = overridePlatform ?? new BrowserPlatform(logger, options);
119119
// Only the browser-specific options are in validatedBrowserOptions.
120120
const validatedBrowserOptions = validateBrowserOptions(options, logger);
121121
// The base options are in baseOptionsWithDefaults.
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
import { Info, PlatformData, SdkData } from '@launchdarkly/js-client-sdk-common';
22

33
export default class BrowserInfo implements Info {
4+
constructor(private readonly _config: { wrapperName?: string; wrapperVersion?: string }) {}
5+
46
platformData(): PlatformData {
57
return {
68
name: 'JS', // Name maintained from previous 3.x implementation.
79
};
810
}
11+
912
sdkData(): SdkData {
10-
return {
13+
const data: SdkData = {
1114
name: '@launchdarkly/js-client-sdk',
1215
version: '0.0.0', // x-release-please-version
1316
userAgentBase: 'JSClient',
1417
};
18+
19+
if (this._config.wrapperName) {
20+
data.wrapperName = this._config.wrapperName;
21+
}
22+
23+
if (this._config.wrapperVersion) {
24+
data.wrapperVersion = this._config.wrapperVersion;
25+
}
26+
27+
return data;
1528
}
1629
}

packages/sdk/browser/src/platform/BrowserPlatform.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Storage,
99
} from '@launchdarkly/js-client-sdk-common';
1010

11+
import { BrowserOptions } from '../options';
1112
import BrowserCrypto from './BrowserCrypto';
1213
import BrowserEncoding from './BrowserEncoding';
1314
import BrowserInfo from './BrowserInfo';
@@ -16,15 +17,16 @@ import LocalStorage, { isLocalStorageSupported } from './LocalStorage';
1617

1718
export default class BrowserPlatform implements Platform {
1819
encoding: Encoding = new BrowserEncoding();
19-
info: Info = new BrowserInfo();
20+
info: Info;
2021
// fileSystem?: Filesystem;
2122
crypto: Crypto = new BrowserCrypto();
2223
requests: Requests = new BrowserRequests();
2324
storage?: Storage;
2425

25-
constructor(logger: LDLogger) {
26+
constructor(logger: LDLogger, options: BrowserOptions) {
2627
if (isLocalStorageSupported()) {
2728
this.storage = new LocalStorage(logger);
2829
}
30+
this.info = new BrowserInfo(options);
2931
}
3032
}

packages/sdk/react-native/__tests__/MobileDataManager.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ describe('given a MobileDataManager with mocked dependencies', () => {
9191
const mockedFetch = mockFetch('{"flagA": true}', 200);
9292
platform = {
9393
crypto: new PlatformCrypto(),
94-
info: new PlatformInfo(config.logger),
94+
info: new PlatformInfo(config.logger, {}),
9595
requests: {
9696
createEventSource: jest.fn((streamUri: string = '', options: any = {}) => ({
9797
streamUri,

packages/sdk/react-native/__tests__/ReactNativeLDClient.test.ts

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jest.mock('../src/platform', () => ({
3838
__esModule: true,
3939
default: jest.fn((logger: LDLogger) => ({
4040
crypto: new PlatformCrypto(),
41-
info: new PlatformInfo(logger),
41+
info: new PlatformInfo(logger, {}),
4242
requests: {
4343
createEventSource: jest.fn(),
4444
fetch: jest.fn(),
@@ -67,7 +67,7 @@ it('uses correct default diagnostic url', () => {
6767
};
6868
(createPlatform as jest.Mock).mockReturnValue({
6969
crypto: new PlatformCrypto(),
70-
info: new PlatformInfo(logger),
70+
info: new PlatformInfo(logger, {}),
7171
requests: {
7272
createEventSource: jest.fn(),
7373
fetch: mockedFetch,
@@ -95,7 +95,7 @@ it('uses correct default analytics event url', async () => {
9595
};
9696
(createPlatform as jest.Mock).mockReturnValue({
9797
crypto: new PlatformCrypto(),
98-
info: new PlatformInfo(logger),
98+
info: new PlatformInfo(logger, {}),
9999
requests: {
100100
createEventSource: createMockEventSource,
101101
fetch: mockedFetch,
@@ -127,7 +127,7 @@ it('uses correct default polling url', async () => {
127127
};
128128
(createPlatform as jest.Mock).mockReturnValue({
129129
crypto: new PlatformCrypto(),
130-
info: new PlatformInfo(logger),
130+
info: new PlatformInfo(logger, {}),
131131
requests: {
132132
createEventSource: jest.fn(),
133133
fetch: mockedFetch,
@@ -158,7 +158,7 @@ it('uses correct default streaming url', (done) => {
158158
};
159159
(createPlatform as jest.Mock).mockReturnValue({
160160
crypto: new PlatformCrypto(),
161-
info: new PlatformInfo(logger),
161+
info: new PlatformInfo(logger, {}),
162162
requests: {
163163
createEventSource: mockedCreateEventSource,
164164
fetch: jest.fn(),
@@ -198,7 +198,7 @@ it('includes authorization header for polling', async () => {
198198
};
199199
(createPlatform as jest.Mock).mockReturnValue({
200200
crypto: new PlatformCrypto(),
201-
info: new PlatformInfo(logger),
201+
info: new PlatformInfo(logger, {}),
202202
requests: {
203203
createEventSource: jest.fn(),
204204
fetch: mockedFetch,
@@ -223,6 +223,41 @@ it('includes authorization header for polling', async () => {
223223
);
224224
});
225225

226+
it('includes x-launchdarkly-wrapper header for polling', async () => {
227+
const mockedFetch = mockFetch('{"flagA": true}', 200);
228+
const logger: LDLogger = {
229+
error: jest.fn(),
230+
warn: jest.fn(),
231+
info: jest.fn(),
232+
debug: jest.fn(),
233+
};
234+
(createPlatform as jest.Mock).mockReturnValue({
235+
crypto: new PlatformCrypto(),
236+
info: new PlatformInfo(logger, { wrapperName: 'Rapper', wrapperVersion: '1.0.0' }),
237+
requests: {
238+
createEventSource: jest.fn(),
239+
fetch: mockedFetch,
240+
getEventSourceCapabilities: jest.fn(),
241+
},
242+
encoding: new PlatformEncoding(),
243+
storage: new PlatformStorage(logger),
244+
});
245+
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
246+
diagnosticOptOut: true,
247+
sendEvents: false,
248+
initialConnectionMode: 'polling',
249+
automaticBackgroundHandling: false,
250+
});
251+
await client.identify({ kind: 'user', key: 'bob' });
252+
253+
expect(mockedFetch).toHaveBeenCalledWith(
254+
expect.anything(),
255+
expect.objectContaining({
256+
headers: expect.objectContaining({ 'x-launchdarkly-wrapper': 'Rapper/1.0.0' }),
257+
}),
258+
);
259+
});
260+
226261
it('includes authorization header for streaming', (done) => {
227262
const mockedCreateEventSource = jest.fn();
228263
const logger: LDLogger = {
@@ -233,7 +268,7 @@ it('includes authorization header for streaming', (done) => {
233268
};
234269
(createPlatform as jest.Mock).mockReturnValue({
235270
crypto: new PlatformCrypto(),
236-
info: new PlatformInfo(logger),
271+
info: new PlatformInfo(logger, {}),
237272
requests: {
238273
createEventSource: mockedCreateEventSource,
239274
fetch: jest.fn(),
@@ -264,6 +299,47 @@ it('includes authorization header for streaming', (done) => {
264299
});
265300
});
266301

302+
it('includes wrapper header for streaming', (done) => {
303+
const mockedCreateEventSource = jest.fn();
304+
const logger: LDLogger = {
305+
error: jest.fn(),
306+
warn: jest.fn(),
307+
info: jest.fn(),
308+
debug: jest.fn(),
309+
};
310+
(createPlatform as jest.Mock).mockReturnValue({
311+
crypto: new PlatformCrypto(),
312+
info: new PlatformInfo(logger, { wrapperName: 'Rapper', wrapperVersion: '1.0.0' }),
313+
requests: {
314+
createEventSource: mockedCreateEventSource,
315+
fetch: jest.fn(),
316+
getEventSourceCapabilities: jest.fn(),
317+
},
318+
encoding: new PlatformEncoding(),
319+
storage: new PlatformStorage(logger),
320+
});
321+
const client = new ReactNativeLDClient('mobile-key', AutoEnvAttributes.Enabled, {
322+
diagnosticOptOut: true,
323+
sendEvents: false,
324+
initialConnectionMode: 'streaming',
325+
automaticBackgroundHandling: false,
326+
});
327+
328+
client
329+
.identify({ kind: 'user', key: 'bob' }, { timeout: 0 })
330+
.then(() => {})
331+
.catch(() => {})
332+
.then(() => {
333+
expect(mockedCreateEventSource).toHaveBeenCalledWith(
334+
expect.anything(),
335+
expect.objectContaining({
336+
headers: expect.objectContaining({ 'x-launchdarkly-wrapper': 'Rapper/1.0.0' }),
337+
}),
338+
);
339+
done();
340+
});
341+
});
342+
267343
it('includes authorization header for events', async () => {
268344
const mockedFetch = mockFetch('{"flagA": true}', 200);
269345
const logger: LDLogger = {
@@ -274,7 +350,7 @@ it('includes authorization header for events', async () => {
274350
};
275351
(createPlatform as jest.Mock).mockReturnValue({
276352
crypto: new PlatformCrypto(),
277-
info: new PlatformInfo(logger),
353+
info: new PlatformInfo(logger, {}),
278354
requests: {
279355
createEventSource: jest.fn(),
280356
fetch: mockedFetch,

packages/sdk/react-native/src/ReactNativeLDClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default class ReactNativeLDClient extends LDClientImpl {
6262
};
6363

6464
const validatedRnOptions = validateOptions(options, logger);
65-
const platform = createPlatform(logger, validatedRnOptions.storage);
65+
const platform = createPlatform(logger, options, validatedRnOptions.storage);
6666

6767
super(
6868
sdkKey,

packages/sdk/react-native/src/platform/PlatformInfo.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { name, version } from '../../package.json';
44
import { ldApplication, ldDevice } from './autoEnv';
55

66
export default class PlatformInfo implements Info {
7-
constructor(private readonly _logger: LDLogger) {}
7+
constructor(
8+
private readonly _logger: LDLogger,
9+
private readonly _config: { wrapperName?: string; wrapperVersion?: string },
10+
) {}
811

912
platformData(): PlatformData {
1013
const data = {
@@ -18,12 +21,20 @@ export default class PlatformInfo implements Info {
1821
}
1922

2023
sdkData(): SdkData {
21-
const data = {
24+
const data: SdkData = {
2225
name,
2326
version,
2427
userAgentBase: 'ReactNativeClient',
2528
};
2629

30+
if (this._config?.wrapperName) {
31+
data.wrapperName = this._config.wrapperName;
32+
}
33+
34+
if (this._config?.wrapperVersion) {
35+
data.wrapperVersion = this._config.wrapperVersion;
36+
}
37+
2738
this._logger.debug(`sdkData: ${JSON.stringify(data, null, 2)}`);
2839
return data;
2940
}

packages/sdk/react-native/src/platform/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { LDLogger, Platform, Storage } from '@launchdarkly/js-client-sdk-common';
22

3+
import RNOptions from '../RNOptions';
34
import PlatformCrypto from './crypto';
45
import PlatformEncoding from './PlatformEncoding';
56
import PlatformInfo from './PlatformInfo';
67
import PlatformRequests from './PlatformRequests';
78
import PlatformStorage from './PlatformStorage';
89

9-
const createPlatform = (logger: LDLogger, storage?: Storage): Platform => ({
10+
const createPlatform = (logger: LDLogger, options: RNOptions, storage?: Storage): Platform => ({
1011
crypto: new PlatformCrypto(),
11-
info: new PlatformInfo(logger),
12+
info: new PlatformInfo(logger, options),
1213
requests: new PlatformRequests(logger),
1314
encoding: new PlatformEncoding(),
1415
storage: storage ?? new PlatformStorage(logger),

0 commit comments

Comments
 (0)