Skip to content

Commit e43426a

Browse files
authored
Additional params to experiment hooks methods. Enable hooks in getAssignment (#8)
* additional params to experiment hooks methods. enable hooks in getAssignment * only check assignment override after enabled check * ensure logging callback is invoked when overriding via hook * create internal method to avoid multiple calls to log assignment/post assignment callback * add undefined check
1 parent 4fdcce5 commit e43426a

File tree

4 files changed

+79
-69
lines changed

4 files changed

+79
-69
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.2.2",
3+
"version": "1.3.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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ export interface IAssignmentHooks {
66
/**
77
* Invoked before a subject is assigned to an experiment variation.
88
*
9+
* @param experimentKey key of the experiment being assigned
910
* @param subject id of subject being assigned
1011
* @returns variation to override for the given subject. If null is returned,
1112
* then the subject will be assigned with the default assignment logic.
1213
* @public
1314
*/
14-
onPreAssignment(subject: string): Promise<string | null>;
15+
onPreAssignment(experimentKey: string, subject: string): string | null;
1516

1617
/**
1718
* Invoked after a subject is assigned. Useful for any post assignment logic needed which is specific
1819
* to an experiment/flag. Do not use this for logging assignments - use IAssignmentLogger instead.
20+
* @param experimentKey key of the experiment being assigned
21+
* @param subject id of subject being assigned
1922
* @param variation the assigned variation
2023
* @public
2124
*/
22-
onPostAssignment(variation: string): Promise<void>;
25+
onPostAssignment(experimentKey: string, subject: string, variation: string): void;
2326
}

src/client/eppo-client.spec.ts

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ describe('EppoClient E2E test', () => {
300300
});
301301
}
302302

