Skip to content

Commit 530eea6

Browse files
authored
Merge pull request #2 from Eppo-exp/assignment-hooks
Assignment hooks and asynchronous method to enable sticky assignments
2 parents 3a4742b + 6a2a8ba commit 530eea6

File tree

5 files changed

+3749
-3661
lines changed

5 files changed

+3749
-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: 75 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,78 @@ 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+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
324+
onPreAssignment(subject: string): Promise<string> {
325+
return Promise.resolve('my-overridden-variation');
326+
},
327+
328+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
329+
onPostAssignment(variation: string): Promise<void> {
330+
return Promise.resolve();
331+
},
332+
},
333+
);
334+
335+
expect(variation).toEqual('my-overridden-variation');
336+
});
337+
338+
it('uses regular assignment logic if onPreAssignment returns null', async () => {
339+
const variation = await client.getAssignmentWithHooks(
340+
'subject-identifer',
341+
experimentName,
342+
{},
343+
{
344+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
345+
onPreAssignment(subject: string): Promise<string | null> {
346+
return Promise.resolve(null);
347+
},
348+
349+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
350+
onPostAssignment(variation: string): Promise<void> {
351+
return Promise.resolve();
352+
},
353+
},
354+
);
355+
356+
expect(variation).not.toEqual(null);
357+
});
358+
});
359+
360+
describe('onPostAssignment', () => {
361+
it('called with assigned variation after assignment', async () => {
362+
const mockHooks = td.object<IAssignmentHooks>();
363+
const variation = await client.getAssignmentWithHooks(
364+
'subject-identifer',
365+
experimentName,
366+
{},
367+
mockHooks,
368+
);
369+
expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
370+
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(variation);
371+
});
372+
});
373+
});
299374
});

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 == null) {
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)