Skip to content

Commit

Permalink
feat: Implement common client side support for auto environment attri…
Browse files Browse the repository at this point in the history
…butes. (#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.
  • Loading branch information
yusinto authored Feb 1, 2024
1 parent 9f562e5 commit 8d80259
Show file tree
Hide file tree
Showing 36 changed files with 2,020 additions and 880 deletions.
1 change: 1 addition & 0 deletions packages/shared/common/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ module.exports = {
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
collectCoverageFrom: ['src/**/*.ts'],
setupFilesAfterEnv: ['@launchdarkly/private-js-mocks/setup'],
};
4 changes: 3 additions & 1 deletion packages/shared/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"build": "npx tsc",
"clean": "npx tsc --build --clean",
"lint": "npx eslint . --ext .ts",
"lint:fix": "yarn run lint --fix"
"lint:fix": "yarn run lint --fix",
"prettier": "prettier --write 'src/*.@(js|ts|tsx|json)'",
"check": "yarn && yarn prettier && yarn lint && tsc && yarn test"
},
"license": "Apache-2.0",
"devDependencies": {
Expand Down
22 changes: 19 additions & 3 deletions packages/shared/common/src/api/platform/AutoEnv.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/* eslint-disable import/prefer-default-export */
/**
* Enable / disable Auto environment attributes. When enabled, the SDK will automatically
* provide data about the mobile environment where the application is running. This data makes it simpler to target
* your mobile customers based on application name or version, or on device characteristics including manufacturer,
* model, operating system, locale, and so on. We recommend enabling this when you configure the SDK. To learn more,
* read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes).
* for more documentation.
*
* The default is disabled.
*/
export enum AutoEnvAttributes {
Disabled,
Enabled,
}

interface AutoEnvCommon {
/**
* Unique key for the context kind.
Expand Down Expand Up @@ -30,8 +46,8 @@ export interface LDDevice extends AutoEnvCommon {
/**
* The family of operating system.
*/
family: string;
name: string;
version: string;
family?: string;
name?: string;
version?: string;
};
}
39 changes: 39 additions & 0 deletions packages/shared/common/src/utils/deepCompact.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import deepCompact from './deepCompact';

describe('deepCompact', () => {
test('if arg is undefined, return it', () => {
const compacted = deepCompact(undefined);
expect(compacted).toBeUndefined();
});

test('should remove all falsy, {} and ignored values', () => {
const data = {
ld_application: {
key: '',
envAttributesVersion: '1.0',
id: 'com.testapp.ld',
name: 'LDApplication.TestApp',
version: '1.1.1',
},
ld_device: {
key: '',
envAttributesVersion: '1.0',
os: {},
manufacturer: 'coconut',
model: null,
storageBytes: undefined,
},
};
const compacted = deepCompact(data, ['key', 'envAttributesVersion']);
expect(compacted).toEqual({
ld_application: {
id: 'com.testapp.ld',
name: 'LDApplication.TestApp',
version: '1.1.1',
},
ld_device: {
manufacturer: 'coconut',
},
});
});
});
24 changes: 24 additions & 0 deletions packages/shared/common/src/utils/deepCompact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import isEmptyObject from './isEmptyObject';

/**
* Strips all falsy and empty {} from a given object. Returns a new object with only truthy values.
* Sourced from below but modified to include checks for empty object and ignoring keys.
* https://www.w3resource.com/javascript-exercises/javascript-array-exercise-47.php
*
* @param obj
* @param ignoreKeys
*/
const deepCompact = <T extends Object>(obj?: T, ignoreKeys?: string[]) => {
if (!obj) {
return obj;
}

return Object.entries(obj).reduce((acc: any, [key, value]) => {
if (Boolean(value) && !isEmptyObject(value) && !ignoreKeys?.includes(key)) {
acc[key] = typeof value === 'object' ? deepCompact(value, ignoreKeys) : value;
}
return acc;
}, {}) as T;
};

