Skip to content

Commit 697c8df

Browse files
authored
[FSSDK-12031] exclude CMAB from user profile service (#1105)
1 parent 5951829 commit 697c8df

File tree

2 files changed

+106
-52
lines changed

2 files changed

+106
-52
lines changed

lib/core/decision_service/index.spec.ts

Lines changed: 102 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import OptimizelyUserContext from '../../optimizely_user_context';
2020
import { bucket } from '../bucketer';
2121
import { getTestProjectConfig, getTestProjectConfigWithFeatures } from '../../tests/test_data';
2222
import { createProjectConfig, ProjectConfig } from '../../project_config/project_config';
23-
import { BucketerParams, Experiment, Holdout, OptimizelyDecideOption, UserAttributes, UserProfile } from '../../shared_types';
23+
import { BucketerParams, Experiment, ExperimentBucketMap, Holdout, OptimizelyDecideOption, UserAttributes, UserProfile } from '../../shared_types';
2424
import { CONTROL_ATTRIBUTES, DECISION_SOURCES } from '../../utils/enums';
2525
import { getDecisionTestDatafile } from '../../tests/decision_test_datafile';
2626
import { Value } from '../../utils/promise/operation_value';
@@ -1607,8 +1607,8 @@ describe('DecisionService', () => {
16071607
return Promise.resolve({
16081608
user_id: 'tester-1',
16091609
experiment_bucket_map: {
1610-
'2003': {
1611-
variation_id: '5001',
1610+
'2001': {
1611+
variation_id: '5002',
16121612
},
16131613
},
16141614
});
@@ -1620,7 +1620,7 @@ describe('DecisionService', () => {
16201620

16211621
mockBucket.mockImplementation((param: BucketerParams) => {
16221622
const ruleKey = param.experimentKey;
1623-
if (ruleKey == 'exp_3') {
1623+
if (ruleKey == 'exp_1') {
16241624
return { result: param.trafficAllocationConfig[0].entityId, reasons: [] }
16251625
}
16261626
return {
@@ -1629,19 +1629,14 @@ describe('DecisionService', () => {
16291629
}
16301630
});
16311631

1632-
cmabService.getDecision.mockResolvedValue({
1633-
variationId: '5003',
1634-
cmabUuid: 'uuid-test',
1635-
});
1636-
16371632
const config = createProjectConfig(getDecisionTestDatafile());
16381633

16391634
const user1 = new OptimizelyUserContext({
16401635
optimizely: {} as any,
16411636
userId: 'tester-1',
16421637
attributes: {
16431638
country: 'BD',
1644-
age: 80, // should satisfy audience condition for exp_3 which is cmab and not others
1639+
age: 22, // should satisfy audience condition for exp_1 which is not a cmab
16451640
},
16461641
});
16471642

@@ -1650,7 +1645,7 @@ describe('DecisionService', () => {
16501645
userId: 'tester-2',
16511646
attributes: {
16521647
country: 'BD',
1653-
age: 80, // should satisfy audience condition for exp_3 which is cmab and not others
1648+
age: 22, // should satisfy audience condition for exp_1 which is not a cmab
16541649
},
16551650
});
16561651

@@ -1661,12 +1656,11 @@ describe('DecisionService', () => {
16611656
const variation = (await value)[0];
16621657

16631658
expect(variation.result).toEqual({
1664-
experiment: config.experimentKeyMap['exp_3'],
1665-
variation: config.variationIdMap['5001'],
1659+
experiment: config.experimentKeyMap['exp_1'],
1660+
variation: config.variationIdMap['5002'],
16661661
decisionSource: DECISION_SOURCES.FEATURE_TEST,
16671662
});
16681663

1669-
expect(cmabService.getDecision).not.toHaveBeenCalled();
16701664
expect(userProfileServiceAsync?.lookup).toHaveBeenCalledTimes(1);
16711665
expect(userProfileServiceAsync?.lookup).toHaveBeenCalledWith('tester-1');
16721666

@@ -1675,9 +1669,8 @@ describe('DecisionService', () => {
16751669

16761670
const variation2 = (await value2)[0];
16771671
expect(variation2.result).toEqual({
1678-
cmabUuid: 'uuid-test',
1679-
experiment: config.experimentKeyMap['exp_3'],
1680-
variation: config.variationIdMap['5003'],
1672+
experiment: config.experimentKeyMap['exp_1'],
1673+
variation: config.variationIdMap['5001'],
16811674
decisionSource: DECISION_SOURCES.FEATURE_TEST,
16821675
});
16831676

@@ -1687,8 +1680,8 @@ describe('DecisionService', () => {
16871680
expect(userProfileServiceAsync?.save).toHaveBeenCalledWith({
16881681
user_id: 'tester-2',
16891682
experiment_bucket_map: {
1690-
'2003': {
1691-
variation_id: '5003',
1683+
'2001': {
1684+
variation_id: '5001',
16921685
},
16931686
},
16941687
});
@@ -1773,7 +1766,7 @@ describe('DecisionService', () => {
17731766

17741767
mockBucket.mockImplementation((param: BucketerParams) => {
17751768
const ruleKey = param.experimentKey;
1776-
if (ruleKey == 'exp_3') {
1769+
if (ruleKey == 'exp_1') {
17771770
return { result: param.trafficAllocationConfig[0].entityId, reasons: [] }
17781771
}
17791772
return {
@@ -1782,19 +1775,14 @@ describe('DecisionService', () => {
17821775
}
17831776
});
17841777

1785-
cmabService.getDecision.mockResolvedValue({
1786-
variationId: '5003',
1787-
cmabUuid: 'uuid-test',
1788-
});
1789-
17901778
const config = createProjectConfig(getDecisionTestDatafile());
17911779

17921780
const user = new OptimizelyUserContext({
17931781
optimizely: {} as any,
17941782
userId: 'tester',
17951783
attributes: {
17961784
country: 'BD',
1797-
age: 80, // should satisfy audience condition for exp_3 which is cmab and not others
1785+
age: 22, // should satisfy audience condition for exp_1 which is not cmab
17981786
},
17991787
});
18001788

@@ -1805,27 +1793,19 @@ describe('DecisionService', () => {
18051793
const variation = (await value)[0];
18061794

18071795
expect(variation.result).toEqual({
1808-
cmabUuid: 'uuid-test',
1809-
experiment: config.experimentKeyMap['exp_3'],
1810-
variation: config.variationIdMap['5003'],
1796+
experiment: config.experimentKeyMap['exp_1'],
1797+
variation: config.variationIdMap['5001'],
18111798
decisionSource: DECISION_SOURCES.FEATURE_TEST,
18121799
});
18131800

18141801
expect(userProfileServiceAsync?.lookup).toHaveBeenCalledWith('tester');
1815-
expect(cmabService.getDecision).toHaveBeenCalledTimes(1);
1816-
expect(cmabService.getDecision).toHaveBeenCalledWith(
1817-
config,
1818-
user,
1819-
'2003', // id of exp_3
1820-
{},
1821-
);
18221802

18231803
expect(userProfileServiceAsync?.save).toHaveBeenCalledTimes(1);
18241804
expect(userProfileServiceAsync?.save).toHaveBeenCalledWith({
18251805
user_id: 'tester',
18261806
experiment_bucket_map: {
1827-
'2003': {
1828-
variation_id: '5003',
1807+
'2001': {
1808+
variation_id: '5001',
18291809
},
18301810
},
18311811
});
@@ -1847,7 +1827,7 @@ describe('DecisionService', () => {
18471827

18481828
mockBucket.mockImplementation((param: BucketerParams) => {
18491829
const ruleKey = param.experimentKey;
1850-
if (ruleKey == 'exp_3') {
1830+
if (ruleKey == 'exp_1') {
18511831
return { result: param.trafficAllocationConfig[0].entityId, reasons: [] }
18521832
}
18531833
return {
@@ -1856,19 +1836,14 @@ describe('DecisionService', () => {
18561836
}
18571837
});
18581838

1859-
cmabService.getDecision.mockResolvedValue({
1860-
variationId: '5003',
1861-
cmabUuid: 'uuid-test',
1862-
});
1863-
18641839
const config = createProjectConfig(getDecisionTestDatafile());
18651840

18661841
const user = new OptimizelyUserContext({
18671842
optimizely: {} as any,
18681843
userId: 'tester',
18691844
attributes: {
18701845
country: 'BD',
1871-
age: 80, // should satisfy audience condition for exp_3 which is cmab and not others
1846+
age: 22, // should satisfy audience condition for exp_1 which is not cmab
18721847
},
18731848
});
18741849

@@ -1879,9 +1854,8 @@ describe('DecisionService', () => {
18791854
const variation = (await value)[0];
18801855

18811856
expect(variation.result).toEqual({
1882-
cmabUuid: 'uuid-test',
1883-
experiment: config.experimentKeyMap['exp_3'],
1884-
variation: config.variationIdMap['5003'],
1857+
experiment: config.experimentKeyMap['exp_1'],
1858+
variation: config.variationIdMap['5001'],
18851859
decisionSource: DECISION_SOURCES.FEATURE_TEST,
18861860
});
18871861

@@ -1892,8 +1866,8 @@ describe('DecisionService', () => {
18921866
expect(userProfileService?.save).toHaveBeenCalledWith({
18931867
user_id: 'tester',
18941868
experiment_bucket_map: {
1895-
'2003': {
1896-
variation_id: '5003',
1869+
'2001': {
1870+
variation_id: '5001',
18971871
},
18981872
},
18991873
});
@@ -1902,6 +1876,84 @@ describe('DecisionService', () => {
19021876
expect(userProfileServiceAsync?.save).not.toHaveBeenCalled();
19031877
});
19041878

1879+
it('should not save cmab decisions to user profile service', async () => {
1880+
const { decisionService, userProfileService, cmabService } = getDecisionService({
1881+
userProfileService: true,
1882+
userProfileServiceAsync: true,
1883+
});
1884+
1885+
const upsSyncMap: Record<string, ExperimentBucketMap> = {};
1886+
const upsAsyncMap: Record<string, ExperimentBucketMap> = {};
1887+
1888+
userProfileService?.lookup.mockImplementation((userId: string) => {
1889+
return upsSyncMap[userId] || null;
1890+
});
1891+
1892+
userProfileService?.save.mockImplementation((userProfile: UserProfile) => {
1893+
upsSyncMap[userProfile.user_id] = userProfile.experiment_bucket_map;
1894+
});
1895+
1896+
mockBucket.mockImplementation((param: BucketerParams) => {
1897+
const ruleKey = param.experimentKey;
1898+
if (ruleKey == 'exp_3') {
1899+
return { result: param.trafficAllocationConfig[0].entityId, reasons: [] }
1900+
}
1901+
return {
1902+
result: null,
1903+
reasons: [],
1904+
}
1905+
});
1906+
1907+
cmabService.getDecision.mockResolvedValueOnce({
1908+
variationId: '5003',
1909+
cmabUuid: 'uuid-test',
1910+
}).mockResolvedValueOnce({
1911+
variationId: '5001',
1912+
cmabUuid: 'uuid-test-2',
1913+
});
1914+
1915+
const config = createProjectConfig(getDecisionTestDatafile());
1916+
1917+
const user = new OptimizelyUserContext({
1918+
optimizely: {} as any,
1919+
userId: 'tester',
1920+
attributes: {
1921+
country: 'BD',
1922+
age: 80, // should satisfy audience condition for exp_3 which is a cmab and not others
1923+
},
1924+
});
1925+
1926+
const feature = config.featureKeyMap['flag_1'];
1927+
const [variation] = await decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get();
1928+
1929+
expect(variation.result).toEqual({
1930+
cmabUuid: 'uuid-test',
1931+
experiment: config.experimentKeyMap['exp_3'],
1932+
variation: config.variationIdMap['5003'],
1933+
decisionSource: DECISION_SOURCES.FEATURE_TEST,
1934+
});
1935+
1936+
expect(userProfileService?.lookup).toHaveBeenCalledTimes(1);
1937+
expect(userProfileService?.lookup).toHaveBeenCalledWith('tester');
1938+
expect(userProfileService?.save).not.toHaveBeenCalled;
1939+
expect(cmabService.getDecision).toHaveBeenCalledTimes(1);
1940+
1941+
// decide again for the same user, now cmab service should return variation 5001
1942+
const [variation2] = await decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get();
1943+
expect(variation2.result).toEqual({
1944+
cmabUuid: 'uuid-test-2',
1945+
experiment: config.experimentKeyMap['exp_3'],
1946+
variation: config.variationIdMap['5001'],
1947+
decisionSource: DECISION_SOURCES.FEATURE_TEST,
1948+
});
1949+
1950+
expect(userProfileService?.lookup).toHaveBeenCalledTimes(2);
1951+
expect(userProfileService?.lookup).toHaveBeenNthCalledWith(2, 'tester');
1952+
expect(userProfileService?.save).not.toHaveBeenCalled;
1953+
expect(cmabService.getDecision).toHaveBeenCalledTimes(2);
1954+
});
1955+
1956+
19051957
describe('holdout', () => {
19061958
beforeEach(async() => {
19071959
mockHoldoutToggle.mockReturnValue(true);

lib/core/decision_service/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,10 @@ export class DecisionService {
339339
variation.key,
340340
experimentKey,
341341
]);
342-
// update experiment bucket map if decide options do not include shouldIgnoreUPS
343-
if (userProfileTracker) {
342+
343+
// store the bucketing decision in user profile
344+
// cmab experiments will be excluded
345+
if (userProfileTracker && !this.isCmab(experiment)) {
344346
this.updateUserProfile(experiment, variation, userProfileTracker);
345347
}
346348

0 commit comments

Comments
 (0)