Skip to content

Commit

Permalink
Sameeran/ff 935 support for getparsedjsonassignment method (#13)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sameerank committed Sep 19, 2023
1 parent 44f48fe commit b2b1e0d
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 75 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk-common",
"version": "1.4.1",
"version": "1.5.0",
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
"main": "dist/index.js",
"files": [
Expand Down
80 changes: 67 additions & 13 deletions src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ describe('EppoClient E2E test', () => {
expect(stringAssignments).toEqual(expectedAssignments);
break;
}
case ValueTestType.JSONType: {
const jsonStringAssignments = assignments.map((a) => a?.stringValue ?? null);
expect(jsonStringAssignments).toEqual(expectedAssignments);
break;
}
}
},
);
Expand All @@ -217,37 +222,51 @@ describe('EppoClient E2E test', () => {
});

it('returns subject from overrides when enabled is true', () => {
window.localStorage.setItem(
flagKey,
JSON.stringify({
...mockExperimentConfig,
typedOverrides: {
'1b50f33aef8f681a13f623963da967ed': 'control',
},
}),
);
const entry = {
...mockExperimentConfig,
enabled: false,
overrides: {
'1b50f33aef8f681a13f623963da967ed': 'override',
},
typedOverrides: {
'1b50f33aef8f681a13f623963da967ed': 'override',
},
};

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

const client = new EppoClient(storage);
const mockLogger = td.object<IAssignmentLogger>();
client.setLogger(mockLogger);

const assignment = client.getAssignment('subject-10', flagKey);
expect(assignment).toEqual('control');
expect(assignment).toEqual('override');
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
});

it('returns subject from overrides when enabled is false', () => {
const entry = {
...mockExperimentConfig,
enabled: false,
overrides: {
'1b50f33aef8f681a13f623963da967ed': 'override',
},
typedOverrides: {
'1b50f33aef8f681a13f623963da967ed': 'control',
'1b50f33aef8f681a13f623963da967ed': 'override',
},
};

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

const client = new EppoClient(storage);
const mockLogger = td.object<IAssignmentLogger>();
client.setLogger(mockLogger);
const assignment = client.getAssignment('subject-10', flagKey);
expect(assignment).toEqual('control');
expect(assignment).toEqual('override');
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
});

it('logs variation assignment', () => {
it('logs variation assignment and experiment key', () => {
const mockLogger = td.object<IAssignmentLogger>();

storage.setEntries({ [flagKey]: mockExperimentConfig });
Expand All @@ -260,6 +279,13 @@ describe('EppoClient E2E test', () => {
expect(assignment).toEqual('control');
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
expect(td.explain(mockLogger.logAssignment).calls[0].args[0].subject).toEqual('subject-10');
expect(td.explain(mockLogger.logAssignment).calls[0].args[0].featureFlag).toEqual(flagKey);
expect(td.explain(mockLogger.logAssignment).calls[0].args[0].experiment).toEqual(
`${flagKey}-${mockExperimentConfig.rules[0].allocationKey}`,
);
expect(td.explain(mockLogger.logAssignment).calls[0].args[0].allocation).toEqual(
`${mockExperimentConfig.rules[0].allocationKey}`,
);
});

it('handles logging exception', () => {
Expand Down Expand Up @@ -326,6 +352,12 @@ describe('EppoClient E2E test', () => {
if (sa === null) return null;
return EppoValue.String(sa);
}
case ValueTestType.JSONType: {
const sa = globalClient.getJSONStringAssignment(subjectKey, experiment);
const oa = globalClient.getParsedJSONAssignment(subjectKey, experiment);
if (oa == null || sa === null) return null;
return EppoValue.JSON(sa, oa);
}
}
});
}
Expand Down Expand Up @@ -368,6 +400,20 @@ describe('EppoClient E2E test', () => {
if (sa === null) return null;
return EppoValue.String(sa);
}
case ValueTestType.JSONType: {
const sa = globalClient.getJSONStringAssignment(
subject.subjectKey,
experiment,
subject.subjectAttributes,
);
const oa = globalClient.getParsedJSONAssignment(
subject.subjectKey,
experiment,
subject.subjectAttributes,
);
if (oa == null || sa === null) return null;
return EppoValue.JSON(sa, oa);
}
}
});
}
Expand All @@ -390,6 +436,9 @@ describe('EppoClient E2E test', () => {
});

it('overrides returned assignment', async () => {
const mockLogger = td.object<IAssignmentLogger>();
client.setLogger(mockLogger);
td.reset();
const variation = await client.getAssignment(
'subject-identifer',
flagKey,
Expand All @@ -412,9 +461,13 @@ describe('EppoClient E2E test', () => {
);

expect(variation).toEqual('my-overridden-variation');
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
});

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

expect(variation).not.toEqual(null);
expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
});
});

Expand Down
100 changes: 46 additions & 54 deletions src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export interface IEppoClient {
assignmentHooks?: IAssignmentHooks,
): number | null;

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

constructor(private configurationStore: IConfigurationStore) {}

