Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/sdk/vercel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,32 @@
"import": "./dist/esm/src/index.js"
},
"main": "./dist/cjs/src/index.js",
"types": "./dist/cjs/src/index.d.ts",
"files": [
"dist"
],
"scripts": {
"doc": "../../../scripts/build-doc.sh .",
"build": "../../../scripts/build-package.sh",
"clean": "rimraf dist",
"tsw": "yarn tsc --watch",
"start": "rimraf dist && yarn tsw",
"lint": "eslint . --ext .ts",
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)'",
"test": "jest --ci --runInBand --coverage",
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore",
"test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest --ci --runInBand",
"coverage": "yarn test --coverage",
"check": "yarn prettier && yarn lint && yarn build && yarn test && yarn doc"
},
"dependencies": {
"@launchdarkly/js-server-sdk-common-edge": "1.0.1",
"@vercel/edge-config": "^0.1.8",
"crypto-js": "^4.1.1"
},
"devDependencies": {
"@types/crypto-js": "^4.1.1",
"@types/jest": "^29.5.0",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"@vercel/edge-config": "^0.1.7",
"eslint": "^8.37.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
Expand All @@ -52,6 +55,7 @@
"jest": "^29.5.0",
"launchdarkly-js-test-helpers": "^2.2.0",
"prettier": "^2.8.7",
"rimraf": "^5.0.0",
"ts-jest": "^29.1.0",
"typedoc": "0.23.26",
"typescript": "^5.0.3"
Expand Down
102 changes: 73 additions & 29 deletions packages/sdk/vercel/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,95 @@
import type { EdgeConfigValue } from '@vercel/edge-config';
import { LDClient } from '@launchdarkly/js-server-sdk-common-edge';
import { LDClient, LDContext } from '@launchdarkly/js-server-sdk-common-edge';
import { init } from './index';
import * as testData from './utils/testData.json';
import mockEdge from './utils/mockEdge';
import mockEdgeConfigClient from './utils/mockEdgeConfigClient';

const sdkKey = 'test-sdk-key';
const flagKey = 'testFlag1';
const context = { kind: 'user', key: 'test-user-key-1' };
const flagKey1 = 'testFlag1';
const flagKey2 = 'testFlag2';
const flagKey3 = 'testFlag3';
const context: LDContext = { kind: 'user', key: 'test-user-key-1' };

describe('init', () => {
let ldClient: LDClient;
const mockGet = mockEdgeConfigClient.get as jest.Mock;

beforeAll(async () => {
// I can't figure out a way around the generic types used in @vercel/edge-config causing us type issues here.
// The tests work as expected
// @ts-ignore
mockEdge.get = jest.fn(async () => testData as EdgeConfigValue);
ldClient = init(sdkKey, mockEdge);
mockGet.mockImplementation(() => Promise.resolve(testData));
ldClient = init(sdkKey, mockEdgeConfigClient);
await ldClient.waitForInitialization();
});

afterAll(() => {
ldClient.close();
});

test('variation', async () => {
const flagDetail = await ldClient.variation(flagKey, context, false);
expect(flagDetail).toBeTruthy();
});
describe('flags', () => {
test('variation default', async () => {
const value = await ldClient.variation(flagKey1, context, false);
expect(value).toBeTruthy();
});

test('variation default rollout', async () => {
const contextWithEmail = { ...context, email: 'test@yahoo.com' };
const value = await ldClient.variation(flagKey2, contextWithEmail, false);
const detail = await ldClient.variationDetail(flagKey2, contextWithEmail, false);

expect(detail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 });
expect(value).toBeTruthy();
});

test('rule match', async () => {
const contextWithEmail = { ...context, email: 'test@gmail.com' };
const value = await ldClient.variation(flagKey1, contextWithEmail, false);
const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false);

expect(detail).toEqual({
reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 },
value: false,
variationIndex: 1,
});
expect(value).toBeFalsy();
});

test('fallthrough', async () => {
const contextWithEmail = { ...context, email: 'test@yahoo.com' };
const value = await ldClient.variation(flagKey1, contextWithEmail, false);
const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false);

expect(detail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 });
expect(value).toBeTruthy();
});

test('variationDetail', async () => {
const flagDetail = await ldClient.variationDetail(flagKey, context, false);
expect(flagDetail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 });
test('allFlags fallthrough', async () => {
const allFlags = await ldClient.allFlagsState(context);

expect(allFlags).toBeDefined();
expect(allFlags.toJSON()).toEqual({
$flagsState: {
testFlag1: { debugEventsUntilDate: 2000, variation: 0, version: 2 },
testFlag2: { debugEventsUntilDate: 2000, variation: 0, version: 2 },
testFlag3: { debugEventsUntilDate: 2000, variation: 0, version: 2 },
},
$valid: true,
testFlag1: true,
testFlag2: true,
testFlag3: true,
});
});
});