export default deepCompact;
2 changes: 2 additions & 0 deletions packages/shared/common/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import clone from './clone';
import { secondsToMillis } from './date';
import deepCompact from './deepCompact';
import fastDeepEqual from './fast-deep-equal';
import { base64UrlEncode, defaultHeaders, httpErrorMessage, LDHeaders, shouldRetry } from './http';
import noop from './noop';
Expand All @@ -9,6 +10,7 @@ import { VoidFunction } from './VoidFunction';
export {
base64UrlEncode,
clone,
deepCompact,
defaultHeaders,
fastDeepEqual,
httpErrorMessage,
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/common/src/utils/isEmptyObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const isEmptyObject = (obj: any) => JSON.stringify(obj) === '{}';

export default isEmptyObject;
106 changes: 104 additions & 2 deletions packages/shared/mocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,111 @@

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

**Internal use only.**
> [!CAUTION]
> Internal use only.
> This project contains JavaScript mocks that are consumed in unit tests in client-side and server-side JavaScript SDKs.
This project contains JavaScript mocks that are consumed in unit tests in client-side and server-side JavaScript SDKs.
## Installation

This package is not published publicly. To use it internally, add the following line to your project's package.json
devDependencies. yarn workspace has been setup to recognize this package so this dependency should automatically work:

```bash
"devDependencies": {
"@launchdarkly/private-js-mocks": "0.0.1",
...
```
Then in your jest config add `@launchdarkly/private-js-mocks/setup` to setupFilesAfterEnv:
```js
// jest.config.js or jest.config.json
module.exports = {
setupFilesAfterEnv: ['@launchdarkly/private-js-mocks/setup'],
...
}
```
## Usage
> [!IMPORTANT]
> basicPlatform and clientContext must be used inside a test because it's setup before each test.
- `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.
- `clientContext`: ClientContext object including `basicPlatform` above. This is setup beforeEach so it must be used inside a test as well.
- `hasher`: a Hasher object returned by `Crypto.createHash`. All functions in this object are jest mocks. This is exported
separately as a top level export because `Crypto` does not expose this publicly and we want to respect that.
## Example
```tsx
import { basicPlatform, clientContext, hasher } from '@launchdarkly/private-js-mocks';
// DOES NOT WORK: crypto is undefined because basicPlatform must be inside a test
// because it's setup by the package in beforeEach.
const { crypto } = basicPlatform; // DON'T DO THIS HERE
// DOES NOT WORK: clientContext must be used inside a test. Otherwise all properties
// of it will be undefined.
const {
basicConfiguration: { serviceEndpoints, tags },
platform: { info },
} = clientContext; // DON'T DO THIS HERE
describe('button', () => {
// DOES NOT WORK: again must be inside an actual test. At the test suite,
// level, beforeEach has not been run.
const { crypto } = basicPlatform; // DON'T DO THIS HERE
// DO THIS
let crypto: Crypto;
let info: Info;
let serviceEndpoints: ServiceEndpoints;
let tags: ApplicationTags;
beforeEach(() => {
// WORKS: basicPlatform and clientContext have been setup by the package.
({ crypto, info } = basicPlatform);
// WORKS
({
basicConfiguration: { serviceEndpoints, tags },
platform: { info },
} = clientContext);
});
afterEach(() => {
jest.resetAllMocks();
});
it('hashes the correct string', () => {
// arrange
const bucketer = new Bucketer(crypto);
// act
const [bucket, hadContext] = bucketer.bucket();
// assert
// WORKS
expect(crypto.createHash).toHaveBeenCalled();
// WORKS: alternatively you can just use the full path to access the properties
// of basicPlatform
expect(basicPlatform.crypto.createHash).toHaveBeenCalled();
// GOTCHA: hasher is a separte import from crypto to respect
// the public Crypto interface.
expect(hasher.update).toHaveBeenCalledWith(expected);
expect(hasher.digest).toHaveBeenCalledWith('hex');
});
});
```
## Developing this package
If you make changes to this package, you'll need to run `yarn build` in the `mocks` directory for changes to take effect.
## Contributing
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/mocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
"type": "commonjs",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./setup": {
"default": "./dist/setupMocks.js"
}
},
"homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/shared/common",
"repository": {
"type": "git",
Expand Down
32 changes: 17 additions & 15 deletions packages/shared/mocks/src/clientContext.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import type { ClientContext } from '@common';

import basicPlatform from './platform';
import { basicPlatform } from './platform';

const clientContext: ClientContext = {
basicConfiguration: {
sdkKey: 'testSdkKey',
serviceEndpoints: {
events: '',
polling: '',
streaming: 'https://mockstream.ld.com',
diagnosticEventPath: '/diagnostic',
analyticsEventPath: '/bulk',
includeAuthorizationHeader: true,
// eslint-disable-next-line import/no-mutable-exports
export let clientContext: ClientContext;
export const setupClientContext = () => {
clientContext = {
basicConfiguration: {
sdkKey: 'testSdkKey',
serviceEndpoints: {
events: '',
polling: '',
streaming: 'https://mockstream.ld.com',
diagnosticEventPath: '/diagnostic',
analyticsEventPath: '/bulk',
includeAuthorizationHeader: true,
},
},
},
platform: basicPlatform,
platform: basicPlatform,
};
};

export default clientContext;
23 changes: 23 additions & 0 deletions packages/shared/mocks/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Hasher } from '@common';

// eslint-disable-next-line import/no-mutable-exports
export let hasher: Hasher;

export const setupCrypto = () => {
let counter = 0;
hasher = {
update: jest.fn(),
digest: jest.fn(() => '1234567890123456'),
};

return {
createHash: jest.fn(() => hasher),
createHmac: jest.fn(),
randomUUID: jest.fn(() => {
counter += 1;
// Will provide a unique value for tests.
// Very much not a UUID of course.
return `${counter}`;
}),
};
};
9 changes: 4 additions & 5 deletions packages/shared/mocks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import clientContext from './clientContext';
import { clientContext } from './clientContext';
import ContextDeduplicator from './contextDeduplicator';
import { crypto, hasher } from './hasher';
import { hasher } from './crypto';
import logger from './logger';
import mockFetch from './mockFetch';
import basicPlatform from './platform';
import { basicPlatform } from './platform';
import { MockStreamingProcessor, setupMockStreamingProcessor } from './streamingProcessor';

export {
basicPlatform,
clientContext,
hasher,
mockFetch,
crypto,
logger,
hasher,
ContextDeduplicator,
MockStreamingProcessor,
setupMockStreamingProcessor,
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/mocks/src/mockFetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Response } from '@common';

import basicPlatform from './platform';
import { basicPlatform } from './platform';

const createMockResponse = (remoteJson: any, statusCode: number) => {
const response: Response = {
Expand Down
Loading

0 comments on commit 8d80259

Please sign in to comment.