Skip to content

Commit 9596c27

Browse files
committed
feat: Add support for inspectors.
1 parent 3e6d404 commit 9596c27

File tree

16 files changed

+843
-9
lines changed

16 files changed

+843
-9
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { AsyncQueue } from 'launchdarkly-js-test-helpers';
2+
3+
import { AutoEnvAttributes, clone } from '@launchdarkly/js-sdk-common';
4+
5+
import { LDInspection } from '../src/api/LDInspection';
6+
import LDClientImpl from '../src/LDClientImpl';
7+
import { Flags, PatchFlag } from '../src/types';
8+
import { createBasicPlatform } from './createBasicPlatform';
9+
import * as mockResponseJson from './evaluation/mockResponse.json';
10+
import { MockEventSource } from './streaming/LDClientImpl.mocks';
11+
import { makeTestDataManagerFactory } from './TestDataManager';
12+
13+
it('calls flag-used inspectors', async () => {
14+
const flagUsedInspector: LDInspection = {
15+
type: 'flag-used',
16+
name: 'test flag used inspector',
17+
method: jest.fn(),
18+
};
19+
const platform = createBasicPlatform();
20+
const factory = makeTestDataManagerFactory('sdk-key', platform, {
21+
disableNetwork: true,
22+
});
23+
const client = new LDClientImpl(
24+
'sdk-key',
25+
AutoEnvAttributes.Disabled,
26+
platform,
27+
{
28+
sendEvents: false,
29+
inspectors: [flagUsedInspector],
30+
logger: {
31+
debug: jest.fn(),
32+
info: jest.fn(),
33+
warn: jest.fn(),
34+
error: jest.fn(),
35+
},
36+
},
37+
factory,
38+
);
39+
40+
await client.identify({ key: 'user-key' });
41+
await client.variation('flag-key', false);
42+
43+
expect(flagUsedInspector.method).toHaveBeenCalledWith(
44+
'flag-key',
45+
{
46+
value: false,
47+
variationIndex: null,
48+
reason: {
49+
kind: 'ERROR',
50+
errorKind: 'FLAG_NOT_FOUND',
51+
},
52+
},
53+
{ key: 'user-key' },
54+
);
55+
});
56+
57+
it('calls client-identity-changed inspectors', async () => {
58+
const identifyInspector: LDInspection = {
59+
type: 'client-identity-changed',
60+
name: 'test client identity inspector',
61+
method: jest.fn(),
62+
};
63+
64+
const platform = createBasicPlatform();
65+
const factory = makeTestDataManagerFactory('sdk-key', platform, {
66+
disableNetwork: true,
67+
});
68+
const client = new LDClientImpl(
69+
'sdk-key',
70+
AutoEnvAttributes.Disabled,
71+
platform,
72+
{
73+
sendEvents: false,
74+
inspectors: [identifyInspector],
75+
logger: {
76+
debug: jest.fn(),
77+
info: jest.fn(),
78+
warn: jest.fn(),
79+
error: jest.fn(),
80+
},
81+
},
82+
factory,
83+
);
84+
85+
await client.identify({ key: 'user-key' });
86+
87+
expect(identifyInspector.method).toHaveBeenCalledWith({ key: 'user-key' });
88+
});
89+
90+
it('calls flag-detail-changed inspector for individial flag changes on patch', async () => {
91+
const eventQueue = new AsyncQueue();
92+
const flagDetailChangedInspector: LDInspection = {
93+
type: 'flag-detail-changed',
94+
name: 'test flag detail changed inspector',
95+
method: jest.fn(() => eventQueue.add({})),
96+
};
97+
const platform = createBasicPlatform();
98+
const factory = makeTestDataManagerFactory('sdk-key', platform);
99+
const client = new LDClientImpl(
100+
'sdk-key',
101+
AutoEnvAttributes.Disabled,
102+
platform,
103+
{
104+
sendEvents: false,
105+
inspectors: [flagDetailChangedInspector],
106+
logger: {
107+
debug: jest.fn(),
108+
info: jest.fn(),
109+
warn: jest.fn(),
110+
error: jest.fn(),
111+
},
112+
},
113+
factory,
114+
);
115+
let mockEventSource: MockEventSource;
116+
117+
const putResponse = clone<Flags>(mockResponseJson);
118+
const putEvents = [{ data: JSON.stringify(putResponse) }];
119+
platform.requests.createEventSource.mockImplementation(
120+
(streamUri: string = '', options: any = {}) => {
121+
mockEventSource = new MockEventSource(streamUri, options);
122+
const patchResponse = clone<PatchFlag>(putResponse['dev-test-flag']);
123+
patchResponse.key = 'dev-test-flag';
124+
patchResponse.value = false;
125+
patchResponse.version += 1;
126+
const patchEvents = [{ data: JSON.stringify(patchResponse) }];
127+
128+
// @ts-ignore
129+
mockEventSource.simulateEvents('patch', patchEvents);
130+
mockEventSource.simulateEvents('put', putEvents);
131+
return mockEventSource;
132+
},
133+
);
134+
135+
await client.identify({ key: 'user-key' }, { waitForNetworkResults: true });
136+
137+
await eventQueue.take();
138+
expect(flagDetailChangedInspector.method).toHaveBeenCalledWith('dev-test-flag', {
139+
reason: null,
140+
value: false,
141+
variationIndex: 0,
142+
});
143+
});
144+
145+
it('calls flag-details-changed inspectors when all flag values change', async () => {
146+
const flagDetailsChangedInspector: LDInspection = {
147+
type: 'flag-details-changed',
148+
name: 'test flag details changed inspector',
149+
method: jest.fn(),
150+
};
151+
const platform = createBasicPlatform();
152+
const factory = makeTestDataManagerFactory('sdk-key', platform);
153+
const client = new LDClientImpl(
154+
'sdk-key',
155+
AutoEnvAttributes.Disabled,
156+
platform,
157+
{
158+
sendEvents: false,
159+
inspectors: [flagDetailsChangedInspector],
160+
logger: {
161+
debug: jest.fn(),
162+
info: jest.fn(),
163+
warn: jest.fn(),
164+
error: jest.fn(),
165+
},
166+
},
167+
factory,
168+
);
169+
let mockEventSource: MockEventSource;
170+
171+
platform.requests.createEventSource.mockImplementation(
172+
(streamUri: string = '', options: any = {}) => {
173+
mockEventSource = new MockEventSource(streamUri, options);
174+
const simulatedEvents = [{ data: JSON.stringify(mockResponseJson) }];
175+
mockEventSource.simulateEvents('put', simulatedEvents);
176+
return mockEventSource;
177+
},
178+
);
179+
180+
await client.identify({ key: 'user-key' }, { waitForNetworkResults: true });
181+
expect(flagDetailsChangedInspector.method).toHaveBeenCalledWith({
182+
'dev-test-flag': { reason: null, value: true, variationIndex: 0 },
183+
'easter-i-tunes-special': { reason: null, value: false, variationIndex: 1 },
184+
'easter-specials': { reason: null, value: 'no specials', variationIndex: 3 },
185+
fdsafdsafdsafdsa: { reason: null, value: true, variationIndex: 0 },
186+
'log-level': { reason: null, value: 'warn', variationIndex: 3 },
187+
'moonshot-demo': { reason: null, value: true, variationIndex: 0 },
188+
test1: { reason: null, value: 's1', variationIndex: 0 },
189+
'this-is-a-test': { reason: null, value: true, variationIndex: 0 },
190+
});
191+
});
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { AsyncQueue } from 'launchdarkly-js-test-helpers';
2+
3+
import { LDLogger } from '@launchdarkly/js-sdk-common';
4+
5+
import InspectorManager from '../../src/inspection/InspectorManager';
6+
7+
describe('given an inspector manager with no registered inspectors', () => {
8+
const logger: LDLogger = {
9+
debug: jest.fn(),
10+
info: jest.fn(),
11+
warn: jest.fn(),
12+
error: jest.fn(),
13+
};
14+
const manager = new InspectorManager([], logger);
15+
16+
it('does not cause errors and does not produce any logs', () => {
17+
manager.onIdentityChanged({ kind: 'user', key: 'key' });
18+
manager.onFlagUsed(
19+
'flag-key',
20+
{
21+
value: null,
22+
reason: null,
23+
},
24+
{ key: 'key' },
25+
);
26+
manager.onFlagsChanged({});
27+
manager.onFlagChanged('flag-key', {
28+
value: null,
29+
reason: null,
30+
});
31+
32+
expect(logger.debug).not.toHaveBeenCalled();
33+
expect(logger.info).not.toHaveBeenCalled();
34+
expect(logger.warn).not.toHaveBeenCalled();
35+
expect(logger.error).not.toHaveBeenCalled();
36+
});
37+
38+
it('reports that it has no inspectors', () => {
39+
expect(manager.hasInspectors()).toBeFalsy();
40+
});
41+
});
42+
43+
describe('given an inspector with callbacks of every type', () => {
44+
/**
45+
* @type {AsyncQueue}
46+
*/
47+
const eventQueue = new AsyncQueue();
48+
const logger: LDLogger = {
49+
debug: jest.fn(),
50+
info: jest.fn(),
51+
warn: jest.fn(),
52+
error: jest.fn(),
53+
};
54+
const manager = new InspectorManager(
55+
[
56+
{
57+
type: 'flag-used',
58+
name: 'my-flag-used-inspector',
59+
method: (flagKey, flagDetail, context) => {
60+
eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context });
61+
},
62+
},
63+
// 'flag-used registered twice.
64+
{
65+
type: 'flag-used',
66+
name: 'my-other-flag-used-inspector',
67+
method: (flagKey, flagDetail, context) => {
68+
eventQueue.add({ type: 'flag-used', flagKey, flagDetail, context });
69+
},
70+
},
71+
{
72+
type: 'flag-details-changed',
73+
name: 'my-flag-details-inspector',
74+
method: (details) => {
75+
eventQueue.add({
76+
type: 'flag-details-changed',
77+
details,
78+
});
79+
},
80+
},
81+
{
82+
type: 'flag-detail-changed',
83+
name: 'my-flag-detail-inspector',
84+
method: (flagKey, flagDetail) => {
85+
eventQueue.add({
86+
type: 'flag-detail-changed',
87+
flagKey,
88+
flagDetail,
89+
});
90+
},
91+
},
92+
{
93+
type: 'client-identity-changed',
94+
name: 'my-identity-inspector',
95+
method: (context) => {
96+
eventQueue.add({
97+
type: 'client-identity-changed',
98+
context,
99+
});
100+
},
101+
},
102+
// Invalid inspector shouldn't have an effect.
103+
{
104+
// @ts-ignore
105+
type: 'potato',
106+
name: 'my-potato-inspector',
107+
method: () => {},
108+
},
109+
],
110+
logger,
111+
);
112+
113+
afterEach(() => {
114+
expect(eventQueue.length()).toEqual(0);
115+
});
116+
117+
afterAll(() => {
118+
eventQueue.close();
119+
});
120+
121+
it('logged that there was a bad inspector', () => {
122+
expect(logger.warn).toHaveBeenCalledWith(
123+
'an inspector: "my-potato-inspector" of an invalid type (potato) was configured',
124+
);
125+
});
126+
127+
it('executes `onFlagUsed` handlers', async () => {
128+
manager.onFlagUsed(
129+
'flag-key',
130+
{
131+
value: 'test',
132+
variationIndex: 1,
133+
reason: {
134+
kind: 'OFF',
135+
},
136+
},
137+
{ key: 'test-key' },
138+
);
139+
140+
const expectedEvent = {
141+
type: 'flag-used',
142+
flagKey: 'flag-key',
143+
flagDetail: {
144+
value: 'test',
145+
variationIndex: 1,
146+
reason: {
147+
kind: 'OFF',
148+
},
149+
},
150+
context: { key: 'test-key' },
151+
};
152+
const event1 = await eventQueue.take();
153+
expect(event1).toMatchObject(expectedEvent);
154+
155+
// There are two handlers, so there should be another event.
156+
const event2 = await eventQueue.take();
157+
expect(event2).toMatchObject(expectedEvent);
158+
});
159+
160+
it('executes `onFlags` handler', async () => {
161+
manager.onFlagsChanged({
162+
example: { value: 'a-value', reason: null },
163+
});
164+
165+
const event = await eventQueue.take();
166+
expect(event).toMatchObject({
167+
type: 'flag-details-changed',
168+
details: {
169+
example: { value: 'a-value' },
170+
},
171+
});
172+
});
173+
174+
it('executes `onFlagChanged` handler', async () => {
175+
manager.onFlagChanged('the-flag', { value: 'a-value', reason: null });
176+
177+
const event = await eventQueue.take();
178+
expect(event).toMatchObject({
179+
type: 'flag-detail-changed',
180+
flagKey: 'the-flag',
181+
flagDetail: {
182+
value: 'a-value',
183+
},
184+
});
185+
});
186+
187+
it('executes `onIdentityChanged` handler', async () => {
188+
manager.onIdentityChanged({ key: 'the-key' });
189+
190+
const event = await eventQueue.take();
191+
expect(event).toMatchObject({
192+
type: 'client-identity-changed',
193+
context: { key: 'the-key' },
194+
});
195+
});
196+
197+
it('reports that it has inspectors', () => {
198+
expect(manager.hasInspectors()).toBeTruthy();
199+
});
200+
});

0 commit comments

Comments
 (0)