@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.
@@ -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;IAE7C,KAAK,EAAE,CAAC,CAAmB;;IAOvC,QAAQ,GAAI,OAAO,CAAC,UAElB;CACL"}
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
- // @ts-expect-error because 'LDFlagSet' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint
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;IAIT,aAAc;QAHd,yJAAyJ;QACzJ,uBAAYC,SAAW,CAAC;QAMxB,uBACAC,YAAW,CAACD;YACR,IAAI,CAACA,KAAK,GAAGA;QACjB;QANIH,eAAe,IAAI;IACvB;AAMJ"}
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 = (...args)=>store.setFlags(...args);
16
+ const subject = ()=>store.setFlags(flags);
17
17
  test('sets flags', ()=>{
18
- subject(flags);
18
+ subject();
19
19
  expect(store.flags).toEqual(flags);
20
20
  });
21
- test('stores copy of flags', ()=>{
22
- subject(flags);
23
- const originalFlags = {
24
- ...flags
25
- };
26
- flags.baz = 'qux';
27
- expect(store.flags).toEqual(originalFlags);
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 = (...args: Parameters<FeatureFlagStore['setFlags']>) =>\n store.setFlags(...args);\n\n test('sets flags', () => {\n subject(flags);\n\n expect(store.flags).toEqual(flags);\n });\n\n test('stores copy of flags', () => {\n subject(flags);\n\n const originalFlags = { ...flags };\n flags.baz = 'qux';\n\n expect(store.flags).toEqual(originalFlags);\n });\n });\n});\n"],"names":["FeatureFlagStore","describe","name","subject","test","expect","flags","toEqual","store","beforeEach","foo","args","setFlags","originalFlags","baz"],"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,CAAC,GAAGQ,OAChBH,MAAMI,QAAQ,IAAID;QAEtBP,KAAK,cAAc;YACfD,QAAQG;YAERD,OAAOG,MAAMF,KAAK,EAAEC,OAAO,CAACD;QAChC;QAEAF,KAAK,wBAAwB;YACzBD,QAAQG;YAER,MAAMO,gBAAgB;gBAAE,GAAGP,KAAK;YAAC;YACjCA,MAAMQ,GAAG,GAAG;YAEZT,OAAOG,MAAMF,KAAK,EAAEC,OAAO,CAACM;QAChC;IACJ;AACJ"}
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,2 @@
1
+ import '@testing-library/jest-dom';
2
+ //# sourceMappingURL=flag-used-inspectors.test.d.ts.map
@@ -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.1",
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.1.0",
30
- "@servicetitan/log-service": "36.3.1",
31
- "@servicetitan/react-ioc": "36.3.1",
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.3",
38
- "mobx-react": "~9.2.1",
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.3.1",
44
- "@servicetitan/react-ioc": "36.3.1",
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": "93a049ba1e5cb737afb7bc95c06020736bc61a38"
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 = (...args: Parameters<FeatureFlagStore['setFlags']>) =>
21
- store.setFlags(...args);
20
+ const subject = () => store.setFlags(flags);
22
21
 
23
22
  test('sets flags', () => {
24
- subject(flags);
23
+ subject();
25
24
 
26
25
  expect(store.flags).toEqual(flags);
27
26
  });
28
27
 
29
- test('stores copy of flags', () => {
30
- subject(flags);
28
+ test('retains the source reference rather than cloning', () => {
29
+ subject();
31
30
 
32
- const originalFlags = { ...flags };
33
- flags.baz = 'qux';
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
- expect(store.flags).toEqual(originalFlags);
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
+ });