Skip to content

Commit

Permalink
additional params to experiment hooks methods. enable hooks in getAss…
Browse files Browse the repository at this point in the history
…ignment
  • Loading branch information
petzel committed Aug 29, 2023
1 parent 8e80d1d commit d4c2ae7
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 74 deletions.
11 changes: 4 additions & 7 deletions js-client-sdk-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ export { constants }
export class EppoClient implements IEppoClient {
constructor(configurationStore: IConfigurationStore);
// (undocumented)
getAssignment(subjectKey: string, experimentKey: string, subjectAttributes?: {}): string;
// (undocumented)
getAssignmentWithHooks(subjectKey: string, experimentKey: string, assignmentHooks: IAssignmentHooks, subjectAttributes?: {}): Promise<string>;
getAssignment(subjectKey: string, experimentKey: string, subjectAttributes?: {}, assignmentHooks?: IAssignmentHooks): string;
// (undocumented)
setLogger(logger: IAssignmentLogger): void;
}
Expand Down Expand Up @@ -60,8 +58,8 @@ export interface IAssignmentEvent {

// @public
export interface IAssignmentHooks {
onPostAssignment(variation: string): Promise<void>;
onPreAssignment(subject: string): Promise<string | null>;
onPostAssignment(experimentKey: string, subject: string, variation: string): void;
onPreAssignment(experimentKey: string, subject: string): string | null;
}

// @public
Expand All @@ -79,8 +77,7 @@ export interface IConfigurationStore {

// @public
export interface IEppoClient {
getAssignment(subjectKey: string, experimentKey: string, subjectAttributes?: Record<string, any>): string;
getAssignmentWithHooks(subjectKey: string, experimentKey: string, assignmentHooks: IAssignmentHooks, subjectAttributes?: Record<string, any>): Promise<string>;
getAssignment(subjectKey: string, experimentKey: string, subjectAttributes?: Record<string, any>, assignmentHooks?: IAssignmentHooks): string;
}

// @public (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk-common",
"version": "1.2.2",
"version": "1.3.0",
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
"main": "dist/index.js",
"files": [
Expand Down
7 changes: 5 additions & 2 deletions src/assignment-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ export interface IAssignmentHooks {
/**
* Invoked before a subject is assigned to an experiment variation.
*
* @param experimentKey key of the experiment being assigned
* @param subject id of subject being assigned
* @returns variation to override for the given subject. If null is returned,
* then the subject will be assigned with the default assignment logic.
* @public
*/
onPreAssignment(subject: string): Promise<string | null>;
onPreAssignment(experimentKey: string, subject: string): string | null;

/**
* Invoked after a subject is assigned. Useful for any post assignment logic needed which is specific
* to an experiment/flag. Do not use this for logging assignments - use IAssignmentLogger instead.
* @param experimentKey key of the experiment being assigned
* @param subject id of subject being assigned
* @param variation the assigned variation
* @public
*/
onPostAssignment(variation: string): Promise<void>;
onPostAssignment(experimentKey: string, subject: string, variation: string): void;
}
67 changes: 39 additions & 28 deletions src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ describe('EppoClient E2E test', () => {
});
}

describe('getAssignmentWithHooks', () => {
describe('getAssignment with hooks', () => {
let client: EppoClient;

beforeAll(() => {
Expand All @@ -307,41 +307,52 @@ describe('EppoClient E2E test', () => {
});

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

it('overrides returned assignment', async () => {
const variation = await client.getAssignmentWithHooks('subject-identifer', experimentName, {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPreAssignment(subject: string): Promise<string> {
return Promise.resolve('my-overridden-variation');
},
const variation = await client.getAssignment(
'subject-identifer',
experimentName,
{},
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPreAssignment(experimentKey: string, subject: string): string {
return 'my-overridden-variation';
},

// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPostAssignment(variation: string): Promise<void> {
return Promise.resolve();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPostAssignment(experimentKey: string, subject: string, variation: string): void {
// no-op
},
},
});
);

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

it('uses regular assignment logic if onPreAssignment returns null', async () => {
const variation = await client.getAssignmentWithHooks('subject-identifer', experimentName, {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPreAssignment(subject: string): Promise<string | null> {
return Promise.resolve(null);
},
const variation = await client.getAssignment(
'subject-identifer',
experimentName,
{},
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPreAssignment(experimentKey: string, subject: string): string | null {
return null;
},

// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPostAssignment(variation: string): Promise<void> {
return Promise.resolve();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPostAssignment(experimentKey: string, subject: string, variation: string): void {
// no-op
},
},
});
);

expect(variation).not.toEqual(null);
});
Expand All @@ -350,13 +361,13 @@ describe('EppoClient E2E test', () => {
describe('onPostAssignment', () => {
it('called with assigned variation after assignment', async () => {
const mockHooks = td.object<IAssignmentHooks>();
const variation = await client.getAssignmentWithHooks(
'subject-identifer',
experimentName,
mockHooks,
);
const subject = 'subject-identifier';
const variation = client.getAssignment(subject, experimentName, {}, mockHooks);
expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(variation);
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(experimentName);
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[1]).toEqual(subject);
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[2]).toEqual(variation);
});
});
});
Expand Down
51 changes: 15 additions & 36 deletions src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface IEppoClient {
* @param experimentKey experiment identifier
* @param subjectAttributes optional attributes associated with the subject, for example name and email.
* The subject attributes are used for evaluating any targeting rules tied to the experiment.
* @param assignmentHooks optional interface for pre and post assignment hooks
* @returns a variation value if the subject is part of the experiment sample, otherwise null
* @public
*/
Expand All @@ -30,26 +31,8 @@ export interface IEppoClient {
experimentKey: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes?: Record<string, any>,
assignmentHooks?: IAssignmentHooks,
): string;

/**
* Asynchronously maps a subject to a variation for a given experiment, with pre and post assignment hooks
*
* @param subjectKey an identifier of the experiment subject, for example a user ID.
* @param experimentKey experiment identifier
* @param assignmentHooks interface for pre and post assignment hooks
* @param subjectAttributes optional attributes associated with the subject, for example name and email.
* The subject attributes are used for evaluating any targeting rules tied to the experiment.
* @returns a variation value if the subject is part of the experiment sample, otherwise null
* @public
*/
getAssignmentWithHooks(
subjectKey: string,
experimentKey: string,
assignmentHooks: IAssignmentHooks,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes?: Record<string, any>,
): Promise<string>;
}

export default class EppoClient implements IEppoClient {
Expand All @@ -58,10 +41,20 @@ export default class EppoClient implements IEppoClient {

constructor(private configurationStore: IConfigurationStore) {}

getAssignment(subjectKey: string, experimentKey: string, subjectAttributes = {}): string {
getAssignment(
subjectKey: string,
experimentKey: string,
subjectAttributes = {},
assignmentHooks: IAssignmentHooks = null,
): string {
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
validateNotBlank(experimentKey, 'Invalid argument: experimentKey cannot be blank');

const assignment = assignmentHooks?.onPreAssignment(experimentKey, subjectKey);
if (assignment != null) {
return assignment;
}

const experimentConfig = this.configurationStore.get<IExperimentConfiguration>(experimentKey);
const allowListOverride = this.getSubjectVariationOverride(subjectKey, experimentConfig);

Expand All @@ -88,27 +81,13 @@ export default class EppoClient implements IEppoClient {
isShardInRange(shard, variation.shardRange),
).value;

assignmentHooks?.onPostAssignment(experimentKey, subjectKey, assignedVariation);

// Finally, log assignment and return assignment.
this.logAssignment(experimentKey, assignedVariation, subjectKey, subjectAttributes);
return assignedVariation;
}

async getAssignmentWithHooks(
subjectKey: string,
experimentKey: string,
assignmentHooks: IAssignmentHooks,
subjectAttributes = {},
): Promise<string> {
let assignment = await assignmentHooks?.onPreAssignment(subjectKey);
if (assignment == null) {
assignment = this.getAssignment(subjectKey, experimentKey, subjectAttributes);
}

assignmentHooks?.onPostAssignment(assignment);

return assignment;
}

public setLogger(logger: IAssignmentLogger) {
this.assignmentLogger = logger;
this.flushQueuedEvents(); // log any events that may have been queued while initializing
Expand Down

0 comments on commit d4c2ae7

Please sign in to comment.