@servicetitan/launchdarkly-service 36.3.1 → 36.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/feature-flag-store.d.ts.map +1 -1
- package/dist/feature-flag-store.js +8 -2
- package/dist/feature-flag-store.js.map +1 -1
- package/dist/tests/feature-flag-store.test.js +23 -9
- package/dist/tests/feature-flag-store.test.js.map +1 -1
- package/dist/tests/flag-used-inspectors.test.d.ts +2 -0
- package/dist/tests/flag-used-inspectors.test.d.ts.map +1 -0
- package/dist/tests/flag-used-inspectors.test.js +91 -0
- package/dist/tests/flag-used-inspectors.test.js.map +1 -0
- package/package.json +9 -9
- package/src/feature-flag-store.ts +8 -1
- package/src/tests/feature-flag-store.test.ts +23 -8
- package/src/tests/flag-used-inspectors.test.tsx +83 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature-flag-store.d.ts","sourceRoot":"","sources":["../src/feature-flag-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAG1D,qBACa,gBAAgB,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS;
|
|
1
|
+
{"version":3,"file":"feature-flag-store.d.ts","sourceRoot":"","sources":["../src/feature-flag-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAG1D,qBACa,gBAAgB,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS;IASzC,KAAK,EAAE,CAAC,CAAmB;;IAO3C,QAAQ,GAAI,OAAO,CAAC,UAElB;CACL"}
|
|
@@ -24,7 +24,13 @@ import { injectable } from '@servicetitan/react-ioc';
|
|
|
24
24
|
import { action, makeObservable, observable } from 'mobx';
|
|
25
25
|
export class FeatureFlagStore {
|
|
26
26
|
constructor(){
|
|
27
|
-
|
|
27
|
+
/*
|
|
28
|
+
* Must stay `.ref` rather than a deep observable. Deep MobX conversion
|
|
29
|
+
* clones the launchdarkly-react-client-sdk useFlags() proxy via descriptor
|
|
30
|
+
* enumeration, which severs it: reads then bypass client.variation() and
|
|
31
|
+
* `flag-used` inspectors never fire. Holding the proxy by reference keeps
|
|
32
|
+
* reads flowing through it.
|
|
33
|
+
*/ // @ts-expect-error because 'LDFlagSet' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint
|
|
28
34
|
_define_property(this, "flags", {});
|
|
29
35
|
_define_property(this, "setFlags", (flags)=>{
|
|
30
36
|
this.flags = flags;
|
|
@@ -33,7 +39,7 @@ export class FeatureFlagStore {
|
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
_ts_decorate([
|
|
36
|
-
observable,
|
|
42
|
+
observable.ref,
|
|
37
43
|
_ts_metadata("design:type", typeof T === "undefined" ? Object : T)
|
|
38
44
|
], FeatureFlagStore.prototype, "flags", void 0);
|
|
39
45
|
_ts_decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/feature-flag-store.ts"],"sourcesContent":["import { injectable } from '@servicetitan/react-ioc';\nimport { LDFlagSet } from 'launchdarkly-react-client-sdk';\nimport { action, makeObservable, observable } from 'mobx';\n\n@injectable()\nexport class FeatureFlagStore<T extends LDFlagSet = LDFlagSet> {\n // @ts-expect-error because 'LDFlagSet' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint\n @observable flags: T = {} as LDFlagSet;\n\n constructor() {\n makeObservable(this);\n }\n\n @action\n setFlags = (flags: T) => {\n this.flags = flags;\n };\n}\n"],"names":["injectable","action","makeObservable","observable","FeatureFlagStore","flags","setFlags"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,SAASA,UAAU,QAAQ,0BAA0B;AAErD,SAASC,MAAM,EAAEC,cAAc,EAAEC,UAAU,QAAQ,OAAO;AAG1D,OAAO,MAAMC;
|
|
1
|
+
{"version":3,"sources":["../src/feature-flag-store.ts"],"sourcesContent":["import { injectable } from '@servicetitan/react-ioc';\nimport { LDFlagSet } from 'launchdarkly-react-client-sdk';\nimport { action, makeObservable, observable } from 'mobx';\n\n@injectable()\nexport class FeatureFlagStore<T extends LDFlagSet = LDFlagSet> {\n /*\n * Must stay `.ref` rather than a deep observable. Deep MobX conversion\n * clones the launchdarkly-react-client-sdk useFlags() proxy via descriptor\n * enumeration, which severs it: reads then bypass client.variation() and\n * `flag-used` inspectors never fire. Holding the proxy by reference keeps\n * reads flowing through it.\n */\n // @ts-expect-error because 'LDFlagSet' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint\n @observable.ref flags: T = {} as LDFlagSet;\n\n constructor() {\n makeObservable(this);\n }\n\n @action\n setFlags = (flags: T) => {\n this.flags = flags;\n };\n}\n"],"names":["injectable","action","makeObservable","observable","FeatureFlagStore","flags","setFlags","ref"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,SAASA,UAAU,QAAQ,0BAA0B;AAErD,SAASC,MAAM,EAAEC,cAAc,EAAEC,UAAU,QAAQ,OAAO;AAG1D,OAAO,MAAMC;IAWT,aAAc;QAVd;;;;;;KAMC,GACD,yJAAyJ;QACzJ,uBAAgBC,SAAW,CAAC;QAM5B,uBACAC,YAAW,CAACD;YACR,IAAI,CAACA,KAAK,GAAGA;QACjB;QANIH,eAAe,IAAI;IACvB;AAMJ;;eAVgBK"}
|
|
@@ -13,18 +13,32 @@ describe(`[launchdarkly-service] ${FeatureFlagStore.name}`, ()=>{
|
|
|
13
13
|
foo: 'bar'
|
|
14
14
|
};
|
|
15
15
|
});
|
|
16
|
-
const subject = (
|
|
16
|
+
const subject = ()=>store.setFlags(flags);
|
|
17
17
|
test('sets flags', ()=>{
|
|
18
|
-
subject(
|
|
18
|
+
subject();
|
|
19
19
|
expect(store.flags).toEqual(flags);
|
|
20
20
|
});
|
|
21
|
-
test('
|
|
22
|
-
subject(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
test('retains the source reference rather than cloning', ()=>{
|
|
22
|
+
subject();
|
|
23
|
+
expect(store.flags).toBe(flags);
|
|
24
|
+
});
|
|
25
|
+
describe('when flags are backed by a get-trapping proxy', ()=>{
|
|
26
|
+
let get;
|
|
27
|
+
beforeEach(()=>{
|
|
28
|
+
get = jest.fn((target, key, receiver)=>Reflect.get(target, key, receiver));
|
|
29
|
+
flags = new Proxy({
|
|
30
|
+
foo: 'bar'
|
|
31
|
+
}, {
|
|
32
|
+
get
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
test('routes single-key reads through the proxy', ()=>{
|
|
36
|
+
subject();
|
|
37
|
+
get.mockClear();
|
|
38
|
+
const value = store.flags.foo;
|
|
39
|
+
expect(value).toBe('bar');
|
|
40
|
+
expect(get).toHaveBeenCalledWith(expect.anything(), 'foo', expect.anything());
|
|
41
|
+
});
|
|
28
42
|
});
|
|
29
43
|
});
|
|
30
44
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tests/feature-flag-store.test.ts"],"sourcesContent":["import { LDFlagSet } from 'launchdarkly-js-client-sdk';\nimport { FeatureFlagStore } from '../feature-flag-store';\n\ndescribe(`[launchdarkly-service] ${FeatureFlagStore.name}`, () => {\n const subject = () => new FeatureFlagStore();\n\n test('has no flags initially', () => {\n expect(subject().flags).toEqual({});\n });\n\n describe('setFlags', () => {\n let store: FeatureFlagStore;\n let flags: LDFlagSet;\n\n beforeEach(() => {\n store = new FeatureFlagStore();\n flags = { foo: 'bar' };\n });\n\n const subject = (
|
|
1
|
+
{"version":3,"sources":["../../src/tests/feature-flag-store.test.ts"],"sourcesContent":["import { LDFlagSet } from 'launchdarkly-js-client-sdk';\nimport { FeatureFlagStore } from '../feature-flag-store';\n\ndescribe(`[launchdarkly-service] ${FeatureFlagStore.name}`, () => {\n const subject = () => new FeatureFlagStore();\n\n test('has no flags initially', () => {\n expect(subject().flags).toEqual({});\n });\n\n describe('setFlags', () => {\n let store: FeatureFlagStore;\n let flags: LDFlagSet;\n\n beforeEach(() => {\n store = new FeatureFlagStore();\n flags = { foo: 'bar' };\n });\n\n const subject = () => store.setFlags(flags);\n\n test('sets flags', () => {\n subject();\n\n expect(store.flags).toEqual(flags);\n });\n\n test('retains the source reference rather than cloning', () => {\n subject();\n\n expect(store.flags).toBe(flags);\n });\n\n describe('when flags are backed by a get-trapping proxy', () => {\n let get: jest.Mock;\n\n beforeEach(() => {\n get = jest.fn((target, key, receiver) => Reflect.get(target, key, receiver));\n flags = new Proxy({ foo: 'bar' }, { get });\n });\n\n test('routes single-key reads through the proxy', () => {\n subject();\n get.mockClear();\n\n const value = store.flags.foo;\n\n expect(value).toBe('bar');\n expect(get).toHaveBeenCalledWith(expect.anything(), 'foo', expect.anything());\n });\n });\n });\n});\n"],"names":["FeatureFlagStore","describe","name","subject","test","expect","flags","toEqual","store","beforeEach","foo","setFlags","toBe","get","jest","fn","target","key","receiver","Reflect","Proxy","mockClear","value","toHaveBeenCalledWith","anything"],"mappings":"AACA,SAASA,gBAAgB,QAAQ,wBAAwB;AAEzDC,SAAS,CAAC,uBAAuB,EAAED,iBAAiBE,IAAI,EAAE,EAAE;IACxD,MAAMC,UAAU,IAAM,IAAIH;IAE1BI,KAAK,0BAA0B;QAC3BC,OAAOF,UAAUG,KAAK,EAAEC,OAAO,CAAC,CAAC;IACrC;IAEAN,SAAS,YAAY;QACjB,IAAIO;QACJ,IAAIF;QAEJG,WAAW;YACPD,QAAQ,IAAIR;YACZM,QAAQ;gBAAEI,KAAK;YAAM;QACzB;QAEA,MAAMP,UAAU,IAAMK,MAAMG,QAAQ,CAACL;QAErCF,KAAK,cAAc;YACfD;YAEAE,OAAOG,MAAMF,KAAK,EAAEC,OAAO,CAACD;QAChC;QAEAF,KAAK,oDAAoD;YACrDD;YAEAE,OAAOG,MAAMF,KAAK,EAAEM,IAAI,CAACN;QAC7B;QAEAL,SAAS,iDAAiD;YACtD,IAAIY;YAEJJ,WAAW;gBACPI,MAAMC,KAAKC,EAAE,CAAC,CAACC,QAAQC,KAAKC,WAAaC,QAAQN,GAAG,CAACG,QAAQC,KAAKC;gBAClEZ,QAAQ,IAAIc,MAAM;oBAAEV,KAAK;gBAAM,GAAG;oBAAEG;gBAAI;YAC5C;YAEAT,KAAK,6CAA6C;gBAC9CD;gBACAU,IAAIQ,SAAS;gBAEb,MAAMC,QAAQd,MAAMF,KAAK,CAACI,GAAG;gBAE7BL,OAAOiB,OAAOV,IAAI,CAAC;gBACnBP,OAAOQ,KAAKU,oBAAoB,CAAClB,OAAOmB,QAAQ,IAAI,OAAOnB,OAAOmB,QAAQ;YAC9E;QACJ;IACJ;AACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flag-used-inspectors.test.d.ts","sourceRoot":"","sources":["../../src/tests/flag-used-inspectors.test.tsx"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Provider, useDependencies } from '@servicetitan/react-ioc';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
import { FeatureFlagStore } from '../feature-flag-store';
|
|
7
|
+
import { LDProvider } from '../ld-provider';
|
|
8
|
+
import { LD_SERVICE_TOKEN } from '../types';
|
|
9
|
+
const FLAG_KEY = 'my-flag';
|
|
10
|
+
const CAMEL_KEY = 'myFlag';
|
|
11
|
+
function createFakeClient(flagValues, inspectors) {
|
|
12
|
+
return {
|
|
13
|
+
allFlags: ()=>({
|
|
14
|
+
...flagValues
|
|
15
|
+
}),
|
|
16
|
+
variation: (key, defaultValue)=>{
|
|
17
|
+
const value = key in flagValues ? flagValues[key] : defaultValue;
|
|
18
|
+
inspectors.filter((i)=>i.type === 'flag-used').forEach((i)=>i.method(key, {
|
|
19
|
+
value
|
|
20
|
+
}, {
|
|
21
|
+
kind: 'user',
|
|
22
|
+
key: 'test'
|
|
23
|
+
}));
|
|
24
|
+
return value;
|
|
25
|
+
},
|
|
26
|
+
waitForInitialization: ()=>Promise.resolve(),
|
|
27
|
+
on: ()=>{},
|
|
28
|
+
off: ()=>{}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const Reader = observer(()=>{
|
|
32
|
+
const [{ flags }] = useDependencies(FeatureFlagStore);
|
|
33
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
34
|
+
"data-testid": "flag-value",
|
|
35
|
+
children: String(flags[CAMEL_KEY])
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('[launchdarkly-service] flag-used inspector integration', ()=>{
|
|
39
|
+
const flagValues = {
|
|
40
|
+
[FLAG_KEY]: true
|
|
41
|
+
};
|
|
42
|
+
let inspectorMethod;
|
|
43
|
+
let ldService;
|
|
44
|
+
beforeEach(()=>{
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
inspectorMethod = jest.fn();
|
|
47
|
+
const inspector = {
|
|
48
|
+
type: 'flag-used',
|
|
49
|
+
name: 'test-inspector',
|
|
50
|
+
method: inspectorMethod
|
|
51
|
+
};
|
|
52
|
+
ldService = {
|
|
53
|
+
getClient: ({ options })=>{
|
|
54
|
+
var _ref;
|
|
55
|
+
return createFakeClient(flagValues, (_ref = options === null || options === void 0 ? void 0 : options.inspectors) !== null && _ref !== void 0 ? _ref : []);
|
|
56
|
+
},
|
|
57
|
+
options: {
|
|
58
|
+
inspectors: [
|
|
59
|
+
inspector
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
const subject = ()=>render(/*#__PURE__*/ _jsx(Provider, {
|
|
65
|
+
singletons: [
|
|
66
|
+
{
|
|
67
|
+
provide: LD_SERVICE_TOKEN,
|
|
68
|
+
useValue: ldService
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
children: /*#__PURE__*/ _jsx(LDProvider, {
|
|
72
|
+
clientSideID: "test",
|
|
73
|
+
projectName: "test",
|
|
74
|
+
children: /*#__PURE__*/ _jsx(Reader, {})
|
|
75
|
+
})
|
|
76
|
+
}));
|
|
77
|
+
describe('when a flag is read through FeatureFlagStore', ()=>{
|
|
78
|
+
beforeEach(async ()=>{
|
|
79
|
+
subject();
|
|
80
|
+
// Wait for <Reader> to mount and read flag
|
|
81
|
+
await screen.findByTestId('flag-value');
|
|
82
|
+
});
|
|
83
|
+
test('fires the flag-used inspector', ()=>{
|
|
84
|
+
expect(inspectorMethod).toHaveBeenCalledWith(FLAG_KEY, expect.objectContaining({
|
|
85
|
+
value: true
|
|
86
|
+
}), expect.anything());
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=flag-used-inspectors.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tests/flag-used-inspectors.test.tsx"],"sourcesContent":["import { Provider, useDependencies } from '@servicetitan/react-ioc';\nimport '@testing-library/jest-dom';\nimport { render, screen } from '@testing-library/react';\nimport { LDClient, LDInspection } from 'launchdarkly-js-client-sdk';\nimport { observer } from 'mobx-react';\nimport { FC } from 'react';\nimport { FeatureFlagStore } from '../feature-flag-store';\nimport { LDProvider } from '../ld-provider';\nimport { LD_SERVICE_TOKEN, LDService } from '../types';\n\nconst FLAG_KEY = 'my-flag';\nconst CAMEL_KEY = 'myFlag';\n\nfunction createFakeClient(\n flagValues: Record<string, unknown>,\n inspectors: LDInspection[]\n): LDClient {\n return {\n allFlags: () => ({ ...flagValues }),\n variation: (key: string, defaultValue: unknown) => {\n const value = key in flagValues ? flagValues[key] : defaultValue;\n inspectors\n .filter(i => i.type === 'flag-used')\n .forEach(i => i.method!(key, { value }, { kind: 'user', key: 'test' }));\n return value;\n },\n waitForInitialization: () => Promise.resolve(),\n on: () => {},\n off: () => {},\n } as Partial<LDClient> as LDClient;\n}\n\nconst Reader: FC = observer(() => {\n const [{ flags }] = useDependencies(FeatureFlagStore);\n return <div data-testid=\"flag-value\">{String(flags[CAMEL_KEY])}</div>;\n});\n\ndescribe('[launchdarkly-service] flag-used inspector integration', () => {\n const flagValues: Record<string, unknown> = { [FLAG_KEY]: true };\n let inspectorMethod: jest.Mock;\n let ldService: LDService;\n\n beforeEach(() => {\n jest.clearAllMocks();\n\n inspectorMethod = jest.fn();\n const inspector: LDInspection = {\n type: 'flag-used',\n name: 'test-inspector',\n method: inspectorMethod,\n };\n\n ldService = {\n getClient: ({ options }) => createFakeClient(flagValues, options?.inspectors ?? []),\n options: { inspectors: [inspector] },\n };\n });\n\n const subject = () =>\n render(\n <Provider singletons={[{ provide: LD_SERVICE_TOKEN, useValue: ldService }]}>\n <LDProvider clientSideID=\"test\" projectName=\"test\">\n <Reader />\n </LDProvider>\n </Provider>\n );\n\n describe('when a flag is read through FeatureFlagStore', () => {\n beforeEach(async () => {\n subject();\n // Wait for <Reader> to mount and read flag\n await screen.findByTestId('flag-value');\n });\n\n test('fires the flag-used inspector', () => {\n expect(inspectorMethod).toHaveBeenCalledWith(\n FLAG_KEY,\n expect.objectContaining({ value: true }),\n expect.anything()\n );\n });\n });\n});\n"],"names":["Provider","useDependencies","render","screen","observer","FeatureFlagStore","LDProvider","LD_SERVICE_TOKEN","FLAG_KEY","CAMEL_KEY","createFakeClient","flagValues","inspectors","allFlags","variation","key","defaultValue","value","filter","i","type","forEach","method","kind","waitForInitialization","Promise","resolve","on","off","Reader","flags","div","data-testid","String","describe","inspectorMethod","ldService","beforeEach","jest","clearAllMocks","fn","inspector","name","getClient","options","subject","singletons","provide","useValue","clientSideID","projectName","findByTestId","test","expect","toHaveBeenCalledWith","objectContaining","anything"],"mappings":";AAAA,SAASA,QAAQ,EAAEC,eAAe,QAAQ,0BAA0B;AACpE,OAAO,4BAA4B;AACnC,SAASC,MAAM,EAAEC,MAAM,QAAQ,yBAAyB;AAExD,SAASC,QAAQ,QAAQ,aAAa;AAEtC,SAASC,gBAAgB,QAAQ,wBAAwB;AACzD,SAASC,UAAU,QAAQ,iBAAiB;AAC5C,SAASC,gBAAgB,QAAmB,WAAW;AAEvD,MAAMC,WAAW;AACjB,MAAMC,YAAY;AAElB,SAASC,iBACLC,UAAmC,EACnCC,UAA0B;IAE1B,OAAO;QACHC,UAAU,IAAO,CAAA;gBAAE,GAAGF,UAAU;YAAC,CAAA;QACjCG,WAAW,CAACC,KAAaC;YACrB,MAAMC,QAAQF,OAAOJ,aAAaA,UAAU,CAACI,IAAI,GAAGC;YACpDJ,WACKM,MAAM,CAACC,CAAAA,IAAKA,EAAEC,IAAI,KAAK,aACvBC,OAAO,CAACF,CAAAA,IAAKA,EAAEG,MAAM,CAAEP,KAAK;oBAAEE;gBAAM,GAAG;oBAAEM,MAAM;oBAAQR,KAAK;gBAAO;YACxE,OAAOE;QACX;QACAO,uBAAuB,IAAMC,QAAQC,OAAO;QAC5CC,IAAI,KAAO;QACXC,KAAK,KAAO;IAChB;AACJ;AAEA,MAAMC,SAAazB,SAAS;IACxB,MAAM,CAAC,EAAE0B,KAAK,EAAE,CAAC,GAAG7B,gBAAgBI;IACpC,qBAAO,KAAC0B;QAAIC,eAAY;kBAAcC,OAAOH,KAAK,CAACrB,UAAU;;AACjE;AAEAyB,SAAS,0DAA0D;IAC/D,MAAMvB,aAAsC;QAAE,CAACH,SAAS,EAAE;IAAK;IAC/D,IAAI2B;IACJ,IAAIC;IAEJC,WAAW;QACPC,KAAKC,aAAa;QAElBJ,kBAAkBG,KAAKE,EAAE;QACzB,MAAMC,YAA0B;YAC5BrB,MAAM;YACNsB,MAAM;YACNpB,QAAQa;QACZ;QAEAC,YAAY;YACRO,WAAW,CAAC,EAAEC,OAAO,EAAE;;uBAAKlC,iBAAiBC,oBAAYiC,oBAAAA,8BAAAA,QAAShC,UAAU,uCAAI,EAAE;;YAClFgC,SAAS;gBAAEhC,YAAY;oBAAC6B;iBAAU;YAAC;QACvC;IACJ;IAEA,MAAMI,UAAU,IACZ3C,qBACI,KAACF;YAAS8C,YAAY;gBAAC;oBAAEC,SAASxC;oBAAkByC,UAAUZ;gBAAU;aAAE;sBACtE,cAAA,KAAC9B;gBAAW2C,cAAa;gBAAOC,aAAY;0BACxC,cAAA,KAACrB;;;IAKjBK,SAAS,gDAAgD;QACrDG,WAAW;YACPQ;YACA,2CAA2C;YAC3C,MAAM1C,OAAOgD,YAAY,CAAC;QAC9B;QAEAC,KAAK,iCAAiC;YAClCC,OAAOlB,iBAAiBmB,oBAAoB,CACxC9C,UACA6C,OAAOE,gBAAgB,CAAC;gBAAEtC,OAAO;YAAK,IACtCoC,OAAOG,QAAQ;QAEvB;IACJ;AACJ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/launchdarkly-service",
|
|
3
|
-
"version": "36.
|
|
3
|
+
"version": "36.4.0",
|
|
4
4
|
"description": "Service for creating and reusing client connections for LaunchDarkly",
|
|
5
5
|
"homepage": "https://docs.st.dev/docs/frontend/uikit/launchdarkly-service",
|
|
6
6
|
"repository": {
|
|
@@ -26,22 +26,22 @@
|
|
|
26
26
|
"src"
|
|
27
27
|
],
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@datadog/browser-rum": "^7.
|
|
30
|
-
"@servicetitan/log-service": "36.
|
|
31
|
-
"@servicetitan/react-ioc": "36.
|
|
29
|
+
"@datadog/browser-rum": "^7.2.0",
|
|
30
|
+
"@servicetitan/log-service": "36.4.0",
|
|
31
|
+
"@servicetitan/react-ioc": "36.4.0",
|
|
32
32
|
"@testing-library/dom": "^10.4.1",
|
|
33
33
|
"@testing-library/jest-dom": "^6.9.1",
|
|
34
34
|
"@testing-library/react": "^16.3.2",
|
|
35
35
|
"launchdarkly-js-client-sdk": "3.5.0",
|
|
36
36
|
"launchdarkly-react-client-sdk": "3.6.1",
|
|
37
|
-
"mobx": "~6.15.
|
|
38
|
-
"mobx-react": "~9.2.
|
|
37
|
+
"mobx": "~6.15.4",
|
|
38
|
+
"mobx-react": "~9.2.2",
|
|
39
39
|
"react": "~18.3.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@datadog/browser-rum": ">=6.19.0",
|
|
43
|
-
"@servicetitan/log-service": "36.
|
|
44
|
-
"@servicetitan/react-ioc": "36.
|
|
43
|
+
"@servicetitan/log-service": "36.4.0",
|
|
44
|
+
"@servicetitan/react-ioc": "36.4.0",
|
|
45
45
|
"launchdarkly-js-client-sdk": "3.5.0",
|
|
46
46
|
"launchdarkly-react-client-sdk": "3.6.1",
|
|
47
47
|
"mobx": ">= 6.13.5",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"cli": {
|
|
60
60
|
"webpack": false
|
|
61
61
|
},
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "8beab9ae3a70ce381e8cd53f2de8691640e96c6e"
|
|
63
63
|
}
|
|
@@ -4,8 +4,15 @@ import { action, makeObservable, observable } from 'mobx';
|
|
|
4
4
|
|
|
5
5
|
@injectable()
|
|
6
6
|
export class FeatureFlagStore<T extends LDFlagSet = LDFlagSet> {
|
|
7
|
+
/*
|
|
8
|
+
* Must stay `.ref` rather than a deep observable. Deep MobX conversion
|
|
9
|
+
* clones the launchdarkly-react-client-sdk useFlags() proxy via descriptor
|
|
10
|
+
* enumeration, which severs it: reads then bypass client.variation() and
|
|
11
|
+
* `flag-used` inspectors never fire. Holding the proxy by reference keeps
|
|
12
|
+
* reads flowing through it.
|
|
13
|
+
*/
|
|
7
14
|
// @ts-expect-error because 'LDFlagSet' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint
|
|
8
|
-
@observable flags: T = {} as LDFlagSet;
|
|
15
|
+
@observable.ref flags: T = {} as LDFlagSet;
|
|
9
16
|
|
|
10
17
|
constructor() {
|
|
11
18
|
makeObservable(this);
|
|
@@ -17,22 +17,37 @@ describe(`[launchdarkly-service] ${FeatureFlagStore.name}`, () => {
|
|
|
17
17
|
flags = { foo: 'bar' };
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
const subject = (
|
|
21
|
-
store.setFlags(...args);
|
|
20
|
+
const subject = () => store.setFlags(flags);
|
|
22
21
|
|
|
23
22
|
test('sets flags', () => {
|
|
24
|
-
subject(
|
|
23
|
+
subject();
|
|
25
24
|
|
|
26
25
|
expect(store.flags).toEqual(flags);
|
|
27
26
|
});
|
|
28
27
|
|
|
29
|
-
test('
|
|
30
|
-
subject(
|
|
28
|
+
test('retains the source reference rather than cloning', () => {
|
|
29
|
+
subject();
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
expect(store.flags).toBe(flags);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('when flags are backed by a get-trapping proxy', () => {
|
|
35
|
+
let get: jest.Mock;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
get = jest.fn((target, key, receiver) => Reflect.get(target, key, receiver));
|
|
39
|
+
flags = new Proxy({ foo: 'bar' }, { get });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('routes single-key reads through the proxy', () => {
|
|
43
|
+
subject();
|
|
44
|
+
get.mockClear();
|
|
45
|
+
|
|
46
|
+
const value = store.flags.foo;
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
expect(value).toBe('bar');
|
|
49
|
+
expect(get).toHaveBeenCalledWith(expect.anything(), 'foo', expect.anything());
|
|
50
|
+
});
|
|
36
51
|
});
|
|
37
52
|
});
|
|
38
53
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Provider, useDependencies } from '@servicetitan/react-ioc';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { LDClient, LDInspection } from 'launchdarkly-js-client-sdk';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
import { FC } from 'react';
|
|
7
|
+
import { FeatureFlagStore } from '../feature-flag-store';
|
|
8
|
+
import { LDProvider } from '../ld-provider';
|
|
9
|
+
import { LD_SERVICE_TOKEN, LDService } from '../types';
|
|
10
|
+
|
|
11
|
+
const FLAG_KEY = 'my-flag';
|
|
12
|
+
const CAMEL_KEY = 'myFlag';
|
|
13
|
+
|
|
14
|
+
function createFakeClient(
|
|
15
|
+
flagValues: Record<string, unknown>,
|
|
16
|
+
inspectors: LDInspection[]
|
|
17
|
+
): LDClient {
|
|
18
|
+
return {
|
|
19
|
+
allFlags: () => ({ ...flagValues }),
|
|
20
|
+
variation: (key: string, defaultValue: unknown) => {
|
|
21
|
+
const value = key in flagValues ? flagValues[key] : defaultValue;
|
|
22
|
+
inspectors
|
|
23
|
+
.filter(i => i.type === 'flag-used')
|
|
24
|
+
.forEach(i => i.method!(key, { value }, { kind: 'user', key: 'test' }));
|
|
25
|
+
return value;
|
|
26
|
+
},
|
|
27
|
+
waitForInitialization: () => Promise.resolve(),
|
|
28
|
+
on: () => {},
|
|
29
|
+
off: () => {},
|
|
30
|
+
} as Partial<LDClient> as LDClient;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const Reader: FC = observer(() => {
|
|
34
|
+
const [{ flags }] = useDependencies(FeatureFlagStore);
|
|
35
|
+
return <div data-testid="flag-value">{String(flags[CAMEL_KEY])}</div>;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('[launchdarkly-service] flag-used inspector integration', () => {
|
|
39
|
+
const flagValues: Record<string, unknown> = { [FLAG_KEY]: true };
|
|
40
|
+
let inspectorMethod: jest.Mock;
|
|
41
|
+
let ldService: LDService;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
jest.clearAllMocks();
|
|
45
|
+
|
|
46
|
+
inspectorMethod = jest.fn();
|
|
47
|
+
const inspector: LDInspection = {
|
|
48
|
+
type: 'flag-used',
|
|
49
|
+
name: 'test-inspector',
|
|
50
|
+
method: inspectorMethod,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
ldService = {
|
|
54
|
+
getClient: ({ options }) => createFakeClient(flagValues, options?.inspectors ?? []),
|
|
55
|
+
options: { inspectors: [inspector] },
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const subject = () =>
|
|
60
|
+
render(
|
|
61
|
+
<Provider singletons={[{ provide: LD_SERVICE_TOKEN, useValue: ldService }]}>
|
|
62
|
+
<LDProvider clientSideID="test" projectName="test">
|
|
63
|
+
<Reader />
|
|
64
|
+
</LDProvider>
|
|
65
|
+
</Provider>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
describe('when a flag is read through FeatureFlagStore', () => {
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
subject();
|
|
71
|
+
// Wait for <Reader> to mount and read flag
|
|
72
|
+
await screen.findByTestId('flag-value');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('fires the flag-used inspector', () => {
|
|
76
|
+
expect(inspectorMethod).toHaveBeenCalledWith(
|
|
77
|
+
FLAG_KEY,
|
|
78
|
+
expect.objectContaining({ value: true }),
|
|
79
|
+
expect.anything()
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|