Skip to content

Commit e32b9e1

Browse files
committed
up
1 parent 7d827e9 commit e32b9e1

File tree

2 files changed

+157
-59
lines changed

2 files changed

+157
-59
lines changed

lib/core/decision_service/index.spec.ts

Lines changed: 150 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,39 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { describe, it, expect, vi } from 'vitest';
16+
import { describe, it, expect, vi, MockInstance, beforeEach } from 'vitest';
1717
import { DecisionService } from '.';
1818
import { getMockLogger } from '../../tests/mock/mock_logger';
19+
import OptimizelyUserContext from '../../optimizely_user_context';
20+
import { bucket } from '../bucketer';
21+
import { getTestProjectConfig, getTestProjectConfigWithFeatures } from '../../tests/test_data';
22+
import { createProjectConfig, ProjectConfig } from '../../project_config/project_config';
23+
import { Experiment } from '../../shared_types';
24+
import { CONTROL_ATTRIBUTES } from '../../utils/enums';
25+
26+
import {
27+
USER_HAS_NO_FORCED_VARIATION,
28+
VALID_BUCKETING_ID,
29+
SAVED_USER_VARIATION,
30+
SAVED_VARIATION_NOT_FOUND,
31+
} from 'log_message';
32+
33+
import {
34+
EXPERIMENT_NOT_RUNNING,
35+
RETURNING_STORED_VARIATION,
36+
USER_NOT_IN_EXPERIMENT,
37+
USER_FORCED_IN_VARIATION,
38+
EVALUATING_AUDIENCES_COMBINED,
39+
AUDIENCE_EVALUATION_RESULT_COMBINED,
40+
USER_IN_ROLLOUT,
41+
USER_NOT_IN_ROLLOUT,
42+
FEATURE_HAS_NO_EXPERIMENTS,
43+
USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE,
44+
USER_NOT_BUCKETED_INTO_TARGETING_RULE,
45+
USER_BUCKETED_INTO_TARGETING_RULE,
46+
NO_ROLLOUT_EXISTS,
47+
USER_MEETS_CONDITIONS_FOR_TARGETING_RULE,
48+
} from '../decision_service/index';
1949

2050
type MockLogger = ReturnType<typeof getMockLogger>;
2151

@@ -35,7 +65,7 @@ type DecisionServiceInstance = {
3565
decisionService: DecisionService;
3666
}
3767

