Skip to content

Impression properties support #398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
2.2.0 (March 28, 2025)
- 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.

2.1.0 (January 17, 2025)
- Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on `SplitView` type objects. Read more in our docs.

Expand Down
311 changes: 74 additions & 237 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-commons",
"version": "2.1.0",
"version": "2.1.1-rc.1",
"description": "Split JavaScript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
2 changes: 1 addition & 1 deletion src/logger/messages/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const codesError: [number, string][] = [
// input validation
[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.'],
[c.ERROR_NOT_PLAIN_OBJECT, '%s: %s must be a plain object.'],
[c.ERROR_SIZE_EXCEEDED, '%s: the maximum size allowed for the properties is 32768 bytes, which was exceeded. Event not queued.'],
[c.ERROR_SIZE_EXCEEDED, '%s: the maximum size allowed for the properties is 32768 bytes, which was exceeded.'],
[c.ERROR_NOT_FINITE, '%s: value must be a finite number.'],
[c.ERROR_NULL, '%s: you passed a null or undefined %s. It must be a non-empty string.'],
[c.ERROR_TOO_LONG, '%s: %s too long. It must have 250 characters or less.'],
Expand Down
2 changes: 1 addition & 1 deletion src/logger/messages/warn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const codesWarn: [number, string][] = codesError.concat([
[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.'],
// input validation
[c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'],
[c.WARN_TRIMMING_PROPERTIES, '%s: Event has more than 300 properties. Some of them will be trimmed when processed.'],
[c.WARN_TRIMMING_PROPERTIES, '%s: more than 300 properties were provided. Some of them will be trimmed when processed.'],
[c.WARN_CONVERTING, '%s: %s "%s" is not of type string, converting.'],
[c.WARN_TRIMMING, '%s: %s "%s" has extra whitespace, trimming.'],
[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.'],
Expand Down
189 changes: 19 additions & 170 deletions src/sdkClient/__tests__/clientAttributesDecoration.spec.ts

Large diffs are not rendered by default.

54 changes: 53 additions & 1 deletion src/sdkClient/__tests__/clientInputValidation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { clientInputValidationDecorator } from '../clientInputValidation';

// Mocks
import { DebugLogger } from '../../logger/browser/DebugLogger';
import { createClientMock } from './testUtils';

const settings: any = {
log: DebugLogger(),
sync: { __splitFiltersValidation: { groupedFilters: { bySet: [] } } }
};

const client: any = {};
const EVALUATION_RESULT = 'on';
const client: any = createClientMock(EVALUATION_RESULT);

const readinessManager: any = {
isReady: () => true,
Expand Down Expand Up @@ -52,4 +54,54 @@ describe('clientInputValidationDecorator', () => {
// @TODO should be 8, but there is an additional log from `getTreatmentsByFlagSet` and `getTreatmentsWithConfigByFlagSet` that should be removed
expect(logSpy).toBeCalledTimes(10);
});

test('should evaluate but log an error if the passed 4th argument (evaluation options) is invalid', () => {
expect(clientWithValidation.getTreatment('key', 'ff', undefined, 'invalid')).toBe(EVALUATION_RESULT);
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatment: evaluation options must be a plain object.');
expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, undefined);

expect(clientWithValidation.getTreatmentWithConfig('key', 'ff', undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentWithConfig: properties must be a plain object.');
expect(client.getTreatmentWithConfig).toBeCalledWith('key', 'ff', undefined, undefined);

expect(clientWithValidation.getTreatments('key', ['ff'], undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatments: properties must be a plain object.');
expect(client.getTreatments).toBeCalledWith('key', ['ff'], undefined, undefined);

expect(clientWithValidation.getTreatmentsWithConfig('key', ['ff'], {}, { properties: true })).toBe(EVALUATION_RESULT);
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfig: properties must be a plain object.');
expect(client.getTreatmentsWithConfig).toBeCalledWith('key', ['ff'], {}, undefined);

expect(clientWithValidation.getTreatmentsByFlagSet('key', 'flagSet', undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSet: properties must be a plain object.');
expect(client.getTreatmentsByFlagSet).toBeCalledWith('key', 'flagset', undefined, undefined);

expect(clientWithValidation.getTreatmentsWithConfigByFlagSet('key', 'flagSet', {}, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
expect(logSpy).toBeCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSet: properties must be a plain object.');
expect(client.getTreatmentsWithConfigByFlagSet).toBeCalledWith('key', 'flagset', {}, undefined);

expect(clientWithValidation.getTreatmentsByFlagSets('key', ['flagSet'], undefined, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSets: properties must be a plain object.');
expect(client.getTreatmentsByFlagSets).toBeCalledWith('key', ['flagset'], undefined, undefined);

expect(clientWithValidation.getTreatmentsWithConfigByFlagSets('key', ['flagSet'], {}, { properties: 'invalid' })).toBe(EVALUATION_RESULT);
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSets: properties must be a plain object.');
expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith('key', ['flagset'], {}, undefined);
});

test('should sanitize the properties in the 4th argument', () => {
expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: { toSanitize: /asd/, correct: 100 }})).toBe(EVALUATION_RESULT);
expect(logSpy).toHaveBeenLastCalledWith('[WARN] splitio => getTreatment: Property "toSanitize" is of invalid type. Setting value to null.');
expect(client.getTreatment).toBeCalledWith('key', 'ff', undefined, { properties: { toSanitize: null, correct: 100 }});
});

test('should ignore the properties in the 4th argument if an empty object is passed', () => {
expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: {} })).toBe(EVALUATION_RESULT);
expect(client.getTreatment).toHaveBeenLastCalledWith('key', 'ff', undefined, undefined);

expect(clientWithValidation.getTreatment('key', 'ff', undefined, { properties: undefined })).toBe(EVALUATION_RESULT);
expect(client.getTreatment).toHaveBeenLastCalledWith('key', 'ff', undefined, undefined);

expect(logSpy).not.toBeCalled();
});
});
15 changes: 15 additions & 0 deletions src/sdkClient/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,18 @@ export function assertClientApi(client: any, sdkStatus?: object) {
expect(typeof client[method]).toBe('function');
});
}

export function createClientMock(returnValue: any) {

return {
getTreatment: jest.fn(()=> returnValue),
getTreatmentWithConfig: jest.fn(()=> returnValue),
getTreatments: jest.fn(()=> returnValue),
getTreatmentsWithConfig: jest.fn(()=> returnValue),
getTreatmentsByFlagSets: jest.fn(()=> returnValue),
getTreatmentsWithConfigByFlagSets: jest.fn(()=> returnValue),
getTreatmentsByFlagSet: jest.fn(()=> returnValue),
getTreatmentsWithConfigByFlagSet: jest.fn(()=> returnValue),
track: jest.fn(()=> returnValue),
};
}
52 changes: 31 additions & 21 deletions src/sdkClient/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ function treatmentsNotReady(featureFlagNames: string[]) {
return evaluations;
}

function stringify(options?: SplitIO.EvaluationOptions) {
if (options && options.properties) {
try {
return JSON.stringify(options.properties);
} catch { /* JSON.stringify should never throw with validated options, but handling just in case */ }
}
}

/**
* Creator of base client with getTreatments and track methods.
*/
Expand All @@ -31,12 +39,12 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
const { log, mode } = settings;
const isAsync = isConsumerMode(mode);

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

const wrapUp = (evaluationResult: IEvaluationResult) => {
const queue: ImpressionDecorated[] = [];
const treatment = processEvaluation(evaluationResult, featureFlagName, key, attributes, withConfig, methodName, queue);
const treatment = processEvaluation(evaluationResult, featureFlagName, key, stringify(options), withConfig, methodName, queue);
impressionsTracker.track(queue, attributes);

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

function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined) {
return getTreatment(key, featureFlagName, attributes, true, GET_TREATMENT_WITH_CONFIG);
function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatment(key, featureFlagName, attributes, options, true, GET_TREATMENT_WITH_CONFIG);
}

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

const wrapUp = (evaluationResults: Record<string, IEvaluationResult>) => {
const queue: ImpressionDecorated[] = [];
const treatments: Record<string, SplitIO.Treatment | SplitIO.TreatmentWithConfig> = {};
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
const properties = stringify(options);
Object.keys(evaluationResults).forEach(featureFlagName => {
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue);
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
});
impressionsTracker.track(queue, attributes);

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

function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes: SplitIO.Attributes | undefined) {
return getTreatments(key, featureFlagNames, attributes, true, GET_TREATMENTS_WITH_CONFIG);
function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatments(key, featureFlagNames, attributes, options, true, GET_TREATMENTS_WITH_CONFIG);
}

function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) {
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) {
const stopTelemetryTracker = telemetryTracker.trackEval(method);

const wrapUp = (evaluationResults: Record<string, IEvaluationResult>) => {
const queue: ImpressionDecorated[] = [];
const treatments: Record<string, SplitIO.Treatment | SplitIO.TreatmentWithConfig> = {};
const evaluations = evaluationResults;
Object.keys(evaluations).forEach(featureFlagName => {
treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, attributes, withConfig, methodName, queue);
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
const properties = stringify(options);
Object.keys(evaluationResults).forEach(featureFlagName => {
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
});
impressionsTracker.track(queue, attributes);

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

function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined) {
return getTreatmentsByFlagSets(key, flagSetNames, attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatmentsByFlagSets(key, flagSetNames, attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
}

function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) {
return getTreatmentsByFlagSets(key, [flagSetName], attributes, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET);
function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET);
}

function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) {
return getTreatmentsByFlagSets(key, [flagSetName], attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
}

// Internal function
function processEvaluation(
evaluation: IEvaluationResult,
featureFlagName: string,
key: SplitIO.SplitKey,
attributes: SplitIO.Attributes | undefined,
properties: string | undefined,
withConfig: boolean,
invokingMethodName: string,
queue: ImpressionDecorated[]
Expand All @@ -148,6 +157,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
bucketingKey,
label,
changeNumber: changeNumber as number,
properties
},
disabled: impressionsDisabled
});
Expand Down
Loading