Skip to content

Commit ee7337a

Browse files
committed
Add afterEventEnqueue hook execution for event enqueueing
1 parent 8b2f757 commit ee7337a

File tree

6 files changed

+380
-93
lines changed

6 files changed

+380
-93
lines changed

src/HookRunner.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const AFTER_EVALUATION_STAGE_NAME = 'afterEvaluation';
44
const BEFORE_IDENTIFY_STAGE_NAME = 'beforeIdentify';
55
const AFTER_IDENTIFY_STAGE_NAME = 'afterIdentify';
66
const AFTER_TRACK_STAGE_NAME = 'afterTrack';
7+
const AFTER_ENQUEUE_STAGE_NAME = 'afterEventEnqueue';
78

89
/**
910
* Safely executes a hook stage function, logging any errors.
@@ -148,6 +149,28 @@ function executeAfterTrack(logger, hooks, hookContext) {
148149
}
149150
}
150151

152+
/**
153+
* Executes the 'afterEventEnqueue' stage for all registered hooks in reverse order.
154+
* @param {{ error: (message: string) => void }} logger The logger instance.
155+
* @param {Array<{ afterEventEnqueue?: (hookContext: object) => void }>} hooks The array of hook instances.
156+
* @param {object} hookContext The full event object that was enqueued.
157+
* @returns {void}
158+
*/
159+
function executeAfterEnqueue(logger, hooks, hookContext) {
160+
// This iterates in reverse, versus reversing a shallow copy of the hooks,
161+
// for efficiency.
162+
for (let hookIndex = hooks.length - 1; hookIndex >= 0; hookIndex -= 1) {
163+
const hook = hooks[hookIndex];
164+
tryExecuteStage(
165+
logger,
166+
AFTER_ENQUEUE_STAGE_NAME,
167+
getHookName(logger, hook),
168+
() => hook?.afterEventEnqueue?.(hookContext),
169+
undefined
170+
);
171+
}
172+
}
173+
151174
/**
152175
* Factory function to create a HookRunner instance.
153176
* Manages the execution of hooks for flag evaluations and identify operations.
@@ -239,11 +262,25 @@ function createHookRunner(logger, initialHooks) {
239262
executeAfterTrack(logger, hooks, hookContext);
240263
}
241264

265+
/**
266+
* Executes the 'afterEventEnqueue' stage for all registered hooks in reverse order.
267+
* @param {object} hookContext The full event object that was enqueued.
268+
* @returns {void}
269+
*/
270+
function afterEventEnqueue(hookContext) {
271+
if (hooksInternal.length === 0) {
272+
return;
273+
}
274+
const hooks = [...hooksInternal];
275+
executeAfterEnqueue(logger, hooks, hookContext);
276+
}
277+
242278
return {
243279
withEvaluation,
244280
identify,
245281
addHook,
246282
afterTrack,
283+
afterEventEnqueue,
247284
};
248285
}
249286

src/__tests__/HookRunner-test.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const createTestHook = (name = 'Test Hook') => ({
1717
beforeIdentify: jest.fn(),
1818
afterIdentify: jest.fn(),
1919
afterTrack: jest.fn(),
20+
afterEventEnqueue: jest.fn(),
2021
});
2122