// @deprecated getAssignment is deprecated in favor of the typed get<Type>Assignment methods
public getAssignment(
subjectKey: string,
flagKey: string,
Expand All @@ -92,13 +93,8 @@ export default class EppoClient implements IEppoClient {
assignmentHooks?: IAssignmentHooks | undefined,
): string | null {
return (
this.getAssignmentVariation(
subjectKey,
flagKey,
subjectAttributes,
assignmentHooks,
ValueType.StringType,
)?.stringValue ?? null
this.getAssignmentVariation(subjectKey, flagKey, subjectAttributes, assignmentHooks)
.stringValue ?? null
);
}

Expand All @@ -115,7 +111,7 @@ export default class EppoClient implements IEppoClient {
subjectAttributes,
assignmentHooks,
ValueType.StringType,
)?.stringValue ?? null
).stringValue ?? null
);
}

Expand All @@ -132,7 +128,7 @@ export default class EppoClient implements IEppoClient {
subjectAttributes,
assignmentHooks,
ValueType.BoolType,
)?.boolValue ?? null
).boolValue ?? null
);
}

Expand All @@ -149,11 +145,11 @@ export default class EppoClient implements IEppoClient {
subjectAttributes,
assignmentHooks,
ValueType.NumericType,
)?.numericValue ?? null
).numericValue ?? null
);
}

public getJSONAssignment(
public getJSONStringAssignment(
subjectKey: string,
flagKey: string,
subjectAttributes: Record<string, EppoValue> = {},
Expand All @@ -165,8 +161,25 @@ export default class EppoClient implements IEppoClient {
flagKey,
subjectAttributes,
assignmentHooks,
ValueType.StringType,
)?.stringValue ?? null
ValueType.JSONType,
).stringValue ?? null
);
}

public getParsedJSONAssignment(
subjectKey: string,
flagKey: string,
subjectAttributes: Record<string, EppoValue> = {},
assignmentHooks?: IAssignmentHooks | undefined,
): object | null {
return (
this.getAssignmentVariation(
subjectKey,
flagKey,
subjectAttributes,
assignmentHooks,
ValueType.JSONType,
).objectValue ?? null
);
}

Expand All @@ -175,8 +188,8 @@ export default class EppoClient implements IEppoClient {
flagKey: string,
subjectAttributes: Record<string, EppoValue> = {},
assignmentHooks: IAssignmentHooks | undefined,
valueType: ValueType,
): EppoValue | null {
valueType?: ValueType,
): EppoValue {
const { allocationKey, assignment } = this.getAssignmentInternal(
subjectKey,
flagKey,
Expand All @@ -197,7 +210,7 @@ export default class EppoClient implements IEppoClient {
flagKey: string,
subjectAttributes = {},
assignmentHooks: IAssignmentHooks | undefined,
valueType: ValueType,
expectedValueType?: ValueType,
): { allocationKey: string | null; assignment: EppoValue } {
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
Expand All @@ -208,11 +221,13 @@ export default class EppoClient implements IEppoClient {
const allowListOverride = this.getSubjectVariationOverride(
subjectKey,
experimentConfig,
valueType,
expectedValueType,
);

if (allowListOverride) {
if (!allowListOverride.isExpectedType()) return nullAssignment;
if (!allowListOverride.isNullType()) {
if (!allowListOverride.isExpectedType()) {
return nullAssignment;
}
return { ...nullAssignment, assignment: allowListOverride };
}

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

const internalAssignment = {
allocationKey: matchedRule.allocationKey,
assignment: EppoValue.Null(),
assignment: EppoValue.generateEppoValue(
expectedValueType,
assignedVariation?.value,
assignedVariation?.typedValue,
),
};

switch (valueType) {
case ValueType.BoolType:
internalAssignment['assignment'] = EppoValue.Bool(assignedVariation as boolean);
break;
case ValueType.NumericType:
internalAssignment['assignment'] = EppoValue.Numeric(assignedVariation as number);
break;
case ValueType.StringType:
internalAssignment['assignment'] = EppoValue.String(assignedVariation as string);
break;
default:
return nullAssignment;
}

return internalAssignment.assignment.isExpectedType() ? internalAssignment : nullAssignment;
}

Expand Down Expand Up @@ -314,25 +318,13 @@ export default class EppoClient implements IEppoClient {
private getSubjectVariationOverride(
subjectKey: string,
experimentConfig: IExperimentConfiguration,
valueType: ValueType,
): EppoValue | null {
expectedValueType?: ValueType,
): EppoValue {
const subjectHash = md5(subjectKey);
const overridden =
const override = experimentConfig?.overrides && experimentConfig.overrides[subjectHash];
const typedOverride =
experimentConfig?.typedOverrides && experimentConfig.typedOverrides[subjectHash];
if (overridden) {
switch (valueType) {
case ValueType.BoolType:
return EppoValue.Bool(overridden as unknown as boolean);
case ValueType.NumericType:
return EppoValue.Numeric(overridden as unknown as number);
case ValueType.StringType:
return EppoValue.String(overridden as string);
default:
return null;
}
}

return null;
return EppoValue.generateEppoValue(expectedValueType, override, typedOverride);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/dto/experiment-configuration-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export interface IExperimentConfiguration {
name: string;
enabled: boolean;
subjectShards: number;
typedOverrides: Record<string, string>;
overrides: Record<string, string>;
typedOverrides: Record<string, number | boolean | string | object>;
allocations: Record<string, IAllocation>;
rules: IRule[];
}
1 change: 1 addition & 0 deletions src/dto/variation-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface IShardRange {

export interface IVariation {
name: string;
value: string;
typedValue: IValue;
shardRange: IShardRange;
}
Loading

0 comments on commit b2b1e0d

Please sign in to comment.