Skip to content
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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": "1.2.1-rc.3",
"version": "1.2.1-rc.5",
"description": "Split Javascript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
5 changes: 4 additions & 1 deletion src/logger/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export const SYNC_CONTINUE_POLLING = 118;
export const SYNC_STOP_POLLING = 119;
export const EVENTS_TRACKER_SUCCESS = 120;
export const IMPRESSIONS_TRACKER_SUCCESS = 121;
export const USER_CONSENT_UPDATED = 122;
export const USER_CONSENT_NOT_UPDATED = 123;

export const ENGINE_VALUE_INVALID = 200;
export const ENGINE_VALUE_NO_ATTRIBUTES = 201;
Expand Down Expand Up @@ -115,10 +117,11 @@ export const ERROR_INVALID_KEY_OBJECT = 317;
export const ERROR_INVALID = 318;
export const ERROR_EMPTY = 319;
export const ERROR_EMPTY_ARRAY = 320;
export const ERROR_INVALID_IMPRESSIONS_MODE = 321;
export const ERROR_INVALID_CONFIG_PARAM = 321;
export const ERROR_HTTP = 322;
export const ERROR_LOCALHOST_MODULE_REQUIRED = 323;
export const ERROR_STORAGE_INVALID = 324;
export const ERROR_NOT_BOOLEAN = 325;

