Skip to content

Commit d67cdd7

Browse files
committed
review updates
1 parent cf3c7da commit d67cdd7

File tree

3 files changed

+71
-18
lines changed

3 files changed

+71
-18
lines changed

lib/core/decision_service/cmab/cmab_service.spec.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,22 @@ const mockProjectConfig = (): ProjectConfig => ({
2424
}
2525
},
2626
},
27-
attributeKeyMap: {
28-
'country': {
27+
attributeIdMap: {
28+
'66': {
2929
id: '66',
30+
key: 'country',
3031
},
31-
'age': {
32+
'77': {
3233
id: '77',
34+
key: 'age',
3335
},
34-
'language': {
36+
'88': {
3537
id: '88',
38+
key: 'language',
3639
},
37-
'gender': {
40+
'99': {
3841
id: '99',
42+
key: 'gender',
3943
},
4044
}
4145
} as any);
@@ -168,6 +172,43 @@ describe('DefaultCmabService', () => {
168172
expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(2);
169173
});
170174

175+
it('should cache the variation and return the same variation if relevant attributes value have not changed but order changed', async () => {
176+
const mockCmabClient = {
177+
fetchDecision: vi.fn().mockResolvedValueOnce('123')
178+
.mockResolvedValueOnce('456')
179+
.mockResolvedValueOnce('789'),
180+
};
181+
182+
const cmabService = new DefaultCmabService({
183+
cmabCache: getMockSyncCache(),
184+
cmabClient: mockCmabClient,
185+
});
186+
187+
const projectConfig = mockProjectConfig();
188+
const userContext11 = mockUserContext('user123', {
189+
country: 'US',
190+
age: '25',
191+
language: 'en',
192+
gender: 'male'
193+
});
194+
195+
const variation11 = await cmabService.getDecision(projectConfig, userContext11, '1234', []);
196+
197+
const userContext12 = mockUserContext('user123', {
198+
gender: 'female',
199+
language: 'en',
200+
country: 'US',
201+
age: '25',
202+
});
203+
204+
const variation12 = await cmabService.getDecision(projectConfig, userContext12, '1234', []);
205+
expect(variation11.variationId).toEqual('123');
206+
expect(variation12.variationId).toEqual('123');
207+
expect(variation11.cmabUuid).toEqual(variation12.cmabUuid);
208+
209+
expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(1);
210+
});
211+
171212
it('should not mix up the cache between different experiments', async () => {
172213
const mockCmabClient = {
173214
fetchDecision: vi.fn().mockResolvedValueOnce('123')

lib/core/decision_service/cmab/cmab_service.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Cache } from "../../../utils/cache/cache";
2222
import { CmabClient } from "./cmab_client";
2323
import { v4 as uuidV4 } from 'uuid';
2424
import murmurhash from "murmurhash";
25+
import { a } from "vitest/dist/chunks/suite.CcK46U-P";
2526

2627
export type CmabDecision = {
2728
variationId: string,
@@ -32,14 +33,14 @@ export interface CmabService {
3233
/**
3334
* Get variation id for the user
3435
* @param {OptimizelyUserContext} userContext
35-
* @param {string} experimentId
36+
* @param {string} ruleId
3637
* @param {OptimizelyDecideOption[]} options
3738
* @return {Promise<CmabDecision>}
3839
*/
3940
getDecision(
4041
projectConfig: ProjectConfig,
4142
userContext: OptimizelyUserContext,
42-
experimentId: string,
43+
ruleId: string,
4344
options: OptimizelyDecideOption[]
4445
): Promise<CmabDecision>
4546
}
@@ -76,7 +77,7 @@ export class DefaultCmabService implements CmabService {
7677
const filteredAttributes = this.filterAttributes(projectConfig, userContext, ruleId);
7778

7879
if (options.includes(OptimizelyDecideOption.IGNORE_CMAB_CACHE)) {
79-
return this.fetchVariation(ruleId, userContext.getUserId(), filteredAttributes);
80+
return this.fetchDecision(ruleId, userContext.getUserId(), filteredAttributes);
8081
}
8182

8283
if (options.includes(OptimizelyDecideOption.RESET_CMAB_CACHE)) {
@@ -90,7 +91,9 @@ export class DefaultCmabService implements CmabService {
9091
}
9192

9293
const cachedValue = await this.cmabCache.get(cacheKey);
93-
const attributesHash = String(murmurhash.v3(JSON.stringify(filteredAttributes)));
94+
95+
const attributesJson = JSON.stringify(filteredAttributes, Object.keys(filteredAttributes).sort());
96+
const attributesHash = String(murmurhash.v3(attributesJson));
9497

9598
if (cachedValue) {
9699
if (cachedValue.attributesHash === attributesHash) {
@@ -100,7 +103,7 @@ export class DefaultCmabService implements CmabService {
100103
}
101104
}
102105

103-
const variation = await this.fetchVariation(ruleId, userContext.getUserId(), filteredAttributes);
106+
const variation = await this.fetchDecision(ruleId, userContext.getUserId(), filteredAttributes);
104107
this.cmabCache.set(cacheKey, {
105108
attributesHash,
106109
variationId: variation.variationId,
@@ -110,7 +113,7 @@ export class DefaultCmabService implements CmabService {
110113
return variation;
111114
}
112115

113-
private async fetchVariation(
116+
private async fetchDecision(
114117
ruleId: string,
115118
userId: string,
116119
attributes: UserAttributes,
@@ -126,19 +129,20 @@ export class DefaultCmabService implements CmabService {
126129
ruleId: string
127130
): UserAttributes {
128131
const filteredAttributes: UserAttributes = {};
129-
const attributes = userContext.getAttributes();
132+
const userAttributes = userContext.getAttributes();
130133

131134
const experiment = projectConfig.experimentIdMap[ruleId];
132135
if (!experiment || !experiment.cmab) {
133136
return filteredAttributes;
134137
}
135138

136139
const cmabAttributeIds = experiment.cmab.attributeIds;
137-
138-
Object.keys(attributes).forEach((key) => {
139-
const attributeId = projectConfig.attributeKeyMap[key].id;
140-
if (cmabAttributeIds.includes(attributeId)) {
141-
filteredAttributes[key] = attributes[key];
140+
141+
cmabAttributeIds.forEach((aid) => {
142+
const attribute = projectConfig.attributeIdMap[aid];
143+
144+
if (userAttributes.hasOwnProperty(attribute.key)) {
145+
filteredAttributes[attribute.key] = userAttributes[attribute.key];
142146
}
143147
});
144148

lib/project_config/project_config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface ProjectConfig {
8888
eventKeyMap: { [key: string]: Event };
8989
audiences: Audience[];
9090
attributeKeyMap: { [key: string]: { id: string } };
91+
attributeIdMap: { [id: string]: { key: string } };
9192
variationIdMap: { [id: string]: OptimizelyVariation };
9293
variationVariableUsageMap: { [id: string]: VariableUsageMap };
9394
audiencesById: { [id: string]: Audience };
@@ -178,7 +179,14 @@ export const createProjectConfig = function(datafileObj?: JSON, datafileStr: str
178179
...keyBy(projectConfig.typedAudiences, 'id'),
179180
}
180181

181-
projectConfig.attributeKeyMap = keyBy(projectConfig.attributes, 'key');
182+
projectConfig.attributes = projectConfig.attributes || [];
183+
projectConfig.attributeKeyMap = {};
184+
projectConfig.attributeIdMap = {};
185+
projectConfig.attributes.forEach(attribute => {
186+
projectConfig.attributeKeyMap[attribute.key] = attribute;
187+
projectConfig.attributeIdMap[attribute.id] = attribute;
188+
});
189+
182190
projectConfig.eventKeyMap = keyBy(projectConfig.events, 'key');
183191
projectConfig.groupIdMap = keyBy(projectConfig.groups, 'id');
184192

0 commit comments

Comments
 (0)