Skip to content

Commit b2b1e0d

Browse files
authored
Sameeran/ff 935 support for getparsedjsonassignment method (#13)
* Add getParsedJSONAssignment method and use in test * Remove overly defensive optional chaining * Increment version * Test that overrides do not get logged * Remove unused import * Add test for new experiment key format * Use td.reset so we can reuse the client between tests * Clean up extra line * Test logs variation assignment and experiment key * wrap JSON stringify and parse with try catch * Add deprecated comment * Increment minor version
1 parent 44f48fe commit b2b1e0d

File tree

7 files changed

+170
-75
lines changed

7 files changed

+170
-75
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "1.4.1",
3+
"version": "1.5.0",
44
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
55
"main": "dist/index.js",
66
"files": [

src/client/eppo-client.spec.ts

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ describe('EppoClient E2E test', () => {
205205
expect(stringAssignments).toEqual(expectedAssignments);
206206
break;
207207
}
208+
case ValueTestType.JSONType: {
209+
const jsonStringAssignments = assignments.map((a) => a?.stringValue ?? null);
210+
expect(jsonStringAssignments).toEqual(expectedAssignments);
211+
break;
212+
}
208213
}
209214
},
210215
);
@@ -217,37 +222,51 @@ describe('EppoClient E2E test', () => {
217222
});
218223

219224
it('returns subject from overrides when enabled is true', () => {
220-
window.localStorage.setItem(
221-
flagKey,
222-
JSON.stringify({
223-
...mockExperimentConfig,
224-
typedOverrides: {
225-
'1b50f33aef8f681a13f623963da967ed': 'control',
226-
},
227-
}),
228-
);
225+
const entry = {
226+
...mockExperimentConfig,
227+
enabled: false,
228+
overrides: {
229+
'1b50f33aef8f681a13f623963da967ed': 'override',
230+
},
231+
typedOverrides: {
232+
'1b50f33aef8f681a13f623963da967ed': 'override',
233+
},
234+
};
235+
236+
storage.setEntries({ [flagKey]: entry });
237+
229238
const client = new EppoClient(storage);
239+
const mockLogger = td.object<IAssignmentLogger>();
240+
client.setLogger(mockLogger);
241+
230242
const assignment = client.getAssignment('subject-10', flagKey);
231-
expect(assignment).toEqual('control');
243+
expect(assignment).toEqual('override');
244+
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
232245
});
233246

234247
it('returns subject from overrides when enabled is false', () => {
235248
const entry = {
236249
...mockExperimentConfig,
237250
enabled: false,
251+
overrides: {
252+
'1b50f33aef8f681a13f623963da967ed': 'override',
253+
},
238254
typedOverrides: {
239-
'1b50f33aef8f681a13f623963da967ed': 'control',
255+
'1b50f33aef8f681a13f623963da967ed': 'override',
240256
},
241257
};
242258

243259
storage.setEntries({ [flagKey]: entry });
244260

245261
const client = new EppoClient(storage);
262+
const mockLogger = td.object<IAssignmentLogger>();
263+
client.setLogger(mockLogger);
246264
const assignment = client.getAssignment('subject-10', flagKey);
247-
expect(assignment).toEqual('control');
265+
expect(assignment).toEqual('override');
266+
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
248267
});
249268

250-
it('logs variation assignment', () => {
269+
it('logs variation assignment and experiment key', () => {
251270
const mockLogger = td.object<IAssignmentLogger>();
252271

253272
storage.setEntries({ [flagKey]: mockExperimentConfig });
@@ -260,6 +279,13 @@ describe('EppoClient E2E test', () => {
260279
expect(assignment).toEqual('control');
261280
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
262281
expect(td.explain(mockLogger.logAssignment).calls[0].args[0].subject).toEqual('subject-10');
282+
expect(td.explain(mockLogger.logAssignment).calls[0].args[0].featureFlag).toEqual(flagKey);
283+
expect(td.explain(mockLogger.logAssignment).calls[0].args[0].experiment).toEqual(
284+
`${flagKey}-${mockExperimentConfig.rules[0].allocationKey}`,
285+
);
286+
expect(td.explain(mockLogger.logAssignment).calls[0].args[0].allocation).toEqual(
287+
`${mockExperimentConfig.rules[0].allocationKey}`,
288+
);
263289
});
264290

265291
it('handles logging exception', () => {
@@ -326,6 +352,12 @@ describe('EppoClient E2E test', () => {
326352
if (sa === null) return null;
327353
return EppoValue.String(sa);
328354
}
355+
case ValueTestType.JSONType: {
356+
const sa = globalClient.getJSONStringAssignment(subjectKey, experiment);
357+
const oa = globalClient.getParsedJSONAssignment(subjectKey, experiment);
358+
if (oa == null || sa === null) return null;
359+
return EppoValue.JSON(sa, oa);
360+
}
329361
}
330362
});
331363
}
@@ -368,6 +400,20 @@ describe('EppoClient E2E test', () => {
368400
if (sa === null) return null;
369401
return EppoValue.String(sa);
370402
}
403+
case ValueTestType.JSONType: {
404+
const sa = globalClient.getJSONStringAssignment(
405+
subject.subjectKey,
406+
experiment,
407+
subject.subjectAttributes,
408+
);
409+
const oa = globalClient.getParsedJSONAssignment(
410+
subject.subjectKey,
411+
experiment,
412+
subject.subjectAttributes,
413+
);
414+
if (oa == null || sa === null) return null;
415+
return EppoValue.JSON(sa, oa);
416+
}
371417
}
372418
});
373419
}
@@ -390,6 +436,9 @@ describe('EppoClient E2E test', () => {
390436
});
391437