// Log prefixes (a.k.a. tags or categories)
export const LOG_PREFIX_SETTINGS = 'settings';
Expand Down
6 changes: 3 additions & 3 deletions src/logger/messages/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export const codesDebug: [number, string][] = codesInfo.concat([
// SDK
[c.CLEANUP_REGISTERING, c.LOG_PREFIX_CLEANUP + 'Registering cleanup handler %s'],
[c.CLEANUP_DEREGISTERING, c.LOG_PREFIX_CLEANUP + 'Deregistering cleanup handler %s'],
[c.RETRIEVE_CLIENT_DEFAULT, ' Retrieving default SDK client.'],
[c.RETRIEVE_CLIENT_EXISTING, ' Retrieving existing SDK client.'],
[c.RETRIEVE_MANAGER, ' Retrieving manager instance.'],
[c.RETRIEVE_CLIENT_DEFAULT, 'Retrieving default SDK client.'],
[c.RETRIEVE_CLIENT_EXISTING, 'Retrieving existing SDK client.'],
[c.RETRIEVE_MANAGER, 'Retrieving manager instance.'],
// synchronizer
[c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Splits data: \n%s'],
[c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up split update using since = %s'],
Expand Down
5 changes: 3 additions & 2 deletions src/logger/messages/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const codesError: [number, string][] = [
[c.ERROR_ENGINE_COMBINER_IFELSEIF, c.LOG_PREFIX_ENGINE_COMBINER + 'Invalid Split, no valid rules found'],
// SDK
[c.ERROR_LOGLEVEL_INVALID, 'logger: Invalid Log Level - No changes to the logs will be applied.'],
[c.ERROR_CLIENT_CANNOT_GET_READY, ' The SDK will not get ready. Reason: %s'],
[c.ERROR_CLIENT_CANNOT_GET_READY, 'The SDK will not get ready. Reason: %s'],
[c.ERROR_IMPRESSIONS_TRACKER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Could not store impressions bulk with %s impression(s). Error: %s'],
[c.ERROR_IMPRESSIONS_LISTENER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Impression listener logImpression method threw: %s.'],
[c.ERROR_EVENTS_TRACKER, c.LOG_PREFIX_EVENTS_TRACKER + 'Failed to queue %s'],
Expand All @@ -28,8 +28,9 @@ export const codesError: [number, string][] = [
[c.ERROR_INVALID, '%s: you passed an invalid %s. It must be a non-empty string.'],
[c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'],
[c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'],
[c.ERROR_NOT_BOOLEAN, '%s: provided param must be a boolean value.'],
// initialization / settings validation
[c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'],
[c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'],
[c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'],
[c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': The provided storage is invalid.%s Fallbacking into default MEMORY storage'],
];
6 changes: 4 additions & 2 deletions src/logger/messages/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export const codesInfo: [number, string][] = codesWarn.concat([
// SDK
[c.IMPRESSION, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Split: %s. Key: %s. Evaluation: %s. Label: %s'],
[c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Queueing corresponding impression.'],
[c.NEW_SHARED_CLIENT, ' New shared client instance created.'],
[c.NEW_FACTORY, ' New Split SDK instance created.'],
[c.NEW_SHARED_CLIENT, 'New shared client instance created.'],
[c.NEW_FACTORY, 'New Split SDK instance created.'],
[c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'],
[c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'],
[c.USER_CONSENT_UPDATED, 'setUserConsent: consent status changed from %s to %s.'],
[c.USER_CONSENT_NOT_UPDATED, 'setUserConsent: call had no effect because it was the current consent status (%s).'],

// synchronizer
[c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'],
Expand Down
39 changes: 39 additions & 0 deletions src/sdkFactory/__tests__/userConsentProps.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { userConsentProps } from '../userConsentProps';
import { syncTaskFactory } from '../../sync/__tests__/syncTask.mock';
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';

test('userConsentProps', () => {
const settings = { ...fullSettings };
const syncManager = { submitter: syncTaskFactory() };

// @ts-ignore
const props = userConsentProps(settings, syncManager);

// getUserConsent returns settings.userConsent
expect(props.getUserConsent()).toBe(settings.userConsent);

// setting user consent to 'GRANTED'
expect(props.setUserConsent(true)).toBe(true);
expect(props.setUserConsent(true)).toBe(true); // calling again has no affect
expect(syncManager.submitter.start).toBeCalledTimes(1); // submitter resumed
expect(syncManager.submitter.stop).toBeCalledTimes(0);
expect(props.getUserConsent()).toBe('GRANTED');

// setting user consent to 'DECLINED'
expect(props.setUserConsent(false)).toBe(true);
expect(props.setUserConsent(false)).toBe(true); // calling again has no affect
expect(syncManager.submitter.start).toBeCalledTimes(1);
expect(syncManager.submitter.stop).toBeCalledTimes(1); // submitter paused
expect(props.getUserConsent()).toBe('DECLINED');

// Invalid values have no effect
expect(props.setUserConsent('DECLINED')).toBe(false); // strings are not valid
expect(props.setUserConsent('GRANTED')).toBe(false);
expect(props.setUserConsent(undefined)).toBe(false);
expect(props.setUserConsent({})).toBe(false);

expect(syncManager.submitter.start).toBeCalledTimes(1);
expect(syncManager.submitter.stop).toBeCalledTimes(1);
expect(props.getUserConsent()).toBe('DECLINED');

});
9 changes: 5 additions & 4 deletions src/sdkFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { createLoggerAPI } from '../logger/sdkLogger';
import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants';
import { metadataBuilder } from '../storages/metadataBuilder';
import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../readiness/constants';
import { objectAssign } from '../utils/lang/objectAssign';

/**
* Modular SDK factory
*/
export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.ISDK | SplitIO.IAsyncSDK {

const { settings, platform, storageFactory, splitApiFactory,
const { settings, platform, storageFactory, splitApiFactory, extraProps,
syncManagerFactory, SignalListener, impressionsObserverFactory, impressionListener,
integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory } = params;
const log = settings.log;
Expand Down Expand Up @@ -88,12 +89,12 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.

log.info(NEW_FACTORY);

return {
// @ts-ignore
return objectAssign({
// Split evaluation and event tracking engine
client: clientMethod,

// Manager API to explore available information
// @ts-ignore
manager() {
log.debug(RETRIEVE_MANAGER);
return managerInstance;
Expand All @@ -103,5 +104,5 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
Logger: createLoggerAPI(settings.log),

settings,
};
}, extraProps && extraProps(settings, syncManager));
}
2 changes: 2 additions & 0 deletions src/sdkFactory/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,6 @@ export interface ISdkFactoryParams {
// Impression observer factory. If provided, will be used for impressions dedupe
impressionsObserverFactory?: () => IImpressionObserver

// Optional function to assign additional properties to the factory instance
extraProps?: (settings: ISettings, syncManager?: ISyncManager) => object
}
40 changes: 40 additions & 0 deletions src/sdkFactory/userConsentProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED, USER_CONSENT_NOT_UPDATED } from '../logger/constants';
import { ISyncManager } from '../sync/types';
import { ISettings } from '../types';
import { CONSENT_GRANTED, CONSENT_DECLINED } from '../utils/constants';
import { isBoolean } from '../utils/lang';

// Extend client-side factory instances with user consent getter/setter
export function userConsentProps(settings: ISettings, syncManager?: ISyncManager) {

const log = settings.log;

return {
setUserConsent(consent: unknown) {
// validate input param
if (!isBoolean(consent)) {
log.warn(ERROR_NOT_BOOLEAN, ['setUserConsent']);
return false;
}

const newConsentStatus = consent ? CONSENT_GRANTED : CONSENT_DECLINED;

if (settings.userConsent !== newConsentStatus) { // @ts-ignore, modify readonly prop
settings.userConsent = newConsentStatus;

if (consent) syncManager?.submitter?.start(); // resumes submitters if transitioning to GRANTED
else syncManager?.submitter?.stop(); // pauses submitters if transitioning to DECLINED

log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]);
} else {
log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]);
}

return true;
},

getUserConsent() {
return settings.userConsent;
}
};
}
8 changes: 2 additions & 6 deletions src/storages/KeyBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { endsWith, startsWith } from '../utils/lang';
import { startsWith } from '../utils/lang';

const everythingAtTheEnd = /[^.]+$/;

const DEFAULT_PREFIX = 'SPLITIO';

export function validatePrefix(prefix: unknown) {
return prefix && typeof prefix === 'string' ?
endsWith(prefix, '.' + DEFAULT_PREFIX) ?
prefix : // suffix already appended
prefix + '.' + DEFAULT_PREFIX : // append suffix
DEFAULT_PREFIX; // use default prefix if none is provided
return prefix ? prefix + '.SPLITIO' : 'SPLITIO';
}

export class KeyBuilder {
Expand Down
9 changes: 8 additions & 1 deletion src/utils/lang/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function get(obj: any, prop: any, val: any): any {
/**
* Parses an array into a map of different arrays, grouping by the specified prop value.
*/
export function groupBy<T extends Record<string, any> >(source: T[], prop: string): Record<string, T[]> {
export function groupBy<T extends Record<string, any>>(source: T[], prop: string): Record<string, T[]> {
const map: Record<string, any[]> = {};

if (Array.isArray(source) && isString(prop)) {
Expand Down Expand Up @@ -164,6 +164,13 @@ export function isString(val: any): val is string {
return typeof val === 'string' || val instanceof String;
}

/**
* String sanitizer. Returns the provided value converted to uppercase if it is a string.
*/
export function stringToUpperCase(val: any) {
return isString(val) ? val.toUpperCase() : val;
}

/**
* Deep copy version of Object.assign using recursion.
* There are some assumptions here. It's for internal use and we don't need verbose errors
Expand Down
3 changes: 2 additions & 1 deletion src/utils/settingsValidation/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const minimalSettingsParams = {
version: 'javascript-test',
},
runtime: () => ({ ip: false, hostname: false } as ISettings['runtime']),
logger: () => (loggerMock as ISettings['log'])
logger: () => (loggerMock as ISettings['log']),
consent: () => undefined
};

describe('settingsValidation', () => {
Expand Down
15 changes: 15 additions & 0 deletions src/utils/settingsValidation/consent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants';
import { ILogger } from '../../logger/types';
import { CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN } from '../constants';
import { stringToUpperCase } from '../lang';

const userConsentValues = [CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN];

export function validateConsent({ userConsent, log }: { userConsent: any, log: ILogger }) {
userConsent = stringToUpperCase(userConsent);

if (userConsentValues.indexOf(userConsent) > -1) return userConsent;

log.error(ERROR_INVALID_CONFIG_PARAM, ['userConsent', userConsentValues, CONSENT_GRANTED]);
return CONSENT_GRANTED;
}
16 changes: 8 additions & 8 deletions src/utils/settingsValidation/impressionsMode.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ERROR_INVALID_IMPRESSIONS_MODE } from '../../logger/constants';
import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants';
import { ILogger } from '../../logger/types';
import { SplitIO } from '../../types';
import { DEBUG, OPTIMIZED } from '../constants';
import { stringToUpperCase } from '../lang';

export function validImpressionsMode(log: ILogger, impressionsMode: string): SplitIO.ImpressionsMode {
impressionsMode = impressionsMode.toUpperCase();
if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) === -1) {
log.error(ERROR_INVALID_IMPRESSIONS_MODE, [[DEBUG, OPTIMIZED], OPTIMIZED]);
impressionsMode = OPTIMIZED;
}
export function validImpressionsMode(log: ILogger, impressionsMode: any): SplitIO.ImpressionsMode {
impressionsMode = stringToUpperCase(impressionsMode);

return impressionsMode as SplitIO.ImpressionsMode;
if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) > -1) return impressionsMode;

log.error(ERROR_INVALID_CONFIG_PARAM, ['impressionsMode', [DEBUG, OPTIMIZED], OPTIMIZED]);
return OPTIMIZED;
}
6 changes: 5 additions & 1 deletion src/utils/settingsValidation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function fromSecondsToMillis(n: number) {
*/
export function settingsValidation(config: unknown, validationParams: ISettingsValidationParams) {

const { defaults, runtime, storage, integrations, logger, localhost } = validationParams;
const { defaults, runtime, storage, integrations, logger, localhost, consent } = validationParams;

// creates a settings object merging base, defaults and config objects.
const withDefaults = merge({}, base, defaults, config) as ISettings;
Expand Down Expand Up @@ -161,5 +161,9 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
// ensure a valid impressionsMode
withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode);

// ensure a valid user consent value
// @ts-ignore, modify readonly prop
withDefaults.userConsent = consent(withDefaults);

return withDefaults;
}
2 changes: 2 additions & 0 deletions src/utils/settingsValidation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export interface ISettingsValidationParams {
logger: (settings: ISettings) => ISettings['log'],
/** Localhost mode validator (`settings.sync.localhostMode`) */
localhost?: (settings: ISettings) => ISettings['sync']['localhostMode'],
/** User consent validator (`settings.userConsent`) */
consent: (settings: ISettings) => ISettings['userConsent'],
}