38-
const getDecisionService = (opt: DecisionServiceInstanceOpt): DecisionServiceInstance => {
68+
const getDecisionService = (opt: DecisionServiceInstanceOpt = {}): DecisionServiceInstance => {
3969
const logger = opt.logger ? getMockLogger() : undefined;
4070
const userProfileService = opt.userProfileService ? {
4171
lookup: vi.fn(),
@@ -55,70 +85,144 @@ const getDecisionService = (opt: DecisionServiceInstanceOpt): DecisionServiceIns
5585
};
5686
};
5787

88+
const mockBucket: MockInstance<typeof bucket> = vi.hoisted(() => vi.fn());
89+
90+
vi.mock('../bucketer', () => ({
91+
bucket: mockBucket,
92+
}));
93+
5894
const testGetVariationWithoutUserProfileService = (decisonService: DecisionServiceInstance) => {
5995

6096
}
97+
const cloneDeep = (d: any) => JSON.parse(JSON.stringify(d));
98+
99+
const testData = getTestProjectConfig();
100+
const testDataWithFeatures = getTestProjectConfigWithFeatures();
101+
102+
const verifyBucketCall = (
103+
call: number,
104+
projectConfig: ProjectConfig,
105+
experiment: Experiment,
106+
user: OptimizelyUserContext,
107+
) => {
108+
const {
109+
experimentId,
110+
experimentKey,
111+
userId,
112+
trafficAllocationConfig,
113+
experimentKeyMap,
114+
experimentIdMap,
115+
groupIdMap,
116+
variationIdMap,
117+
bucketingId,
118+
} = mockBucket.mock.calls[call][0];
119+
expect(experimentId).toBe(experiment.id);
120+
expect(experimentKey).toBe(experiment.key);
121+
expect(userId).toBe(user.getUserId());
122+
expect(trafficAllocationConfig).toBe(experiment.trafficAllocation);
123+
expect(experimentKeyMap).toBe(projectConfig.experimentKeyMap);
124+
expect(experimentIdMap).toBe(projectConfig.experimentIdMap);
125+
expect(groupIdMap).toBe(projectConfig.groupIdMap);
126+
expect(variationIdMap).toBe(projectConfig.variationIdMap);
127+
expect(bucketingId).toBe(user.getAttributes()[CONTROL_ATTRIBUTES.BUCKETING_ID] || user.getUserId());
128+
};
61129

62130
describe('DecisionService', () => {
63131
describe('getVariation', function() {
64-
it('should return the correct variation for the given experiment key and user ID for a running experiment', () => {
65-
user = new OptimizelyUserContext({
66-
shouldIdentifyUser: false,
67-
optimizely: {},
132+
beforeEach(() => {
133+
mockBucket.mockClear();
134+
});
135+
136+
it('should return the correct variation from bucketer for the given experiment key and user ID for a running experiment', () => {
137+
const user = new OptimizelyUserContext({
138+
optimizely: {} as any,
68139
userId: 'tester'
69140
});
70-
var fakeDecisionResponse = {
141+
142+
const config = createProjectConfig(cloneDeep(testData));
143+
144+
const fakeDecisionResponse = {
71145
result: '111128',
72146
reasons: [],
73147
};
74-
experiment = configObj.experimentIdMap['111127'];
75-
bucketerStub.returns(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data`
76-
assert.strictEqual(
77-
'control',
78-
decisionServiceInstance.getVariation(configObj, experiment, user).result
79-
);
80-
sinon.assert.calledOnce(bucketerStub);
148+
149+
const experiment = config.experimentIdMap['111127'];
150+
151+
mockBucket.mockReturnValue(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data`
152+
153+
const { decisionService } = getDecisionService();
154+
155+
const variation = decisionService.getVariation(config, experiment, user);
156+
157+
expect(variation.result).toBe('control');
158+
159+
expect(mockBucket).toHaveBeenCalledTimes(1);
160+
verifyBucketCall(0, config, experiment, user);
81161
});
82162

83-
// it('should return the whitelisted variation if the user is whitelisted', function() {
84-
// user = new OptimizelyUserContext({
85-
// shouldIdentifyUser: false,
86-
// optimizely: {},
87-
// userId: 'user2'
88-
// });
89-
// experiment = configObj.experimentIdMap['122227'];
90-
// assert.strictEqual(
91-
// 'variationWithAudience',
92-
// decisionServiceInstance.getVariation(configObj, experiment, user).result
93-
// );
94-
// sinon.assert.notCalled(bucketerStub);
95-
// assert.strictEqual(1, mockLogger.debug.callCount);
96-
// assert.strictEqual(1, mockLogger.info.callCount);
163+
it('should return the whitelisted variation if the user is whitelisted', function() {
164+
const user = new OptimizelyUserContext({
165+
optimizely: {} as any,
166+
userId: 'user2'
167+
});
97168

98-
// assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user2']);
169+
const config = createProjectConfig(cloneDeep(testData));
99170

100-
// assert.deepEqual(mockLogger.info.args[0], [USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience']);
101-
// });
171+
const experiment = config.experimentIdMap['122227'];
102172

103-
// it('should return null if the user does not meet audience conditions', function() {
104-
// user = new OptimizelyUserContext({
105-
// shouldIdentifyUser: false,
106-
// optimizely: {},
107-
// userId: 'user3'
108-
// });
109-
// experiment = configObj.experimentIdMap['122227'];
110-
// assert.isNull(
111-
// decisionServiceInstance.getVariation(configObj, experiment, user, { foo: 'bar' }).result
112-
// );
173+
const { decisionService, logger } = getDecisionService({ logger: true });
174+
175+
const variation = decisionService.getVariation(config, experiment, user);
113176

114-
// assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user3']);
177+
expect(variation.result).toBe('variationWithAudience');
178+
expect(mockBucket).not.toHaveBeenCalled();
179+
expect(logger?.debug).toHaveBeenCalledTimes(1);
180+
expect(logger?.info).toHaveBeenCalledTimes(1);
115181

116-
// assert.deepEqual(mockLogger.debug.args[1], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]);
182+
expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user2');
183+
expect(logger?.info).toHaveBeenNthCalledWith(1, USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience');
184+
});
117185

118-
// assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']);
186+
it('should return null if the user does not meet audience conditions', () => {
187+
const user = new OptimizelyUserContext({
188+
optimizely: {} as any,
189+
userId: 'user2'
190+
});
119191

120-
// assert.deepEqual(mockLogger.info.args[1], [USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences']);
121-
// });
192+
const config = createProjectConfig(cloneDeep(testData));
193+
194+
const experiment = config.experimentIdMap['122227'];
195+
196+
const { decisionService, logger } = getDecisionService({ logger: true });
197+
198+
const variation = decisionService.getVariation(config, experiment, user);
199+
200+
expect(variation.result).toBe('variationWithAudience');
201+
expect(mockBucket).not.toHaveBeenCalled();
202+
expect(logger?.debug).toHaveBeenCalledTimes(1);
203+
expect(logger?.info).toHaveBeenCalledTimes(1);
204+
205+
expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user2');
206+
expect(logger?.info).toHaveBeenNthCalledWith(1, USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience');
207+
208+
user = new OptimizelyUserContext({
209+
shouldIdentifyUser: false,
210+
optimizely: {},
211+
userId: 'user3'
212+
});
213+
experiment = configObj.experimentIdMap['122227'];
214+
assert.isNull(
215+
decisionServiceInstance.getVariation(configObj, experiment, user, { foo: 'bar' }).result
216+
);
217+
218+
assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user3']);
219+
220+
assert.deepEqual(mockLogger.debug.args[1], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]);
221+
222+
assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']);
223+
224+
assert.deepEqual(mockLogger.info.args[1], [USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences']);
225+
});
122226

123227
// it('should return null if the experiment is not running', function() {
124228
// user = new OptimizelyUserContext({

lib/core/decision_service/index.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,18 +170,22 @@ export class DecisionService {
170170
): DecisionResponse<string | null> {
171171
const userId = user.getUserId();
172172
const attributes = user.getAttributes();
173+
173174
// by default, the bucketing ID should be the user ID
174175
const bucketingId = this.getBucketingId(userId, attributes);
175-
const decideReasons: (string | number)[][] = [];
176176
const experimentKey = experiment.key;
177-
if (!this.checkIfExperimentIsActive(configObj, experimentKey)) {
177+
178+
const decideReasons: (string | number)[][] = [];
179+
180+
if (!isActive(configObj, experimentKey)) {
178181
this.logger?.info(EXPERIMENT_NOT_RUNNING, experimentKey);
179182
decideReasons.push([EXPERIMENT_NOT_RUNNING, experimentKey]);
180183
return {
181184
result: null,
182185
reasons: decideReasons,
183186
};
184187
}
188+
185189
const decisionForcedVariation = this.getForcedVariation(configObj, experimentKey, userId);
186190
decideReasons.push(...decisionForcedVariation.reasons);
187191
const forcedVariationKey = decisionForcedVariation.result;
@@ -192,6 +196,7 @@ export class DecisionService {
192196
reasons: decideReasons,
193197
};
194198
}
199+
195200
const decisionWhitelistedVariation = this.getWhitelistedVariation(experiment, userId);
196201
decideReasons.push(...decisionWhitelistedVariation.reasons);
197202
let variation = decisionWhitelistedVariation.result;
@@ -202,7 +207,6 @@ export class DecisionService {
202207
};
203208
}
204209

205-
206210
// check for sticky bucketing if decide options do not include shouldIgnoreUPS
207211
if (!shouldIgnoreUPS) {
208212
variation = this.getStoredVariation(configObj, experiment, userId, userProfileTracker.userProfile);
@@ -349,16 +353,6 @@ export class DecisionService {
349353
return { ...userProfile.experiment_bucket_map, ...attributeExperimentBucketMap as any };
350354
}
351355

352-
/**
353-
* Checks whether the experiment is running
354-
* @param {ProjectConfig} configObj The parsed project configuration object
355-
* @param {string} experimentKey Key of experiment being validated
356-
* @return {boolean} True if experiment is running
357-
*/
358-
private checkIfExperimentIsActive(configObj: ProjectConfig, experimentKey: string): boolean {
359-
return isActive(configObj, experimentKey);
360-
}
361-
362356
/**
363357
* Checks if user is whitelisted into any variation and return that variation if so
364358
* @param {Experiment} experiment

0 commit comments

Comments
 (0)