392438
it('overrides returned assignment', async () => {
439+
const mockLogger = td.object<IAssignmentLogger>();
440+
client.setLogger(mockLogger);
441+
td.reset();
393442
const variation = await client.getAssignment(
394443
'subject-identifer',
395444
flagKey,
@@ -412,9 +461,13 @@ describe('EppoClient E2E test', () => {
412461
);
413462

414463
expect(variation).toEqual('my-overridden-variation');
464+
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
415465
});
416466

417467
it('uses regular assignment logic if onPreAssignment returns null', async () => {
468+
const mockLogger = td.object<IAssignmentLogger>();
469+
client.setLogger(mockLogger);
470+
td.reset();
418471
const variation = await client.getAssignment(
419472
'subject-identifer',
420473
flagKey,
@@ -436,6 +489,7 @@ describe('EppoClient E2E test', () => {
436489
);
437490

438491
expect(variation).not.toEqual(null);
492+
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
439493
});
440494
});
441495

src/client/eppo-client.ts

Lines changed: 46 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export interface IEppoClient {
6969
assignmentHooks?: IAssignmentHooks,
7070
): number | null;
7171

72-
getJSONAssignment(
72+
getJSONStringAssignment(
7373
subjectKey: string,
7474
flagKey: string,
7575
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -84,6 +84,7 @@ export default class EppoClient implements IEppoClient {
8484

8585
constructor(private configurationStore: IConfigurationStore) {}
8686

87+
// @deprecated getAssignment is deprecated in favor of the typed get<Type>Assignment methods
8788
public getAssignment(
8889
subjectKey: string,
8990
flagKey: string,
@@ -92,13 +93,8 @@ export default class EppoClient implements IEppoClient {
9293
assignmentHooks?: IAssignmentHooks | undefined,
9394
): string | null {
9495
return (
95-
this.getAssignmentVariation(
96-
subjectKey,
97-
flagKey,
98-
subjectAttributes,
99-
assignmentHooks,
100-
ValueType.StringType,
101-
)?.stringValue ?? null
96+
this.getAssignmentVariation(subjectKey, flagKey, subjectAttributes, assignmentHooks)
97+
.stringValue ?? null
10298
);
10399
}
104100

@@ -115,7 +111,7 @@ export default class EppoClient implements IEppoClient {
115111
subjectAttributes,
116112
assignmentHooks,
117113
ValueType.StringType,
118-
)?.stringValue ?? null
114+
).stringValue ?? null
119115
);
120116
}
121117

@@ -132,7 +128,7 @@ export default class EppoClient implements IEppoClient {
132128
subjectAttributes,
133129
assignmentHooks,
134130
ValueType.BoolType,
135-
)?.boolValue ?? null
131+
).boolValue ?? null
136132
);
137133
}
138134

@@ -149,11 +145,11 @@ export default class EppoClient implements IEppoClient {
149145
subjectAttributes,
150146
assignmentHooks,
151147
ValueType.NumericType,
152-
)?.numericValue ?? null
148+
).numericValue ?? null
153149
);
154150
}
155151

156-
public getJSONAssignment(
152+
public getJSONStringAssignment(
157153
subjectKey: string,
158154
flagKey: string,
159155
subjectAttributes: Record<string, EppoValue> = {},
@@ -165,8 +161,25 @@ export default class EppoClient implements IEppoClient {
165161
flagKey,
166162
subjectAttributes,
167163
assignmentHooks,
168-
ValueType.StringType,
169-
)?.stringValue ?? null
164+
ValueType.JSONType,
165+
).stringValue ?? null
166+
);
167+
}
168+
169+
public getParsedJSONAssignment(
170+
subjectKey: string,
171+
flagKey: string,
172+
subjectAttributes: Record<string, EppoValue> = {},
173+
assignmentHooks?: IAssignmentHooks | undefined,
174+
): object | null {
175+
return (
176+
this.getAssignmentVariation(
177+
subjectKey,
178+
flagKey,
179+
subjectAttributes,
180+
assignmentHooks,
181+
ValueType.JSONType,
182+
).objectValue ?? null
170183
);
171184
}
172185

