Skip to content

Commit e255b1c

Browse files
authored
feat: add tracks assignment and exposure (#55)
* feat: add tracks assignment and exposure * docs: add comments * feat: bump experiment-core version
1 parent c73f364 commit e255b1c

File tree

5 files changed

+123
-52
lines changed

5 files changed

+123
-52
lines changed

packages/node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"dependencies": {
3434
"@amplitude/analytics-node": "^1.3.4",
3535
"@amplitude/analytics-types": "^1.3.1",
36-
"@amplitude/experiment-core": "^0.7.2",
36+
"@amplitude/experiment-core": "0.12.0",
3737
"eventsource": "^2.0.2"
3838
}
3939
}

packages/node/src/remote/client.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
FetchError,
44
SdkEvaluationApi,
55
} from '@amplitude/experiment-core';
6+
import { GetVariantsOptions } from '@amplitude/experiment-core/dist/types/src/api/evaluation-api';
67

78
import { version as PACKAGE_VERSION } from '../../gen/version';
89
import { FetchHttpClient, WrapperClient } from '../transport/http';
@@ -113,10 +114,23 @@ export class RemoteEvaluationClient {
113114
options?: FetchOptions,
114115
): Promise<Record<string, Variant>> {
115116
const userContext = this.addContext(user || {});
116-
const results = await this.evaluationApi.getVariants(userContext, {
117+
const getVariantsOptions: GetVariantsOptions = {
117118
flagKeys: options?.flagKeys,
118119
timeoutMillis: timeoutMillis,
119-
});
120+
};
121+
if (options?.tracksAssignment) {
122+
getVariantsOptions.trackingOption = options?.tracksAssignment
123+
? 'track'
124+
: 'no-track';
125+
}
126+
if (options?.tracksExposure) {
127+
(getVariantsOptions as any).exposureTrackingOption =
128+
options?.tracksExposure ? 'track' : 'no-track';
129+
}
130+
const results = await this.evaluationApi.getVariants(
131+
userContext,
132+
getVariantsOptions,
133+
);
120134
this.logger.debug('[Experiment] Fetched variants: ', results);
121135
return evaluationVariantsToVariants(results);
122136
}

packages/node/src/types/fetch.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,16 @@ export type FetchOptions = {
66
* Specific flag keys to evaluate and set variants for.
77
*/
88
flagKeys?: string[];
9+
10+
/**
11+
* Whether to track exposure events for the request.
12+
* If not provided, the default is not to track exposure events.
13+
*/
14+
tracksExposure?: boolean;
15+
16+
/**
17+
* Whether to track assignment events for the request.
18+
* If not provided, the default is to track assignment events.
19+
*/
20+
tracksAssignment?: boolean;
921
};

packages/node/test/remote/client.test.ts

Lines changed: 90 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,104 @@ const API_KEY = 'server-qz35UwzJ5akieoAdIgzM4m9MIiOLXLoz';
66

77
const testUser: ExperimentUser = { user_id: 'test_user' };
88

