Skip to content

Commit 7565f89

Browse files
Merge pull request #317 from splitio/development
Release v1.16.0
2 parents 7a6fab8 + 526f718 commit 7565f89

File tree

18 files changed

+227
-88
lines changed

18 files changed

+227
-88
lines changed

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
1.16.0 (June 13, 2024)
2+
- Added the `getOptions` method to the `IPlatform` interface to allow the SDK to pass request options to the `fetch` function and `EventSource` constructor when fetching data from the Split servers. The method is optional and, if provided, it is called twice: first for the `fetch` options and then for the `EventSource` options. Useful for advanced use cases like configuring a proxy or validating HTTPS certificates in NodeJS.
3+
- Updated the Redis storage to lazily import the `ioredis` dependency when the storage is created. This prevents errors when the SDK is imported or bundled in a .mjs file, as `ioredis` is a CommonJS module.
4+
- Bugfixing - Restored some input validation error logs that were removed in version 1.12.0. The logs inform the user when the `getTreatment(s)` methods are called with an invalid value as feature flag name or flag set name.
5+
- Bugfixing - Fixed localhost mode to emit SDK_UPDATE when mocked feature flags are updated in the `features` object map of the config object (Related to issue https://github.com/splitio/javascript-browser-client/issues/119).
6+
17
1.15.0 (May 13, 2024)
28
- Added an optional settings validation parameter to let overwrite the default flag spec version, used by the JS Synchronizer.
39

package-lock.json

Lines changed: 16 additions & 16 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": "1.15.0",
3+
"version": "1.16.0",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Test target
2+
import { clientInputValidationDecorator } from '../clientInputValidation';
3+
4+
// Mocks
5+
import { DebugLogger } from '../../logger/browser/DebugLogger';
6+
7+
const settings: any = {
8+
log: DebugLogger(),
9+
sync: { __splitFiltersValidation: { groupedFilters: { bySet: [] } } }
10+
};
11+
12+
const client: any = {};
13+
14+
const readinessManager: any = {
15+
isReady: () => true,
16+
isDestroyed: () => false
17+
};
18+
19+
describe('clientInputValidationDecorator', () => {
20+
const clientWithValidation = clientInputValidationDecorator(settings, client, readinessManager);
21+
const logSpy = jest.spyOn(console, 'log');
22+
23+
beforeEach(() => {
24+
logSpy.mockClear();
25+
});
26+
27+
test('should return control and log an error if the passed 2nd argument (feature flag(s) or flag set(s)) is invalid', () => {
28+
expect(clientWithValidation.getTreatment('key')).toEqual('control');
29+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatment: you passed a null or undefined feature flag name. It must be a non-empty string.');
30+
31+
expect(clientWithValidation.getTreatmentWithConfig('key', [])).toEqual({ treatment: 'control', config: null });
32+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentWithConfig: you passed an invalid feature flag name. It must be a non-empty string.');
33+
34+
expect(clientWithValidation.getTreatments('key')).toEqual({});
35+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatments: feature flag names must be a non-empty array.');
36+
37+
expect(clientWithValidation.getTreatmentsWithConfig('key', [])).toEqual({});
38+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfig: feature flag names must be a non-empty array.');
39+
40+
expect(clientWithValidation.getTreatmentsByFlagSet('key')).toEqual({});
41+
expect(logSpy).toBeCalledWith('[ERROR] splitio => getTreatmentsByFlagSet: you passed a null or undefined flag set. It must be a non-empty string.');
42+
43+
expect(clientWithValidation.getTreatmentsWithConfigByFlagSet('key', [])).toEqual({});
44+
expect(logSpy).toBeCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSet: you passed an invalid flag set. It must be a non-empty string.');
45+
46+
expect(clientWithValidation.getTreatmentsByFlagSets('key')).toEqual({});
47+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsByFlagSets: flag sets must be a non-empty array.');
48+
49+
expect(clientWithValidation.getTreatmentsWithConfigByFlagSets('key', [])).toEqual({});
50+
expect(logSpy).toHaveBeenLastCalledWith('[ERROR] splitio => getTreatmentsWithConfigByFlagSets: flag sets must be a non-empty array.');
51+
52+
// @TODO should be 8, but there is an additional log from `getTreatmentsByFlagSet` and `getTreatmentsWithConfigByFlagSet` that should be removed
53+
expect(logSpy).toBeCalledTimes(10);
54+
});
55+
});

src/sdkClient/clientInputValidation.ts

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,26 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
3131
/**
3232
* Avoid repeating this validations code
3333
*/
34-
function validateEvaluationParams(maybeKey: SplitIO.SplitKey, maybeFeatureFlagNameOrNames: string | string[] | undefined, maybeAttributes: SplitIO.Attributes | undefined, methodName: string, maybeFlagSetNameOrNames?: string[]) {
35-
const multi = startsWith(methodName, GET_TREATMENTS);
34+
function validateEvaluationParams(maybeKey: SplitIO.SplitKey, maybeNameOrNames: string | string[], maybeAttributes: SplitIO.Attributes | undefined, methodName: string) {
3635
const key = validateKey(log, maybeKey, methodName);
37-
let splitOrSplits: string | string[] | false = false;
38-
let flagSetOrFlagSets: string[] = [];
39-
if (maybeFeatureFlagNameOrNames) {
40-
splitOrSplits = multi ? validateSplits(log, maybeFeatureFlagNameOrNames, methodName) : validateSplit(log, maybeFeatureFlagNameOrNames, methodName);
41-
}
36+
37+
const nameOrNames = methodName.indexOf('ByFlagSet') > -1 ?
38+
validateFlagSets(log, methodName, maybeNameOrNames as string[], settings.sync.__splitFiltersValidation.groupedFilters.bySet) :
39+
startsWith(methodName, GET_TREATMENTS) ?
40+
validateSplits(log, maybeNameOrNames, methodName) :
41+
validateSplit(log, maybeNameOrNames, methodName);
42+
4243
const attributes = validateAttributes(log, maybeAttributes, methodName);
4344
const isNotDestroyed = validateIfNotDestroyed(log, readinessManager, methodName);
44-
if (maybeFlagSetNameOrNames) {
45-
flagSetOrFlagSets = validateFlagSets(log, methodName, maybeFlagSetNameOrNames, settings.sync.__splitFiltersValidation.groupedFilters.bySet);
46-
}
4745

48-
validateIfOperational(log, readinessManager, methodName, splitOrSplits);
46+
validateIfOperational(log, readinessManager, methodName, nameOrNames);
4947

50-
const valid = isNotDestroyed && key && (splitOrSplits || flagSetOrFlagSets.length > 0) && attributes !== false;
48+
const valid = isNotDestroyed && key && nameOrNames && attributes !== false;
5149

5250
return {
5351
valid,
5452
key,
55-
splitOrSplits,
56-
flagSetOrFlagSets,
53+
nameOrNames,
5754
attributes
5855
};
5956
}
@@ -66,7 +63,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
6663
const params = validateEvaluationParams(maybeKey, maybeFeatureFlagName, maybeAttributes, GET_TREATMENT);
6764

6865
if (params.valid) {
69-
return client.getTreatment(params.key as SplitIO.SplitKey, params.splitOrSplits as string, params.attributes as SplitIO.Attributes | undefined);
66+
return client.getTreatment(params.key as SplitIO.SplitKey, params.nameOrNames as string, params.attributes as SplitIO.Attributes | undefined);
7067
} else {
7168
return wrapResult(CONTROL);
7269
}
@@ -76,7 +73,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
7673
const params = validateEvaluationParams(maybeKey, maybeFeatureFlagName, maybeAttributes, GET_TREATMENT_WITH_CONFIG);
7774

7875
if (params.valid) {
79-
return client.getTreatmentWithConfig(params.key as SplitIO.SplitKey, params.splitOrSplits as string, params.attributes as SplitIO.Attributes | undefined);
76+
return client.getTreatmentWithConfig(params.key as SplitIO.SplitKey, params.nameOrNames as string, params.attributes as SplitIO.Attributes | undefined);
8077
} else {
8178
return wrapResult(objectAssign({}, CONTROL_WITH_CONFIG));
8279
}
@@ -86,10 +83,10 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
8683
const params = validateEvaluationParams(maybeKey, maybeFeatureFlagNames, maybeAttributes, GET_TREATMENTS);
8784

8885
if (params.valid) {
89-
return client.getTreatments(params.key as SplitIO.SplitKey, params.splitOrSplits as string[], params.attributes as SplitIO.Attributes | undefined);
86+
return client.getTreatments(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined);
9087
} else {
9188
const res: SplitIO.Treatments = {};
92-
if (params.splitOrSplits) (params.splitOrSplits as string[]).forEach((split: string) => res[split] = CONTROL);
89+
if (params.nameOrNames) (params.nameOrNames as string[]).forEach((split: string) => res[split] = CONTROL);
9390

9491
return wrapResult(res);
9592
}
@@ -99,50 +96,50 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
9996
const params = validateEvaluationParams(maybeKey, maybeFeatureFlagNames, maybeAttributes, GET_TREATMENTS_WITH_CONFIG);
10097

10198
if (params.valid) {
102-
return client.getTreatmentsWithConfig(params.key as SplitIO.SplitKey, params.splitOrSplits as string[], params.attributes as SplitIO.Attributes | undefined);
99+
return client.getTreatmentsWithConfig(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined);
103100
} else {
104101
const res: SplitIO.TreatmentsWithConfig = {};
105-
if (params.splitOrSplits) (params.splitOrSplits as string[]).forEach(split => res[split] = objectAssign({}, CONTROL_WITH_CONFIG));
102+
if (params.nameOrNames) (params.nameOrNames as string[]).forEach(split => res[split] = objectAssign({}, CONTROL_WITH_CONFIG));
106103

107104
return wrapResult(res);
108105
}
109106
}
110107

111108
function getTreatmentsByFlagSets(maybeKey: SplitIO.SplitKey, maybeFlagSets: string[], maybeAttributes?: SplitIO.Attributes) {
112-
const params = validateEvaluationParams(maybeKey, undefined, maybeAttributes, GET_TREATMENTS_BY_FLAG_SETS, maybeFlagSets);
109+
const params = validateEvaluationParams(maybeKey, maybeFlagSets, maybeAttributes, GET_TREATMENTS_BY_FLAG_SETS);
113110

114111
if (params.valid) {
115-
return client.getTreatmentsByFlagSets(params.key as SplitIO.SplitKey, params.flagSetOrFlagSets as string[], params.attributes as SplitIO.Attributes | undefined);
112+
return client.getTreatmentsByFlagSets(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined);
116113
} else {
117114
return wrapResult({});
118115
}
119116
}
120117

121118
function getTreatmentsWithConfigByFlagSets(maybeKey: SplitIO.SplitKey, maybeFlagSets: string[], maybeAttributes?: SplitIO.Attributes) {
122-
const params = validateEvaluationParams(maybeKey, undefined, maybeAttributes, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, maybeFlagSets);
119+
const params = validateEvaluationParams(maybeKey, maybeFlagSets, maybeAttributes, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
123120

124121
if (params.valid) {
125-
return client.getTreatmentsWithConfigByFlagSets(params.key as SplitIO.SplitKey, params.flagSetOrFlagSets as string[], params.attributes as SplitIO.Attributes | undefined);
122+
return client.getTreatmentsWithConfigByFlagSets(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined);
126123
} else {
127124
return wrapResult({});
128125
}
129126
}
130127

131128
function getTreatmentsByFlagSet(maybeKey: SplitIO.SplitKey, maybeFlagSet: string, maybeAttributes?: SplitIO.Attributes) {
132-
const params = validateEvaluationParams(maybeKey, undefined, maybeAttributes, GET_TREATMENTS_BY_FLAG_SET, [maybeFlagSet]);
129+
const params = validateEvaluationParams(maybeKey, [maybeFlagSet], maybeAttributes, GET_TREATMENTS_BY_FLAG_SET);
133130

134131
if (params.valid) {
135-
return client.getTreatmentsByFlagSet(params.key as SplitIO.SplitKey, params.flagSetOrFlagSets[0] as string, params.attributes as SplitIO.Attributes | undefined);
132+
return client.getTreatmentsByFlagSet(params.key as SplitIO.SplitKey, (params.nameOrNames as string[])[0], params.attributes as SplitIO.Attributes | undefined);
136133
} else {
137134
return wrapResult({});
138135
}
139136
}
140137

141138
function getTreatmentsWithConfigByFlagSet(maybeKey: SplitIO.SplitKey, maybeFlagSet: string, maybeAttributes?: SplitIO.Attributes) {
142-
const params = validateEvaluationParams(maybeKey, undefined, maybeAttributes, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, [maybeFlagSet]);
139+
const params = validateEvaluationParams(maybeKey, [maybeFlagSet], maybeAttributes, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
143140

144141
if (params.valid) {
145-
return client.getTreatmentsWithConfigByFlagSet(params.key as SplitIO.SplitKey, params.flagSetOrFlagSets[0] as string, params.attributes as SplitIO.Attributes | undefined);
142+
return client.getTreatmentsWithConfigByFlagSet(params.key as SplitIO.SplitKey, (params.nameOrNames as string[])[0], params.attributes as SplitIO.Attributes | undefined);
146143
} else {
147144
return wrapResult({});
148145
}

src/sdkFactory/__tests__/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const fullParamsForSyncSDK = {
4949
platform: {
5050
getEventSource: jest.fn(),
5151
getFetch: jest.fn(),
52+
getOptions: jest.fn(),
5253
EventEmitter
5354
},
5455
splitApiFactory: jest.fn(),

src/sdkFactory/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ export interface IPlatform {
1717
/**
1818
* If provided, it is used to retrieve the Fetch API for HTTP requests. Otherwise, the global fetch is used.
1919
*/
20-
getFetch?: () => (IFetch | undefined)
20+
getFetch?: (settings: ISettings) => (IFetch | undefined)
21+
/**
22+
* If provided, it is used to pass additional options to fetch and eventsource calls.
23+
*/
24+
getOptions?: (settings: ISettings) => object
2125
/**
2226
* If provided, it is used to retrieve the EventSource constructor for streaming support.
2327
*/
24-
getEventSource?: () => (IEventSourceConstructor | undefined)
28+
getEventSource?: (settings: ISettings) => (IEventSourceConstructor | undefined)
2529
/**
2630
* EventEmitter constructor, like NodeJS.EventEmitter or a polyfill.
2731
*/

src/services/splitApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ function userKeyToQueryParam(userKey: string) {
2222
*/
2323
export function splitApiFactory(
2424
settings: ISettings,
25-
platform: Pick<IPlatform, 'getFetch'>,
25+
platform: IPlatform,
2626
telemetryTracker: ITelemetryTracker
2727
): ISplitApi {
2828

2929
const urls = settings.urls;
3030
const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
3131
const SplitSDKImpressionsMode = settings.sync.impressionsMode;
3232
const flagSpecVersion = settings.sync.flagSpecVersion;
33-
const splitHttpClient = splitHttpClientFactory(settings, platform.getFetch);
33+
const splitHttpClient = splitHttpClientFactory(settings, platform);
3434

3535
return {
3636
// @TODO throw errors if health check requests fail, to log them in the Synchronizer

src/services/splitHttpClient.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ const messageNoFetch = 'Global fetch API is not available.';
1010
* Factory of Split HTTP clients, which are HTTP clients with predefined headers for Split endpoints.
1111
*
1212
* @param settings SDK settings, used to access authorizationKey, logger instance and metadata (SDK version, ip and hostname) to set additional headers
13-
* @param getFetch retrieves the Fetch API for HTTP requests
13+
* @param platform object containing environment-specific dependencies
1414
*/
15-
export function splitHttpClientFactory(settings: ISettings, getFetch?: IPlatform['getFetch']): ISplitHttpClient {
15+
export function splitHttpClientFactory(settings: ISettings, { getOptions, getFetch }: IPlatform): ISplitHttpClient {
1616

1717
const { log, core: { authorizationKey }, version, runtime: { ip, hostname } } = settings;
18-
const fetch = getFetch && getFetch();
18+
const options = getOptions && getOptions(settings);
19+
const fetch = getFetch && getFetch(settings);
1920

2021
// if fetch is not available, log Error
2122
if (!fetch) log.error(ERROR_CLIENT_CANNOT_GET_READY, [messageNoFetch]);
@@ -32,11 +33,11 @@ export function splitHttpClientFactory(settings: ISettings, getFetch?: IPlatform
3233

3334
return function httpClient(url: string, reqOpts: IRequestOptions = {}, latencyTracker: (error?: NetworkError) => void = () => { }, logErrorsAsInfo: boolean = false): Promise<IResponse> {
3435

35-
const request = {
36+
const request = objectAssign({
3637
headers: reqOpts.headers ? objectAssign({}, headers, reqOpts.headers) : headers,
3738
method: reqOpts.method || 'GET',
3839
body: reqOpts.body
39-
};
40+
}, options);
4041

4142
// using `fetch(url, options)` signature to work with unfetch, a lightweight ponyfill of fetch API.
4243
return fetch ? fetch(url, request)

0 commit comments

Comments
 (0)