303-
describe('getAssignmentWithHooks', () => {
303+
describe('getAssignment with hooks', () => {
304304
let client: EppoClient;
305305

306306
beforeAll(() => {
@@ -309,41 +309,52 @@ describe('EppoClient E2E test', () => {
309309
});
310310

311311
describe('onPreAssignment', () => {
312-
it('called with subject ID', () => {
312+
it('called with experiment key and subject id', () => {
313313
const mockHooks = td.object<IAssignmentHooks>();
314-
client.getAssignmentWithHooks('subject-identifer', experimentName, mockHooks);
314+
client.getAssignment('subject-identifer', experimentName, {}, mockHooks);
315315
expect(td.explain(mockHooks.onPreAssignment).callCount).toEqual(1);
316-
expect(td.explain(mockHooks.onPreAssignment).calls[0].args[0]).toEqual('subject-identifer');
316+
expect(td.explain(mockHooks.onPreAssignment).calls[0].args[0]).toEqual(experimentName);
317+
expect(td.explain(mockHooks.onPreAssignment).calls[0].args[1]).toEqual('subject-identifer');
317318
});
318319

319320
it('overrides returned assignment', async () => {
320-
const variation = await client.getAssignmentWithHooks('subject-identifer', experimentName, {
321-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
322-
onPreAssignment(subject: string): Promise<string> {
323-
return Promise.resolve('my-overridden-variation');
324-
},
321+
const variation = await client.getAssignment(
322+
'subject-identifer',
323+
experimentName,
324+
{},
325+
{
326+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
327+
onPreAssignment(experimentKey: string, subject: string): string {
328+
return 'my-overridden-variation';
329+
},
325330

326-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
327-
onPostAssignment(variation: string): Promise<void> {
328-
return Promise.resolve();
331+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
332+
onPostAssignment(experimentKey: string, subject: string, variation: string): void {
333+
// no-op
334+
},
329335
},
330-
});
336+
);
331337

332338
expect(variation).toEqual('my-overridden-variation');
333339
});
334340

335341
it('uses regular assignment logic if onPreAssignment returns null', async () => {
336-
const variation = await client.getAssignmentWithHooks('subject-identifer', experimentName, {
337-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
338-
onPreAssignment(subject: string): Promise<string | null> {
339-
return Promise.resolve(null);
340-
},
342+
const variation = await client.getAssignment(
343+
'subject-identifer',
344+
experimentName,
345+
{},
346+
{
347+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
348+
onPreAssignment(experimentKey: string, subject: string): string | null {
349+
return null;
350+
},
341351

342-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
343-
onPostAssignment(variation: string): Promise<void> {
344-
return Promise.resolve();
352+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
353+
onPostAssignment(experimentKey: string, subject: string, variation: string): void {
354+
// no-op
355+
},
345356
},
346-
});
357+
);
347358

348359
expect(variation).not.toEqual(null);
349360
});
@@ -352,13 +363,13 @@ describe('EppoClient E2E test', () => {
352363
describe('onPostAssignment', () => {
353364
it('called with assigned variation after assignment', async () => {
354365
const mockHooks = td.object<IAssignmentHooks>();
355-
const variation = await client.getAssignmentWithHooks(
356-
'subject-identifer',
357-
experimentName,
358-
mockHooks,
359-
);
366+
const subject = 'subject-identifier';
367+
const variation = client.getAssignment(subject, experimentName, {}, mockHooks);
368+
expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
360369
expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
361-
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(variation);
370+
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(experimentName);
371+
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[1]).toEqual(subject);
372+
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[2]).toEqual(variation);
362373
});
363374
});
364375
});

src/client/eppo-client.ts

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface IEppoClient {
2222
* @param experimentKey experiment identifier
2323
* @param subjectAttributes optional attributes associated with the subject, for example name and email.
2424
* The subject attributes are used for evaluating any targeting rules tied to the experiment.
25+
* @param assignmentHooks optional interface for pre and post assignment hooks
2526
* @returns a variation value if the subject is part of the experiment sample, otherwise null
2627
* @public
2728
*/
@@ -30,26 +31,8 @@ export interface IEppoClient {
3031
experimentKey: string,
3132
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3233
subjectAttributes?: Record<string, any>,
34+
assignmentHooks?: IAssignmentHooks,
3335
): string;
34-
35-
/**
36-
* Asynchronously maps a subject to a variation for a given experiment, with pre and post assignment hooks
37-
*
38-
* @param subjectKey an identifier of the experiment subject, for example a user ID.
39-
* @param experimentKey experiment identifier
40-
* @param assignmentHooks interface for pre and post assignment hooks
41-
* @param subjectAttributes optional attributes associated with the subject, for example name and email.
42-
* The subject attributes are used for evaluating any targeting rules tied to the experiment.
43-
* @returns a variation value if the subject is part of the experiment sample, otherwise null
44-
* @public
45-
*/
46-
getAssignmentWithHooks(
47-
subjectKey: string,
48-
experimentKey: string,
49-
assignmentHooks: IAssignmentHooks,
50-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51-
subjectAttributes?: Record<string, any>,
52-
): Promise<string>;
5336
}
5437

5538
export default class EppoClient implements IEppoClient {
@@ -58,10 +41,35 @@ export default class EppoClient implements IEppoClient {
5841

5942
constructor(private configurationStore: IConfigurationStore) {}
6043

61-
getAssignment(subjectKey: string, experimentKey: string, subjectAttributes = {}): string {
44+
getAssignment(
45+
subjectKey: string,
46+
experimentKey: string,
47+
subjectAttributes = {},
48+
assignmentHooks: IAssignmentHooks = null,
49+
): string {
6250
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
6351
validateNotBlank(experimentKey, 'Invalid argument: experimentKey cannot be blank');
6452

53+
const assignment = this.getAssignmentInternal(
54+
subjectKey,
55+
experimentKey,
56+
subjectAttributes,
57+
assignmentHooks,
58+
);
59+
assignmentHooks?.onPostAssignment(experimentKey, subjectKey, assignment);
60+
61+
if (assignment !== null)
62+
this.logAssignment(experimentKey, assignment, subjectKey, subjectAttributes);
63+
64+
return assignment;
65+
}
66+
67+
private getAssignmentInternal(
68+
subjectKey: string,
69+
experimentKey: string,
70+
subjectAttributes = {},
71+
assignmentHooks: IAssignmentHooks = null,
72+
): string {
6573
const experimentConfig = this.configurationStore.get<IExperimentConfiguration>(experimentKey);
6674
const allowListOverride = this.getSubjectVariationOverride(subjectKey, experimentConfig);
6775

@@ -70,6 +78,12 @@ export default class EppoClient implements IEppoClient {
7078
// Check for disabled flag.
7179
if (!experimentConfig?.enabled) return null;
7280

81+
// check for overridden assignment via hook
82+
const overriddenAssignment = assignmentHooks?.onPreAssignment(experimentKey, subjectKey);
83+
if (overriddenAssignment !== null && overriddenAssignment !== undefined) {
84+
return overriddenAssignment;
85+
}
86+
7387
// Attempt to match a rule from the list.
7488
const matchedRule = findMatchingRule(subjectAttributes || {}, experimentConfig.rules);
7589
if (!matchedRule) return null;
@@ -88,27 +102,9 @@ export default class EppoClient implements IEppoClient {
88102
isShardInRange(shard, variation.shardRange),
89103
).value;
90104

91-
// Finally, log assignment and return assignment.
92-
this.logAssignment(experimentKey, assignedVariation, subjectKey, subjectAttributes);
93105
return assignedVariation;
94106
}
95107

96-
async getAssignmentWithHooks(
97-
subjectKey: string,
98-
experimentKey: string,
99-
assignmentHooks: IAssignmentHooks,
100-
subjectAttributes = {},
101-
): Promise<string> {
102-
let assignment = await assignmentHooks?.onPreAssignment(subjectKey);
103-
if (assignment == null) {
104-
assignment = this.getAssignment(subjectKey, experimentKey, subjectAttributes);
105-
}
106-
107-
assignmentHooks?.onPostAssignment(assignment);
108-
109-
return assignment;
110-
}
111-
112108
public setLogger(logger: IAssignmentLogger) {
113109
this.assignmentLogger = logger;
114110
this.flushQueuedEvents(); // log any events that may have been queued while initializing

0 commit comments

Comments
 (0)