Skip to content

Commit dd1ae75

Browse files
committed
assignment hooks and asynchronous method to enable sticky assignments
1 parent 3a4742b commit dd1ae75

File tree

5 files changed

+3747
-3661
lines changed

5 files changed

+3747
-3661
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
55
"main": "dist/index.js",
66
"files": [

src/assignment-hooks.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Implement this interface to override an assignment or receive a callback post assignment
3+
* @public
4+
*/
5+
export interface IAssignmentHooks {
6+
/**
7+
* Invoked before a subject is assigned to an experiment variation.
8+
*
9+
* @param subject id of subject being assigned
10+
* @returns variation to override for the given subject. If null is returned,
11+
* then the subject will be assigned with the default assignment logic.
12+
* @public
13+
*/
14+
onPreAssignment(subject: string): Promise<string | null>;
15+
16+
/**
17+
* Invoked after a subject is assigned. Useful for any post assignment logic needed which is specific
18+
* to an experiment/flag. Do not use this for logging assignments - use IAssignmentLogger instead.
19+
* @param variation the assigned variation
20+
* @public
21+
*/
22+
onPostAssignment(variation: string): Promise<void>;
23+
}

src/client/eppo-client.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
readAssignmentTestData,
1111
readMockRacResponse,
1212
} from '../../test/testHelpers';
13+
import { IAssignmentHooks } from '../assignment-hooks';
1314
import { IAssignmentLogger } from '../assignment-logger';
1415
import { IConfigurationStore } from '../configuration-store';
1516
import { MAX_EVENT_QUEUE_SIZE } from '../constants';
@@ -296,4 +297,76 @@ describe('EppoClient E2E test', () => {
296297
return globalClient.getAssignment(subject.subjectKey, experiment, subject.subjectAttributes);
297298
});
298299
}
300+
301+
describe('getAssignmentWithHooks', () => {
302+
let client: EppoClient;
303+
304+
beforeAll(() => {
305+
storage.setEntries({ [experimentName]: mockExperimentConfig });
306+
client = new EppoClient(storage);
307+
});
308+
309+
describe('onPreAssignment', () => {
310+
it('called with subject ID', () => {
311+
const mockHooks = td.object<IAssignmentHooks>();
312+
client.getAssignmentWithHooks('subject-identifer', experimentName, {}, mockHooks);
313+
expect(td.explain(mockHooks.onPreAssignment).callCount).toEqual(1);
314+
expect(td.explain(mockHooks.onPreAssignment).calls[0].args[0]).toEqual('subject-identifer');
315+
});
316+
317+
it('overrides returned assignment', async () => {
318+
const variation = await client.getAssignmentWithHooks(
319+
'subject-identifer',
320+
experimentName,
321+
{},
322+
{
323+
onPreAssignment(subject: string): Promise<string> {
324+
return Promise.resolve('my-overridden-variation');
325+
},
326+
327+
onPostAssignment(variation: string): Promise<void> {
328+
return Promise.resolve();
329+
},
330+
},
331+
);
332+
333+
expect(variation).toEqual('my-overridden-variation');
334+
});
335+
336+
it('uses regular assignment logic if onPreAssignment returns null', async () => {
337+
const variation = await client.getAssignmentWithHooks(
338+
'subject-identifer',
339+
experimentName,
340+
{},
341+
{
342+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
343+
onPreAssignment(subject: string): Promise<string | null> {
344+
return Promise.resolve(null);
345+
},
346+
347+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
348+
onPostAssignment(variation: string): Promise<void> {
349+
return Promise.resolve();
350+
},
351+
},
352+
);
353+
354+
expect(variation).not.toEqual(null);
355+
});
356+
});
357+
358+
describe('onPostAssignment', () => {
359+
it('called with assigned variation after assignment', async () => {
360+
const mockHooks = td.object<IAssignmentHooks>();
361+
const variation = await client.getAssignmentWithHooks(
362+
'subject-identifer',
363+
experimentName,
364+
{},
365+
mockHooks,
366+
);
367+
expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
368+
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(variation);
369+
});
370+
});
371+
});
299372
});

src/client/eppo-client.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as md5 from 'md5';
22

3+
import { IAssignmentHooks } from '../assignment-hooks';
34
import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger';
45
import { IConfigurationStore } from '../configuration-store';
56
import { MAX_EVENT_QUEUE_SIZE } from '../constants';
@@ -73,6 +74,22 @@ export default class EppoClient implements IEppoClient {
7374
return assignedVariation;
7475
}
7576

77+
async getAssignmentWithHooks(
78+
subjectKey: string,
79+
experimentKey: string,
80+
subjectAttributes = {},
81+
assignmentHooks: IAssignmentHooks,
82+
): Promise<string> {
83+
let assignment = await assignmentHooks?.onPreAssignment(subjectKey);
84+
if (!assignment) {
85+
assignment = this.getAssignment(subjectKey, experimentKey, subjectAttributes);
86+
}
87+
88+
assignmentHooks?.onPostAssignment(assignment);
89+
90+
return assignment;
91+
}
92+
7693
public setLogger(logger: IAssignmentLogger) {
7794
this.assignmentLogger = logger;
7895
this.flushQueuedEvents(); // log any events that may have been queued while initializing

0 commit comments

Comments
 (0)