test('allFlags', async () => {
const allFlags = await ldClient.allFlagsState(context);

expect(allFlags).toBeDefined();
expect(allFlags.toJSON()).toEqual({
$flagsState: {
testFlag1: { debugEventsUntilDate: null, variation: 0, version: 2 },
testFlag2: { debugEventsUntilDate: null, variation: 1, version: 2 },
},
$valid: true,
testFlag1: true,
testFlag2: false,
describe('segments', () => {
test('segment by country', async () => {
const contextWithCountry = { ...context, country: 'australia' };
const value = await ldClient.variation(flagKey3, contextWithCountry, false);
const detail = await ldClient.variationDetail(flagKey3, contextWithCountry, false);

expect(detail).toEqual({
reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 },
value: false,
variationIndex: 1,
});
expect(value).toBeFalsy();
});
});
});
18 changes: 14 additions & 4 deletions packages/sdk/vercel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
import type { EdgeConfigClient } from '@vercel/edge-config';
import {
BasicLogger,
EdgeFeatureStore,
EdgeProvider,
init as initEdge,
LDClient,
LDOptions,
} from '@launchdarkly/js-server-sdk-common-edge';
import VercelFeatureStore from './vercelFeatureStore';
import createPlatformInfo from './createPlatformInfo';

export * from '@launchdarkly/js-server-sdk-common-edge';
Expand All @@ -33,20 +34,29 @@ export type { LDClient };
* (`new LDClient()/new LDClientImpl()/new LDClient()`); the SDK does not currently support
* this.
*
* @param edgeConfig
* The Vercel Edge Config client configured for LaunchDarkly.
* @param sdkKey
* The client side SDK key. This is only used to query the edgeConfig above,
* not to connect with LaunchDarkly servers.
* @param edgeConfig
* The Vercel Edge Config client configured for LaunchDarkly.
* @param options
* Optional configuration settings. The only supported option is logger.
* @return
* The new {@link LDClient} instance.
*/
export const init = (sdkKey: string, edgeConfig: EdgeConfigClient, options: LDOptions = {}) => {
const logger = options.logger ?? BasicLogger.get();

// vercel does not support string gets so we have to patch it
const edgeProvider: EdgeProvider = {
get: async (rootKey: string) => {
const json = await edgeConfig.get(rootKey);
return json ? JSON.stringify(json) : null;
},
};

return initEdge(sdkKey, createPlatformInfo(), {
featureStore: new VercelFeatureStore(edgeConfig, sdkKey, logger),
featureStore: new EdgeFeatureStore(edgeProvider, sdkKey, 'Vercel', logger),
logger,
...options,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { EdgeConfigClient } from '@vercel/edge-config';

const mockEdge: EdgeConfigClient = {
const mockEdgeConfigClient: EdgeConfigClient = {
get: jest.fn(),
getAll: jest.fn(),
digest: jest.fn(),
has: jest.fn(),
};

export default mockEdge;
export default mockEdgeConfigClient;
129 changes: 125 additions & 4 deletions packages/sdk/vercel/src/utils/testData.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,26 @@
"on": true,
"prerequisites": [],
"targets": [],
"rules": [],
"rules": [
{
"variation": 1,
"id": "rule1",
"clauses": [
{
"contextKind": "user",
"attribute": "/email",
"op": "contains",
"values": ["gmail"],
"negate": false
}
],
"trackEvents": false,
"rollout": {
"bucketBy": "bucket",
"variations": [{ "variation": 1, "weight": 100 }]
}
}
],
"fallthrough": {
"variation": 0
},
Expand All @@ -19,16 +38,58 @@
"salt": "aef830243d6640d0a973be89988e008d",
"trackEvents": false,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"debugEventsUntilDate": 2000,
"version": 2,
"deleted": false
},
"testFlag2": {
"key": "testFlag2",
"on": false,
"on": true,
"prerequisites": [],
"targets": [],
"rules": [],
"fallthrough": {
"variation": 0,
"rollout": {
"bucketBy": "bucket",
"variations": [{ "variation": 1, "weight": 100 }],
"contextKind:": "user",
"attribute": "/email"
}
},
"offVariation": 1,
"variations": [true, false],
"clientSideAvailability": {
"usingMobileKey": true,
"usingEnvironmentId": true
},
"clientSide": true,
"salt": "aef830243d6640d0a973be89988e008d",
"trackEvents": false,
"trackEventsFallthrough": false,
"debugEventsUntilDate": 2000,
"version": 2,
"deleted": false
},
"testFlag3": {
"key": "testFlag3",
"on": true,
"prerequisites": [],
"targets": [],
"rules": [
{
"variation": 1,
"id": "rule1",
"clauses": [
{
"op": "segmentMatch",
"values": ["testSegment1"],
"negate": false
}
],
"trackEvents": false
}
],
"fallthrough": {
"variation": 0
},
Expand All @@ -42,9 +103,69 @@
"salt": "aef830243d6640d0a973be89988e008d",
"trackEvents": false,
"trackEventsFallthrough": false,
"debugEventsUntilDate": null,
"debugEventsUntilDate": 2000,
"version": 2,
"deleted": false
}
},
"segments": {
"testSegment1": {
"name": "testSegment1",
"tags": [],
"creationDate": 1676063792158,
"key": "testSegment1",
"included": [],
"excluded": [],
"includedContexts": [],
"excludedContexts": [],
"_links": {
"parent": { "href": "/api/v2/segments/default/test", "type": "application/json" },
"self": {
"href": "/api/v2/segments/default/test/beta-users-1",
"type": "application/json"
},
"site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
},
"rules": [
{
"id": "rule-country",
"clauses": [
{
"attribute": "country",
"op": "in",
"values": ["australia"],
"negate": false
}
]
}
],
"version": 1,
"deleted": false,
"_access": { "denied": [], "allowed": [] },
"generation": 1
},
"testSegment2": {
"name": "testSegment2",
"tags": [],
"creationDate": 1676063792158,
"key": "testSegment2",
"included": [],
"excluded": [],
"includedContexts": [],
"excludedContexts": [],
"_links": {
"parent": { "href": "/api/v2/segments/default/test", "type": "application/json" },
"self": {
"href": "/api/v2/segments/default/test/beta-users-1",
"type": "application/json"
},
"site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
},
"rules": [],
"version": 1,
"deleted": false,
"_access": { "denied": [], "allowed": [] },
"generation": 1
}
}
}
Loading