9-
test('ExperimentClient.fetch, success', async () => {
10-
const client = new RemoteEvaluationClient(API_KEY, {});
11-
const variants = await client.fetch(testUser);
12-
const variant = variants['sdk-ci-test'];
13-
delete variant.metadata;
14-
expect(variant).toEqual({ key: 'on', value: 'on', payload: 'payload' });
15-
});
9+
describe('ExperimentClient.fetch', () => {
10+
beforeEach(() => {
11+
jest.clearAllMocks();
12+
jest.resetAllMocks();
13+
jest.restoreAllMocks();
14+
});
1615

17-
test('ExperimentClient.fetch, no retries, timeout failure', async () => {
18-
const client = new RemoteEvaluationClient(API_KEY, {
19-
fetchRetries: 0,
20-
fetchTimeoutMillis: 0,
16+
test('ExperimentClient.fetch, success', async () => {
17+
const client = new RemoteEvaluationClient(API_KEY, {});
18+
const variants = await client.fetch(testUser);
19+
const variant = variants['sdk-ci-test'];
20+
delete variant.metadata;
21+
expect(variant).toEqual({ key: 'on', value: 'on', payload: 'payload' });
2122
});
22-
const variants = await client.fetch(testUser);
23-
expect(variants).toEqual({});
24-
});
2523

26-
test('ExperimentClient.fetch, no retries, timeout failure, retry success', async () => {
27-
const client = new RemoteEvaluationClient(API_KEY, {
28-
fetchRetries: 1,
29-
fetchTimeoutMillis: 0,
24+
test('ExperimentClient.fetch, no retries, timeout failure', async () => {
25+
const client = new RemoteEvaluationClient(API_KEY, {
26+
fetchRetries: 0,
27+
fetchTimeoutMillis: 0,
28+
});
29+
const variants = await client.fetch(testUser);
30+
expect(variants).toEqual({});
3031
});
31-
const variants = await client.fetch(testUser);
32-
const variant = variants['sdk-ci-test'];
33-
delete variant.metadata;
34-
expect(variant).toEqual({ key: 'on', value: 'on', payload: 'payload' });
35-
});
3632

37-
test('ExperimentClient.fetch, retry once, timeout first then succeed with 0 backoff', async () => {
38-
const client = new RemoteEvaluationClient(API_KEY, {
39-
fetchTimeoutMillis: 0,
40-
fetchRetries: 1,
41-
fetchRetryBackoffMinMillis: 0,
42-
fetchRetryTimeoutMillis: 10_000,
33+
test('ExperimentClient.fetch, no retries, timeout failure, retry success', async () => {
34+
const client = new RemoteEvaluationClient(API_KEY, {
35+
fetchRetries: 1,
36+
fetchTimeoutMillis: 0,
37+
});
38+
const variants = await client.fetch(testUser);
39+
const variant = variants['sdk-ci-test'];
40+
delete variant.metadata;
41+
expect(variant).toEqual({ key: 'on', value: 'on', payload: 'payload' });
4342
});
44-
const variants = await client.fetch(testUser);
45-
const variant = variants['sdk-ci-test'];
46-
delete variant.metadata;
47-
expect(variant).toEqual({ key: 'on', value: 'on', payload: 'payload' });
48-
});
4943

50-
test('ExperimentClient.fetch, v1 off returns undefined', async () => {
51-
const client = new RemoteEvaluationClient(API_KEY, {});
52-
const variant = (await client.fetch({}))['sdk-ci-test'];
53-
expect(variant).toBeUndefined();
54-
});
44+
test('ExperimentClient.fetch, retry once, timeout first then succeed with 0 backoff', async () => {
45+
const client = new RemoteEvaluationClient(API_KEY, {
46+
fetchTimeoutMillis: 0,
47+
fetchRetries: 1,
48+
fetchRetryBackoffMinMillis: 0,
49+
fetchRetryTimeoutMillis: 10_000,
50+
});
51+
const variants = await client.fetch(testUser);
52+
const variant = variants['sdk-ci-test'];
53+
delete variant.metadata;
54+
expect(variant).toEqual({ key: 'on', value: 'on', payload: 'payload' });
55+
});
56+
57+
test('ExperimentClient.fetch, v1 off returns undefined', async () => {
58+
const client = new RemoteEvaluationClient(API_KEY, {});
59+
const variant = (await client.fetch({}))['sdk-ci-test'];
60+
expect(variant).toBeUndefined();
61+
});
62+
63+
test('ExperimentClient.fetch, v2 off returns default variant', async () => {
64+
const client = new RemoteEvaluationClient(API_KEY, {});
65+
const variant = (await client.fetchV2({}))['sdk-ci-test'];
66+
expect(variant.key).toEqual('off');
67+
expect(variant.value).toBeUndefined();
68+
expect(variant.metadata.default).toEqual(true);
69+
});
70+
71+
test('ExperimentClient.fetch, v2 no tracksAssignment and no tracksExposure', async () => {
72+
const client = new RemoteEvaluationClient(API_KEY, {});
73+
const getVariantsSpy = jest.spyOn(
74+
(client as any).evaluationApi,
75+
'getVariants',
76+
);
77+
const variants = await client.fetchV2(testUser);
78+
expect(variants['sdk-ci-test'].key).toEqual('on');
79+
expect(getVariantsSpy).toHaveBeenCalledWith(
80+
expect.objectContaining(testUser),
81+
expect.not.objectContaining({
82+
trackingOption: expect.any(String),
83+
exposureTrackingOption: expect.any(String),
84+
}),
85+
);
86+
});
5587

56-
test('ExperimentClient.fetch, v2 off returns default variant', async () => {
57-
const client = new RemoteEvaluationClient(API_KEY, {});
58-
const variant = (await client.fetchV2({}))['sdk-ci-test'];
59-
expect(variant.key).toEqual('off');
60-
expect(variant.value).toBeUndefined();
61-
expect(variant.metadata.default).toEqual(true);
88+
test('ExperimentClient.fetch, v2 tracksAssignment and tracksExposure', async () => {
89+
const client = new RemoteEvaluationClient(API_KEY, {});
90+
const getVariantsSpy = jest.spyOn(
91+
(client as any).evaluationApi,
92+
'getVariants',
93+
);
94+
const variants = await client.fetchV2(testUser, {
95+
tracksAssignment: true,
96+
tracksExposure: true,
97+
});
98+
expect(variants['sdk-ci-test'].key).toEqual('on');
99+
expect(getVariantsSpy).toHaveBeenCalledWith(
100+
expect.objectContaining(testUser),
101+
expect.objectContaining({
102+
trackingOption: 'track',
103+
exposureTrackingOption: 'track',
104+
}),
105+
);
106+
});
62107
});
63108

64109
describe('ExperimentClient.fetch, retry with different response codes', () => {

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@
3636
resolved "https://registry.yarnpkg.com/@amplitude/analytics-types/-/analytics-types-1.3.3.tgz#c7b2a21e6ab0eb1670cce4d03127b62c373c6ed4"
3737
integrity sha512-V4/h+izhG7NyVfIva1uhe6bToI/l5n+UnEomL3KEO9DkFoKiOG7KmXo/fmzfU6UmD1bUEWmy//hUFF16BfrEww==
3838

39-
"@amplitude/experiment-core@^0.7.2":
40-
version "0.7.2"
41-
resolved "https://registry.yarnpkg.com/@amplitude/experiment-core/-/experiment-core-0.7.2.tgz#f94219d68d86322e8d580c8fbe0672dcd29f86bb"
42-
integrity sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA==
39+
"@amplitude/experiment-core@0.12.0":
40+
version "0.12.0"
41+
resolved "https://registry.yarnpkg.com/@amplitude/experiment-core/-/experiment-core-0.12.0.tgz#9a2ee4c054da6dd629bbabbcbbf20188cdece3dd"
42+
integrity sha512-EiLLxcyJD8T3GFsMPxBfWx9n9fBw6rC0RJwccPXLzResE0HnGZZpVWF86ZndnYmEMD1lUUjWi41N1ymEzodI5w==
4343
dependencies:
4444
js-base64 "^3.7.5"
4545

0 commit comments

Comments
 (0)