Skip to content
16 changes: 11 additions & 5 deletions src/assignment-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,27 @@ export interface IAssignmentHooks {
/**
* Invoked before a subject is assigned to an experiment variation.
*
* @param experimentKey key of the experiment being assigned
* @param flagKey key of the feature flag being used for assignment
* @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(experimentKey: string, subject: string): EppoValue | null;
onPreAssignment(flagKey: string, subject: string): EppoValue | 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
* to a flag or allocation. Do not use this for logging assignments - use IAssignmentLogger instead.
* @param flagKey key of the feature flag being used for assignment
* @param subject id of subject being assigned
* @param variation the assigned variation
* @param allocationKey key of the allocation being used for assignment
* @public
*/
onPostAssignment(experimentKey: string, subject: string, variation: EppoValue | null): void;
onPostAssignment(
flagKey: string,
subject: string,
variation: EppoValue | null,
allocationKey?: string | null,
): void;
}
10 changes: 10 additions & 0 deletions src/assignment-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@
* @public
*/
export interface IAssignmentEvent {
/**
* An Eppo allocation key
*/
allocation: string;

/**
* An Eppo experiment key
*/
experiment: string;

/**
* An Eppo feature flag key
*/
featureFlag: string;

/**
* The assigned variation
*/
Expand Down
63 changes: 31 additions & 32 deletions src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ describe('EppoClient E2E test', () => {
mock.teardown();
});

const experimentName = 'mock-experiment';
const flagKey = 'mock-experiment';

const mockExperimentConfig = {
name: experimentName,
name: flagKey,
enabled: true,
subjectShards: 100,
overrides: {},
Expand Down Expand Up @@ -132,14 +132,14 @@ describe('EppoClient E2E test', () => {

describe('setLogger', () => {
beforeAll(() => {
storage.setEntries({ [experimentName]: mockExperimentConfig });
storage.setEntries({ [flagKey]: mockExperimentConfig });
});

it('Invokes logger for queued events', () => {
const mockLogger = td.object<IAssignmentLogger>();

const client = new EppoClient(storage);
client.getAssignment('subject-to-be-logged', experimentName);
client.getAssignment('subject-to-be-logged', flagKey);
client.setLogger(mockLogger);

expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
Expand All @@ -153,7 +153,7 @@ describe('EppoClient E2E test', () => {

const client = new EppoClient(storage);

client.getAssignment('subject-to-be-logged', experimentName);
client.getAssignment('subject-to-be-logged', flagKey);
client.setLogger(mockLogger);
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);

Expand All @@ -166,7 +166,7 @@ describe('EppoClient E2E test', () => {

const client = new EppoClient(storage);
for (let i = 0; i < MAX_EVENT_QUEUE_SIZE + 100; i++) {
client.getAssignment(`subject-to-be-logged-${i}`, experimentName);
client.getAssignment(`subject-to-be-logged-${i}`, flagKey);
}
client.setLogger(mockLogger);
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(MAX_EVENT_QUEUE_SIZE);
Expand Down Expand Up @@ -218,7 +218,7 @@ describe('EppoClient E2E test', () => {

it('returns subject from overrides when enabled is true', () => {
window.localStorage.setItem(
experimentName,
flagKey,
JSON.stringify({
...mockExperimentConfig,
typedOverrides: {
Expand All @@ -227,7 +227,7 @@ describe('EppoClient E2E test', () => {
}),
);
const client = new EppoClient(storage);
const assignment = client.getAssignment('subject-10', experimentName);
const assignment = client.getAssignment('subject-10', flagKey);
expect(assignment).toEqual('control');
});

Expand All @@ -240,22 +240,22 @@ describe('EppoClient E2E test', () => {
},
};

storage.setEntries({ [experimentName]: entry });
storage.setEntries({ [flagKey]: entry });

const client = new EppoClient(storage);
const assignment = client.getAssignment('subject-10', experimentName);
const assignment = client.getAssignment('subject-10', flagKey);
expect(assignment).toEqual('control');
});

it('logs variation assignment', () => {
const mockLogger = td.object<IAssignmentLogger>();

storage.setEntries({ [experimentName]: mockExperimentConfig });
storage.setEntries({ [flagKey]: mockExperimentConfig });
const client = new EppoClient(storage);
client.setLogger(mockLogger);

const subjectAttributes = { foo: 3 };
const assignment = client.getAssignment('subject-10', experimentName, subjectAttributes);
const assignment = client.getAssignment('subject-10', flagKey, subjectAttributes);

expect(assignment).toEqual('control');
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
Expand All @@ -266,12 +266,12 @@ describe('EppoClient E2E test', () => {
const mockLogger = td.object<IAssignmentLogger>();
td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow(new Error('logging error'));

storage.setEntries({ [experimentName]: mockExperimentConfig });
storage.setEntries({ [flagKey]: mockExperimentConfig });
const client = new EppoClient(storage);
client.setLogger(mockLogger);

const subjectAttributes = { foo: 3 };
const assignment = client.getAssignment('subject-10', experimentName, subjectAttributes);
const assignment = client.getAssignment('subject-10', flagKey, subjectAttributes);

expect(assignment).toEqual('control');
});
Expand All @@ -293,14 +293,14 @@ describe('EppoClient E2E test', () => {
],
};

storage.setEntries({ [experimentName]: entry });
storage.setEntries({ [flagKey]: entry });

const client = new EppoClient(storage);
let assignment = client.getAssignment('subject-10', experimentName, { appVersion: 9 });
let assignment = client.getAssignment('subject-10', flagKey, { appVersion: 9 });
expect(assignment).toEqual(null);
assignment = client.getAssignment('subject-10', experimentName);
assignment = client.getAssignment('subject-10', flagKey);
expect(assignment).toEqual(null);
assignment = client.getAssignment('subject-10', experimentName, { appVersion: 11 });
assignment = client.getAssignment('subject-10', flagKey, { appVersion: 11 });
expect(assignment).toEqual('control');
});

Expand Down Expand Up @@ -376,23 +376,23 @@ describe('EppoClient E2E test', () => {
let client: EppoClient;

beforeAll(() => {
storage.setEntries({ [experimentName]: mockExperimentConfig });
storage.setEntries({ [flagKey]: mockExperimentConfig });
client = new EppoClient(storage);
});

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

it('overrides returned assignment', async () => {
const variation = await client.getAssignment(
'subject-identifer',
experimentName,
flagKey,
{},
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -402,9 +402,9 @@ describe('EppoClient E2E test', () => {

// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPostAssignment(
experimentKey: string,
subject: string,
variation: EppoValue | null,
experimentKey: string, // eslint-disable-line @typescript-eslint/no-unused-vars
subject: string, // eslint-disable-line @typescript-eslint/no-unused-vars
variation: EppoValue | null, // eslint-disable-line @typescript-eslint/no-unused-vars
): void {
// no-op
},
Expand All @@ -417,19 +417,18 @@ describe('EppoClient E2E test', () => {
it('uses regular assignment logic if onPreAssignment returns null', async () => {
const variation = await client.getAssignment(
'subject-identifer',
experimentName,
flagKey,
{},
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPreAssignment(experimentKey: string, subject: string): EppoValue | null {
return null;
},

// eslint-disable-next-line @typescript-eslint/no-unused-vars
onPostAssignment(
experimentKey: string,
subject: string,
variation: EppoValue | null,
experimentKey: string, // eslint-disable-line @typescript-eslint/no-unused-vars
subject: string, // eslint-disable-line @typescript-eslint/no-unused-vars
variation: EppoValue | null, // eslint-disable-line @typescript-eslint/no-unused-vars
): void {
// no-op
},
Expand All @@ -444,10 +443,10 @@ describe('EppoClient E2E test', () => {
it('called with assigned variation after assignment', async () => {
const mockHooks = td.object<IAssignmentHooks>();
const subject = 'subject-identifier';
const variation = client.getAssignment(subject, experimentName, {}, mockHooks);
const variation = client.getAssignment(subject, flagKey, {}, 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(experimentName);
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(flagKey);
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[1]).toEqual(subject);
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[2]).toEqual(
EppoValue.String(variation ?? ''),
Expand Down
Loading