@@ -175,8 +188,8 @@ export default class EppoClient implements IEppoClient {
175188
flagKey: string,
176189
subjectAttributes: Record<string, EppoValue> = {},
177190
assignmentHooks: IAssignmentHooks | undefined,
178-
valueType: ValueType,
179-
): EppoValue | null {
191+
valueType?: ValueType,
192+
): EppoValue {
180193
const { allocationKey, assignment } = this.getAssignmentInternal(
181194
subjectKey,
182195
flagKey,
@@ -197,7 +210,7 @@ export default class EppoClient implements IEppoClient {
197210
flagKey: string,
198211
subjectAttributes = {},
199212
assignmentHooks: IAssignmentHooks | undefined,
200-
valueType: ValueType,
213+
expectedValueType?: ValueType,
201214
): { allocationKey: string | null; assignment: EppoValue } {
202215
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
203216
validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
@@ -208,11 +221,13 @@ export default class EppoClient implements IEppoClient {
208221
const allowListOverride = this.getSubjectVariationOverride(
209222
subjectKey,
210223
experimentConfig,
211-
valueType,
224+
expectedValueType,
212225
);
213226

214-
if (allowListOverride) {
215-
if (!allowListOverride.isExpectedType()) return nullAssignment;
227+
if (!allowListOverride.isNullType()) {
228+
if (!allowListOverride.isExpectedType()) {
229+
return nullAssignment;
230+
}
216231
return { ...nullAssignment, assignment: allowListOverride };
217232
}
218233

@@ -242,27 +257,16 @@ export default class EppoClient implements IEppoClient {
242257
const shard = getShard(`assignment-${subjectKey}-${flagKey}`, subjectShards);
243258
const assignedVariation = variations.find((variation) =>
244259
isShardInRange(shard, variation.shardRange),
245-
)?.typedValue;
260+
);
246261

247262
const internalAssignment = {
248263
allocationKey: matchedRule.allocationKey,
249-
assignment: EppoValue.Null(),
264+
assignment: EppoValue.generateEppoValue(
265+
expectedValueType,
266+
assignedVariation?.value,
267+
assignedVariation?.typedValue,
268+
),
250269
};
251-
252-
switch (valueType) {
253-
case ValueType.BoolType:
254-
internalAssignment['assignment'] = EppoValue.Bool(assignedVariation as boolean);
255-
break;
256-
case ValueType.NumericType:
257-
internalAssignment['assignment'] = EppoValue.Numeric(assignedVariation as number);
258-
break;
259-
case ValueType.StringType:
260-
internalAssignment['assignment'] = EppoValue.String(assignedVariation as string);
261-
break;
262-
default:
263-
return nullAssignment;
264-
}
265-
266270
return internalAssignment.assignment.isExpectedType() ? internalAssignment : nullAssignment;
267271
}
268272

@@ -314,25 +318,13 @@ export default class EppoClient implements IEppoClient {
314318
private getSubjectVariationOverride(
315319
subjectKey: string,
316320
experimentConfig: IExperimentConfiguration,
317-
valueType: ValueType,
318-
): EppoValue | null {
321+
expectedValueType?: ValueType,
322+
): EppoValue {
319323
const subjectHash = md5(subjectKey);
320-
const overridden =
324+
const override = experimentConfig?.overrides && experimentConfig.overrides[subjectHash];
325+
const typedOverride =
321326
experimentConfig?.typedOverrides && experimentConfig.typedOverrides[subjectHash];
322-
if (overridden) {
323-
switch (valueType) {
324-
case ValueType.BoolType:
325-
return EppoValue.Bool(overridden as unknown as boolean);
326-
case ValueType.NumericType:
327-
return EppoValue.Numeric(overridden as unknown as number);
328-
case ValueType.StringType:
329-
return EppoValue.String(overridden as string);
330-
default:
331-
return null;
332-
}
333-
}
334-
335-
return null;
327+
return EppoValue.generateEppoValue(expectedValueType, override, typedOverride);
336328
}
337329

338330
/**

src/dto/experiment-configuration-dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export interface IExperimentConfiguration {
55
name: string;
66
enabled: boolean;
77
subjectShards: number;
8-
typedOverrides: Record<string, string>;
8+
overrides: Record<string, string>;
9+
typedOverrides: Record<string, number | boolean | string | object>;
910
allocations: Record<string, IAllocation>;
1011
rules: IRule[];
1112
}

src/dto/variation-dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface IShardRange {
77

88
export interface IVariation {
99
name: string;
10+
value: string;
1011
typedValue: IValue;
1112
shardRange: IShardRange;
1213
}

0 commit comments

Comments
 (0)