Skip to content

Commit 8d80259

Browse files
authored
feat: Implement common client side support for auto environment attributes. (#356)
This is a follow up to #355. This PR implements client-sdk support for auto env. In addition, the mocks internal api have been updated to work better with crypto and mock resets. Server sdks are impacted by the mocks api changes so these are fixed in this PR as well. ### Note #357 Fixes react-native build and tests. #358 adds application name and versionName to LDOptions.
1 parent 9f562e5 commit 8d80259

36 files changed

+2020
-880
lines changed

packages/shared/common/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ module.exports = {
44
testEnvironment: 'node',
55
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
66
collectCoverageFrom: ['src/**/*.ts'],
7+
setupFilesAfterEnv: ['@launchdarkly/private-js-mocks/setup'],
78
};

packages/shared/common/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"build": "npx tsc",
2525
"clean": "npx tsc --build --clean",
2626
"lint": "npx eslint . --ext .ts",
27-
"lint:fix": "yarn run lint --fix"
27+
"lint:fix": "yarn run lint --fix",
28+
"prettier": "prettier --write 'src/*.@(js|ts|tsx|json)'",
29+
"check": "yarn && yarn prettier && yarn lint && tsc && yarn test"
2830
},
2931
"license": "Apache-2.0",
3032
"devDependencies": {

packages/shared/common/src/api/platform/AutoEnv.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/* eslint-disable import/prefer-default-export */
2+
/**
3+
* Enable / disable Auto environment attributes. When enabled, the SDK will automatically
4+
* provide data about the mobile environment where the application is running. This data makes it simpler to target
5+
* your mobile customers based on application name or version, or on device characteristics including manufacturer,
6+
* model, operating system, locale, and so on. We recommend enabling this when you configure the SDK. To learn more,
7+
* read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes).
8+
* for more documentation.
9+
*
10+
* The default is disabled.
11+
*/
12+
export enum AutoEnvAttributes {
13+
Disabled,
14+
Enabled,
15+
}
16+
117
interface AutoEnvCommon {
218
/**
319
* Unique key for the context kind.
@@ -30,8 +46,8 @@ export interface LDDevice extends AutoEnvCommon {
3046
/**
3147
* The family of operating system.
3248
*/
33-
family: string;
34-
name: string;
35-
version: string;
49+
family?: string;
50+
name?: string;
51+
version?: string;
3652
};
3753
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import deepCompact from './deepCompact';
2+
3+
describe('deepCompact', () => {
4+
test('if arg is undefined, return it', () => {
5+
const compacted = deepCompact(undefined);
6+
expect(compacted).toBeUndefined();
7+
});
8+
9+
test('should remove all falsy, {} and ignored values', () => {
10+
const data = {
11+
ld_application: {
12+
key: '',
13+
envAttributesVersion: '1.0',
14+
id: 'com.testapp.ld',
15+
name: 'LDApplication.TestApp',
16+
version: '1.1.1',
17+
},
18+
ld_device: {
19+
key: '',
20+
envAttributesVersion: '1.0',
21+
os: {},
22+
manufacturer: 'coconut',
23+
model: null,
24+
storageBytes: undefined,
25+
},
26+
};
27+
const compacted = deepCompact(data, ['key', 'envAttributesVersion']);
28+
expect(compacted).toEqual({
29+
ld_application: {
30+
id: 'com.testapp.ld',
31+
name: 'LDApplication.TestApp',
32+
version: '1.1.1',
33+
},
34+
ld_device: {
35+
manufacturer: 'coconut',
36+
},
37+
});
38+
});
39+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import isEmptyObject from './isEmptyObject';
2+
3+
/**
4+
* Strips all falsy and empty {} from a given object. Returns a new object with only truthy values.
5+
* Sourced from below but modified to include checks for empty object and ignoring keys.
6+
* https://www.w3resource.com/javascript-exercises/javascript-array-exercise-47.php
7+
*
8+
* @param obj
9+
* @param ignoreKeys
10+
*/
11+
const deepCompact = <T extends Object>(obj?: T, ignoreKeys?: string[]) => {
12+
if (!obj) {
13+
return obj;
14+
}
15+
16+
return Object.entries(obj).reduce((acc: any, [key, value]) => {
17+
if (Boolean(value) && !isEmptyObject(value) && !ignoreKeys?.includes(key)) {
18+
acc[key] = typeof value === 'object' ? deepCompact(value, ignoreKeys) : value;
19+
}
20+
return acc;
21+
}, {}) as T;
22+
};
23+
24+
export default deepCompact;

packages/shared/common/src/utils/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import clone from './clone';
22
import { secondsToMillis } from './date';
3+
import deepCompact from './deepCompact';
34
import fastDeepEqual from './fast-deep-equal';
45
import { base64UrlEncode, defaultHeaders, httpErrorMessage, LDHeaders, shouldRetry } from './http';
56
import noop from './noop';
@@ -9,6 +10,7 @@ import { VoidFunction } from './VoidFunction';
910
export {
1011
base64UrlEncode,
1112
clone,
13+
deepCompact,
1214
defaultHeaders,
1315
fastDeepEqual,
1416
httpErrorMessage,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const isEmptyObject = (obj: any) => JSON.stringify(obj) === '{}';
2+
3+
export default isEmptyObject;

packages/shared/mocks/README.md

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,111 @@
22

33
[![Actions Status][mocks-ci-badge]][mocks-ci]
44

5-
**Internal use only.**
5+
> [!CAUTION]
6+
> Internal use only.
7+
> This project contains JavaScript mocks that are consumed in unit tests in client-side and server-side JavaScript SDKs.
68
7-
This project contains JavaScript mocks that are consumed in unit tests in client-side and server-side JavaScript SDKs.
9+
## Installation
10+
11+
This package is not published publicly. To use it internally, add the following line to your project's package.json
12+
devDependencies. yarn workspace has been setup to recognize this package so this dependency should automatically work:
13+
14+
```bash
15+
"devDependencies": {
16+
"@launchdarkly/private-js-mocks": "0.0.1",
17+
...
18+
```
19+
20+
Then in your jest config add `@launchdarkly/private-js-mocks/setup` to setupFilesAfterEnv:
21+
22+
```js
23+
// jest.config.js or jest.config.json
24+
module.exports = {
25+
setupFilesAfterEnv: ['@launchdarkly/private-js-mocks/setup'],
26+
...
27+
}
28+
```
29+
30+
## Usage
31+
32+
> [!IMPORTANT]
33+
> basicPlatform and clientContext must be used inside a test because it's setup before each test.
34+
35+
- `basicPlatform`: a concrete but basic implementation of [Platform](https://github.com/launchdarkly/js-core/blob/main/packages/shared/common/src/api/platform/Platform.ts). This is setup beforeEach so it must be used inside a test.
36+
37+
- `clientContext`: ClientContext object including `basicPlatform` above. This is setup beforeEach so it must be used inside a test as well.
38+
39+
- `hasher`: a Hasher object returned by `Crypto.createHash`. All functions in this object are jest mocks. This is exported
40+
separately as a top level export because `Crypto` does not expose this publicly and we want to respect that.
41+
42+
## Example
43+
44+
```tsx
45+
import { basicPlatform, clientContext, hasher } from '@launchdarkly/private-js-mocks';
46+
47+
// DOES NOT WORK: crypto is undefined because basicPlatform must be inside a test
48+
// because it's setup by the package in beforeEach.
49+
const { crypto } = basicPlatform; // DON'T DO THIS HERE
50+
51+
// DOES NOT WORK: clientContext must be used inside a test. Otherwise all properties
52+
// of it will be undefined.
53+
const {
54+
basicConfiguration: { serviceEndpoints, tags },
55+
platform: { info },
56+
} = clientContext; // DON'T DO THIS HERE
57+
58+
describe('button', () => {
59+
// DOES NOT WORK: again must be inside an actual test. At the test suite,
60+
// level, beforeEach has not been run.
61+
const { crypto } = basicPlatform; // DON'T DO THIS HERE
62+
63+
// DO THIS
64+
let crypto: Crypto;
65+
let info: Info;
66+
let serviceEndpoints: ServiceEndpoints;
67+
let tags: ApplicationTags;
68+
69+
beforeEach(() => {
70+
// WORKS: basicPlatform and clientContext have been setup by the package.
71+
({ crypto, info } = basicPlatform);
72+
73+
// WORKS
74+
({
75+
basicConfiguration: { serviceEndpoints, tags },
76+
platform: { info },
77+
} = clientContext);
78+
});
79+
80+
afterEach(() => {
81+
jest.resetAllMocks();
82+
});
83+
84+
it('hashes the correct string', () => {
85+
// arrange
86+
const bucketer = new Bucketer(crypto);
87+
88+
// act
89+
const [bucket, hadContext] = bucketer.bucket();
90+
91+
// assert
92+
// WORKS
93+
expect(crypto.createHash).toHaveBeenCalled();
94+
95+
// WORKS: alternatively you can just use the full path to access the properties
96+
// of basicPlatform
97+
expect(basicPlatform.crypto.createHash).toHaveBeenCalled();
98+
99+
// GOTCHA: hasher is a separte import from crypto to respect
100+
// the public Crypto interface.
101+
expect(hasher.update).toHaveBeenCalledWith(expected);
102+
expect(hasher.digest).toHaveBeenCalledWith('hex');
103+
});
104+
});
105+
```
106+
107+
## Developing this package
108+
109+
If you make changes to this package, you'll need to run `yarn build` in the `mocks` directory for changes to take effect.
8110
9111
## Contributing
10112

packages/shared/mocks/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
"type": "commonjs",
66
"main": "./dist/index.js",
77
"types": "./dist/index.d.ts",
8+
"exports": {
9+
".": "./dist/index.js",
10+
"./setup": {
11+
"default": "./dist/setupMocks.js"
12+
}
13+
},
814
"homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/shared/common",
915
"repository": {
1016
"type": "git",

packages/shared/mocks/src/clientContext.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import type { ClientContext } from '@common';
22

3-
import basicPlatform from './platform';
3+
import { basicPlatform } from './platform';
44

5-
const clientContext: ClientContext = {
6-
basicConfiguration: {
7-
sdkKey: 'testSdkKey',
8-
serviceEndpoints: {
9-
events: '',
10-
polling: '',
11-
streaming: 'https://mockstream.ld.com',
12-
diagnosticEventPath: '/diagnostic',
13-
analyticsEventPath: '/bulk',
14-
includeAuthorizationHeader: true,
5+
// eslint-disable-next-line import/no-mutable-exports
6+
export let clientContext: ClientContext;
7+
export const setupClientContext = () => {
8+
clientContext = {
9+
basicConfiguration: {
10+
sdkKey: 'testSdkKey',
11+
serviceEndpoints: {
12+
events: '',
13+
polling: '',
14+
streaming: 'https://mockstream.ld.com',
15+
diagnosticEventPath: '/diagnostic',
16+
analyticsEventPath: '/bulk',
17+
includeAuthorizationHeader: true,
18+
},
1519
},
16-
},
17-
platform: basicPlatform,
20+
platform: basicPlatform,
21+
};
1822
};
19-
20-
export default clientContext;

0 commit comments

Comments
 (0)