Skip to content

Commit 503b0cc

Browse files
committed
pass subject to assignment function
1 parent 97ca89a commit 503b0cc

15 files changed

+222
-132
lines changed

docs/js-client-sdk.attributevaluetype.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

docs/js-client-sdk.iclientconfig.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,4 @@ export interface IClientConfig
1818
| --- | --- | --- |
1919
| [apiKey](./js-client-sdk.iclientconfig.apikey.md) | string | Eppo API key |
2020
| [baseUrl?](./js-client-sdk.iclientconfig.baseurl.md) | string | <i>(Optional)</i> Base URL of the Eppo API. Clients should use the default setting in most cases. |
21-
| [subjectAttributes?](./js-client-sdk.iclientconfig.subjectattributes.md) | Record&lt;string, [AttributeValueType](./js-client-sdk.attributevaluetype.md)<!-- -->&gt; | <i>(Optional)</i> 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. |
22-
| [subjectKey](./js-client-sdk.iclientconfig.subjectkey.md) | string | An identifier of the experiment subject, for example a user ID. |
2321

docs/js-client-sdk.iclientconfig.subjectattributes.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

docs/js-client-sdk.iclientconfig.subjectkey.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

docs/js-client-sdk.ieppoclient.getassignment.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ Maps a subject to a variation for a given experiment.
99
<b>Signature:</b>
1010

1111
```typescript
12-
getAssignment(experimentKey: string): string;
12+
getAssignment(subjectKey: string, experimentKey: string, subjectAttributes?: Record<string, any>): string;
1313
```
1414

1515
## Parameters
1616

1717
| Parameter | Type | Description |
1818
| --- | --- | --- |
19+
| subjectKey | string | an identifier of the experiment subject, for example a user ID. |
1920
| experimentKey | string | experiment identifier |
21+
| subjectAttributes | Record&lt;string, any&gt; | <i>(Optional)</i> 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. |
2022

2123
<b>Returns:</b>
2224

docs/js-client-sdk.ieppoclient.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ export interface IEppoClient
1616

1717
| Method | Description |
1818
| --- | --- |
19-
| [getAssignment(experimentKey)](./js-client-sdk.ieppoclient.getassignment.md) | Maps a subject to a variation for a given experiment. |
19+
| [getAssignment(subjectKey, experimentKey, subjectAttributes)](./js-client-sdk.ieppoclient.getassignment.md) | Maps a subject to a variation for a given experiment. |
2020

docs/js-client-sdk.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,3 @@
1818
| [IClientConfig](./js-client-sdk.iclientconfig.md) | Configuration used for initializing the Eppo client |
1919
| [IEppoClient](./js-client-sdk.ieppoclient.md) | Client for assigning experiment variations. |
2020

21-
## Type Aliases
22-
23-
| Type Alias | Description |
24-
| --- | --- |
25-
| [AttributeValueType](./js-client-sdk.attributevaluetype.md) | |
26-

