From 5d85fa0276dbdc4d1165583fdaec77ee462e984c Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 27 Apr 2023 17:23:26 -0700 Subject: [PATCH 01/10] fix: fixed bug where the feature store does not deserialize KV values correctly. --- packages/sdk/cloudflare/package.json | 2 +- .../cloudflare/src/createFeatureStore.test.ts | 28 ++++-- .../sdk/cloudflare/src/createFeatureStore.ts | 29 ++++-- packages/sdk/cloudflare/src/index.test.ts | 91 ++++++++++++++----- .../sdk/cloudflare/src/utils/testData.json | 79 +++++++++++++++- packages/shared/sdk-server/src/store/index.ts | 3 +- .../sdk-server/src/store/serialization.ts | 1 + 7 files changed, 188 insertions(+), 45 deletions(-) diff --git a/packages/sdk/cloudflare/package.json b/packages/sdk/cloudflare/package.json index d4735de2ce..8a5f8c92b3 100644 --- a/packages/sdk/cloudflare/package.json +++ b/packages/sdk/cloudflare/package.json @@ -33,7 +33,7 @@ "start": "rimraf dist && yarn tsw", "lint": "eslint . --ext .ts", "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore", - "test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest --ci --runInBand --coverage", + "test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" jest --ci --runInBand", "check": "yarn prettier && yarn lint && yarn build && yarn test && yarn doc" }, "dependencies": { diff --git a/packages/sdk/cloudflare/src/createFeatureStore.test.ts b/packages/sdk/cloudflare/src/createFeatureStore.test.ts index cda7e2f9c8..717a4424b6 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.test.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.test.ts @@ -7,6 +7,16 @@ import * as testData from './utils/testData.json'; describe('createFeatureStore', () => { const sdkKey = 'sdkKey'; const kvKey = `LD-Env-${sdkKey}`; + const { + testFlag1: { debugEventsUntilDate: d1, ...testFlag1Subset }, + testFlag2: { debugEventsUntilDate: d2, ...testFlag2Subset }, + testFlag3: { debugEventsUntilDate: d3, ...testFlag3Subset }, + } = testData.flags; + const testDataFlagsSubset = { + testFlag1: testFlag1Subset, + testFlag2: testFlag2Subset, + testFlag3: testFlag3Subset, + }; const mockLogger = { error: jest.fn(), warn: jest.fn(), @@ -18,7 +28,7 @@ describe('createFeatureStore', () => { let asyncFeatureStore: AsyncStoreFacade; beforeEach(() => { - mockGet.mockImplementation(() => Promise.resolve(testData)); + mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData))); featureStore = createFeatureStore(mockKV, sdkKey, mockLogger); asyncFeatureStore = new AsyncStoreFacade(featureStore); }); @@ -31,8 +41,8 @@ describe('createFeatureStore', () => { test('get flag', async () => { const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1'); - expect(mockGet).toHaveBeenCalledWith(kvKey, { type: 'json' }); - expect(flag).toEqual(testData.flags.testFlag1); + expect(mockGet).toHaveBeenCalledWith(kvKey); + expect(flag).toMatchObject(testFlag1Subset); }); test('invalid flag key', async () => { @@ -44,8 +54,8 @@ describe('createFeatureStore', () => { test('get segment', async () => { const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'testSegment1'); - expect(mockGet).toHaveBeenCalledWith(kvKey, { type: 'json' }); - expect(segment).toEqual(testData.segments.testSegment1); + expect(mockGet).toHaveBeenCalledWith(kvKey); + expect(segment).toMatchObject(testData.segments.testSegment1); }); test('invalid segment key', async () => { @@ -66,15 +76,15 @@ describe('createFeatureStore', () => { test('all flags', async () => { const flag = await asyncFeatureStore.all({ namespace: 'features' }); - expect(mockGet).toHaveBeenCalledWith(kvKey, { type: 'json' }); - expect(flag).toEqual(testData.flags); + expect(mockGet).toHaveBeenCalledWith(kvKey); + expect(flag).toMatchObject(testDataFlagsSubset); }); test('all segments', async () => { const segment = await asyncFeatureStore.all({ namespace: 'segments' }); - expect(mockGet).toHaveBeenCalledWith(kvKey, { type: 'json' }); - expect(segment).toEqual(testData.segments); + expect(mockGet).toHaveBeenCalledWith(kvKey); + expect(segment).toMatchObject(testData.segments); }); test('invalid DataKind', async () => { diff --git a/packages/sdk/cloudflare/src/createFeatureStore.ts b/packages/sdk/cloudflare/src/createFeatureStore.ts index 87b19a615a..c4f8540e88 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.ts @@ -7,6 +7,7 @@ import { LDFeatureStoreItem, LDFeatureStoreKindData, noop, + deserializePoll, } from '@launchdarkly/js-server-sdk-common-edge'; const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LDLogger) => { @@ -17,15 +18,20 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD flagKey: string, callback: (res: LDFeatureStoreItem | null) => void = noop ): void { - logger.debug(`Requesting ${flagKey} from ${key}`); + const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; + logger.debug(`Requesting ${flagKey} from ${key}:${kindKey}`); + kvNamespace - .get(key, { type: 'json' }) + .get(key) .then((i) => { if (i === null) { - logger.error('Feature data not found in KV.'); + throw new Error(`The ${kindKey} key: ${key} is not found in KV.`); + } + + const item = deserializePoll(i); + if (!item) { + throw new Error(`Error deserializing ${kindKey}`); } - const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; - const item = i as LDFeatureStoreItem; callback(item[kindKey][flagKey]); }) .catch((err) => { @@ -35,14 +41,19 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD }, all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): void { const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; - logger.debug(`Requesting all ${kindKey} data from KV.`); + logger.debug(`Requesting all from ${key}:${kindKey}`); kvNamespace - .get(key, { type: 'json' }) + .get(key) .then((i) => { if (i === null) { - logger.error('Feature data not found in KV.'); + throw new Error(`The ${kindKey} key: ${key} is not found in KV.`); } - const item = i as LDFeatureStoreItem; + + const item = deserializePoll(i); + if (!item) { + throw new Error(`Error deserializing ${kindKey}`); + } + callback(item[kindKey]); }) .catch((err) => { diff --git a/packages/sdk/cloudflare/src/index.test.ts b/packages/sdk/cloudflare/src/index.test.ts index 4db9f790a1..77f5b15796 100644 --- a/packages/sdk/cloudflare/src/index.test.ts +++ b/packages/sdk/cloudflare/src/index.test.ts @@ -1,5 +1,5 @@ import type { KVNamespace } from '@cloudflare/workers-types'; -import { LDClient } from '@launchdarkly/js-server-sdk-common-edge'; +import { LDClient, LDContext } from '@launchdarkly/js-server-sdk-common-edge'; import { Miniflare } from 'miniflare'; import { init } from './index'; import * as allFlagsSegments from './utils/testData.json'; @@ -11,8 +11,10 @@ const mf = new Miniflare({ }); 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' }; const namespace = 'LD_KV'; const rootEnvKey = `LD-Env-${sdkKey}`; @@ -31,28 +33,73 @@ describe('init', () => { 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: { variation: 0, version: 2 }, + testFlag2: { variation: 0, version: 2 }, + testFlag3: { 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(); }); }); }); diff --git a/packages/sdk/cloudflare/src/utils/testData.json b/packages/sdk/cloudflare/src/utils/testData.json index c3498bd291..495819a24d 100644 --- a/packages/sdk/cloudflare/src/utils/testData.json +++ b/packages/sdk/cloudflare/src/utils/testData.json @@ -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 }, @@ -25,10 +44,52 @@ }, "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": null, + "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 }, @@ -65,7 +126,19 @@ }, "site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" } }, - "rules": [], + "rules": [ + { + "id": "rule-country", + "clauses": [ + { + "attribute": "country", + "op": "in", + "values": ["australia"], + "negate": false + } + ] + } + ], "version": 1, "deleted": false, "_access": { "denied": [], "allowed": [] }, diff --git a/packages/shared/sdk-server/src/store/index.ts b/packages/shared/sdk-server/src/store/index.ts index ff3b0c3049..941c99aa72 100644 --- a/packages/shared/sdk-server/src/store/index.ts +++ b/packages/shared/sdk-server/src/store/index.ts @@ -1,4 +1,5 @@ import AsyncStoreFacade from './AsyncStoreFacade'; +import { deserializePoll } from './serialization'; // eslint-disable-next-line import/prefer-default-export -export { AsyncStoreFacade }; +export { AsyncStoreFacade, deserializePoll }; diff --git a/packages/shared/sdk-server/src/store/serialization.ts b/packages/shared/sdk-server/src/store/serialization.ts index 62cef866bb..a75f803a4e 100644 --- a/packages/shared/sdk-server/src/store/serialization.ts +++ b/packages/shared/sdk-server/src/store/serialization.ts @@ -22,6 +22,7 @@ export function reviver(this: any, key: string, value: any): any { } interface FlagsAndSegments { + [key: string]: { [name: string]: Flag } | { [name: string]: Segment }; flags: { [name: string]: Flag }; segments: { [name: string]: Segment }; } From b8f8fc6d3f215a061744b21eec33960404b11388 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 27 Apr 2023 17:31:17 -0700 Subject: [PATCH 02/10] chore: remove eslint comment --- packages/shared/sdk-server/src/store/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/shared/sdk-server/src/store/index.ts b/packages/shared/sdk-server/src/store/index.ts index 941c99aa72..c8c7c0e463 100644 --- a/packages/shared/sdk-server/src/store/index.ts +++ b/packages/shared/sdk-server/src/store/index.ts @@ -1,5 +1,4 @@ import AsyncStoreFacade from './AsyncStoreFacade'; import { deserializePoll } from './serialization'; -// eslint-disable-next-line import/prefer-default-export export { AsyncStoreFacade, deserializePoll }; From bfdcae3d519e31d1c34405a80a42a712486322c5 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Thu, 27 Apr 2023 17:55:40 -0700 Subject: [PATCH 03/10] chore:update example to target rules rather than a contrived scenario. --- packages/sdk/cloudflare/example/package.json | 2 +- packages/sdk/cloudflare/example/src/index.ts | 9 ++- .../sdk/cloudflare/example/src/testData.json | 79 ++++++++++++++++++- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/packages/sdk/cloudflare/example/package.json b/packages/sdk/cloudflare/example/package.json index 2af63e6b9b..0a611ef0c0 100644 --- a/packages/sdk/cloudflare/example/package.json +++ b/packages/sdk/cloudflare/example/package.json @@ -5,7 +5,7 @@ "module": "./dist/index.mjs", "packageManager": "yarn@3.4.1", "dependencies": { - "@launchdarkly/cloudflare-server-sdk": "0.0.1" + "@launchdarkly/cloudflare-server-sdk": "2.0.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20230321.0", diff --git a/packages/sdk/cloudflare/example/src/index.ts b/packages/sdk/cloudflare/example/src/index.ts index fc8804da43..9658d32c07 100644 --- a/packages/sdk/cloudflare/example/src/index.ts +++ b/packages/sdk/cloudflare/example/src/index.ts @@ -4,7 +4,7 @@ export default { async fetch(request: Request, env: Bindings): Promise { const sdkKey = 'test-sdk-key'; const flagKey = 'testFlag1'; - const context = { kind: 'user', key: 'test-user-key-1' }; + const context = { kind: 'user', key: 'test-user-key-1', email: 'test@gmail.com' }; // start using ld const client = initLD(sdkKey, env.LD_KV); @@ -13,9 +13,10 @@ export default { const flagDetail = await client.variationDetail(flagKey, context, false); const allFlags = await client.allFlagsState(context); - const resp = `${flagKey}: ${flagValue}, detail: ${JSON.stringify( - flagDetail - )}, allFlags: ${JSON.stringify(allFlags)}`; + const resp = ` + ${flagKey}: ${flagValue} + detail: ${JSON.stringify(flagDetail)} + allFlags: ${JSON.stringify(allFlags)}`; // eslint-disable-next-line console.log(`------------- ${resp}`); diff --git a/packages/sdk/cloudflare/example/src/testData.json b/packages/sdk/cloudflare/example/src/testData.json index c3498bd291..495819a24d 100644 --- a/packages/sdk/cloudflare/example/src/testData.json +++ b/packages/sdk/cloudflare/example/src/testData.json @@ -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 }, @@ -25,10 +44,52 @@ }, "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": null, + "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 }, @@ -65,7 +126,19 @@ }, "site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" } }, - "rules": [], + "rules": [ + { + "id": "rule-country", + "clauses": [ + { + "attribute": "country", + "op": "in", + "values": ["australia"], + "negate": false + } + ] + } + ], "version": 1, "deleted": false, "_access": { "denied": [], "allowed": [] }, From f3ee6c771eb769681937121e3c065edc8fa081da Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 28 Apr 2023 00:58:18 -0700 Subject: [PATCH 04/10] chore: improve logging and nullish check. --- packages/sdk/cloudflare/src/createFeatureStore.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/sdk/cloudflare/src/createFeatureStore.ts b/packages/sdk/cloudflare/src/createFeatureStore.ts index c4f8540e88..f7b8a7baf9 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.ts @@ -19,13 +19,13 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD callback: (res: LDFeatureStoreItem | null) => void = noop ): void { const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; - logger.debug(`Requesting ${flagKey} from ${key}:${kindKey}`); + logger.debug(`Requesting ${flagKey} from ${key}.${kindKey}`); kvNamespace .get(key) .then((i) => { - if (i === null) { - throw new Error(`The ${kindKey} key: ${key} is not found in KV.`); + if (!i) { + throw new Error(`${key}.${kindKey} is not found in KV.`); } const item = deserializePoll(i); @@ -41,12 +41,12 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD }, all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): void { const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; - logger.debug(`Requesting all from ${key}:${kindKey}`); + logger.debug(`Requesting all from ${key}.${kindKey}`); kvNamespace .get(key) .then((i) => { - if (i === null) { - throw new Error(`The ${kindKey} key: ${key} is not found in KV.`); + if (!i) { + throw new Error(`${key}.${kindKey} is not found in KV.`); } const item = deserializePoll(i); From b87000ad54394102f74b424ff06145d43baf5883 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 28 Apr 2023 10:33:53 -0700 Subject: [PATCH 05/10] chore: add yarn coverage --- packages/sdk/cloudflare/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sdk/cloudflare/package.json b/packages/sdk/cloudflare/package.json index 8a5f8c92b3..a799fc63eb 100644 --- a/packages/sdk/cloudflare/package.json +++ b/packages/sdk/cloudflare/package.json @@ -34,6 +34,7 @@ "lint": "eslint . --ext .ts", "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": { From 5a35dd93a062027e407e201c3ba69647c11c0757 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 28 Apr 2023 11:05:41 -0700 Subject: [PATCH 06/10] chore: debugging null debugEventsUntilDate --- packages/sdk/cloudflare/src/createFeatureStore.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sdk/cloudflare/src/createFeatureStore.test.ts b/packages/sdk/cloudflare/src/createFeatureStore.test.ts index 717a4424b6..b7be03fdc6 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.test.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.test.ts @@ -8,9 +8,9 @@ describe('createFeatureStore', () => { const sdkKey = 'sdkKey'; const kvKey = `LD-Env-${sdkKey}`; const { - testFlag1: { debugEventsUntilDate: d1, ...testFlag1Subset }, - testFlag2: { debugEventsUntilDate: d2, ...testFlag2Subset }, - testFlag3: { debugEventsUntilDate: d3, ...testFlag3Subset }, + testFlag1: { ...testFlag1Subset }, + testFlag2: { ...testFlag2Subset }, + testFlag3: { ...testFlag3Subset }, } = testData.flags; const testDataFlagsSubset = { testFlag1: testFlag1Subset, @@ -38,11 +38,11 @@ describe('createFeatureStore', () => { }); describe('get', () => { - test('get flag', async () => { + test.only('get flag', async () => { const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1'); expect(mockGet).toHaveBeenCalledWith(kvKey); - expect(flag).toMatchObject(testFlag1Subset); + expect(flag).toEqual(testFlag1Subset); }); test('invalid flag key', async () => { From bbe678bb841bef1d60dbca47383e6559e1e9397e Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 28 Apr 2023 11:46:28 -0700 Subject: [PATCH 07/10] chore: set debugEventsUntilDate for unit tests. --- .../sdk/cloudflare/src/createFeatureStore.test.ts | 14 ++------------ packages/sdk/cloudflare/src/index.test.ts | 6 +++--- packages/sdk/cloudflare/src/utils/testData.json | 6 +++--- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/sdk/cloudflare/src/createFeatureStore.test.ts b/packages/sdk/cloudflare/src/createFeatureStore.test.ts index b7be03fdc6..16a6ad604d 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.test.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.test.ts @@ -7,16 +7,6 @@ import * as testData from './utils/testData.json'; describe('createFeatureStore', () => { const sdkKey = 'sdkKey'; const kvKey = `LD-Env-${sdkKey}`; - const { - testFlag1: { ...testFlag1Subset }, - testFlag2: { ...testFlag2Subset }, - testFlag3: { ...testFlag3Subset }, - } = testData.flags; - const testDataFlagsSubset = { - testFlag1: testFlag1Subset, - testFlag2: testFlag2Subset, - testFlag3: testFlag3Subset, - }; const mockLogger = { error: jest.fn(), warn: jest.fn(), @@ -42,7 +32,7 @@ describe('createFeatureStore', () => { const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1'); expect(mockGet).toHaveBeenCalledWith(kvKey); - expect(flag).toEqual(testFlag1Subset); + expect(flag).toMatchObject(testData.flags.testFlag1); }); test('invalid flag key', async () => { @@ -77,7 +67,7 @@ describe('createFeatureStore', () => { const flag = await asyncFeatureStore.all({ namespace: 'features' }); expect(mockGet).toHaveBeenCalledWith(kvKey); - expect(flag).toMatchObject(testDataFlagsSubset); + expect(flag).toMatchObject(testData); }); test('all segments', async () => { diff --git a/packages/sdk/cloudflare/src/index.test.ts b/packages/sdk/cloudflare/src/index.test.ts index 77f5b15796..7a7847ffe5 100644 --- a/packages/sdk/cloudflare/src/index.test.ts +++ b/packages/sdk/cloudflare/src/index.test.ts @@ -76,9 +76,9 @@ describe('init', () => { expect(allFlags).toBeDefined(); expect(allFlags.toJSON()).toEqual({ $flagsState: { - testFlag1: { variation: 0, version: 2 }, - testFlag2: { variation: 0, version: 2 }, - testFlag3: { variation: 0, version: 2 }, + 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, diff --git a/packages/sdk/cloudflare/src/utils/testData.json b/packages/sdk/cloudflare/src/utils/testData.json index 495819a24d..b9e5296c03 100644 --- a/packages/sdk/cloudflare/src/utils/testData.json +++ b/packages/sdk/cloudflare/src/utils/testData.json @@ -38,7 +38,7 @@ "salt": "aef830243d6640d0a973be89988e008d", "trackEvents": false, "trackEventsFallthrough": false, - "debugEventsUntilDate": null, + "debugEventsUntilDate": 2000, "version": 2, "deleted": false }, @@ -67,7 +67,7 @@ "salt": "aef830243d6640d0a973be89988e008d", "trackEvents": false, "trackEventsFallthrough": false, - "debugEventsUntilDate": null, + "debugEventsUntilDate": 2000, "version": 2, "deleted": false }, @@ -103,7 +103,7 @@ "salt": "aef830243d6640d0a973be89988e008d", "trackEvents": false, "trackEventsFallthrough": false, - "debugEventsUntilDate": null, + "debugEventsUntilDate": 2000, "version": 2, "deleted": false } From 469330dbed7167ba36265d2d71ebc66742f68aa0 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 28 Apr 2023 12:21:59 -0700 Subject: [PATCH 08/10] fix: Suggestions for serialization. (#111) fix: Suggestions. --- .../sdk/cloudflare/src/createFeatureStore.ts | 28 ++++++++++++++++--- .../sdk-server/src/store/serialization.ts | 9 +++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/sdk/cloudflare/src/createFeatureStore.ts b/packages/sdk/cloudflare/src/createFeatureStore.ts index f7b8a7baf9..fb3318abb0 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.ts @@ -15,11 +15,11 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD const store: LDFeatureStore = { get( kind: DataKind, - flagKey: string, + dataKey: string, callback: (res: LDFeatureStoreItem | null) => void = noop ): void { const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; - logger.debug(`Requesting ${flagKey} from ${key}.${kindKey}`); + logger.debug(`Requesting ${dataKey} from ${key}.${kindKey}`); kvNamespace .get(key) @@ -32,7 +32,17 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD if (!item) { throw new Error(`Error deserializing ${kindKey}`); } - callback(item[kindKey][flagKey]); + switch (kind.namespace) { + case 'features': + callback(item.flags[dataKey]); + break; + case 'segments': + callback(item.segments[dataKey]); + break; + default: + // Unsupported data kind. + callback(null); + } }) .catch((err) => { logger.error(err); @@ -54,7 +64,17 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD throw new Error(`Error deserializing ${kindKey}`); } - callback(item[kindKey]); + switch (kind.namespace) { + case 'features': + callback(item.flags); + break; + case 'segments': + callback(item.segments); + break; + default: + // Unsupported data kind. + callback({}); + } }) .catch((err) => { logger.error(err); diff --git a/packages/shared/sdk-server/src/store/serialization.ts b/packages/shared/sdk-server/src/store/serialization.ts index a75f803a4e..945f3b00c3 100644 --- a/packages/shared/sdk-server/src/store/serialization.ts +++ b/packages/shared/sdk-server/src/store/serialization.ts @@ -22,7 +22,6 @@ export function reviver(this: any, key: string, value: any): any { } interface FlagsAndSegments { - [key: string]: { [name: string]: Flag } | { [name: string]: Segment }; flags: { [name: string]: Flag }; segments: { [name: string]: Segment }; } @@ -158,6 +157,14 @@ export function deserializeAll(data: string): AllData | undefined { return parsed; } +/** + * This function is intended for usage inside LaunchDarkly SDKs. + * This function should NOT be used by customer applications. + * This function may be changed or removed without a major version. + * + * @param data String data from launchdarkly. + * @returns The parsed and processed data. + */ export function deserializePoll(data: string): FlagsAndSegments | undefined { const parsed = tryParse(data) as FlagsAndSegments; From 1ce6592cc90324f2a60fcd7eac49c8db8a79447f Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 28 Apr 2023 12:26:49 -0700 Subject: [PATCH 09/10] chore: removed .only --- packages/sdk/cloudflare/src/createFeatureStore.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/cloudflare/src/createFeatureStore.test.ts b/packages/sdk/cloudflare/src/createFeatureStore.test.ts index 16a6ad604d..8fd019a27a 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.test.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.test.ts @@ -28,7 +28,7 @@ describe('createFeatureStore', () => { }); describe('get', () => { - test.only('get flag', async () => { + test('get flag', async () => { const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1'); expect(mockGet).toHaveBeenCalledWith(kvKey); From d507dfcfcff897fce101fb85837196cdaa11a630 Mon Sep 17 00:00:00 2001 From: Yusinto Ngadiman Date: Fri, 28 Apr 2023 12:38:20 -0700 Subject: [PATCH 10/10] fix: broken unit tests. minor syntax improvements. --- .../cloudflare/src/createFeatureStore.test.ts | 6 +++--- .../sdk/cloudflare/src/createFeatureStore.ts | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/sdk/cloudflare/src/createFeatureStore.test.ts b/packages/sdk/cloudflare/src/createFeatureStore.test.ts index 8fd019a27a..6f1b7738a3 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.test.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.test.ts @@ -64,10 +64,10 @@ describe('createFeatureStore', () => { describe('all', () => { test('all flags', async () => { - const flag = await asyncFeatureStore.all({ namespace: 'features' }); + const flags = await asyncFeatureStore.all({ namespace: 'features' }); expect(mockGet).toHaveBeenCalledWith(kvKey); - expect(flag).toMatchObject(testData); + expect(flags).toMatchObject(testData.flags); }); test('all segments', async () => { @@ -80,7 +80,7 @@ describe('createFeatureStore', () => { test('invalid DataKind', async () => { const flag = await asyncFeatureStore.all({ namespace: 'InvalidDataKind' }); - expect(flag).toBeUndefined(); + expect(flag).toEqual({}); }); test('invalid kv key', async () => { diff --git a/packages/sdk/cloudflare/src/createFeatureStore.ts b/packages/sdk/cloudflare/src/createFeatureStore.ts index fb3318abb0..2b0b82e2d4 100644 --- a/packages/sdk/cloudflare/src/createFeatureStore.ts +++ b/packages/sdk/cloudflare/src/createFeatureStore.ts @@ -18,7 +18,8 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD dataKey: string, callback: (res: LDFeatureStoreItem | null) => void = noop ): void { - const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; + const { namespace } = kind; + const kindKey = namespace === 'features' ? 'flags' : namespace; logger.debug(`Requesting ${dataKey} from ${key}.${kindKey}`); kvNamespace @@ -32,7 +33,8 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD if (!item) { throw new Error(`Error deserializing ${kindKey}`); } - switch (kind.namespace) { + + switch (namespace) { case 'features': callback(item.flags[dataKey]); break; @@ -40,8 +42,7 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD callback(item.segments[dataKey]); break; default: - // Unsupported data kind. - callback(null); + throw new Error(`Unsupported DataKind: ${namespace}`); } }) .catch((err) => { @@ -50,7 +51,8 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD }); }, all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): void { - const kindKey = kind.namespace === 'features' ? 'flags' : kind.namespace; + const { namespace } = kind; + const kindKey = namespace === 'features' ? 'flags' : namespace; logger.debug(`Requesting all from ${key}.${kindKey}`); kvNamespace .get(key) @@ -64,7 +66,7 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD throw new Error(`Error deserializing ${kindKey}`); } - switch (kind.namespace) { + switch (namespace) { case 'features': callback(item.flags); break; @@ -72,8 +74,7 @@ const createFeatureStore = (kvNamespace: KVNamespace, sdkKey: string, logger: LD callback(item.segments); break; default: - // Unsupported data kind. - callback({}); + throw new Error(`Unsupported DataKind: ${namespace}`); } }) .catch((err) => {