Skip to content

Commit 65dd894

Browse files
Merge branch 'development' into cache_expiration_baseline
2 parents eb408ad + 81fbc9a commit 65dd894

19 files changed

+393
-557
lines changed

CHANGES.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
2.2.0 (January XX, 2025)
1+
2.2.0 (March 28, 2025)
2+
- Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split backend.
23
- Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`:
34
- `storage.expirationDays` to specify the validity period of the rollout cache.
45
- `storage.clearOnInit` to clear the rollout cache on SDK initialization.

package-lock.json

Lines changed: 74 additions & 237 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-commons",
3-
"version": "2.1.0",
3+
"version": "2.1.1-rc.1",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/logger/messages/error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const codesError: [number, string][] = [
2121
// input validation
2222
[c.ERROR_EVENT_TYPE_FORMAT, '%s: you passed "%s", event_type must adhere to the regular expression /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/g. This means an event_type must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, underscore, period, or colon as separators of alphanumeric characters.'],
2323
[c.ERROR_NOT_PLAIN_OBJECT, '%s: %s must be a plain object.'],
24-
[c.ERROR_SIZE_EXCEEDED, '%s: the maximum size allowed for the properties is 32768 bytes, which was exceeded. Event not queued.'],
24+
[c.ERROR_SIZE_EXCEEDED, '%s: the maximum size allowed for the properties is 32768 bytes, which was exceeded.'],
2525
[c.ERROR_NOT_FINITE, '%s: value must be a finite number.'],
2626
[c.ERROR_NULL, '%s: you passed a null or undefined %s. It must be a non-empty string.'],
2727
[c.ERROR_TOO_LONG, '%s: %s too long. It must have 250 characters or less.'],

src/logger/messages/warn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const codesWarn: [number, string][] = codesError.concat([
1818
[c.CLIENT_NO_LISTENER, 'No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'],
1919
// input validation
2020
[c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'],
21-
[c.WARN_TRIMMING_PROPERTIES, '%s: Event has more than 300 properties. Some of them will be trimmed when processed.'],
21+
[c.WARN_TRIMMING_PROPERTIES, '%s: more than 300 properties were provided. Some of them will be trimmed when processed.'],
2222
[c.WARN_CONVERTING, '%s: %s "%s" is not of type string, converting.'],
2323
[c.WARN_TRIMMING, '%s: %s "%s" has extra whitespace, trimming.'],
2424
[c.WARN_NOT_EXISTENT_SPLIT, '%s: feature flag "%s" does not exist in this environment. Please double check what feature flags exist in the Split user interface.'],

src/sdkClient/__tests__/clientAttributesDecoration.spec.ts

Lines changed: 19 additions & 170 deletions
Large diffs are not rendered by default.

src/sdkClient/__tests__/clientInputValidation.spec.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { clientInputValidationDecorator } from '../clientInputValidation';
33

44
// Mocks
55
import { DebugLogger } from '../../logger/browser/DebugLogger';
6+
import { createClientMock } from './testUtils';
67

78
const settings: any = {
89
log: DebugLogger(),
910
sync: { __splitFiltersValidation: { groupedFilters: { bySet: [] } } }
1011
};
1112

12-
const client: any = {};
13+
const EVALUATION_RESULT = 'on';
14+
const client: any = createClientMock(EVALUATION_RESULT);
1315

1416
const readinessManager: any = {
1517
isReady: () => true,
@@ -52,4 +54,54 @@ describe('clientInputValidationDecorator', () => {
5254
// @TODO should be 8, but there is an additional log from `getTreatmentsByFlagSet` and `getTreatmentsWithConfigByFlagSet` that should be removed
5355
expect(logSpy).toBeCalledTimes(10);
5456
});
57+
58+
test('should evaluate but log an error if the passed 4th argument (evaluation options) is invalid', () => {
59+
expect(clientWithValidation.getTreatment('key', 'ff', undefined, 'invalid')).toBe(EVALUATION_RESULT);
60+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatment: evaluation options must be a plain object.');
61+
expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, undefined);
62+
63+
expect(clientWithValidation.getTreatmentWithConfig('key', 'ff', undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
64+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentWithConfig: properties must be a plain object.');
65+
expect(client.getTreatmentWithConfig).toBeCalledWith('key', 'ff', undefined, undefined);
66+
67+
expect(clientWithValidation.getTreatments('key', ['ff'], undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
68+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatments: properties must be a plain object.');
69+
expect(client.getTreatments).toBeCalledWith('key', ['ff'], undefined, undefined);
70+
71+
expect(clientWithValidation.getTreatmentsWithConfig('key', ['ff'], {}, { properties: true })).toBe(EVALUATION_RESULT);
72+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfig: properties must be a plain object.');
73+
expect(client.getTreatmentsWithConfig).toBeCalledWith('key', ['ff'], {}, undefined);
74+
75+
expect(clientWithValidation.getTreatmentsByFlagSet('key', 'flagSet', undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
76+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSet: properties must be a plain object.');
77+
expect(client.getTreatmentsByFlagSet).toBeCalledWith('key', 'flagset', undefined, undefined);
78+
79+
expect(clientWithValidation.getTreatmentsWithConfigByFlagSet('key', 'flagSet', {}, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
80+
expect(logSpy).toBeCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSet: properties must be a plain object.');
81+
expect(client.getTreatmentsWithConfigByFlagSet).toBeCalledWith('key', 'flagset', {}, undefined);
82+
83+
expect(clientWithValidation.getTreatmentsByFlagSets('key', ['flagSet'], undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
84+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSets: properties must be a plain object.');
85+
expect(client.getTreatmentsByFlagSets).toBeCalledWith('key', ['flagset'], undefined, undefined);
86+
87+
expect(clientWithValidation.getTreatmentsWithConfigByFlagSets('key', ['flagSet'], {}, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
88+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSets: properties must be a plain object.');
89+
expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith('key', ['flagset'], {}, undefined);
90+
});
91+
92+
test('should sanitize the properties in the 4th argument', () => {
93+
expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: { toSanitize: /asd/, correct: 100 }})).toBe(EVALUATION_RESULT);
94+
expect(logSpy).toHaveBeenLastCalledWith('[WARN] splitio => getTreatment: Property "toSanitize" is of invalid type. Setting value to null.');
95+
expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, { properties: { toSanitize: null, correct: 100 }});
96+
});
97+
98+
test('should ignore the properties in the 4th argument if an empty object is passed', () => {
99+
expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: {} })).toBe(EVALUATION_RESULT);
100+
expect(client.getTreatment).toHaveBeenLastCalledWith('key', 'ff', undefined, undefined);
101+
102+
expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: undefined })).toBe(EVALUATION_RESULT);
103+
expect(client.getTreatment).toHaveBeenLastCalledWith('key', 'ff', undefined, undefined);
104+
105+
expect(logSpy).not.toBeCalled();
106+
});
55107
});

src/sdkClient/__tests__/testUtils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,18 @@ export function assertClientApi(client: any, sdkStatus?: object) {
88
expect(typeof client[method]).toBe('function');
99
});
1010
}
11+
12+
export function createClientMock(returnValue: any) {
13+
14+
return {
15+
getTreatment: jest.fn(()=> returnValue),
16+
getTreatmentWithConfig: jest.fn(()=> returnValue),
17+
getTreatments: jest.fn(()=> returnValue),
18+
getTreatmentsWithConfig: jest.fn(()=> returnValue),
19+
getTreatmentsByFlagSets: jest.fn(()=> returnValue),
20+
getTreatmentsWithConfigByFlagSets: jest.fn(()=> returnValue),
21+
getTreatmentsByFlagSet: jest.fn(()=> returnValue),
22+
getTreatmentsWithConfigByFlagSet: jest.fn(()=> returnValue),
23+
track: jest.fn(()=> returnValue),
24+
};
25+
}

src/sdkClient/client.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ function treatmentsNotReady(featureFlagNames: string[]) {
2323
return evaluations;
2424
}
2525

26+
function stringify(options?: SplitIO.EvaluationOptions) {
27+
if (options && options.properties) {
28+
try {
29+
return JSON.stringify(options.properties);
30+
} catch { /* JSON.stringify should never throw with validated options, but handling just in case */ }
31+
}
32+
}
33+
2634
/**
2735
* Creator of base client with getTreatments and track methods.
2836
*/
@@ -31,12 +39,12 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
3139
const { log, mode } = settings;
3240
const isAsync = isConsumerMode(mode);
3341

34-
function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined, withConfig = false, methodName = GET_TREATMENT) {
42+
function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENT) {
3543
const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENT_WITH_CONFIG : TREATMENT);
3644

3745
const wrapUp = (evaluationResult: IEvaluationResult) => {
3846
const queue: ImpressionDecorated[] = [];
39-
const treatment = processEvaluation(evaluationResult, featureFlagName, key, attributes, withConfig, methodName, queue);
47+
const treatment = processEvaluation(evaluationResult, featureFlagName, key, stringify(options), withConfig, methodName, queue);
4048
impressionsTracker.track(queue, attributes);
4149

4250
stopTelemetryTracker(queue[0] && queue[0].imp.label);
@@ -52,18 +60,19 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
5260
return thenable(evaluation) ? evaluation.then((res) => wrapUp(res)) : wrapUp(evaluation);
5361
}
5462

55-
function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined) {
56-
return getTreatment(key, featureFlagName, attributes, true, GET_TREATMENT_WITH_CONFIG);
63+
function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
64+
return getTreatment(key, featureFlagName, attributes, options, true, GET_TREATMENT_WITH_CONFIG);
5765
}
5866

59-
function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false, methodName = GET_TREATMENTS) {
67+
function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENTS) {
6068
const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENTS_WITH_CONFIG : TREATMENTS);
6169

6270
const wrapUp = (evaluationResults: Record<string, IEvaluationResult>) => {
6371
const queue: ImpressionDecorated[] = [];
64-
const treatments: Record<string, SplitIO.Treatment | SplitIO.TreatmentWithConfig> = {};
72+
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
73+
const properties = stringify(options);
6574
Object.keys(evaluationResults).forEach(featureFlagName => {
66-
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue);
75+
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
6776
});
6877
impressionsTracker.track(queue, attributes);
6978

@@ -80,19 +89,19 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
8089
return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations);
8190
}
8291

83-
function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes: SplitIO.Attributes | undefined) {
84-
return getTreatments(key, featureFlagNames, attributes, true, GET_TREATMENTS_WITH_CONFIG);
92+
function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
93+
return getTreatments(key, featureFlagNames, attributes, options, true, GET_TREATMENTS_WITH_CONFIG);
8594
}
8695

87-
function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) {
96+
function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) {
8897
const stopTelemetryTracker = telemetryTracker.trackEval(method);
8998

9099
const wrapUp = (evaluationResults: Record<string, IEvaluationResult>) => {
91100
const queue: ImpressionDecorated[] = [];
92-
const treatments: Record<string, SplitIO.Treatment | SplitIO.TreatmentWithConfig> = {};
93-
const evaluations = evaluationResults;
94-
Object.keys(evaluations).forEach(featureFlagName => {
95-
treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue);
101+
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
102+
const properties = stringify(options);
103+
Object.keys(evaluationResults).forEach(featureFlagName => {
104+
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
96105
});
97106
impressionsTracker.track(queue, attributes);
98107

@@ -109,24 +118,24 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
109118
return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations);
110119
}
111120

112-
function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined) {
113-
return getTreatmentsByFlagSets(key, flagSetNames, attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
121+
function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
122+
return getTreatmentsByFlagSets(key, flagSetNames, attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
114123
}
115124

116-
function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) {
117-
return getTreatmentsByFlagSets(key, [flagSetName], attributes, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET);
125+
function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
126+
return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET);
118127
}
119128

120-
function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) {
121-
return getTreatmentsByFlagSets(key, [flagSetName], attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
129+
function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
130+
return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
122131
}
123132

124133
// Internal function
125134
function processEvaluation(
126135
evaluation: IEvaluationResult,
127136
featureFlagName: string,
128137
key: SplitIO.SplitKey,
129-
attributes: SplitIO.Attributes | undefined,
138+
properties: string | undefined,
130139
withConfig: boolean,
131140
invokingMethodName: string,
132141
queue: ImpressionDecorated[]
@@ -148,6 +157,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
148157
bucketingKey,
149158
label,
150159
changeNumber: changeNumber as number,
160+
properties
151161
},
152162
disabled: impressionsDisabled
153163
});

0 commit comments

Comments
 (0)