js-client-sdk.api.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@
44
55
```ts
66

7-
// @public (undocumented)
8-
export type AttributeValueType = string | number;
9-
107
// @public
118
export function getInstance(): IEppoClient;
129

1310
// @public
1411
export interface IClientConfig {
1512
apiKey: string;
1613
baseUrl?: string;
17-
subjectAttributes?: Record<string, AttributeValueType>;
18-
subjectKey: string;
1914
}
2015

2116
// @public
2217
export interface IEppoClient {
23-
getAssignment(experimentKey: string): string;
18+
getAssignment(subjectKey: string, experimentKey: string, subjectAttributes?: Record<string, any>): string;
2419
}
2520

2621
// @public

src/eppo-client.spec.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import ExperimentConfigurationRequestor from './experiment/experiment-configurat
1212
import { IVariation } from './experiment/variation';
1313
import { OperatorType } from './rule';
1414

15-
import { init } from '.';
15+
import { getInstance, init } from '.';
1616

1717
describe('EppoClient E2E test', () => {
18-
beforeAll(() => {
18+
beforeAll(async () => {
1919
window.sessionStorage.clear();
2020
mock.setup();
2121
mock.get(/randomized_assignment\/config*/, (_req, res) => {
@@ -34,6 +34,7 @@ describe('EppoClient E2E test', () => {
3434
});
3535
return res.status(200).body(JSON.stringify({ experiments: assignmentConfig }));
3636
});
37+
await init({ apiKey: 'dummy', baseUrl: 'http://127.0.0.1:4000' });
3738
});
3839

3940
afterAll(() => {
@@ -51,7 +52,7 @@ describe('EppoClient E2E test', () => {
5152
expectedAssignments,
5253
}: IAssignmentTestCase) => {
5354
console.log(`---- Test Case for ${experiment} Experiment ----`);
54-
const assignments = await getAssignments(subjects, experiment);
55+
const assignments = getAssignments(subjects, experiment);
5556
// verify the assingments don't change across test runs (deterministic)
5657
expect(assignments).toEqual(expectedAssignments);
5758
const expectedVariationSplitPercentage = percentExposure / variations.length;
@@ -99,8 +100,8 @@ describe('EppoClient E2E test', () => {
99100
a90ea45116d251a43da56e03d3dd7275: 'variant-2',
100101
},
101102
});
102-
const client = new EppoClient('subject-1', mockConfigRequestor);
103-
const assignment = client.getAssignment(experiment);
103+
const client = new EppoClient(mockConfigRequestor);
104+
const assignment = client.getAssignment('subject-1', experiment);
104105
expect(assignment).toEqual('variant-2');
105106
});
106107

@@ -141,14 +142,12 @@ describe('EppoClient E2E test', () => {
141142
},
142143
],
143144
});
144-
let client = new EppoClient('subject-1', mockConfigRequestor, { appVersion: 9 });
145-
let assignment = client.getAssignment(experiment);
145+
const client = new EppoClient(mockConfigRequestor);
146+
let assignment = client.getAssignment('subject-1', experiment, { appVersion: 9 });
146147
expect(assignment).toEqual(null);
147-
client = new EppoClient('subject-1', mockConfigRequestor);
148-
assignment = client.getAssignment(experiment);
148+
assignment = client.getAssignment('subject-1', experiment);
149149
expect(assignment).toEqual(null);
150-
client = new EppoClient('subject-1', mockConfigRequestor, { appVersion: 11 });
151-
assignment = client.getAssignment(experiment);
150+
assignment = client.getAssignment('subject-1', experiment, { appVersion: 11 });
152151
expect(assignment).toEqual('control');
153152
});
154153

@@ -172,17 +171,10 @@ describe('EppoClient E2E test', () => {
172171
expect(percentage).toBeLessThanOrEqual(expectedPercentage + 0.05);
173172
}
174173

175-
async function getAssignments(subjects: string[], experiment: string): Promise<string[]> {
176-
const assignments: string[] = [];
177-
for (const subjectKey of subjects) {
178-
const client = await init({
179-
apiKey: 'dummy',
180-
baseUrl: 'http://127.0.0.1:4000',
181-
subjectKey,
182-
});
183-
const assignment = client.getAssignment(experiment);
184-
assignments.push(assignment);
185-
}
186-
return assignments;
174+
function getAssignments(subjects: string[], experiment: string): string[] {
175+
const client = getInstance();
176+
return subjects.map((subjectKey) => {
177+
return client.getAssignment(subjectKey, experiment);
178+
});
187179
}
188180
});

src/eppo-client.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createHash } from 'crypto';
22

33
import { IExperimentConfiguration } from './experiment/experiment-configuration';
44
import ExperimentConfigurationRequestor from './experiment/experiment-configuration-requestor';
5-
import { Rule } from './experiment/rule';
5+
import { Rule } from './rule';
66
import { matchesAnyRule } from './rule_evaluator';
77
import { getShard, isShardInRange } from './shard';
88
import { validateNotBlank } from './validation';
@@ -15,48 +15,57 @@ export interface IEppoClient {
1515
/**
1616
* Maps a subject to a variation for a given experiment.
1717
*
18+
* @param subjectKey an identifier of the experiment subject, for example a user ID.
1819
* @param experimentKey experiment identifier
20+
* @param subjectAttributes optional attributes associated with the subject, for example name and email.
21+
* The subject attributes are used for evaluating any targeting rules tied to the experiment.
1922
* @returns a variation value if the subject is part of the experiment sample, otherwise null
2023
* @public
2124
*/
22-
getAssignment(experimentKey: string): string;
25+
getAssignment(
26+
subjectKey: string,
27+
experimentKey: string,
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
subjectAttributes?: Record<string, any>,
30+
): string;
2331
}
2432

2533
export default class EppoClient implements IEppoClient {
26-
constructor(
27-
private subjectKey: string,
28-
private configurationRequestor: ExperimentConfigurationRequestor,
29-
private subjectAttributes = {},
30-
) {}
34+
constructor(private configurationRequestor: ExperimentConfigurationRequestor) {}
3135

32-
getAssignment(experimentKey: string): string {
36+
getAssignment(subjectKey: string, experimentKey: string, subjectAttributes = {}): string {
37+
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
3338
validateNotBlank(experimentKey, 'Invalid argument: experimentKey cannot be blank');
3439
const experimentConfig = this.configurationRequestor.getConfiguration(experimentKey);
3540
if (
3641
!experimentConfig?.enabled ||
37-
!this.subjectAttributesSatisfyRules(experimentConfig.rules) ||
38-
!this.isInExperimentSample(experimentKey, experimentConfig)
42+
!this.subjectAttributesSatisfyRules(subjectAttributes, experimentConfig.rules) ||
43+
!this.isInExperimentSample(subjectKey, experimentKey, experimentConfig)
3944
) {
4045
return null;
4146
}
42-
const override = this.getSubjectVariationOverride(experimentConfig);
47+
const override = this.getSubjectVariationOverride(subjectKey, experimentConfig);
4348
if (override) {
4449
return override;
4550
}
4651
const { variations, subjectShards } = experimentConfig;
47-
const shard = getShard(`assignment-${this.subjectKey}-${experimentKey}`, subjectShards);
52+
const shard = getShard(`assignment-${subjectKey}-${experimentKey}`, subjectShards);
4853
return variations.find((variation) => isShardInRange(shard, variation.shardRange)).name;
4954
}
5055

51-
private subjectAttributesSatisfyRules(rules?: Rule[]) {
56+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
57+
private subjectAttributesSatisfyRules(subjectAttributes?: Record<string, any>, rules?: Rule[]) {
5258
if (!rules || rules.length === 0) {
5359
return true;
5460
}
55-
return matchesAnyRule(this.subjectAttributes || {}, rules);
61+
return matchesAnyRule(subjectAttributes || {}, rules);
5662
}
5763

58-
private getSubjectVariationOverride(experimentConfig: IExperimentConfiguration): string {
59-
const subjectHash = createHash('md5').update(this.subjectKey).digest('hex');
64+
private getSubjectVariationOverride(
65+
subjectKey: string,
66+
experimentConfig: IExperimentConfiguration,
67+
): string {
68+
const subjectHash = createHash('md5').update(subjectKey).digest('hex');
6069
return experimentConfig.overrides[subjectHash];
6170
}
6271

@@ -66,11 +75,12 @@ export default class EppoClient implements IEppoClient {
6675
* Given a hash function output (bucket), check whether the bucket is between 0 and exposure_percent * total_buckets.
6776
*/
6877
private isInExperimentSample(
78+
subjectKey: string,
6979
experimentKey: string,
7080
experimentConfig: IExperimentConfiguration,
7181
): boolean {
7282
const { percentExposure, subjectShards } = experimentConfig;
73-
const shard = getShard(`exposure-${this.subjectKey}-${experimentKey}`, subjectShards);
83+
const shard = getShard(`exposure-${subjectKey}-${experimentKey}`, subjectShards);
7484
return shard <= percentExposure * subjectShards;
7585
}
7686
}

0 commit comments

Comments
 (0)