Skip to content

Commit 6b7bf7e

Browse files
authored
feat: Add plugin support for node. (#880)
This adds plugin support for the node server SDK. Plugin support is leaf-SDK specific because plugins use the interface of the SDK. The interface of the SDK has slightly different capabilities based on implementation. Unfortunately this isn't a simple problem to tackle with regards to organization. We could use composition with the common functionality being generic over the SDK interface. Doing so would require some breaking changes to all the SDK implementation, but could be worth it in the future. For not though we have to settle for some complexity with the plugin interface being defined per-sdk and the call to the initialization code being per-sdk. This was able to re-use the generic types and functions which means this isn't very much code. This SDK had another complexity which is the event emitter. For convenient implementation of events we were using a mixin, but this mixin approach means we didn't have the full interface of the SDK available at the construction time of the SDK. So this PR moves the emitter implementation into the node client.
1 parent 635ed79 commit 6b7bf7e

File tree

9 files changed

+482
-25
lines changed

9 files changed

+482
-25
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { integrations, LDContext, LDLogger } from '@launchdarkly/js-server-sdk-common';
2+
3+
import { LDOptions } from '../src/api/LDOptions';
4+
import { LDPlugin } from '../src/api/LDPlugin';
5+
import LDClientNode from '../src/LDClientNode';
6+
import NodeInfo from '../src/platform/NodeInfo';
7+
8+
// Test for plugin registration
9+
it('registers plugins and executes hooks during initialization', async () => {
10+
const logger: LDLogger = {
11+
debug: jest.fn(),
12+
info: jest.fn(),
13+
warn: jest.fn(),
14+
error: jest.fn(),
15+
};
16+
17+
const mockHook: integrations.Hook = {
18+
getMetadata(): integrations.HookMetadata {
19+
return {
20+
name: 'test-hook',
21+
};
22+
},
23+
beforeEvaluation: jest.fn(() => ({})),
24+
afterEvaluation: jest.fn(() => ({})),
25+
};
26+
27+
const mockPlugin: LDPlugin = {
28+
getMetadata: () => ({ name: 'test-plugin' }),
29+
register: jest.fn(),
30+
getHooks: () => [mockHook],
31+
};
32+
33+
const client = new LDClientNode('test', { offline: true, logger, plugins: [mockPlugin] });
34+
35+
// Verify the plugin was registered
36+
expect(mockPlugin.register).toHaveBeenCalled();
37+
38+
// Now test that hooks work by calling identify and variation
39+
const context: LDContext = { key: 'user-key', kind: 'user' };
40+
41+
await client.variation('flag-key', context, false);
42+
43+
expect(mockHook.beforeEvaluation).toHaveBeenCalledWith(
44+
{
45+
context,
46+
defaultValue: false,
47+
flagKey: 'flag-key',
48+
method: 'LDClient.variation',
49+
environmentId: undefined,
50+
},
51+
{},
52+
);
53+
54+
expect(mockHook.afterEvaluation).toHaveBeenCalled();
55+
});
56+
57+
// Test for multiple plugins with hooks
58+
it('registers multiple plugins and executes all hooks', async () => {
59+
const logger: LDLogger = {
60+
debug: jest.fn(),
61+
info: jest.fn(),
62+
warn: jest.fn(),
63+
error: jest.fn(),
64+
};
65+
66+
const mockHook1: integrations.Hook = {
67+
getMetadata(): integrations.HookMetadata {
68+
return {
69+
name: 'test-hook-1',
70+
};
71+
},
72+
beforeEvaluation: jest.fn(() => ({})),
73+
afterEvaluation: jest.fn(() => ({})),
74+
};
75+
76+
const mockHook2: integrations.Hook = {
77+
getMetadata(): integrations.HookMetadata {
78+
return {
79+
name: 'test-hook-2',
80+
};
81+
},
82+
beforeEvaluation: jest.fn(() => ({})),
83+
afterEvaluation: jest.fn(() => ({})),
84+
};
85+
86+
const mockPlugin1: LDPlugin = {
87+
getMetadata: () => ({ name: 'test-plugin-1' }),
88+
register: jest.fn(),
89+
getHooks: () => [mockHook1],
90+
};
91+
92+
const mockPlugin2: LDPlugin = {
93+
getMetadata: () => ({ name: 'test-plugin-2' }),
94+
register: jest.fn(),
95+
getHooks: () => [mockHook2],
96+
};
97+
98+
const client = new LDClientNode('test', {
99+
offline: true,
100+
logger,
101+
plugins: [mockPlugin1, mockPlugin2],
102+
});
103+
104+
// Verify plugins were registered
105+
expect(mockPlugin1.register).toHaveBeenCalled();
106+
expect(mockPlugin2.register).toHaveBeenCalled();
107+
108+
// Test that both hooks work
109+
const context: LDContext = { key: 'user-key', kind: 'user' };
110+
await client.variation('flag-key', context, false);
111+
112+
expect(mockHook1.beforeEvaluation).toHaveBeenCalled();
113+
expect(mockHook1.afterEvaluation).toHaveBeenCalled();
114+
expect(mockHook2.beforeEvaluation).toHaveBeenCalled();
115+
expect(mockHook2.afterEvaluation).toHaveBeenCalled();
116+
});
117+
118+
it('passes correct environmentMetadata to plugin getHooks and register functions', async () => {
119+
const logger: LDLogger = {
120+
debug: jest.fn(),
121+
info: jest.fn(),
122+
warn: jest.fn(),
123+
error: jest.fn(),
124+
};
125+
126+
const mockHook: integrations.Hook = {
127+
getMetadata(): integrations.HookMetadata {
128+
return {
129+
name: 'test-hook',
130+
};
131+
},
132+
beforeEvaluation: jest.fn(() => ({})),
133+
afterEvaluation: jest.fn(() => ({})),
134+
};
135+
136+
const mockPlugin: LDPlugin = {
137+
getMetadata: () => ({ name: 'test-plugin' }),
138+
register: jest.fn(),
139+
getHooks: jest.fn(() => [mockHook]),
140+
};
141+
142+
const options: LDOptions = {
143+
wrapperName: 'test-wrapper',
144+
wrapperVersion: '2.0.0',
145+
application: {
146+
id: 'test-app',
147+
name: 'TestApp',
148+
version: '3.0.0',
149+
versionName: '3',
150+
},
151+
offline: true,
152+
logger,
153+
plugins: [mockPlugin],
154+
};
155+
156+
// eslint-disable-next-line no-new
157+
new LDClientNode('test', options);
158+
const platformInfo = new NodeInfo(options);
159+
const sdkData = platformInfo.sdkData();
160+
expect(sdkData.name).toBeDefined();
161+
expect(sdkData.version).toBeDefined();
162+
163+
// Verify getHooks was called with correct environmentMetadata
164+
expect(mockPlugin.getHooks).toHaveBeenCalledWith({
165+
sdk: {
166+
name: sdkData.userAgentBase,
167+
version: sdkData.version,
168+
wrapperName: options.wrapperName,
169+
wrapperVersion: options.wrapperVersion,
170+
},
171+
application: {
172+
id: options.application?.id,
173+
name: options.application?.name,
174+
version: options.application?.version,
175+
versionName: options.application?.versionName,
176+
},
177+
sdkKey: 'test',
178+
});
179+
180+
// Verify register was called with correct environmentMetadata
181+
expect(mockPlugin.register).toHaveBeenCalledWith(
182+
expect.any(Object), // client
183+
{
184+
sdk: {
185+
name: sdkData.userAgentBase,
186+
version: sdkData.version,
187+
wrapperName: options.wrapperName,
188+
wrapperVersion: options.wrapperVersion,
189+
},
190+
application: {
191+
id: options.application?.id,
192+
version: options.application?.version,
193+
name: options.application?.name,
194+
versionName: options.application?.versionName,
195+
},
196+
sdkKey: 'test',
197+
},
198+
);
199+
});
200+
201+
it('passes correct environmentMetadata without optional fields', async () => {
202+
const logger: LDLogger = {
203+
debug: jest.fn(),
204+
info: jest.fn(),
205+
warn: jest.fn(),
206+
error: jest.fn(),
207+
};
208+
209+
const mockHook: integrations.Hook = {
210+
getMetadata(): integrations.HookMetadata {
211+
return {
212+
name: 'test-hook',
213+
};
214+
},
215+
beforeEvaluation: jest.fn(() => ({})),
216+
afterEvaluation: jest.fn(() => ({})),
217+
};
218+
219+
const mockPlugin: LDPlugin = {
220+
getMetadata: () => ({ name: 'test-plugin' }),
221+
register: jest.fn(),
222+
getHooks: jest.fn(() => [mockHook]),
223+
};
224+
225+
const options: LDOptions = {
226+
offline: true,
227+
logger,
228+
plugins: [mockPlugin],
229+
};
230+
231+
// eslint-disable-next-line no-new
232+
new LDClientNode('test', options);
233+
234+
const platformInfo = new NodeInfo(options);
235+
const sdkData = platformInfo.sdkData();
236+
expect(sdkData.name).toBeDefined();
237+
expect(sdkData.version).toBeDefined();
238+
239+
// Verify getHooks was called with correct environmentMetadata
240+
expect(mockPlugin.getHooks).toHaveBeenCalledWith({
241+
sdk: {
242+
name: sdkData.userAgentBase,
243+
version: sdkData.version,
244+
},
245+
sdkKey: 'test',
246+
});
247+
248+
// Verify register was called with correct environmentMetadata
249+
expect(mockPlugin.register).toHaveBeenCalledWith(
250+
expect.any(Object), // client
251+
{
252+
sdk: {
253+
name: sdkData.userAgentBase,
254+
version: sdkData.version,
255+
},
256+
sdkKey: 'test',
257+
},
258+
);
259+
});

0 commit comments

Comments
 (0)