2223
describe('Given a logger, runner, and hook', () => {
@@ -379,12 +380,114 @@ describe('Given a logger, runner, and hook', () => {
379380
expect(logger.error).not.toHaveBeenCalled();
380381
});
381382

383+
it('should execute afterEventEnqueue hooks', () => {
384+
const context = { kind: 'user', key: 'user-123' };
385+
const event = {
386+
kind: 'feature',
387+
key: 'test-flag',
388+
context,
389+
value: true,
390+
variation: 1,
391+
default: false,
392+
creationDate: new Date().getTime(),
393+
version: 42,
394+
trackEvents: true,
395+
};
396+
397+
hookRunner.afterEventEnqueue(event);
398+
399+
expect(testHook.afterEventEnqueue).toHaveBeenCalledWith(event);
400+
});
401+
402+
it('should handle errors in afterEventEnqueue hooks', () => {
403+
const errorHook = {
404+
getMetadata: jest.fn().mockReturnValue({ name: 'Error Hook' }),
405+
afterEventEnqueue: jest.fn().mockImplementation(() => {
406+
throw new Error('Hook error');
407+
}),
408+
};
409+
410+
const errorHookRunner = createHookRunner(logger, [errorHook]);
411+
412+
errorHookRunner.afterEventEnqueue({
413+
kind: 'custom',
414+
key: 'test-event',
415+
context: { kind: 'user', key: 'user-123' },
416+
creationDate: new Date().getTime(),
417+
});
418+
419+
expect(logger.error).toHaveBeenCalledWith(
420+
expect.stringContaining(
421+
'An error was encountered in "afterEventEnqueue" of the "Error Hook" hook: Error: Hook error'
422+
)
423+
);
424+
});
425+
426+
it('should skip afterEventEnqueue execution if there are no hooks', () => {
427+
const emptyHookRunner = createHookRunner(logger, []);
428+
429+
emptyHookRunner.afterEventEnqueue({
430+
kind: 'identify',
431+
context: { kind: 'user', key: 'user-123' },
432+
creationDate: new Date().getTime(),
433+
});
434+
435+
expect(logger.error).not.toHaveBeenCalled();
436+
});
437+
438+
it('should execute afterEventEnqueue hooks for different event types', () => {
439+
const context = { kind: 'user', key: 'user-123' };
440+
const creationDate = new Date().getTime();
441+
442+
// Test feature event
443+
const featureEvent = {
444+
kind: 'feature',
445+
key: 'test-flag',
446+
context,
447+
value: true,
448+
variation: 1,
449+
default: false,
450+
creationDate,
451+
version: 42,
452+
};
453+
454+
hookRunner.afterEventEnqueue(featureEvent);
455+
expect(testHook.afterEventEnqueue).toHaveBeenCalledWith(featureEvent);
456+
457+
// Test custom event
458+
const customEvent = {
459+
kind: 'custom',
460+
key: 'test-event',
461+
context,
462+
data: { custom: 'data' },
463+
metricValue: 123,
464+
creationDate,
465+
url: 'https://example.com',
466+
};
467+
468+
hookRunner.afterEventEnqueue(customEvent);
469+
expect(testHook.afterEventEnqueue).toHaveBeenCalledWith(customEvent);
470+
471+
// Test identify event
472+
const identifyEvent = {
473+
kind: 'identify',
474+
context,
475+
creationDate,
476+
};
477+
478+
hookRunner.afterEventEnqueue(identifyEvent);
479+
expect(testHook.afterEventEnqueue).toHaveBeenCalledWith(identifyEvent);
480+
481+
expect(testHook.afterEventEnqueue).toHaveBeenCalledTimes(3);
482+
});
483+
382484
it('executes hook stages in the specified order', () => {
383485
const beforeEvalOrder = [];
384486
const afterEvalOrder = [];
385487
const beforeIdentifyOrder = [];
386488
const afterIdentifyOrder = [];
387489
const afterTrackOrder = [];
490+
const afterEventEnqueueOrder = [];
388491

389492
const createMockHook = id => ({
390493
getMetadata: jest.fn().mockReturnValue({ name: `Hook ${id}` }),
@@ -407,6 +510,9 @@ describe('Given a logger, runner, and hook', () => {
407510
afterTrack: jest.fn().mockImplementation(() => {
408511
afterTrackOrder.push(id);
409512
}),
513+
afterEventEnqueue: jest.fn().mockImplementation(() => {
514+
afterEventEnqueueOrder.push(id);
515+
}),
410516
});
411517

412518
const hookA = createMockHook('a');
@@ -435,6 +541,14 @@ describe('Given a logger, runner, and hook', () => {
435541
metricValue: 42,
436542
});
437543

544+
// Test event enqueue order
545+
runner.afterEventEnqueue({
546+
kind: 'custom',
547+
key: 'test-event',
548+
context: { kind: 'user', key: 'bob' },
549+
creationDate: new Date().getTime(),
550+
});
551+
438552
// Verify evaluation hooks order
439553
expect(beforeEvalOrder).toEqual(['a', 'b', 'c']);
440554
expect(afterEvalOrder).toEqual(['c', 'b', 'a']);
@@ -445,5 +559,8 @@ describe('Given a logger, runner, and hook', () => {
445559

446560
// Verify track hooks order
447561
expect(afterTrackOrder).toEqual(['c', 'b', 'a']);
562+
563+
// Verify event enqueue hooks order
564+
expect(afterEventEnqueueOrder).toEqual(['c', 'b', 'a']);
448565
});
449566
});

src/__tests__/LDClient-hooks-test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,61 @@ describe('LDClient Hooks Integration', () => {
251251
});
252252
});
253253
});
254+
255+
it('should execute afterEventEnqueue hooks when events are enqueued', async () => {
256+
const testHook = {
257+
beforeEvaluation: jest.fn(),
258+
afterEvaluation: jest.fn(),
259+
beforeIdentify: jest.fn(),
260+
afterIdentify: jest.fn(),
261+
afterTrack: jest.fn(),
262+
afterEventEnqueue: jest.fn(),
263+
getMetadata() {
264+
return {
265+
name: 'test hook',
266+
};
267+
},
268+
};
269+
270+
await withClient(initialContext, { sendEvents: true }, [testHook], async client => {
271+
// Track a custom event which should trigger afterEventEnqueue
272+
client.track('test-event', { test: 'data' }, 42);
273+
274+
// Evaluate a flag which should trigger afterEventEnqueue for the feature event
275+
client.variation('test-flag', false);
276+
277+
// Check that afterEventEnqueue was called for both events
278+
expect(testHook.afterEventEnqueue).toHaveBeenCalledTimes(3); // identify + custom + feature events
279+
280+
// Verify the custom event
281+
expect(testHook.afterEventEnqueue).toHaveBeenCalledWith(
282+
expect.objectContaining({
283+
kind: 'custom',
284+
key: 'test-event',
285+
context: expect.objectContaining({ kind: 'user', key: 'user-key-initial' }),
286+
data: { test: 'data' },
287+
metricValue: 42,
288+
})
289+
);
290+
291+
// Verify the feature event
292+
expect(testHook.afterEventEnqueue).toHaveBeenCalledWith(
293+
expect.objectContaining({
294+
kind: 'feature',
295+
key: 'test-flag',
296+
context: expect.objectContaining({ kind: 'user', key: 'user-key-initial' }),
297+
value: false,
298+
default: false,
299+
})
300+
);
301+
302+
// Verify the identify event (from initialization)
303+
expect(testHook.afterEventEnqueue).toHaveBeenCalledWith(
304+
expect.objectContaining({
305+
kind: 'identify',
306+
context: expect.objectContaining({ kind: 'user', key: 'user-key-initial' }),
307+
})
308+
);
309+
});
310+
});
254311
});

src/__tests__/LDClient-plugins-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const createTestHook = (name = 'Test Hook') => ({
1818
beforeIdentify: jest.fn().mockImplementation((_ctx, data) => data),
1919
afterIdentify: jest.fn().mockImplementation((_ctx, data) => data),
2020
afterTrack: jest.fn().mockImplementation((_ctx, data) => data),
21+
afterEventEnqueue: jest.fn(),
2122
});
2223

2324
// Define a basic Plugin structure for tests

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
171171
if (shouldEnqueueEvent()) {
172172
logger.debug(messages.debugEnqueueingEvent(event.kind));
173173
events.enqueue(event);
174+
hookRunner.afterEventEnqueue(event);
174175
}
175176
}
176177

0 commit comments

Comments
 (0)