diff --git a/.eslintrc b/.eslintrc index 4f5939ef..a44d175d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -67,7 +67,8 @@ ], "rules": { "no-restricted-syntax": ["error", "ForOfStatement", "ForInStatement", "ArrayPattern"], - "compat/compat": ["error", "defaults, not ie < 10, not node < 6"] + "compat/compat": ["error", "defaults, not ie < 10, not node < 6"], + "no-throw-literal": "error" }, "parserOptions": { "ecmaVersion": 2015, diff --git a/.nvmrc b/.nvmrc index a62187b7..958b5a36 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v10.16 +v14 diff --git a/jest.config.js b/jest.config.js index ed62d821..439ab3b6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,7 @@ module.exports = { preset: 'ts-jest', // Test files are .js and .ts files inside of __tests__ folders and with a suffix of .test or .spec - testMatch: ['**/__tests__/**/?(*.)+(spec|test).[jt]s'], + testMatch: ['/src/**/__tests__/**/?(*.)+(spec|test).[jt]s'], // Included files for test coverage (npm run test:coverage) collectCoverageFrom: [ diff --git a/package-lock.json b/package-lock.json index a4d7edff..6619f383 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.0.1-beta.2", + "version": "0.0.1-beta.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d0a9df2a..f3b4817d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.0.1-beta.2", + "version": "0.0.1-beta.3", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 96bd81bc..d7466caa 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -8,6 +8,7 @@ import { ISplit, MaybeThenable } from '../dtos/types'; import { SplitIO } from '../types'; import { IStorageAsync, IStorageSync } from '../storages/types'; import { IEvaluation, IEvaluationResult, IEvaluator, ISplitEvaluator } from './types'; +import { ILogger } from '../logger/types'; function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { return { @@ -26,9 +27,9 @@ export default class Engine { } } - static parse(splitFlatStructure: ISplit, storage: IStorageSync | IStorageAsync) { + static parse(log: ILogger, splitFlatStructure: ISplit, storage: IStorageSync | IStorageAsync) { const conditions = splitFlatStructure.conditions; - const evaluator = parser(conditions, storage); + const evaluator = parser(log, conditions, storage); return new Engine(splitFlatStructure, evaluator); } diff --git a/src/evaluator/__tests__/evaluate-feature.spec.ts b/src/evaluator/__tests__/evaluate-feature.spec.ts index e5416a20..77af0c9d 100644 --- a/src/evaluator/__tests__/evaluate-feature.spec.ts +++ b/src/evaluator/__tests__/evaluate-feature.spec.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { evaluateFeature } from '../index'; import * as LabelsConstants from '../../utils/labels'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; const splitsMock = { regular: '{"changeNumber":1487277320548,"trafficAllocationSeed":1667452163,"trafficAllocation":100,"trafficTypeName":"user","name":"always-on","seed":1684183541,"configurations":{},"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}', @@ -31,10 +32,11 @@ test('EVALUATOR / should return label exception, treatment control and config nu config: null }; const evaluationPromise = evaluateFeature( + loggerMock, 'fake-key', 'throw_exception', null, - mockStorage + mockStorage, ); // This validation is async because the only exception possible when retrieving a Split would happen with Async storages. @@ -54,79 +56,88 @@ test('EVALUATOR / should return right label, treatment and config if storage ret }; const evaluationWithConfig = evaluateFeature( + loggerMock, 'fake-key', 'config', null, - mockStorage + mockStorage, ); expect(evaluationWithConfig).toEqual(expectedOutput); // If the split is retrieved successfully we should get the right evaluation result, label and config. const evaluationNotFound = evaluateFeature( + loggerMock, 'fake-key', 'not_existent_split', null, - mockStorage + mockStorage, ); expect(evaluationNotFound).toEqual(expectedOutputControl); // If the split is not retrieved successfully because it does not exist, we should get the right evaluation result, label and config. const evaluation = evaluateFeature( + loggerMock, 'fake-key', 'regular', null, - mockStorage + mockStorage, ); expect(evaluation).toEqual({ ...expectedOutput, config: null }); // If the split is retrieved successfully we should get the right evaluation result, label and config. If Split has no config it should have config equal null. const evaluationKilled = evaluateFeature( + loggerMock, 'fake-key', 'killed', null, - mockStorage + mockStorage, ); expect(evaluationKilled).toEqual({ ...expectedOutput, treatment: 'off', config: null, label: LabelsConstants.SPLIT_KILLED }); // If the split is retrieved but is killed, we should get the right evaluation result, label and config. const evaluationArchived = evaluateFeature( + loggerMock, 'fake-key', 'archived', null, - mockStorage + mockStorage, ); expect(evaluationArchived).toEqual({ ...expectedOutput, treatment: 'control', label: LabelsConstants.SPLIT_ARCHIVED, config: null }); // If the split is retrieved but is archived, we should get the right evaluation result, label and config. const evaluationtrafficAlocation1 = evaluateFeature( + loggerMock, 'fake-key', 'trafficAlocation1', null, - mockStorage + mockStorage, ); expect(evaluationtrafficAlocation1).toEqual({ ...expectedOutput, label: LabelsConstants.NOT_IN_SPLIT, config: null, treatment: 'off' }); // If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and config. const evaluationKilledWithConfig = evaluateFeature( + loggerMock, 'fake-key', 'killedWithConfig', null, - mockStorage + mockStorage, ); expect(evaluationKilledWithConfig).toEqual({ ...expectedOutput, treatment: 'off', label: LabelsConstants.SPLIT_KILLED }); // If the split is retrieved but is killed, we should get the right evaluation result, label and config. const evaluationArchivedWithConfig = evaluateFeature( + loggerMock, 'fake-key', 'archivedWithConfig', null, - mockStorage + mockStorage, ); expect(evaluationArchivedWithConfig).toEqual({ ...expectedOutput, treatment: 'control', label: LabelsConstants.SPLIT_ARCHIVED, config: null }); // If the split is retrieved but is archived, we should get the right evaluation result, label and config. const evaluationtrafficAlocation1WithConfig = evaluateFeature( + loggerMock, 'fake-key', 'trafficAlocation1WithConfig', null, - mockStorage + mockStorage, ); expect(evaluationtrafficAlocation1WithConfig).toEqual({ ...expectedOutput, label: LabelsConstants.NOT_IN_SPLIT, treatment: 'off' }); // If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and config. diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index f5dc13b5..7f836d1b 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { evaluateFeatures } from '../index'; import * as LabelsConstants from '../../utils/labels'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; const splitsMock = { regular: '{"changeNumber":1487277320548,"trafficAllocationSeed":1667452163,"trafficAllocation":100,"trafficTypeName":"user","name":"always-on","seed":1684183541,"configurations":{},"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}', @@ -43,10 +44,11 @@ test('EVALUATOR - Multiple evaluations at once / should return label exception, // This validation is async because the only exception possible when retrieving a Split would happen with Async storages. const evaluation = await evaluateFeatures( + loggerMock, 'fake-key', ['throw_exception'], null, - mockStorage + mockStorage, ); expect(evaluation).toEqual(expectedOutput); // If there was an error on the `getSplits` we should get the results for exception. @@ -66,10 +68,11 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre }; const multipleEvaluationAtOnce = await evaluateFeatures( + loggerMock, 'fake-key', ['config', 'not_existent_split', 'regular', 'killed', 'archived', 'trafficAlocation1', 'killedWithConfig', 'archivedWithConfig', 'trafficAlocation1WithConfig'], null, - mockStorage + mockStorage, ); // assert evaluationWithConfig expect(multipleEvaluationAtOnce['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config. diff --git a/src/evaluator/combiners/__tests__/and.spec.ts b/src/evaluator/combiners/__tests__/and.spec.ts index cf52100a..1990546e 100644 --- a/src/evaluator/combiners/__tests__/and.spec.ts +++ b/src/evaluator/combiners/__tests__/and.spec.ts @@ -1,15 +1,16 @@ import andCombiner from '../and'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('COMBINER AND / should always return true', async function () { - let AND = andCombiner([() => true, () => true, () => true]); + let AND = andCombiner(loggerMock, [() => true, () => true, () => true]); expect(await AND('always true')).toBe(true); // should always return true }); test('COMBINER AND / should always return false', async function () { - let AND = andCombiner([() => true, () => true, () => false]); + let AND = andCombiner(loggerMock, [() => true, () => true, () => false]); expect(await AND('always false')).toBe(false); // should always return false }); diff --git a/src/evaluator/combiners/__tests__/ifelseif.spec.ts b/src/evaluator/combiners/__tests__/ifelseif.spec.ts index 6783bb80..eb874ebc 100644 --- a/src/evaluator/combiners/__tests__/ifelseif.spec.ts +++ b/src/evaluator/combiners/__tests__/ifelseif.spec.ts @@ -1,5 +1,6 @@ // @ts-nocheck import ifElseIfCombinerFactory from '../ifelseif'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('IF ELSE IF COMBINER / should correctly propagate context parameters and predicates returns value', async function () { let inputKey = 'sample'; @@ -16,7 +17,7 @@ test('IF ELSE IF COMBINER / should correctly propagate context parameters and pr } let predicates = [evaluator]; - let ifElseIfEvaluator = ifElseIfCombinerFactory(predicates); + let ifElseIfEvaluator = ifElseIfCombinerFactory(loggerMock, predicates); expect(await ifElseIfEvaluator(inputKey, inputSeed, inputAttributes) === evaluationResult).toBe(true); console.log(`evaluator should return ${evaluationResult}`); @@ -35,7 +36,7 @@ test('IF ELSE IF COMBINER / should stop evaluating when one matcher return a tre } ]; - let ifElseIfEvaluator = ifElseIfCombinerFactory(predicates); + let ifElseIfEvaluator = ifElseIfCombinerFactory(loggerMock, predicates); expect(await ifElseIfEvaluator()).toBe('exclude'); // exclude treatment found }); @@ -53,7 +54,7 @@ test('IF ELSE IF COMBINER / should return undefined if there is none matching ru } ]; - const ifElseIfEvaluator = ifElseIfCombinerFactory(predicates); + const ifElseIfEvaluator = ifElseIfCombinerFactory(loggerMock, predicates); expect(await ifElseIfEvaluator() === undefined).toBe(true); }); diff --git a/src/evaluator/combiners/and.ts b/src/evaluator/combiners/and.ts index 761fb079..271801dd 100644 --- a/src/evaluator/combiners/and.ts +++ b/src/evaluator/combiners/and.ts @@ -1,19 +1,19 @@ import { findIndex } from '../../utils/lang'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:combiner'); +import { ILogger } from '../../logger/types'; import thenable from '../../utils/promise/thenable'; import { MaybeThenable } from '../../dtos/types'; import { IMatcher } from '../types'; +import { ENGINE_COMBINER_AND } from '../../logger/constants'; -function andResults(results: boolean[]): boolean { - // Array.prototype.every is supported by target environments - const hasMatchedAll = results.every(value => value); +export default function andCombinerContext(log: ILogger, matchers: IMatcher[]) { - log.debug(`[andCombiner] evaluates to ${hasMatchedAll}`); - return hasMatchedAll; -} + function andResults(results: boolean[]): boolean { + // Array.prototype.every is supported by target environments + const hasMatchedAll = results.every(value => value); -export default function andCombinerContext(matchers: IMatcher[]) { + log.debug(ENGINE_COMBINER_AND, [hasMatchedAll]); + return hasMatchedAll; + } return function andCombiner(...params: any): MaybeThenable { const matcherResults = matchers.map(matcher => matcher(...params)); diff --git a/src/evaluator/combiners/ifelseif.ts b/src/evaluator/combiners/ifelseif.ts index 961b05a9..9aa162f7 100644 --- a/src/evaluator/combiners/ifelseif.ts +++ b/src/evaluator/combiners/ifelseif.ts @@ -1,39 +1,39 @@ import { findIndex } from '../../utils/lang'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:combiner'); +import { ILogger } from '../../logger/types'; import thenable from '../../utils/promise/thenable'; import * as LabelsConstants from '../../utils/labels'; import { CONTROL } from '../../utils/constants'; import { SplitIO } from '../../types'; import { IEvaluation, IEvaluator, ISplitEvaluator } from '../types'; +import { ENGINE_COMBINER_IFELSEIF, ENGINE_COMBINER_IFELSEIF_NO_TREATMENT, ERROR_ENGINE_COMBINER_IFELSEIF } from '../../logger/constants'; -function unexpectedInputHandler() { - log.error('Invalid Split provided, no valid conditions found'); +export default function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): IEvaluator { - return { - treatment: CONTROL, - label: LabelsConstants.EXCEPTION - }; -} + function unexpectedInputHandler() { + log.error(ERROR_ENGINE_COMBINER_IFELSEIF); -function computeTreatment(predicateResults: Array) { - const len = predicateResults.length; + return { + treatment: CONTROL, + label: LabelsConstants.EXCEPTION + }; + } - for (let i = 0; i < len; i++) { - const evaluation = predicateResults[i]; + function computeTreatment(predicateResults: Array) { + const len = predicateResults.length; - if (evaluation !== undefined) { - log.debug(`Treatment found: ${evaluation.treatment}`); + for (let i = 0; i < len; i++) { + const evaluation = predicateResults[i]; - return evaluation; - } - } + if (evaluation !== undefined) { + log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]); - log.debug('All predicates evaluated, no treatment found.'); - return undefined; -} + return evaluation; + } + } -export default function ifElseIfCombinerContext(predicates: IEvaluator[]): IEvaluator { + log.debug(ENGINE_COMBINER_IFELSEIF_NO_TREATMENT); + return undefined; + } function ifElseIfCombiner(key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // In Async environments we are going to have async predicates. There is none way to know diff --git a/src/evaluator/condition/__tests__/engineUtils.spec.ts b/src/evaluator/condition/__tests__/engineUtils.spec.ts index ce249244..0f1a6d87 100644 --- a/src/evaluator/condition/__tests__/engineUtils.spec.ts +++ b/src/evaluator/condition/__tests__/engineUtils.spec.ts @@ -1,5 +1,6 @@ import * as engineUtils from '../engineUtils'; import Treatments from '../../treatments'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; const treatmentsMock = Treatments.parse([{ treatment: 'on', @@ -15,7 +16,7 @@ test('ENGINE / should always evaluate to "off"', () => { let startTime = Date.now(); - expect(engineUtils.getTreatment(bucketingKey, seed, treatmentsMock) === 'off').toBe(true); // treatment should be 'off' + expect(engineUtils.getTreatment(loggerMock, bucketingKey, seed, treatmentsMock) === 'off').toBe(true); // treatment should be 'off' let endTime = Date.now(); @@ -29,7 +30,7 @@ test('ENGINE / should always evaluate to "on"', () => { let startTime = Date.now(); - expect(engineUtils.getTreatment(bucketingKey, seed, treatmentsMock) === 'on').toBe(true); // treatment should be 'on' + expect(engineUtils.getTreatment(loggerMock, bucketingKey, seed, treatmentsMock) === 'on').toBe(true); // treatment should be 'on' let endTime = Date.now(); diff --git a/src/evaluator/condition/engineUtils.ts b/src/evaluator/condition/engineUtils.ts index b21ce367..bacd3b10 100644 --- a/src/evaluator/condition/engineUtils.ts +++ b/src/evaluator/condition/engineUtils.ts @@ -1,16 +1,16 @@ +import { ENGINE_BUCKET } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { bucket } from '../../utils/murmur3/murmur3'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine'); /** * Get the treatment name given a key, a seed, and the percentage of each treatment. */ -export function getTreatment(key: string, seed: number, treatments: { getTreatmentFor: (x: number) => string }) { +export function getTreatment(log: ILogger, key: string, seed: number, treatments: { getTreatmentFor: (x: number) => string }) { const _bucket = bucket(key, seed); const treatment = treatments.getTreatmentFor(_bucket); - log.debug(`[engine] using algo 'murmur' bucket ${_bucket} for key ${key} using seed ${seed} - treatment ${treatment}`); + log.debug(ENGINE_BUCKET, [_bucket, key, seed, treatment]); return treatment; } diff --git a/src/evaluator/condition/index.ts b/src/evaluator/condition/index.ts index f0ab2658..542617ea 100644 --- a/src/evaluator/condition/index.ts +++ b/src/evaluator/condition/index.ts @@ -4,11 +4,12 @@ import * as LabelsConstants from '../../utils/labels'; import { MaybeThenable } from '../../dtos/types'; import { IEvaluation, IEvaluator, ISplitEvaluator } from '../types'; import { SplitIO } from '../../types'; +import { ILogger } from '../../logger/types'; // Build Evaluation object if and only if matchingResult is true -function match(matchingResult: boolean, bucketingKey: string | undefined, seed: number, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined { +function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed: number, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined { if (matchingResult) { - const treatment = getTreatment(bucketingKey as string, seed, treatments); + const treatment = getTreatment(log, bucketingKey as string, seed, treatments); return { treatment, @@ -21,7 +22,7 @@ function match(matchingResult: boolean, bucketingKey: string | undefined, seed: } // Condition factory -export default function conditionContext(matcherEvaluator: (...args: any) => MaybeThenable, treatments: { getTreatmentFor: (x: number) => string }, label: string, conditionType: 'ROLLOUT' | 'WHITELIST'): IEvaluator { +export default function conditionContext(log: ILogger, matcherEvaluator: (...args: any) => MaybeThenable, treatments: { getTreatmentFor: (x: number) => string }, label: string, conditionType: 'ROLLOUT' | 'WHITELIST'): IEvaluator { return function conditionEvaluator(key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { @@ -40,10 +41,10 @@ export default function conditionContext(matcherEvaluator: (...args: any) => May const matches = matcherEvaluator(key, attributes, splitEvaluator); if (thenable(matches)) { - return matches.then(result => match(result, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label)); + return matches.then(result => match(log, result, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label)); } - return match(matches, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label); + return match(log, matches, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label); }; } diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 02407bef..8dd5aa14 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -7,6 +7,7 @@ import { ISplit, MaybeThenable } from '../dtos/types'; import { IStorageAsync, IStorageSync } from '../storages/types'; import { IEvaluationResult } from './types'; import { SplitIO } from '../types'; +import { ILogger } from '../logger/types'; const treatmentException = { treatment: CONTROL, @@ -15,10 +16,11 @@ const treatmentException = { }; export function evaluateFeature( + log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, - storage: IStorageSync | IStorageAsync + storage: IStorageSync | IStorageAsync, ): MaybeThenable { let stringifiedSplit; @@ -33,26 +35,29 @@ export function evaluateFeature( if (thenable(stringifiedSplit)) { return stringifiedSplit.then((result) => getEvaluation( + log, result, key, attributes, - storage + storage, )); } return getEvaluation( + log, stringifiedSplit, key, attributes, - storage + storage, ); } export function evaluateFeatures( + log: ILogger, key: SplitIO.SplitKey, splitNames: string[], attributes: SplitIO.Attributes | undefined, - storage: IStorageSync | IStorageAsync + storage: IStorageSync | IStorageAsync, ): MaybeThenable> { let stringifiedSplits; const evaluations: Record = {}; @@ -70,15 +75,16 @@ export function evaluateFeatures( } return (thenable(stringifiedSplits)) ? - stringifiedSplits.then(splits => getEvaluations(splitNames, splits, key, attributes, storage)) : - getEvaluations(splitNames, stringifiedSplits, key, attributes, storage); + stringifiedSplits.then(splits => getEvaluations(log, splitNames, splits, key, attributes, storage)) : + getEvaluations(log, splitNames, stringifiedSplits, key, attributes, storage); } function getEvaluation( + log: ILogger, stringifiedSplit: string | null, key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, - storage: IStorageSync | IStorageAsync + storage: IStorageSync | IStorageAsync, ): MaybeThenable { let evaluation: MaybeThenable = { treatment: CONTROL, @@ -88,7 +94,7 @@ function getEvaluation( if (stringifiedSplit) { const splitJSON: ISplit = JSON.parse(stringifiedSplit); - const split = Engine.parse(splitJSON, storage); + const split = Engine.parse(log, splitJSON, storage); evaluation = split.getTreatment(key, attributes, evaluateFeature); // If the storage is async, evaluation and changeNumber will return a thenable @@ -109,16 +115,18 @@ function getEvaluation( } function getEvaluations( + log: ILogger, splitNames: string[], splits: Record, key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, - storage: IStorageSync | IStorageAsync + storage: IStorageSync | IStorageAsync, ): MaybeThenable> { const result: Record = {}; const thenables: Promise[] = []; splitNames.forEach(splitName => { const evaluation = getEvaluation( + log, splits[splitName], key, attributes, diff --git a/src/evaluator/matchers/__tests__/all.spec.ts b/src/evaluator/matchers/__tests__/all.spec.ts index 2f5f44ec..d4a47b09 100644 --- a/src/evaluator/matchers/__tests__/all.spec.ts +++ b/src/evaluator/matchers/__tests__/all.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER ALL_KEYS / should always return true', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { type: matcherTypes.ALL_KEYS, value: undefined } as IMatcherDto) as IMatcher; diff --git a/src/evaluator/matchers/__tests__/between.spec.ts b/src/evaluator/matchers/__tests__/between.spec.ts index d3b1840a..ca95ba16 100644 --- a/src/evaluator/matchers/__tests__/between.spec.ts +++ b/src/evaluator/matchers/__tests__/between.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and 20', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.BETWEEN, value: { diff --git a/src/evaluator/matchers/__tests__/boolean.spec.ts b/src/evaluator/matchers/__tests__/boolean.spec.ts index d03ee30f..515b46d5 100644 --- a/src/evaluator/matchers/__tests__/boolean.spec.ts +++ b/src/evaluator/matchers/__tests__/boolean.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER BOOLEAN / should return true ONLY when the value is true', function () { // @ts-ignore - const matcher = matcherFactory({ + const matcher = matcherFactory(loggerMock, { type: matcherTypes.EQUAL_TO_BOOLEAN, value: true } as IMatcherDto) as IMatcher; diff --git a/src/evaluator/matchers/__tests__/cont_all.spec.ts b/src/evaluator/matchers/__tests__/cont_all.spec.ts index 4acaac65..8ea23697 100644 --- a/src/evaluator/matchers/__tests__/cont_all.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_all.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER CONTAINS_ALL_OF_SET / should return true ONLY when value contains all of set ["update", "add"]', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_ALL_OF_SET, value: ['update', 'add'] diff --git a/src/evaluator/matchers/__tests__/cont_any.spec.ts b/src/evaluator/matchers/__tests__/cont_any.spec.ts index 87d40b22..13896900 100644 --- a/src/evaluator/matchers/__tests__/cont_any.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_any.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER CONTAINS_ANY_OF_SET / should return true ONLY when value contains any of set ["update", "add"]', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_ANY_OF_SET, value: ['update', 'add'] diff --git a/src/evaluator/matchers/__tests__/cont_str.spec.ts b/src/evaluator/matchers/__tests__/cont_str.spec.ts index 91aead24..f8c56d4e 100644 --- a/src/evaluator/matchers/__tests__/cont_str.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_str.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER CONTAINS_STRING / should return true ONLY when the value is contained in ["roni", "bad", "ar"]', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_STRING, value: ['roni', 'bad', 'ar'] diff --git a/src/evaluator/matchers/__tests__/dependency.spec.ts b/src/evaluator/matchers/__tests__/dependency.spec.ts index 5e7cd5b3..6f4f62ec 100644 --- a/src/evaluator/matchers/__tests__/dependency.spec.ts +++ b/src/evaluator/matchers/__tests__/dependency.spec.ts @@ -3,6 +3,7 @@ import matcherFactory from '..'; import { evaluateFeature } from '../../index'; import { IMatcher, IMatcherDto } from '../../types'; import { IStorageSync } from '../../../storages/types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; const ALWAYS_ON_SPLIT = '{"trafficTypeName":"user","name":"always-on","trafficAllocation":100,"trafficAllocationSeed":1012950810,"seed":-725161385,"status":"ACTIVE","killed":false,"defaultTreatment":"off","changeNumber":1494364996459,"algo":2,"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}'; const ALWAYS_OFF_SPLIT = '{"trafficTypeName":"user","name":"always-off","trafficAllocation":100,"trafficAllocationSeed":-331690370,"seed":403891040,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1494365020316,"algo":2,"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment all"}]}'; @@ -18,7 +19,7 @@ const mockStorage = { }; test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split returns one of the expected treatments', function () { - const matcherTrueAlwaysOn = matcherFactory({ + const matcherTrueAlwaysOn = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'always-on', @@ -26,7 +27,7 @@ test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split ret } } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; - const matcherFalseAlwaysOn = matcherFactory({ + const matcherFalseAlwaysOn = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'always-on', @@ -34,7 +35,7 @@ test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split ret } } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; - const matcherTrueAlwaysOff = matcherFactory({ + const matcherTrueAlwaysOff = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'always-off', @@ -42,7 +43,7 @@ test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split ret } } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; - const matcherFalseAlwaysOff = matcherFactory({ + const matcherFalseAlwaysOff = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'always-off', @@ -57,7 +58,7 @@ test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split ret }); test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { - const matcherParentNotExist = matcherFactory({ + const matcherParentNotExist = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'not-existent-split', @@ -66,7 +67,7 @@ test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; // @ts-ignore - const matcherNoTreatmentsExpected = matcherFactory({ + const matcherNoTreatmentsExpected = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'always-on', @@ -75,7 +76,7 @@ test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; // @ts-ignore - const matcherParentNameEmpty = matcherFactory({ + const matcherParentNameEmpty = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: '', @@ -84,7 +85,7 @@ test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; // @ts-ignore - const matcherParentNameWrongType = matcherFactory({ + const matcherParentNameWrongType = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: { some: 44 }, @@ -92,7 +93,7 @@ test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { } } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; - const matcherExpectedTreatmentWrongTypeMatching = matcherFactory({ + const matcherExpectedTreatmentWrongTypeMatching = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'always-on', @@ -100,7 +101,7 @@ test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { } } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; - const matcherExpectedTreatmentWrongTypeNotMatching = matcherFactory({ + const matcherExpectedTreatmentWrongTypeNotMatching = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'always-off', @@ -110,7 +111,7 @@ test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { } as IMatcherDto, mockStorage as IStorageSync) as IMatcher; // @ts-ignore - const matcherExpectationsListWrongType = matcherFactory({ + const matcherExpectationsListWrongType = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { split: 'always-on', diff --git a/src/evaluator/matchers/__tests__/eq.spec.ts b/src/evaluator/matchers/__tests__/eq.spec.ts index 99380de4..40b40f8d 100644 --- a/src/evaluator/matchers/__tests__/eq.spec.ts +++ b/src/evaluator/matchers/__tests__/eq.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER EQUAL / should return true ONLY when the value is equal to 10', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.EQUAL_TO, value: 10 diff --git a/src/evaluator/matchers/__tests__/eq_set.spec.ts b/src/evaluator/matchers/__tests__/eq_set.spec.ts index 49bc1c72..17450cf6 100644 --- a/src/evaluator/matchers/__tests__/eq_set.spec.ts +++ b/src/evaluator/matchers/__tests__/eq_set.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER EQUAL_TO_SET / should return true ONLY when value is equal to set ["update", "add"]', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.EQUAL_TO_SET, value: ['update', 'add'] diff --git a/src/evaluator/matchers/__tests__/ew.spec.ts b/src/evaluator/matchers/__tests__/ew.spec.ts index e058d239..c5980d0b 100644 --- a/src/evaluator/matchers/__tests__/ew.spec.ts +++ b/src/evaluator/matchers/__tests__/ew.spec.ts @@ -1,25 +1,26 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["a", "b", "c"]', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.ENDS_WITH, value: ['a', 'b', 'c'] } as IMatcherDto) as IMatcher; - expect(matcher('america')).toBe(true); // america end with ["a", "b", "c"] - expect(matcher('blob')).toBe(true); // blob end with ["a", "b", "c"] - expect(matcher('zodiac')).toBe(true); // zodiac end with ["a", "b", "c"] - expect(matcher('violin')).toBe(false); // t end with ["a", "b", "c"] - expect(matcher('manager')).toBe(false); // t end with ["a", "b", "c"] + expect(matcher('america')).toBe(true); // america ends with ["a", "b", "c"] + expect(matcher('blob')).toBe(true); // blob ends with ["a", "b", "c"] + expect(matcher('zodiac')).toBe(true); // zodiac ends with ["a", "b", "c"] + expect(matcher('violin')).toBe(false); // violin doesn't end with ["a", "b", "c"] + expect(matcher('manager')).toBe(false); // manager doesn't end with ["a", "b", "c"] }); test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["demo.test.org"]', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.ENDS_WITH, value: ['demo.test.org'] diff --git a/src/evaluator/matchers/__tests__/gte.spec.ts b/src/evaluator/matchers/__tests__/gte.spec.ts index 030dd2a8..f7a4ec54 100644 --- a/src/evaluator/matchers/__tests__/gte.spec.ts +++ b/src/evaluator/matchers/__tests__/gte.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER GREATER THAN OR EQUAL / should return true ONLY when the value is greater than or equal to 10', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.GREATER_THAN_OR_EQUAL_TO, value: 10 diff --git a/src/evaluator/matchers/__tests__/lte.spec.ts b/src/evaluator/matchers/__tests__/lte.spec.ts index d592c5b5..ad920020 100644 --- a/src/evaluator/matchers/__tests__/lte.spec.ts +++ b/src/evaluator/matchers/__tests__/lte.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER LESS THAN OR EQUAL / should return true ONLY when the value is less than or equal to 10', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.LESS_THAN_OR_EQUAL_TO, value: 10 diff --git a/src/evaluator/matchers/__tests__/part_of.spec.ts b/src/evaluator/matchers/__tests__/part_of.spec.ts index a68c371b..9472d389 100644 --- a/src/evaluator/matchers/__tests__/part_of.spec.ts +++ b/src/evaluator/matchers/__tests__/part_of.spec.ts @@ -1,10 +1,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER PART_OF_SET / should return true ONLY when value is part of of set ["update", "add", "delete"]', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.PART_OF_SET, value: ['update', 'add', 'delete'] diff --git a/src/evaluator/matchers/__tests__/regex.spec.ts b/src/evaluator/matchers/__tests__/regex.spec.ts index a68005ba..3465d623 100644 --- a/src/evaluator/matchers/__tests__/regex.spec.ts +++ b/src/evaluator/matchers/__tests__/regex.spec.ts @@ -3,10 +3,11 @@ import matcherFactory from '..'; import fs from 'fs'; import rl from 'readline'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER REGEX (STRING) / should match the attribute value only with the string starts with hello', function () { // @ts-ignore - const matcher = matcherFactory({ + const matcher = matcherFactory(loggerMock, { type: matcherTypes.MATCHES_STRING, value: '^hello' } as IMatcherDto) as IMatcher; @@ -17,7 +18,7 @@ test('MATCHER REGEX (STRING) / should match the attribute value only with the st test('MATCHER REGEX (STRING) / incorrectly matches unicode characters', function () { // @ts-ignore - const matcher = matcherFactory({ + const matcher = matcherFactory(loggerMock, { type: matcherTypes.MATCHES_STRING, value: 'a.b' } as IMatcherDto) as IMatcher; @@ -46,7 +47,7 @@ test('MATCHER REGEX (STRING) / incorrectly matches unicode characters', function const isTestTrue = test === 'true'; // @ts-ignore - const matcher = matcherFactory({ + const matcher = matcherFactory(loggerMock, { type: matcherTypes.MATCHES_STRING, value: regex } as IMatcherDto) as IMatcher; diff --git a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts index 09d57789..7514a862 100644 --- a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts +++ b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts @@ -2,11 +2,12 @@ import { matcherTypes } from '../../matcherTypes'; import matcherFactory from '../..'; import { IMatcher, IMatcherDto } from '../../../types'; import { IStorageSync } from '../../../../storages/types'; +import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () { const segment = 'employees'; - const matcherTrue = matcherFactory({ + const matcherTrue = matcherFactory(loggerMock, { type: matcherTypes.IN_SEGMENT, value: segment } as IMatcherDto, { @@ -17,7 +18,7 @@ test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined i } } as IStorageSync) as IMatcher; - const matcherFalse = matcherFactory({ + const matcherFalse = matcherFactory(loggerMock, { type: matcherTypes.IN_SEGMENT, value: segment + 'asd' } as IMatcherDto, { diff --git a/src/evaluator/matchers/__tests__/segment/server_side.spec.ts b/src/evaluator/matchers/__tests__/segment/server_side.spec.ts index 38b7ae70..97d9dcc7 100644 --- a/src/evaluator/matchers/__tests__/segment/server_side.spec.ts +++ b/src/evaluator/matchers/__tests__/segment/server_side.spec.ts @@ -2,11 +2,12 @@ import { matcherTypes } from '../../matcherTypes'; import matcherFactory from '../..'; import { IMatcher, IMatcherDto } from '../../../types'; import { IStorageSync } from '../../../../storages/types'; +import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; test('MATCHER IN_SEGMENT / should return true ONLY when the key is defined inside the segment', async function () { const segment = 'employees'; - const matcher = matcherFactory({ + const matcher = matcherFactory(loggerMock, { type: matcherTypes.IN_SEGMENT, value: segment } as IMatcherDto, { diff --git a/src/evaluator/matchers/__tests__/sw.spec.ts b/src/evaluator/matchers/__tests__/sw.spec.ts index 59787aa2..ec8cdb5b 100644 --- a/src/evaluator/matchers/__tests__/sw.spec.ts +++ b/src/evaluator/matchers/__tests__/sw.spec.ts @@ -1,18 +1,19 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER STARTS_WITH / should return true ONLY when the value starts with ["a", "b", "c"]', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.STARTS_WITH, value: ['a', 'b', 'c'] } as IMatcherDto) as IMatcher; - expect(matcher('awesome')).toBe(true); // awesome start with ["a", "b", "c"] - expect(matcher('black')).toBe(true); // black start with ["a", "b", "c"] - expect(matcher('chello')).toBe(true); // chello start with ["a", "b", "c"] - expect(matcher('violin')).toBe(false); // t start with ["a", "b", "c"] - expect(matcher('manager')).toBe(false); // t start with ["a", "b", "c"] + expect(matcher('awesome')).toBe(true); // awesome starts with ["a", "b", "c"] + expect(matcher('black')).toBe(true); // black starts with ["a", "b", "c"] + expect(matcher('chello')).toBe(true); // chello starts with ["a", "b", "c"] + expect(matcher('violin')).toBe(false); // violin doesn't start with ["a", "b", "c"] + expect(matcher('manager')).toBe(false); // manager doesn't start with ["a", "b", "c"] }); diff --git a/src/evaluator/matchers/__tests__/whitelist.spec.ts b/src/evaluator/matchers/__tests__/whitelist.spec.ts index 9ffb4369..4c7aa723 100644 --- a/src/evaluator/matchers/__tests__/whitelist.spec.ts +++ b/src/evaluator/matchers/__tests__/whitelist.spec.ts @@ -2,10 +2,11 @@ import { matcherTypes } from '../matcherTypes'; import matcherFactory from '..'; import { _Set } from '../../../utils/lang/sets'; import { IMatcher, IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('MATCHER WHITELIST / should return true ONLY when the key is defined', function () { // @ts-ignore - let matcher = matcherFactory({ + let matcher = matcherFactory(loggerMock, { type: matcherTypes.WHITELIST, value: new _Set().add('key') } as IMatcherDto) as IMatcher; diff --git a/src/evaluator/matchers/all.ts b/src/evaluator/matchers/all.ts index 0a4fd8a2..8b8c64fb 100644 --- a/src/evaluator/matchers/all.ts +++ b/src/evaluator/matchers/all.ts @@ -1,12 +1,10 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_ALL } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; -function allMatcher(runtimeAttr: string): boolean { - log.debug('[allMatcher] is always true'); +export default function allMatcherContext(log: ILogger) { + return function allMatcher(runtimeAttr: string): boolean { + log.debug(ENGINE_MATCHER_ALL); - return runtimeAttr != null; -} - -export default function allMatcherContext() { - return allMatcher; + return runtimeAttr != null; + }; } diff --git a/src/evaluator/matchers/between.ts b/src/evaluator/matchers/between.ts index ba17c6c8..f56709b3 100644 --- a/src/evaluator/matchers/between.ts +++ b/src/evaluator/matchers/between.ts @@ -1,13 +1,13 @@ import { IBetweenMatcherData } from '../../dtos/types'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_BETWEEN } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; -export default function betweenMatcherContext(ruleVO: IBetweenMatcherData) /*: Function */ { +export default function betweenMatcherContext(log: ILogger, ruleVO: IBetweenMatcherData) /*: Function */ { return function betweenMatcher(runtimeAttr: number): boolean { let isBetween = runtimeAttr >= ruleVO.start && runtimeAttr <= ruleVO.end; - log.debug(`[betweenMatcher] is ${runtimeAttr} between ${ruleVO.start} and ${ruleVO.end}? ${isBetween}`); + log.debug(ENGINE_MATCHER_BETWEEN, [runtimeAttr, ruleVO.start, ruleVO.end, isBetween]); return isBetween; }; diff --git a/src/evaluator/matchers/boolean.ts b/src/evaluator/matchers/boolean.ts index f9c5d4cc..6e64a268 100644 --- a/src/evaluator/matchers/boolean.ts +++ b/src/evaluator/matchers/boolean.ts @@ -1,11 +1,11 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_BOOLEAN } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; -export default function booleanMatcherContext(ruleAttr: boolean) /*: Function */ { +export default function booleanMatcherContext(log: ILogger, ruleAttr: boolean) /*: Function */ { return function booleanMatcher(runtimeAttr: boolean): boolean { let booleanMatches = ruleAttr === runtimeAttr; - log.debug(`[booleanMatcher] ${ruleAttr} === ${runtimeAttr}`); + log.debug(ENGINE_MATCHER_BOOLEAN, [ruleAttr, runtimeAttr]); return booleanMatches; }; diff --git a/src/evaluator/matchers/cont_all.ts b/src/evaluator/matchers/cont_all.ts index 51e2eccb..02bc35e1 100644 --- a/src/evaluator/matchers/cont_all.ts +++ b/src/evaluator/matchers/cont_all.ts @@ -1,8 +1,8 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_CONTAINS_ALL } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { findIndex } from '../../utils/lang'; -export default function containsAllMatcherContext(ruleAttr: string[]) /*: Function */ { +export default function containsAllMatcherContext(log: ILogger, ruleAttr: string[]) /*: Function */ { return function containsAllMatcher(runtimeAttr: string[]): boolean { let containsAll = true; @@ -15,7 +15,7 @@ export default function containsAllMatcherContext(ruleAttr: string[]) /*: Functi } } - log.debug(`[containsAllMatcher] ${runtimeAttr} contains all elements of ${ruleAttr}? ${containsAll}`); + log.debug(ENGINE_MATCHER_CONTAINS_ALL, [runtimeAttr, ruleAttr, containsAll]); return containsAll; }; diff --git a/src/evaluator/matchers/cont_any.ts b/src/evaluator/matchers/cont_any.ts index d0e87c6f..69fcdb3e 100644 --- a/src/evaluator/matchers/cont_any.ts +++ b/src/evaluator/matchers/cont_any.ts @@ -1,8 +1,8 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_CONTAINS_ANY } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { findIndex } from '../../utils/lang'; -export default function containsAnyMatcherContext(ruleAttr: string[]) /*: Function */ { +export default function containsAnyMatcherContext(log: ILogger, ruleAttr: string[]) /*: Function */ { return function containsAnyMatcher(runtimeAttr: string[]): boolean { let containsAny = false; @@ -10,7 +10,7 @@ export default function containsAnyMatcherContext(ruleAttr: string[]) /*: Functi if (findIndex(runtimeAttr, e => e === ruleAttr[i]) >= 0) containsAny = true; } - log.debug(`[containsAnyMatcher] ${runtimeAttr} contains at least an element of ${ruleAttr}? ${containsAny}`); + log.debug(ENGINE_MATCHER_CONTAINS_ANY, [runtimeAttr, ruleAttr, containsAny]); return containsAny; }; diff --git a/src/evaluator/matchers/cont_str.ts b/src/evaluator/matchers/cont_str.ts index ee551238..0a15d13e 100644 --- a/src/evaluator/matchers/cont_str.ts +++ b/src/evaluator/matchers/cont_str.ts @@ -1,12 +1,12 @@ import { isString } from '../../utils/lang'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ILogger } from '../../logger/types'; +import { ENGINE_MATCHER_CONTAINS_STRING } from '../../logger/constants'; -export default function containsStringMatcherContext(ruleAttr: string[]) /*: Function */ { +export default function containsStringMatcherContext(log: ILogger, ruleAttr: string[]) /*: Function */ { return function containsStringMatcher(runtimeAttr: string): boolean { let contains = ruleAttr.some(e => isString(runtimeAttr) && runtimeAttr.indexOf(e) > -1); - log.debug(`[containsStringMatcher] ${runtimeAttr} contains ${ruleAttr}? ${contains}`); + log.debug(ENGINE_MATCHER_CONTAINS_STRING, [runtimeAttr, ruleAttr, contains]); return contains; }; diff --git a/src/evaluator/matchers/dependency.ts b/src/evaluator/matchers/dependency.ts index cdbd5486..e3b8723b 100644 --- a/src/evaluator/matchers/dependency.ts +++ b/src/evaluator/matchers/dependency.ts @@ -1,27 +1,27 @@ import { IDependencyMatcherData, MaybeThenable } from '../../dtos/types'; -import { logFactory } from '../../logger/sdkLogger'; import { IStorageAsync, IStorageSync } from '../../storages/types'; -const log = logFactory('splitio-engine:matcher'); +import { ILogger } from '../../logger/types'; import thenable from '../../utils/promise/thenable'; import { IDependencyMatcherValue, IEvaluation, ISplitEvaluator } from '../types'; +import { ENGINE_MATCHER_DEPENDENCY, ENGINE_MATCHER_DEPENDENCY_PRE } from '../../logger/constants'; -function checkTreatment(evaluation: IEvaluation, acceptableTreatments: string[], parentName: string) { - let matches = false; +export default function dependencyMatcherContext(log: ILogger, { split, treatments }: IDependencyMatcherData, storage: IStorageSync | IStorageAsync) { - if (Array.isArray(acceptableTreatments)) { - matches = acceptableTreatments.indexOf(evaluation.treatment as string) !== -1; - } + function checkTreatment(evaluation: IEvaluation, acceptableTreatments: string[], parentName: string) { + let matches = false; - log.debug(`[dependencyMatcher] Parent split "${parentName}" evaluated to "${evaluation.treatment}" with label "${evaluation.label}". ${parentName} evaluated treatment is part of [${acceptableTreatments}] ? ${matches}.`); + if (Array.isArray(acceptableTreatments)) { + matches = acceptableTreatments.indexOf(evaluation.treatment as string) !== -1; + } - return matches; -} + log.debug(ENGINE_MATCHER_DEPENDENCY, [parentName, evaluation.treatment, evaluation.label, parentName, acceptableTreatments, matches]); -export default function dependencyMatcherContext({ split, treatments }: IDependencyMatcherData, storage: IStorageSync | IStorageAsync) { + return matches; + } return function dependencyMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { - log.debug(`[dependencyMatcher] will evaluate parent split: "${split}" with key: ${JSON.stringify(key)} ${attributes ? `\n attributes: ${JSON.stringify(attributes)}` : ''}`); - const evaluation = splitEvaluator(key, split, attributes, storage); + log.debug(ENGINE_MATCHER_DEPENDENCY_PRE, [split, JSON.stringify(key), attributes ? '\n attributes: ' + JSON.stringify(attributes) : '']); + const evaluation = splitEvaluator(log, key, split, attributes, storage); if (thenable(evaluation)) { return evaluation.then(ev => checkTreatment(ev, treatments, split)); diff --git a/src/evaluator/matchers/eq.ts b/src/evaluator/matchers/eq.ts index b01fda26..ddccc264 100644 --- a/src/evaluator/matchers/eq.ts +++ b/src/evaluator/matchers/eq.ts @@ -1,11 +1,11 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_EQUAL } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; -export default function equalToMatcherContext(ruleAttr: number) /*: Function */ { +export default function equalToMatcherContext(log: ILogger, ruleAttr: number) /*: Function */ { return function equalToMatcher(runtimeAttr: number): boolean { let isEqual = runtimeAttr === ruleAttr; - log.debug(`[equalToMatcher] is ${runtimeAttr} equal to ${ruleAttr}? ${isEqual}`); + log.debug(ENGINE_MATCHER_EQUAL, [runtimeAttr, ruleAttr, isEqual]); return isEqual; }; diff --git a/src/evaluator/matchers/eq_set.ts b/src/evaluator/matchers/eq_set.ts index d02c16a7..f0fddd27 100644 --- a/src/evaluator/matchers/eq_set.ts +++ b/src/evaluator/matchers/eq_set.ts @@ -1,8 +1,8 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_EQUAL_TO_SET } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { findIndex } from '../../utils/lang'; -export default function equalToSetMatcherContext(ruleAttr: string[]) /*: Function */ { +export default function equalToSetMatcherContext(log: ILogger, ruleAttr: string[]) /*: Function */ { return function equalToSetMatcher(runtimeAttr: string[]): boolean { // Length being the same is the first condition. let isEqual = runtimeAttr.length === ruleAttr.length; @@ -12,7 +12,7 @@ export default function equalToSetMatcherContext(ruleAttr: string[]) /*: Functio if (findIndex(ruleAttr, e => e === runtimeAttr[i]) < 0) isEqual = false; } - log.debug(`[equalToSetMatcher] is ${runtimeAttr} equal to set ${ruleAttr}? ${isEqual}`); + log.debug(ENGINE_MATCHER_EQUAL_TO_SET, [runtimeAttr, ruleAttr, isEqual]); return isEqual; }; diff --git a/src/evaluator/matchers/ew.ts b/src/evaluator/matchers/ew.ts index 24185e06..20d46c63 100644 --- a/src/evaluator/matchers/ew.ts +++ b/src/evaluator/matchers/ew.ts @@ -1,12 +1,12 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_ENDS_WITH } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { endsWith as strEndsWith } from '../../utils/lang'; -export default function endsWithMatcherContext(ruleAttr: string[]) /*: Function */ { +export default function endsWithMatcherContext(log: ILogger, ruleAttr: string[]) /*: Function */ { return function endsWithMatcher(runtimeAttr: string): boolean { let endsWith = ruleAttr.some(e => strEndsWith(runtimeAttr, e)); - log.debug(`[endsWithMatcher] ${runtimeAttr} ends with ${ruleAttr}? ${endsWith}`); + log.debug(ENGINE_MATCHER_ENDS_WITH, [runtimeAttr, ruleAttr, endsWith]); return endsWith; }; diff --git a/src/evaluator/matchers/gte.ts b/src/evaluator/matchers/gte.ts index 5afcbc00..a08f55f5 100644 --- a/src/evaluator/matchers/gte.ts +++ b/src/evaluator/matchers/gte.ts @@ -1,11 +1,11 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_GREATER } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; -export default function greaterThanEqualMatcherContext(ruleAttr: number) /*: Function */ { +export default function greaterThanEqualMatcherContext(log: ILogger, ruleAttr: number) /*: Function */ { return function greaterThanEqualMatcher(runtimeAttr: number): boolean { let isGreaterEqualThan = runtimeAttr >= ruleAttr; - log.debug(`[greaterThanEqualMatcher] is ${runtimeAttr} greater than ${ruleAttr}? ${isGreaterEqualThan}`); + log.debug(ENGINE_MATCHER_GREATER, [runtimeAttr, ruleAttr, isGreaterEqualThan]); return isGreaterEqualThan; }; diff --git a/src/evaluator/matchers/index.ts b/src/evaluator/matchers/index.ts index 07d1af91..8cf50779 100644 --- a/src/evaluator/matchers/index.ts +++ b/src/evaluator/matchers/index.ts @@ -17,6 +17,7 @@ import booleanMatcher from './boolean'; import stringMatcher from './string'; import { IStorageAsync, IStorageSync } from '../../storages/types'; import { IMatcher, IMatcherDto } from '../types'; +import { ILogger } from '../../logger/types'; const matchers = [ undefined, // UNDEFINED: 0, @@ -42,7 +43,7 @@ const matchers = [ /** * Matcher factory. */ -export default function matcherFactory(matcherDto: IMatcherDto, storage?: IStorageSync | IStorageAsync): IMatcher | undefined { +export default function matcherFactory(log: ILogger, matcherDto: IMatcherDto, storage?: IStorageSync | IStorageAsync): IMatcher | undefined { let { type, value @@ -50,6 +51,6 @@ export default function matcherFactory(matcherDto: IMatcherDto, storage?: IStora let matcherFn; // @ts-ignore - if (matchers[type]) matcherFn = matchers[type](value, storage); // There is no index-out-of-bound exception in JavaScript + if (matchers[type]) matcherFn = matchers[type](log, value, storage); // There is no index-out-of-bound exception in JavaScript return matcherFn; } diff --git a/src/evaluator/matchers/lte.ts b/src/evaluator/matchers/lte.ts index bce0a915..06883b94 100644 --- a/src/evaluator/matchers/lte.ts +++ b/src/evaluator/matchers/lte.ts @@ -1,11 +1,11 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_LESS } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; -export default function lessThanEqualMatcherContext(ruleAttr: number) /*: function */ { +export default function lessThanEqualMatcherContext(log: ILogger, ruleAttr: number) /*: function */ { return function lessThanEqualMatcher(runtimeAttr: number): boolean { let isLessEqualThan = runtimeAttr <= ruleAttr; - log.debug(`[lessThanEqualMatcher] is ${runtimeAttr} less than ${ruleAttr}? ${isLessEqualThan}`); + log.debug(ENGINE_MATCHER_LESS, [runtimeAttr, ruleAttr, isLessEqualThan]); return isLessEqualThan; }; diff --git a/src/evaluator/matchers/part_of.ts b/src/evaluator/matchers/part_of.ts index 238a7d97..ccabf3e4 100644 --- a/src/evaluator/matchers/part_of.ts +++ b/src/evaluator/matchers/part_of.ts @@ -1,8 +1,8 @@ -import { logFactory } from '../../logger/sdkLogger'; import { findIndex } from '../../utils/lang'; -const log = logFactory('splitio-engine:matcher'); +import { ILogger } from '../../logger/types'; +import { ENGINE_MATCHER_PART_OF } from '../../logger/constants'; -export default function partOfMatcherContext(ruleAttr: string[]) /*: Function */ { +export default function partOfMatcherContext(log: ILogger, ruleAttr: string[]) /*: Function */ { return function partOfMatcher(runtimeAttr: string[]): boolean { // To be part of the length should be minor or equal. let isPartOf = runtimeAttr.length <= ruleAttr.length; @@ -12,7 +12,7 @@ export default function partOfMatcherContext(ruleAttr: string[]) /*: Function */ if (findIndex(ruleAttr, e => e === runtimeAttr[i]) < 0) isPartOf = false; } - log.debug(`[partOfMatcher] ${runtimeAttr} is part of ${ruleAttr}? ${isPartOf}`); + log.debug(ENGINE_MATCHER_PART_OF, [runtimeAttr, ruleAttr, isPartOf]); return isPartOf; }; diff --git a/src/evaluator/matchers/segment.ts b/src/evaluator/matchers/segment.ts index b4c67fdf..3e9db7d5 100644 --- a/src/evaluator/matchers/segment.ts +++ b/src/evaluator/matchers/segment.ts @@ -1,22 +1,22 @@ import { MaybeThenable } from '../../dtos/types'; -import { logFactory } from '../../logger/sdkLogger'; import { ISegmentsCacheBase } from '../../storages/types'; -const log = logFactory('splitio-engine:matcher'); +import { ILogger } from '../../logger/types'; import thenable from '../../utils/promise/thenable'; +import { ENGINE_MATCHER_SEGMENT } from '../../logger/constants'; -export default function matcherSegmentContext(segmentName: string, storage: { segments: ISegmentsCacheBase }) { +export default function matcherSegmentContext(log: ILogger, segmentName: string, storage: { segments: ISegmentsCacheBase }) { return function segmentMatcher(key: string): MaybeThenable { const isInSegment = storage.segments.isInSegment(segmentName, key); if (thenable(isInSegment)) { isInSegment.then(result => { - log.debug(`[asyncSegmentMatcher] evaluated ${segmentName} / ${key} => ${isInSegment}`); + log.debug(ENGINE_MATCHER_SEGMENT, [segmentName, key, isInSegment]); return result; }); } else { - log.debug(`[segmentMatcher] evaluated ${segmentName} / ${key} => ${isInSegment}`); + log.debug(ENGINE_MATCHER_SEGMENT, [segmentName, key, isInSegment]); } return isInSegment; diff --git a/src/evaluator/matchers/string.ts b/src/evaluator/matchers/string.ts index f885cb20..f0e0512f 100644 --- a/src/evaluator/matchers/string.ts +++ b/src/evaluator/matchers/string.ts @@ -1,21 +1,21 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_STRING_INVALID, ENGINE_MATCHER_STRING } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; -export default function stringMatcherContext(ruleAttr: string) /*: Function */ { +export default function stringMatcherContext(log: ILogger, ruleAttr: string) /*: Function */ { return function stringMatcher(runtimeAttr: string): boolean { let re; try { re = new RegExp(ruleAttr); } catch (e) { - log.debug(`[stringMatcher] ${ruleAttr} is an invalid regex`); + log.debug(ENGINE_MATCHER_STRING_INVALID, [ruleAttr]); return false; } let regexMatches = re.test(runtimeAttr); - log.debug(`[stringMatcher] does ${runtimeAttr} matches with ${ruleAttr}? ${regexMatches ? 'yes' : 'no'}`); + log.debug(ENGINE_MATCHER_STRING, [runtimeAttr, ruleAttr, regexMatches ? 'yes' : 'no']); return regexMatches; }; diff --git a/src/evaluator/matchers/sw.ts b/src/evaluator/matchers/sw.ts index 6e5f3962..e2bfc03e 100644 --- a/src/evaluator/matchers/sw.ts +++ b/src/evaluator/matchers/sw.ts @@ -1,12 +1,12 @@ -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ENGINE_MATCHER_STARTS_WITH } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { startsWith } from '../../utils/lang'; -export default function startsWithMatcherContext(ruleAttr: string[]) /*: Function */ { +export default function startsWithMatcherContext(log: ILogger, ruleAttr: string[]) /*: Function */ { return function startsWithMatcher(runtimeAttr: string): boolean { let matches = ruleAttr.some(e => startsWith(runtimeAttr, e)); - log.debug(`[startsWithMatcher] ${runtimeAttr} starts with ${ruleAttr}? ${matches}`); + log.debug(ENGINE_MATCHER_STARTS_WITH, [runtimeAttr, ruleAttr, matches]); return matches; }; diff --git a/src/evaluator/matchers/whitelist.ts b/src/evaluator/matchers/whitelist.ts index fac0355d..b84cb129 100644 --- a/src/evaluator/matchers/whitelist.ts +++ b/src/evaluator/matchers/whitelist.ts @@ -1,12 +1,12 @@ import { setToArray, ISet } from '../../utils/lang/sets'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:matcher'); +import { ILogger } from '../../logger/types'; +import { ENGINE_MATCHER_WHITELIST } from '../../logger/constants'; -export default function whitelistMatcherContext(ruleAttr: ISet) /*: Function */ { +export default function whitelistMatcherContext(log: ILogger, ruleAttr: ISet) /*: Function */ { return function whitelistMatcher(runtimeAttr: string): boolean { let isInWhitelist = ruleAttr.has(runtimeAttr); - log.debug(`[whitelistMatcher] evaluated ${runtimeAttr} in [${setToArray(ruleAttr).join(',')}] => ${isInWhitelist}`); + log.debug(ENGINE_MATCHER_WHITELIST, [runtimeAttr, setToArray(ruleAttr).join(','), isInWhitelist]); return isInWhitelist; }; diff --git a/src/evaluator/parser/__tests__/boolean.spec.ts b/src/evaluator/parser/__tests__/boolean.spec.ts index 464e6525..81f8a30b 100644 --- a/src/evaluator/parser/__tests__/boolean.spec.ts +++ b/src/evaluator/parser/__tests__/boolean.spec.ts @@ -2,11 +2,12 @@ import parser from '..'; import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / if user.boolean is true then split 100%:on', async function () { // @ts-ignore - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ diff --git a/src/evaluator/parser/__tests__/index.spec.ts b/src/evaluator/parser/__tests__/index.spec.ts index 32414290..a27fecc7 100644 --- a/src/evaluator/parser/__tests__/index.spec.ts +++ b/src/evaluator/parser/__tests__/index.spec.ts @@ -3,10 +3,11 @@ import parser from '..'; import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; import { bucket } from '../../../utils/murmur3/murmur3'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / if user is in segment all 100%:on', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -32,7 +33,7 @@ test('PARSER / if user is in segment all 100%:on', async function () { test('PARSER / if user is in segment all 100%:off', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -60,7 +61,7 @@ test('PARSER / if user is in segment all 100%:off', async function () { test('PARSER / NEGATED if user is in segment all 100%:on, then no match', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -85,7 +86,7 @@ test('PARSER / NEGATED if user is in segment all 100%:on, then no match', async test('PARSER / if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -124,7 +125,7 @@ test('PARSER / if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on test('PARSER / NEGATED if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on, negated results', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -163,7 +164,7 @@ test('PARSER / NEGATED if user is in segment ["u1", "u2", "u3", "u4"] then split test('PARSER / if user.account is in list ["v1", "v2", "v3"] then split 100:on', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -209,7 +210,7 @@ test('PARSER / if user.account is in list ["v1", "v2", "v3"] then split 100:on', test('PARSER / NEGATED if user.account is in list ["v1", "v2", "v3"] then split 100:on, negated results', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -255,7 +256,7 @@ test('PARSER / NEGATED if user.account is in list ["v1", "v2", "v3"] then split }); test('PARSER / if user.account is in segment all then split 100:on', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -281,7 +282,7 @@ test('PARSER / if user.account is in segment all then split 100:on', async funct test('PARSER / if user.attr is between 10 and 20 then split 100:on', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -322,7 +323,7 @@ test('PARSER / if user.attr is between 10 and 20 then split 100:on', async funct test('PARSER / NEGATED if user.attr is between 10 and 20 then split 100:on, negated results', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -364,7 +365,7 @@ test('PARSER / NEGATED if user.attr is between 10 and 20 then split 100:on, nega test('PARSER / if user.attr <= datetime 1458240947021 then split 100:on', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -409,7 +410,7 @@ test('PARSER / if user.attr <= datetime 1458240947021 then split 100:on', async test('PARSER / NEGATED if user.attr <= datetime 1458240947021 then split 100:on, negated results', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -455,7 +456,7 @@ test('PARSER / NEGATED if user.attr <= datetime 1458240947021 then split 100:on, test('PARSER / if user.attr >= datetime 1458240947021 then split 100:on', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -500,7 +501,7 @@ test('PARSER / if user.attr >= datetime 1458240947021 then split 100:on', async test('PARSER / NEGATED if user.attr >= datetime 1458240947021 then split 100:on, negated results', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -546,7 +547,7 @@ test('PARSER / NEGATED if user.attr >= datetime 1458240947021 then split 100:on, test('PARSER / if user.attr = datetime 1458240947021 then split 100:on', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -591,7 +592,7 @@ test('PARSER / if user.attr = datetime 1458240947021 then split 100:on', async f test('PARSER / NEGATED if user.attr = datetime 1458240947021 then split 100:on, negated results', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -636,7 +637,7 @@ test('PARSER / NEGATED if user.attr = datetime 1458240947021 then split 100:on, }); test('PARSER / if user is in segment all then split 20%:A,20%:B,60%:A', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ diff --git a/src/evaluator/parser/__tests__/invalidMatcher.spec.ts b/src/evaluator/parser/__tests__/invalidMatcher.spec.ts index d140aea9..3ff988bd 100644 --- a/src/evaluator/parser/__tests__/invalidMatcher.spec.ts +++ b/src/evaluator/parser/__tests__/invalidMatcher.spec.ts @@ -1,9 +1,10 @@ // @ts-nocheck import parser from '..'; import { ISplitCondition } from '../../../dtos/types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / handle invalid matcher as control', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -32,7 +33,7 @@ test('PARSER / handle invalid matcher as control', async function () { }); test('PARSER / handle invalid matcher as control (complex example)', async function () { - const evaluator = parser([ + const evaluator = parser(loggerMock, [ { 'conditionType': 'WHITELIST', 'matcherGroup': { @@ -132,7 +133,7 @@ test('PARSER / handle invalid matcher as control (complex example)', async funct }); test('PARSER / handle invalid matcher as control (complex example mixing invalid and valid matchers)', async function () { - const evaluator = parser([ + const evaluator = parser(loggerMock, [ { 'conditionType': 'WHITELIST', 'matcherGroup': { diff --git a/src/evaluator/parser/__tests__/regex.spec.ts b/src/evaluator/parser/__tests__/regex.spec.ts index 0c83f0a0..ee2b0904 100644 --- a/src/evaluator/parser/__tests__/regex.spec.ts +++ b/src/evaluator/parser/__tests__/regex.spec.ts @@ -2,10 +2,11 @@ import parser from '..'; import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / if user.string is true then split 100%:on', async function () { // @ts-ignore - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ diff --git a/src/evaluator/parser/__tests__/set.spec.ts b/src/evaluator/parser/__tests__/set.spec.ts index f46e1c0a..de652ed3 100644 --- a/src/evaluator/parser/__tests__/set.spec.ts +++ b/src/evaluator/parser/__tests__/set.spec.ts @@ -2,13 +2,14 @@ import parser from '..'; import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; // // EQUAL_TO_SET // test('PARSER / if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on', async function () { const label = 'permissions = ["read", "write"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -43,7 +44,7 @@ test('PARSER / if user.permissions ["read", "write"] equal to set ["read", "writ test('PARSER / if user.permissions ["write", "read"] equal to set ["read", "write"] then split 100:on', async function () { const label = 'permissions = ["read", "write"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -78,7 +79,7 @@ test('PARSER / if user.permissions ["write", "read"] equal to set ["read", "writ test('PARSER / if user.permissions ["1", 2] equal to set ["1", "2"] then split 100:on', async function () { const label = 'permissions = ["1", "2"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -113,7 +114,7 @@ test('PARSER / if user.permissions ["1", 2] equal to set ["1", "2"] then split 1 test('PARSER / if user.permissions ["read", "write", "delete"] equal to set ["read", "write"] then not match', async function () { const label = 'permissions = ["read", "write"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -147,7 +148,7 @@ test('PARSER / if user.permissions ["read", "write", "delete"] equal to set ["re test('PARSER / if user.permissions ["read"] equal to set ["read", "write"] then not match', async function () { const label = 'permissions = ["read", "write"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -181,7 +182,7 @@ test('PARSER / if user.permissions ["read"] equal to set ["read", "write"] then test('PARSER / if user.permissions ["read", "delete"] equal to set ["read", "write"] then not match', async function () { const label = 'permissions = ["read", "write"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -215,7 +216,7 @@ test('PARSER / if user.permissions ["read", "delete"] equal to set ["read", "wri test('PARSER / if user.countries ["argentina", "usa"] equal to set ["usa","argentina"] then split 100:on', async function () { const label = 'countries = ["usa","argentina"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -249,7 +250,7 @@ test('PARSER / if user.countries ["argentina", "usa"] equal to set ["usa","argen test('PARSER / if attribute is not an array we should not match equal to set', async function () { const label = 'countries = ["usa","argentina"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -284,7 +285,7 @@ test('PARSER / if attribute is not an array we should not match equal to set', a test('PARSER / if attribute is an EMPTY array we should not match equal to set', async function () { const label = 'countries = ["usa","argentina"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -317,7 +318,7 @@ test('PARSER / if attribute is an EMPTY array we should not match equal to set', test('PARSER / NEGATED if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on should not match', async function () { const label = 'not permissions = ["read", "write"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -350,7 +351,7 @@ test('PARSER / NEGATED if user.permissions ["read", "write"] equal to set ["read test('PARSER / NEGATED if user.permissions ["read"] equal to set ["read", "write"] false, then match', async function () { const label = 'not permissions = ["read", "write"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -384,7 +385,7 @@ test('PARSER / NEGATED if user.permissions ["read"] equal to set ["read", "write test('PARSER / NEGATED if attribute is not an array we should not match equal to set, so match', async function () { const label = 'countries = ["usa","argentina"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -421,7 +422,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match equal to test('PARSER / NEGATED if attribute is an EMPTY array we should not match equal to set, so match', async function () { const label = 'countries = ["usa","argentina"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -458,7 +459,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match equal // test('PARSER / if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on', async function () { const label = 'permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -493,7 +494,7 @@ test('PARSER / if user.permissions ["read", "edit", "delete"] contains all of se test('PARSER / if user.permissions ["edit", "read", "delete"] contains all of set ["read", "edit"] then split 100:on', async function () { const label = 'permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -528,7 +529,7 @@ test('PARSER / if user.permissions ["edit", "read", "delete"] contains all of se test('PARSER / if user.permissions [1, "edit", "delete"] contains all of set ["1", "edit"] then split 100:on', async function () { const label = 'permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -563,7 +564,7 @@ test('PARSER / if user.permissions [1, "edit", "delete"] contains all of set ["1 test('PARSER / if user.permissions ["read"] contains all of set ["read", "edit"] then not match', async function () { const label = 'permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -597,7 +598,7 @@ test('PARSER / if user.permissions ["read"] contains all of set ["read", "edit"] test('PARSER / if user.permissions ["read", "delete", "manage"] contains all of set ["read", "edit"] then not match', async function () { const label = 'permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -631,7 +632,7 @@ test('PARSER / if user.permissions ["read", "delete", "manage"] contains all of test('PARSER / if attribute is not an array we should not match contains all', async function () { const label = 'permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -666,7 +667,7 @@ test('PARSER / if attribute is not an array we should not match contains all', a test('PARSER / if attribute is an EMPTY array we should not match contains all', async function () { const label = 'permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -699,7 +700,7 @@ test('PARSER / if attribute is an EMPTY array we should not match contains all', test('PARSER / NEGATED if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on should not match', async function () { const label = 'not permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -732,7 +733,7 @@ test('PARSER / NEGATED if user.permissions ["read", "edit", "delete"] contains a test('PARSER / NEGATED if user.permissions ["read"] contains all of set ["read", "edit"] false, so match', async function () { const label = 'not permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -766,7 +767,7 @@ test('PARSER / NEGATED if user.permissions ["read"] contains all of set ["read", test('PARSER / NEGATED if attribute is not an array we should not match contains all, so match', async function () { const label = 'not permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -803,7 +804,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match contains test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains all, so match', async function () { const label = 'not permissions contains ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -840,7 +841,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match contai // test('PARSER / if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on', async function () { const label = 'permissions part of ["read", "edit", "delete"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -875,7 +876,7 @@ test('PARSER / if user.permissions ["read", "edit"] is part of set ["read", "edi test('PARSER / if user.permissions ["edit", "read"] is part of set ["read", "edit", "delete"] then split 100:on', async function () { const label = 'permissions part of ["read", "edit", "delete"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -910,7 +911,7 @@ test('PARSER / if user.permissions ["edit", "read"] is part of set ["read", "edi test('PARSER / if user.permissions [1, "edit"] is part of set ["1", "edit", "delete"] then split 100:on', async function () { const label = 'permissions part of ["1", "edit", "delete"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -945,7 +946,7 @@ test('PARSER / if user.permissions [1, "edit"] is part of set ["1", "edit", "del test('PARSER / if user.permissions ["admin", "magic"] is part of set ["read", "edit"] then not match', async function () { const label = 'permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -979,7 +980,7 @@ test('PARSER / if user.permissions ["admin", "magic"] is part of set ["read", "e test('PARSER / if attribute is not an array we should not match part of', async function () { const label = 'permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1014,7 +1015,7 @@ test('PARSER / if attribute is not an array we should not match part of', async test('PARSER / if attribute is an EMPTY array we should not match part of', async function () { const label = 'permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1047,7 +1048,7 @@ test('PARSER / if attribute is an EMPTY array we should not match part of', asyn test('PARSER / NEGATED if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on should not match', async function () { const label = 'not permissions part of ["read", "edit", "delete"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1080,7 +1081,7 @@ test('PARSER / NEGATED if user.permissions ["read", "edit"] is part of set ["rea test('PARSER / NEGATED if user.permissions ["admin", "magic"] is part of set ["read", "edit"] false, then match', async function () { const label = 'not permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1114,7 +1115,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "magic"] is part of set ["r test('PARSER / NEGATED if attribute is not an array we should not match part of, so match', async function () { const label = 'not permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1151,7 +1152,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match part of, test('PARSER / NEGATED if attribute is an EMPTY array we should not match part of, so match', async function () { const label = 'not permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1188,7 +1189,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match part o // test('PARSER / if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on', async function () { const label = 'permissions part of ["read", "edit", "delete"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1223,7 +1224,7 @@ test('PARSER / if user.permissions ["admin", "edit"] contains any of set ["read" test('PARSER / if user.permissions ["admin", 1] contains any of set ["read", "1", "delete"] then split 100:on', async function () { const label = 'permissions part of ["read", "1", "delete"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1258,7 +1259,7 @@ test('PARSER / if user.permissions ["admin", 1] contains any of set ["read", "1" test('PARSER / if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] then not match', async function () { const label = 'permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1292,7 +1293,7 @@ test('PARSER / if user.permissions ["admin", "magic"] contains any of set ["read test('PARSER / if attribute is not an array we should not match contains any', async function () { const label = 'permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1327,7 +1328,7 @@ test('PARSER / if attribute is not an array we should not match contains any', a test('PARSER / if attribute is an EMPTY array we should not match contains any', async function () { const label = 'permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1360,7 +1361,7 @@ test('PARSER / if attribute is an EMPTY array we should not match contains any', test('PARSER / NEGATED if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on should not match', async function () { const label = 'not permissions part of ["read", "edit", "delete"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1393,7 +1394,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "edit"] contains any of set test('PARSER / NEGATED if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] false, then should match', async function () { const label = 'not permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1427,7 +1428,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "magic"] contains any of se test('PARSER / NEGATED if attribute is not an array we should not match contains any, then should match', async function () { const label = 'not permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1459,7 +1460,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match contains test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains any, then should match', async function () { const label = 'not permissions part of ["read", "edit"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ diff --git a/src/evaluator/parser/__tests__/string.spec.ts b/src/evaluator/parser/__tests__/string.spec.ts index 10304629..326ade21 100644 --- a/src/evaluator/parser/__tests__/string.spec.ts +++ b/src/evaluator/parser/__tests__/string.spec.ts @@ -2,13 +2,14 @@ import parser from '..'; import { ISplitCondition } from '../../../dtos/types'; import { keyParser } from '../../../utils/key'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; // // STARTS WITH // test('PARSER / if user.email starts with ["nico"] then split 100:on', async function () { const label = 'email starts with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -42,7 +43,7 @@ test('PARSER / if user.email starts with ["nico"] then split 100:on', async func test('PARSER / if user.email = 123, starts with ["1"] then split 100:on should match', async function () { const label = 'email starts with ["1"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -76,7 +77,7 @@ test('PARSER / if user.email = 123, starts with ["1"] then split 100:on should m test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async function () { const label = 'email starts with ["nico", "marcio", "facu"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -110,7 +111,7 @@ test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 1 test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async function () { const label = 'email starts with ["nico", "marcio", "facu"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -144,7 +145,7 @@ test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 1 test('PARSER / if user.email does not start with ["nico"] then not match', async function () { // const label = 'email starts with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -176,7 +177,7 @@ test('PARSER / if user.email does not start with ["nico"] then not match', async test('PARSER / if user.email is an EMPTY string, start with ["nico"] should not match', async function () { // const label = 'email starts with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -206,7 +207,7 @@ test('PARSER / if user.email is an EMPTY string, start with ["nico"] should not test('PARSER / if user.email is not a string, start with ["nico"] should not match', async function () { // const label = 'email starts with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -239,7 +240,7 @@ test('PARSER / if user.email is not a string, start with ["nico"] should not mat test('PARSER / NEGATED if user.email starts with ["nico"] then split 100:on, so not match', async function () { const label = 'not email starts with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -272,7 +273,7 @@ test('PARSER / NEGATED if user.email starts with ["nico"] then split 100:on, so test('PARSER / NEGATED if user.email does not start with ["nico"] should not match, then match', async function () { const label = 'not email starts with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -306,7 +307,7 @@ test('PARSER / NEGATED if user.email does not start with ["nico"] should not mat test('PARSER / NEGATED if user.email is an EMPTY string, start with ["nico"] should not match, so negation should', async function () { const label = 'not email starts with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -338,7 +339,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, start with ["nico"] sho test('PARSER / NEGATED if user.email is not a string, start with ["nico"] should not match, so negation should', async function () { const label = 'not email starts with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -377,7 +378,7 @@ test('PARSER / NEGATED if user.email is not a string, start with ["nico"] should // test('PARSER / if user.email ends with ["split.io"] then split 100:on', async function () { const label = 'email ends with ["split.io"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -411,7 +412,7 @@ test('PARSER / if user.email ends with ["split.io"] then split 100:on', async fu test('PARSER / if user.email = 123, ends with ["3"] then split 100:on should match', async function () { const label = 'email starts with ["3"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -444,7 +445,7 @@ test('PARSER / if user.email = 123, ends with ["3"] then split 100:on should mat test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async function () { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -478,7 +479,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async function () { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -512,7 +513,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] but attribute is "" then split 100:on', async function () { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -545,7 +546,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] test('PARSER / if user.email does not end with ["split.io"] then not match', async function () { const label = 'email ends with ["split.io"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -578,7 +579,7 @@ test('PARSER / if user.email does not end with ["split.io"] then not match', asy test('PARSER / if user.email is an EMPTY string, end with ["nico"] should not match', async function () { // const label = 'email ends with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -608,7 +609,7 @@ test('PARSER / if user.email is an EMPTY string, end with ["nico"] should not ma test('PARSER / if user.email is not a string, end with ["nico"] should not match', async function () { // const label = 'email ends with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -643,7 +644,7 @@ test('PARSER / if user.email is not a string, end with ["nico"] should not match test('PARSER / NEGATED if user.email ends with ["split.io"] then split 100:on, so not match', async function () { const label = 'not email ends with ["split.io"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -675,7 +676,7 @@ test('PARSER / NEGATED if user.email ends with ["split.io"] then split 100:on, s test('PARSER / NEGATED if user.email does not end with ["split.io"] then no match, so match', async function () { const label = 'not email ends with ["split.io"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -708,7 +709,7 @@ test('PARSER / NEGATED if user.email does not end with ["split.io"] then no matc test('PARSER / NEGATED if user.email is an EMPTY string, end with ["nico"] should not match, so negation should', async function () { const label = 'not email ends with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -740,7 +741,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, end with ["nico"] shoul test('PARSER / NEGATED if user.email is not a string, end with ["nico"] should not match, so negation should', async function () { const label = 'not email ends with ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -779,7 +780,7 @@ test('PARSER / NEGATED if user.email is not a string, end with ["nico"] should n // test('PARSER / if user.email contains ["@split"] then split 100:on', async function () { const label = 'email contains ["@split"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -813,7 +814,7 @@ test('PARSER / if user.email contains ["@split"] then split 100:on', async funct test('PARSER / if user.email = 123, contains ["2"] then split 100:on should match', async function () { const label = 'email contains ["2"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -847,7 +848,7 @@ test('PARSER / if user.email = 123, contains ["2"] then split 100:on should matc test('PARSER / if user.email contains ["@split"] (beginning) then split 100:on', async function () { const label = 'email contains ["@split"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -881,7 +882,7 @@ test('PARSER / if user.email contains ["@split"] (beginning) then split 100:on', test('PARSER / if user.email contains ["@split"] (end) then split 100:on', async function () { const label = 'email contains ["@split"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -915,7 +916,7 @@ test('PARSER / if user.email contains ["@split"] (end) then split 100:on', async test('PARSER / if user.email contains ["@split"] (whole string matches) then split 100:on', async function () { const label = 'email contains ["@split"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -949,7 +950,7 @@ test('PARSER / if user.email contains ["@split"] (whole string matches) then spl test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async function () { const label = 'email contains ["@split", "@gmail", "@hotmail"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -983,7 +984,7 @@ test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then spli test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async function () { const label = 'email contains ["@split", "@gmail", "@hotmail"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1017,7 +1018,7 @@ test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then spli test('PARSER / if user.email does not contain ["@split"] then not match', async function () { const label = 'email contains ["@split"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1050,7 +1051,7 @@ test('PARSER / if user.email does not contain ["@split"] then not match', async test('PARSER / if user.email is an EMPTY string, contains ["nico"] should not match', async function () { // const label = 'email contains ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1080,7 +1081,7 @@ test('PARSER / if user.email is an EMPTY string, contains ["nico"] should not ma test('PARSER / if user.email is not a string, contains ["nico"] should not match', async function () { // const label = 'email contains ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1115,7 +1116,7 @@ test('PARSER / if user.email is not a string, contains ["nico"] should not match test('PARSER / NEGATED if user.email contains ["@split"] then split 100:on, then no match', async function () { const label = 'not email contains ["@split"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1148,7 +1149,7 @@ test('PARSER / NEGATED if user.email contains ["@split"] then split 100:on, then test('PARSER / NEGATED if user.email does not contain ["@split"] then not match, so match', async function () { const label = 'email contains ["@split"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1181,7 +1182,7 @@ test('PARSER / NEGATED if user.email does not contain ["@split"] then not match, test('PARSER / NEGATED if user.email is an EMPTY string, contains ["nico"] should not match, so negation should', async function () { const label = 'not email contains ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ @@ -1213,7 +1214,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, contains ["nico"] shoul test('PARSER / NEGATED if user.email is not a string, contains ["nico"] should not match, so negation should', async function () { const label = 'not email contains ["nico"]'; - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', matchers: [{ diff --git a/src/evaluator/parser/__tests__/trafficAllocation.spec.ts b/src/evaluator/parser/__tests__/trafficAllocation.spec.ts index e2bd4ece..ddf5afae 100644 --- a/src/evaluator/parser/__tests__/trafficAllocation.spec.ts +++ b/src/evaluator/parser/__tests__/trafficAllocation.spec.ts @@ -1,11 +1,13 @@ +// @ts-nocheck import parser from '..'; import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', @@ -32,7 +34,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', a test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% with bucket below 99', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', @@ -59,7 +61,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% wi test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% and bucket returns 100', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', @@ -86,7 +88,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% an test('PARSER / if user is whitelisted and in segment all 100%:off with trafficAllocation as 0%', async function () { - const evaluator = parser([{ + const evaluator = parser(loggerMock, [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', diff --git a/src/evaluator/parser/index.ts b/src/evaluator/parser/index.ts index cd290cfb..4fdc6f0f 100644 --- a/src/evaluator/parser/index.ts +++ b/src/evaluator/parser/index.ts @@ -10,8 +10,9 @@ import { IEvaluator, IMatcherDto, ISplitEvaluator } from '../types'; import { ISplitCondition } from '../../dtos/types'; import { IStorageAsync, IStorageSync } from '../../storages/types'; import { SplitIO } from '../../types'; +import { ILogger } from '../../logger/types'; -export default function parser(conditions: ISplitCondition[], storage?: IStorageSync | IStorageAsync): IEvaluator { +export default function parser(log: ILogger, conditions: ISplitCondition[], storage: IStorageSync | IStorageAsync): IEvaluator { let predicates = []; for (let i = 0; i < conditions.length; i++) { @@ -27,11 +28,11 @@ export default function parser(conditions: ISplitCondition[], storage?: IStorage // create a set of pure functions from the matcher's dto const expressions = matchers.map((matcherDto: IMatcherDto) => { - const matcher = matcherFactory(matcherDto, storage); + const matcher = matcherFactory(log, matcherDto, storage); // Evaluator function. return (key: string, attributes: SplitIO.Attributes, splitEvaluator: ISplitEvaluator) => { - const value = sanitizeValue(key, matcherDto, attributes); + const value = sanitizeValue(log, key, matcherDto, attributes); const result = value !== undefined && matcher ? matcher(value, splitEvaluator) : false; if (thenable(result)) { @@ -53,7 +54,8 @@ export default function parser(conditions: ISplitCondition[], storage?: IStorage } predicates.push(conditionFactory( - andCombiner(expressions), + log, + andCombiner(log, expressions), Treatments.parse(partitions), label, conditionType @@ -61,5 +63,5 @@ export default function parser(conditions: ISplitCondition[], storage?: IStorage } // Instanciate evaluator given the set of conditions using if else if logic - return ifElseIfCombiner(predicates); + return ifElseIfCombiner(log, predicates); } diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index bda369e0..ccab4db8 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -2,6 +2,7 @@ import { IBetweenMatcherData, IDependencyMatcherData, MaybeThenable } from '../d import { IStorageAsync, IStorageSync } from '../storages/types'; import { ISet } from '../utils/lang/sets'; import { SplitIO } from '../types'; +import { ILogger } from '../logger/types'; export interface IDependencyMatcherValue { key: SplitIO.SplitKey, @@ -26,7 +27,7 @@ export interface IEvaluation { export type IEvaluationResult = IEvaluation & { treatment: string } -export type ISplitEvaluator = (key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes, storage: IStorageSync | IStorageAsync) => MaybeThenable +export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes, storage: IStorageSync | IStorageAsync) => MaybeThenable export type IEvaluator = (key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts index ba09b9b5..228d7d0a 100644 --- a/src/evaluator/value/index.ts +++ b/src/evaluator/value/index.ts @@ -1,17 +1,17 @@ import { SplitIO } from '../../types'; -import { logFactory } from '../../logger/sdkLogger'; import { IMatcherDto } from '../types'; -const log = logFactory('splitio-engine:value'); +import { ILogger } from '../../logger/types'; import sanitizeValue from './sanitize'; +import { ENGINE_VALUE, ENGINE_VALUE_NO_ATTRIBUTES, ENGINE_VALUE_INVALID } from '../../logger/constants'; -function parseValue(key: string, attributeName: string | null, attributes: SplitIO.Attributes) { +function parseValue(log: ILogger, key: string, attributeName: string | null, attributes: SplitIO.Attributes) { let value = undefined; if (attributeName) { if (attributes) { value = attributes[attributeName]; - log.debug(`Extracted attribute [${attributeName}], [${value}] will be used for matching.`); + log.debug(ENGINE_VALUE, [attributeName, value]); } else { - log.warn(`Defined attribute [${attributeName}], no attributes received.`); + log.warn(ENGINE_VALUE_NO_ATTRIBUTES, [attributeName]); } } else { value = key; @@ -23,15 +23,15 @@ function parseValue(key: string, attributeName: string | null, attributes: Split /** * Defines value to be matched (key / attribute). */ -export default function value(key: string, matcherDto: IMatcherDto, attributes: SplitIO.Attributes) { +export default function value(log: ILogger, key: string, matcherDto: IMatcherDto, attributes: SplitIO.Attributes) { const attributeName = matcherDto.attribute; - const valueToMatch = parseValue(key, attributeName, attributes); - const sanitizedValue = sanitizeValue(matcherDto.type, valueToMatch, matcherDto.dataType, attributes); + const valueToMatch = parseValue(log, key, attributeName, attributes); + const sanitizedValue = sanitizeValue(log, matcherDto.type, valueToMatch, matcherDto.dataType, attributes); if (sanitizedValue !== undefined) { return sanitizedValue; } else { - log.warn(`Value ${valueToMatch} ${attributeName ? `for attribute ${attributeName} ` : + ''}doesn't match with expected type.`); + log.warn(ENGINE_VALUE_INVALID, [valueToMatch + (attributeName ? ' for attribute ' + attributeName : '')]); return; } } diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 4f7a283a..46cae859 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -1,10 +1,10 @@ import { SplitIO } from '../../types'; import { IDependencyMatcherValue } from '../types'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-engine:sanitize'); +import { ILogger } from '../../logger/types'; import { isObject, uniq, toString, toNumber } from '../../utils/lang'; import { zeroSinceHH, zeroSinceSS } from '../convertions'; import { matcherTypes, dataTypes } from '../matchers/matcherTypes'; +import { ENGINE_SANITIZE } from '../../logger/constants'; function sanitizeNumber(val: any): number | undefined { const num = toNumber(val); @@ -69,7 +69,7 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) { /** * Sanitize matcher value */ -export default function sanitize(matcherTypeID: number, value: string | number | boolean | Array | undefined, dataType: string, attributes: SplitIO.Attributes) { +export default function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | undefined, dataType: string, attributes: SplitIO.Attributes) { const processor = getProcessingFunction(matcherTypeID, dataType); let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; @@ -99,7 +99,7 @@ export default function sanitize(matcherTypeID: number, value: string | number | sanitizedValue = processor(sanitizedValue, attributes); } - log.debug(`Attempted to sanitize [${value}] which should be of type [${dataType}]. \n Sanitized and processed value => [${sanitizedValue instanceof Object ? JSON.stringify(sanitizedValue) : sanitizedValue}]`); + log.debug(ENGINE_SANITIZE, [value, dataType, sanitizedValue instanceof Object ? JSON.stringify(sanitizedValue) : sanitizedValue]); return sanitizedValue; } diff --git a/src/index.ts b/src/index.ts index 7cc9b5d7..7abf494f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,3 @@ export { evaluateFeature, evaluateFeatures } from './evaluator'; // Utils: export { hash128 } from './utils/murmur3/murmur3_128'; export { hash } from './utils/murmur3/murmur3'; - -// Logger -export { logFactory, API } from './logger/sdkLogger'; diff --git a/src/integrations/__tests__/browser.spec.ts b/src/integrations/__tests__/browser.spec.ts index 45b99fe4..9bab3e99 100644 --- a/src/integrations/__tests__/browser.spec.ts +++ b/src/integrations/__tests__/browser.spec.ts @@ -1,6 +1,7 @@ import { GOOGLE_ANALYTICS_TO_SPLIT, SPLIT_TO_GOOGLE_ANALYTICS } from '../../utils/constants/browser'; import { SPLIT_IMPRESSION, SPLIT_EVENT } from '../../utils/constants'; import { IIntegrationManager } from '../types'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; // Mock integration modules (GaToSplit and SplitToGa). @@ -17,10 +18,11 @@ const SplitToGaQueueMethod = jest.fn(); }); -const fakeContext = { +const fakeParams = { storage: 'fakeStorage', settings: { - core: 'fakeCore' + core: 'fakeCore', + log: loggerMock } }; @@ -44,18 +46,18 @@ describe('IntegrationsManagerFactory for browser', () => { expect(instance1).toBe(undefined); // The instance should be undefined if settings.integrations does not contain integrations that register a listener. let integrations: BrowserIntegration[] = [{ type: GOOGLE_ANALYTICS_TO_SPLIT }, { type: SPLIT_TO_GOOGLE_ANALYTICS }]; - const instance2 = browserIMF(integrations, fakeContext as any) as IIntegrationManager; - expect((GaToSplitMock as jest.Mock).mock.calls.length).toBe(1); // GaToSplit invoked once - expect((SplitToGaMock as unknown as jest.Mock).mock.calls.length).toBe(1); // SplitToGa invoked once + const instance2 = browserIMF(integrations, fakeParams as any) as IIntegrationManager; + expect(GaToSplitMock).toBeCalledTimes(1); // GaToSplit invoked once + expect(SplitToGaMock).toBeCalledTimes(1); // SplitToGa invoked once expect(typeof instance2.handleImpression).toBe('function'); // The instance should implement the handleImpression method if settings.integrations has items that register a listener. expect(typeof instance2.handleEvent).toBe('function'); // The instance should implement the handleEvent method if settings.integrations has items that register a listener. clearMocks(); integrations = [{ type: GOOGLE_ANALYTICS_TO_SPLIT }, { type: SPLIT_TO_GOOGLE_ANALYTICS }, { type: GOOGLE_ANALYTICS_TO_SPLIT }, { type: SPLIT_TO_GOOGLE_ANALYTICS }, { type: SPLIT_TO_GOOGLE_ANALYTICS }]; - browserIMF(integrations, fakeContext as any); - expect((GaToSplitMock as jest.Mock).mock.calls.length).toBe(2); // GaToSplit invoked twice - expect((SplitToGaMock as unknown as jest.Mock).mock.calls.length).toBe(3); // SplitToGa invoked thrice + browserIMF(integrations, fakeParams as any); + expect(GaToSplitMock).toBeCalledTimes(2); // GaToSplit invoked twice + expect(SplitToGaMock).toBeCalledTimes(3); // SplitToGa invoked thrice clearMocks(); }); @@ -65,9 +67,9 @@ describe('IntegrationsManagerFactory for browser', () => { type: 'GOOGLE_ANALYTICS_TO_SPLIT', prefix: 'some-prefix' }]; - browserIMF(integrations, fakeContext as any); + browserIMF(integrations, fakeParams as any); - expect((GaToSplitMock as jest.Mock).mock.calls).toEqual([[integrations[0], fakeContext.storage, fakeContext.settings.core]]); // Invokes GaToSplit integration module with options, storage and core settings + expect((GaToSplitMock as jest.Mock).mock.calls).toEqual([[integrations[0], fakeParams]]); // Invokes GaToSplit integration module with options, storage and core settings clearMocks(); }); @@ -77,9 +79,9 @@ describe('IntegrationsManagerFactory for browser', () => { type: 'SPLIT_TO_GOOGLE_ANALYTICS', events: true }]; - const instance = browserIMF(integrations, fakeContext as any); + const instance = browserIMF(integrations, fakeParams as any); - expect((SplitToGaMock as unknown as jest.Mock).mock.calls).toEqual([[integrations[0]]]); // Invokes SplitToGa integration module with options + expect((SplitToGaMock as unknown as jest.Mock).mock.calls).toEqual([[fakeParams.settings.log, integrations[0]]]); // Invokes SplitToGa integration module with options const fakeImpression = 'fake'; // @ts-expect-error instance.handleImpression(fakeImpression); diff --git a/src/integrations/ga/GaToSplit.ts b/src/integrations/ga/GaToSplit.ts index 1a95aea3..befa31f1 100644 --- a/src/integrations/ga/GaToSplit.ts +++ b/src/integrations/ga/GaToSplit.ts @@ -1,7 +1,6 @@ /* eslint-disable no-undef */ import objectAssign from 'object-assign'; import { isString, isFiniteNumber, uniqAsStrings } from '../../utils/lang'; -import { logFactory } from '../../logger/sdkLogger'; import { validateEvent, validateEventValue, @@ -9,11 +8,13 @@ import { validateKey, validateTrafficType, } from '../../utils/inputValidation'; -import { IEventsCacheBase } from '../../storages/types'; -import { SplitIO, ISettings } from '../../types'; +import { SplitIO } from '../../types'; import { Identity, GoogleAnalyticsToSplitOptions } from './types'; -const logName = 'splitio-ga-to-split', logNameMapper = logName + ':mapper'; -const log = logFactory(logName); +import { ILogger } from '../../logger/types'; +import { IIntegrationFactoryParams } from '../types'; + +const logPrefix = 'ga-to-split: '; +const logNameMapper = 'ga-to-split:mapper'; /** * Provides a plugin to use with analytics.js, accounting for the possibility @@ -125,24 +126,24 @@ export function validateIdentities(identities?: Identity[]) { * @param {EventData} data event data instance to validate. Precondition: data != undefined * @returns {boolean} Whether the data instance is a valid EventData or not. */ -export function validateEventData(eventData: any): eventData is SplitIO.EventData { - if (!validateEvent(eventData.eventTypeId, logNameMapper)) +export function validateEventData(log: ILogger, eventData: any): eventData is SplitIO.EventData { + if (!validateEvent(log, eventData.eventTypeId, logNameMapper)) return false; - if (validateEventValue(eventData.value, logNameMapper) === false) + if (validateEventValue(log, eventData.value, logNameMapper) === false) return false; - const { properties } = validateEventProperties(eventData.properties, logNameMapper); + const { properties } = validateEventProperties(log, eventData.properties, logNameMapper); if (properties === false) return false; if (eventData.timestamp && !isFiniteNumber(eventData.timestamp)) return false; - if (eventData.key && validateKey(eventData.key, logNameMapper) === false) + if (eventData.key && validateKey(log, eventData.key, logNameMapper) === false) return false; - if (eventData.trafficTypeName && validateTrafficType(eventData.trafficTypeName, logNameMapper) === false) + if (eventData.trafficTypeName && validateTrafficType(log, eventData.trafficTypeName, logNameMapper) === false) return false; return true; @@ -153,10 +154,11 @@ const INVALID_SUBSTRING_REGEX = /[^-_.:a-zA-Z0-9]+/g; /** * Fixes the passed string value to comply with EventTypeId format, by removing invalid characters and truncating if necessary. * + * @param {object} log factory logger * @param {string} eventTypeId string value to fix. * @returns {string} Fixed version of `eventTypeId`. */ -export function fixEventTypeId(eventTypeId: any) { +export function fixEventTypeId(log: ILogger, eventTypeId: any) { // return the input eventTypeId if it cannot be fixed if (!isString(eventTypeId) || eventTypeId.length === 0) { return eventTypeId; @@ -167,7 +169,7 @@ export function fixEventTypeId(eventTypeId: any) { .replace(INVALID_PREFIX_REGEX, '') .replace(INVALID_SUBSTRING_REGEX, '_'); const truncated = fixed.slice(0, 80); - if (truncated.length < fixed.length) log.warn('EventTypeId was truncated because it cannot be more than 80 characters long.'); + if (truncated.length < fixed.length) log.warn(logPrefix + 'EventTypeId was truncated because it cannot be more than 80 characters long.'); return truncated; } @@ -178,8 +180,11 @@ export function fixEventTypeId(eventTypeId: any) { * @param {object} sdkOptions options passed at the SDK integrations settings (isomorphic SDK) or the GoogleAnalyticsToSplit plugin (pluggable browser SDK) * @param {object} storage SDK storage passed to track events * @param {object} coreSettings core settings used to define an identity if no one provided as SDK or plugin options + * @param {object} log factory logger */ -export default function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, storage: { events: IEventsCacheBase }, coreSettings: ISettings['core']) { +export default function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, params: IIntegrationFactoryParams) { + + const { storage, settings: { core: coreSettings, log } } = params; const defaultOptions = { prefix: defaultPrefix, @@ -205,19 +210,19 @@ export default function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, sto const validIdentities = validateIdentities(opts.identities); if (validIdentities.length === 0) { - log.warn('No valid identities were provided. Please check that you are passing a valid list of identities or providing a traffic type at the SDK configuration.'); + log.warn(logPrefix + 'No valid identities were provided. Please check that you are passing a valid list of identities or providing a traffic type at the SDK configuration.'); return; } const invalids = validIdentities.length - opts.identities.length; if (invalids) { - log.warn(`${invalids} identities were discarded because they are invalid or duplicated. Identities must be an array of objects with key and trafficType.`); + log.warn(logPrefix + `${invalids} identities were discarded because they are invalid or duplicated. Identities must be an array of objects with key and trafficType.`); } opts.identities = validIdentities; // Validate prefix if (!isString(opts.prefix)) { - log.warn('The provided `prefix` was ignored since it is invalid. Please check that you are passing a string object as `prefix`.'); + log.warn(logPrefix + 'The provided `prefix` was ignored since it is invalid. Please check that you are passing a string object as `prefix`.'); opts.prefix = undefined; } @@ -234,7 +239,7 @@ export default function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, sto try { if (opts.filter && !opts.filter(model)) return; } catch (err) { - log.warn(`GaToSplit custom filter threw: ${err}`); + log.warn(logPrefix + `custom filter threw: ${err}`); return; } @@ -244,7 +249,7 @@ export default function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, sto try { eventData = opts.mapper(model, eventData as SplitIO.EventData); } catch (err) { - log.warn(`GaToSplit custom mapper threw: ${err}`); + log.warn(logPrefix + `custom mapper threw: ${err}`); return; } if (!eventData) @@ -254,9 +259,9 @@ export default function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, sto // Add prefix. Nothing is appended if the prefix is falsy, e.g. undefined or ''. if (opts.prefix) eventData.eventTypeId = `${opts.prefix}.${eventData.eventTypeId}`; - eventData.eventTypeId = fixEventTypeId(eventData.eventTypeId); + eventData.eventTypeId = fixEventTypeId(log, eventData.eventTypeId); - if (!validateEventData(eventData)) + if (!validateEventData(log, eventData)) return; // Store the event @@ -273,7 +278,7 @@ export default function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, sto } }); - log.info('Started GA-to-Split integration'); + log.info(logPrefix + 'integration started'); } } diff --git a/src/integrations/ga/GaToSplitPlugin.ts b/src/integrations/ga/GaToSplitPlugin.ts index 5a4e9449..04a16905 100644 --- a/src/integrations/ga/GaToSplitPlugin.ts +++ b/src/integrations/ga/GaToSplitPlugin.ts @@ -6,6 +6,6 @@ export default function GaToSplitPlugin(options: GoogleAnalyticsToSplitOptions) // GaToSplit integration factory return (params: IIntegrationFactoryParams) => { - return GaToSplit(options, params.storage, params.settings.core); + return GaToSplit(options, params); }; } diff --git a/src/integrations/ga/SplitToGa.ts b/src/integrations/ga/SplitToGa.ts index e0fb82ea..df933c21 100644 --- a/src/integrations/ga/SplitToGa.ts +++ b/src/integrations/ga/SplitToGa.ts @@ -1,13 +1,14 @@ /* eslint-disable no-undef */ -import { logFactory } from '../../logger/sdkLogger'; import { uniq } from '../../utils/lang'; import { SPLIT_IMPRESSION, SPLIT_EVENT } from '../../utils/constants'; import { SplitIO } from '../../types'; import { IIntegration } from '../types'; import { SplitToGoogleAnalyticsOptions } from './types'; -const log = logFactory('splitio-split-to-ga'); +import { ILogger } from '../../logger/types'; +const logPrefix = 'split-to-ga: '; const noGaWarning = '`ga` command queue not found.'; +const noHit = 'No hit was sent.'; export default class SplitToGa implements IIntegration { @@ -19,6 +20,7 @@ export default class SplitToGa implements IIntegration { private mapper?: (data: SplitIO.IntegrationData, defaultMapping: UniversalAnalytics.FieldsObject) => UniversalAnalytics.FieldsObject; private impressions: boolean | undefined; private events: boolean | undefined; + private log: ILogger; // Default mapper function. static defaultMapper({ type, payload }: SplitIO.IntegrationData): UniversalAnalytics.FieldsObject { @@ -54,13 +56,14 @@ export default class SplitToGa implements IIntegration { * Other validations (e.g., an `event` hitType must have a `eventCategory` and `eventAction`) are handled * and logged (as warnings or errors depending the case) by GA debugger, but the hit is sent anyway. * + * @param {object} log factory logger * @param {UniversalAnalytics.FieldsObject} fieldsObject object to validate. * @returns {boolean} Whether the data instance is a valid FieldsObject or not. */ - static validateFieldsObject(fieldsObject: any): fieldsObject is UniversalAnalytics.FieldsObject { + static validateFieldsObject(log: ILogger, fieldsObject: any): fieldsObject is UniversalAnalytics.FieldsObject { if (fieldsObject && fieldsObject.hitType) return true; - log.warn('your custom mapper returned an invalid FieldsObject instance. It must be an object with at least a `hitType` field.'); + log.warn(logPrefix + 'your custom mapper returned an invalid FieldsObject instance. It must be an object with at least a `hitType` field.'); return false; } @@ -68,9 +71,10 @@ export default class SplitToGa implements IIntegration { * constructor description * @param {object} options options passed at the SDK integrations settings (isomorphic SDK) or the SplitToGoogleAnalytics plugin (pluggable browser SDK) */ - constructor(options?: SplitToGoogleAnalyticsOptions) { + constructor(log: ILogger, options: SplitToGoogleAnalyticsOptions) { this.trackerNames = SplitToGa.defaultTrackerNames; + this.log = log; if (options) { if (typeof options.filter === 'function') this.filter = options.filter; @@ -85,8 +89,8 @@ export default class SplitToGa implements IIntegration { this.events = options.events; } - log.info('Started Split-to-GA integration'); - if (typeof SplitToGa.getGa() !== 'function') log.warn(noGaWarning + ' No hits will be sent until it is available.'); + log.info(logPrefix + 'integration started'); + if (typeof SplitToGa.getGa() !== 'function') log.warn(logPrefix + `${noGaWarning} No hits will be sent until it is available.`); } queue(data: SplitIO.IntegrationData) { @@ -108,10 +112,10 @@ export default class SplitToGa implements IIntegration { if (this.mapper) { fieldsObject = this.mapper(data, fieldsObject); // don't send the hit if it is falsy or invalid - if (!fieldsObject || !SplitToGa.validateFieldsObject(fieldsObject)) return; + if (!fieldsObject || !SplitToGa.validateFieldsObject(this.log, fieldsObject)) return; } } catch (err) { - log.warn(`SplitToGa queue method threw: ${err}. No hit was sent.`); + this.log.warn(logPrefix + `queue method threw: ${err}. ${noHit}`); return; } @@ -124,7 +128,7 @@ export default class SplitToGa implements IIntegration { ga(sendCommand, fieldsObject); }); } else { - log.warn(noGaWarning + ' No hit was sent.'); + this.log.warn(logPrefix + `${noGaWarning} ${noHit}`); } } diff --git a/src/integrations/ga/SplitToGaPlugin.ts b/src/integrations/ga/SplitToGaPlugin.ts index 309ebf53..9001c180 100644 --- a/src/integrations/ga/SplitToGaPlugin.ts +++ b/src/integrations/ga/SplitToGaPlugin.ts @@ -1,10 +1,11 @@ +import { IIntegrationFactoryParams } from '../types'; import SplitToGa from './SplitToGa'; import { SplitToGoogleAnalyticsOptions } from './types'; -export default function SplitToGaPlugin(options: SplitToGoogleAnalyticsOptions) { +export default function SplitToGaPlugin(options: SplitToGoogleAnalyticsOptions = {}) { // SplitToGa integration factory - return () => { - return new SplitToGa(options); + return (params: IIntegrationFactoryParams) => { + return new SplitToGa(params.settings.log, options); }; } diff --git a/src/integrations/ga/__tests__/GaToSplit.spec.ts b/src/integrations/ga/__tests__/GaToSplit.spec.ts index f48dd1c6..21c4a1ef 100644 --- a/src/integrations/ga/__tests__/GaToSplit.spec.ts +++ b/src/integrations/ga/__tests__/GaToSplit.spec.ts @@ -3,6 +3,7 @@ import { IEventsCacheSync } from '../../../storages/types'; import { SplitIO, ISettings } from '../../../types'; import GaToSplit, { validateIdentities, defaultPrefix, defaultMapper, validateEventData, fixEventTypeId } from '../GaToSplit'; import { gaMock, gaRemove, modelMock } from './gaMock'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; const hitSample: UniversalAnalytics.FieldsObject = { hitType: 'pageview', @@ -56,37 +57,37 @@ test('validateIdentities', () => { }); test('validateEventData', () => { - expect(() => { validateEventData(undefined); }).toThrow(); // throws exception if passed object is undefined - expect(() => { validateEventData(null); }).toThrow(); // throws exception if passed object is null + expect(() => { validateEventData(loggerMock, undefined); }).toThrow(); // throws exception if passed object is undefined + expect(() => { validateEventData(loggerMock, null); }).toThrow(); // throws exception if passed object is null - expect(validateEventData({})).toBe(false); // event must have a valid eventTypeId - expect(validateEventData({ eventTypeId: 'type' })).toBe(true); // event must have a valid eventTypeId - expect(validateEventData({ eventTypeId: 123 })).toBe(false); // event must have a valid eventTypeId + expect(validateEventData(loggerMock, {})).toBe(false); // event must have a valid eventTypeId + expect(validateEventData(loggerMock, { eventTypeId: 'type' })).toBe(true); // event must have a valid eventTypeId + expect(validateEventData(loggerMock, { eventTypeId: 123 })).toBe(false); // event must have a valid eventTypeId - expect(validateEventData({ eventTypeId: 'type', value: 'value' })).toBe(false); // event must have a valid value if present - expect(validateEventData({ eventTypeId: 'type', value: 0 })).toBe(true); // event must have a valid value if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', value: 'value' })).toBe(false); // event must have a valid value if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', value: 0 })).toBe(true); // event must have a valid value if present - expect(validateEventData({ eventTypeId: 'type', properties: ['prop1'] })).toBe(false); // event must have valid properties if present - expect(validateEventData({ eventTypeId: 'type', properties: { prop1: 'prop1' } })).toBe(true); // event must have valid properties if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', properties: ['prop1'] })).toBe(false); // event must have valid properties if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', properties: { prop1: 'prop1' } })).toBe(true); // event must have valid properties if present - expect(validateEventData({ eventTypeId: 'type', timestamp: true })).toBe(false); // event must have a valid timestamp if present - expect(validateEventData({ eventTypeId: 'type', timestamp: Date.now() })).toBe(true); // event must have a valid timestamp if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', timestamp: true })).toBe(false); // event must have a valid timestamp if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', timestamp: Date.now() })).toBe(true); // event must have a valid timestamp if present - expect(validateEventData({ eventTypeId: 'type', key: true })).toBe(false); // event must have a valid key if present - expect(validateEventData({ eventTypeId: 'type', key: 'key' })).toBe(true); // event must have a valid key if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', key: true })).toBe(false); // event must have a valid key if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', key: 'key' })).toBe(true); // event must have a valid key if present - expect(validateEventData({ eventTypeId: 'type', trafficTypeName: true })).toBe(false); // event must have a valid trafficTypeName if present - expect(validateEventData({ eventTypeId: 'type', trafficTypeName: 'tt' })).toBe(true); // event must have a valid trafficTypeName if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', trafficTypeName: true })).toBe(false); // event must have a valid trafficTypeName if present + expect(validateEventData(loggerMock, { eventTypeId: 'type', trafficTypeName: 'tt' })).toBe(true); // event must have a valid trafficTypeName if present }); test('fixEventTypeId', () => { - expect(fixEventTypeId(undefined)).toBe(undefined); - expect(fixEventTypeId(111)).toBe(111); - expect(fixEventTypeId('')).toBe(''); - expect(fixEventTypeId('()')).toBe(''); - expect(fixEventTypeId('()+_')).toBe(''); - expect(fixEventTypeId(' some event ')).toBe('some_event_'); - expect(fixEventTypeId(' -*- some -.%^ event =+ ')).toBe('some_-._event_'); + expect(fixEventTypeId(loggerMock, undefined)).toBe(undefined); + expect(fixEventTypeId(loggerMock, 111)).toBe(111); + expect(fixEventTypeId(loggerMock, '')).toBe(''); + expect(fixEventTypeId(loggerMock, '()')).toBe(''); + expect(fixEventTypeId(loggerMock, '()+_')).toBe(''); + expect(fixEventTypeId(loggerMock, ' some event ')).toBe('some_event_'); + expect(fixEventTypeId(loggerMock, ' -*- some -.%^ event =+ ')).toBe('some_-._event_'); }); test('defaultMapper', () => { @@ -110,6 +111,11 @@ const fakeStorage = { track: jest.fn() } as IEventsCacheSync }; +const fakeParams = { + storage: fakeStorage, + settings: { core: coreSettings, log: loggerMock } +}; + // Returns a new event by copying defaultEvent function customMapper(model: UniversalAnalytics.Model, defaultEvent: SplitIO.EventData) { return { ...defaultEvent, properties: { ...defaultEvent.properties, someProp: 'someProp' } }; @@ -136,7 +142,7 @@ test('GaToSplit', () => { const { ga, tracker } = gaMock(); // provide SplitTracker plugin - GaToSplit({}, fakeStorage, coreSettings); + GaToSplit({}, fakeParams as any); // @ts-expect-error let [arg1, arg2, SplitTracker] = ga.mock.calls.pop() as [string, string, any]; expect([arg1, arg2]).toEqual(['provide', 'splitTracker']); @@ -180,7 +186,7 @@ test('GaToSplit', () => { // provide a new SplitTracker plugin with custom SDK options GaToSplit({ mapper: customMapper2, filter: customFilter, identities: customIdentities, prefix: '' - }, fakeStorage, coreSettings); + }, fakeParams as any); // @ts-expect-error [arg1, arg2, SplitTracker] = ga.mock.calls.pop(); expect([arg1, arg2]).toEqual(['provide', 'splitTracker']); @@ -205,7 +211,7 @@ test('GaToSplit', () => { // provide a new SplitTracker plugin with custom SDK options GaToSplit({ mapper: customMapper3, filter: customFilter, identities: customIdentities, prefix: '' - }, fakeStorage, coreSettings); + }, fakeParams as any); // @ts-ignore [arg1, arg2, SplitTracker] = ga.mock.calls.pop(); expect([arg1, arg2]).toEqual(['provide', 'splitTracker']); @@ -231,7 +237,7 @@ test('GaToSplit: `hits` flag param', () => { // test setup const { ga, tracker } = gaMock(); - GaToSplit({}, fakeStorage, coreSettings); // @ts-expect-error + GaToSplit({}, fakeParams as any); // @ts-expect-error let SplitTracker: any = ga.mock.calls.pop()[2]; // init plugin with custom options @@ -240,7 +246,7 @@ test('GaToSplit: `hits` flag param', () => { // send hit and assert that it was not tracked as a Split event (fakeStorage.events.track as jest.Mock).mockClear(); window.ga('send', hitSample); - expect((fakeStorage.events.track as jest.Mock).mock.calls.length).toBe(0); + expect(fakeStorage.events.track).toBeCalledTimes(0); // test teardown gaRemove(); diff --git a/src/integrations/ga/__tests__/SplitToGa.spec.ts b/src/integrations/ga/__tests__/SplitToGa.spec.ts index fd6a3829..849350ca 100644 --- a/src/integrations/ga/__tests__/SplitToGa.spec.ts +++ b/src/integrations/ga/__tests__/SplitToGa.spec.ts @@ -2,7 +2,7 @@ import { SplitIO } from '../../../types'; import { SPLIT_IMPRESSION, SPLIT_EVENT } from '../../../utils/constants'; // Mocks -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { gaMock, gaRemove } from './gaMock'; // Test target @@ -58,17 +58,17 @@ const defaultEventFieldsObject = { describe('SplitToGa', () => { test('SplitToGa.validateFieldsObject', () => { - expect(SplitToGa.validateFieldsObject(undefined)).toBe(false); - expect(SplitToGa.validateFieldsObject(null)).toBe(false); - expect(SplitToGa.validateFieldsObject(123)).toBe(false); - expect(SplitToGa.validateFieldsObject(true)).toBe(false); - expect(SplitToGa.validateFieldsObject('something')).toBe(false); - expect(SplitToGa.validateFieldsObject(/asd/ig)).toBe(false); - expect(SplitToGa.validateFieldsObject(function () { })).toBe(false); - - expect(SplitToGa.validateFieldsObject({})).toBe(false); // An empty object is an invalid FieldsObject instance - expect(SplitToGa.validateFieldsObject({ hitType: 10 })).toBe(true); // A fields object instance must have a HitType - expect(SplitToGa.validateFieldsObject({ hitType: 'event', ignoredProp: 'ignoredProp' })).toBe(true); // A fields object instance must have a HitType + expect(SplitToGa.validateFieldsObject(loggerMock, undefined)).toBe(false); + expect(SplitToGa.validateFieldsObject(loggerMock, null)).toBe(false); + expect(SplitToGa.validateFieldsObject(loggerMock, 123)).toBe(false); + expect(SplitToGa.validateFieldsObject(loggerMock, true)).toBe(false); + expect(SplitToGa.validateFieldsObject(loggerMock, 'something')).toBe(false); + expect(SplitToGa.validateFieldsObject(loggerMock, /asd/ig)).toBe(false); + expect(SplitToGa.validateFieldsObject(loggerMock, function () { })).toBe(false); + + expect(SplitToGa.validateFieldsObject(loggerMock, {})).toBe(false); // An empty object is an invalid FieldsObject instance + expect(SplitToGa.validateFieldsObject(loggerMock, { hitType: 10 })).toBe(true); // A fields object instance must have a HitType + expect(SplitToGa.validateFieldsObject(loggerMock, { hitType: 'event', ignoredProp: 'ignoredProp' })).toBe(true); // A fields object instance must have a HitType }); test('SplitToGa.defaultMapper', () => { @@ -79,25 +79,25 @@ describe('SplitToGa', () => { }); test('SplitToGa.getGa', () => { - mockClear(); + loggerMock.mockClear(); const { ga } = gaMock(); expect(SplitToGa.getGa()).toBe(ga); // should return ga command queue if it exists - let integration = new SplitToGa(); + let integration = new SplitToGa(loggerMock, {}); expect(typeof integration).toBe('object'); - expect(loggerMock.warn.mock.calls.length).toBe(0); + expect(loggerMock.warn).not.toBeCalled(); gaRemove(); expect(SplitToGa.getGa()).toBe(undefined); // should return undefined if ga command queue does not exist - integration = new SplitToGa(); + integration = new SplitToGa(loggerMock, {}); expect(typeof integration).toBe('object'); // SplitToGa instances should be created even if ga command queue does not exist // @ts-expect-error integration.queue('fake-data'); expect(loggerMock.warn.mock.calls).toEqual([ // Warn when creating and queueing while ga command queue does not exist - ['`ga` command queue not found. No hits will be sent until it is available.'], - ['`ga` command queue not found. No hit was sent.'] + ['split-to-ga: `ga` command queue not found. No hits will be sent until it is available.'], + ['split-to-ga: `ga` command queue not found. No hit was sent.'] ]); }); @@ -107,16 +107,16 @@ describe('SplitToGa', () => { const { ga } = gaMock(); /** Default behaviour **/ - const instance = new SplitToGa() as SplitToGa; + const instance = new SplitToGa(loggerMock, {}) as SplitToGa; instance.queue(fakeImpression); // should queue `ga send` with the default mapped FieldsObject for impressions, appended with `splitHit` field - expect(ga.mock.calls[ga.mock.calls.length - 1]).toEqual(['send', { ...defaultImpressionFieldsObject, splitHit: true }]); + expect(ga).lastCalledWith('send', { ...defaultImpressionFieldsObject, splitHit: true }); instance.queue(fakeEvent); // should queue `ga send` with the default mapped FieldsObject for events, appended with `splitHit` field - expect(ga.mock.calls[ga.mock.calls.length - 1]).toEqual(['send', { ...defaultEventFieldsObject, splitHit: true }]); + expect(ga).lastCalledWith('send', { ...defaultEventFieldsObject, splitHit: true }); - expect(ga.mock.calls.length).toBe(2); + expect(ga).toBeCalledTimes(2); /** Custom behaviour **/ // Custom filter @@ -131,14 +131,14 @@ describe('SplitToGa', () => { } as UniversalAnalytics.FieldsObject; } const trackerNames = ['', 'namedTracker']; - const instance2 = new SplitToGa({ + const instance2 = new SplitToGa(loggerMock, { filter: customFilter, mapper: customMapper, trackerNames, }) as SplitToGa; ga.mockClear(); instance2.queue(fakeImpression); - expect(ga.mock.calls.length === 0).toBe(true); // t queue `ga send` if a Split data (impression or event) is filtered + expect(ga).not.toBeCalled(); // shouldn't queue `ga send` if a Split data (impression or event) is filtered instance2.queue(fakeEvent); expect(ga.mock.calls).toEqual([ @@ -146,48 +146,48 @@ describe('SplitToGa', () => { [`${trackerNames[1]}.send`, { ...customMapper(), splitHit: true }] ]); // should queue `ga send` with the custom trackerName and FieldsObject from customMapper, appended with `splitHit` field - expect(ga.mock.calls.length).toBe(2); + expect(ga).toBeCalledTimes(2); // Custom mapper that returns the default FieldsObject function customMapper2(data: SplitIO.IntegrationData, defaultFieldsObject: UniversalAnalytics.FieldsObject) { return defaultFieldsObject; } - const instance3 = new SplitToGa({ + const instance3 = new SplitToGa(loggerMock, { mapper: customMapper2, }) as SplitToGa; ga.mockClear(); instance3.queue(fakeImpression); // should queue `ga send` with the custom FieldsObject from customMapper2, appended with `splitHit` field - expect(ga.mock.calls[ga.mock.calls.length - 1]).toEqual(['send', { ...customMapper2(fakeImpression, defaultImpressionFieldsObject), splitHit: true }]); + expect(ga).lastCalledWith('send', { ...customMapper2(fakeImpression, defaultImpressionFieldsObject), splitHit: true }); - expect(ga.mock.calls.length).toBe(1); + expect(ga).toBeCalledTimes(1); // Custom mapper that throws an error function customMapper3() { throw 'some error'; } - const instance4 = new SplitToGa({ // @ts-expect-error + const instance4 = new SplitToGa(loggerMock, { // @ts-expect-error mapper: customMapper3, }) as SplitToGa; ga.mockClear(); instance4.queue(fakeImpression); - expect(ga.mock.calls.length === 0).toBe(true); // t queue `ga send` if a custom mapper throw an exception + expect(ga).not.toBeCalled(); // shouldn't queue `ga send` if a custom mapper throw an exception // `impressions` flags - const instance5 = new SplitToGa({ + const instance5 = new SplitToGa(loggerMock, { impressions: false, }) as SplitToGa; ga.mockClear(); instance5.queue(fakeImpression); - expect(ga.mock.calls.length === 0).toBe(true); // t queue `ga send` for an impression if `impressions` flag is false + expect(ga).not.toBeCalled(); // shouldn't queue `ga send` for an impression if `impressions` flag is false // `impressions` flags - const instance6 = new SplitToGa({ + const instance6 = new SplitToGa(loggerMock, { events: false, }) as SplitToGa; ga.mockClear(); instance6.queue(fakeEvent); - expect(ga.mock.calls.length === 0).toBe(true); // t queue `ga send` for a event if `events` flag is false + expect(ga).not.toBeCalled(); // shouldn't queue `ga send` for a event if `events` flag is false // test teardown gaRemove(); diff --git a/src/listeners/__tests__/browser.spec.ts b/src/listeners/__tests__/browser.spec.ts index d1be1968..32c69f59 100644 --- a/src/listeners/__tests__/browser.spec.ts +++ b/src/listeners/__tests__/browser.spec.ts @@ -1,7 +1,7 @@ import BrowserSignalListener from '../browser'; import { IEventsCacheSync, IImpressionCountsCacheSync, IImpressionsCacheSync, IStorageSync } from '../../storages/types'; import { ISplitApi } from '../../services/types'; -import { settingsSplitApi } from '../../utils/settingsValidation/__tests__/settings.mocks'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; /* Mocks start */ @@ -62,8 +62,6 @@ const fakeSplitApi = { postTestImpressionsCount: jest.fn(() => Promise.resolve()) } as ISplitApi; -const fakeSettings = settingsSplitApi; - const UNLOAD_DOM_EVENT = 'unload'; const unloadEventListeners = new Set<() => any>(); @@ -109,7 +107,7 @@ function triggerUnloadEvent() { test('Browser JS listener / Impressions optimized mode', (done) => { - const listener = new BrowserSignalListener(undefined, fakeSettings, fakeStorageOptimized as IStorageSync, fakeSplitApi); + const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageOptimized as IStorageSync, fakeSplitApi); listener.start(); @@ -120,15 +118,15 @@ test('Browser JS listener / Impressions optimized mode', (done) => { setTimeout(() => { // Unload event was triggered. Thus sendBeacon method should have been called three times. - expect((global.window.navigator.sendBeacon as jest.Mock).mock.calls.length).toBe(3); + expect(global.window.navigator.sendBeacon).toBeCalledTimes(3); // Http post services should have not been called - expect((fakeSplitApi.postTestImpressionsBulk as jest.Mock).mock.calls.length).toBe(0); - expect((fakeSplitApi.postEventsBulk as jest.Mock).mock.calls.length).toBe(0); - expect((fakeSplitApi.postTestImpressionsCount as jest.Mock).mock.calls.length).toBe(0); + expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); + expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); + expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); // pre-check and call stop - expect((global.window.removeEventListener as jest.Mock).mock.calls.length).toBe(0); + expect(global.window.removeEventListener).not.toBeCalled(); listener.stop(); // removed correct listener from correct signal on stop. @@ -140,7 +138,7 @@ test('Browser JS listener / Impressions optimized mode', (done) => { test('Browser JS listener / Impressions debug mode', (done) => { - const listener = new BrowserSignalListener(undefined, fakeSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); + const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); listener.start(); @@ -151,15 +149,15 @@ test('Browser JS listener / Impressions debug mode', (done) => { setTimeout(() => { // Unload event was triggered. Thus sendBeacon method should have been called twice. - expect((global.window.navigator.sendBeacon as jest.Mock).mock.calls.length).toBe(2); + expect(global.window.navigator.sendBeacon).toBeCalledTimes(2); // Http post services should have not been called - expect((fakeSplitApi.postTestImpressionsBulk as jest.Mock).mock.calls.length).toBe(0); - expect((fakeSplitApi.postEventsBulk as jest.Mock).mock.calls.length).toBe(0); - expect((fakeSplitApi.postTestImpressionsCount as jest.Mock).mock.calls.length).toBe(0); + expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); + expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); + expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); // pre-check and call stop - expect((global.window.removeEventListener as jest.Mock).mock.calls.length === 0).toBe(true); + expect(global.window.removeEventListener).not.toBeCalled(); listener.stop(); // removed correct listener from correct signal on stop. @@ -174,7 +172,7 @@ test('Browser JS listener / Impressions debug mode without sendBeacon API', (don const sendBeacon = global.navigator.sendBeacon; // @ts-expect-error global.navigator.sendBeacon = undefined; - const listener = new BrowserSignalListener(undefined, fakeSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); + const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); listener.start(); @@ -185,15 +183,15 @@ test('Browser JS listener / Impressions debug mode without sendBeacon API', (don setTimeout(() => { // Unload event was triggered. Thus sendBeacon method should have been called twice. - expect((sendBeacon as jest.Mock).mock.calls.length).toBe(0); + expect(sendBeacon).not.toBeCalled(); // Http post services should have not been called - expect((fakeSplitApi.postTestImpressionsBulk as jest.Mock).mock.calls.length).toBe(1); - expect((fakeSplitApi.postEventsBulk as jest.Mock).mock.calls.length).toBe(1); - expect((fakeSplitApi.postTestImpressionsCount as jest.Mock).mock.calls.length).toBe(0); + expect(fakeSplitApi.postTestImpressionsBulk).toBeCalledTimes(1); + expect(fakeSplitApi.postEventsBulk).toBeCalledTimes(1); + expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); // pre-check and call stop - expect((global.window.removeEventListener as jest.Mock).mock.calls.length === 0).toBe(true); + expect(global.window.removeEventListener).not.toBeCalled(); listener.stop(); // removed correct listener from correct signal on stop. diff --git a/src/listeners/__tests__/node.spec.ts b/src/listeners/__tests__/node.spec.ts index ad8c0598..cf5794ed 100644 --- a/src/listeners/__tests__/node.spec.ts +++ b/src/listeners/__tests__/node.spec.ts @@ -1,4 +1,5 @@ import NodeSignalListener from '../node'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; const processOnSpy = jest.spyOn(process, 'on'); const processRemoveListenerSpy = jest.spyOn(process, 'removeListener'); @@ -7,7 +8,7 @@ const processKillSpy = jest.spyOn(process, 'kill').mockImplementation(() => true test('Node JS listener / Signal Listener class methods and start/stop functionality', () => { const handlerMock = jest.fn(); - const listener = new NodeSignalListener(handlerMock); + const listener = new NodeSignalListener(handlerMock, fullSettings); listener.start(); @@ -16,7 +17,7 @@ test('Node JS listener / Signal Listener class methods and start/stop functional expect(processOnSpy.mock.calls).toEqual([['SIGTERM', listener._sigtermHandler]]); // pre-check and call stop - expect(processRemoveListenerSpy.mock.calls.length === 0).toBe(true); + expect(processRemoveListenerSpy).not.toBeCalled(); listener.stop(); // removed correct listener from correct signal on stop. @@ -27,26 +28,26 @@ test('Node JS listener / Signal Listener class methods and start/stop functional test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () => { const handlerMock = jest.fn(); - const listener = new NodeSignalListener(handlerMock); + const listener = new NodeSignalListener(handlerMock, fullSettings); listener.start(); // Stub stop function since we don't want side effects on test. jest.spyOn(listener, 'stop'); // Control asserts. - expect((listener.stop as jest.Mock).mock.calls.length).toBe(0); - expect(handlerMock.mock.calls.length).toBe(0); - expect(processKillSpy.mock.calls.length).toBe(0); + expect(listener.stop).not.toBeCalled(); + expect(handlerMock).not.toBeCalled(); + expect(processKillSpy).not.toBeCalled(); // Call function // @ts-expect-error listener._sigtermHandler(); // Handler was properly called. - expect(handlerMock.mock.calls.length).toBe(1); + expect(handlerMock).toBeCalledTimes(1); // Clean up is called. - expect((listener.stop as jest.Mock).mock.calls.length).toBe(1); + expect(listener.stop).toBeCalledTimes(1); // It called for kill again, so the shutdown keeps going. expect(processKillSpy.mock.calls).toEqual([[process.pid, 'SIGTERM']]); @@ -56,28 +57,28 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () test('Node JS listener / Signal Listener SIGTERM callback with sync handler that throws an error', () => { const handlerMock = jest.fn(() => { throw 'some error'; }); - const listener = new NodeSignalListener(handlerMock); + const listener = new NodeSignalListener(handlerMock, fullSettings); listener.start(); // Stub stop function since we don't want side effects on test. jest.spyOn(listener, 'stop'); // Control asserts. - expect((listener.stop as jest.Mock).mock.calls.length).toBe(0); - expect(handlerMock.mock.calls.length).toBe(0); - expect(processKillSpy.mock.calls.length).toBe(0); + expect(listener.stop).not.toBeCalled(); + expect(handlerMock).not.toBeCalled(); + expect(processKillSpy).not.toBeCalled(); // Call function. // @ts-expect-error listener._sigtermHandler(); // Handler was properly called. - expect(handlerMock.mock.calls.length).toBe(1); + expect(handlerMock).toBeCalledTimes(1); // Even if the handler throws, clean up is called. - expect((listener.stop as jest.Mock).mock.calls.length).toBe(1); + expect(listener.stop).toBeCalledTimes(1); // Even if the handler throws, it should call for kill again, so the shutdown keeps going. - expect(processKillSpy.mock.calls.length).toBe(1); + expect(processKillSpy).toBeCalledTimes(1); expect(processKillSpy.mock.calls).toEqual([[process.pid, 'SIGTERM']]); // Reset the kill spy since it's used on other tests. @@ -93,7 +94,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler', a }); const handlerMock = jest.fn(() => fakePromise); - const listener = new NodeSignalListener(handlerMock); + const listener = new NodeSignalListener(handlerMock, fullSettings); // Stub stop function since we don't want side effects on test. jest.spyOn(listener, 'stop'); @@ -106,17 +107,17 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler', a listener._sigtermHandler(); // Handler was properly called. - expect(handlerMock.mock.calls.length).toBe(1); + expect(handlerMock).toBeCalledTimes(1); // Check that the wrap up is waiting for the promise to be resolved. - expect((listener.stop as jest.Mock).mock.calls.length).toBe(0); - expect(processKillSpy.mock.calls.length).toBe(0); + expect(listener.stop).not.toBeCalled(); + expect(processKillSpy).not.toBeCalled(); fakePromise.then(() => { // Clean up is called even if there is an error. - expect((listener.stop as jest.Mock).mock.calls.length).toBe(1); + expect(listener.stop).toBeCalledTimes(1); // It called for kill again, so the shutdown keeps going. - expect(processKillSpy.mock.calls.length).toBe(1); + expect(processKillSpy).toBeCalledTimes(1); expect(processKillSpy.mock.calls).toEqual([[process.pid, 'SIGTERM']]); // Reset the kill spy since it's used on other tests. @@ -137,7 +138,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha }); const handlerMock = jest.fn(() => fakePromise); - const listener = new NodeSignalListener(handlerMock); + const listener = new NodeSignalListener(handlerMock, fullSettings); // Stub stop function since we don't want side effects on test. jest.spyOn(listener, 'stop'); @@ -150,18 +151,18 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha const handlerPromise = listener._sigtermHandler(); // Handler was properly called. - expect(handlerMock.mock.calls.length).toBe(1); + expect(handlerMock).toBeCalledTimes(1); // Check that the wrap up is waiting for the promise to be resolved. - expect((listener.stop as jest.Mock).mock.calls.length).toBe(0); - expect(processKillSpy.mock.calls.length).toBe(0); + expect(listener.stop).not.toBeCalled(); + expect(processKillSpy).not.toBeCalled(); // Calling .then since the wrapUp handler does not throw. (handlerPromise as Promise).then(() => { // Clean up is called. - expect((listener.stop as jest.Mock).mock.calls.length).toBe(1); + expect(listener.stop).toBeCalledTimes(1); // It called for kill again, so the shutdown keeps going. - expect(processKillSpy.mock.calls.length).toBe(1); + expect(processKillSpy).toBeCalledTimes(1); expect(processKillSpy.mock.calls).toEqual([[process.pid, 'SIGTERM']]); /* Clean up everything */ diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index e7a47ce5..d937448e 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -1,7 +1,6 @@ /* eslint-disable no-undef */ // @TODO eventually migrate to JS-Browser-SDK package. import { ISignalListener } from './types'; -import { logFactory } from '../logger/sdkLogger'; import { IRecorderCacheConsumerSync, IStorageSync } from '../storages/types'; import { fromImpressionsCollector } from '../sync/submitters/impressionsSyncTask'; import { fromImpressionCountsCollector } from '../sync/submitters/impressionCountsSyncTask'; @@ -11,11 +10,11 @@ import { ImpressionsPayload } from '../sync/submitters/types'; import { MaybeThenable } from '../dtos/types'; import { OPTIMIZED, DEBUG } from '../utils/constants'; import objectAssign from 'object-assign'; - -const log = logFactory('splitio-client:cleanup'); +import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; // 'unload' event is used instead of 'beforeunload', since 'unload' is not a cancelable event, so no other listeners can stop the event from occurring. const UNLOAD_DOM_EVENT = 'unload'; +const EVENT_NAME = 'for unload page event.'; /** * We'll listen for 'unload' event over the window object, since it's the standard way to listen page reload and close. @@ -41,7 +40,7 @@ export default class BrowserSignalListener implements ISignalListener { */ start() { if (typeof window !== 'undefined' && window.addEventListener) { - log.debug('Registering flush handler when unload page event is triggered.'); + this.settings.log.debug(CLEANUP_REGISTERING, [EVENT_NAME]); window.addEventListener(UNLOAD_DOM_EVENT, this.flushData); } } @@ -53,7 +52,7 @@ export default class BrowserSignalListener implements ISignalListener { */ stop() { if (typeof window !== 'undefined' && window.removeEventListener) { - log.debug('Deregistering flush handler when unload page event is triggered.'); + this.settings.log.debug(CLEANUP_DEREGISTERING, [EVENT_NAME]); window.removeEventListener(UNLOAD_DOM_EVENT, this.flushData); } } diff --git a/src/listeners/node.ts b/src/listeners/node.ts index 5e9a10c1..18b01519 100644 --- a/src/listeners/node.ts +++ b/src/listeners/node.ts @@ -1,9 +1,12 @@ // @TODO eventually migrate to JS-Node-SDK package. import { ISignalListener } from './types'; import thenable from '../utils/promise/thenable'; -import { logFactory } from '../logger/sdkLogger'; import { MaybeThenable } from '../dtos/types'; -const log = logFactory('splitio-client:cleanup'); +import { ISettings } from '../types'; +import { logPrefixCleanup, CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; + +const SIGTERM = 'SIGTERM'; +const EVENT_NAME = 'for SIGTERM signal.'; /** * We'll listen for SIGTERM since it's the standard signal for server shutdown. @@ -13,23 +16,24 @@ const log = logFactory('splitio-client:cleanup'); * the process is already exiting. */ export default class NodeSignalListener implements ISignalListener { - private handler: any; - constructor(handler: () => MaybeThenable) { - this.handler = handler; + constructor( + private handler: () => MaybeThenable, + private settings: ISettings + ) { this._sigtermHandler = this._sigtermHandler.bind(this); } start() { - log.debug('Registering cleanup handlers.'); + this.settings.log.debug(CLEANUP_REGISTERING, [EVENT_NAME]); // eslint-disable-next-line no-undef - process.on('SIGTERM', this._sigtermHandler); + process.on(SIGTERM, this._sigtermHandler); } stop() { - log.debug('Deregistering cleanup handlers.'); + this.settings.log.debug(CLEANUP_DEREGISTERING, [EVENT_NAME]); // eslint-disable-next-line no-undef - process.removeListener('SIGTERM', this._sigtermHandler); + process.removeListener(SIGTERM, this._sigtermHandler); } /** @@ -42,17 +46,17 @@ export default class NodeSignalListener implements ISignalListener { // This handler prevented the default behaviour, start again. // eslint-disable-next-line no-undef - process.kill(process.pid, 'SIGTERM'); + process.kill(process.pid, SIGTERM); }; - log.debug('Split SDK graceful shutdown after SIGTERM.'); + this.settings.log.debug(logPrefixCleanup + 'Split SDK graceful shutdown after SIGTERM.'); let handlerResult = null; try { handlerResult = this.handler(); } catch (err) { - log.error(`Error with Split graceful shutdown: ${err}`); + this.settings.log.error(logPrefixCleanup + `Error with Split SDK graceful shutdown: ${err}`); } if (thenable(handlerResult)) { diff --git a/src/logger/__tests__/index.spec.ts b/src/logger/__tests__/index.spec.ts index 4ab2d782..93085c25 100644 --- a/src/logger/__tests__/index.spec.ts +++ b/src/logger/__tests__/index.spec.ts @@ -1,4 +1,6 @@ -import { Logger, LogLevels, setLogLevel, isLogLevelString } from '../index'; +import { LogLevel } from '../../types'; +import { _Map } from '../../utils/lang/maps'; +import { Logger, LogLevels, isLogLevelString, _sprintf } from '../index'; // We'll set this only once. These are the constants we will use for // comparing the LogLevel values. @@ -10,12 +12,6 @@ export const LOG_LEVELS = { NONE: 'NONE' }; -test('SPLIT LOGGER / setLogLevel utility function', () => { - expect(typeof setLogLevel).toBe('function'); // setLogLevel should be a function - expect(setLogLevel).not.toThrow(); // Calling setLogLevel should not throw an error. - -}); - test('SPLIT LOGGER / isLogLevelString utility function', () => { expect(typeof isLogLevelString).toBe('function'); // isLogLevelString should be a function expect(isLogLevelString(LOG_LEVELS.DEBUG)).toBe(true); // Calling isLogLevelString should return true with a LOG_LEVELS value @@ -32,18 +28,18 @@ test('SPLIT LOGGER / LogLevels exposed mappings', () => { test('SPLIT LOGGER / Logger class shape', () => { expect(typeof Logger).toBe('function'); // Logger should be a class we can instantiate. - const logger = new Logger('test-category', {}); + const logger = new Logger({ prefix: 'test-category' }); expect(typeof logger.debug).toBe('function'); // instance.debug should be a method. expect(typeof logger.info).toBe('function'); // instance.info should be a method. expect(typeof logger.warn).toBe('function'); // instance.warn should be a method. expect(typeof logger.error).toBe('function'); // instance.error should be a method. - + expect(typeof logger.setLogLevel).toBe('function'); // instance.setLogLevel should be a method. }); const LOG_LEVELS_IN_ORDER = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE']; /* Utility function to avoid repeating too much code */ -function testLogLevels(levelToTest: string) { +function testLogLevels(levelToTest: LogLevel) { // Builds the expected message. const buildExpectedMessage = (lvl: string, category: string, msg: string, showLevel?: boolean) => { let res = ''; @@ -57,27 +53,24 @@ function testLogLevels(levelToTest: string) { const consoleLogSpy = jest.spyOn(global.console, 'log'); // Runs the suite with the given value for showLevel option. - const runTests = (showLevel?: boolean, displayAllErrors?: boolean) => { + const runTests = (showLevel?: boolean, useCodes?: boolean) => { let logLevelLogsCounter = 0; let testForNoLog = false; const logMethod = levelToTest.toLowerCase(); - const logCategory = `test-category-${logMethod}${displayAllErrors ? 'displayAllErrors' : ''}`; - const instance = new Logger(logCategory, { - showLevel, displayAllErrors - }); + const logCategory = `test-category-${logMethod}`; + const instance = new Logger({ prefix: logCategory, showLevel }, + useCodes ? new _Map([[1, 'Test log for level %s with showLevel: %s %s']]) : undefined); LOG_LEVELS_IN_ORDER.forEach((logLevel, i) => { - const logMsg = `Test log for level ${levelToTest} (${displayAllErrors ? 'But all errors are configured to display' : 'Errors not forced to display'}) with showLevel: ${showLevel} ${logLevelLogsCounter}`; + const logMsg = `Test log for level ${levelToTest} with showLevel: ${showLevel} ${logLevelLogsCounter}`; const expectedMessage = buildExpectedMessage(levelToTest, logCategory, logMsg, showLevel); - // Log error should always be visible. - if (logMethod === LOG_LEVELS.ERROR.toLowerCase() && displayAllErrors) testForNoLog = false; - // Set the logLevel for this iteration. - setLogLevel(LogLevels[logLevel]); + instance.setLogLevel(LogLevels[logLevel]); // Call the method // @ts-ignore - instance[logMethod](logMsg); + if (useCodes) instance[logMethod](1, [levelToTest, showLevel, logLevelLogsCounter]); // @ts-ignore + else instance[logMethod](logMsg); // Assert if console.log was called. const actualMessage = consoleLogSpy.mock.calls[consoleLogSpy.mock.calls.length - 1][0]; if (testForNoLog) { @@ -95,9 +88,9 @@ function testLogLevels(levelToTest: string) { // Show logLevel runTests(true); - runTests(true, true); // Hide logLevel runTests(false); + // Hide logLevel and use message codes runTests(false, true); // Restore spied object. @@ -124,3 +117,17 @@ test('SPLIT LOGGER / Logger class public methods behaviour - instance.error', () testLogLevels(LogLevels.ERROR); }); + +test('SPLIT LOGGER / _sprintf', () => { + expect(_sprintf()).toBe(''); + expect(_sprintf(undefined, [/regex/, 'arg', 10, {}])).toBe(''); + + expect(_sprintf('text')).toBe('text'); + expect(_sprintf('text', [])).toBe('text'); + expect(_sprintf('text', [/regex/, 'arg', 10, {}])).toBe('text'); + + expect(_sprintf('text %s', [])).toBe('text undefined'); + expect(_sprintf('text %s', ['arg1'])).toBe('text arg1'); + expect(_sprintf('text %s', ['arg1', 'arg2'])).toBe('text arg1'); + expect(_sprintf('%s text %s', ['arg1', true, 'arg3'])).toBe('arg1 text true'); +}); diff --git a/src/logger/__tests__/sdkLogger.mock.ts b/src/logger/__tests__/sdkLogger.mock.ts index 0ba6aa71..a2cc184f 100644 --- a/src/logger/__tests__/sdkLogger.mock.ts +++ b/src/logger/__tests__/sdkLogger.mock.ts @@ -1,23 +1,21 @@ -/** - * This util mocks the logFactory at sdkLogger module, to spy on it - */ - -jest.mock('../sdkLogger'); - -import { logFactory } from '../sdkLogger'; +import { LogLevel } from '../../types'; export const loggerMock = { warn: jest.fn(), error: jest.fn(), debug: jest.fn(), info: jest.fn(), -}; + setLogLevel: jest.fn(), -(logFactory as jest.Mock).mockReturnValue(loggerMock); + mockClear() { + this.warn.mockClear(); + this.error.mockClear(); + this.debug.mockClear(); + this.info.mockClear(); + this.setLogLevel.mockClear(); + } +}; -export function mockClear() { - loggerMock.warn.mockClear(); - loggerMock.error.mockClear(); - loggerMock.debug.mockClear(); - loggerMock.info.mockClear(); +export function getLoggerLogLevel(logger: any): LogLevel | undefined { + if (logger) return logger.options.logLevel; } diff --git a/src/logger/__tests__/sdkLogger.spec.ts b/src/logger/__tests__/sdkLogger.spec.ts index d607409c..a69a587c 100644 --- a/src/logger/__tests__/sdkLogger.spec.ts +++ b/src/logger/__tests__/sdkLogger.spec.ts @@ -1,22 +1,29 @@ -import { logFactory, API } from '../sdkLogger'; -import { Logger } from '../index'; -import { LOG_LEVELS } from './index.spec'; +import { createLoggerAPI } from '../sdkLogger'; +import { Logger, LogLevels } from '../index'; +import { getLoggerLogLevel } from './sdkLogger.mock'; -test('SPLIT SDK LOGGER FACTORY / methods and props', () => { - - expect(typeof logFactory).toBe('function'); // Importing the module should return a function. +test('LoggerAPI / methods and props', () => { + // creates a LoggerAPI instance + const logger = new Logger(); + const API = createLoggerAPI(logger); expect(typeof API).toBe('object'); // Our logger should expose an API object. - expect(typeof API.enable).toBe('function'); // API object should have enable method. - expect(typeof API.disable).toBe('function'); // API object should have disable method. + expect(typeof API.setLogLevel).toBe('function'); // API object should have setLogLevel method. - expect(API.LogLevel).toEqual(LOG_LEVELS); // API object should have LogLevel prop including all available levels. + API.setLogLevel('INFO'); + expect(getLoggerLogLevel(logger)).toBe('INFO'); // calling setLogLevel should update the log level. + // @ts-ignore, passing wrong type + API.setLogLevel('warn'); + expect(getLoggerLogLevel(logger)).toBe('INFO'); // calling setLogLevel with an invalid value should not update the log level. -}); + expect(typeof API.enable).toBe('function'); // API object should have enable method. + API.enable(); + expect(getLoggerLogLevel(logger)).toBe('DEBUG'); // calling enable should update logger log level to DEBUG. -test('SPLIT SDK LOGGER FACTORY / create factory returned instance', () => { - const logger = logFactory('category', {}); + expect(typeof API.disable).toBe('function'); // API object should have disable method. + API.disable(); + expect(getLoggerLogLevel(logger)).toBe('NONE'); // calling disable should update logger log level to NONE. - expect(logger instanceof Logger).toBe(true); // Our logger should expose an API object. + expect(API.LogLevel).toEqual(LogLevels); // API object should have LogLevel prop including all available levels. }); diff --git a/src/logger/browser/DebugLogger.ts b/src/logger/browser/DebugLogger.ts new file mode 100644 index 00000000..105e1890 --- /dev/null +++ b/src/logger/browser/DebugLogger.ts @@ -0,0 +1,7 @@ +import { Logger } from '../index'; +import { codesDebug } from '../messages/debug'; +import { _Map } from '../../utils/lang/maps'; + +export function DebugLogger() { + return new Logger({ logLevel: 'DEBUG' }, new _Map(codesDebug)); +} diff --git a/src/logger/browser/ErrorLogger.ts b/src/logger/browser/ErrorLogger.ts new file mode 100644 index 00000000..f0702d89 --- /dev/null +++ b/src/logger/browser/ErrorLogger.ts @@ -0,0 +1,7 @@ +import { Logger } from '../index'; +import { codesError } from '../messages/error'; +import { _Map } from '../../utils/lang/maps'; + +export function ErrorLogger() { + return new Logger({ logLevel: 'ERROR' }, new _Map(codesError)); +} diff --git a/src/logger/browser/InfoLogger.ts b/src/logger/browser/InfoLogger.ts new file mode 100644 index 00000000..bdf9be75 --- /dev/null +++ b/src/logger/browser/InfoLogger.ts @@ -0,0 +1,7 @@ +import { Logger } from '../index'; +import { codesInfo } from '../messages/info'; +import { _Map } from '../../utils/lang/maps'; + +export function InfoLogger() { + return new Logger({ logLevel: 'INFO' }, new _Map(codesInfo)); +} diff --git a/src/logger/browser/WarnLogger.ts b/src/logger/browser/WarnLogger.ts new file mode 100644 index 00000000..8456d012 --- /dev/null +++ b/src/logger/browser/WarnLogger.ts @@ -0,0 +1,7 @@ +import { Logger } from '../index'; +import { codesWarn } from '../messages/warn'; +import { _Map } from '../../utils/lang/maps'; + +export function WarnLogger() { + return new Logger({ logLevel: 'WARN' }, new _Map(codesWarn)); +} diff --git a/src/logger/browser/__tests__/index.spec.ts b/src/logger/browser/__tests__/index.spec.ts new file mode 100644 index 00000000..178c6a59 --- /dev/null +++ b/src/logger/browser/__tests__/index.spec.ts @@ -0,0 +1,21 @@ +import { getLoggerLogLevel } from '../../__tests__/sdkLogger.mock'; +import { DebugLogger } from '../DebugLogger'; +import { InfoLogger } from '../InfoLogger'; +import { WarnLogger } from '../WarnLogger'; +import { ErrorLogger } from '../ErrorLogger'; + +test('DebugLogger', () => { + expect(getLoggerLogLevel(DebugLogger())).toBe('DEBUG'); +}); + +test('InfoLogger', () => { + expect(getLoggerLogLevel(InfoLogger())).toBe('INFO'); +}); + +test('WarnLogger', () => { + expect(getLoggerLogLevel(WarnLogger())).toBe('WARN'); +}); + +test('ErrorLogger', () => { + expect(getLoggerLogLevel(ErrorLogger())).toBe('ERROR'); +}); diff --git a/src/logger/constants.ts b/src/logger/constants.ts new file mode 100644 index 00000000..46cac217 --- /dev/null +++ b/src/logger/constants.ts @@ -0,0 +1,139 @@ +/** + * Message codes used to trim string log messages from commons and client-side API modules, + * in order to reduce the minimal SDK size for Browser and eventually other client-side environments. + * + * Modules related to the server-side API (e.g., segmentsSyncTask), platform-specific components (e.g., signal listeners) + * and pluggable components (e.g., pluggable integrations & storages) can use the logger with string literals. + */ +export const ENGINE_COMBINER_AND = 0; +export const ENGINE_COMBINER_IFELSEIF = 1; +export const ENGINE_COMBINER_IFELSEIF_NO_TREATMENT = 2; +export const ENGINE_BUCKET = 3; +export const ENGINE_MATCHER_ALL = 4; +export const ENGINE_MATCHER_BETWEEN = 5; +export const ENGINE_MATCHER_BOOLEAN = 6; +export const ENGINE_MATCHER_CONTAINS_ALL = 7; +export const ENGINE_MATCHER_CONTAINS_ANY = 8; +export const ENGINE_MATCHER_CONTAINS_STRING = 9; +export const ENGINE_MATCHER_DEPENDENCY = 10; +export const ENGINE_MATCHER_DEPENDENCY_PRE = 11; +export const ENGINE_MATCHER_EQUAL = 12; +export const ENGINE_MATCHER_EQUAL_TO_SET = 13; +export const ENGINE_MATCHER_ENDS_WITH = 14; +export const ENGINE_MATCHER_GREATER = 15; +export const ENGINE_MATCHER_LESS = 16; +export const ENGINE_MATCHER_PART_OF = 17; +export const ENGINE_MATCHER_SEGMENT = 18; +export const ENGINE_MATCHER_STRING = 19; +export const ENGINE_MATCHER_STRING_INVALID = 20; +export const ENGINE_MATCHER_STARTS_WITH = 21; +export const ENGINE_MATCHER_WHITELIST = 22; +export const ENGINE_VALUE = 23; +export const ENGINE_SANITIZE = 24; +export const CLEANUP_REGISTERING = 25; +export const CLEANUP_DEREGISTERING = 26; +export const RETRIEVE_CLIENT_DEFAULT = 27; +export const RETRIEVE_CLIENT_EXISTING = 28; +export const RETRIEVE_MANAGER = 29; +export const SYNC_OFFLINE_DATA = 30; +export const SYNC_SPLITS_FETCH = 31; +export const SYNC_SPLITS_NEW = 32; +export const SYNC_SPLITS_REMOVED = 33; +export const SYNC_SPLITS_SEGMENTS = 34; +export const STREAMING_NEW_MESSAGE = 35; +export const SYNC_TASK_START = 36; +export const SYNC_TASK_EXECUTE = 37; +export const SYNC_TASK_STOP = 38; +export const SETTINGS_SPLITS_FILTER = 39; + +export const CLIENT_READY_FROM_CACHE = 100; +export const CLIENT_READY = 101; +export const IMPRESSION = 102; +export const IMPRESSION_QUEUEING = 103; +export const NEW_SHARED_CLIENT = 104; +export const NEW_FACTORY = 105; +export const POLLING_SMART_PAUSING = 106; +export const POLLING_START = 107; +export const POLLING_STOP = 108; +export const SYNC_SPLITS_FETCH_RETRY = 109; +export const STREAMING_REFRESH_TOKEN = 110; +export const STREAMING_RECONNECT = 111; +export const STREAMING_CONNECTING = 112; +export const STREAMING_DISABLED = 113; +export const STREAMING_DISCONNECTING = 114; +export const SUBMITTERS_PUSH_FULL_EVENTS_QUEUE = 115; +export const SUBMITTERS_PUSH = 116; +export const SYNC_START_POLLING = 117; +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 ENGINE_VALUE_INVALID = 200; +export const ENGINE_VALUE_NO_ATTRIBUTES = 201; +export const CLIENT_NO_LISTENER = 202; +export const CLIENT_NOT_READY = 203; +export const SYNC_MYSEGMENTS_FETCH_RETRY = 204; +export const SYNC_SPLITS_FETCH_FAILS = 205; +export const STREAMING_PARSING_ERROR_FAILS = 206; +export const STREAMING_PARSING_MESSAGE_FAILS = 207; +export const STREAMING_FALLBACK = 208; +export const SUBMITTERS_PUSH_FAILS = 209; +export const SUBMITTERS_PUSH_RETRY = 210; +export const WARN_SETTING_NULL = 211; +export const WARN_TRIMMING_PROPERTIES = 212; +export const WARN_CONVERTING = 213; +export const WARN_TRIMMING = 214; +export const WARN_NOT_EXISTENT_SPLIT = 215; +export const WARN_LOWERCASE_TRAFFIC_TYPE = 216; +export const WARN_NOT_EXISTENT_TT = 217; +export const WARN_INTEGRATION_INVALID = 218; +export const WARN_SPLITS_FILTER_IGNORED = 219; +export const WARN_SPLITS_FILTER_INVALID = 220; +export const WARN_SPLITS_FILTER_EMPTY = 221; +export const WARN_STORAGE_INVALID = 222; +export const WARN_API_KEY = 223; + +export const ERROR_ENGINE_COMBINER_IFELSEIF = 300; +export const ERROR_LOGLEVEL_INVALID = 301; +export const ERROR_CLIENT_LISTENER = 302; +export const ERROR_CLIENT_CANNOT_GET_READY = 303; +export const ERROR_SYNC_OFFLINE_LOADING = 304; +export const ERROR_STREAMING_SSE = 305; +export const ERROR_STREAMING_AUTH = 306; +export const ERROR_IMPRESSIONS_TRACKER = 307; +export const ERROR_IMPRESSIONS_LISTENER = 308; +export const ERROR_EVENTS_TRACKER = 309; +export const ERROR_EVENT_TYPE_FORMAT = 310; +export const ERROR_NOT_PLAIN_OBJECT = 311; +export const ERROR_SIZE_EXCEEDED = 312; +export const ERROR_NOT_FINITE = 313; +export const ERROR_CLIENT_DESTROYED = 314; +export const ERROR_NULL = 315; +export const ERROR_TOO_LONG = 316; +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_HTTP = 322; + +// Log prefixes (a.k.a. tags or categories) +export const logPrefixSettings = 'settings'; +export const logPrefixInstantiation = 'Factory instantiation'; +export const logPrefixEngine = 'engine'; +export const logPrefixEngineCombiner = logPrefixEngine + ':combiner: '; +export const logPrefixEngineMatcher = logPrefixEngine + ':matcher: '; +export const logPrefixEngineValue = logPrefixEngine + ':value: '; +export const logPrefixSync = 'sync'; +export const logPrefixSyncManager = logPrefixSync + ':sync-manager: '; +export const logPrefixSyncOffline = logPrefixSync + ':offline: '; +export const logPrefixSyncStreaming = logPrefixSync + ':streaming: '; +export const logPrefixSyncSplits = logPrefixSync + ':split-changes: '; +export const logPrefixSyncSegments = logPrefixSync + ':segment-changes: '; +export const logPrefixSyncMysegments = logPrefixSync + ':my-segments: '; +export const logPrefixSyncPolling = logPrefixSync + ':polling-manager: '; +export const logPrefixSyncSubmitters = logPrefixSync + ':submitter: '; +export const logPrefixImpressionsTracker = 'impressions-tracker: '; +export const logPrefixEventsTracker = 'events-tracker: '; +export const logPrefixCleanup = 'cleanup: '; diff --git a/src/logger/index.ts b/src/logger/index.ts index e80cffe0..4738e191 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,7 +1,8 @@ import objectAssign from 'object-assign'; -import { ILoggerOptions } from './types'; +import { ILoggerOptions, ILogger } from './types'; import { find } from '../utils/lang'; import { LogLevel } from '../types'; +import { IMap, _Map } from '../utils/lang/maps'; export const LogLevels: { [level: string]: LogLevel } = { DEBUG: 'DEBUG', @@ -11,58 +12,79 @@ export const LogLevels: { [level: string]: LogLevel } = { NONE: 'NONE' }; -// DEBUG is the default. The log level is not specific to an SDK instance. -let globalLogLevel = LogLevels.DEBUG; - -export function setLogLevel(level: LogLevel) { - globalLogLevel = level; -} +const LogLevelRanks = { + DEBUG: 1, + INFO: 2, + WARN: 3, + ERROR: 4, + NONE: 5 +}; export function isLogLevelString(str: string): str is LogLevel { return !!find(LogLevels, (lvl: string) => str === lvl); } +// exported for testing purposes only +export function _sprintf(format: string = '', args: any[] = []): string { + let i = 0; + return format.replace(/%s/g, function () { + return args[i++]; + }); +} + const defaultOptions = { + prefix: 'splitio', + logLevel: LogLevels.NONE, showLevel: true, - displayAllErrors: false }; -export class Logger { - private category: any; - private options: any; +export class Logger implements ILogger { + + private options: Required; + private codes: IMap; + private logLevel: number; - constructor(category: string, options: ILoggerOptions) { - this.category = category; + constructor(options?: ILoggerOptions, codes?: IMap) { this.options = objectAssign({}, defaultOptions, options); + this.codes = codes || new _Map(); + this.logLevel = LogLevelRanks[this.options.logLevel]; } - debug(msg: string) { - if (this._shouldLog(LogLevels.DEBUG)) - this._log(LogLevels.DEBUG, msg); + setLogLevel(logLevel: LogLevel) { + this.options.logLevel = logLevel; + this.logLevel = LogLevelRanks[logLevel]; } - info(msg: string) { - if (this._shouldLog(LogLevels.INFO)) - this._log(LogLevels.INFO, msg); + debug(msg: string | number, args?: any[]) { + if (this._shouldLog(LogLevelRanks.DEBUG)) this._log(LogLevels.DEBUG, msg, args); } - warn(msg: string) { - if (this._shouldLog(LogLevels.WARN)) - this._log(LogLevels.WARN, msg); + info(msg: string | number, args?: any[]) { + if (this._shouldLog(LogLevelRanks.INFO)) this._log(LogLevels.INFO, msg, args); } - error(msg: string) { - if (this.options.displayAllErrors || this._shouldLog(LogLevels.ERROR)) - this._log(LogLevels.ERROR, msg); + warn(msg: string | number, args?: any[]) { + if (this._shouldLog(LogLevelRanks.WARN)) this._log(LogLevels.WARN, msg, args); } - _log(level: string, text: string) { - const formattedText = this._generateLogMessage(level, text); + error(msg: string | number, args?: any[]) { + if (this._shouldLog(LogLevelRanks.ERROR)) this._log(LogLevels.ERROR, msg, args); + } + + private _log(level: LogLevel, msg: string | number, args?: any[]) { + if (typeof msg === 'number') { + const format = this.codes.get(msg); + msg = format ? _sprintf(format, args) : `Message code ${msg}${args ? ', with args: ' + args.toString() : ''}`; + } else { + if (args) msg = _sprintf(msg, args); + } + + const formattedText = this._generateLogMessage(level, msg); console.log(formattedText); } - _generateLogMessage(level: string, text: string) { + private _generateLogMessage(level: LogLevel, text: string) { const textPre = ' => '; let result = ''; @@ -70,19 +92,14 @@ export class Logger { result += '[' + level + ']' + (level === LogLevels.INFO || level === LogLevels.WARN ? ' ' : '') + ' '; } - if (this.category) { - result += this.category + textPre; + if (this.options.prefix) { + result += this.options.prefix + textPre; } return result += text; } - _shouldLog(level: LogLevel) { - const logLevel = globalLogLevel; - const levels = Object.keys(LogLevels).map((f) => LogLevels[f as keyof typeof LogLevels]); - const index = levels.indexOf(level); // What's the index of what it's trying to check if it should log - const levelIdx = levels.indexOf(logLevel); // What's the current log level index. - - return index >= levelIdx; + private _shouldLog(level: number) { + return level >= this.logLevel; } } diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts new file mode 100644 index 00000000..4f1444bc --- /dev/null +++ b/src/logger/messages/debug.ts @@ -0,0 +1,49 @@ +import * as c from '../constants'; +import { codesInfo } from './info'; + +export const codesDebug: [number, string][] = codesInfo.concat([ + // evaluator + [c.ENGINE_COMBINER_AND, c.logPrefixEngineCombiner + '[andCombiner] evaluates to %s'], + [c.ENGINE_COMBINER_IFELSEIF, c.logPrefixEngineCombiner + 'Treatment found: %s'], + [c.ENGINE_COMBINER_IFELSEIF_NO_TREATMENT, c.logPrefixEngineCombiner + 'All predicates evaluated, no treatment found.'], + [c.ENGINE_BUCKET, c.logPrefixEngine + ': using algo "murmur" bucket %s for key %s using seed %s - treatment %s'], + [c.ENGINE_MATCHER_ALL, c.logPrefixEngineMatcher + '[allMatcher] is always true'], + [c.ENGINE_MATCHER_BETWEEN, c.logPrefixEngineMatcher + '[betweenMatcher] is %s between %s and %s? %s'], + [c.ENGINE_MATCHER_BOOLEAN, c.logPrefixEngineMatcher + '[booleanMatcher] %s === %s'], + [c.ENGINE_MATCHER_CONTAINS_ALL, c.logPrefixEngineMatcher + '[containsAllMatcher] %s contains all elements of %s? %s'], + [c.ENGINE_MATCHER_CONTAINS_ANY, c.logPrefixEngineMatcher + '[containsAnyMatcher] %s contains at least an element of %s? %s'], + [c.ENGINE_MATCHER_CONTAINS_STRING, c.logPrefixEngineMatcher + '[containsStringMatcher] %s contains %s? %s'], + [c.ENGINE_MATCHER_DEPENDENCY, c.logPrefixEngineMatcher + '[dependencyMatcher] parent split "%s" evaluated to "%s" with label "%s". %s evaluated treatment is part of [%s] ? %s.'], + [c.ENGINE_MATCHER_DEPENDENCY_PRE, c.logPrefixEngineMatcher + '[dependencyMatcher] will evaluate parent split: "%s" with key: %s %s'], + [c.ENGINE_MATCHER_EQUAL, c.logPrefixEngineMatcher + '[equalToMatcher] is %s equal to %s? %s'], + [c.ENGINE_MATCHER_EQUAL_TO_SET, c.logPrefixEngineMatcher + '[equalToSetMatcher] is %s equal to set %s? %s'], + [c.ENGINE_MATCHER_ENDS_WITH, c.logPrefixEngineMatcher + '[endsWithMatcher] %s ends with %s? %s'], + [c.ENGINE_MATCHER_GREATER, c.logPrefixEngineMatcher + '[greaterThanEqualMatcher] is %s greater than %s? %s'], + [c.ENGINE_MATCHER_LESS, c.logPrefixEngineMatcher + '[lessThanEqualMatcher] is %s less than %s? %s'], + [c.ENGINE_MATCHER_PART_OF, c.logPrefixEngineMatcher + '[partOfMatcher] %s is part of %s? %s'], + [c.ENGINE_MATCHER_SEGMENT, c.logPrefixEngineMatcher + '[segmentMatcher] evaluated %s / %s => %s'], + [c.ENGINE_MATCHER_STRING, c.logPrefixEngineMatcher + '[stringMatcher] does %s matches with %s? %s'], + [c.ENGINE_MATCHER_STRING_INVALID, c.logPrefixEngineMatcher + '[stringMatcher] %s is an invalid regex'], + [c.ENGINE_MATCHER_STARTS_WITH, c.logPrefixEngineMatcher + '[startsWithMatcher] %s starts with %s? %s'], + [c.ENGINE_MATCHER_WHITELIST, c.logPrefixEngineMatcher + '[whitelistMatcher] evaluated %s in [%s] => %s'], + [c.ENGINE_VALUE, c.logPrefixEngineValue + 'Extracted attribute [%s], [%s] will be used for matching.'], + [c.ENGINE_SANITIZE, c.logPrefixEngine + ':sanitize: Attempted to sanitize [%s] which should be of type [%s]. Sanitized and processed value => [%s]'], + // SDK + [c.CLEANUP_REGISTERING, c.logPrefixCleanup + 'Registering cleanup handler %s'], + [c.CLEANUP_DEREGISTERING, c.logPrefixCleanup + '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.'], + // synchronizer + [c.SYNC_OFFLINE_DATA, c.logPrefixSyncOffline + 'Splits data: \n%s'], + [c.SYNC_SPLITS_FETCH, c.logPrefixSyncSplits + 'Spin up split update using since = %s'], + [c.SYNC_SPLITS_NEW, c.logPrefixSyncSplits + 'New splits %s'], + [c.SYNC_SPLITS_REMOVED, c.logPrefixSyncSplits + 'Removed splits %s'], + [c.SYNC_SPLITS_SEGMENTS, c.logPrefixSyncSplits + 'Segment names collected %s'], + [c.STREAMING_NEW_MESSAGE, c.logPrefixSyncStreaming + 'New SSE message received, with data: %s.'], + [c.SYNC_TASK_START, c.logPrefixSync + ': Starting %s. Running each %s millis'], + [c.SYNC_TASK_EXECUTE, c.logPrefixSync + ': Running %s'], + [c.SYNC_TASK_STOP, c.logPrefixSync + ': Stopping %s'], + // initialization / settings validation + [c.SETTINGS_SPLITS_FILTER, c.logPrefixSettings + ': splits filtering criteria is "%s".'] +]); diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts new file mode 100644 index 00000000..0eea8c02 --- /dev/null +++ b/src/logger/messages/error.ts @@ -0,0 +1,33 @@ +import * as c from '../constants'; + +export const codesError: [number, string][] = [ + // evaluator + [c.ERROR_ENGINE_COMBINER_IFELSEIF, c.logPrefixEngineCombiner + '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_IMPRESSIONS_TRACKER, c.logPrefixImpressionsTracker + 'Could not store impressions bulk with %s impression(s). Error: %s'], + [c.ERROR_IMPRESSIONS_LISTENER, c.logPrefixImpressionsTracker + 'Impression listener logImpression method threw: %s.'], + [c.ERROR_EVENTS_TRACKER, c.logPrefixEventsTracker + 'Failed to queue %s'], + // synchronizer + [c.ERROR_SYNC_OFFLINE_LOADING, c.logPrefixSyncOffline + 'There was an issue loading the mock Splits data, no changes will be applied to the current cache. %s'], + [c.ERROR_STREAMING_SSE, c.logPrefixSyncStreaming + 'Fail to connect to streaming, with error message: %s'], + [c.ERROR_STREAMING_AUTH, c.logPrefixSyncStreaming + 'Failed to authenticate for streaming. Error: "%s".'], + [c.ERROR_HTTP, ' Response status is not OK. Status: %s. URL: %s. Message: %s'], + // client status + [c.ERROR_CLIENT_LISTENER, 'A listener was added for %s on the SDK, which has already fired and won\'t be emitted again. The callback won\'t be executed.'], + [c.ERROR_CLIENT_DESTROYED, '%s: Client has already been destroyed - no calls possible.'], + // 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_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.'], + [c.ERROR_INVALID_KEY_OBJECT, '%s: Key must be an object with bucketingKey and matchingKey with valid string properties.'], + [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.'], + // initialization / settings validation + [c.ERROR_INVALID_IMPRESSIONS_MODE, c.logPrefixSettings + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'], +]; diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts new file mode 100644 index 00000000..2877a9d8 --- /dev/null +++ b/src/logger/messages/info.ts @@ -0,0 +1,33 @@ +import * as c from '../constants'; +import { codesWarn } from './warn'; + +const READY_MSG = 'Split SDK is ready'; + +export const codesInfo: [number, string][] = codesWarn.concat([ + // client status + [c.CLIENT_READY_FROM_CACHE, READY_MSG + ' from cache'], + [c.CLIENT_READY, READY_MSG], + // SDK + [c.IMPRESSION, c.logPrefixImpressionsTracker +'Split: %s. Key: %s. Evaluation: %s. Label: %s'], + [c.IMPRESSION_QUEUEING, c.logPrefixImpressionsTracker +'Queueing corresponding impression.'], + [c.NEW_SHARED_CLIENT, ' New shared client instance created.'], + [c.NEW_FACTORY, ' New Split SDK instance created.'], + [c.EVENTS_TRACKER_SUCCESS, c.logPrefixEventsTracker + 'Successfully qeued %s'], + [c.IMPRESSIONS_TRACKER_SUCCESS, c.logPrefixImpressionsTracker + 'Successfully stored %s impression(s).'], + + // synchronizer + [c.POLLING_SMART_PAUSING, c.logPrefixSyncPolling + 'Turning segments data polling %s.'], + [c.POLLING_START, c.logPrefixSyncPolling + 'Starting polling'], + [c.POLLING_STOP, c.logPrefixSyncPolling + 'Stopping polling'], + [c.SYNC_SPLITS_FETCH_RETRY, c.logPrefixSyncSplits + 'Retrying download of splits #%s. Reason: %s'], + [c.SUBMITTERS_PUSH_FULL_EVENTS_QUEUE, c.logPrefixSyncSubmitters + 'Flushing full events queue and reseting timer.'], + [c.SUBMITTERS_PUSH, c.logPrefixSyncSubmitters + 'Pushing %s %s.'], + [c.STREAMING_REFRESH_TOKEN, c.logPrefixSyncStreaming + 'Refreshing streaming token in %s seconds.'], + [c.STREAMING_RECONNECT, c.logPrefixSyncStreaming + 'Attempting to reconnect in %s seconds.'], + [c.STREAMING_CONNECTING, c.logPrefixSyncStreaming + 'Connecting to streaming.'], + [c.STREAMING_DISABLED, c.logPrefixSyncStreaming + 'Streaming is disabled for given Api key. Switching to polling mode.'], + [c.STREAMING_DISCONNECTING, c.logPrefixSyncStreaming + 'Disconnecting from streaming.'], + [c.SYNC_START_POLLING, c.logPrefixSyncManager + 'Streaming not available. Starting polling.'], + [c.SYNC_CONTINUE_POLLING, c.logPrefixSyncManager + 'Streaming couldn\'t connect. Continue polling.'], + [c.SYNC_STOP_POLLING, c.logPrefixSyncManager + 'Streaming (re)connected. Syncing and stopping polling.'], +]); diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts new file mode 100644 index 00000000..e10da2f5 --- /dev/null +++ b/src/logger/messages/warn.ts @@ -0,0 +1,34 @@ +import * as c from '../constants'; +import { codesError } from './error'; + +export const codesWarn: [number, string][] = codesError.concat([ + // evaluator + [c.ENGINE_VALUE_INVALID, c.logPrefixEngineValue + 'Value %s doesn\'t match with expected type.'], + [c.ENGINE_VALUE_NO_ATTRIBUTES, c.logPrefixEngineValue + 'Defined attribute [%s], no attributes received.'], + // synchronizer + [c.SYNC_MYSEGMENTS_FETCH_RETRY, c.logPrefixSyncMysegments + 'Retrying download of segments #%s. Reason: %s'], + [c.SYNC_SPLITS_FETCH_FAILS, c.logPrefixSyncSplits + 'Error while doing fetch of Splits. %s'], + [c.STREAMING_PARSING_ERROR_FAILS, c.logPrefixSyncStreaming + 'Error parsing SSE error notification: %s'], + [c.STREAMING_PARSING_MESSAGE_FAILS, c.logPrefixSyncStreaming + 'Error parsing SSE message notification: %s'], + [c.STREAMING_FALLBACK, c.logPrefixSyncStreaming + 'Falling back to polling mode. Reason: %s'], + [c.SUBMITTERS_PUSH_FAILS, c.logPrefixSyncSubmitters + 'Droping %s %s after retry. Reason: %s.'], + [c.SUBMITTERS_PUSH_RETRY, c.logPrefixSyncSubmitters + 'Failed to push %s %s, keeping data to retry on next iteration. Reason: %s.'], + // client status + [c.CLIENT_NOT_READY, '%s: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method.'], + [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_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: split "%s" does not exist in this environment, please double check what splits exist in the web console.'], + [c.WARN_LOWERCASE_TRAFFIC_TYPE, '%s: traffic_type_name should be all lowercase - converting string to lowercase.'], + [c.WARN_NOT_EXISTENT_TT, '%s: traffic type "%s" does not have any corresponding split in this environment, make sure you\'re tracking your events to a valid traffic type defined in the web console.'], + // initialization / settings validation + [c.WARN_INTEGRATION_INVALID, c.logPrefixSettings+': %s integration %s at settings %s invalid. %s'], + [c.WARN_SPLITS_FILTER_IGNORED, c.logPrefixSettings+': split filters have been configured but will have no effect if mode is not "%s", since synchronization is being deferred to an external tool.'], + [c.WARN_SPLITS_FILTER_INVALID, c.logPrefixSettings+': split filter at position %s is invalid. It must be an object with a valid filter type ("byName" or "byPrefix") and a list of "values".'], + [c.WARN_SPLITS_FILTER_EMPTY, c.logPrefixSettings+': splitFilters configuration must be a non-empty array of filter objects.'], + [c.WARN_STORAGE_INVALID, c.logPrefixSettings+': The provided storage is invalid. Fallbacking into default MEMORY storage'], + [c.WARN_API_KEY, c.logPrefixSettings+': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'] +]); diff --git a/src/logger/sdkLogger.ts b/src/logger/sdkLogger.ts index a7dbe9a8..41c3635a 100644 --- a/src/logger/sdkLogger.ts +++ b/src/logger/sdkLogger.ts @@ -1,79 +1,46 @@ -/** - * This file defines the logger interface and default options for the SDKs, not necessarily for the Logger as it's own. - */ -import { ILoggerOptions } from './types'; -import { Logger, LogLevels, setLogLevel, isLogLevelString } from '.'; -import { isLocalStorageAvailable } from '../utils/env/isLocalStorageAvailable'; -import { isNode } from '../utils/env/isNode'; -import { merge } from '../utils/lang'; +import { LogLevels, isLogLevelString } from './index'; import { ILoggerAPI } from '../types'; - -// @TODO when integrating with other packages, find the best way to update LoggerOption defaults per package (node, evaluator, etc.) -const defaultOptions: ILoggerOptions = { - showLevel: true, - displayAllErrors: false -}; - -// @TODO when integrating with other packages, find the best way to handle initial state per package/environment -const LS_KEY = 'splitio_debug'; -const ENV_VAR_KEY = 'SPLITIO_DEBUG'; +import { ILogger } from './types'; +import { ERROR_LOGLEVEL_INVALID } from './constants'; /** - * Logger initial debug level, that is disabled ('NONE') by default. - * Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'. - * Other acceptable values are 'on', 'enable' and 'enabled', which are equivalent to 'DEBUG'. - * Any other string value is equivalent to disable. + * The public Logger utility API exposed via SplitFactory, used to update the log level. + * + * @param log the factory logger instance to handle */ -const initialState = String( - // eslint-disable-next-line no-undef - isNode ? process.env[ENV_VAR_KEY] : isLocalStorageAvailable() ? localStorage.getItem(LS_KEY) : '' -); - -// we expose the logger instance creator -export const logFactory = (namespace: string, options = {}) => new Logger(namespace, merge(options, defaultOptions)); +export function createLoggerAPI(log: ILogger): ILoggerAPI { -const ownLog = logFactory('splitio-utils:logger'); - -/** - * The public Logger utility API exposed via SplitFactory. - */ -export const API: ILoggerAPI = { - /** - * Enables all the logs. - */ - enable() { - setLogLevel(LogLevels.DEBUG); - }, - /** - * Sets a custom log Level for the SDK. - * @param {string} logLevel - Custom LogLevel value. - */ - setLogLevel(logLevel: string) { + function setLogLevel(logLevel: string) { if (isLogLevelString(logLevel)) { - setLogLevel(logLevel); + log.setLogLevel(logLevel); } else { - ownLog.error('Invalid Log Level - No changes to the logs will be applied.'); + log.error(ERROR_LOGLEVEL_INVALID); } - }, - /** - * Disables all the log levels. - */ - disable() { - // Disabling is equal logLevel none - setLogLevel(LogLevels.NONE); - }, - /** - * Exposed for usage with setLogLevel - */ - LogLevel: LogLevels -}; + } + + return { + /** + * Enables all the logs. + */ + enable() { + setLogLevel(LogLevels.DEBUG); + }, + /** + * Sets a custom log Level for the SDK. + * @param {string} logLevel - Custom LogLevel value. + */ + setLogLevel, + /** + * Disables all the log levels. + */ + disable() { + // Disabling is equal logLevel none + setLogLevel(LogLevels.NONE); + }, + /** + * Exposed for usage with setLogLevel + */ + LogLevel: LogLevels + }; -// Kept to avoid a breaking change ('on', 'enable' and 'enabled' are equivalent) -if (/^(enabled?|on)/i.test(initialState)) { - API.enable(); -} else if (isLogLevelString(initialState)) { - API.setLogLevel(initialState); -} else { - // By default it starts disabled. - API.disable(); } diff --git a/src/logger/types.ts b/src/logger/types.ts index 3ef2b261..05043be4 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -1,4 +1,19 @@ +import { LogLevel } from '../types'; + export interface ILoggerOptions { - showLevel?: boolean, - displayAllErrors?: boolean + prefix?: string, + logLevel?: LogLevel, + showLevel?: boolean, // @TODO remove this param eventually since it is not being set `false` anymore +} + +export interface ILogger { + setLogLevel(logLevel: LogLevel): void + + debug(msg: string | number, args?: any[]): void + + info(msg: string | number, args?: any[]): void + + warn(msg: string | number, args?: any[]): void + + error(msg: string | number, args?: any[]): void } diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index 139fde4b..3cc3c765 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -1,9 +1,10 @@ // @ts-nocheck -import { loggerMock, mockClear } from '../../logger/__tests__/sdkLogger.mock'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { IEventEmitter } from '../../types'; import { SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT, SDK_UPDATE } from '../constants'; import sdkReadinessManagerFactory from '../sdkReadinessManager'; import { IReadinessManager } from '../types'; +import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO_LISTENER } from '../../logger/constants'; const EventEmitterMock = jest.fn(() => ({ on: jest.fn(), @@ -34,14 +35,12 @@ function emitTimeoutEvent(readinessManager: IReadinessManager) { describe('SDK Readiness Manager - Event emitter', () => { - afterEach(() => { - mockClear(); - }); + afterEach(() => { loggerMock.mockClear(); }); test('Providing the gate object to get the SDK status interface that manages events', () => { expect(typeof sdkReadinessManagerFactory).toBe('function'); // The module exposes a function. - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); expect(typeof sdkReadinessManager).toBe('object'); // The function result contains the readiness manager and a sdkStatus object. const gateMock = sdkReadinessManager.readinessManager.gate; const sdkStatus = sdkReadinessManager.sdkStatus; @@ -58,7 +57,7 @@ describe('SDK Readiness Manager - Event emitter', () => { expect(sdkStatus.Event.SDK_READY_TIMED_OUT).toBe(SDK_READY_TIMED_OUT); // which contains the constants for the events, for backwards compatibility. expect(sdkStatus.Event.SDK_UPDATE).toBe(SDK_UPDATE); // which contains the constants for the events, for backwards compatibility. - expect(gateMock.once.mock.calls.length).toBe(3); // It should make three one time only subscriptions + expect(gateMock.once).toBeCalledTimes(3); // It should make three one time only subscriptions const sdkReadyResolvePromiseCall = gateMock.once.mock.calls[0]; const sdkReadyRejectPromiseCall = gateMock.once.mock.calls[1]; @@ -67,7 +66,7 @@ describe('SDK Readiness Manager - Event emitter', () => { expect(sdkReadyRejectPromiseCall[0]).toBe(SDK_READY_TIMED_OUT); // A one time only subscription is also on the SDK_READY_TIMED_OUT event, for rejecting the full blown ready promise. expect(sdkReadyFromCacheListenersCheckCall[0]).toBe(SDK_READY_FROM_CACHE); // A one time only subscription is on the SDK_READY_FROM_CACHE event, to log the event and update internal state. - expect(gateMock.on.mock.calls.length).toBe(2); // It should also add two persistent listeners + expect(gateMock.on).toBeCalledTimes(2); // It should also add two persistent listeners const removeListenerSubCall = gateMock.on.mock.calls[0]; const addListenerSubCall = gateMock.on.mock.calls[1]; @@ -77,61 +76,61 @@ describe('SDK Readiness Manager - Event emitter', () => { }); test('The event callbacks should work as expected - SDK_READY_FROM_CACHE', () => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); const gateMock = sdkReadinessManager.readinessManager.gate; const readyFromCacheEventCB = gateMock.once.mock.calls[2][1]; readyFromCacheEventCB(); - expect(loggerMock.info.mock.calls.length).toBe(1); // If the SDK_READY_FROM_CACHE event fires, we get a info message. - expect(loggerMock.info.mock.calls[0]).toEqual(['Split SDK is ready from cache.']); // Telling us the SDK is ready to be used with data from cache. + expect(loggerMock.info).toBeCalledTimes(1); // If the SDK_READY_FROM_CACHE event fires, we get a info message. + expect(loggerMock.info).toBeCalledWith(CLIENT_READY_FROM_CACHE); // Telling us the SDK is ready to be used with data from cache. }); test('The event callbacks should work as expected - SDK_READY emits with no callbacks', () => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); // Get the callbacks const addListenerCB = sdkReadinessManager.readinessManager.gate.on.mock.calls[1][1]; emitReadyEvent(sdkReadinessManager.readinessManager); - expect(loggerMock.warn.mock.calls.length).toBe(1); // If the SDK_READY event fires and we have no callbacks for it (neither event nor ready promise) we get a warning. - expect(loggerMock.warn.mock.calls[0]).toEqual(['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.']); // Telling us there were no listeners and evaluations before this point may have been incorrect. + expect(loggerMock.warn).toBeCalledTimes(1); // If the SDK_READY event fires and we have no callbacks for it (neither event nor ready promise) we get a warning. + expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // Telling us there were no listeners and evaluations before this point may have been incorrect. - expect(loggerMock.info.mock.calls.length).toBe(1); // If the SDK_READY event fires, we get a info message. - expect(loggerMock.info.mock.calls[0]).toEqual(['Split SDK is ready.']); // Telling us the SDK is ready. + expect(loggerMock.info).toBeCalledTimes(1); // If the SDK_READY event fires, we get a info message. + expect(loggerMock.info).toBeCalledWith(CLIENT_READY); // Telling us the SDK is ready. // Now it's marked as ready. addListenerCB('this event we do not care'); - expect(loggerMock.error.mock.calls.length).toBe(0); // Now if we add a listener to an event unrelated with readiness, we get no errors logged. + expect(loggerMock.error).not.toBeCalled(); // Now if we add a listener to an event unrelated with readiness, we get no errors logged. addListenerCB(SDK_READY); - expect(loggerMock.error.mock.calls).toEqual([['A listener was added for SDK_READY on the SDK, which has already fired and won\'t be emitted again. The callback won\'t be executed.']]); // If we try to add a listener to SDK_READY we get the corresponding warning. + expect(loggerMock.error).toBeCalledWith(ERROR_CLIENT_LISTENER, ['SDK_READY']); // If we try to add a listener for the already emitted SDK_READY event, we get the corresponding error. loggerMock.error.mockClear(); addListenerCB(SDK_READY_TIMED_OUT); - expect(loggerMock.error.mock.calls).toEqual([['A listener was added for SDK_READY_TIMED_OUT on the SDK, which has already fired and won\'t be emitted again. The callback won\'t be executed.']]); // If we try to add a listener to SDK_READY_TIMED_OUT we get the corresponding warning. + expect(loggerMock.error).toBeCalledWith(ERROR_CLIENT_LISTENER, ['SDK_READY_TIMED_OUT']); // If we try to add a listener for the already emitted SDK_READY_TIMED_OUT event, we get the corresponding error. }); test('The event callbacks should work as expected - SDK_READY emits with callbacks', () => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); // Get the callbacks const addListenerCB = sdkReadinessManager.readinessManager.gate.on.mock.calls[1][1]; addListenerCB(SDK_READY); - expect(loggerMock.warn.mock.calls.length).toBe(0); // We are adding a listener to the ready event before it is ready, so no warnings are logged. - expect(loggerMock.error.mock.calls.length).toBe(0); // We are adding a listener to the ready event before it is ready, so no errors are logged. + expect(loggerMock.warn).not.toBeCalled(); // We are adding a listener to the ready event before it is ready, so no warnings are logged. + expect(loggerMock.error).not.toBeCalled(); // We are adding a listener to the ready event before it is ready, so no errors are logged. emitReadyEvent(sdkReadinessManager.readinessManager); - expect(loggerMock.warn.mock.calls.length).toBe(0); // As we had at least one listener, we get no warnings. - expect(loggerMock.error.mock.calls.length).toBe(0); // As we had at least one listener, we get no errors. + expect(loggerMock.warn).not.toBeCalled(); // As we had at least one listener, we get no warnings. + expect(loggerMock.error).not.toBeCalled(); // As we had at least one listener, we get no errors. - expect(loggerMock.info.mock.calls.length).toBe(1); // If the SDK_READY event fires, we get a info message. - expect(loggerMock.info.mock.calls[0]).toEqual(['Split SDK is ready.']); // Telling us the SDK is ready. + expect(loggerMock.info).toBeCalledTimes(1); // If the SDK_READY event fires, we get a info message. + expect(loggerMock.info).toBeCalledWith(CLIENT_READY); // Telling us the SDK is ready. }); test('The event callbacks should work as expected - If we end up removing the listeners for SDK_READY, it behaves as if it had none', () => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); const gateMock = sdkReadinessManager.readinessManager.gate; // Get the callbacks @@ -147,11 +146,11 @@ describe('SDK Readiness Manager - Event emitter', () => { removeListenerCB(SDK_READY); emitReadyEvent(sdkReadinessManager.readinessManager); - expect(loggerMock.warn.mock.calls[0]).toEqual(['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.']); // We get the warning. + expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // We get the warning. }); test('The event callbacks should work as expected - If we end up removing the listeners for SDK_READY, it behaves as if it had none', () => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); const gateMock = sdkReadinessManager.readinessManager.gate; // Get the callbacks @@ -168,12 +167,12 @@ describe('SDK Readiness Manager - Event emitter', () => { removeListenerCB('random event'); emitReadyEvent(sdkReadinessManager.readinessManager); - expect(loggerMock.warn.mock.calls.length).toBe(0); // No warning when the SDK is ready as we still have one listener. + expect(loggerMock.warn).not.toBeCalled(); // No warning when the SDK is ready as we still have one listener. }); test('The event callbacks should work as expected - SDK_READY emits with expected internal callbacks', () => { // the sdkReadinessManager expects more than one SDK_READY callback to not log the "No listeners" warning - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, undefined /* default readyTimeout */, 1 /* internalReadyCbCount */); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock, undefined /* default readyTimeout */, 1 /* internalReadyCbCount */); const gateMock = sdkReadinessManager.readinessManager.gate; // Get the callbacks @@ -185,19 +184,19 @@ describe('SDK Readiness Manager - Event emitter', () => { addListenerCB(SDK_READY); removeListenerCB(SDK_READY); - expect(loggerMock.warn.mock.calls.length).toBe(0); // We are adding/removing listeners to the ready event before it is ready, so no warnings are logged. - expect(loggerMock.error.mock.calls.length).toBe(0); // We are adding/removing listeners to the ready event before it is ready, so no errors are logged. + expect(loggerMock.warn).not.toBeCalled(); // We are adding/removing listeners to the ready event before it is ready, so no warnings are logged. + expect(loggerMock.error).not.toBeCalled(); // We are adding/removing listeners to the ready event before it is ready, so no errors are logged. emitReadyEvent(sdkReadinessManager.readinessManager); - expect(loggerMock.warn.mock.calls.length).not.toBe(0); // As we had the same amount of listeners that the expected, we get a warning. - expect(loggerMock.error.mock.calls.length).toBe(0); // As we had at least one listener, we get no errors. + expect(loggerMock.warn).toBeCalled(); // As we had the same amount of listeners that the expected, we get a warning. + expect(loggerMock.error).not.toBeCalled(); // As we had at least one listener, we get no errors. }); }); describe('SDK Readiness Manager - Ready promise', () => { test('.ready() promise behaviour for clients', async (done) => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); const ready = sdkReadinessManager.sdkStatus.ready(); expect(ready instanceof Promise).toBe(true); // It should return a promise. @@ -226,7 +225,7 @@ describe('SDK Readiness Manager - Ready promise', () => { // control assertion. stubs already reset. expect(testPassedCount).toBe(2); - const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); const readyForTimeout = sdkReadinessManagerForTimedout.sdkStatus.ready(); @@ -256,7 +255,7 @@ describe('SDK Readiness Manager - Ready promise', () => { await ready.then( () => { expect('It should be a resolved promise when the SDK is ready, even after an SDK timeout.'); - mockClear(); + loggerMock.mockClear(); testPassedCount++; expect(testPassedCount).toBe(5); done(); @@ -266,14 +265,14 @@ describe('SDK Readiness Manager - Ready promise', () => { }); test('Full blown ready promise count as a callback and resolves on SDK_READY', (done) => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); const readyPromise = sdkReadinessManager.sdkStatus.ready(); // Get the callback const readyEventCB = sdkReadinessManager.readinessManager.gate.once.mock.calls[0][1]; readyEventCB(); - expect(loggerMock.warn.mock.calls).toEqual([['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.']]); // We would get the warning if the SDK get\'s ready before attaching any callbacks to ready promise. + expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // We would get the warning if the SDK get\'s ready before attaching any callbacks to ready promise. loggerMock.warn.mockClear(); readyPromise.then(() => { @@ -284,11 +283,11 @@ describe('SDK Readiness Manager - Ready promise', () => { }); readyEventCB(); - expect(loggerMock.warn.mock.calls.length).toBe(0); // But if we have a listener there are no warnings. + expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener there are no warnings. }); test('.ready() rejected promises have a default onRejected handler that just logs the error', (done) => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock); + const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); let readyForTimeout = sdkReadinessManager.sdkStatus.ready(); emitTimeoutEvent(sdkReadinessManager.readinessManager); // make the SDK "timed out" @@ -297,7 +296,7 @@ describe('SDK Readiness Manager - Ready promise', () => { () => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); } ); - expect(loggerMock.error.mock.calls.length === 0).toBe(true); // not called until promise is rejected + expect(loggerMock.error).not.toBeCalled(); // not called until promise is rejected setTimeout(() => { expect(loggerMock.error.mock.calls).toEqual([[timeoutErrorMessage]]); // If we don\'t handle the rejected promise, an error is logged. diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index 91176f6b..07f8f284 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -3,18 +3,13 @@ import promiseWrapper from '../utils/promise/wrapper'; import { readinessManagerFactory } from './readinessManager'; import { ISdkReadinessManager } from './types'; import { IEventEmitter } from '../types'; -import { logFactory } from '../logger/sdkLogger'; import { SDK_READY, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE } from './constants'; -const log = logFactory(''); +import { ILogger } from '../logger/types'; +import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO_LISTENER } from '../logger/constants'; const NEW_LISTENER_EVENT = 'newListener'; const REMOVE_LISTENER_EVENT = 'removeListener'; -// default onRejected handler, that just logs the error, if ready promise doesn't have one. -function defaultOnRejected(err: any) { - log.error(err); -} - /** * SdkReadinessManager factory, which provides the public status API of SDK clients and manager: ready promise, readiness event emitter and constants (SDK_READY, etc). * It also updates logs related warnings and errors. @@ -25,6 +20,7 @@ function defaultOnRejected(err: any) { * @param readinessManager optional readinessManager to use. only used internally for `shared` method */ export default function sdkReadinessManagerFactory( + log: ILogger, EventEmitter: new () => IEventEmitter, readyTimeout = 0, internalReadyCbCount = 0, @@ -39,7 +35,7 @@ export default function sdkReadinessManagerFactory( readinessManager.gate.on(NEW_LISTENER_EVENT, (event: any) => { if (event === SDK_READY || event === SDK_READY_TIMED_OUT) { if (readinessManager.isReady()) { - log.error(`A listener was added for ${event === SDK_READY ? 'SDK_READY' : 'SDK_READY_TIMED_OUT'} on the SDK, which has already fired and won't be emitted again. The callback won't be executed.`); + log.error(ERROR_CLIENT_LISTENER, [event === SDK_READY ? 'SDK_READY' : 'SDK_READY_TIMED_OUT']); } else if (event === SDK_READY) { readyCbCount++; } @@ -50,15 +46,20 @@ export default function sdkReadinessManagerFactory( const readyPromise = generateReadyPromise(); readinessManager.gate.once(SDK_READY_FROM_CACHE, () => { - log.info('Split SDK is ready from cache.'); + log.info(CLIENT_READY_FROM_CACHE); }); + // default onRejected handler, that just logs the error, if ready promise doesn't have one. + function defaultOnRejected(err: any) { + log.error(err); + } + function generateReadyPromise() { const promise = promiseWrapper(new Promise((resolve, reject) => { readinessManager.gate.once(SDK_READY, () => { - log.info('Split SDK is ready.'); + log.info(CLIENT_READY); - if (readyCbCount === internalReadyCbCount && !promise.hasOnFulfilled()) log.warn('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.'); + if (readyCbCount === internalReadyCbCount && !promise.hasOnFulfilled()) log.warn(CLIENT_NO_LISTENER); resolve(); }); readinessManager.gate.once(SDK_READY_TIMED_OUT, reject); @@ -72,7 +73,7 @@ export default function sdkReadinessManagerFactory( readinessManager, shared(readyTimeout = 0, internalReadyCbCount = 0) { - return sdkReadinessManagerFactory(EventEmitter, readyTimeout, internalReadyCbCount, readinessManager.shared(readyTimeout)); + return sdkReadinessManagerFactory(log, EventEmitter, readyTimeout, internalReadyCbCount, readinessManager.shared(readyTimeout)); }, sdkStatus: objectAssign( diff --git a/src/sdkClient/__tests__/sdkClientMethod.spec.ts b/src/sdkClient/__tests__/sdkClientMethod.spec.ts index 908070de..5adc1d88 100644 --- a/src/sdkClient/__tests__/sdkClientMethod.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethod.spec.ts @@ -1,3 +1,4 @@ +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { CONSUMER_MODE, STANDALONE_MODE } from '../../utils/constants'; import { sdkClientMethodFactory } from '../sdkClientMethod'; import { assertClientApi } from './testUtils'; @@ -11,7 +12,7 @@ const paramMocks = [ syncManager: undefined, sdkReadinessManager: { sdkStatus: jest.fn(), readinessManager: { destroy: jest.fn() } }, signalListener: undefined, - settings: { mode: CONSUMER_MODE } + settings: { mode: CONSUMER_MODE, log: loggerMock } }, // SyncManager (i.e., Sync SDK) and Signal listener { @@ -19,7 +20,7 @@ const paramMocks = [ syncManager: { stop: jest.fn(), flush: jest.fn(() => Promise.resolve()) }, sdkReadinessManager: { sdkStatus: jest.fn(), readinessManager: { destroy: jest.fn() } }, signalListener: { stop: jest.fn() }, - settings: { mode: STANDALONE_MODE } + settings: { mode: STANDALONE_MODE, log: loggerMock } } ]; @@ -39,14 +40,14 @@ test.each(paramMocks)('sdkClientMethodFactory', (params) => { // `client.destroy` method should stop internal components (other client methods where validated in `client.spec.ts`) client.destroy().then(() => { - expect(params.sdkReadinessManager.readinessManager.destroy.mock.calls.length).toBe(1); + expect(params.sdkReadinessManager.readinessManager.destroy).toBeCalledTimes(1); expect(params.storage.destroy).toBeCalledTimes(1); if (params.syncManager) { expect(params.syncManager.stop).toBeCalledTimes(1); expect(params.syncManager.flush).toBeCalledTimes(1); } - if (params.signalListener) expect(params.signalListener.stop.mock.calls.length).toBe(1); + if (params.signalListener) expect(params.signalListener.stop).toBeCalledTimes(1); }); // calling the function with parameters should throw an error diff --git a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts index ba066267..345909f9 100644 --- a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts @@ -210,12 +210,10 @@ describe('sdkClientMethodCSFactory', () => { const paramsWithInvalidKeyAndTT = { ...params, settings: { + ...params.settings, core: { key: true, // invalid key trafficType: '' // invalid TT - }, - startup: { - readyTimeout: 1, } } }; diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 9f66f9e8..e9f178d8 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -8,8 +8,7 @@ import { CONTROL } from '../utils/constants'; import { IClientFactoryParams } from './types'; import { IEvaluationResult } from '../evaluator/types'; import { SplitIO, ImpressionDTO } from '../types'; -import { logFactory } from '../logger/sdkLogger'; -const log = logFactory('splitio-client'); +import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants'; /** @@ -17,7 +16,7 @@ const log = logFactory('splitio-client'); */ // @TODO missing time tracking to collect telemetry export default function clientFactory(params: IClientFactoryParams): SplitIO.IClient | SplitIO.IAsyncClient { - const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker } = params; + const { sdkReadinessManager: { readinessManager }, storage, settings: { log, mode }, impressionsTracker, eventTracker } = params; function getTreatment(key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, withConfig = false) { const wrapUp = (evaluationResult: IEvaluationResult) => { @@ -27,7 +26,7 @@ export default function clientFactory(params: IClientFactoryParams): SplitIO.ICl return treatment; }; - const evaluation = evaluateFeature(key, splitName, attributes, storage); + const evaluation = evaluateFeature(log, key, splitName, attributes, storage); return thenable(evaluation) ? evaluation.then((res) => wrapUp(res)) : wrapUp(evaluation); } @@ -47,7 +46,7 @@ export default function clientFactory(params: IClientFactoryParams): SplitIO.ICl return treatments; }; - const evaluations = evaluateFeatures(key, splitNames, attributes, storage); + const evaluations = evaluateFeatures(log, key, splitNames, attributes, storage); return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations); } @@ -76,10 +75,10 @@ export default function clientFactory(params: IClientFactoryParams): SplitIO.ICl } const { treatment, label, changeNumber, config = null } = evaluation; - log.info(`Split: ${splitName}. Key: ${matchingKey}. Evaluation: ${treatment}. Label: ${label}`); + log.info(IMPRESSION, [splitName, matchingKey, treatment, label]); - if (validateSplitExistance(readinessManager, splitName, label, invokingMethodName)) { - log.info('Queueing corresponding impression.'); + if (validateSplitExistance(log, readinessManager, splitName, label, invokingMethodName)) { + log.info(IMPRESSION_QUEUEING); queue.push({ feature: splitName, keyName: matchingKey, @@ -114,7 +113,7 @@ export default function clientFactory(params: IClientFactoryParams): SplitIO.ICl }; // This may be async but we only warn, we don't actually care if it is valid or not in terms of queueing the event. - validateTrafficTypeExistance(readinessManager, storage.splits, settings.mode, trafficTypeName, 'track'); + validateTrafficTypeExistance(log, readinessManager, storage.splits, mode, trafficTypeName, 'track'); return eventTracker.track(eventData as SplitIO.EventData, size); } diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index 262037df..c0fc26bf 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -16,24 +16,25 @@ import { CONTROL, CONTROL_WITH_CONFIG } from '../utils/constants'; import { IReadinessManager } from '../readiness/types'; import { MaybeThenable } from '../dtos/types'; import { SplitIO } from '../types'; +import { ILogger } from '../logger/types'; /** * Decorator that validates the input before actually executing the client methods. * We should "guard" the client here, while not polluting the "real" implementation of those methods. */ -export default function clientInputValidationDecorator(client: TClient, readinessManager: IReadinessManager, isStorageSync = false): TClient { +export default function clientInputValidationDecorator(log: ILogger, client: TClient, readinessManager: IReadinessManager, isStorageSync = false): TClient { /** * Avoid repeating this validations code */ function validateEvaluationParams(maybeKey: SplitIO.SplitKey, maybeSplitOrSplits: string | string[], maybeAttributes: SplitIO.Attributes | undefined, methodName: string) { const multi = startsWith(methodName, 'getTreatments'); - const key = validateKey(maybeKey, methodName); - const splitOrSplits = multi ? validateSplits(maybeSplitOrSplits, methodName) : validateSplit(maybeSplitOrSplits, methodName); - const attributes = validateAttributes(maybeAttributes, methodName); - const isOperational = validateIfNotDestroyed(readinessManager); + const key = validateKey(log, maybeKey, methodName); + const splitOrSplits = multi ? validateSplits(log, maybeSplitOrSplits, methodName) : validateSplit(log, maybeSplitOrSplits, methodName); + const attributes = validateAttributes(log, maybeAttributes, methodName); + const isOperational = validateIfNotDestroyed(log, readinessManager, methodName); - validateIfOperational(readinessManager, methodName); + validateIfOperational(log, readinessManager, methodName); const valid = isOperational && key && splitOrSplits && attributes !== false; @@ -97,12 +98,12 @@ export default function clientInputValidationDecorator SplitIO.IClient | SplitIO.IAsyncClient { - + const log = params.settings.log; const clientInstance = sdkClientFactory(params); return function client() { @@ -16,7 +15,7 @@ export function sdkClientMethodFactory(params: ISdkClientFactoryParams): () => S throw new Error('Shared Client not supported by the storage mechanism. Create isolated instances instead.'); } - log.debug('Retrieving SDK client.'); + log.debug(RETRIEVE_CLIENT_DEFAULT); return clientInstance; }; } diff --git a/src/sdkClient/sdkClientMethodCS.ts b/src/sdkClient/sdkClientMethodCS.ts index f2490aae..80089705 100644 --- a/src/sdkClient/sdkClientMethodCS.ts +++ b/src/sdkClient/sdkClientMethodCS.ts @@ -2,30 +2,31 @@ import clientCSDecorator from './clientCS'; import { ISdkClientFactoryParams } from './types'; import { SplitIO } from '../types'; import { validateKey } from '../utils/inputValidation/key'; -import { logFactory } from '../logger/sdkLogger'; import { getMatching, keyParser } from '../utils/key'; import { sdkClientFactory } from './sdkClient'; import { IStorageSyncCS } from '../storages/types'; import { ISyncManagerCS } from '../sync/types'; import objectAssign from 'object-assign'; -const log = logFactory('splitio'); +import { RETRIEVE_CLIENT_DEFAULT, NEW_SHARED_CLIENT, RETRIEVE_CLIENT_EXISTING } from '../logger/constants'; function buildInstanceId(key: SplitIO.SplitKey) { // @ts-ignore return `${key.matchingKey ? key.matchingKey : key}-${key.bucketingKey ? key.bucketingKey : key}-`; } +const method = 'Client instantiation'; + /** * Factory of client method for the client-side API variant where TT is ignored and thus * clients don't have a binded TT for the track method. */ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?: SplitIO.SplitKey) => SplitIO.ICsClient { - const { storage, syncManager, sdkReadinessManager, settings: { core: { key }, startup: { readyTimeout } } } = params; + const { storage, syncManager, sdkReadinessManager, settings: { core: { key }, startup: { readyTimeout }, log } } = params; // Keeping similar behaviour as in the isomorphic JS SDK: if settings key is invalid, // `false` value is used as binded key of the default client, but trafficType is ignored // @TODO handle as a non-recoverable error - const validKey = validateKey(key, 'Client instantiation'); + const validKey = validateKey(log, key, method); const mainClientInstance = clientCSDecorator( sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore @@ -41,12 +42,12 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? return function client(key?: SplitIO.SplitKey) { if (key === undefined) { - log.debug('Retrieving default SDK client.'); + log.debug(RETRIEVE_CLIENT_DEFAULT); return mainClientInstance; } // Validate the key value. The trafficType (2nd argument) is ignored - const validKey = validateKey(key, 'Shared Client instantiation'); + const validKey = validateKey(log, key, method); if (validKey === false) { throw new Error('Shared Client needs a valid key.'); } @@ -75,9 +76,9 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? sharedSyncManager.start(); - log.info('New shared client instance created.'); + log.info(NEW_SHARED_CLIENT); } else { - log.debug('Retrieving existing SDK client.'); + log.debug(RETRIEVE_CLIENT_EXISTING); } return clientInstances[instanceId]; diff --git a/src/sdkClient/sdkClientMethodCSWithTT.ts b/src/sdkClient/sdkClientMethodCSWithTT.ts index b49df093..684f3a5b 100644 --- a/src/sdkClient/sdkClientMethodCSWithTT.ts +++ b/src/sdkClient/sdkClientMethodCSWithTT.ts @@ -3,34 +3,35 @@ import { ISdkClientFactoryParams } from './types'; import { SplitIO } from '../types'; import { validateKey } from '../utils/inputValidation/key'; import { validateTrafficType } from '../utils/inputValidation/trafficType'; -import { logFactory } from '../logger/sdkLogger'; import { getMatching, keyParser } from '../utils/key'; import { sdkClientFactory } from './sdkClient'; import { IStorageSyncCS } from '../storages/types'; import { ISyncManagerCS } from '../sync/types'; import objectAssign from 'object-assign'; -const log = logFactory('splitio'); +import { RETRIEVE_CLIENT_DEFAULT, NEW_SHARED_CLIENT, RETRIEVE_CLIENT_EXISTING } from '../logger/constants'; function buildInstanceId(key: SplitIO.SplitKey, trafficType?: string) { // @ts-ignore return `${key.matchingKey ? key.matchingKey : key}-${key.bucketingKey ? key.bucketingKey : key}-${trafficType !== undefined ? trafficType : ''}`; } +const method = 'Client instantiation'; + /** * Factory of client method for the client-side (browser) variant of the Isomorphic JS SDK, * where clients can have a binded TT for the track method, which is provided via the settings * (default client) or the client method (shared clients). */ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?: SplitIO.SplitKey, trafficType?: string) => SplitIO.ICsClient { - const { storage, syncManager, sdkReadinessManager, settings: { core: { key, trafficType }, startup: { readyTimeout } } } = params; + const { storage, syncManager, sdkReadinessManager, settings: { core: { key, trafficType }, startup: { readyTimeout }, log } } = params; // Keeping the behaviour as in the isomorphic JS SDK: if settings key or TT are invalid, // `false` value is used as binded key/TT of the default client, which leads to several issues. // @TODO update when supporting non-recoverable errors - const validKey = validateKey(key, 'Client instantiation'); + const validKey = validateKey(log, key, method); let validTrafficType; if (trafficType !== undefined) { - validTrafficType = validateTrafficType(trafficType, 'Client instantiation'); + validTrafficType = validateTrafficType(log, trafficType, method); } const mainClientInstance = clientCSDecorator( @@ -48,19 +49,19 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? return function client(key?: SplitIO.SplitKey, trafficType?: string) { if (key === undefined) { - log.debug('Retrieving default SDK client.'); + log.debug(RETRIEVE_CLIENT_DEFAULT); return mainClientInstance; } // Validate the key value - const validKey = validateKey(key, 'Shared Client instantiation'); + const validKey = validateKey(log, key, `Shared ${method}`); if (validKey === false) { throw new Error('Shared Client needs a valid key.'); } let validTrafficType; if (trafficType !== undefined) { - validTrafficType = validateTrafficType(trafficType, 'Shared Client instantiation'); + validTrafficType = validateTrafficType(log, trafficType, `Shared ${method}`); if (validTrafficType === false) { throw new Error('Shared Client needs a valid traffic type or no traffic type at all.'); } @@ -90,9 +91,9 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? sharedSyncManager.start(); - log.info('New shared client instance created.'); + log.info(NEW_SHARED_CLIENT); } else { - log.debug('Retrieving existing SDK client.'); + log.debug(RETRIEVE_CLIENT_EXISTING); } return clientInstances[instanceId]; diff --git a/src/sdkFactory/__tests__/index.spec.ts b/src/sdkFactory/__tests__/index.spec.ts index 4ba94a97..3e758f68 100644 --- a/src/sdkFactory/__tests__/index.spec.ts +++ b/src/sdkFactory/__tests__/index.spec.ts @@ -1,6 +1,5 @@ import { ISdkFactoryParams } from '../types'; import { sdkFactory } from '../index'; -import { API } from '../../logger/sdkLogger'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; import { SplitIO } from '../../types'; import EventEmitter from '../../utils/MinEvents'; @@ -14,6 +13,12 @@ const mockStorage = { events: jest.fn(), impressions: jest.fn() }; +const loggerApiMock = 'loggerApi'; +jest.mock('../../logger/sdkLogger', () => { + return { + createLoggerAPI: () => loggerApiMock + }; +}); // IAsyncSDK, minimal params const paramsForAsyncSDK = { @@ -48,7 +53,7 @@ const fullParamsForSyncSDK = { /** End Mocks */ function assertSdkApi(sdk: SplitIO.IAsyncSDK | SplitIO.ISDK | SplitIO.ICsSDK, params: any) { - expect(sdk.Logger).toBe(API); + expect(sdk.Logger).toBe(loggerApiMock); expect(sdk.settings).toBe(params.settings); expect(sdk.client).toBe(params.sdkClientMethodFactory.mock.results[0].value); expect(sdk.manager()).toBe(params.sdkManagerFactory.mock.results[0].value); diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 1db36dd2..2fd3e67c 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -1,6 +1,5 @@ import { ISdkFactoryParams } from './types'; import sdkReadinessManagerFactory from '../readiness/sdkReadinessManager'; -import { logFactory, API } from '../logger/sdkLogger'; import buildMetadata from '../utils/settingsValidation/buildMetadata'; import impressionsTrackerFactory from '../trackers/impressionsTracker'; import eventTrackerFactory from '../trackers/eventTracker'; @@ -10,8 +9,8 @@ import { ISplitApi } from '../services/types'; import { getMatching } from '../utils/key'; import { shouldBeOptimized } from '../trackers/impressionObserver/utils'; import { validateAndTrackApiKey } from '../utils/inputValidation/apiKey'; - -const log = logFactory('splitio'); +import { createLoggerAPI } from '../logger/sdkLogger'; +import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants'; /** * Modular SDK factory @@ -21,15 +20,16 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const { settings, platform, storageFactory, splitApiFactory, syncManagerFactory, SignalListener, impressionsObserverFactory, impressionListener, integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory } = params; + const log = settings.log; // @TODO handle non-recoverable errors: not start sync, mark the SDK as destroyed, etc. // We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle. - validateAndTrackApiKey(settings.core.authorizationKey); + validateAndTrackApiKey(log, settings.core.authorizationKey); const metadata = buildMetadata(settings); // @TODO handle non-recoverable error, such as, `fetch` api not available, invalid API Key, etc. - const sdkReadinessManager = sdkReadinessManagerFactory(platform.EventEmitter, settings.startup.readyTimeout); + const sdkReadinessManager = sdkReadinessManagerFactory(log, platform.EventEmitter, settings.startup.readyTimeout); const storageFactoryParams = { eventsQueueSize: settings.scheduler.eventsQueueSize, @@ -44,7 +44,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // ATM, only used by InRedisStorage. @TODO pass a callback to simplify custom storages. readinessManager: sdkReadinessManager.readinessManager, - metadata + metadata, + log }; const storage = storageFactory(storageFactoryParams); @@ -64,20 +65,20 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // trackers const observer = impressionsObserverFactory && impressionsObserverFactory(); - const impressionsTracker = impressionsTrackerFactory(storage.impressions, metadata, impressionListener, integrationsManager, observer, storage.impressionCounts); - const eventTracker = eventTrackerFactory(storage.events, integrationsManager); + const impressionsTracker = impressionsTrackerFactory(log, storage.impressions, metadata, impressionListener, integrationsManager, observer, storage.impressionCounts); + const eventTracker = eventTrackerFactory(log, storage.events, integrationsManager); // signal listener const signalListener = SignalListener && new SignalListener(syncManager && syncManager.flush, settings, storage, splitApi); // Sdk client and manager const clientMethod = sdkClientMethodFactory({ eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }); - const managerInstance = sdkManagerFactory && sdkManagerFactory(storage.splits, sdkReadinessManager); + const managerInstance = sdkManagerFactory(log, storage.splits, sdkReadinessManager); syncManager && syncManager.start(); signalListener && signalListener.start(); - log.info('New Split SDK instance created.'); + log.info(NEW_FACTORY); return { // Split evaluation and event tracking engine @@ -86,13 +87,12 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // Manager API to explore available information // @ts-ignore manager() { - if (managerInstance) log.info('Manager instance retrieved.'); - else log.error('Manager instance is not available. Provide the manager module on settings.'); + log.debug(RETRIEVE_MANAGER); return managerInstance; }, // Logger wrapper API - Logger: API, + Logger: createLoggerAPI(settings.log), settings, }; diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 22b34646..dcd6c0d2 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -1,6 +1,7 @@ import { MaybeThenable } from '../dtos/types'; import { IIntegrationManager, IIntegrationFactoryParams } from '../integrations/types'; import { ISignalListener } from '../listeners/types'; +import { ILogger } from '../logger/types'; import { ISdkReadinessManager } from '../readiness/types'; import { ISdkClientFactoryParams } from '../sdkClient/types'; import { IFetch, ISplitApi } from '../services/types'; @@ -45,7 +46,8 @@ export interface ISdkFactoryParams { syncManagerFactory?: (params: ISyncManagerFactoryParams) => ISyncManager, // Sdk manager factory - sdkManagerFactory?: ( + sdkManagerFactory: ( + log: ILogger, splits: ISplitsCacheSync | ISplitsCacheAsync, sdkReadinessManager: ISdkReadinessManager ) => SplitIO.IManager | SplitIO.IAsyncManager, diff --git a/src/sdkManager/__tests__/index.asyncCache.spec.ts b/src/sdkManager/__tests__/index.asyncCache.spec.ts index 993704d5..46cffb11 100644 --- a/src/sdkManager/__tests__/index.asyncCache.spec.ts +++ b/src/sdkManager/__tests__/index.asyncCache.spec.ts @@ -5,6 +5,7 @@ import { sdkManagerFactory } from '../index'; import SplitsCacheInRedis from '../../storages/inRedis/SplitsCacheInRedis'; import KeyBuilderSS from '../../storages/KeyBuilderSS'; import { ISdkReadinessManager } from '../../readiness/types'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; // @ts-expect-error const sdkReadinessManagerMock = { @@ -20,8 +21,8 @@ test('MANAGER API / Async cache (In Redis)', async () => { /** Setup: create manager */ const connection = new Redis({}); // @ts-expect-error const keys = new KeyBuilderSS(); - const cache = new SplitsCacheInRedis(keys, connection); - const manager = sdkManagerFactory(cache, sdkReadinessManagerMock); + const cache = new SplitsCacheInRedis(loggerMock, keys, connection); + const manager = sdkManagerFactory(loggerMock, cache, sdkReadinessManagerMock); await cache.clear(); await cache.addSplit(splitObject.name, JSON.stringify(splitObject)); diff --git a/src/sdkManager/__tests__/index.syncCache.spec.ts b/src/sdkManager/__tests__/index.syncCache.spec.ts index 85440d58..4da65cd4 100644 --- a/src/sdkManager/__tests__/index.syncCache.spec.ts +++ b/src/sdkManager/__tests__/index.syncCache.spec.ts @@ -3,6 +3,7 @@ import splitView from './mocks/output.json'; import { sdkManagerFactory } from '../index'; import SplitsCacheInMemory from '../../storages/inMemory/SplitsCacheInMemory'; import { ISdkReadinessManager } from '../../readiness/types'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; // @ts-expect-error const sdkReadinessManagerMock = { @@ -17,7 +18,7 @@ describe('MANAGER API / Sync cache (In Memory)', () => { /** Setup: create manager */ const cache = new SplitsCacheInMemory(); - const manager = sdkManagerFactory(cache, sdkReadinessManagerMock); + const manager = sdkManagerFactory(loggerMock, cache, sdkReadinessManagerMock); cache.addSplit(splitObject.name, JSON.stringify(splitObject)); test('List all splits', () => { diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index f5a9051b..a929d082 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -6,6 +6,7 @@ import { ISplitsCacheAsync, ISplitsCacheSync } from '../storages/types'; import { ISdkReadinessManager } from '../readiness/types'; import { ISplit } from '../dtos/types'; import { SplitIO } from '../types'; +import { ILogger } from '../logger/types'; function collectTreatments(splitObject: ISplit) { const conditions = splitObject.conditions; @@ -51,6 +52,7 @@ function objectsToViews(jsons: string[]) { } export function sdkManagerFactory( + log: ILogger, splits: TSplitCache, { readinessManager, sdkStatus }: ISdkReadinessManager ): TSplitCache extends ISplitsCacheAsync ? SplitIO.IAsyncManager : SplitIO.IManager { @@ -64,8 +66,8 @@ export function sdkManagerFactory { - validateSplitExistance(readinessManager, splitName, result, SPLIT_FN_LABEL); + validateSplitExistance(log, readinessManager, splitName, result, SPLIT_FN_LABEL); return objectToView(result); }); } - validateSplitExistance(readinessManager, splitName, split, SPLIT_FN_LABEL); + validateSplitExistance(log, readinessManager, splitName, split, SPLIT_FN_LABEL); return objectToView(split); }, @@ -86,7 +88,7 @@ export function sdkManagerFactory (IFetch | undefined), getOptions?: () => object): ISplitHttpClient { +export function splitHttpClientFactory(log: ILogger, apikey: string, metadata: IMetadata, getFetch?: () => (IFetch | undefined), getOptions?: () => object): ISplitHttpClient { const options = getOptions && getOptions(); const fetch = getFetch && getFetch(); // if fetch is not available, log Error - if (!fetch) log.error(messageNoFetch + ' The SDK will not get ready.'); + if (!fetch) log.error(ERROR_CLIENT_CANNOT_GET_READY, [messageNoFetch]); const headers: Record = { 'Accept': 'application/json', @@ -45,7 +45,7 @@ export function splitHttpClientFactory(apikey: string, metadata: IMetadata, getF return fetch ? fetch(url, request) // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful .then(response => { - if (!response.ok) { + if (!response.ok) { // eslint-disable-next-line no-throw-literal throw { response }; } return response; @@ -66,7 +66,7 @@ export function splitHttpClientFactory(apikey: string, metadata: IMetadata, getF } if (!resp || resp.status !== 403) { // 403's log we'll be handled somewhere else. - log[logErrorsAsInfo ? 'info' : 'error'](`Response status is not OK. Status: ${resp ? resp.status : 'NO_STATUS'}. URL: ${url}. Message: ${msg}`); + log[logErrorsAsInfo ? 'info' : 'error'](ERROR_HTTP, [resp ? resp.status : 'NO_STATUS', url, msg]); } // passes `undefined` as statusCode if not an HTTP error (resp === undefined) diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 0fe30585..36eef20d 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -99,7 +99,8 @@ export default abstract class AbstractSplitsCacheSync implements ISplitsCacheSyn * Given a parsed split, it returns a boolean flagging if its conditions use segments matchers (rules & whitelists). * This util is intended to simplify the implementation of `splitsCache::usesSegments` method */ -export function usesSegments({ conditions = [] }: ISplit) { +export function usesSegments(split: ISplit) { + const conditions = split.conditions || []; for (let i = 0; i < conditions.length; i++) { const matchers = conditions[i].matcherGroup.matchers; diff --git a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts index 6f93eba7..020f823a 100644 --- a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts @@ -1,16 +1,16 @@ +import { ILogger } from '../../logger/types'; import AbstractSegmentsCacheSync from '../AbstractSegmentsCacheSync'; -import { logFactory } from '../../logger/sdkLogger'; import KeyBuilderCS from '../KeyBuilderCS'; -const log = logFactory('splitio-storage:localstorage'); - -const DEFINED = '1'; +import { logPrefix, DEFINED } from './constants'; export default class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { private readonly keys: KeyBuilderCS; + private readonly log: ILogger; - constructor(keys: KeyBuilderCS) { + constructor(log: ILogger, keys: KeyBuilderCS) { super(); + this.log = log; this.keys = keys; // There is not need to flush segments cache like splits cache, since resetSegments receives the up-to-date list of active segments } @@ -20,7 +20,7 @@ export default class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { * @NOTE this method is not being used at the moment. */ clear() { - log.info('Flushing MySegments data from localStorage'); + this.log.info(logPrefix + 'Flushing MySegments data from localStorage'); // We cannot simply call `localStorage.clear()` since that implies removing user items from the storage // We could optimize next sentence, since it implies iterating over all localStorage items @@ -34,7 +34,7 @@ export default class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { localStorage.setItem(segmentKey, DEFINED); return true; } catch (e) { - log.error(e); + this.log.error(logPrefix + e); return false; } } @@ -46,7 +46,7 @@ export default class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { localStorage.removeItem(segmentKey); return true; } catch (e) { - log.error(e); + this.log.error(logPrefix + e); return false; } } diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 742ee1c4..58d5ba09 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -2,8 +2,8 @@ import { ISplit, ISplitFiltersValidation } from '../../dtos/types'; import AbstractSplitsCacheSync, { usesSegments } from '../AbstractSplitsCacheSync'; import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang'; import KeyBuilderCS from '../KeyBuilderCS'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-storage:localstorage'); +import { ILogger } from '../../logger/types'; +import { logPrefix } from './constants'; /** * ISplitsCacheSync implementation that stores split definitions in browser LocalStorage. @@ -21,7 +21,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { * @param {number | undefined} expirationTimestamp * @param {ISplitFiltersValidation} splitFiltersValidation */ - constructor(keys: KeyBuilderCS, expirationTimestamp?: number, splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { byName: [], byPrefix: [] }, validFilters: [] }) { + constructor(private readonly log: ILogger, keys: KeyBuilderCS, expirationTimestamp?: number, splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { byName: [], byPrefix: [] }, validFilters: [] }) { super(); this.keys = keys; this.splitFiltersValidation = splitFiltersValidation; @@ -52,7 +52,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } } catch (e) { - log.error(e); + this.log.error(logPrefix + e); } } @@ -72,7 +72,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } } catch (e) { - log.error(e); + this.log.error(logPrefix + e); } } @@ -82,7 +82,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { * We cannot simply call `localStorage.clear()` since that implies removing user items from the storage. */ clear() { - log.info('Flushing Splits data from localStorage'); + this.log.info(logPrefix + 'Flushing Splits data from localStorage'); // collect item keys const len = localStorage.length; @@ -114,7 +114,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { return true; } catch (e) { - log.error(e); + this.log.error(logPrefix + e); return false; } } @@ -129,7 +129,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { return 1; } catch (e) { - log.error(e); + this.log.error(logPrefix + e); return 0; } } @@ -147,14 +147,14 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { // when using a new split query, we must update it at the store if (this.updateNewFilter) { - log.info('Split filter query was modified. Updating cache.'); + this.log.info(logPrefix + 'Split filter query was modified. Updating cache.'); const queryKey = this.keys.buildSplitsFilterQueryKey(); const queryString = this.splitFiltersValidation.queryString; try { if (queryString) localStorage.setItem(queryKey, queryString); else localStorage.removeItem(queryKey); } catch (e) { - log.error(e); + this.log.error(logPrefix + e); } this.updateNewFilter = false; } @@ -166,7 +166,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { this.hasSync = true; return true; } catch (e) { - log.error(e); + this.log.error(logPrefix + e); return false; } } @@ -273,7 +273,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { }); } } catch (e) { - log.error(e); + this.log.error(logPrefix + e); } } // if the filter didn't change, nothing is done diff --git a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts index 7040e797..158f8c2c 100644 --- a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts @@ -1,9 +1,10 @@ import MySegmentsCacheInLocal from '../MySegmentsCacheInLocal'; import KeyBuilderCS from '../../KeyBuilderCS'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('SEGMENT CACHE / in LocalStorage', () => { const keys = new KeyBuilderCS('SPLITIO', 'user'); - const cache = new MySegmentsCacheInLocal(keys); + const cache = new MySegmentsCacheInLocal(loggerMock, keys); cache.clear(); diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 48a4e53f..4430afe7 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -1,8 +1,9 @@ import SplitsCacheInLocal from '../SplitsCacheInLocal'; import KeyBuilderCS from '../../KeyBuilderCS'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('SPLIT CACHE / LocalStorage', () => { - const cache = new SplitsCacheInLocal(new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); cache.clear(); @@ -43,7 +44,7 @@ test('SPLIT CACHE / LocalStorage', () => { }); test('SPLIT CACHE / LocalStorage / Get Keys', () => { - const cache = new SplitsCacheInLocal(new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); cache.addSplit('lol1', 'something'); cache.addSplit('lol2', 'something else'); @@ -55,7 +56,7 @@ test('SPLIT CACHE / LocalStorage / Get Keys', () => { }); test('SPLIT CACHE / LocalStorage / Add Splits', () => { - const cache = new SplitsCacheInLocal(new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); cache.addSplits([ ['lol1', 'something'], @@ -69,7 +70,7 @@ test('SPLIT CACHE / LocalStorage / Add Splits', () => { }); test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { - const cache = new SplitsCacheInLocal(new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); cache.addSplits([ // loop of addSplit ['split1', '{ "trafficTypeName": "user_tt" }'], @@ -108,7 +109,7 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { }); test('SPLIT CACHE / LocalStorage / killLocally', () => { - const cache = new SplitsCacheInLocal(new KeyBuilderCS('SPLITIO', 'user')); + const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); cache.addSplit('lol1', '{ "name": "something"}'); cache.addSplit('lol2', '{ "name": "something else"}'); const initialChangeNumber = cache.getChangeNumber(); @@ -117,24 +118,24 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { let updated = cache.killLocally('nonexistent_split', 'other_treatment', 101); const nonexistentSplit = cache.getSplit('nonexistent_split'); - expect(updated).toBe( false); // t exist + expect(updated).toBe(false); // killLocally resolves without update if split doesn't exist expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent // kill an existent split updated = cache.killLocally('lol1', 'some_treatment', 100); let lol1Split = JSON.parse(cache.getSplit('lol1') as string); - expect(updated).toBe( true); // killLocally resolves with update if split is changed + expect(updated).toBe(true); // killLocally resolves with update if split is changed expect(lol1Split.killed).toBe(true); // existing split must be killed - expect(lol1Split.defaultTreatment).toBe( 'some_treatment'); // existing split must have new default treatment - expect(lol1Split.changeNumber).toBe( 100); // existing split must have the given change number - expect(cache.getChangeNumber()).toBe( initialChangeNumber); // cache changeNumber is not changed + expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have new default treatment + expect(lol1Split.changeNumber).toBe(100); // existing split must have the given change number + expect(cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed // not update if changeNumber is old updated = cache.killLocally('lol1', 'some_treatment_2', 90); lol1Split = JSON.parse(cache.getSplit('lol1') as string); - expect(updated).toBe( false); // killLocally resolves without update if changeNumber is old - expect(lol1Split.defaultTreatment).not.toBe( 'some_treatment_2'); // existing split is not updated if given changeNumber is older + expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old + expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older }); diff --git a/src/storages/inLocalStorage/__tests__/index.spec.ts b/src/storages/inLocalStorage/__tests__/index.spec.ts index 163301b0..131f2eff 100644 --- a/src/storages/inLocalStorage/__tests__/index.spec.ts +++ b/src/storages/inLocalStorage/__tests__/index.spec.ts @@ -1,23 +1,48 @@ +// Mocks +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { IStorageFactoryParams } from '../../types'; +const fakeInMemoryStorage = 'fakeStorage'; +const fakeInMemoryStorageFactory = jest.fn(() => fakeInMemoryStorage); +jest.mock('../../inMemory/InMemoryStorageCS', () => { + return { + InMemoryStorageCSFactory: fakeInMemoryStorageFactory + }; +}); + +// Test target import { InLocalStorage } from '../index'; describe('IN LOCAL STORAGE', () => { - test('return undefined if LocalStorage API is not available', () => { + // @ts-ignore + const internalSdkParams: IStorageFactoryParams = { log: loggerMock }; + + afterEach(() => { + fakeInMemoryStorageFactory.mockClear(); + }); + + test('calls InMemoryStorage factory if LocalStorage API is not available', () => { const originalLocalStorage = Object.getOwnPropertyDescriptor(global, 'localStorage'); Object.defineProperty(global, 'localStorage', {}); // delete global localStorage property - const storage = InLocalStorage({ prefix: 'prefix' }); - expect(storage).toBe(undefined); + const storageFactory = InLocalStorage({ prefix: 'prefix' }); + const storage = storageFactory(internalSdkParams); + + expect(fakeInMemoryStorageFactory).toBeCalledWith(internalSdkParams); // calls InMemoryStorage factory + expect(storage).toBe(fakeInMemoryStorage); Object.defineProperty(global, 'localStorage', originalLocalStorage as PropertyDescriptor); // restore original localStorage }); - test('return a new storage if LocalStorage API is available', () => { + test('calls its own storage factory if LocalStorage API is available', () => { + + const storageFactory = InLocalStorage({ prefix: 'prefix' }); + const storage = storageFactory(internalSdkParams); - const storage = InLocalStorage({ prefix: 'prefix' }); - expect(typeof storage).toBe('function'); + expect(typeof storage).toBe('object'); + expect(fakeInMemoryStorageFactory).not.toBeCalled(); // doesn't call InMemoryStorage factory }); diff --git a/src/storages/inLocalStorage/constants.ts b/src/storages/inLocalStorage/constants.ts new file mode 100644 index 00000000..74624310 --- /dev/null +++ b/src/storages/inLocalStorage/constants.ts @@ -0,0 +1,2 @@ +export const logPrefix = 'storage:localstorage: '; +export const DEFINED = '1'; diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 6eb1fcf8..226d771c 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -6,11 +6,11 @@ import KeyBuilderCS from '../KeyBuilderCS'; import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable'; import SplitsCacheInLocal from './SplitsCacheInLocal'; import MySegmentsCacheInLocal from './MySegmentsCacheInLocal'; -import { logFactory } from '../../logger/sdkLogger'; import MySegmentsCacheInMemory from '../inMemory/MySegmentsCacheInMemory'; import SplitsCacheInMemory from '../inMemory/SplitsCacheInMemory'; import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; -const log = logFactory('splitio-storage:localstorage'); +import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; +import { logPrefix } from './constants'; export interface InLocalStorageOptions { prefix?: string @@ -21,22 +21,23 @@ export interface InLocalStorageOptions { */ export function InLocalStorage(options: InLocalStorageOptions = {}) { - // Fallback to InMemoryStorage if LocalStorage API is not available - if (!isLocalStorageAvailable()) { - log.warn('LocalStorage API is unavailable. Fallbacking into default MEMORY storage'); - return; - } - const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; return function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSyncCS { + // Fallback to InMemoryStorage if LocalStorage API is not available + if (!isLocalStorageAvailable()) { + params.log.warn(logPrefix + 'LocalStorage API is unavailable. Fallbacking into default MEMORY storage'); + return InMemoryStorageCSFactory(params); + } + + const log = params.log; const keys = new KeyBuilderCS(prefix, params.matchingKey as string); const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; return { - splits: new SplitsCacheInLocal(keys, expirationTimestamp, params.splitFiltersValidation), - segments: new MySegmentsCacheInLocal(keys), + splits: new SplitsCacheInLocal(log, keys, expirationTimestamp, params.splitFiltersValidation), + segments: new MySegmentsCacheInLocal(log, keys), impressions: new ImpressionsCacheInMemory(), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), @@ -55,7 +56,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}) { return { splits: this.splits, - segments: new MySegmentsCacheInLocal(childKeysBuilder), + segments: new MySegmentsCacheInLocal(log, childKeysBuilder), impressions: this.impressions, impressionCounts: this.impressionCounts, events: this.events, diff --git a/src/storages/inRedis/EventsCacheInRedis.ts b/src/storages/inRedis/EventsCacheInRedis.ts index e0fda5bc..5a8caab5 100644 --- a/src/storages/inRedis/EventsCacheInRedis.ts +++ b/src/storages/inRedis/EventsCacheInRedis.ts @@ -1,10 +1,11 @@ import { IEventsCacheAsync } from '../types'; import { IRedisMetadata } from '../../dtos/types'; import KeyBuilderSS from '../KeyBuilderSS'; -import { logFactory } from '../../logger/sdkLogger'; import { Redis } from 'ioredis'; import { SplitIO } from '../../types'; -const log = logFactory('splitio-storage:redis'); +import { ILogger } from '../../logger/types'; + +const logPrefix = 'storage:redis: '; export default class EventsCacheInRedis implements IEventsCacheAsync { @@ -13,7 +14,7 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { private readonly metadata: IRedisMetadata; private readonly eventsKey: string; - constructor(keys: KeyBuilderSS, redis: Redis, metadata: IRedisMetadata) { + constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis, metadata: IRedisMetadata) { this.keys = keys; this.redis = redis; this.metadata = metadata; @@ -32,7 +33,7 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { // We use boolean values to signal successful queueing .then(() => true) .catch(err => { - log.error(`Error adding event to queue: ${err}.`); + this.log.error(logPrefix + `Error adding event to queue: ${err}.`); return false; }); } diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index a27f7b4f..7d0636bc 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -1,10 +1,11 @@ import ioredis from 'ioredis'; +import { ILogger } from '../../logger/types'; import { merge, isString } from '../../utils/lang'; import { _Set, setToArray, ISet } from '../../utils/lang/sets'; import thenable from '../../utils/promise/thenable'; import timeout from '../../utils/promise/timeout'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-storage:redis-adapter'); + +const logPrefix = 'storage:redis-adapter: '; // If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis. const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget']; @@ -32,15 +33,17 @@ interface IRedisCommand { * Redis adapter on top of the library of choice (written with ioredis) for some extra control. */ export default class RedisAdapter extends ioredis { + private readonly log: ILogger private _options: object; private _notReadyCommandsQueue?: IRedisCommand[]; private _runningCommands: ISet>; - constructor(storageSettings: Record) { + constructor(log: ILogger, storageSettings: Record) { const options = RedisAdapter._defineOptions(storageSettings); // Call the ioredis constructor super(...RedisAdapter._defineLibrarySettings(options)); + this.log = log; this._options = options; this._notReadyCommandsQueue = []; this._runningCommands = new _Set(); @@ -52,16 +55,16 @@ export default class RedisAdapter extends ioredis { _listenToEvents() { this.once('ready', () => { const commandsCount = this._notReadyCommandsQueue ? this._notReadyCommandsQueue.length : 0; - log.info(`Redis connection established. Queued commands: ${commandsCount}.`); + this.log.info(logPrefix + `Redis connection established. Queued commands: ${commandsCount}.`); this._notReadyCommandsQueue && this._notReadyCommandsQueue.forEach(queued => { - log.info(`Executing queued ${queued.name} command.`); + this.log.info(logPrefix + `Executing queued ${queued.name} command.`); queued.command().then(queued.resolve).catch(queued.reject); }); // After the SDK is ready for the first time we'll stop queueing commands. This is just so we can keep handling BUR for them. this._notReadyCommandsQueue = undefined; }); this.once('close', () => { - log.info('Redis connection closed.'); + this.log.info(logPrefix + 'Redis connection closed.'); }); } @@ -75,7 +78,7 @@ export default class RedisAdapter extends ioredis { const params = arguments; function commandWrapper() { - log.debug(`Executing ${method}.`); + instance.log.debug(logPrefix + `Executing ${method}.`); // Return original method const result = originalMethod.apply(instance, params); @@ -90,7 +93,7 @@ export default class RedisAdapter extends ioredis { result.then(cleanUpRunningCommandsCb, cleanUpRunningCommandsCb); return timeout(instance._options.operationTimeout, result).catch(err => { - log.error(`${method} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`); + instance.log.error(logPrefix + `${method} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`); // Handling is not the adapter responsibility. throw err; }); @@ -123,19 +126,19 @@ export default class RedisAdapter extends ioredis { setTimeout(function deferedDisconnect() { if (instance._runningCommands.size > 0) { - log.info(`Attempting to disconnect but there are ${instance._runningCommands.size} commands still waiting for resolution. Defering disconnection until those finish.`); + instance.log.info(logPrefix + `Attempting to disconnect but there are ${instance._runningCommands.size} commands still waiting for resolution. Defering disconnection until those finish.`); Promise.all(setToArray(instance._runningCommands)) .then(() => { - log.debug('Pending commands finished successfully, disconnecting.'); + instance.log.debug(logPrefix + 'Pending commands finished successfully, disconnecting.'); originalMethod.apply(instance, params); }) .catch(e => { - log.warn(`Pending commands finished with error: ${e}. Proceeding with disconnection.`); + instance.log.warn(logPrefix + `Pending commands finished with error: ${e}. Proceeding with disconnection.`); originalMethod.apply(instance, params); }); } else { - log.debug('No commands pending execution, disconnect.'); + instance.log.debug(logPrefix + 'No commands pending execution, disconnect.'); // Nothing pending, just proceed. originalMethod.apply(instance, params); } diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index bd5de621..c601b549 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -1,9 +1,10 @@ import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import KeyBuilderSS from '../KeyBuilderSS'; import { ISplitsCacheAsync } from '../types'; -import { logFactory } from '../../logger/sdkLogger'; import { Redis } from 'ioredis'; -const log = logFactory('splitio-storage:redis'); +import { ILogger } from '../../logger/types'; + +const logPrefix = 'storage:redis: '; /** * Discard errors for an answer of multiple operations. @@ -25,7 +26,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { private readonly keys: KeyBuilderSS; private redisError?: string; - constructor(keys: KeyBuilderSS, redis: Redis) { + constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis) { this.redis = redis; this.keys = keys; @@ -82,7 +83,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { */ getSplit(name: string): Promise { if (this.redisError) { - log.error(this.redisError); + this.log.error(logPrefix + this.redisError); throw this.redisError; } @@ -136,14 +137,14 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { .then((ttCount: string | null | number) => { ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { - log.info(`Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); + this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); return false; } return ttCount > 0; }) .catch(e => { - log.error(`Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); + this.log.error(logPrefix + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); // If there is an error, bypass the validation so the event can get tracked. return true; }); @@ -168,7 +169,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { */ getSplits(names: string[]): Promise> { if (this.redisError) { - log.error(this.redisError); + this.log.error(logPrefix + this.redisError); throw this.redisError; } @@ -182,7 +183,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { return Promise.resolve(splits); }) .catch(e => { - log.error(`Could not grab splits due to an error: ${e}.`); + this.log.error(logPrefix + `Could not grab splits due to an error: ${e}.`); return Promise.reject(e); }); } diff --git a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts index 6b146041..c4384726 100644 --- a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts @@ -1,6 +1,7 @@ import Redis from 'ioredis'; import find from 'lodash/find'; import isEqual from 'lodash/isEqual'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import KeyBuilderSS from '../../KeyBuilderSS'; import EventsCacheInRedis from '../EventsCacheInRedis'; import { metadataBuilder } from '../index'; @@ -26,11 +27,11 @@ test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async expect(redisValues.length).toBe(0); // control assertion, there are no events previously queued. - const cache = new EventsCacheInRedis(keys, connection, fakeRedisMetadata); + const cache = new EventsCacheInRedis(loggerMock, keys, connection, fakeRedisMetadata); // I'll use a "bad" instance so I can force an issue with the rpush command. I'll store an integer and will make the cache try to use rpush there. await connection.set('non-list-key', 10); // @ts-expect-error - const faultyCache = new EventsCacheInRedis({ + const faultyCache = new EventsCacheInRedis(loggerMock, { buildEventsKey: () => 'non-list-key' }, connection, fakeRedisMetadata); diff --git a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts index 93dcb55c..36892885 100644 --- a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts @@ -2,12 +2,13 @@ import Redis from '../RedisAdapter'; import KeyBuilderSS from '../../KeyBuilderSS'; import ImpressionsCacheInRedis from '../ImpressionsCacheInRedis'; import IORedis, { BooleanResponse } from 'ioredis'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('IMPRESSIONS CACHE IN REDIS / should incrementally store values', async () => { const prefix = 'impr_cache_ut'; const impressionsKey = `${prefix}.impressions`; const testMeta = { thisIsTheMeta: true }; - const connection = new Redis({}); + const connection = new Redis(loggerMock, {}); // @ts-expect-error const keys = new KeyBuilderSS(prefix); // @ts-expect-error const c = new ImpressionsCacheInRedis(keys, connection, testMeta); @@ -67,7 +68,7 @@ test('IMPRESSIONS CACHE IN REDIS / should not resolve track before calling expir const prefix = 'impr_cache_ut_2'; const impressionsKey = `${prefix}.impressions`; const testMeta = { thisIsTheMeta: true }; - const connection = new Redis({}); + const connection = new Redis(loggerMock, {}); // @ts-expect-error const keys = new KeyBuilderSS(prefix); // @ts-expect-error const c = new ImpressionsCacheInRedis(keys, connection, testMeta); @@ -95,9 +96,9 @@ test('IMPRESSIONS CACHE IN REDIS / should not resolve track before calling expir // @ts-expect-error c.track([i1, i2]).then(() => { connection.quit(); // Try to disconnect right away. - expect(spy1.mock.calls.length).not.toBe(0); // Redis rpush was called once before executing external callback. + expect(spy1).toBeCalled(); // Redis rpush was called once before executing external callback. // Following assertion fails if the expire takes place after disconnected and throws unhandledPromiseRejection - expect(spy2.mock.calls.length).not.toBe(0); // Redis expire was called once before executing external callback. + expect(spy2).toBeCalled(); // Redis expire was called once before executing external callback. }).catch(e => { throw new Error(`An error was generated from the redis expire tests: ${e}`); }).then(() => { diff --git a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts index 77f34a12..ba369d8c 100644 --- a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts +++ b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts @@ -5,7 +5,8 @@ import reduce from 'lodash/reduce'; import { _Set, setToArray } from '../../../utils/lang/sets'; // Mocking sdkLogger -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +const logPrefix = 'storage:redis-adapter: '; // Mocking ioredis @@ -55,336 +56,334 @@ import timeout from '../../../utils/promise/timeout'; import RedisAdapter from '../RedisAdapter'; function clearAllMocks() { - mockClear(); // clear logger mocks + loggerMock.mockClear(); ioredisMock.once.mockClear(); } -/** - * Logs here won't be changing much, so we could validate those. It's not important the exact message but what do they represent. - */ -test('STORAGE Redis Adapter / Class', () => { - expect(Object.getPrototypeOf(RedisAdapter)).toBe(ioredis); // The returned class extends from the library of choice (ioredis). +describe('STORAGE Redis Adapter', () => { - const instance = new RedisAdapter({ - url: 'redis://localhost:6379/0', - connectionTimeout: 10000, - operationTimeout: 10000 - }); + afterEach(clearAllMocks); - expect(instance instanceof RedisAdapter).toBe(true); // Of course created instance should be an instance of the adapter. - expect(instance instanceof ioredis).toBe(true); // And as the class extends from the library, the instance is an instance of the library as well. + /** + * Logs here won't be changing much, so we could validate those. It's not important the exact message but what do they represent. + */ + test('Class', () => { + expect(Object.getPrototypeOf(RedisAdapter)).toBe(ioredis); // The returned class extends from the library of choice (ioredis). - expect(typeof instance._options === 'object').toBe(true); // The instance will have an options object. - expect(Array.isArray(instance._notReadyCommandsQueue)).toBe(true); // The instance will have an array as the _notReadyCommandsQueue property. - expect(instance._runningCommands instanceof _Set).toBe(true); // The instance will have a set as the _runningCommands property. -}); + const instance = new RedisAdapter(loggerMock, { + url: 'redis://localhost:6379/0', + connectionTimeout: 10000, + operationTimeout: 10000 + }); -test('STORAGE Redis Adapter / ioredis constructor params and static method _defineLibrarySettings', () => { - const redisUrl = 'redis://localhost:6379/0'; - const redisParams = { - host: 'fake_host', port: '6355', 'db': 5, pass: 'fake_pass' - }; + expect(instance instanceof RedisAdapter).toBe(true); // Of course created instance should be an instance of the adapter. + expect(instance instanceof ioredis).toBe(true); // And as the class extends from the library, the instance is an instance of the library as well. - new RedisAdapter({ - url: redisUrl, - connectionTimeout: 123, - operationTimeout: 124 - }); - // Keep in mind we're storing the arguments object, not a true array. - expect(constructorParams.length).toBe(2); // In this signature, the constructor receives two params. - expect(constructorParams[0]).toBe(redisUrl); // When we use the Redis URL, that should be the first parameter passed to ioredis constructor - expect(constructorParams[1]).toEqual({ enableOfflineQueue: false, connectTimeout: 10000, lazyConnect: false }); // and the second parameter would be the default settings for the lib. - - new RedisAdapter({ - ...redisParams, - connectionTimeout: 123, - operationTimeout: 124 + expect(typeof instance._options === 'object').toBe(true); // The instance will have an options object. + expect(Array.isArray(instance._notReadyCommandsQueue)).toBe(true); // The instance will have an array as the _notReadyCommandsQueue property. + expect(instance._runningCommands instanceof _Set).toBe(true); // The instance will have a set as the _runningCommands property. }); - expect(constructorParams.length).toBe(1); // In this signature, the constructor receives one param. - // we keep "pass" instead of "password" on our settings API to be backwards compatible. - expect(constructorParams[0]).toEqual({ host: redisParams.host, port: redisParams.port, db: redisParams.db, password: redisParams.pass, enableOfflineQueue: false, connectTimeout: 10000, lazyConnect: false }); // If we send all the redis params separate, it will pass one object to the library containing that and the rest of the options. - - new RedisAdapter({ - ...redisParams, - url: redisUrl, - connectionTimeout: 123, - operationTimeout: 124 + test('ioredis constructor params and static method _defineLibrarySettings', () => { + const redisUrl = 'redis://localhost:6379/0'; + const redisParams = { + host: 'fake_host', port: '6355', 'db': 5, pass: 'fake_pass' + }; + + new RedisAdapter(loggerMock, { + url: redisUrl, + connectionTimeout: 123, + operationTimeout: 124 + }); + // Keep in mind we're storing the arguments object, not a true array. + expect(constructorParams.length).toBe(2); // In this signature, the constructor receives two params. + expect(constructorParams[0]).toBe(redisUrl); // When we use the Redis URL, that should be the first parameter passed to ioredis constructor + expect(constructorParams[1]).toEqual({ enableOfflineQueue: false, connectTimeout: 10000, lazyConnect: false }); // and the second parameter would be the default settings for the lib. + + new RedisAdapter(loggerMock, { + ...redisParams, + connectionTimeout: 123, + operationTimeout: 124 + }); + + expect(constructorParams.length).toBe(1); // In this signature, the constructor receives one param. + // we keep "pass" instead of "password" on our settings API to be backwards compatible. + expect(constructorParams[0]).toEqual({ host: redisParams.host, port: redisParams.port, db: redisParams.db, password: redisParams.pass, enableOfflineQueue: false, connectTimeout: 10000, lazyConnect: false }); // If we send all the redis params separate, it will pass one object to the library containing that and the rest of the options. + + new RedisAdapter(loggerMock, { + ...redisParams, + url: redisUrl, + connectionTimeout: 123, + operationTimeout: 124 + }); + + expect(constructorParams.length).toBe(2); // In this signature, the constructor receives two params. + expect(constructorParams[0]).toBe(redisUrl); // When we use the Redis URL, even if we specified all the other params one by one the URL takes precedence, so that should be the first parameter passed to ioredis constructor + expect(constructorParams[1]).toEqual({ enableOfflineQueue: false, connectTimeout: 10000, lazyConnect: false }); // and the second parameter would be the default settings for the lib. }); - expect(constructorParams.length).toBe(2); // In this signature, the constructor receives two params. - expect(constructorParams[0]).toBe(redisUrl); // When we use the Redis URL, even if we specified all the other params one by one the URL takes precedence, so that should be the first parameter passed to ioredis constructor - expect(constructorParams[1]).toEqual({ enableOfflineQueue: false, connectTimeout: 10000, lazyConnect: false }); // and the second parameter would be the default settings for the lib. -}); - -test('STORAGE Redis Adapter / static method - _defineOptions', () => { - const defaultOptions = { - connectionTimeout: 10000, - operationTimeout: 5000 - }; - - expect(RedisAdapter._defineOptions({})).toEqual(defaultOptions); // We get the default options if we use an empty object. - - expect(RedisAdapter._defineOptions({ - url: 'redis_url' - })).toEqual({ - connectionTimeout: 10000, - operationTimeout: 5000, - url: 'redis_url' - }); // We get the merge of the provided and the default options. - - const opts = { - host: 'host', port: 'port', db: 'db', pass: 'pass' - }; - - expect(RedisAdapter._defineOptions(opts)).not.toEqual(opts); // Provided options are not mutated. - expect(opts).toEqual({ host: 'host', port: 'port', db: 'db', pass: 'pass' }); // Provided options are not mutated. - - expect(RedisAdapter._defineOptions(opts)).toEqual(merge({}, defaultOptions, opts)); // We get the merge of the provided and the default options. - - expect(RedisAdapter._defineOptions({ - random: 1, - crap: 'I do not think I can make it', - secret: 'shh', - url: 'I will make it' - })).toEqual(merge({}, defaultOptions, { url: 'I will make it' })); // Unwanted options will be skipped. -}); - -test('STORAGE Redis Adapter / instance methods - _listenToEvents', (done) => { - // Reset all stubs - clearAllMocks(); - - expect(ioredisMock.once.mock.calls.length).toBe(0); // Control assertion - expect(ioredisMock[METHODS_TO_PROMISE_WRAP[0]].mock.calls.length).toBe(0); // Control assertion - - const instance = new RedisAdapter({ - url: 'redis://localhost:6379/0' + test('static method - _defineOptions', () => { + const defaultOptions = { + connectionTimeout: 10000, + operationTimeout: 5000 + }; + + expect(RedisAdapter._defineOptions({})).toEqual(defaultOptions); // We get the default options if we use an empty object. + + expect(RedisAdapter._defineOptions({ + url: 'redis_url' + })).toEqual({ + connectionTimeout: 10000, + operationTimeout: 5000, + url: 'redis_url' + }); // We get the merge of the provided and the default options. + + const opts = { + host: 'host', port: 'port', db: 'db', pass: 'pass' + }; + + expect(RedisAdapter._defineOptions(opts)).not.toEqual(opts); // Provided options are not mutated. + expect(opts).toEqual({ host: 'host', port: 'port', db: 'db', pass: 'pass' }); // Provided options are not mutated. + + expect(RedisAdapter._defineOptions(opts)).toEqual(merge({}, defaultOptions, opts)); // We get the merge of the provided and the default options. + + expect(RedisAdapter._defineOptions({ + random: 1, + crap: 'I do not think I can make it', + secret: 'shh', + url: 'I will make it' + })).toEqual(merge({}, defaultOptions, { url: 'I will make it' })); // Unwanted options will be skipped. }); - expect(ioredisMock.once.mock.calls.length).toBe(2); // If the method was called, it should have called the `once` function twice. If that it the case we can assume that the method was called on creation. + test('instance methods - _listenToEvents', (done) => { + expect(ioredisMock.once).not.toBeCalled(); // Control assertion + expect(ioredisMock[METHODS_TO_PROMISE_WRAP[0]]).not.toBeCalled(); // Control assertion - // Reset stubs again, we'll check the behaviour calling the method directly. - clearAllMocks(); - expect(ioredisMock.once.mock.calls.length).toBe(0); // Control assertion - expect(ioredisMock[METHODS_TO_PROMISE_WRAP[METHODS_TO_PROMISE_WRAP.length - 1]].mock.calls.length).toBe(0); // Control assertion + const instance = new RedisAdapter(loggerMock, { + url: 'redis://localhost:6379/0' + }); - instance._listenToEvents(); + expect(ioredisMock.once).toBeCalledTimes(2); // If the method was called, it should have called the `once` function twice. If that it the case we can assume that the method was called on creation. - expect(ioredisMock.once.mock.calls.length).toBe(2); // The "once" method of the instance should be called twice. + // Reset stubs again, we'll check the behaviour calling the method directly. + clearAllMocks(); + expect(ioredisMock.once).not.toBeCalled(); // Control assertion + expect(ioredisMock[METHODS_TO_PROMISE_WRAP[METHODS_TO_PROMISE_WRAP.length - 1]]).not.toBeCalled(); // Control assertion - const firstCallArgs = ioredisMock.once.mock.calls[0]; + instance._listenToEvents(); - expect(firstCallArgs[0]).toBe('ready'); // First argument for the first call should be the "ready" event. - expect(typeof firstCallArgs[1]).toBe('function'); // second argument for the first call should be a callback function. + expect(ioredisMock.once).toBeCalledTimes(2); // The "once" method of the instance should be called twice. - const secondCallArgs = ioredisMock.once.mock.calls[1]; + const firstCallArgs = ioredisMock.once.mock.calls[0]; - expect(secondCallArgs[0]).toBe('close'); // First argument for the first call should be the "close" event. - expect(typeof secondCallArgs[1]).toBe('function'); // second argument for the first call should be a callback function. + expect(firstCallArgs[0]).toBe('ready'); // First argument for the first call should be the "ready" event. + expect(typeof firstCallArgs[1]).toBe('function'); // second argument for the first call should be a callback function. - expect(loggerMock.warn.mock.calls.length).toBe(0); // Control assertion - secondCallArgs[1](); // Execute the callback for "close" + const secondCallArgs = ioredisMock.once.mock.calls[1]; - expect(loggerMock.info.mock.calls.length).toBe(1); // The callback for the "close" event will only log info to the user about what is going on. - expect(loggerMock.info.mock.calls[0]).toEqual(['Redis connection closed.']); // The callback for the "close" event will only log info to the user about what is going on. + expect(secondCallArgs[0]).toBe('close'); // First argument for the first call should be the "close" event. + expect(typeof secondCallArgs[1]).toBe('function'); // second argument for the first call should be a callback function. - loggerMock.info.mockClear(); - expect(loggerMock.info.mock.calls.length).toBe(0); // Control assertion - expect(Array.isArray(instance._notReadyCommandsQueue)).toBe(true); // Control assertion + expect(loggerMock.warn).not.toBeCalled(); // Control assertion + secondCallArgs[1](); // Execute the callback for "close" - // Without any offline commands queued, execute the callback for "ready" - firstCallArgs[1](); + expect(loggerMock.info).toBeCalledTimes(1); // The callback for the "close" event will only log info to the user about what is going on. + expect(loggerMock.info.mock.calls[0]).toEqual([logPrefix + 'Redis connection closed.']); // The callback for the "close" event will only log info to the user about what is going on. - expect(loggerMock.info.mock.calls.length).not.toBe(0); // The callback for the "ready" event will inform the user about the trigger. - expect(loggerMock.info.mock.calls[0]).toEqual(['Redis connection established. Queued commands: 0.']); // The callback for the "ready" event will inform the user about the trigger. - expect(instance._notReadyCommandsQueue).toBe(undefined); // After the DB is ready, it will clean up the offline commands queue so we do not queue commands anymore. + loggerMock.info.mockClear(); + expect(loggerMock.info).not.toBeCalled(); // Control assertion + expect(Array.isArray(instance._notReadyCommandsQueue)).toBe(true); // Control assertion - // Don't do this at home - const queuedGetCommand = { - command: jest.fn(() => Promise.resolve()), - name: 'GET', - resolve: jest.fn(), - reject: jest.fn() - }; - const queuedSetCommand = { - command: jest.fn(() => Promise.reject()), - name: 'SET', - resolve: jest.fn(), - reject: jest.fn() - }; - instance._notReadyCommandsQueue = [queuedGetCommand, queuedSetCommand]; - loggerMock.info.mockClear(); + // Without any offline commands queued, execute the callback for "ready" + firstCallArgs[1](); - // execute the callback for "ready" once more - firstCallArgs[1](); + expect(loggerMock.info).toBeCalledTimes(1); // The callback for the "ready" event will inform the user about the trigger. + expect(loggerMock.info).toBeCalledWith(logPrefix + 'Redis connection established. Queued commands: 0.'); // The callback for the "ready" event will inform the user about the trigger. + expect(instance._notReadyCommandsQueue).toBe(undefined); // After the DB is ready, it will clean up the offline commands queue so we do not queue commands anymore. - expect(loggerMock.info.mock.calls.length).toBe(3); // If we had queued commands, it will log the event (1 call) as well as each executed command (n calls). - expect(loggerMock.info.mock.calls).toEqual([ - ['Redis connection established. Queued commands: 2.'], // The callback for the "ready" event will inform the user about the trigger and the amount of queued commands. - ['Executing queued GET command.'], // If we had queued, it will log the event as well as each executed command. - ['Executing queued SET command.'] // If we had queued commands, it will log the event as well as each executed command. - ]); - expect(queuedGetCommand.command.mock.calls.length).toBe(1); // It will execute each queued command. + // Don't do this at home + const queuedGetCommand = { + command: jest.fn(() => Promise.resolve()), + name: 'GET', + resolve: jest.fn(), + reject: jest.fn() + }; + const queuedSetCommand = { + command: jest.fn(() => Promise.reject()), + name: 'SET', + resolve: jest.fn(), + reject: jest.fn() + }; + instance._notReadyCommandsQueue = [queuedGetCommand, queuedSetCommand]; + loggerMock.info.mockClear(); - setTimeout(() => { // Remember this is tied to a promise. - expect(queuedGetCommand.resolve.mock.calls.length).not.toBe(0); // And depending on what happens with the command promise, it will call the resolve or reject function for the promise wrapper. - expect(queuedGetCommand.reject.mock.calls.length).toBe(0); // And depending on what happens with the command promise, it will call the resolve or reject function for the promise wrapper. - expect(queuedSetCommand.reject.mock.calls.length).not.toBe(0); // And depending on what happens with the command promise, it will call the resolve or reject function for the promise wrapper. - expect(queuedSetCommand.resolve.mock.calls.length).toBe(0); // And depending on what happens with the command promise, it will call the resolve or reject function for the promise wrapper. + // execute the callback for "ready" once more + firstCallArgs[1](); - done(); - }, 5); -}); + expect(loggerMock.info).toBeCalledTimes(3); // If we had queued commands, it will log the event (1 call) as well as each executed command (n calls). + expect(loggerMock.info.mock.calls).toEqual([ + [logPrefix + 'Redis connection established. Queued commands: 2.'], // The callback for the "ready" event will inform the user about the trigger and the amount of queued commands. + [logPrefix + 'Executing queued GET command.'], // If we had queued, it will log the event as well as each executed command. + [logPrefix + 'Executing queued SET command.'] // If we had queued commands, it will log the event as well as each executed command. + ]); + expect(queuedGetCommand.command).toBeCalledTimes(1); // It will execute each queued command. -test('STORAGE Redis Adapter / instance methods - _setTimeoutWrappers and queueing commands 1/2 - Error path', (done) => { - clearAllMocks(); + setTimeout(() => { // Remember this is tied to a promise. + expect(queuedGetCommand.resolve).toBeCalled(); // And depending on what happens with the command promise, it will call the resolve or reject function for the promise wrapper. + expect(queuedGetCommand.reject).not.toBeCalled(); // And depending on what happens with the command promise, it will call the resolve or reject function for the promise wrapper. + expect(queuedSetCommand.reject).toBeCalled(); // And depending on what happens with the command promise, it will call the resolve or reject function for the promise wrapper. + expect(queuedSetCommand.resolve).not.toBeCalled(); // And depending on what happens with the command promise, it will call the resolve or reject function for the promise wrapper. - const instance = new RedisAdapter({ - url: 'redis://localhost:6379/0' + done(); + }, 5); }); - forEach(METHODS_TO_PROMISE_WRAP, methodName => { - expect(instance[methodName]).not.toBe(ioredisMock[methodName]); // Method "${methodName}" from ioredis library should be wrapped. - expect(ioredisMock[methodName].mock.calls.length).toBe(0); // Checking that the method was not called yet. - - const startingQueueLength = instance._notReadyCommandsQueue.length; + test('instance methods - _setTimeoutWrappers and queueing commands 1/2 - Error path', (done) => { - // We do have the commands queue on this state, so a call for this methods will queue the command. - const wrapperResult = instance[methodName](methodName); - expect(wrapperResult instanceof Promise).toBe(true); // The result is a promise since we are queueing commands on this state. + const instance = new RedisAdapter(loggerMock, { + url: 'redis://localhost:6379/0' + }); - expect(instance._notReadyCommandsQueue.length).toBe(startingQueueLength + 1); // The queue should have one more item. - const queuedCommand = instance._notReadyCommandsQueue[0]; + forEach(METHODS_TO_PROMISE_WRAP, methodName => { + expect(instance[methodName]).not.toBe(ioredisMock[methodName]); // Method "${methodName}" from ioredis library should be wrapped. + expect(ioredisMock[methodName]).not.toBeCalled(); // Checking that the method was not called yet. - expect(typeof queuedCommand.resolve).toBe('function'); // The queued item should have the correct form. - expect(typeof queuedCommand.reject).toBe('function'); // The queued item should have the correct form. - expect(typeof queuedCommand.command).toBe('function'); // The queued item should have the correct form. - expect(queuedCommand.name).toBe(methodName.toUpperCase()); // The queued item should have the correct form. - }); - - instance._notReadyCommandsQueue = false; // Remove the queue. - loggerMock.error.resetHistory; - - forEach(METHODS_TO_PROMISE_WRAP, (methodName, index) => { - // We do NOT have the commands queue on this state, so a call for this methods will execute the command. - expect(ioredisMock[methodName].mock.calls.length).toBe(0); // Control assertion - Original method (${methodName}) was not called yet + const startingQueueLength = instance._notReadyCommandsQueue.length; - const previousTimeoutCalls = timeout.mock.calls.length; - let previousRunningCommandsSize = instance._runningCommands.size; - instance[methodName](methodName).catch(() => { }); // Swallow exception so it's not spread to logs. - expect(ioredisMock[methodName].mock.calls.length).not.toBe(0); // Original method (${methodName}) is called right away (through wrapper) when we are not queueing anymore. - expect(instance._runningCommands.size).toBe(previousRunningCommandsSize + 1); // If the result of the operation was a thenable it will add the item to the running commands queue. + // We do have the commands queue on this state, so a call for this methods will queue the command. + const wrapperResult = instance[methodName](methodName); + expect(wrapperResult instanceof Promise).toBe(true); // The result is a promise since we are queueing commands on this state. - expect(timeout.mock.calls.length).toBe(previousTimeoutCalls + 1); // The promise returned by the original method should have a timeout wrapper. + expect(instance._notReadyCommandsQueue.length).toBe(startingQueueLength + 1); // The queue should have one more item. + const queuedCommand = instance._notReadyCommandsQueue[0]; - // Get the original promise (the one passed to timeout) - const commandTimeoutResolver = timeoutPromiseResolvers[0]; + expect(typeof queuedCommand.resolve).toBe('function'); // The queued item should have the correct form. + expect(typeof queuedCommand.reject).toBe('function'); // The queued item should have the correct form. + expect(typeof queuedCommand.command).toBe('function'); // The queued item should have the correct form. + expect(queuedCommand.name).toBe(methodName.toUpperCase()); // The queued item should have the correct form. + }); - expect(timeout.mock.calls[0]).toEqual([5000, commandTimeoutResolver.originalPromise]); // Timeout function should have received the correct ms amount and the right promise. - expect(instance._runningCommands.has(commandTimeoutResolver.originalPromise)).toBe(true); // Correct promise should be the one on the _runningCommands queue. + instance._notReadyCommandsQueue = false; // Remove the queue. + loggerMock.error.resetHistory; - commandTimeoutResolver.rej('test'); - setTimeout(() => { // Allow the promises to tick. - expect(instance._runningCommands.has(commandTimeoutResolver.originalPromise)).toBe(false); // After a command finishes with error, it's promise is removed from the instance._runningCommands queue. - expect(loggerMock.error.mock.calls[index]).toEqual([`${methodName} operation threw an error or exceeded configured timeout of 5000ms. Message: test`]); // The log error method should be called with the corresponding messages, depending on the method, error and operationTimeout. - }, 0); - }); + forEach(METHODS_TO_PROMISE_WRAP, (methodName, index) => { + // We do NOT have the commands queue on this state, so a call for this methods will execute the command. + expect(ioredisMock[methodName]).not.toBeCalled(); // Control assertion - Original method (${methodName}) was not called yet - setTimeout(() => { - done(); - }, 200); -}); + const previousTimeoutCalls = timeout.mock.calls.length; + let previousRunningCommandsSize = instance._runningCommands.size; + instance[methodName](methodName).catch(() => { }); // Swallow exception so it's not spread to logs. + expect(ioredisMock[methodName]).toBeCalled(); // Original method (${methodName}) is called right away (through wrapper) when we are not queueing anymore. + expect(instance._runningCommands.size).toBe(previousRunningCommandsSize + 1); // If the result of the operation was a thenable it will add the item to the running commands queue. -test('STORAGE Redis Adapter / instance methods - _setTimeoutWrappers and queueing commands 2/2 - Success path', (done) => { - clearAllMocks(); + expect(timeout).toBeCalledTimes(previousTimeoutCalls + 1); // The promise returned by the original method should have a timeout wrapper. - const instance = new RedisAdapter({ - url: 'redis://localhost:6379/0' - }); + // Get the original promise (the one passed to timeout) + const commandTimeoutResolver = timeoutPromiseResolvers[0]; - instance._notReadyCommandsQueue = false; // Connection is "ready" + expect(timeout.mock.calls[0]).toEqual([5000, commandTimeoutResolver.originalPromise]); // Timeout function should have received the correct ms amount and the right promise. + expect(instance._runningCommands.has(commandTimeoutResolver.originalPromise)).toBe(true); // Correct promise should be the one on the _runningCommands queue. - forEach(METHODS_TO_PROMISE_WRAP, methodName => { - // Just call the wrapped method, we don't care about all the paths tested on the previous case, just how it behaves when the command is resolved. - instance[methodName](methodName); - // Get the original promise (the one passed to timeout) - const commandTimeoutResolver = timeoutPromiseResolvers[0]; + commandTimeoutResolver.rej('test'); + setTimeout(() => { // Allow the promises to tick. + expect(instance._runningCommands.has(commandTimeoutResolver.originalPromise)).toBe(false); // After a command finishes with error, it's promise is removed from the instance._runningCommands queue. + expect(loggerMock.error.mock.calls[index]).toEqual([`${logPrefix}${methodName} operation threw an error or exceeded configured timeout of 5000ms. Message: test`]); // The log error method should be called with the corresponding messages, depending on the method, error and operationTimeout. + }, 0); + }); - commandTimeoutResolver.res('test'); - setTimeout(() => { // Allow the promises to tick. - expect(loggerMock.error.mock.calls.length).toBe(0); // No error should be logged - expect(instance._runningCommands.has(commandTimeoutResolver.originalPromise)).toBe(false); // After a command finishes successfully, it's promise is removed from the instance._runningCommands queue. - }, 0); + setTimeout(() => { + done(); + }, 200); }); - setTimeout(() => { - done(); - }, 200); -}); - -test('STORAGE Redis Adapter / instance methods - _setDisconnectWrapper', (done) => { - clearAllMocks(); - - const instance = new RedisAdapter({ - url: 'redis://localhost:6379/0' + test('instance methods - _setTimeoutWrappers and queueing commands 2/2 - Success path', (done) => { + const instance = new RedisAdapter(loggerMock, { + url: 'redis://localhost:6379/0' + }); + + instance._notReadyCommandsQueue = false; // Connection is "ready" + + forEach(METHODS_TO_PROMISE_WRAP, methodName => { + // Just call the wrapped method, we don't care about all the paths tested on the previous case, just how it behaves when the command is resolved. + instance[methodName](methodName); + // Get the original promise (the one passed to timeout) + const commandTimeoutResolver = timeoutPromiseResolvers[0]; + + commandTimeoutResolver.res('test'); + setTimeout(() => { // Allow the promises to tick. + expect(loggerMock.error).not.toBeCalled(); // No error should be logged + expect(instance._runningCommands.has(commandTimeoutResolver.originalPromise)).toBe(false); // After a command finishes successfully, it's promise is removed from the instance._runningCommands queue. + }, 0); + }); + + setTimeout(() => { + done(); + }, 200); }); - expect(instance.disconnect).not.toBe(ioredisMock.disconnect); // disconnect() method from redis library should be wrapped. + test('instance methods - _setDisconnectWrapper', (done) => { + const instance = new RedisAdapter(loggerMock, { + url: 'redis://localhost:6379/0' + }); - // Call the method. - // Note that there are no commands on the queue for this first run. - instance.disconnect(); - expect(ioredisMock.disconnect.mock.calls.length).toBe(0); // Original method should not be called right away. + expect(instance.disconnect).not.toBe(ioredisMock.disconnect); // disconnect() method from redis library should be wrapped. - setTimeout(() => { // o queued commands timeout wrapper. - expect(loggerMock.debug.mock.calls).toEqual([['No commands pending execution, disconnect.']]); - expect(ioredisMock.disconnect.mock.calls.length).toBe(1); // Original method should have been called once, asynchronously - loggerMock.debug.mockClear(); - ioredisMock.disconnect.mockClear(); - - // Second run, two pending commands, one will fail. - instance._runningCommands.add(Promise.resolve()); + // Call the method. + // Note that there are no commands on the queue for this first run. instance.disconnect(); - const rejectedPromise = Promise.reject('test-error'); - instance._runningCommands.add(rejectedPromise); - rejectedPromise.catch(() => { }); // Swallow the unhandled to avoid unhandledRejection warns - - setTimeout(() => { // queued with rejection timeout wrapper - expect(loggerMock.info.mock.calls).toEqual([['Attempting to disconnect but there are 2 commands still waiting for resolution. Defering disconnection until those finish.']]); - - Promise.all(setToArray(instance._runningCommands)).catch(e => { - setImmediate(() => { // Allow the callback to execute before checking. - expect(loggerMock.warn.mock.calls[0]).toEqual([`Pending commands finished with error: ${e}. Proceeding with disconnection.`]); // Should warn about the error but tell user that will disconnect anyways. - expect(ioredisMock.disconnect.mock.calls.length).toBe(1); // Original method should have been called once, asynchronously - - loggerMock.info.mockClear(); - loggerMock.warn.mockClear(); - ioredisMock.disconnect.mockClear(); - - // Third run, pending commands all successful - instance._runningCommands.clear(); - instance._runningCommands.add(Promise.resolve()); - instance._runningCommands.add(Promise.resolve()); - instance.disconnect(); - instance._runningCommands.add(Promise.resolve()); - instance._runningCommands.add(Promise.resolve()); - - setTimeout(() => { - expect(loggerMock.info.mock.calls).toEqual([['Attempting to disconnect but there are 4 commands still waiting for resolution. Defering disconnection until those finish.']]); - - Promise.all(setToArray(instance._runningCommands)).then(() => { // This one will go through success path - setImmediate(() => { - expect(loggerMock.debug.mock.calls).toEqual([['Pending commands finished successfully, disconnecting.']]); - expect(ioredisMock.disconnect.mock.calls.length).toBe(1); // Original method should have been called once, asynchronously + expect(ioredisMock.disconnect).not.toBeCalled(); // Original method should not be called right away. + + setTimeout(() => { // o queued commands timeout wrapper. + expect(loggerMock.debug.mock.calls).toEqual([[logPrefix + 'No commands pending execution, disconnect.']]); + expect(ioredisMock.disconnect).toBeCalledTimes(1); // Original method should have been called once, asynchronously + loggerMock.debug.mockClear(); + ioredisMock.disconnect.mockClear(); + + // Second run, two pending commands, one will fail. + instance._runningCommands.add(Promise.resolve()); + instance.disconnect(); + const rejectedPromise = Promise.reject('test-error'); + instance._runningCommands.add(rejectedPromise); + rejectedPromise.catch(() => { }); // Swallow the unhandled to avoid unhandledRejection warns + + setTimeout(() => { // queued with rejection timeout wrapper + expect(loggerMock.info.mock.calls).toEqual([[logPrefix + 'Attempting to disconnect but there are 2 commands still waiting for resolution. Defering disconnection until those finish.']]); + + Promise.all(setToArray(instance._runningCommands)).catch(e => { + setImmediate(() => { // Allow the callback to execute before checking. + expect(loggerMock.warn.mock.calls[0]).toEqual([`${logPrefix}Pending commands finished with error: ${e}. Proceeding with disconnection.`]); // Should warn about the error but tell user that will disconnect anyways. + expect(ioredisMock.disconnect).toBeCalledTimes(1); // Original method should have been called once, asynchronously + + loggerMock.info.mockClear(); + loggerMock.warn.mockClear(); + ioredisMock.disconnect.mockClear(); + + // Third run, pending commands all successful + instance._runningCommands.clear(); + instance._runningCommands.add(Promise.resolve()); + instance._runningCommands.add(Promise.resolve()); + instance.disconnect(); + instance._runningCommands.add(Promise.resolve()); + instance._runningCommands.add(Promise.resolve()); + + setTimeout(() => { + expect(loggerMock.info.mock.calls).toEqual([[logPrefix + 'Attempting to disconnect but there are 4 commands still waiting for resolution. Defering disconnection until those finish.']]); + + Promise.all(setToArray(instance._runningCommands)).then(() => { // This one will go through success path + setImmediate(() => { + expect(loggerMock.debug.mock.calls).toEqual([[logPrefix + 'Pending commands finished successfully, disconnecting.']]); + expect(ioredisMock.disconnect).toBeCalledTimes(1); // Original method should have been called once, asynchronously + }); }); - }); - }, 10); + }, 10); + }); }); - }); + }, 10); }, 10); - }, 10); - setTimeout(() => { - done(); - }, 400); + setTimeout(() => { + done(); + }, 400); + }); + }); diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index bcaf990b..1c03268e 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -1,12 +1,13 @@ import Redis from 'ioredis'; import SplitsCacheInRedis from '../SplitsCacheInRedis'; import KeyBuilderSS from '../../KeyBuilderSS'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('SPLITS CACHE / Redis', async () => { const connection = new Redis(); // @ts-expect-error const keys = new KeyBuilderSS(); - const cache = new SplitsCacheInRedis(keys, connection); + const cache = new SplitsCacheInRedis(loggerMock, keys, connection); await cache.clear(); @@ -55,7 +56,7 @@ test('SPLITS CACHE / Redis / trafficTypeExists tests', async () => { const connection = new Redis(); // @ts-expect-error const keys = new KeyBuilderSS(prefix); - const cache = new SplitsCacheInRedis(keys, connection); + const cache = new SplitsCacheInRedis(loggerMock, keys, connection); const testTTName = 'tt_test_name'; const testTTNameNoCount = 'tt_test_name_2'; diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 4c150b75..52093236 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -35,9 +35,10 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { return function InRedisStorageFactory(params: IStorageFactoryParams): IStorageAsync { + const log = params.log; const metadata = metadataBuilder(params.metadata); const keys = new KeyBuilderSS(prefix, { version: metadata.s, ip: metadata.i, hostname: metadata.n }); - const redisClient = new RedisAdapter(options.options || {}); + const redisClient = new RedisAdapter(log, options.options || {}); // subscription to Redis connect event in order to emit SDK_READY event // @TODO pass a callback to simplify custom storages @@ -47,10 +48,10 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { }); return { - splits: new SplitsCacheInRedis(keys, redisClient), + splits: new SplitsCacheInRedis(log, keys, redisClient), segments: new SegmentsCacheInRedis(keys, redisClient), impressions: new ImpressionsCacheInRedis(keys, redisClient, metadata), - events: new EventsCacheInRedis(keys, redisClient, metadata), + events: new EventsCacheInRedis(log, keys, redisClient, metadata), latencies: new LatenciesCacheInRedis(keys, redisClient), counts: new CountsCacheInRedis(keys, redisClient), diff --git a/src/storages/types.ts b/src/storages/types.ts index 2925e7cc..c4b94ad2 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,4 +1,5 @@ import { MaybeThenable, IMetadata, ISplitFiltersValidation } from '../dtos/types'; +import { ILogger } from '../logger/types'; import { IReadinessManager } from '../readiness/types'; import { SplitIO, ImpressionDTO } from '../types'; @@ -236,6 +237,7 @@ export type IStorageAsync = IStorageBase< export type DataLoader = (storage: IStorageSync, matchingKey: string) => void export interface IStorageFactoryParams { + log: ILogger, eventsQueueSize: number, optimize: boolean /* whether create the `impressionCounts` cache (OPTIMIZED impression mode) or not (DEBUG impression mode) */, dataLoader?: DataLoader, @@ -246,5 +248,5 @@ export interface IStorageFactoryParams { // ATM, only used by InRedisStorage. @TODO pass a callback to simplify custom storages. readinessManager: IReadinessManager, - metadata: IMetadata + metadata: IMetadata, } diff --git a/src/sync/offline/splitsParser/splitsParserFromFile.ts b/src/sync/offline/splitsParser/splitsParserFromFile.ts index 9d5bf70d..b56052c6 100644 --- a/src/sync/offline/splitsParser/splitsParserFromFile.ts +++ b/src/sync/offline/splitsParser/splitsParserFromFile.ts @@ -4,12 +4,13 @@ import fs from 'fs'; import path from 'path'; // @ts-ignore import yaml from 'js-yaml'; -import { logFactory } from '../../../logger/sdkLogger'; import { isString, endsWith, find, forOwn, uniq, } from '../../../utils/lang'; import parseCondition, { IMockSplitEntry } from './parseCondition'; import { ISplitPartial } from '../../../dtos/types'; import { SplitIO } from '../../../types'; -const log = logFactory('splitio-offline:splits-fetcher'); +import { ILogger } from '../../../logger/types'; + +const logPrefix = 'sync:offline:splits-fetcher: '; type IYamlSplitEntry = Record @@ -30,16 +31,16 @@ function configFilesPath(configFilePath?: SplitIO.MockedFeaturesFilePath): Split // Validate the extensions if (!(endsWith(configFilePath, '.yaml', true) || endsWith(configFilePath, '.yml', true) || endsWith(configFilePath, '.split', true))) - throw `Invalid extension specified for Splits mock file. Accepted extensions are ".yml" and ".yaml". Your specified file is ${configFilePath}`; + throw new Error(`Invalid extension specified for Splits mock file. Accepted extensions are ".yml" and ".yaml". Your specified file is ${configFilePath}`); if (!fs.existsSync(configFilePath as SplitIO.MockedFeaturesFilePath)) - throw `Split configuration not found in ${configFilePath} - Please review your Split file location.`; + throw new Error(`Split configuration not found in ${configFilePath} - Please review your Split file location.`); return configFilePath as SplitIO.MockedFeaturesFilePath; } // Parse `.split` configuration file and return a map of "Split Objects" -function readSplitConfigFile(filePath: SplitIO.MockedFeaturesFilePath): false | Record { +function readSplitConfigFile(log: ILogger, filePath: SplitIO.MockedFeaturesFilePath): false | Record { const SPLIT_POSITION = 0; const TREATMENT_POSITION = 1; let data; @@ -59,12 +60,12 @@ function readSplitConfigFile(filePath: SplitIO.MockedFeaturesFilePath): false | let tuple: string | string[] = line.trim(); if (tuple === '' || tuple.charAt(0) === '#') { - log.debug(`Ignoring empty line or comment at #${index}`); + log.debug(logPrefix + `Ignoring empty line or comment at #${index}`); } else { tuple = tuple.split(/\s+/); if (tuple.length !== 2) { - log.debug(`Ignoring line since it does not have exactly two columns #${index}`); + log.debug(logPrefix + `Ignoring line since it does not have exactly two columns #${index}`); } else { const splitName = tuple[SPLIT_POSITION]; const condition = parseCondition({ treatment: tuple[TREATMENT_POSITION] }); @@ -79,7 +80,7 @@ function readSplitConfigFile(filePath: SplitIO.MockedFeaturesFilePath): false | } // Parse `.yml` or `.yaml` configuration files and return a map of "Split Objects" -function readYAMLConfigFile(filePath: SplitIO.MockedFeaturesFilePath): false | Record { +function readYAMLConfigFile(log: ILogger, filePath: SplitIO.MockedFeaturesFilePath): false | Record { let data = ''; let yamldoc = null; @@ -101,7 +102,7 @@ function readYAMLConfigFile(filePath: SplitIO.MockedFeaturesFilePath): false | R const splitName = Object.keys(splitEntry)[0]; if (!splitName || !isString(splitEntry[splitName].treatment)) - log.error('Ignoring entry on YAML since the format is incorrect.'); + log.error(logPrefix + 'Ignoring entry on YAML since the format is incorrect.'); const mockData = splitEntry[splitName]; @@ -160,16 +161,16 @@ function arrangeConditions(mocksData: Record & { } // Load the content of a configuration file into an Object -export default function splitsParserFromFile(settings: { features?: SplitIO.MockedFeaturesFilePath }): false | Record { - const filePath = configFilesPath(settings.features); +export default function splitsParserFromFile({ features, log }: { features?: SplitIO.MockedFeaturesFilePath, log: ILogger }): false | Record { + const filePath = configFilesPath(features); let mockData: false | Record; // If we have a filePath, it means the extension is correct, choose the parser. if (endsWith(filePath, '.split')) { - log.warn('.split mocks will be deprecated soon in favor of YAML files, which provide more targeting power. Take a look in our documentation.'); - mockData = readSplitConfigFile(filePath); + log.warn(logPrefix + '.split mocks will be deprecated soon in favor of YAML files, which provide more targeting power. Take a look in our documentation.'); + mockData = readSplitConfigFile(log, filePath); } else { - mockData = readYAMLConfigFile(filePath); + mockData = readYAMLConfigFile(log, filePath); } return mockData; diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 0a31a0fc..66c51db1 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -1,5 +1,4 @@ import { forOwn } from '../../../utils/lang'; -import { logFactory } from '../../../logger/sdkLogger'; import { IReadinessManager } from '../../../readiness/types'; import { ISplitsCacheSync } from '../../../storages/types'; import { ISplitsParser } from '../splitsParser/types'; @@ -9,7 +8,7 @@ import { ISyncTask } from '../../types'; import { ISettings } from '../../../types'; import { CONTROL } from '../../../utils/constants'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; -const log = logFactory('splitio-producer:offline'); +import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants'; /** * Offline equivalent of `splitChangesUpdaterFactory` @@ -21,6 +20,8 @@ export function fromObjectUpdaterFactory( settings: ISettings, ): () => Promise { + const log = settings.log; + return function objectUpdater() { const splits: [string, string][] = []; let loadError = null; @@ -29,12 +30,11 @@ export function fromObjectUpdaterFactory( splitsMock = splitsParser(settings); } catch (err) { loadError = err; - log.error(`There was an issue loading the mock Splits data, no changes will be applied to the current cache. ${err}`); + log.error(ERROR_SYNC_OFFLINE_LOADING, [err]); } if (!loadError && splitsMock) { - log.debug('Splits data: '); - log.debug(JSON.stringify(splitsMock)); + log.debug(SYNC_OFFLINE_DATA, [JSON.stringify(splitsMock)]); forOwn(splitsMock, function (val, name) { splits.push([ @@ -76,6 +76,7 @@ export default function fromObjectSyncTaskFactory( settings: ISettings ): ISyncTask<[], boolean> { return syncTaskFactory( + settings.log, fromObjectUpdaterFactory( splitsParser, storage, @@ -83,6 +84,6 @@ export default function fromObjectSyncTaskFactory( settings, ), settings.scheduler.offlineRefreshRate, - 'offlineUpdater' + 'offlineUpdater', ); } diff --git a/src/sync/polling/pollingManagerCS.ts b/src/sync/polling/pollingManagerCS.ts index 1e4c7ac2..7fbbc747 100644 --- a/src/sync/polling/pollingManagerCS.ts +++ b/src/sync/polling/pollingManagerCS.ts @@ -1,5 +1,4 @@ import { ISegmentsSyncTask, ISplitsSyncTask, IPollingManagerCS } from './types'; -import { logFactory } from '../../logger/sdkLogger'; import { forOwn } from '../../utils/lang'; import { IReadinessManager } from '../../readiness/types'; import { ISplitApi } from '../../services/types'; @@ -9,7 +8,7 @@ import splitsSyncTaskFactory from './syncTasks/splitsSyncTask'; import { ISettings } from '../../types'; import { getMatching } from '../../utils/key'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; -const log = logFactory('splitio-sync:polling-manager'); +import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants'; /** * Expose start / stop mechanism for polling data from services. @@ -22,6 +21,8 @@ export default function pollingManagerCSFactory( settings: ISettings, ): IPollingManagerCS { + const log = settings.log; + const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(splitApi.fetchSplitChanges, storage, readiness, settings); // Map of matching keys to their corresponding MySegmentsSyncTask. @@ -47,7 +48,7 @@ export default function pollingManagerCSFactory( if (!splitsSyncTask.isRunning()) return; // noop if not doing polling const splitsHaveSegments = storage.splits.usesSegments(); if (splitsHaveSegments !== mySegmentsSyncTask.isRunning()) { - log.info(`Turning segments data polling ${splitsHaveSegments ? 'ON' : 'OFF'}.`); + log.info(POLLING_SMART_PAUSING, [splitsHaveSegments ? 'ON' : 'OFF']); if (splitsHaveSegments) { startMySegmentsSyncTasks(); } else { @@ -76,7 +77,7 @@ export default function pollingManagerCSFactory( // Start periodic fetching (polling) start() { - log.info('Starting polling'); + log.info(POLLING_START); splitsSyncTask.start(); if (storage.splits.usesSegments()) startMySegmentsSyncTasks(); @@ -84,7 +85,7 @@ export default function pollingManagerCSFactory( // Stop periodic fetching (polling) stop() { - log.info('Stopping polling'); + log.info(POLLING_STOP); if (splitsSyncTask.isRunning()) splitsSyncTask.stop(); stopMySegmentsSyncTasks(); diff --git a/src/sync/polling/pollingManagerSS.ts b/src/sync/polling/pollingManagerSS.ts index 6284b115..24b27600 100644 --- a/src/sync/polling/pollingManagerSS.ts +++ b/src/sync/polling/pollingManagerSS.ts @@ -5,9 +5,8 @@ import { IReadinessManager } from '../../readiness/types'; import { ISplitApi } from '../../services/types'; import { ISettings } from '../../types'; import { IPollingManager, ISegmentsSyncTask, ISplitsSyncTask } from './types'; -import { logFactory } from '../../logger/sdkLogger'; import thenable from '../../utils/promise/thenable'; -const log = logFactory('splitio-sync:polling-manager'); +import { POLLING_START, POLLING_STOP, logPrefixSyncPolling } from '../../logger/constants'; /** * Expose start / stop mechanism for pulling data from services. @@ -19,6 +18,8 @@ export default function pollingManagerSSFactory( settings: ISettings ): IPollingManager { + const log = settings.log; + const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(splitApi.fetchSplitChanges, storage, readiness, settings); const segmentsSyncTask: ISegmentsSyncTask = segmentsSyncTaskFactory(splitApi.fetchSegmentChanges, storage, readiness, settings); @@ -28,9 +29,9 @@ export default function pollingManagerSSFactory( // Start periodic fetching (polling) start() { - log.info('Starting polling'); - log.debug(`Splits will be refreshed each ${settings.scheduler.featuresRefreshRate} millis`); - log.debug(`Segments will be refreshed each ${settings.scheduler.segmentsRefreshRate} millis`); + log.info(POLLING_START); + log.debug(logPrefixSyncPolling + `Splits will be refreshed each ${settings.scheduler.featuresRefreshRate} millis`); + log.debug(logPrefixSyncPolling + `Segments will be refreshed each ${settings.scheduler.segmentsRefreshRate} millis`); const startingUp = splitsSyncTask.start(); if (thenable(startingUp)) { @@ -42,7 +43,7 @@ export default function pollingManagerSSFactory( // Stop periodic fetching (polling) stop() { - log.info('Stopping polling'); + log.info(POLLING_STOP); if (splitsSyncTask.isRunning()) splitsSyncTask.stop(); if (segmentsSyncTask.isRunning()) segmentsSyncTask.stop(); diff --git a/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts index 3f6ebeb1..9fb8ab89 100644 --- a/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts @@ -9,6 +9,7 @@ import splitChangesMock1 from '../../../../__tests__/mocks/splitchanges.since.-1 import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { settingsSplitApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; import EventEmitter from '../../../../utils/MinEvents'; +import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; const activeSplitWithSegments = { name: 'Split1', @@ -55,7 +56,7 @@ test('splitChangesUpdater / compute splits mutation', () => { test('splitChangesUpdater / factory', (done) => { - fetchMock.once('*', { status: 200, body: splitChangesMock1 }); + fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock, EventEmitter }); const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); @@ -69,14 +70,14 @@ test('splitChangesUpdater / factory', (done) => { const readinessManager = readinessManagerFactory(EventEmitter); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); - const splitChangesUpdater = splitChangesUpdaterFactory(splitChangesFetcher, splitsCache, segmentsCache, readinessManager.splits, 1000, 1); + const splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, readinessManager.splits, 1000, 1); splitChangesUpdater().then((result) => { - expect(setChangeNumber.mock.calls.length).toBe(1); + expect(setChangeNumber).toBeCalledTimes(1); expect(setChangeNumber).lastCalledWith(splitChangesMock1.till); - expect(addSplits.mock.calls.length).toBe(1); + expect(addSplits).toBeCalledTimes(1); expect(addSplits.mock.calls[0][0].length).toBe(splitChangesMock1.splits.length); - expect(removeSplits.mock.calls.length).toBe(1); + expect(removeSplits).toBeCalledTimes(1); expect(removeSplits).lastCalledWith([]); expect(registerSegments).toBeCalledTimes(1); expect(splitsEmitSpy).toBeCalledWith('SDK_SPLITS_ARRIVED'); diff --git a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts index be805204..6b19166d 100644 --- a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts @@ -5,13 +5,12 @@ import { SplitError } from '../../../utils/lang/errors'; import timeout from '../../../utils/promise/timeout'; import syncTaskFactory from '../../syncTask'; import { ISegmentsSyncTask } from '../types'; - -import { logFactory } from '../../../logger/sdkLogger'; import { IFetchMySegments } from '../../../services/types'; import mySegmentsFetcherFactory from '../fetchers/mySegmentsFetcher'; import { ISettings } from '../../../types'; import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; -const log = logFactory('splitio-sync:my-segments'); +import { ILogger } from '../../../logger/types'; +import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants'; type IMySegmentsUpdater = (segmentList?: string[], noCache?: boolean) => Promise @@ -22,12 +21,13 @@ type IMySegmentsUpdater = (segmentList?: string[], noCache?: boolean) => Promise * - uses `segmentsEventEmitter` to emit events related to segments data updates */ function mySegmentsUpdaterFactory( + log: ILogger, mySegmentsFetcher: IMySegmentsFetcher, splitsCache: ISplitsCacheSync, mySegmentsCache: ISegmentsCacheSync, segmentsEventEmitter: ISegmentsEventEmitter, requestTimeoutBeforeReady: number, - retriesOnFailureBeforeReady: number + retriesOnFailureBeforeReady: number, ): IMySegmentsUpdater { let readyOnAlreadyExistentState = true; @@ -74,7 +74,7 @@ function mySegmentsUpdaterFactory( if (startingUp && retriesOnFailureBeforeReady > retry) { retry += 1; - log.warn(`Retrying download of segments #${retry}. Reason: ${error}`); + log.warn(SYNC_MYSEGMENTS_FETCH_RETRY, [retry, error]); return _mySegmentsUpdater(retry); // no need to forward `segmentList` and `noCache` params } else { startingUp = false; @@ -104,15 +104,17 @@ export default function mySegmentsSyncTaskFactory( matchingKey: string ): ISegmentsSyncTask { return syncTaskFactory( + settings.log, mySegmentsUpdaterFactory( + settings.log, mySegmentsFetcherFactory(fetchMySegments, matchingKey), storage.splits, storage.segments, readiness.segments, settings.startup.requestTimeoutBeforeReady, - settings.startup.retriesOnFailureBeforeReady + settings.startup.retriesOnFailureBeforeReady, ), settings.scheduler.segmentsRefreshRate, - 'mySegmentsUpdater' + 'mySegmentsUpdater', ); } diff --git a/src/sync/polling/syncTasks/segmentsSyncTask.ts b/src/sync/polling/syncTasks/segmentsSyncTask.ts index 39a91f44..4d0d400b 100644 --- a/src/sync/polling/syncTasks/segmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/segmentsSyncTask.ts @@ -6,13 +6,12 @@ import { findIndex } from '../../../utils/lang'; import { SplitError } from '../../../utils/lang/errors'; import syncTaskFactory from '../../syncTask'; import { ISegmentsSyncTask } from '../types'; -import { logFactory } from '../../../logger/sdkLogger'; import segmentChangesFetcherFactory from '../fetchers/segmentChangesFetcher'; import { IFetchSegmentChanges } from '../../../services/types'; import { ISettings } from '../../../types'; import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; -const log = logFactory('splitio-sync:segment-changes'); -const inputValidationLog = logFactory('', { displayAllErrors: true }); +import { ILogger } from '../../../logger/types'; +import { logPrefixInstantiation, logPrefixSyncSegments } from '../../../logger/constants'; type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) => Promise @@ -23,9 +22,10 @@ type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetch * - uses `segmentsEventEmitter` to emit events related to segments data updates */ function segmentChangesUpdaterFactory( + log: ILogger, segmentChangesFetcher: ISegmentChangesFetcher, segmentsCache: ISegmentsCacheSync, - readiness: IReadinessManager + readiness: IReadinessManager, ): ISegmentChangesUpdater { let readyOnAlreadyExistentState = true; @@ -48,7 +48,7 @@ function segmentChangesUpdaterFactory( * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications. */ return function segmentChangesUpdater(segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) { - log.debug('Started segments update'); + log.debug(logPrefixSyncSegments + 'Started segments update'); // If not a segment name provided, read list of available segments names to be updated. let segments = segmentNames ? segmentNames : segmentsCache.getRegisteredSegments(); @@ -61,7 +61,7 @@ function segmentChangesUpdaterFactory( const segmentName = segments[index]; const since = segmentsCache.getChangeNumber(segmentName); - log.debug(`Processing segment ${segmentName}`); + log.debug(logPrefixSyncSegments + `Processing segment ${segmentName}`); updaters.push(segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { let changeNumber = -1; @@ -73,7 +73,7 @@ function segmentChangesUpdaterFactory( changeNumber = x.till; } - log.debug(`Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); + log.debug(logPrefixSyncSegments + `Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); }); return changeNumber; @@ -97,7 +97,7 @@ function segmentChangesUpdaterFactory( if (error.statusCode === 403) { // @TODO although factory status is destroyed, synchronization is not stopped readiness.destroy(); - inputValidationLog.error('Factory instantiation: you passed a Browser type authorizationKey, please grab an Api Key from the Split web console that is of type SDK.'); + log.error(logPrefixInstantiation + ': you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.'); } return false; @@ -113,10 +113,12 @@ export default function segmentsSyncTaskFactory( settings: ISettings, ): ISegmentsSyncTask { return syncTaskFactory( + settings.log, segmentChangesUpdaterFactory( + settings.log, segmentChangesFetcherFactory(fetchSegmentChanges), storage.segments, - readiness + readiness, ), settings.scheduler.segmentsRefreshRate, 'segmentChangesUpdater' diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index f8df3913..a69e579a 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -7,13 +7,13 @@ import { IReadinessManager, ISplitsEventEmitter } from '../../../readiness/types import timeout from '../../../utils/promise/timeout'; import syncTaskFactory from '../../syncTask'; import { ISplitsSyncTask } from '../types'; -import { logFactory } from '../../../logger/sdkLogger'; import splitChangesFetcherFactory from '../fetchers/splitChangesFetcher'; import { IFetchSplitChanges } from '../../../services/types'; import thenable from '../../../utils/promise/thenable'; import { ISettings } from '../../../types'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; -const log = logFactory('splitio-sync:split-changes'); +import { ILogger } from '../../../logger/types'; +import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; type ISplitChangesUpdater = (noCache?: boolean) => Promise @@ -82,12 +82,13 @@ export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { * Exported for testing purposes. */ export function splitChangesUpdaterFactory( + log: ILogger, splitChangesFetcher: ISplitChangesFetcher, splitsCache: ISplitsCacheSync, segmentsCache: ISegmentsCacheSync, splitsEventEmitter: ISplitsEventEmitter, requestTimeoutBeforeReady: number, - retriesOnFailureBeforeReady: number + retriesOnFailureBeforeReady: number, ): ISplitChangesUpdater { let startingUp = true; @@ -115,7 +116,7 @@ export function splitChangesUpdaterFactory( * @param {number} retry current number of retry attemps */ function _splitChangesUpdater(since: number, retry = 0): Promise { - log.debug(`Spin up split update using since = ${since}`); + log.debug(SYNC_SPLITS_FETCH, [since]); const fetcherPromise = splitChangesFetcher(since, noCache, _promiseDecorator) .then((splitChanges: ISplitChangesResponse) => { @@ -123,9 +124,9 @@ export function splitChangesUpdaterFactory( const mutation = computeSplitsMutation(splitChanges.splits); - log.debug(`New splits ${mutation.added.length}`); - log.debug(`Removed splits ${mutation.removed.length}`); - log.debug(`Segment names collected ${mutation.segments.length}`); + log.debug(SYNC_SPLITS_NEW, [mutation.added.length]); + log.debug(SYNC_SPLITS_REMOVED, [mutation.removed.length]); + log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); // Write into storage // @TODO if allowing custom storages, wrap errors as SplitErrors to distinguish from user callback errors @@ -151,11 +152,11 @@ export function splitChangesUpdaterFactory( startingUp = false; // Stop retrying. } - log.warn(`Error while doing fetch of Splits. ${error}`); + log.warn(SYNC_SPLITS_FETCH_FAILS, [error]); if (startingUp && retriesOnFailureBeforeReady > retry) { retry += 1; - log.info(`Retrying download of splits #${retry}. Reason: ${error}`); + log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]); return _splitChangesUpdater(since, retry); } else { startingUp = false; @@ -183,15 +184,17 @@ export default function splitsSyncTaskFactory( settings: ISettings, ): ISplitsSyncTask { return syncTaskFactory( + settings.log, splitChangesUpdaterFactory( + settings.log, splitChangesFetcherFactory(fetchSplitChanges), storage.splits, storage.segments, readiness.splits, settings.startup.requestTimeoutBeforeReady, - settings.startup.retriesOnFailureBeforeReady + settings.startup.retriesOnFailureBeforeReady, ), settings.scheduler.featuresRefreshRate, - 'splitChangesUpdater' + 'splitChangesUpdater', ); } diff --git a/src/sync/streaming/AuthClient/__tests__/index.spec.ts b/src/sync/streaming/AuthClient/__tests__/index.spec.ts index 6784439c..98182a59 100644 --- a/src/sync/streaming/AuthClient/__tests__/index.spec.ts +++ b/src/sync/streaming/AuthClient/__tests__/index.spec.ts @@ -9,7 +9,7 @@ import EventEmitter from '../../../../utils/MinEvents'; import { authenticateFactory, hashUserKey } from '../index'; const authorizationKey = settingsSplitApi.core.authorizationKey; -const authUrl = settingsSplitApi.urls.auth; +const authUrl = settingsSplitApi.urls.auth; // @ts-ignore const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock, EventEmitter }); const authenticate = authenticateFactory(splitApi.fetchAuth); diff --git a/src/sync/streaming/SSEClient/__tests__/index.spec.ts b/src/sync/streaming/SSEClient/__tests__/index.spec.ts index 0e1e3170..c660ad09 100644 --- a/src/sync/streaming/SSEClient/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEClient/__tests__/index.spec.ts @@ -35,7 +35,7 @@ test('SSClient / setEventHandler, open and close methods', () => { instance.open(authDataSample); let esconnection = instance.connection; // instance of EventSource used to mock events esconnection.emitOpen(); - expect(handler.handleOpen.mock.calls.length).toBe(1); // handleOpen called when connection is opened + expect(handler.handleOpen).toBeCalledTimes(1); // handleOpen called when connection is opened handler.handleOpen.mockClear(); // emit message @@ -56,25 +56,25 @@ test('SSClient / setEventHandler, open and close methods', () => { // open attempt without open event emitted instance.open(authDataSample); - expect(handler.handleOpen.mock.calls.length).toBe(0); // handleOpen not called until open event is emitted + expect(handler.handleOpen).not.toBeCalled(); // handleOpen not called until open event is emitted // open a new connection instance.open(authDataSample); instance.connection.emitOpen(); - expect(handler.handleOpen.mock.calls.length).toBe(1); // handleOpen called when connection is open + expect(handler.handleOpen).toBeCalledTimes(1); // handleOpen called when connection is open // reopen the connection handler.handleOpen.mockClear(); instance.reopen(); instance.connection.emitOpen(); - expect(handler.handleOpen.mock.calls.length).toBe(1); // handleOpen called if connection is reopen + expect(handler.handleOpen).toBeCalledTimes(1); // handleOpen called if connection is reopen // remove event handler before opening a new connection handler.handleOpen.mockClear(); instance.setEventHandler(undefined); instance.open(authDataSample); instance.connection.emitOpen(); - expect(handler.handleOpen.mock.calls.length).toBe(0); // handleOpen not called if connection is open but the handler was removed + expect(handler.handleOpen).not.toBeCalled(); // handleOpen not called if connection is open but the handler was removed }); diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index 7d9ac832..52473d0d 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -1,6 +1,7 @@ // @ts-nocheck import SSEHandlerFactory from '..'; import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE } from '../../constants'; +import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; // update messages import splitUpdateMessage from '../../../../__tests__/mocks/message.SPLIT_UPDATE.1457552620999.json'; @@ -28,7 +29,7 @@ const pushEmitter = { emit: jest.fn() }; test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (NotificationKeeper)', () => { pushEmitter.emit.mockClear(); - const sseHandler = SSEHandlerFactory(pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); // handleOpen @@ -71,7 +72,7 @@ test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (Notificatio test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { pushEmitter.emit.mockClear(); - const sseHandler = SSEHandlerFactory(pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); sseHandler.handleOpen(); // CONTROL messages @@ -99,7 +100,7 @@ test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { sseHandler.handleMessage(controlStreamingDisabledSec); // testing STREAMING_DISABLED with second region expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_NONRETRYABLE_ERROR); // must emit PUSH_NONRETRYABLE_ERROR if received a STREAMING_DISABLED control message - const sseHandler2 = SSEHandlerFactory(pushEmitter); + const sseHandler2 = SSEHandlerFactory(loggerMock, pushEmitter); sseHandler2.handleOpen(); sseHandler2.handleMessage(controlStreamingPausedSec); // testing STREAMING_PAUSED with second region @@ -111,7 +112,7 @@ test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { }); test('`handlerMessage` for update notifications (NotificationProcessor)', () => { - const sseHandler = SSEHandlerFactory(pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); sseHandler.handleOpen(); pushEmitter.emit.mockClear(); @@ -134,7 +135,7 @@ test('`handlerMessage` for update notifications (NotificationProcessor)', () => }); test('handleError', () => { - const sseHandler = SSEHandlerFactory(pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); sseHandler.handleOpen(); pushEmitter.emit.mockClear(); @@ -165,7 +166,7 @@ test('handleError', () => { }); test('handlerMessage: ignore invalid events', () => { - const sseHandler = SSEHandlerFactory(pushEmitter); + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); sseHandler.handleOpen(); pushEmitter.emit.mockClear(); diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index b68be6f9..716564d8 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -1,11 +1,11 @@ import { errorParser, messageParser } from './NotificationParser'; import notificationKeeperFactory from './NotificationKeeper'; import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE } from '../constants'; -import { logFactory } from '../../../logger/sdkLogger'; import { IPushEventEmitter } from '../types'; import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError } from './types'; -const log = logFactory('splitio-sync:sse-handler'); +import { ILogger } from '../../../logger/types'; +import { STREAMING_PARSING_ERROR_FAILS, ERROR_STREAMING_SSE, STREAMING_PARSING_MESSAGE_FAILS, STREAMING_NEW_MESSAGE } from '../../../logger/constants'; function isRetryableError(error: INotificationError) { if (error.parsedData && error.parsedData.code) { @@ -22,9 +22,10 @@ function isRetryableError(error: INotificationError) { /** * Factory for SSEHandler, which processes SSEClient messages and emits the corresponding push events. * + * @param log factory logger * @param pushEmitter emitter for events related to streaming support */ -export default function SSEHandlerFactory(pushEmitter: IPushEventEmitter): ISseEventHandler { +export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter): ISseEventHandler { const notificationKeeper = notificationKeeperFactory(pushEmitter); @@ -39,11 +40,11 @@ export default function SSEHandlerFactory(pushEmitter: IPushEventEmitter): ISseE try { errorWithParsedData = errorParser(error); } catch (err) { - log.warn(`Error parsing SSE error notification: ${err}`); + log.warn(STREAMING_PARSING_ERROR_FAILS, [err]); } let errorMessage = errorWithParsedData.parsedData && errorWithParsedData.parsedData.message; - log.error(`Fail to connect to streaming, with error message: ${errorMessage}`); + log.error(ERROR_STREAMING_SSE, [errorMessage]); if (isRetryableError(errorWithParsedData)) { pushEmitter.emit(PUSH_RETRYABLE_ERROR); @@ -58,12 +59,12 @@ export default function SSEHandlerFactory(pushEmitter: IPushEventEmitter): ISseE try { messageWithParsedData = messageParser(message); } catch (err) { - log.warn(`Error parsing new SSE message notification: ${err}`); + log.warn(STREAMING_PARSING_MESSAGE_FAILS, [err]); return; } const { parsedData, data, channel, timestamp } = messageWithParsedData; - log.debug(`New SSE message received, with data: ${data}.`); + log.debug(STREAMING_NEW_MESSAGE, [data]); // we only handle update events if streaming is up. if (!notificationKeeper.isStreamingUp() && parsedData.type !== OCCUPANCY && parsedData.type !== CONTROL) diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 9daddb3a..0e7f1559 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -12,15 +12,13 @@ import SegmentsUpdateWorker from './UpdateWorkers/SegmentsUpdateWorker'; import SplitsUpdateWorker from './UpdateWorkers/SplitsUpdateWorker'; import { authenticateFactory, hashUserKey } from './AuthClient'; import { forOwn } from '../../utils/lang'; -import { logFactory } from '../../logger/sdkLogger'; import SSEClient from './SSEClient'; import { IFetchAuth } from '../../services/types'; import { ISettings } from '../../types'; import { getMatching } from '../../utils/key'; import { MY_SEGMENTS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP } from './constants'; import { IPlatform } from '../../sdkFactory/types'; - -const log = logFactory('splitio-sync:push-manager'); +import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT } from '../../logger/constants'; /** * PushManager factory: @@ -36,18 +34,20 @@ export default function pushManagerFactory( settings: ISettings, ): IPushManagerCS | undefined { + const log = settings.log; + let sseClient: ISSEClient; try { sseClient = new SSEClient(settings.urls.streaming, platform.getEventSource); } catch (e) { - log.warn(e + 'Falling back to polling mode.'); + log.warn(STREAMING_FALLBACK, [e]); return; } const authenticate = authenticateFactory(fetchAuth); // init feedback loop const pushEmitter = new platform.EventEmitter() as IPushEventEmitter; - const sseHandler = SSEHandlerFactory(pushEmitter); + const sseHandler = SSEHandlerFactory(log, pushEmitter); sseClient.setEventHandler(sseHandler); // [Only for client-side] map of hashes to user keys, to dispatch MY_SEGMENTS_UPDATE events to the corresponding MySegmentsUpdateWorker @@ -88,14 +88,14 @@ export default function pushManagerFactory( // Set token refresh 10 minutes before expirationTime const delayInSeconds = expirationTime - issuedAt - SECONDS_BEFORE_EXPIRATION; - log.info(`Refreshing streaming token in ${delayInSeconds} seconds.`); + log.info(STREAMING_REFRESH_TOKEN, [delayInSeconds]); timeoutId = setTimeout(connectPush, delayInSeconds * 1000); } function connectPush() { disconnected = false; - log.info('Connecting to push streaming.'); + log.info(STREAMING_CONNECTING); const userKeys = userKey ? Object.keys(workers) : undefined; authenticate(userKeys).then( @@ -105,7 +105,7 @@ export default function pushManagerFactory( // 'pushEnabled: false' is handled as a PUSH_NONRETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to // close the sseClient in case the org has been bloqued while the instance was connected to streaming if (!authData.pushEnabled) { - log.info('Streaming is not available. Switching to polling mode.'); + log.info(STREAMING_DISABLED); pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); return; } @@ -122,7 +122,7 @@ export default function pushManagerFactory( function (error) { if (disconnected) return; - log.error(`Failed to authenticate for streaming. Error: "${error.message}".`); + log.error(ERROR_STREAMING_AUTH, [error.message]); // Handle 4XX HTTP errors: 401 (invalid API Key) or 400 (using incorrect API Key, i.e., client-side API Key on server-side) if (error.statusCode >= 400 && error.statusCode < 500) { @@ -139,7 +139,7 @@ export default function pushManagerFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { disconnected = true; - log.info('Disconnecting from push streaming.'); + log.info(STREAMING_DISCONNECTING); sseClient.close(); if (timeoutId) clearTimeout(timeoutId); @@ -177,7 +177,7 @@ export default function pushManagerFactory( // retry streaming reconnect with backoff algorithm let delayInMillis = connectPushRetryBackoff.scheduleCall(); - log.info(`Attempting to reconnect in ${delayInMillis / 1000} seconds.`); + log.info(STREAMING_RECONNECT, [delayInMillis / 1000]); pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already }); diff --git a/src/sync/streaming/pushManagerCS.ts b/src/sync/streaming/pushManagerCS.ts index 19435741..5611eaa0 100644 --- a/src/sync/streaming/pushManagerCS.ts +++ b/src/sync/streaming/pushManagerCS.ts @@ -12,14 +12,12 @@ import MySegmentsUpdateWorker from './UpdateWorkers/MySegmentsUpdateWorker'; import SplitsUpdateWorker from './UpdateWorkers/SplitsUpdateWorker'; import { authenticateFactory, hashUserKey } from './AuthClient'; import { forOwn } from '../../utils/lang'; -import { logFactory } from '../../logger/sdkLogger'; import SSEClient from './SSEClient'; import { IFetchAuth } from '../../services/types'; import { ISettings } from '../../types'; import { getMatching } from '../../utils/key'; import { IPlatform } from '../../sdkFactory/types'; - -const log = logFactory('splitio-sync:push-manager'); +import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT } from '../../logger/constants'; /** * PushManager factory for client-side, with support for multiple clients. @@ -34,18 +32,20 @@ export default function pushManagerCSFactory( settings: ISettings ): IPushManagerCS | undefined { + const log = settings.log; + let sseClient: ISSEClient; try { sseClient = new SSEClient(settings.urls.streaming, platform.getEventSource); } catch (e) { - log.warn(e + 'Falling back to polling mode.'); + log.warn(STREAMING_FALLBACK, [e]); return; } const authenticate = authenticateFactory(fetchAuth); // init feedback loop const pushEmitter = new platform.EventEmitter() as IPushEventEmitter; - const sseHandler = SSEHandlerFactory(pushEmitter); + const sseHandler = SSEHandlerFactory(log, pushEmitter); sseClient.setEventHandler(sseHandler); // [Only for client-side] map of hashes to user keys, to dispatch MY_SEGMENTS_UPDATE events to the corresponding MySegmentsUpdateWorker @@ -84,14 +84,14 @@ export default function pushManagerCSFactory( // Set token refresh 10 minutes before expirationTime const delayInSeconds = expirationTime - issuedAt - SECONDS_BEFORE_EXPIRATION; - log.info(`Refreshing streaming token in ${delayInSeconds} seconds.`); + log.info(STREAMING_REFRESH_TOKEN, [delayInSeconds]); timeoutId = setTimeout(connectPush, delayInSeconds * 1000); } function connectPush() { disconnected = false; - log.info('Connecting to push streaming.'); + log.info(STREAMING_CONNECTING); const userKeys = Object.keys(workers); // [Only for client-side] authenticate(userKeys).then( @@ -101,7 +101,7 @@ export default function pushManagerCSFactory( // 'pushEnabled: false' is handled as a PUSH_NONRETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to // close the sseClient in case the org has been bloqued while the instance was connected to streaming if (!authData.pushEnabled) { - log.info('Streaming is not available. Switching to polling mode.'); + log.info(STREAMING_DISABLED); pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); return; } @@ -118,7 +118,7 @@ export default function pushManagerCSFactory( function (error) { if (disconnected) return; - log.error(`Failed to authenticate for streaming. Error: "${error.message}".`); + log.error(ERROR_STREAMING_AUTH, [error.message]); // Handle 4XX HTTP errors: 401 (invalid API Key) or 400 (using incorrect API Key, i.e., client-side API Key on server-side) if (error.statusCode >= 400 && error.statusCode < 500) { @@ -135,7 +135,7 @@ export default function pushManagerCSFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { disconnected = true; - log.info('Disconnecting from push streaming.'); + log.info(STREAMING_DISCONNECTING); sseClient.close(); if (timeoutId) clearTimeout(timeoutId); @@ -172,7 +172,7 @@ export default function pushManagerCSFactory( // retry streaming reconnect with backoff algorithm let delayInMillis = connectPushRetryBackoff.scheduleCall(); - log.info(`Attempting to reconnect in ${delayInMillis / 1000} seconds.`); + log.info(STREAMING_RECONNECT, [delayInMillis / 1000]); pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already }); diff --git a/src/sync/streaming/pushManagerSS.ts b/src/sync/streaming/pushManagerSS.ts index 4584c1dd..37de7fce 100644 --- a/src/sync/streaming/pushManagerSS.ts +++ b/src/sync/streaming/pushManagerSS.ts @@ -9,14 +9,12 @@ import Backoff from '../../utils/Backoff'; import SSEHandlerFactory from './SSEHandler'; import SegmentsUpdateWorker from './UpdateWorkers/SegmentsUpdateWorker'; import SplitsUpdateWorker from './UpdateWorkers/SplitsUpdateWorker'; -import { logFactory } from '../../logger/sdkLogger'; import { IFetchAuth } from '../../services/types'; import { authenticateFactory } from './AuthClient'; import SSEClient from './SSEClient'; import { ISettings } from '../../types'; import { IPlatform } from '../../sdkFactory/types'; - -const log = logFactory('splitio-sync:push-manager'); +import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT } from '../../logger/constants'; /** * PushManager factory for server-side @@ -30,18 +28,20 @@ export default function pushManagerSSFactory( settings: ISettings ): IPushManager | undefined { + const log = settings.log; + let sseClient: ISSEClient; try { sseClient = new SSEClient(settings.urls.streaming, platform.getEventSource); } catch (e) { - log.warn(e + 'Falling back to polling mode.'); + log.warn(STREAMING_FALLBACK, [e]); return; } const authenticate = authenticateFactory(fetchAuth); // init feedback loop (pushEmitter) const pushEmitter = new platform.EventEmitter() as IPushEventEmitter; - const sseHandler = SSEHandlerFactory(pushEmitter); + const sseHandler = SSEHandlerFactory(log, pushEmitter); sseClient.setEventHandler(sseHandler); // init workers @@ -65,14 +65,14 @@ export default function pushManagerSSFactory( // Set token refresh 10 minutes before expirationTime const delayInSeconds = expirationTime - issuedAt - SECONDS_BEFORE_EXPIRATION; - log.info(`Refreshing streaming token in ${delayInSeconds} seconds.`); + log.info(STREAMING_REFRESH_TOKEN, [delayInSeconds]); timeoutId = setTimeout(connectPush, delayInSeconds * 1000); } function connectPush() { disconnected = false; - log.info('Connecting to push streaming.'); + log.info(STREAMING_CONNECTING); authenticate().then( function (authData) { @@ -81,7 +81,7 @@ export default function pushManagerSSFactory( // 'pushEnabled: false' is handled as a PUSH_NONRETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to // close the sseClient in case the org has been bloqued while the instance was connected to streaming if (!authData.pushEnabled) { - log.info('Streaming is not available. Switching to polling mode.'); + log.info(STREAMING_DISABLED); pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); return; } @@ -95,7 +95,7 @@ export default function pushManagerSSFactory( function (error) { if (disconnected) return; - log.error(`Failed to authenticate for streaming. Error: "${error.message}".`); + log.error(ERROR_STREAMING_AUTH, [error.message]); // Handle 4XX HTTP errors: 401 (invalid API Key) or 400 (using incorrect API Key, i.e., client-side API Key on server-side) if (error.statusCode >= 400 && error.statusCode < 500) { @@ -112,7 +112,7 @@ export default function pushManagerSSFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { disconnected = true; - log.info('Disconnecting from push streaming.'); + log.info(STREAMING_DISCONNECTING); sseClient.close(); if (timeoutId) clearTimeout(timeoutId); @@ -149,7 +149,7 @@ export default function pushManagerSSFactory( // retry streaming reconnect with backoff algorithm let delayInMillis = connectPushRetryBackoff.scheduleCall(); - log.info(`Attempting to reconnect in ${delayInMillis / 1000} seconds.`); + log.info(STREAMING_RECONNECT, [delayInMillis / 1000]); pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already }); diff --git a/src/sync/submitters/eventsSyncTask.ts b/src/sync/submitters/eventsSyncTask.ts index cb68cf39..3a15a6e0 100644 --- a/src/sync/submitters/eventsSyncTask.ts +++ b/src/sync/submitters/eventsSyncTask.ts @@ -2,13 +2,14 @@ import { IEventsCacheSync } from '../../storages/types'; import { IPostEventsBulk } from '../../services/types'; import { ISyncTask, ITimeTracker } from '../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-sync:submitters'); +import { ILogger } from '../../logger/types'; +import { SUBMITTERS_PUSH_FULL_EVENTS_QUEUE } from '../../logger/constants'; /** * Sync task that periodically posts tracked events */ export function eventsSyncTaskFactory( + log: ILogger, postEventsBulk: IPostEventsBulk, eventsCache: IEventsCacheSync, eventsPushRate: number, @@ -17,7 +18,7 @@ export function eventsSyncTaskFactory( ): ISyncTask { // don't retry events. - const syncTask = submitterSyncTaskFactory(postEventsBulk, eventsCache, eventsPushRate, 'queued events', latencyTracker); + const syncTask = submitterSyncTaskFactory(log, postEventsBulk, eventsCache, eventsPushRate, 'queued events', latencyTracker); // Set a timer for the first push of events, if (eventsFirstPushWindow > 0) { @@ -35,7 +36,7 @@ export function eventsSyncTaskFactory( // register eventsSubmitter to be executed when events cache is full eventsCache.setOnFullQueueCb(() => { - log.info('Flushing full events queue and reseting timer.'); + log.info(SUBMITTERS_PUSH_FULL_EVENTS_QUEUE); syncTask.execute(); }); diff --git a/src/sync/submitters/impressionCountsSyncTask.ts b/src/sync/submitters/impressionCountsSyncTask.ts index 2e5534c1..a24ea41e 100644 --- a/src/sync/submitters/impressionCountsSyncTask.ts +++ b/src/sync/submitters/impressionCountsSyncTask.ts @@ -3,6 +3,7 @@ import { IPostTestImpressionsCount } from '../../services/types'; import { IImpressionCountsCacheSync } from '../../storages/types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; import { ImpressionCountsPayload } from './types'; +import { ILogger } from '../../logger/types'; /** * Converts `impressionCounts` data from cache into request payload. @@ -34,11 +35,12 @@ const IMPRESSIONS_COUNT_RATE = 1800000; // 30 minutes * Sync task that periodically posts impression counts */ export function impressionCountsSyncTaskFactory( + log: ILogger, postTestImpressionsCount: IPostTestImpressionsCount, impressionCountsCache: IImpressionCountsCacheSync, latencyTracker?: ITimeTracker ): ISyncTask { // retry impressions counts only once. - return submitterSyncTaskFactory(postTestImpressionsCount, impressionCountsCache, IMPRESSIONS_COUNT_RATE, 'impression counts', latencyTracker, fromImpressionCountsCollector, 1); + return submitterSyncTaskFactory(log, postTestImpressionsCount, impressionCountsCache, IMPRESSIONS_COUNT_RATE, 'impression counts', latencyTracker, fromImpressionCountsCollector, 1); } diff --git a/src/sync/submitters/impressionsSyncTask.ts b/src/sync/submitters/impressionsSyncTask.ts index 715e00e0..c5ff1d02 100644 --- a/src/sync/submitters/impressionsSyncTask.ts +++ b/src/sync/submitters/impressionsSyncTask.ts @@ -5,6 +5,7 @@ import { IImpressionsCacheSync } from '../../storages/types'; import { ImpressionDTO } from '../../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; import { ImpressionsPayload } from './types'; +import { ILogger } from '../../logger/types'; /** * Converts `impressions` data from cache into request payload. @@ -40,6 +41,7 @@ export function fromImpressionsCollector(sendLabels: boolean, data: ImpressionDT * Sync task that periodically posts impressions data */ export function impressionsSyncTaskFactory( + log: ILogger, postTestImpressionsBulk: IPostTestImpressionsBulk, impressionsCache: IImpressionsCacheSync, impressionsRefreshRate: number, @@ -48,5 +50,5 @@ export function impressionsSyncTaskFactory( ): ISyncTask { // retry impressions only once. - return submitterSyncTaskFactory(postTestImpressionsBulk, impressionsCache, impressionsRefreshRate, 'impressions', latencyTracker, fromImpressionsCollector.bind(undefined, sendLabels), 1); + return submitterSyncTaskFactory(log, postTestImpressionsBulk, impressionsCache, impressionsRefreshRate, 'impressions', latencyTracker, fromImpressionsCollector.bind(undefined, sendLabels), 1); } diff --git a/src/sync/submitters/metricsSyncTask.ts b/src/sync/submitters/metricsSyncTask.ts index 600d247c..2f743884 100644 --- a/src/sync/submitters/metricsSyncTask.ts +++ b/src/sync/submitters/metricsSyncTask.ts @@ -4,6 +4,7 @@ import { ICountsCacheSync, ILatenciesCacheSync } from '../../storages/types'; import { IPostMetricsCounters, IPostMetricsTimes } from '../../services/types'; import { ISyncTask, ITimeTracker } from '../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; +import { ILogger } from '../../logger/types'; // extract POST payload object from cache function fromCache(propertyName: 'latencies' | 'delta') { @@ -22,19 +23,21 @@ function fromCache(propertyName: 'latencies' | 'delta') { * Sync task that periodically posts telemetry counts */ export function countsSyncTaskFactory( + log: ILogger, postMetricsCounters: IPostMetricsCounters, countsCache: ICountsCacheSync, metricsRefreshRate: number, latencyTracker?: ITimeTracker ): ISyncTask { - return submitterSyncTaskFactory(postMetricsCounters, countsCache, metricsRefreshRate, 'count metrics', latencyTracker, fromCache('delta')); + return submitterSyncTaskFactory(log, postMetricsCounters, countsCache, metricsRefreshRate, 'count metrics', latencyTracker, fromCache('delta')); } /** * Sync task that periodically posts telemetry latencies */ export function latenciesSyncTaskFactory( + log: ILogger, postMetricsLatencies: IPostMetricsTimes, latenciesCache: ILatenciesCacheSync, metricsRefreshRate: number, @@ -42,5 +45,5 @@ export function latenciesSyncTaskFactory( ): ISyncTask { // don't retry metrics. - return submitterSyncTaskFactory(postMetricsLatencies, latenciesCache, metricsRefreshRate, 'latency metrics', latencyTracker, fromCache('latencies')); + return submitterSyncTaskFactory(log, postMetricsLatencies, latenciesCache, metricsRefreshRate, 'latency metrics', latencyTracker, fromCache('latencies')); } diff --git a/src/sync/submitters/submitterSyncTask.ts b/src/sync/submitters/submitterSyncTask.ts index f2d4a854..cbee1913 100644 --- a/src/sync/submitters/submitterSyncTask.ts +++ b/src/sync/submitters/submitterSyncTask.ts @@ -1,13 +1,14 @@ import syncTaskFactory from '../syncTask'; -import { logFactory } from '../../logger/sdkLogger'; import { ISyncTask, ITimeTracker } from '../types'; import { IRecorderCacheConsumerSync } from '../../storages/types'; -const log = logFactory('splitio-sync:submitters'); +import { ILogger } from '../../logger/types'; +import { SUBMITTERS_PUSH, SUBMITTERS_PUSH_FAILS, SUBMITTERS_PUSH_RETRY } from '../../logger/constants'; /** * Base function to create submitter sync tasks, such as ImpressionsSyncTask and EventsSyncTask */ export function submitterSyncTaskFactory( + log: ILogger, postClient: (body: string) => Promise, sourceCache: IRecorderCacheConsumerSync, postRate: number, @@ -25,7 +26,7 @@ export function submitterSyncTaskFactory( const data = sourceCache.state(); const dataCount: number | '' = typeof data.length === 'number' ? data.length : ''; - log.info(`Pushing ${dataCount} ${dataName}.`); + log.info(SUBMITTERS_PUSH, [dataCount, dataName]); const latencyTrackerStop = latencyTracker && latencyTracker.start(); const jsonPayload = JSON.stringify(fromCacheToPayload ? fromCacheToPayload(data) : data); @@ -36,14 +37,14 @@ export function submitterSyncTaskFactory( sourceCache.clear(); // we clear the queue if request successes. }).catch(err => { if (!maxRetries) { - log.warn(`Droping ${dataCount} ${dataName} after retry. Reason ${err}.`); + log.warn(SUBMITTERS_PUSH_FAILS, [dataCount, dataName, err]); } else if (retries === maxRetries) { retries = 0; sourceCache.clear(); // we clear the queue if request fails after retries. - log.warn(`Droping ${dataCount} ${dataName} after retry. Reason ${err}.`); + log.warn(SUBMITTERS_PUSH_FAILS, [dataCount, dataName, err]); } else { retries++; - log.warn(`Failed to push ${dataCount} ${dataName}, keeping data to retry on next iteration. Reason ${err}.`); + log.warn(SUBMITTERS_PUSH_RETRY, [dataCount, dataName, err]); } }); @@ -51,5 +52,5 @@ export function submitterSyncTaskFactory( return latencyTrackerStop ? postPromise.then(latencyTrackerStop).catch(latencyTrackerStop) : postPromise; } - return syncTaskFactory(postData, postRate, dataName + ' submitter'); + return syncTaskFactory(log, postData, postRate, dataName + ' submitter'); } diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index f0eb4b6a..04a0c98f 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -7,9 +7,8 @@ import { IReadinessManager } from '../readiness/types'; import { IStorageSync } from '../storages/types'; import { IPushManagerFactoryParams, IPushManager, IPushManagerCS } from './streaming/types'; import { IPollingManager, IPollingManagerCS, IPollingManagerFactoryParams } from './polling/types'; -import { logFactory } from '../logger/sdkLogger'; import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants'; -export const log = logFactory('splitio-sync:sync-manager'); +import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants'; /** * Online SyncManager factory. @@ -35,6 +34,8 @@ export function syncManagerOnlineFactory( readiness }: ISyncManagerFactoryParams): ISyncManagerCS { + const log = settings.log; + /** Polling Manager */ const pollingManager = pollingManagerFactory(splitApi, storage, readiness, settings); @@ -46,11 +47,11 @@ export function syncManagerOnlineFactory( /** Submitter Manager */ // It is not inyected via a factory as push and polling managers, because at the moment it is mandatory and the same for server-side and client-side variants const submitters = [ - impressionsSyncTaskFactory(splitApi.postTestImpressionsBulk, storage.impressions, settings.scheduler.impressionsRefreshRate, settings.core.labelsEnabled), - eventsSyncTaskFactory(splitApi.postEventsBulk, storage.events, settings.scheduler.eventsPushRate, settings.startup.eventsFirstPushWindow) + impressionsSyncTaskFactory(log, splitApi.postTestImpressionsBulk, storage.impressions, settings.scheduler.impressionsRefreshRate, settings.core.labelsEnabled), + eventsSyncTaskFactory(log, splitApi.postEventsBulk, storage.events, settings.scheduler.eventsPushRate, settings.startup.eventsFirstPushWindow) // @TODO add telemetry submitter ]; - if (storage.impressionCounts) submitters.push(impressionCountsSyncTaskFactory(splitApi.postTestImpressionsCount, storage.impressionCounts)); + if (storage.impressionCounts) submitters.push(impressionCountsSyncTaskFactory(log, splitApi.postTestImpressionsCount, storage.impressionCounts)); const submitter = syncTaskComposite(submitters); @@ -58,15 +59,15 @@ export function syncManagerOnlineFactory( function startPolling() { if (!pollingManager.isRunning()) { - log.info('Streaming not available. Starting periodic fetch of data.'); + log.info(SYNC_START_POLLING); pollingManager.start(); } else { - log.info('Streaming couldn\'t connect. Continue periodic fetch of data.'); + log.info(SYNC_CONTINUE_POLLING); } } function stopPollingAndSyncAll() { - log.info('PUSH (re)connected. Syncing and stopping periodic fetch of data.'); + log.info(SYNC_STOP_POLLING); // if polling, stop if (pollingManager.isRunning()) pollingManager.stop(); diff --git a/src/sync/syncTask.ts b/src/sync/syncTask.ts index 530fc9b5..321ea99e 100644 --- a/src/sync/syncTask.ts +++ b/src/sync/syncTask.ts @@ -1,11 +1,11 @@ -import { logFactory } from '../logger/sdkLogger'; +import { SYNC_TASK_EXECUTE, SYNC_TASK_START, SYNC_TASK_STOP } from '../logger/constants'; +import { ILogger } from '../logger/types'; import { ISyncTask } from './types'; -const log = logFactory('splitio-sync:task'); /** * factory of sync tasks */ -export default function syncTaskFactory(task: (...args: Input) => Promise, period: number, taskName = 'task'): ISyncTask { +export default function syncTaskFactory(log: ILogger, task: (...args: Input) => Promise, period: number, taskName = 'task'): ISyncTask { // flag that indicates if the task is being executed let executing = false; @@ -16,7 +16,7 @@ export default function syncTaskFactory(task: (...a function execute(...args: Input) { executing = true; - log.debug(`Running ${taskName}`); + log.debug(SYNC_TASK_EXECUTE, [taskName]); return task(...args).then(result => { executing = false; if (running) timeoutID = setTimeout(execute, period, ...args); @@ -34,7 +34,7 @@ export default function syncTaskFactory(task: (...a start(...args: Input) { if (!running) { running = true; - log.debug(`Starting ${taskName}. Running each ${period} millis`); + log.debug(SYNC_TASK_START, [taskName, period]); return execute(...args); } }, @@ -42,7 +42,7 @@ export default function syncTaskFactory(task: (...a stop() { running = false; if (timeoutID) { - log.debug(`Stopping ${taskName}`); + log.debug(SYNC_TASK_STOP, [taskName]); clearTimeout(timeoutID); timeoutID = undefined; } diff --git a/src/trackers/__tests__/eventTracker.spec.ts b/src/trackers/__tests__/eventTracker.spec.ts index 57c44655..0edb462e 100644 --- a/src/trackers/__tests__/eventTracker.spec.ts +++ b/src/trackers/__tests__/eventTracker.spec.ts @@ -1,3 +1,4 @@ +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { SplitIO } from '../../types'; import eventTrackerFactory from '../eventTracker'; @@ -17,7 +18,7 @@ describe('Event Tracker', () => { test('Tracker API', () => { expect(typeof eventTrackerFactory).toBe('function'); // The module should return a function which acts as a factory. - const instance = eventTrackerFactory(fakeEventsCache, fakeIntegrationsManager); + const instance = eventTrackerFactory(loggerMock, fakeEventsCache, fakeIntegrationsManager); expect(typeof instance.track).toBe('function'); // The instance should implement the track method. }); @@ -45,11 +46,11 @@ describe('Event Tracker', () => { } }); - const tracker = eventTrackerFactory(fakeEventsCache, fakeIntegrationsManager); + const tracker = eventTrackerFactory(loggerMock, fakeEventsCache, fakeIntegrationsManager); const result1 = tracker.track(fakeEvent, 1); expect(fakeEventsCache.track.mock.calls[0]).toEqual([fakeEvent, 1]); // Should be present in the event cache. - expect(fakeIntegrationsManager.handleEvent.mock.calls.length).toBe(0); // The integration manager handleEvent method should not be executed synchronously. + expect(fakeIntegrationsManager.handleEvent).not.toBeCalled(); // The integration manager handleEvent method should not be executed synchronously. expect(result1).toBe(true); // Should return the value of the event cache. setTimeout(() => { @@ -64,14 +65,14 @@ describe('Event Tracker', () => { expect(tracked).toBe(false); // Should return the value of the event cache resolved promise. setTimeout(() => { - expect(fakeIntegrationsManager.handleEvent.mock.calls.length).toBe(1); // Untracked event should not be sent to integration manager. + expect(fakeIntegrationsManager.handleEvent).toBeCalledTimes(1); // Untracked event should not be sent to integration manager. const result3 = tracker.track(fakeEvent, 3) as Promise; expect(fakeEventsCache.track.mock.calls[2]).toEqual([fakeEvent, 3]); // Should be present in the event cache. result3.then(tracked => { - expect(fakeIntegrationsManager.handleEvent.mock.calls.length).toBe(1); // Tracked event should not be sent to integration manager synchronously + expect(fakeIntegrationsManager.handleEvent).toBeCalledTimes(1); // Tracked event should not be sent to integration manager synchronously expect(tracked).toBe(true); // Should return the value of the event cache resolved promise. setTimeout(() => { diff --git a/src/trackers/__tests__/impressionsTracker.spec.ts b/src/trackers/__tests__/impressionsTracker.spec.ts index f45de3ed..9a89a61d 100644 --- a/src/trackers/__tests__/impressionsTracker.spec.ts +++ b/src/trackers/__tests__/impressionsTracker.spec.ts @@ -4,6 +4,7 @@ import { impressionObserverSSFactory } from '../impressionObserver/impressionObs import { impressionObserverCSFactory } from '../impressionObserver/impressionObserverCS'; import { IMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; /* Mocks */ @@ -36,13 +37,13 @@ describe('Impressions Tracker', () => { expect(typeof impressionsTrackerFactory).toBe('function'); // The module should return a function which acts as a factory. const { fakeImpressionsCache, fakeMetadata } = generateMocks(); - const instance = impressionsTrackerFactory(fakeImpressionsCache, fakeMetadata); + const instance = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata); expect(typeof instance.track).toBe('function'); // The instance should implement the track method which will actually track queued impressions. }); test('Should be able to track impressions (in DEBUG mode without Previous Time).', () => { const { fakeImpressionsCache, fakeMetadata } = generateMocks(); - const tracker = impressionsTrackerFactory(fakeImpressionsCache, fakeMetadata); + const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata); const imp1 = { feature: '10', @@ -54,7 +55,7 @@ describe('Impressions Tracker', () => { feature: '30', } as ImpressionDTO; - expect(fakeImpressionsCache.track.mock.calls.length).toBe(0); // cache method should not be called by just creating a tracker + expect(fakeImpressionsCache.track).not.toBeCalled(); // cache method should not be called by just creating a tracker tracker.track([imp1, imp2, imp3]); @@ -63,7 +64,7 @@ describe('Impressions Tracker', () => { test('Tracked impressions should be sent to impression listener and integration manager when we invoke .track()', (done) => { const { fakeImpressionsCache, fakeMetadata, fakeListener, fakeIntegrationsManager } = generateMocks(); - const tracker = impressionsTrackerFactory(fakeImpressionsCache, fakeMetadata, fakeListener, fakeIntegrationsManager); + const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata, fakeListener, fakeIntegrationsManager); const fakeImpression = { feature: 'impression' @@ -75,20 +76,20 @@ describe('Impressions Tracker', () => { fake: 'attributes' }; - expect(fakeImpressionsCache.track.mock.calls.length).toBe(0); // The storage should not be invoked if we haven't tracked impressions. - expect(fakeListener.logImpression.mock.calls.length).toBe(0); // The listener should not be invoked if we haven't tracked impressions. - expect(fakeIntegrationsManager.handleImpression.mock.calls.length).toBe(0); // The integrations manager handleImpression method should not be invoked if we haven't tracked impressions. + expect(fakeImpressionsCache.track).not.toBeCalled(); // The storage should not be invoked if we haven't tracked impressions. + expect(fakeListener.logImpression).not.toBeCalled(); // The listener should not be invoked if we haven't tracked impressions. + expect(fakeIntegrationsManager.handleImpression).not.toBeCalled(); // The integrations manager handleImpression method should not be invoked if we haven't tracked impressions. // We signal that we actually want to track the queued impressions. tracker.track([fakeImpression, fakeImpression2], fakeAttributes); expect(fakeImpressionsCache.track.mock.calls[0][0]).toEqual([fakeImpression, fakeImpression2]); // Even with a listener, impression should be sent to the cache - expect(fakeListener.logImpression.mock.calls.length).toBe(0); // The listener should not be executed synchronously. - expect(fakeIntegrationsManager.handleImpression.mock.calls.length).toBe(0); // The integrations manager handleImpression method should not be executed synchronously. + expect(fakeListener.logImpression).not.toBeCalled(); // The listener should not be executed synchronously. + expect(fakeIntegrationsManager.handleImpression).not.toBeCalled(); // The integrations manager handleImpression method should not be executed synchronously. setTimeout(() => { - expect(fakeListener.logImpression.mock.calls.length).toBe(2); // The listener should be executed after the timeout wrapping make it to the queue stack, once per each tracked impression. - expect(fakeIntegrationsManager.handleImpression.mock.calls.length).toBe(2); // The integrations manager handleImpression method should be executed after the timeout wrapping make it to the queue stack, once per each tracked impression. + expect(fakeListener.logImpression).toBeCalledTimes(2); // The listener should be executed after the timeout wrapping make it to the queue stack, once per each tracked impression. + expect(fakeIntegrationsManager.handleImpression).toBeCalledTimes(2); // The integrations manager handleImpression method should be executed after the timeout wrapping make it to the queue stack, once per each tracked impression. const impressionData1 = { impression: fakeImpression, attributes: fakeAttributes, sdkLanguageVersion: fakeMetadata.version, ip: fakeMetadata.ip, hostname: fakeMetadata.hostname }; const impressionData2 = { impression: fakeImpression2, attributes: fakeAttributes, sdkLanguageVersion: fakeMetadata.version, ip: fakeMetadata.ip, hostname: fakeMetadata.hostname }; @@ -138,11 +139,11 @@ describe('Impressions Tracker', () => { impression3.time = 1234567891; const trackers = [ - impressionsTrackerFactory(fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverSSFactory()), - impressionsTrackerFactory(fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverCSFactory()) + impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverSSFactory()), + impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverCSFactory()) ]; - expect(fakeImpressionsCache.track.mock.calls.length).toBe(0); // storage method should not be called until impressions are tracked. + expect(fakeImpressionsCache.track).not.toBeCalled(); // storage method should not be called until impressions are tracked. trackers.forEach(tracker => { tracker.track([impression, impression2, impression3]); @@ -167,9 +168,9 @@ describe('Impressions Tracker', () => { impression3.time = Date.now(); const impressionCountsCache = new ImpressionCountsCacheInMemory(); - const tracker = impressionsTrackerFactory(fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverCSFactory(), impressionCountsCache); + const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverCSFactory(), impressionCountsCache); - expect(fakeImpressionsCache.track.mock.calls.length).toBe(0); // cache method should not be called by just creating a tracker + expect(fakeImpressionsCache.track).not.toBeCalled(); // cache method should not be called by just creating a tracker tracker.track([impression, impression2, impression3]); diff --git a/src/trackers/eventTracker.ts b/src/trackers/eventTracker.ts index 081bef70..b18c0546 100644 --- a/src/trackers/eventTracker.ts +++ b/src/trackers/eventTracker.ts @@ -1,10 +1,10 @@ import objectAssign from 'object-assign'; import thenable from '../utils/promise/thenable'; -import { logFactory } from '../logger/sdkLogger'; import { IEventsCacheBase } from '../storages/types'; import { IEventsHandler, IEventTracker } from './types'; import { SplitIO } from '../types'; -const log = logFactory('splitio-client:event-tracker'); +import { ILogger } from '../logger/types'; +import { EVENTS_TRACKER_SUCCESS, ERROR_EVENTS_TRACKER } from '../logger/constants'; /** * Event tracker stores events in cache and pass them to the integrations manager if provided. @@ -13,6 +13,7 @@ const log = logFactory('splitio-client:event-tracker'); * @param integrationsManager optional event handler used for integrations */ export default function eventTrackerFactory( + log: ILogger, eventsCache: IEventsCacheBase, integrationsManager?: IEventsHandler ): IEventTracker { @@ -23,7 +24,7 @@ export default function eventTrackerFactory( const msg = `event of type "${eventTypeId}" for traffic type "${trafficTypeName}". Key: ${key}. Value: ${value}. Timestamp: ${timestamp}. ${properties ? 'With properties.' : 'With no properties.'}`; if (tracked) { - log.info(`Successfully qeued ${msg}`); + log.info(EVENTS_TRACKER_SUCCESS, [msg]); if (integrationsManager) { // Wrap in a timeout because we don't want it to be blocking. setTimeout(function () { @@ -35,7 +36,7 @@ export default function eventTrackerFactory( }, 0); } } else { - log.warn(`Failed to queue ${msg}`); + log.error(ERROR_EVENTS_TRACKER, [msg]); } return tracked; diff --git a/src/trackers/impressionsTracker.ts b/src/trackers/impressionsTracker.ts index aa9bf94f..5d227892 100644 --- a/src/trackers/impressionsTracker.ts +++ b/src/trackers/impressionsTracker.ts @@ -1,13 +1,13 @@ import objectAssign from 'object-assign'; import thenable from '../utils/promise/thenable'; import { truncateTimeFrame } from '../utils/time'; -import { logFactory } from '../logger/sdkLogger'; import { IImpressionCountsCacheBase, IImpressionsCacheBase } from '../storages/types'; import { IImpressionsHandler, IImpressionsTracker } from './types'; import { IMetadata } from '../dtos/types'; import { SplitIO, ImpressionDTO } from '../types'; import { IImpressionObserver } from './impressionObserver/types'; -const log = logFactory('splitio-client:impressions-tracker'); +import { ILogger } from '../logger/types'; +import { IMPRESSIONS_TRACKER_SUCCESS, ERROR_IMPRESSIONS_TRACKER, ERROR_IMPRESSIONS_LISTENER } from '../logger/constants'; /** * Impressions tracker stores impressions in cache and pass them to the listener and integrations manager if provided. @@ -20,6 +20,7 @@ const log = logFactory('splitio-client:impressions-tracker'); * @param countsCache optional cache to save impressions count. If provided, impressions will be deduped (OPTIMIZED mode) */ export default function impressionsTrackerFactory( + log: ILogger, impressionsCache: IImpressionsCacheBase, // @TODO consider passing only an optional integrationsManager to handle impressions @@ -62,9 +63,9 @@ export default function impressionsTrackerFactory( // If we're on an async storage, handle error and log it. if (thenable(res)) { res.then(() => { - log.debug(`Successfully stored ${impressionsCount} impression${impressionsCount === 1 ? '' : 's'}.`); + log.info(IMPRESSIONS_TRACKER_SUCCESS, [impressionsCount]); }).catch(err => { - log.error(`Could not store impressions bulk with ${impressionsCount} impression${impressionsCount === 1 ? '' : 's'}. Error: ${err}`); + log.error(ERROR_IMPRESSIONS_TRACKER, [impressionsCount, err]); }); } @@ -88,7 +89,7 @@ export default function impressionsTrackerFactory( try { // An exception on the listeners should not break the SDK. if (impressionListener) impressionListener.logImpression(impressionData); } catch (err) { - log.error(`Impression listener logImpression method threw: ${err}.`); + log.error(ERROR_IMPRESSIONS_LISTENER, [err]); } }, 0); } diff --git a/src/types.ts b/src/types.ts index c21bea1c..ae2d3fd1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import { IIntegration, IIntegrationFactoryParams } from './integrations/types'; +import { ILogger } from './logger/types'; /* eslint-disable no-use-before-define */ import { IStorageFactoryParams, IStorageSyncCS, IStorageSync, IStorageAsync } from './storages/types'; @@ -88,7 +89,7 @@ export interface ISettings { auth: string, streaming: string }, - readonly debug: boolean, + readonly debug: boolean | LogLevel, readonly version: string, features: SplitIO.MockedFeaturesFilePath | SplitIO.MockedFeaturesMap, readonly streamingEnabled: boolean, @@ -99,7 +100,8 @@ export interface ISettings { readonly runtime: { ip: string | false hostname: string | false - } + }, + readonly log: ILogger } /** * Log levels. diff --git a/src/utils/inputValidation/__tests__/apiKey.spec.ts b/src/utils/inputValidation/__tests__/apiKey.spec.ts index 1fd8aa76..c1cbb25d 100644 --- a/src/utils/inputValidation/__tests__/apiKey.spec.ts +++ b/src/utils/inputValidation/__tests__/apiKey.spec.ts @@ -1,37 +1,32 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_EMPTY, ERROR_NULL, ERROR_INVALID, WARN_API_KEY } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateApiKey, validateAndTrackApiKey, releaseApiKey } from '../apiKey'; -const errorMsgs = { - WRONG_TYPE_API_KEY: 'Factory instantiation: you passed an invalid api_key, api_key must be a non-empty string.', - EMPTY_API_KEY: 'Factory instantiation: you passed an empty api_key, api_key must be a non-empty string.', - NULL_API_KEY: 'Factory instantiation: you passed a null or undefined api_key, api_key must be a non-empty string.' -}; - const invalidKeys = [ - { key: '', msg: errorMsgs.EMPTY_API_KEY }, - { key: null, msg: errorMsgs.NULL_API_KEY }, - { key: undefined, msg: errorMsgs.NULL_API_KEY }, - { key: () => { }, msg: errorMsgs.WRONG_TYPE_API_KEY }, - { key: new Promise(r => r()), msg: errorMsgs.WRONG_TYPE_API_KEY }, - { key: Symbol('asd'), msg: errorMsgs.WRONG_TYPE_API_KEY }, - { key: [], msg: errorMsgs.WRONG_TYPE_API_KEY }, - { key: true, msg: errorMsgs.WRONG_TYPE_API_KEY }, - { key: NaN, msg: errorMsgs.WRONG_TYPE_API_KEY }, - { key: Infinity, msg: errorMsgs.WRONG_TYPE_API_KEY }, - { key: -Infinity, msg: errorMsgs.WRONG_TYPE_API_KEY }, - { key: {}, msg: errorMsgs.WRONG_TYPE_API_KEY } + { key: '', msg: ERROR_EMPTY }, + { key: null, msg: ERROR_NULL }, + { key: undefined, msg: ERROR_NULL }, + { key: () => { }, msg: ERROR_INVALID }, + { key: new Promise(r => r()), msg: ERROR_INVALID }, + { key: Symbol('asd'), msg: ERROR_INVALID }, + { key: [], msg: ERROR_INVALID }, + { key: true, msg: ERROR_INVALID }, + { key: NaN, msg: ERROR_INVALID }, + { key: Infinity, msg: ERROR_INVALID }, + { key: -Infinity, msg: ERROR_INVALID }, + { key: {}, msg: ERROR_INVALID } ]; describe('validateApiKey', () => { + afterEach(() => { loggerMock.mockClear(); }); + test('Should return the passed api key if it is a valid string without logging any errors', () => { const validApiKey = 'qjok3snti4dgsticade5hfphmlucarsflv14'; - expect(validateApiKey(validApiKey)).toBe(validApiKey); // It should return the passed string if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - - mockClear(); + expect(validateApiKey(loggerMock, validApiKey)).toBe(validApiKey); // It should return the passed string if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. }); test('Should return false and log error if the api key is invalid', () => { @@ -39,87 +34,83 @@ describe('validateApiKey', () => { const invalidApiKey = invalidKeys[i]['key']; const expectedLog = invalidKeys[i]['msg']; - expect(validateApiKey(invalidApiKey)).toBe(false); // Invalid strings should return false. + expect(validateApiKey(loggerMock, invalidApiKey)).toBe(false); // Invalid strings should return false. expect(loggerMock.error.mock.calls[0][0]).toEqual(expectedLog); // The error should be logged for the invalid string. loggerMock.error.mockClear(); } - - mockClear(); }); }); describe('validateAndTrackApiKey', () => { + afterEach(() => { loggerMock.mockClear(); }); + test('Should log a warning if we are instantiating more than one factory (different api keys)', () => { const validApiKey1 = 'qjok3snti4dgsticade5hfphmlucarsflv14'; const validApiKey2 = 'qjok3snti4dgsticade5hfphmlucars92uih'; const validApiKey3 = '84ynbsnti4dgsticade5hfphmlucars92uih'; - expect(validateAndTrackApiKey(validApiKey1)).toBe(validApiKey1); - expect(loggerMock.warn.mock.calls.length).toBe(0); // If this is the first api key we are registering, there is no warning. - - expect(validateAndTrackApiKey(validApiKey2)).toBe(validApiKey2); - expect(loggerMock.warn.mock.calls).toEqual([['Factory instantiation: You already have an instance of the Split factory. Make sure you definitely want this additional instance. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application.']]); // We register a new api key, we get a warning. + expect(validateAndTrackApiKey(loggerMock, validApiKey1)).toBe(validApiKey1); + expect(loggerMock.warn).not.toBeCalled(); // If this is the first api key we are registering, there is no warning. - expect(validateAndTrackApiKey(validApiKey3)).toBe(validApiKey3); - expect(loggerMock.warn.mock.calls[0]).toEqual(['Factory instantiation: You already have an instance of the Split factory. Make sure you definitely want this additional instance. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application.']); // We register a new api key, we get a warning. + expect(validateAndTrackApiKey(loggerMock, validApiKey2)).toBe(validApiKey2); + expect(validateAndTrackApiKey(loggerMock, validApiKey3)).toBe(validApiKey3); + expect(loggerMock.warn).toBeCalledWith(WARN_API_KEY, ['an instance of the Split factory']); // we get a warning when we register a new api key. // We will release the used keys and expect no warnings next time. releaseApiKey(validApiKey1); releaseApiKey(validApiKey2); releaseApiKey(validApiKey3); - mockClear(); + loggerMock.mockClear(); - expect(validateAndTrackApiKey(validApiKey1)).toBe(validApiKey1); - expect(loggerMock.warn.mock.calls.length).toBe(0); // If all the keys were released and we try again, there is no warning. + expect(validateAndTrackApiKey(loggerMock, validApiKey1)).toBe(validApiKey1); + expect(loggerMock.warn).not.toBeCalled(); // If all the keys were released and we try again, there is no warning. releaseApiKey(validApiKey1); // clean up the cache of api keys for next test - mockClear(); }); test('Should log a warning if we are instantiating more than one factory (same api key)', () => { const validApiKey = '84ynbsnti4dgsticade5hfphmlucars92uih'; - expect(validateAndTrackApiKey(validApiKey)).toBe(validApiKey); - expect(loggerMock.warn.mock.calls.length).toBe(0); // If this is the first api key we are registering, there is no warning. + expect(validateAndTrackApiKey(loggerMock, validApiKey)).toBe(validApiKey); + expect(loggerMock.warn).not.toBeCalled(); // If this is the first api key we are registering, there is no warning. - expect(validateAndTrackApiKey(validApiKey)).toBe(validApiKey); + expect(validateAndTrackApiKey(loggerMock, validApiKey)).toBe(validApiKey); // Same key one more time, 2 instances plus new one. - expect(validateAndTrackApiKey(validApiKey)).toBe(validApiKey); + expect(validateAndTrackApiKey(loggerMock, validApiKey)).toBe(validApiKey); // Same key one more time, 3 instances plus new one. - expect(validateAndTrackApiKey(validApiKey)).toBe(validApiKey); + expect(validateAndTrackApiKey(loggerMock, validApiKey)).toBe(validApiKey); expect(loggerMock.warn.mock.calls).toEqual([ - ['Factory instantiation: You already have 1 factory with this API Key. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application.'], // We register a the same api key again, we get a warning with the number of instances we have. - ['Factory instantiation: You already have 2 factories with this API Key. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application.'], // We register a the same api key again, we get a warning with the number of instances we have. - ['Factory instantiation: You already have 3 factories with this API Key. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application.'] // We register a the same api key again, we get a warning with the number of instances we have. - ]); + [WARN_API_KEY, ['1 factory with this API Key']], + [WARN_API_KEY, ['2 factories with this API Key']], + [WARN_API_KEY, ['3 factories with this API Key']] + ]); // We get a warning each time we register the same api key, with the number of instances we have. // We will release the used api key leaving only 1 "use" on the cache. releaseApiKey(validApiKey); releaseApiKey(validApiKey); releaseApiKey(validApiKey); - mockClear(); + loggerMock.mockClear(); // So we get the warning again. - expect(validateAndTrackApiKey(validApiKey)).toBe(validApiKey); - expect(loggerMock.warn.mock.calls).toEqual([['Factory instantiation: You already have 1 factory with this API Key. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application.']]); // We register a the same api key again, we get a warning with the number of instances we have. + expect(validateAndTrackApiKey(loggerMock, validApiKey)).toBe(validApiKey); + expect(loggerMock.warn).toBeCalledWith(WARN_API_KEY, ['1 factory with this API Key']); // Leave it with 0 releaseApiKey(validApiKey); releaseApiKey(validApiKey); - mockClear(); + loggerMock.mockClear(); - expect(validateAndTrackApiKey(validApiKey)).toBe(validApiKey); - expect(loggerMock.warn.mock.calls.length).toBe(0); // s users, there is no warning when we use it again. + expect(validateAndTrackApiKey(loggerMock, validApiKey)).toBe(validApiKey); + expect(loggerMock.warn).not.toBeCalled(); // s users, there is no warning when we use it again. releaseApiKey(validApiKey); // clean up the cache just in case a new test is added - mockClear(); }); }); diff --git a/src/utils/inputValidation/__tests__/attributes.spec.ts b/src/utils/inputValidation/__tests__/attributes.spec.ts index 6331c325..e414b4ea 100644 --- a/src/utils/inputValidation/__tests__/attributes.spec.ts +++ b/src/utils/inputValidation/__tests__/attributes.spec.ts @@ -1,4 +1,5 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_NOT_PLAIN_OBJECT } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateAttributes } from '../attributes'; @@ -18,37 +19,33 @@ const invalidAttributes = [ describe('INPUT VALIDATION for Attributes', () => { + afterEach(() => { loggerMock.mockClear(); }); + test('Should return the passed object if it is a valid attributes map without logging any errors', () => { const validAttributes = { amIvalid: 'yes', 'are_you_sure': true, howMuch: 10 }; - expect(validateAttributes(validAttributes, 'some_method_attrs')).toEqual(validAttributes); // It should return the passed map if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear; + expect(validateAttributes(loggerMock, validAttributes, 'some_method_attrs')).toEqual(validAttributes); // It should return the passed map if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should return the passed value if it is null or undefined (since attributes are optional) without logging any errors', () => { - expect(validateAttributes(null, 'some_method_attrs')).toBe(null); // It should return the passed null. - expect(validateAttributes(undefined, 'some_method_attrs')).toBe(undefined); // It should return the passed undefined. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(validateAttributes(loggerMock, null, 'some_method_attrs')).toBe(null); // It should return the passed null. + expect(validateAttributes(loggerMock, undefined, 'some_method_attrs')).toBe(undefined); // It should return the passed undefined. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should return false and log error if attributes map is invalid', () => { for (let i = 0; i < invalidAttributes.length; i++) { const invalidAttribute = invalidAttributes[i]; - expect(validateAttributes(invalidAttribute, 'test_method')).toBe(false); // Invalid attribute objects should return false. - expect(loggerMock.error.mock.calls[0][0]).toEqual('test_method: attributes must be a plain object.'); // The error should be logged for the invalid attributes map. + expect(validateAttributes(loggerMock, invalidAttribute, 'test_method')).toBe(false); // Invalid attribute objects should return false. + expect(loggerMock.error).lastCalledWith(ERROR_NOT_PLAIN_OBJECT, ['test_method', 'attributes']); // The error should be logged for the invalid attributes map. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); }); diff --git a/src/utils/inputValidation/__tests__/event.spec.ts b/src/utils/inputValidation/__tests__/event.spec.ts index 1d8d2209..2e54136b 100644 --- a/src/utils/inputValidation/__tests__/event.spec.ts +++ b/src/utils/inputValidation/__tests__/event.spec.ts @@ -1,65 +1,57 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_INVALID, ERROR_EMPTY, ERROR_NULL, ERROR_EVENT_TYPE_FORMAT } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateEvent } from '../event'; -const errorMsgs = { - NULL_EVENT: () => 'you passed a null or undefined event_type, event_type must be a non-empty string.', - WRONG_TYPE_EVENT: () => 'you passed an invalid event_type, event_type must be a non-empty string.', - EMPTY_EVENT: () => 'you passed an empty event_type, event_type must be a non-empty string.', - WRONG_FORMAT_EVENT: (invalidEvent: any) => `you passed "${invalidEvent}", 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.` -}; - const invalidEvents = [ - { event: [], msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: () => { }, msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: false, msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: true, msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: {}, msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: Object.create({}), msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: 'something+withInvalidchars', msg: errorMsgs.WRONG_FORMAT_EVENT }, - { event: 'with spaces', msg: errorMsgs.WRONG_FORMAT_EVENT }, - { event: ' asd', msg: errorMsgs.WRONG_FORMAT_EVENT }, - { event: 'asd ', msg: errorMsgs.WRONG_FORMAT_EVENT }, - { event: '?', msg: errorMsgs.WRONG_FORMAT_EVENT }, - { event: '', msg: errorMsgs.EMPTY_EVENT }, - { event: NaN, msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: -Infinity, msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: Infinity, msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: new Promise(res => res), msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: Symbol('asd'), msg: errorMsgs.WRONG_TYPE_EVENT }, - { event: null, msg: errorMsgs.NULL_EVENT }, - { event: undefined, msg: errorMsgs.NULL_EVENT } + { event: [], msg: ERROR_INVALID }, + { event: () => { }, msg: ERROR_INVALID }, + { event: false, msg: ERROR_INVALID }, + { event: true, msg: ERROR_INVALID }, + { event: {}, msg: ERROR_INVALID }, + { event: Object.create({}), msg: ERROR_INVALID }, + { event: 'something+withInvalidchars', msg: ERROR_EVENT_TYPE_FORMAT }, + { event: 'with spaces', msg: ERROR_EVENT_TYPE_FORMAT }, + { event: ' asd', msg: ERROR_EVENT_TYPE_FORMAT }, + { event: 'asd ', msg: ERROR_EVENT_TYPE_FORMAT }, + { event: '?', msg: ERROR_EVENT_TYPE_FORMAT }, + { event: '', msg: ERROR_EMPTY }, + { event: NaN, msg: ERROR_INVALID }, + { event: -Infinity, msg: ERROR_INVALID }, + { event: Infinity, msg: ERROR_INVALID }, + { event: new Promise(res => res), msg: ERROR_INVALID }, + { event: Symbol('asd'), msg: ERROR_INVALID }, + { event: null, msg: ERROR_NULL }, + { event: undefined, msg: ERROR_NULL } ]; describe('INPUT VALIDATION for Event types', () => { - test('Should return the provided event type if it is a valid string without logging any errors', () => { + afterEach(() => { loggerMock.mockClear(); }); - expect(validateEvent('valid:Too', 'some_method_eventType')).toBe('valid:Too'); // It should return the provided string if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(validateEvent('I.am.valid-string_ValUe', 'some_method_eventType')).toBe('I.am.valid-string_ValUe'); // It should return the provided string if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(validateEvent('a', 'some_method_eventType')).toBe('a'); // It should return the provided string if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. + test('Should return the provided event type if it is a valid string without logging any errors', () => { - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(validateEvent(loggerMock, 'valid:Too', 'some_method_eventType')).toBe('valid:Too'); // It should return the provided string if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(validateEvent(loggerMock, 'I.am.valid-string_ValUe', 'some_method_eventType')).toBe('I.am.valid-string_ValUe'); // It should return the provided string if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(validateEvent(loggerMock, 'a', 'some_method_eventType')).toBe('a'); // It should return the provided string if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should return false and log error if event type is not a valid string', () => { for (let i = 0; i < invalidEvents.length; i++) { const invalidValue = invalidEvents[i]['event']; - const expectedLog = invalidEvents[i]['msg'](invalidValue); + const expectedLog = invalidEvents[i]['msg']; - expect(validateEvent(invalidValue, 'test_method')).toBe(false); // Invalid event types should always return false. - expect(loggerMock.error.mock.calls[0][0]).toEqual(`test_method: ${expectedLog}`); // Should log the error for the invalid event type. + expect(validateEvent(loggerMock, invalidValue, 'test_method')).toBe(false); // Invalid event types should always return false. + expect(loggerMock.error).toBeCalledWith(expectedLog, expectedLog === ERROR_EVENT_TYPE_FORMAT ? ['test_method', invalidValue] : ['test_method']); // Should log the error for the invalid event type. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); }); diff --git a/src/utils/inputValidation/__tests__/eventProperties.spec.ts b/src/utils/inputValidation/__tests__/eventProperties.spec.ts index 057311ac..d17f08bd 100644 --- a/src/utils/inputValidation/__tests__/eventProperties.spec.ts +++ b/src/utils/inputValidation/__tests__/eventProperties.spec.ts @@ -1,4 +1,5 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_NOT_PLAIN_OBJECT, ERROR_SIZE_EXCEEDED, WARN_SETTING_NULL, WARN_TRIMMING_PROPERTIES } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateEventProperties } from '../eventProperties'; @@ -34,39 +35,37 @@ const invalidValues = [ describe('INPUT VALIDATION for Event Properties', () => { + afterEach(() => { loggerMock.mockClear(); }); + test('Not setting the properties object is acceptable', () => { - expect(validateEventProperties(undefined, 'some_method_eventProps')).toEqual({ + expect(validateEventProperties(loggerMock, undefined, 'some_method_eventProps')).toEqual({ properties: null, size: 1024 }); // It should return null in replacement of undefined since it is valid with default event size.'); - expect(validateEventProperties(undefined, 'some_method_eventProps')).toEqual({ + expect(validateEventProperties(loggerMock, undefined, 'some_method_eventProps')).toEqual({ properties: null, size: 1024 }); // It should return the passed null since it is valid with default event size.'); - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('When setting a value for properties, only objects are acceptable', () => { invalidValues.forEach(val => { - expect(validateEventProperties(val, 'some_method_eventProps')).toEqual({ + expect(validateEventProperties(loggerMock, val, 'some_method_eventProps')).toEqual({ properties: false, size: 1024 }); // It should return default size and properties false if the properties value is not an object or null/undefined.'); - expect(loggerMock.error.mock.calls).toEqual([['some_method_eventProps: properties must be a plain object.']]); // Should log an error. + expect(loggerMock.error).toBeCalledWith(ERROR_NOT_PLAIN_OBJECT, ['some_method_eventProps', 'properties']); // Should log an error. loggerMock.error.mockClear(); }); - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('It should return the properties object when valid and also the correct event size', () => { @@ -79,7 +78,7 @@ describe('INPUT VALIDATION for Event Properties', () => { number2: 0.250, nullProp: null }; - const output = validateEventProperties(validProperties, 'some_method_eventProps'); + const output = validateEventProperties(loggerMock, validProperties, 'some_method_eventProps'); expect(output).toEqual({ properties: validProperties, @@ -89,10 +88,8 @@ describe('INPUT VALIDATION for Event Properties', () => { expect(validProperties).not.toBe(output.properties); // Returned properties should be a clone. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('It should return the properties object when valid and also the correct event size, nulling any invalid prop', () => { @@ -108,7 +105,7 @@ describe('INPUT VALIDATION for Event Properties', () => { willBeNulled3: [], willBeNulled4: new Map() }; - const output = validateEventProperties(providedProperties, 'some_method_eventProps'); + const output = validateEventProperties(loggerMock, providedProperties, 'some_method_eventProps'); expect(output).toEqual({ properties: { @@ -121,14 +118,12 @@ describe('INPUT VALIDATION for Event Properties', () => { expect(providedProperties).not.toBe(output.properties); // Returned properties should be a clone. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(4); // It should have logged one warning per each property of the invalid type. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).toBeCalledTimes(4); // It should have logged one warning per each property of the invalid type. ['willBeNulled1', 'willBeNulled2', 'willBeNulled3', 'willBeNulled4'].forEach((key, index) => { - expect(loggerMock.warn.mock.calls[index][0]).toBe(`some_method_eventProps: Property ${key} is of invalid type. Setting value to null.`); + expect(loggerMock.warn.mock.calls[index]).toEqual([WARN_SETTING_NULL, ['some_method_eventProps', key]]); }); - - mockClear(); }); test('It should log a warning if the object has more than the max amount of allowed keys, logging a warning and returning the event (if other validations pass)', () => { @@ -137,7 +132,7 @@ describe('INPUT VALIDATION for Event Properties', () => { for (let i = 0; i < 300; i++) { validProperties[i] = null; // all will be null so we do not exceed the size. } - let output = validateEventProperties(validProperties, 'some_method_eventProps'); + let output = validateEventProperties(loggerMock, validProperties, 'some_method_eventProps'); expect(output).toEqual({ properties: validProperties, @@ -145,12 +140,12 @@ describe('INPUT VALIDATION for Event Properties', () => { }); // It should return the properties and the event size.'); - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. // @ts-ignore validProperties.a = null; // Adding one prop to exceed the limit. - output = validateEventProperties(validProperties, 'some_method_eventProps'); + output = validateEventProperties(loggerMock, validProperties, 'some_method_eventProps'); expect(output).toEqual({ properties: validProperties, @@ -158,10 +153,8 @@ describe('INPUT VALIDATION for Event Properties', () => { }); // It should return the properties and the event size.'); - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls).toEqual([['some_method_eventProps: Event has more than 300 properties. Some of them will be trimmed when processed.']]); // It should have logged a warning. - - mockClear(); + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).toBeCalledWith(WARN_TRIMMING_PROPERTIES, ['some_method_eventProps']); // It should have logged a warning. }); const fiveHundredChars = 'JKHSAKFJASHFJKASHSFKHAKJSGJKASGH1234567890JASHGJHASGJKAHSJKGHAJKSHGJKAHGJKASHajksghkjahsgjkhsakjghjkashgjkhagjkhajksghjkahsgjksahgjkahsgjkhasgjkhsagjkabsgjhaenjkrnjkwnqrkjnqwekjrnkjweqntkjnjkenasdjkngjksdajkghkjdasgkjnadsjgn asdkjgnkjsadngkjnasdjkngjknasdkjgnasdlgnsdakgnlkasndugbuoewqoitnwlkgadsgjdnsagubadisugboisdngklasdgndsgbjasdbgjkasbdgubuiqwetoiqhweiojtioweqhtiohqweiohtiowqehtoihewqiobtgoiqwengiowqnegionwqeogiqwneoignqiowegnioqewgnwqoiegnoiqwengiowqnegoinqwgionqwegionwqeoignqwegoinoiadnfaosignoiansgk'; @@ -173,7 +166,7 @@ describe('INPUT VALIDATION for Event Properties', () => { validProperties[i] = fiveHundredChars; // key length is two, plus 510 chars it is 512 which multiplied by the byte size of each char is 1kb each key. } // It should be right on the size limit. - let output = validateEventProperties(validProperties, 'some_method_eventProps'); + let output = validateEventProperties(loggerMock, validProperties, 'some_method_eventProps'); expect(output).toEqual({ properties: validProperties, @@ -182,13 +175,13 @@ describe('INPUT VALIDATION for Event Properties', () => { // It should return the properties and the event size.'); expect(validProperties).not.toBe(output.properties); // Returned properties should be a clone. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // Should not log any warnings. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // Should not log any warnings. // @ts-ignore validProperties.a = null; // exceed by two bytes (1 char string key which is two bytes, null value which we count as 0 to match other SDKs) - output = validateEventProperties(validProperties, 'some_method_eventProps'); + output = validateEventProperties(loggerMock, validProperties, 'some_method_eventProps'); expect(output).toEqual({ properties: false, @@ -196,11 +189,7 @@ describe('INPUT VALIDATION for Event Properties', () => { }); // It should return false instead of the properties and the event size.'); - expect(loggerMock.warn.mock.calls.length).toBe(0); // Should not log any warnings. - expect(loggerMock.error.mock.calls).toEqual([['some_method_eventProps: The maximum size allowed for the properties is 32768 bytes, which was exceeded. Event not queued.']]); // Should log an error. - - loggerMock.error.mockClear(); - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // Should not log any warnings. + expect(loggerMock.error).toBeCalledWith(ERROR_SIZE_EXCEEDED, ['some_method_eventProps']); // Should log an error. }); }); diff --git a/src/utils/inputValidation/__tests__/eventValue.spec.ts b/src/utils/inputValidation/__tests__/eventValue.spec.ts index 042bba6e..dc7d0f33 100644 --- a/src/utils/inputValidation/__tests__/eventValue.spec.ts +++ b/src/utils/inputValidation/__tests__/eventValue.spec.ts @@ -1,4 +1,5 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_NOT_FINITE } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateEventValue } from '../eventValue'; @@ -19,40 +20,36 @@ const invalidValues = [ describe('INPUT VALIDATION for Event Values', () => { - test('Should return the passed value if it is a valid finite number without logging any errors', () => { - expect(validateEventValue(50, 'some_method_eventValue')).toBe(50); // It should return the passed number if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(validateEventValue(-50, 'some_method_eventValue')).toBe(-50); // It should return the passed number if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. + afterEach(() => { loggerMock.mockClear(); }); - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + test('Should return the passed value if it is a valid finite number without logging any errors', () => { + expect(validateEventValue(loggerMock, 50, 'some_method_eventValue')).toBe(50); // It should return the passed number if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(validateEventValue(loggerMock, -50, 'some_method_eventValue')).toBe(-50); // It should return the passed number if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should return the passed value if it is a null or undefined (since it is optional) without logging any errors', () => { - expect(validateEventValue(null, 'some_method_eventValue')).toBe(null); // It should return the passed number if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(validateEventValue(undefined, 'some_method_eventValue')).toBe(undefined); // It should return the passed number if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. + expect(validateEventValue(loggerMock, null, 'some_method_eventValue')).toBe(null); // It should return the passed number if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(validateEventValue(loggerMock, undefined, 'some_method_eventValue')).toBe(undefined); // It should return the passed number if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should return false and log error if event value is not a valid finite number', () => { for (let i = 0; i < invalidValues.length; i++) { const invalidValue = invalidValues[i]; - expect(validateEventValue(invalidValue, 'test_method')).toBe(false); // Invalid event values should always return false. - expect(loggerMock.error.mock.calls[0][0]).toEqual('test_method: value must be a finite number.'); // Should log the error for the invalid event value. + expect(validateEventValue(loggerMock, invalidValue, 'test_method')).toBe(false); // Invalid event values should always return false. + expect(loggerMock.error).toBeCalledWith(ERROR_NOT_FINITE, ['test_method']); // Should log the error for the invalid event value. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); }); diff --git a/src/utils/inputValidation/__tests__/isOperational.spec.ts b/src/utils/inputValidation/__tests__/isOperational.spec.ts index 60681862..f78afb51 100644 --- a/src/utils/inputValidation/__tests__/isOperational.spec.ts +++ b/src/utils/inputValidation/__tests__/isOperational.spec.ts @@ -1,31 +1,30 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { CLIENT_NOT_READY, ERROR_CLIENT_DESTROYED } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateIfNotDestroyed, validateIfOperational } from '../isOperational'; describe('validateIfNotDestroyed', () => { + afterEach(() => { loggerMock.mockClear(); }); + test('Should return true if the client/factory evaluates as operational (not destroyed).', () => { const readinessManagerMock = { isDestroyed: jest.fn(() => false) }; // @ts-ignore - expect(validateIfNotDestroyed(readinessManagerMock)).toBe(true); // It should return true if the client is operational (it is NOT destroyed). - expect(readinessManagerMock.isDestroyed.mock.calls.length).toBe(1); // It checks for destroyed status using the context. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // Should not log any warnings. - - mockClear(); + expect(validateIfNotDestroyed(loggerMock, readinessManagerMock)).toBe(true); // It should return true if the client is operational (it is NOT destroyed). + expect(readinessManagerMock.isDestroyed).toBeCalledTimes(1); // It checks for destroyed status using the context. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // Should not log any warnings. }); test('Should return false and log error if attributes map is invalid', () => { const readinessManagerMock = { isDestroyed: jest.fn(() => true) }; // @ts-ignore - expect(validateIfNotDestroyed(readinessManagerMock)).toBe(false); // It should return false if the client is NOT operational (it is destroyed). - expect(readinessManagerMock.isDestroyed.mock.calls.length).toBe(1); // It checks for destroyed status using the context. - expect(loggerMock.error.mock.calls).toEqual([['Client has already been destroyed - no calls possible.']]); // Should log an error. - expect(loggerMock.warn.mock.calls.length).toBe(0); // But it should not log any warnings. - - mockClear(); + expect(validateIfNotDestroyed(loggerMock, readinessManagerMock, 'test_method')).toBe(false); // It should return false if the client is NOT operational (it is destroyed). + expect(readinessManagerMock.isDestroyed).toBeCalledTimes(1); // It checks for destroyed status using the context. + expect(loggerMock.error).toBeCalledWith(ERROR_CLIENT_DESTROYED, ['test_method']); // Should log an error. + expect(loggerMock.warn).not.toBeCalled(); // But it should not log any warnings. }); }); @@ -35,37 +34,31 @@ describe('validateIfOperational', () => { const readinessManagerMock = { isReady: jest.fn(() => true) }; // @ts-ignore - expect(validateIfOperational(readinessManagerMock, 'test_method')).toBe(true); // It should return true if SDK was ready. - expect(readinessManagerMock.isReady.mock.calls.length).toBe(1); // It checks for readiness status using the context. - expect(loggerMock.warn.mock.calls.length).toBe(0); // But it should not log any warnings. - expect(loggerMock.error.mock.calls.length).toBe(0); // But it should not log any errors. - - mockClear(); + expect(validateIfOperational(loggerMock, readinessManagerMock, 'test_method')).toBe(true); // It should return true if SDK was ready. + expect(readinessManagerMock.isReady).toBeCalledTimes(1); // It checks for readiness status using the context. + expect(loggerMock.warn).not.toBeCalled(); // But it should not log any warnings. + expect(loggerMock.error).not.toBeCalled(); // But it should not log any errors. }); test('Should return true and log nothing if the SDK was ready from cache.', () => { const readinessManagerMock = { isReady: jest.fn(() => false), isReadyFromCache: jest.fn(() => true) }; // @ts-ignore - expect(validateIfOperational(readinessManagerMock, 'test_method')).toBe(true); // It should return true if SDK was ready. - expect(readinessManagerMock.isReady.mock.calls.length).toBe(1); // It checks for SDK_READY status. - expect(readinessManagerMock.isReadyFromCache.mock.calls.length).toBe(1); // It checks for SDK_READY_FROM_CACHE status. - expect(loggerMock.warn.mock.calls.length).toBe(0); // But it should not log any warnings. - expect(loggerMock.error.mock.calls.length).toBe(0); // But it should not log any errors. - - mockClear(); + expect(validateIfOperational(loggerMock, readinessManagerMock, 'test_method')).toBe(true); // It should return true if SDK was ready. + expect(readinessManagerMock.isReady).toBeCalledTimes(1); // It checks for SDK_READY status. + expect(readinessManagerMock.isReadyFromCache).toBeCalledTimes(1); // It checks for SDK_READY_FROM_CACHE status. + expect(loggerMock.warn).not.toBeCalled(); // But it should not log any warnings. + expect(loggerMock.error).not.toBeCalled(); // But it should not log any errors. }); test('Should return false and log a warning if the SDK was not ready.', () => { const readinessManagerMock = { isReady: jest.fn(() => false), isReadyFromCache: jest.fn(() => false) }; // @ts-ignore - expect(validateIfOperational(readinessManagerMock, 'test_method')).toBe(false); // It should return true if SDK was ready. - expect(readinessManagerMock.isReady.mock.calls.length).toBe(1); // It checks for SDK_READY status. - expect(readinessManagerMock.isReadyFromCache.mock.calls.length).toBe(1); // It checks for SDK_READY_FROM_CACHE status. - expect(loggerMock.warn.mock.calls).toEqual([['test_method: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method.']]); // It should log the expected warning. - expect(loggerMock.error.mock.calls.length).toBe(0); // But it should not log any errors. - - mockClear(); + expect(validateIfOperational(loggerMock, readinessManagerMock, 'test_method')).toBe(false); // It should return true if SDK was ready. + expect(readinessManagerMock.isReady).toBeCalledTimes(1); // It checks for SDK_READY status. + expect(readinessManagerMock.isReadyFromCache).toBeCalledTimes(1); // It checks for SDK_READY_FROM_CACHE status. + expect(loggerMock.warn).toBeCalledWith(CLIENT_NOT_READY, ['test_method']); // It should log the expected warning. + expect(loggerMock.error).not.toBeCalled(); // But it should not log any errors. }); }); diff --git a/src/utils/inputValidation/__tests__/key.spec.ts b/src/utils/inputValidation/__tests__/key.spec.ts index 1a91002e..74bdb62e 100644 --- a/src/utils/inputValidation/__tests__/key.spec.ts +++ b/src/utils/inputValidation/__tests__/key.spec.ts @@ -1,35 +1,27 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_EMPTY, ERROR_INVALID, ERROR_INVALID_KEY_OBJECT, ERROR_NULL, ERROR_TOO_LONG, WARN_CONVERTING } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateKey } from '../key'; -const errorMsgs = { - EMPTY_KEY: (keyType: string) => `you passed an empty string, ${keyType} must be a non-empty string.`, - LONG_KEY: (keyType: string) => `${keyType} too long, ${keyType} must be 250 characters or less.`, - NULL_KEY: (keyType: string) => `you passed a null or undefined ${keyType}, ${keyType} must be a non-empty string.`, - WRONG_TYPE_KEY: (keyType: string) => `you passed an invalid ${keyType} type, ${keyType} must be a non-empty string.`, - NUMERIC_KEY: (keyType: string, value: any) => `${keyType} "${value}" is not of type string, converting.`, - WRONG_KEY_PROPS: 'Key must be an object with bucketingKey and matchingKey with valid string properties.' -}; - const invalidKeys = [ - { key: '', msg: errorMsgs.EMPTY_KEY }, - { key: 'a'.repeat(251), msg: errorMsgs.LONG_KEY }, - { key: null, msg: errorMsgs.NULL_KEY }, - { key: undefined, msg: errorMsgs.NULL_KEY }, - { key: () => { }, msg: errorMsgs.WRONG_TYPE_KEY }, - { key: new Promise(r => r()), msg: errorMsgs.WRONG_TYPE_KEY }, - { key: Symbol('asd'), msg: errorMsgs.WRONG_TYPE_KEY }, - { key: [], msg: errorMsgs.WRONG_TYPE_KEY }, - { key: true, msg: errorMsgs.WRONG_TYPE_KEY }, - { key: NaN, msg: errorMsgs.WRONG_TYPE_KEY }, - { key: Infinity, msg: errorMsgs.WRONG_TYPE_KEY }, - { key: -Infinity, msg: errorMsgs.WRONG_TYPE_KEY }, + { key: '', msg: ERROR_EMPTY }, + { key: 'a'.repeat(251), msg: ERROR_TOO_LONG }, + { key: null, msg: ERROR_NULL }, + { key: undefined, msg: ERROR_NULL }, + { key: () => { }, msg: ERROR_INVALID }, + { key: new Promise(r => r()), msg: ERROR_INVALID }, + { key: Symbol('asd'), msg: ERROR_INVALID }, + { key: [], msg: ERROR_INVALID }, + { key: true, msg: ERROR_INVALID }, + { key: NaN, msg: ERROR_INVALID }, + { key: Infinity, msg: ERROR_INVALID }, + { key: -Infinity, msg: ERROR_INVALID }, ]; const stringifyableKeys = [ - { key: 200, msg: errorMsgs.NUMERIC_KEY }, - { key: 1235891238571295, msg: errorMsgs.NUMERIC_KEY }, - { key: 0, msg: errorMsgs.NUMERIC_KEY } + { key: 200, msg: WARN_CONVERTING }, + { key: 1235891238571295, msg: WARN_CONVERTING }, + { key: 0, msg: WARN_CONVERTING } ]; const invalidKeyObjects = [ @@ -42,84 +34,80 @@ const invalidKeyObjects = [ describe('INPUT VALIDATION for Key', () => { + afterEach(() => { loggerMock.mockClear(); }); + test('String and Object keys / Should return the key with no errors logged if the key is correct', () => { const validKey = 'validKey'; const validObjKey = { matchingKey: 'valid', bucketingKey: 'alsoValid' }; - expect(validateKey(validKey, 'some_method_keys')).toEqual(validKey); // It will return the valid key. - expect(loggerMock.error.mock.calls.length).toBe(0); // No errors should be logged. + expect(validateKey(loggerMock, validKey, 'some_method_keys')).toEqual(validKey); // It will return the valid key. + expect(loggerMock.error).not.toBeCalled(); // No errors should be logged. - expect(validateKey(validObjKey, 'some_method_keys')).toEqual(validObjKey); // It will return the valid key. - expect(loggerMock.error.mock.calls.length).toBe(0); // No errors should be logged. + expect(validateKey(loggerMock, validObjKey, 'some_method_keys')).toEqual(validObjKey); // It will return the valid key. + expect(loggerMock.error).not.toBeCalled(); // No errors should be logged. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('String key / Should return false and log error if key is invalid', () => { for (let i = 0; i < invalidKeys.length; i++) { const invalidKey = invalidKeys[i]['key']; - const expectedLog = invalidKeys[i]['msg']('key'); + const expectedLog = invalidKeys[i]['msg']; - expect(validateKey(invalidKey, 'test_method')).toBe(false); // Invalid keys should return false. - expect(loggerMock.error.mock.calls[0][0]).toEqual(`test_method: ${expectedLog}`); // The error should be logged for the invalid key. + expect(validateKey(loggerMock, invalidKey, 'test_method')).toBe(false); // Invalid keys should return false. + expect(loggerMock.error).toBeCalledWith(expectedLog, ['test_method', 'key']); // The error should be logged for the invalid key. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('String key / Should return stringified version of the key if it is convertible to one and log a warning.', () => { for (let i = 0; i < stringifyableKeys.length; i++) { const invalidKey = stringifyableKeys[i]['key']; - const expectedLog = stringifyableKeys[i]['msg']('key', invalidKey); + const expectedLog = stringifyableKeys[i]['msg']; - validateKey(invalidKey, 'test_method'); - expect(loggerMock.warn.mock.calls[0][0]).toEqual(`test_method: ${expectedLog}`); // But if the logger allows for warnings, it should be logged. + validateKey(loggerMock, invalidKey, 'test_method'); + expect(loggerMock.warn).toBeCalledWith(expectedLog, ['test_method', 'key', invalidKey]); // But if the logger allows for warnings, it should be logged. loggerMock.warn.mockClear(); } - expect(loggerMock.error.mock.calls.length).toBe(0); // It should have not logged any errors. - - mockClear(); + expect(loggerMock.error).not.toBeCalled(); // It should have not logged any errors. }); test('Object key / Should return false and log error if a part of the key is invalid', () => { // Test invalid object format for (let i = 0; i < invalidKeyObjects.length; i++) { - expect(validateKey(invalidKeyObjects[i], 'test_method')).toBe(false); // Invalid key objects should return false. - expect(loggerMock.error.mock.calls[loggerMock.error.mock.calls.length - 1][0]).toEqual(`test_method: ${errorMsgs.WRONG_KEY_PROPS}`); // The error should be logged for the invalid key. + expect(validateKey(loggerMock, invalidKeyObjects[i], 'test_method')).toBe(false); // Invalid key objects should return false. + expect(loggerMock.error).lastCalledWith(ERROR_INVALID_KEY_OBJECT, ['test_method']); // The error should be logged for the invalid key. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. - mockClear(); + loggerMock.mockClear(); // Test invalid matchingKey for (let i = 0; i < invalidKeys.length; i++) { const invalidKey = { matchingKey: invalidKeys[i]['key'], bucketingKey: 'thisIsValid' }; - const expectedLog = invalidKeys[i]['msg']('matchingKey'); + const expectedLog = invalidKeys[i]['msg']; - expect(validateKey(invalidKey, 'test_method')).toBe(false); // Invalid keys should return false. - expect(loggerMock.error.mock.calls[0][0]).toEqual(`test_method: ${expectedLog}`); // The error should be logged for the invalid key. + expect(validateKey(loggerMock, invalidKey, 'test_method')).toBe(false); // Invalid keys should return false. + expect(loggerMock.error).toBeCalledWith(expectedLog, ['test_method', 'matchingKey']); // The error should be logged for the invalid key. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. - mockClear(); + loggerMock.mockClear(); // Test invalid bucketingKey for (let i = 0; i < invalidKeys.length; i++) { @@ -127,33 +115,31 @@ describe('INPUT VALIDATION for Key', () => { matchingKey: 'thisIsValidToo', bucketingKey: invalidKeys[i]['key'] }; - const expectedLog = invalidKeys[i]['msg']('bucketingKey'); + const expectedLog = invalidKeys[i]['msg']; - expect(validateKey(invalidKey, 'test_method')).toBe(false); // Invalid keys should return false. - expect(loggerMock.error.mock.calls[0][0]).toEqual(`test_method: ${expectedLog}`); // The error should be logged for the invalid key. + expect(validateKey(loggerMock, invalidKey, 'test_method')).toBe(false); // Invalid keys should return false. + expect(loggerMock.error).toBeCalledWith(expectedLog, ['test_method', 'bucketingKey']); // The error should be logged for the invalid key. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. - mockClear(); + loggerMock.mockClear(); // Just one test that if both are invalid we get the log for both. let invalidKey = { matchingKey: invalidKeys[0]['key'], bucketingKey: invalidKeys[1]['key'] }; - let expectedLogMK = invalidKeys[0]['msg']('matchingKey'); - let expectedLogBK = invalidKeys[1]['msg']('bucketingKey'); - - expect(validateKey(invalidKey, 'test_method')).toBe(false); // Invalid keys should return false. - expect(loggerMock.error.mock.calls[0][0]).toEqual(`test_method: ${expectedLogMK}`); // The error should be logged for the invalid key property. - expect(loggerMock.error.mock.calls[1][0]).toEqual(`test_method: ${expectedLogBK}`); // The error should be logged for the invalid key property. + let expectedLogMK = invalidKeys[0]['msg']; + let expectedLogBK = invalidKeys[1]['msg']; - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(validateKey(loggerMock, invalidKey, 'test_method')).toBe(false); // Invalid keys should return false. + expect(loggerMock.error).nthCalledWith(1, expectedLogMK, ['test_method', 'matchingKey']); // The error should be logged for the invalid key property. + expect(loggerMock.error).nthCalledWith(2, expectedLogBK, ['test_method', 'bucketingKey']); // The error should be logged for the invalid key property. - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Object key / Should return stringified version of the key props if those are convertible and log the corresponding warnings', () => { @@ -165,15 +151,13 @@ describe('INPUT VALIDATION for Key', () => { matchingKey: String(invalidKey.matchingKey), bucketingKey: String(invalidKey.bucketingKey), }; - let expectedLogMK = stringifyableKeys[0]['msg']('matchingKey', invalidKey.matchingKey); - let expectedLogBK = stringifyableKeys[1]['msg']('bucketingKey', invalidKey.bucketingKey); - - expect(validateKey(invalidKey, 'test_method')).toEqual(expectedKey); // If a key object had stringifyable values, those will be stringified and Key returned. - expect(loggerMock.warn.mock.calls[0][0]).toEqual(`test_method: ${expectedLogMK}`); // The warning should be logged for the stringified prop if warnings are enabled. - expect(loggerMock.warn.mock.calls[1][0]).toEqual(`test_method: ${expectedLogBK}`); // The warning should be logged for the stringified prop if warnings are enabled. + let expectedLogMK = stringifyableKeys[0]['msg']; + let expectedLogBK = stringifyableKeys[1]['msg']; - expect(loggerMock.error.mock.calls.length).toBe(0); // It should have not logged any errors. + expect(validateKey(loggerMock, invalidKey, 'test_method')).toEqual(expectedKey); // If a key object had stringifyable values, those will be stringified and Key returned. + expect(loggerMock.warn).nthCalledWith(1, expectedLogMK, ['test_method', 'matchingKey', invalidKey.matchingKey]); // The warning should be logged for the stringified prop if warnings are enabled. + expect(loggerMock.warn).nthCalledWith(2, expectedLogBK, ['test_method', 'bucketingKey', invalidKey.bucketingKey]); // The warning should be logged for the stringified prop if warnings are enabled. - mockClear(); + expect(loggerMock.error).not.toBeCalled(); // It should have not logged any errors. }); }); diff --git a/src/utils/inputValidation/__tests__/preloadedData.spec.ts b/src/utils/inputValidation/__tests__/preloadedData.spec.ts index 29f3a8a6..381cba4a 100644 --- a/src/utils/inputValidation/__tests__/preloadedData.spec.ts +++ b/src/utils/inputValidation/__tests__/preloadedData.spec.ts @@ -138,20 +138,20 @@ test('INPUT VALIDATION for preloadedData', () => { for (let i = 0; i < testCases.length; i++) { const testCase = testCases[i]; - expect(validatePreloadedData(testCase.input, method)).toBe(testCase.output); + expect(validatePreloadedData(loggerMock, testCase.input, method)).toBe(testCase.output); if (testCase.error) { expect(loggerMock.error.mock.calls[0]).toEqual([testCase.error]); // Should log the error for the invalid preloadedData. loggerMock.error.mockClear(); } else { - expect(loggerMock.error.mock.calls.length === 0).toBe(true); // Should not log any error. + expect(loggerMock.error).not.toBeCalled(); // Should not log any error. } if (testCase.warn) { expect(loggerMock.warn.mock.calls[0]).toEqual([testCase.warn]); // Should log the warning for the given preloadedData. loggerMock.warn.mockClear(); } else { - expect(loggerMock.warn.mock.calls.length === 0).toBe(true); // Should not log any warning. + expect(loggerMock.warn).not.toBeCalled(); // Should not log any warning. } } }); diff --git a/src/utils/inputValidation/__tests__/split.spec.ts b/src/utils/inputValidation/__tests__/split.spec.ts index 53fa76c0..85d14be6 100644 --- a/src/utils/inputValidation/__tests__/split.spec.ts +++ b/src/utils/inputValidation/__tests__/split.spec.ts @@ -1,30 +1,24 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_INVALID, ERROR_NULL, ERROR_EMPTY, WARN_TRIMMING } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateSplit } from '../split'; -const errorMsgs = { - NULL_SPLIT: () => 'you passed a null or undefined split name, split name must be a non-empty string.', - WRONG_TYPE_SPLIT: () => 'you passed an invalid split name, split name must be a non-empty string.', - EMPTY_SPLIT: () => 'you passed an empty split name, split name must be a non-empty string.', - TRIMMABLE_SPLIT: (splitName: string) => `split name "${splitName}" has extra whitespace, trimming.` -}; - const invalidSplits = [ - { split: [], msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: () => { }, msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: Object.create({}), msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: {}, msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: true, msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: false, msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: 10, msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: 0, msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: NaN, msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: Infinity, msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: null, msg: errorMsgs.NULL_SPLIT }, - { split: undefined, msg: errorMsgs.NULL_SPLIT }, - { split: new Promise(res => res), msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: Symbol('asd'), msg: errorMsgs.WRONG_TYPE_SPLIT }, - { split: '', msg: errorMsgs.EMPTY_SPLIT } + { split: [], msg: ERROR_INVALID }, + { split: () => { }, msg: ERROR_INVALID }, + { split: Object.create({}), msg: ERROR_INVALID }, + { split: {}, msg: ERROR_INVALID }, + { split: true, msg: ERROR_INVALID }, + { split: false, msg: ERROR_INVALID }, + { split: 10, msg: ERROR_INVALID }, + { split: 0, msg: ERROR_INVALID }, + { split: NaN, msg: ERROR_INVALID }, + { split: Infinity, msg: ERROR_INVALID }, + { split: null, msg: ERROR_NULL }, + { split: undefined, msg: ERROR_NULL }, + { split: new Promise(res => res), msg: ERROR_INVALID }, + { split: Symbol('asd'), msg: ERROR_INVALID }, + { split: '', msg: ERROR_EMPTY } ]; const trimmableSplits = [ @@ -35,47 +29,43 @@ const trimmableSplits = [ describe('INPUT VALIDATION for Split name', () => { + afterEach(() => { loggerMock.mockClear(); }); + test('Should return the provided split name if it is a valid string without logging any errors', () => { - expect(validateSplit('splitName', 'some_method_splitName')).toBe('splitName'); // It should return the provided string if it is valid. + expect(validateSplit(loggerMock, 'splitName', 'some_method_splitName')).toBe('splitName'); // It should return the provided string if it is valid. expect(loggerMock.error.mock.calls[0]).not.toEqual('some_method_splitName'); // Should not log any errors. - expect(validateSplit('split_name', 'some_method_splitName')).toBe('split_name'); // It should return the provided string if it is valid. + expect(validateSplit(loggerMock, 'split_name', 'some_method_splitName')).toBe('split_name'); // It should return the provided string if it is valid. expect(loggerMock.error.mock.calls[0]).not.toEqual('some_method_splitName'); // Should not log any errors. - expect(validateSplit('A_split-name_29', 'some_method_splitName')).toBe('A_split-name_29'); // It should return the provided string if it is valid. + expect(validateSplit(loggerMock, 'A_split-name_29', 'some_method_splitName')).toBe('A_split-name_29'); // It should return the provided string if it is valid. expect(loggerMock.error.mock.calls[0]).not.toEqual('some_method_splitName'); // Should not log any errors. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should trim split name if it is a valid string with trimmable spaces and log a warning (if those are enabled)', () => { for (let i = 0; i < trimmableSplits.length; i++) { const trimmableSplit = trimmableSplits[i]; - expect(validateSplit(trimmableSplit, 'some_method_splitName')).toBe(trimmableSplit.trim()); // It should return the trimmed version of the split name received. - expect(loggerMock.warn.mock.calls[0][0]).toEqual(`some_method_splitName: ${errorMsgs.TRIMMABLE_SPLIT(trimmableSplit)}`); // Should log a warning if those are enabled. + expect(validateSplit(loggerMock, trimmableSplit, 'some_method_splitName')).toBe(trimmableSplit.trim()); // It should return the trimmed version of the split name received. + expect(loggerMock.warn).toBeCalledWith(WARN_TRIMMING, ['some_method_splitName', 'split name', trimmableSplit]); // Should log a warning if those are enabled. loggerMock.warn.mockClear(); } - expect(loggerMock.error.mock.calls.length).toBe(0); // It should have not logged any errors. - - mockClear(); + expect(loggerMock.error).not.toBeCalled(); // It should have not logged any errors. }); test('Should return false and log error if split name is not a valid string', () => { for (let i = 0; i < invalidSplits.length; i++) { const invalidValue = invalidSplits[i]['split']; // @ts-ignore - const expectedLog = invalidSplits[i]['msg'](invalidValue); + const expectedLog = invalidSplits[i]['msg']; - expect(validateSplit(invalidValue, 'test_method')).toBe(false); // Invalid event types should always return false. - expect(loggerMock.error.mock.calls[0][0]).toEqual(`test_method: ${expectedLog}`); // Should log the error for the invalid event type. + expect(validateSplit(loggerMock, invalidValue, 'test_method')).toBe(false); // Invalid event types should always return false. + expect(loggerMock.error).toBeCalledWith(expectedLog, ['test_method', 'split name']); // Should log the error for the invalid event type. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); }); diff --git a/src/utils/inputValidation/__tests__/splitExistance.spec.ts b/src/utils/inputValidation/__tests__/splitExistance.spec.ts index 3f6a3c1a..4bc2b8e8 100644 --- a/src/utils/inputValidation/__tests__/splitExistance.spec.ts +++ b/src/utils/inputValidation/__tests__/splitExistance.spec.ts @@ -1,50 +1,47 @@ import * as LabelConstants from '../../labels'; -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateSplitExistance } from '../splitExistance'; import { IReadinessManager } from '../../../readiness/types'; - -const errorMsgs = { - NOT_EXISTENT_SPLIT: (splitName: string) => `you passed "${splitName}" that does not exist in this environment, please double check what Splits exist in the web console.` -}; +import { WARN_NOT_EXISTENT_SPLIT } from '../../../logger/constants'; describe('Split existance (special case)', () => { + afterEach(() => { loggerMock.mockClear(); }); + test('Should return a boolean indicating if the SDK was ready and there was no Split object or "definition not found" label', () => { // @ts-expect-error let readinessManagerMock = { isReady: jest.fn(() => false) // Fake the signal for the non ready SDK } as IReadinessManager; - expect(validateSplitExistance(readinessManagerMock, 'some_split', {}, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(validateSplitExistance(readinessManagerMock, 'some_split', null, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(validateSplitExistance(readinessManagerMock, 'some_split', undefined, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(validateSplitExistance(readinessManagerMock, 'some_split', 'a label', 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(validateSplitExistance(readinessManagerMock, 'some_split', LabelConstants.SPLIT_NOT_FOUND, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'some_split', {}, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'some_split', null, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'some_split', undefined, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'some_split', 'a label', 'test_method')).toBe(true); // Should always return true when the SDK is not ready. + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'some_split', LabelConstants.SPLIT_NOT_FOUND, 'test_method')).toBe(true); // Should always return true when the SDK is not ready. - expect(loggerMock.warn.mock.calls.length).toBe(0); // There should have been no warning logs since the SDK was not ready yet. - expect(loggerMock.error.mock.calls.length).toBe(0); // There should have been no error logs since the SDK was not ready yet. + expect(loggerMock.warn).not.toBeCalled(); // There should have been no warning logs since the SDK was not ready yet. + expect(loggerMock.error).not.toBeCalled(); // There should have been no error logs since the SDK was not ready yet. // Prepare the mock to fake that the SDK is ready now. (readinessManagerMock.isReady as jest.Mock).mockImplementation(() => true); - expect(validateSplitExistance(readinessManagerMock, 'other_split', {}, 'other_method')).toBe(true); // Should return true if it receives a Split Object instead of null (when the object is not found, for manager). - expect(validateSplitExistance(readinessManagerMock, 'other_split', 'a label', 'other_method')).toBe(true); // Should return true if it receives a Label and it is not split not found (when the Split was not found on the storage, for client). - - expect(loggerMock.warn.mock.calls.length).toBe(0); // There should have been no warning logs since the values we used so far were considered valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // There should have been no error logs since the values we used so far were considered valid. + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'other_split', {}, 'other_method')).toBe(true); // Should return true if it receives a Split Object instead of null (when the object is not found, for manager). + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'other_split', 'a label', 'other_method')).toBe(true); // Should return true if it receives a Label and it is not split not found (when the Split was not found on the storage, for client). - expect(validateSplitExistance(readinessManagerMock, 'other_split', null, 'other_method')).toBe(false); // Should return false if it receives a non-truthy value as a split object or label - expect(validateSplitExistance(readinessManagerMock, 'other_split', undefined, 'other_method')).toBe(false); // Should return false if it receives a non-truthy value as a split object or label - expect(validateSplitExistance(readinessManagerMock, 'other_split', LabelConstants.SPLIT_NOT_FOUND, 'other_method')).toBe(false); // Should return false if it receives a label but it is the split not found one. + expect(loggerMock.warn).not.toBeCalled(); // There should have been no warning logs since the values we used so far were considered valid. + expect(loggerMock.error).not.toBeCalled(); // There should have been no error logs since the values we used so far were considered valid. - expect(loggerMock.warn.mock.calls.length).toBe(3); // It should have logged 3 warnings, one per each time we called it - loggerMock.warn.mock.calls.forEach(call => expect(call[0]).toBe(`other_method: ${errorMsgs.NOT_EXISTENT_SPLIT('other_split')}`)); // Warning logs should have the correct message. + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'other_split', null, 'other_method')).toBe(false); // Should return false if it receives a non-truthy value as a split object or label + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'other_split', undefined, 'other_method')).toBe(false); // Should return false if it receives a non-truthy value as a split object or label + expect(validateSplitExistance(loggerMock, readinessManagerMock, 'other_split', LabelConstants.SPLIT_NOT_FOUND, 'other_method')).toBe(false); // Should return false if it receives a label but it is the split not found one. - expect(loggerMock.error.mock.calls.length).toBe(0); // We log warnings, not errors. + expect(loggerMock.warn).toBeCalledTimes(3); // It should have logged 3 warnings, one per each time we called it + loggerMock.warn.mock.calls.forEach(call => expect(call).toEqual([WARN_NOT_EXISTENT_SPLIT, ['other_method', 'other_split']])); // Warning logs should have the correct message. - mockClear(); + expect(loggerMock.error).not.toBeCalled(); // We log warnings, not errors. }); }); diff --git a/src/utils/inputValidation/__tests__/splits.spec.ts b/src/utils/inputValidation/__tests__/splits.spec.ts index 1c7a717f..221cbf1b 100644 --- a/src/utils/inputValidation/__tests__/splits.spec.ts +++ b/src/utils/inputValidation/__tests__/splits.spec.ts @@ -2,13 +2,14 @@ import uniq from 'lodash/uniq'; import startsWith from 'lodash/startsWith'; // mocks sdkLogger -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_EMPTY_ARRAY } from '../../../logger/constants'; // mocks validateSplit jest.mock('../split'); import { validateSplit } from '../split'; const validateSplitMock = validateSplit as jest.Mock; -validateSplitMock.mockImplementation((maybeSplit) => maybeSplit); +validateSplitMock.mockImplementation((_, maybeSplit) => maybeSplit); // test target import { validateSplits } from '../splits'; @@ -34,54 +35,54 @@ const invalidSplits = [ describe('INPUT VALIDATION for Split names', () => { afterEach(() => { - mockClear(); + loggerMock.mockClear(); validateSplitMock.mockClear(); }); test('Should return the provided array if it is a valid splits names array without logging any errors', () => { const validArr = ['splitName1', 'split_name_2', 'split-name-3']; - expect(validateSplits(validArr, 'some_method_splits')).toEqual(validArr); // It should return the provided array without changes if it is valid. - expect(validateSplitMock.mock.calls.length).toBe(validArr.length); // Should have validated each value independently. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors on the collection. + expect(validateSplits(loggerMock, validArr, 'some_method_splits')).toEqual(validArr); // It should return the provided array without changes if it is valid. + expect(validateSplitMock).toBeCalledTimes(validArr.length); // Should have validated each value independently. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors on the collection. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should return the provided array if it is a valid splits names array removing duplications, without logging any errors', () => { const validArr = ['split_name', 'split_name', 'split-name']; - expect(validateSplits(validArr, 'some_method_splits')).toEqual(uniq(validArr)); // It should return the provided array without changes if it is valid. - expect(validateSplitMock.mock.calls.length).toBe(validArr.length); // Should have validated each value independently. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors on the collection. + expect(validateSplits(loggerMock, validArr, 'some_method_splits')).toEqual(uniq(validArr)); // It should return the provided array without changes if it is valid. + expect(validateSplitMock).toBeCalledTimes(validArr.length); // Should have validated each value independently. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors on the collection. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should return false and log an error for the array if it is invalid', () => { for (let i = 0; i < invalidSplits.length; i++) { - expect(validateSplits(invalidSplits[i], 'test_method')).toBe(false); // It will return false as the array is of an incorrect type. - expect(loggerMock.error.mock.calls).toEqual([['test_method: split_names must be a non-empty array.']]); // Should log the error for the collection. - expect(validateSplitMock.mock.calls.length).toBe(0); // Should not try to validate any inner value if there is no valid array. + expect(validateSplits(loggerMock, invalidSplits[i], 'test_method')).toBe(false); // It will return false as the array is of an incorrect type. + expect(loggerMock.error).toBeCalledWith(ERROR_EMPTY_ARRAY, ['test_method', 'split_names']); // Should log the error for the collection. + expect(validateSplitMock).not.toBeCalled(); // Should not try to validate any inner value if there is no valid array. loggerMock.error.mockClear(); } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should strip out any invalid value from the array', () => { // We use a mock function for individual validation. - validateSplitMock.mockImplementation(value => startsWith(value, 'invalid') ? false : value); + validateSplitMock.mockImplementation((_, value) => startsWith(value, 'invalid') ? false : value); const myArr = ['valid_name', 'invalid_name', 'invalid_val_2', 'something_valid']; - expect(validateSplits(myArr, 'test_method')).toEqual(['valid_name', 'something_valid']); // It will return the array without the invalid values. + expect(validateSplits(loggerMock, myArr, 'test_method')).toEqual(['valid_name', 'something_valid']); // It will return the array without the invalid values. for (let i = 0; i < myArr.length; i++) { - expect(validateSplitMock.mock.calls[i]).toEqual([myArr[i], 'test_method', 'split name']); // Should validate any inner value independently. + expect(validateSplitMock.mock.calls[i]).toEqual([loggerMock, myArr[i], 'test_method', 'split name']); // Should validate any inner value independently. } - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any error for the collection. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings for the collection. + expect(loggerMock.error).not.toBeCalled(); // Should not log any error for the collection. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings for the collection. }); }); diff --git a/src/utils/inputValidation/__tests__/trafficType.spec.ts b/src/utils/inputValidation/__tests__/trafficType.spec.ts index d7ab1e2b..bbea3d23 100644 --- a/src/utils/inputValidation/__tests__/trafficType.spec.ts +++ b/src/utils/inputValidation/__tests__/trafficType.spec.ts @@ -1,30 +1,24 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { ERROR_NULL, ERROR_INVALID, ERROR_EMPTY, WARN_LOWERCASE_TRAFFIC_TYPE } from '../../../logger/constants'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { validateTrafficType } from '../trafficType'; -const errorMsgs = { - NULL_TRAFFIC_TYPE: 'you passed a null or undefined traffic_type_name, traffic_type_name must be a non-empty string.', - WRONG_TYPE_TRAFFIC_TYPE: 'you passed an invalid traffic_type_name, traffic_type_name must be a non-empty string.', - EMPTY_TRAFFIC_TYPE: 'you passed an empty traffic_type_name, traffic_type_name must be a non-empty string.', - LOWERCASE_TRAFFIC_TYPE: 'traffic_type_name should be all lowercase - converting string to lowercase.' -}; - const invalidTrafficTypes = [ - { tt: [], msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: () => { }, msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: Object.create({}), msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: {}, msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: true, msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: false, msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: 10, msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: 0, msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: NaN, msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: Infinity, msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: null, msg: errorMsgs.NULL_TRAFFIC_TYPE }, - { tt: undefined, msg: errorMsgs.NULL_TRAFFIC_TYPE }, - { tt: new Promise(res => res), msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: Symbol('asd'), msg: errorMsgs.WRONG_TYPE_TRAFFIC_TYPE }, - { tt: '', msg: errorMsgs.EMPTY_TRAFFIC_TYPE } + { tt: [], msg: ERROR_INVALID }, + { tt: () => { }, msg: ERROR_INVALID }, + { tt: Object.create({}), msg: ERROR_INVALID }, + { tt: {}, msg: ERROR_INVALID }, + { tt: true, msg: ERROR_INVALID }, + { tt: false, msg: ERROR_INVALID }, + { tt: 10, msg: ERROR_INVALID }, + { tt: 0, msg: ERROR_INVALID }, + { tt: NaN, msg: ERROR_INVALID }, + { tt: Infinity, msg: ERROR_INVALID }, + { tt: null, msg: ERROR_NULL }, + { tt: undefined, msg: ERROR_NULL }, + { tt: new Promise(res => res), msg: ERROR_INVALID }, + { tt: Symbol('asd'), msg: ERROR_INVALID }, + { tt: '', msg: ERROR_EMPTY } ]; const convertibleTrafficTypes = [ @@ -35,30 +29,28 @@ const convertibleTrafficTypes = [ describe('INPUT VALIDATION for Traffic Types', () => { - test('Should return the provided traffic type if it is a valid string without logging any errors', () => { - expect(validateTrafficType('traffictype', 'some_method_trafficType')).toBe('traffictype'); // It should return the provided string if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(validateTrafficType('traffic_type', 'some_method_trafficType')).toBe('traffic_type'); // It should return the provided string if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - expect(validateTrafficType('traffic-type-23', 'some_method_trafficType')).toBe('traffic-type-23'); // It should return the provided string if it is valid. - expect(loggerMock.error.mock.calls.length).toBe(0); // Should not log any errors. - - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. + afterEach(() => { loggerMock.mockClear(); }); - mockClear(); + test('Should return the provided traffic type if it is a valid string without logging any errors', () => { + expect(validateTrafficType(loggerMock, 'traffictype', 'some_method_trafficType')).toBe('traffictype'); // It should return the provided string if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(validateTrafficType(loggerMock, 'traffic_type', 'some_method_trafficType')).toBe('traffic_type'); // It should return the provided string if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(validateTrafficType(loggerMock, 'traffic-type-23', 'some_method_trafficType')).toBe('traffic-type-23'); // It should return the provided string if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); test('Should lowercase the whole traffic type if it is a valid string with uppercases and log a warning (if those are enabled)', () => { for (let i = 0; i < convertibleTrafficTypes.length; i++) { const convertibleTrafficType = convertibleTrafficTypes[i]; - expect(validateTrafficType(convertibleTrafficType, 'some_method_trafficType')).toBe(convertibleTrafficType.toLowerCase()); // It should return the lowercase version of the traffic type received. - expect(loggerMock.warn.mock.calls[i][0]).toEqual(`some_method_trafficType: ${errorMsgs.LOWERCASE_TRAFFIC_TYPE}`); // Should log a warning. + expect(validateTrafficType(loggerMock, convertibleTrafficType, 'some_method_trafficType')).toBe(convertibleTrafficType.toLowerCase()); // It should return the lowercase version of the traffic type received. + expect(loggerMock.warn.mock.calls[i]).toEqual([WARN_LOWERCASE_TRAFFIC_TYPE, ['some_method_trafficType']]); // Should log a warning. } - expect(loggerMock.error.mock.calls.length).toBe(0); // It should have not logged any errors. - - mockClear(); + expect(loggerMock.error).not.toBeCalled(); // It should have not logged any errors. }); test('Should return false and log error if traffic type is not a valid string', () => { @@ -66,12 +58,10 @@ describe('INPUT VALIDATION for Traffic Types', () => { const invalidValue = invalidTrafficTypes[i]['tt']; const expectedLog = invalidTrafficTypes[i]['msg']; - expect(validateTrafficType(invalidValue, 'test_method')).toBe(false); // Invalid traffic types should always return false. - expect(loggerMock.error.mock.calls[i][0]).toEqual(`test_method: ${expectedLog}`); // Should log the error for the invalid traffic type. + expect(validateTrafficType(loggerMock, invalidValue, 'test_method')).toBe(false); // Invalid traffic types should always return false. + expect(loggerMock.error.mock.calls[i]).toEqual([expectedLog, ['test_method']]); // Should log the error for the invalid traffic type. } - expect(loggerMock.warn.mock.calls.length).toBe(0); // It should have not logged any warnings. - - mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); }); diff --git a/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts b/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts index c1869f97..ba2f1666 100644 --- a/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts +++ b/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts @@ -2,9 +2,10 @@ import { IReadinessManager } from '../../../readiness/types'; import { ISplitsCacheBase } from '../../../storages/types'; import { LOCALHOST_MODE, STANDALONE_MODE } from '../../constants'; import thenable from '../../promise/thenable'; +import { WARN_NOT_EXISTENT_TT } from '../../../logger/constants'; /** Mocks */ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; const TEST_EXISTENT_TT = 'test_existent_tt'; const TEST_NOT_EXISTENT_TT = 'test_not_existent_tt'; @@ -29,83 +30,73 @@ const splitsCacheMock = { /** Test target */ import { validateTrafficTypeExistance } from '../trafficTypeExistance'; -const errorMsgs = { - NOT_EXISTENT_TT: (ttName: string) => `Traffic Type ${ttName} does not have any corresponding Splits in this environment, make sure you're tracking your events to a valid traffic type defined in the Split console.` -}; - describe('validateTrafficTypeExistance', () => { + afterEach(() => { + loggerMock.mockClear(); + splitsCacheMock.trafficTypeExists.mockClear(); + }); + test('Should return true without going to the storage and log nothing if the SDK is not ready or in localhost mode', () => { // Not ready and not localstorage - expect(validateTrafficTypeExistance(readinessManagerMock, splitsCacheMock, STANDALONE_MODE, 'test_tt', 'test_method')).toBe(true); // If the SDK is not ready yet, it will return true. - expect(splitsCacheMock.trafficTypeExists.mock.calls.length).toBe(0); // If the SDK is not ready yet, it does not try to go to the storage. - expect(loggerMock.error.mock.calls.length).toBe(0); // If the SDK is not ready yet, it will not log any errors. - expect(loggerMock.error.mock.calls.length).toBe(0); // If the SDK is not ready yet, it will not log any errors. + expect(validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, 'test_tt', 'test_method')).toBe(true); // If the SDK is not ready yet, it will return true. + expect(splitsCacheMock.trafficTypeExists).not.toBeCalled(); // If the SDK is not ready yet, it does not try to go to the storage. + expect(loggerMock.error).not.toBeCalled(); // If the SDK is not ready yet, it will not log any errors. + expect(loggerMock.error).not.toBeCalled(); // If the SDK is not ready yet, it will not log any errors. // Ready and in localstorage mode. readinessManagerMock.isReady.mockImplementation(() => true); - expect(validateTrafficTypeExistance(readinessManagerMock, splitsCacheMock, LOCALHOST_MODE, 'test_tt', 'test_method')).toBe(true); // If the SDK is in localhost mode, it will return true. - expect(splitsCacheMock.trafficTypeExists.mock.calls.length).toBe(0); // If the SDK is in localhost mode, it does not try to go to the storage. - expect(loggerMock.warn.mock.calls.length).toBe(0); // If the SDK is in localhost mode, it will not log any warnings. - expect(loggerMock.error.mock.calls.length).toBe(0); // If the SDK is in localhost mode, it will not log any errors. - - mockClear(); + expect(validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, LOCALHOST_MODE, 'test_tt', 'test_method')).toBe(true); // If the SDK is in localhost mode, it will return true. + expect(splitsCacheMock.trafficTypeExists).not.toBeCalled(); // If the SDK is in localhost mode, it does not try to go to the storage. + expect(loggerMock.warn).not.toBeCalled(); // If the SDK is in localhost mode, it will not log any warnings. + expect(loggerMock.error).not.toBeCalled(); // If the SDK is in localhost mode, it will not log any errors. }); test('Should return true and log nothing if SDK Ready, not localhost mode and the traffic type exists in the storage', () => { // Ready, standalone, and the TT exists in the storage. readinessManagerMock.isReady.mockImplementation(() => true); - expect(validateTrafficTypeExistance(readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_EXISTENT_TT, 'test_method')).toBe(true); // If the SDK is in condition to validate but the TT exists, it will return true. + expect(validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_EXISTENT_TT, 'test_method')).toBe(true); // If the SDK is in condition to validate but the TT exists, it will return true. expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the storage. - expect(loggerMock.warn.mock.calls.length).toBe(0); // If the SDK is in condition to validate but the TT exists, it will not log any warnings. - expect(loggerMock.error.mock.calls.length).toBe(0); // If the SDK is in condition to validate but the TT exists, it will not log any errors. - - mockClear(); - splitsCacheMock.trafficTypeExists.mockClear(); + expect(loggerMock.warn).not.toBeCalled(); // If the SDK is in condition to validate but the TT exists, it will not log any warnings. + expect(loggerMock.error).not.toBeCalled(); // If the SDK is in condition to validate but the TT exists, it will not log any errors. }); test('Should return false and log warning if SDK Ready, not localhost mode and the traffic type does NOT exist in the storage', () => { // Ready, standalone, and the TT not exists in the storage. - expect(validateTrafficTypeExistance(readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_NOT_EXISTENT_TT, 'test_method_y')).toBe(false); // If the SDK is in condition to validate but the TT does not exist in the storage, it will return false. + expect(validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_NOT_EXISTENT_TT, 'test_method_y')).toBe(false); // If the SDK is in condition to validate but the TT does not exist in the storage, it will return false. expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the storage. - expect(loggerMock.warn.mock.calls).toEqual([[`test_method_y: ${errorMsgs.NOT_EXISTENT_TT(TEST_NOT_EXISTENT_TT)}`]]); // If the SDK is in condition to validate but the TT does not exist in the storage, it will log the expected warning. - expect(loggerMock.error.mock.calls.length).toBe(0); // It logged a warning so no errors should be logged. - - mockClear(); - splitsCacheMock.trafficTypeExists.mockClear(); + expect(loggerMock.warn).toBeCalledWith(WARN_NOT_EXISTENT_TT, ['test_method_y', TEST_NOT_EXISTENT_TT]); // If the SDK is in condition to validate but the TT does not exist in the storage, it will log the expected warning. + expect(loggerMock.error).not.toBeCalled(); // It logged a warning so no errors should be logged. }); test('validateTrafficTypeExistance w/async storage - If the storage is async but the SDK is in condition to validate, it will validate that the TT exists on the storage', async () => { // Ready, standalone, the TT exists in the storage. - const validationPromise = validateTrafficTypeExistance(readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_EXISTENT_ASYNC_TT, 'test_method_z'); + const validationPromise = validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_EXISTENT_ASYNC_TT, 'test_method_z'); expect(thenable(validationPromise)).toBe(true); // If the storage is async, it should also return a promise. expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the async storage. - expect(loggerMock.warn.mock.calls.length).toBe(0); // We are still fetching the data from the storage, no logs yet. - expect(loggerMock.error.mock.calls.length).toBe(0); // We are still fetching the data from the storage, no logs yet. + expect(loggerMock.warn).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. + expect(loggerMock.error).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. const isValid = await validationPromise; expect(isValid).toBe(true); // As the split existed, it will resolve to true. - expect(loggerMock.warn.mock.calls.length).toBe(0); // It was valid so no logs. - expect(loggerMock.error.mock.calls.length).toBe(0); // It was valid so no logs. + expect(loggerMock.warn).not.toBeCalled(); // It was valid so no logs. + expect(loggerMock.error).not.toBeCalled(); // It was valid so no logs. // Second round, a TT that does not exist on the asnyc storage splitsCacheMock.trafficTypeExists.mockClear(); - const validationPromise2 = validateTrafficTypeExistance(readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_NOT_EXISTENT_ASYNC_TT, 'test_method_z'); + const validationPromise2 = validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_NOT_EXISTENT_ASYNC_TT, 'test_method_z'); expect(thenable(validationPromise2)).toBe(true); // If the storage is async, it should also return a promise. expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the async storage. - expect(loggerMock.warn.mock.calls.length).toBe(0); // We are still fetching the data from the storage, no logs yet. - expect(loggerMock.error.mock.calls.length).toBe(0); // We are still fetching the data from the storage, no logs yet. + expect(loggerMock.warn).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. + expect(loggerMock.error).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. const isValid2 = await validationPromise2; expect(isValid2).toBe(false); // As the split is not on the storage, it will resolve to false, failing the validation.. - expect(loggerMock.warn.mock.calls).toEqual([[`test_method_z: ${errorMsgs.NOT_EXISTENT_TT(TEST_NOT_EXISTENT_ASYNC_TT)}`]]); // If the SDK is in condition to validate but the TT does not exist in the storage, it will log the expected warning. - expect(loggerMock.error.mock.calls.length).toBe(0); // It logged a warning so no errors should be logged. - - mockClear(); - splitsCacheMock.trafficTypeExists.mockClear(); + expect(loggerMock.warn).toBeCalledWith(WARN_NOT_EXISTENT_TT, ['test_method_z', TEST_NOT_EXISTENT_ASYNC_TT]); // If the SDK is in condition to validate but the TT does not exist in the storage, it will log the expected warning. + expect(loggerMock.error).not.toBeCalled(); // It logged a warning so no errors should be logged. }); }); diff --git a/src/utils/inputValidation/apiKey.ts b/src/utils/inputValidation/apiKey.ts index 0a54963d..0b891abf 100644 --- a/src/utils/inputValidation/apiKey.ts +++ b/src/utils/inputValidation/apiKey.ts @@ -1,24 +1,21 @@ +import { ERROR_NULL, ERROR_EMPTY, ERROR_INVALID, WARN_API_KEY, logPrefixInstantiation } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { isString } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('', { - // Errors on API key validation are important enough so that one day we might force logging them or throw an exception on startup. - displayAllErrors: true -}); -function apiKeyError(reason: string) { return `Factory instantiation: ${reason}, api_key must be a non-empty string.`; } +const item = 'api_key'; /** validates the given api key */ -export function validateApiKey(maybeApiKey: any): string | false { +export function validateApiKey(log: ILogger, maybeApiKey: any): string | false { let apiKey: string | false = false; if (maybeApiKey == undefined) { // eslint-disable-line eqeqeq - log.error(apiKeyError('you passed a null or undefined api_key')); + log.error(ERROR_NULL, [logPrefixInstantiation, item]); } else if (isString(maybeApiKey)) { if (maybeApiKey.length > 0) apiKey = maybeApiKey; else - log.error(apiKeyError('you passed an empty api_key')); + log.error(ERROR_EMPTY, [logPrefixInstantiation, item]); } else { - log.error(apiKeyError('you passed an invalid api_key')); + log.error(ERROR_INVALID, [logPrefixInstantiation, item]); } return apiKey; @@ -26,11 +23,9 @@ export function validateApiKey(maybeApiKey: any): string | false { const usedKeysMap: Record = {}; -function apiKeyWarn(reason: string) { return `Factory instantiation: ${reason}. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application.`; } - /** validates the given api key and also warns if it is in use */ -export function validateAndTrackApiKey(maybeApiKey: any): string | false { - const apiKey = validateApiKey(maybeApiKey); +export function validateAndTrackApiKey(log: ILogger, maybeApiKey: any): string | false { + const apiKey = validateApiKey(log, maybeApiKey); // If the apiKey is correct, we'll save it as the instance creation should work. if (apiKey) { @@ -38,10 +33,10 @@ export function validateAndTrackApiKey(maybeApiKey: any): string | false { // If this key is not present, only warning scenarios is that we have factories for other keys. usedKeysMap[apiKey] = 1; if (Object.keys(usedKeysMap).length > 1) { - log.warn(apiKeyWarn('You already have an instance of the Split factory. Make sure you definitely want this additional instance')); + log.warn(WARN_API_KEY, ['an instance of the Split factory']); } } else { - log.warn(apiKeyWarn(`You already have ${usedKeysMap[apiKey]} ${usedKeysMap[apiKey] === 1 ? 'factory' : 'factories'} with this API Key`)); + log.warn(WARN_API_KEY, [`${usedKeysMap[apiKey]} ${usedKeysMap[apiKey] === 1 ? 'factory' : 'factories'} with this API Key`]); usedKeysMap[apiKey]++; } } diff --git a/src/utils/inputValidation/attributes.ts b/src/utils/inputValidation/attributes.ts index a8e02e7f..e45f67c0 100644 --- a/src/utils/inputValidation/attributes.ts +++ b/src/utils/inputValidation/attributes.ts @@ -1,13 +1,13 @@ import { isObject } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; import { SplitIO } from '../../types'; -const log = logFactory(''); +import { ILogger } from '../../logger/types'; +import { ERROR_NOT_PLAIN_OBJECT } from '../../logger/constants'; -export function validateAttributes(maybeAttrs: any, method: string): SplitIO.Attributes | undefined | false { +export function validateAttributes(log: ILogger, maybeAttrs: any, method: string): SplitIO.Attributes | undefined | false { // Attributes are optional if (isObject(maybeAttrs) || maybeAttrs == undefined) // eslint-disable-line eqeqeq return maybeAttrs; - log.error(`${method}: attributes must be a plain object.`); + log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'attributes']); return false; } diff --git a/src/utils/inputValidation/event.ts b/src/utils/inputValidation/event.ts index 70c78493..03e79c9e 100644 --- a/src/utils/inputValidation/event.ts +++ b/src/utils/inputValidation/event.ts @@ -1,19 +1,19 @@ +import { ERROR_EVENT_TYPE_FORMAT, ERROR_NULL, ERROR_INVALID, ERROR_EMPTY } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { isString } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory(''); const EVENT_TYPE_REGEX = /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/; -export function validateEvent(maybeEvent: any, method: string): string | false { +export function validateEvent(log: ILogger, maybeEvent: any, method: string): string | false { if (maybeEvent == undefined) { // eslint-disable-line eqeqeq - log.error(`${method}: you passed a null or undefined event_type, event_type must be a non-empty string.`); + log.error(ERROR_NULL, [method]); } else if (!isString(maybeEvent)) { - log.error(`${method}: you passed an invalid event_type, event_type must be a non-empty string.`); + log.error(ERROR_INVALID, [method]); } else { // It is a string. if (maybeEvent.length === 0) { - log.error(`${method}: you passed an empty event_type, event_type must be a non-empty string.`); + log.error(ERROR_EMPTY, [method]); } else if (!EVENT_TYPE_REGEX.test(maybeEvent)) { - log.error(`${method}: you passed "${maybeEvent}", 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.`); + log.error(ERROR_EVENT_TYPE_FORMAT, [method, maybeEvent]); } else { return maybeEvent; } diff --git a/src/utils/inputValidation/eventProperties.ts b/src/utils/inputValidation/eventProperties.ts index 980c2bfb..8a599a97 100644 --- a/src/utils/inputValidation/eventProperties.ts +++ b/src/utils/inputValidation/eventProperties.ts @@ -1,7 +1,7 @@ import { isObject, shallowClone, isString, isFiniteNumber, isBoolean } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; import { SplitIO } from '../../types'; -const log = logFactory(''); +import { ILogger } from '../../logger/types'; +import { ERROR_NOT_PLAIN_OBJECT, ERROR_SIZE_EXCEEDED, WARN_SETTING_NULL, WARN_TRIMMING_PROPERTIES } from '../../logger/constants'; const ECMA_SIZES = { NULL: 0, // While on the JSON it's going to occupy more space, we'll take it as 0 for the approximation. @@ -13,11 +13,11 @@ const MAX_PROPERTIES_AMOUNT = 300; const MAX_PROPERTIES_SIZE = 1024 * 32; const BASE_EVENT_SIZE = 1024; // We assume 1kb events without properties (avg measured) -export function validateEventProperties(maybeProperties: any, method: string): { properties: SplitIO.Properties | null | false, size: number } { +export function validateEventProperties(log: ILogger, maybeProperties: any, method: string): { properties: SplitIO.Properties | null | false, size: number } { if (maybeProperties == undefined) return { properties: null, size: BASE_EVENT_SIZE }; // eslint-disable-line eqeqeq if (!isObject(maybeProperties)) { - log.error(`${method}: properties must be a plain object.`); + log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'properties']); return { properties: false, size: BASE_EVENT_SIZE }; } @@ -30,7 +30,7 @@ export function validateEventProperties(maybeProperties: any, method: string): { }; if (keys.length > MAX_PROPERTIES_AMOUNT) { - log.warn(`${method}: Event has more than 300 properties. Some of them will be trimmed when processed.`); + log.warn(WARN_TRIMMING_PROPERTIES, [method]); } for (let i = 0; i < keys.length; i++) { @@ -47,7 +47,7 @@ export function validateEventProperties(maybeProperties: any, method: string): { clone[keys[i]] = null; val = null; isNullVal = true; - log.warn(`${method}: Property ${keys[i]} is of invalid type. Setting value to null.`); + log.warn(WARN_SETTING_NULL, [method, keys[i]]); } if (isNullVal) output.size += ECMA_SIZES.NULL; @@ -56,7 +56,7 @@ export function validateEventProperties(maybeProperties: any, method: string): { else if (isStringVal) output.size += val.length * ECMA_SIZES.STRING; if (output.size > MAX_PROPERTIES_SIZE) { - log.error(`${method}: The maximum size allowed for the properties is 32768 bytes, which was exceeded. Event not queued.`); + log.error(ERROR_SIZE_EXCEEDED, [method]); output.properties = false; break; } diff --git a/src/utils/inputValidation/eventValue.ts b/src/utils/inputValidation/eventValue.ts index 07bff6ff..9b7bdd16 100644 --- a/src/utils/inputValidation/eventValue.ts +++ b/src/utils/inputValidation/eventValue.ts @@ -1,11 +1,11 @@ +import { ERROR_NOT_FINITE } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { isFiniteNumber } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory(''); -export function validateEventValue(maybeValue: any, method: string): number | false { +export function validateEventValue(log: ILogger, maybeValue: any, method: string): number | false { if (isFiniteNumber(maybeValue) || maybeValue == undefined) // eslint-disable-line eqeqeq return maybeValue; - log.error(`${method}: value must be a finite number.`); + log.error(ERROR_NOT_FINITE, [method]); return false; } diff --git a/src/utils/inputValidation/isOperational.ts b/src/utils/inputValidation/isOperational.ts index 98a99c9d..05ad0aad 100644 --- a/src/utils/inputValidation/isOperational.ts +++ b/src/utils/inputValidation/isOperational.ts @@ -1,17 +1,17 @@ -import { logFactory } from '../../logger/sdkLogger'; +import { ERROR_CLIENT_DESTROYED, CLIENT_NOT_READY } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { IReadinessManager } from '../../readiness/types'; -const log = logFactory('', { displayAllErrors: true }); -export function validateIfNotDestroyed(readinessManager: IReadinessManager): boolean { +export function validateIfNotDestroyed(log: ILogger, readinessManager: IReadinessManager, method: string): boolean { if (!readinessManager.isDestroyed()) return true; - log.error('Client has already been destroyed - no calls possible.'); + log.error(ERROR_CLIENT_DESTROYED, [method]); return false; } -export function validateIfOperational(readinessManager: IReadinessManager, method: string) { +export function validateIfOperational(log: ILogger, readinessManager: IReadinessManager, method: string) { if (readinessManager.isReady() || readinessManager.isReadyFromCache()) return true; - log.warn(`${method}: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method.`); + log.warn(CLIENT_NOT_READY, [method]); return false; } diff --git a/src/utils/inputValidation/key.ts b/src/utils/inputValidation/key.ts index 3805fe99..9068bed6 100644 --- a/src/utils/inputValidation/key.ts +++ b/src/utils/inputValidation/key.ts @@ -1,17 +1,17 @@ import { isObject, isString, isFiniteNumber, toString } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; import { SplitIO } from '../../types'; -const log = logFactory(''); +import { ILogger } from '../../logger/types'; +import { ERROR_NULL, WARN_CONVERTING, ERROR_EMPTY, ERROR_TOO_LONG, ERROR_INVALID, ERROR_INVALID_KEY_OBJECT } from '../../logger/constants'; const KEY_MAX_LENGTH = 250; -function validateKeyValue(maybeKey: any, method: string, type: string): string | false { +function validateKeyValue(log: ILogger, maybeKey: any, method: string, type: string): string | false { if (maybeKey == undefined) { // eslint-disable-line eqeqeq - log.error(`${method}: you passed a null or undefined ${type}, ${type} must be a non-empty string.`); + log.error(ERROR_NULL, [method, type]); return false; } if (isFiniteNumber(maybeKey)) { - log.warn(`${method}: ${type} "${maybeKey}" is not of type string, converting.`); + log.warn(WARN_CONVERTING, [method, type, maybeKey]); return toString(maybeKey); } if (isString(maybeKey)) { @@ -22,30 +22,30 @@ function validateKeyValue(maybeKey: any, method: string, type: string): string | if (maybeKey.length > 0 && maybeKey.length <= KEY_MAX_LENGTH) return maybeKey; if (maybeKey.length === 0) { - log.error(`${method}: you passed an empty string, ${type} must be a non-empty string.`); + log.error(ERROR_EMPTY, [method, type]); } else if (maybeKey.length > KEY_MAX_LENGTH) { - log.error(`${method}: ${type} too long, ${type} must be 250 characters or less.`); + log.error(ERROR_TOO_LONG, [method, type]); } } else { - log.error(`${method}: you passed an invalid ${type} type, ${type} must be a non-empty string.`); + log.error(ERROR_INVALID, [method, type]); } return false; } -export function validateKey(maybeKey: any, method: string): SplitIO.SplitKey | false { +export function validateKey(log: ILogger, maybeKey: any, method: string): SplitIO.SplitKey | false { if (isObject(maybeKey)) { // Validate key object - const matchingKey = validateKeyValue(maybeKey.matchingKey, method, 'matchingKey'); - const bucketingKey = validateKeyValue(maybeKey.bucketingKey, method, 'bucketingKey'); + const matchingKey = validateKeyValue(log, maybeKey.matchingKey, method, 'matchingKey'); + const bucketingKey = validateKeyValue(log, maybeKey.bucketingKey, method, 'bucketingKey'); if (matchingKey && bucketingKey) return { matchingKey, bucketingKey }; - log.error(`${method}: Key must be an object with bucketingKey and matchingKey with valid string properties.`); + log.error(ERROR_INVALID_KEY_OBJECT, [method]); return false; } else { - return validateKeyValue(maybeKey, method, 'key'); + return validateKeyValue(log, maybeKey, method, 'key'); } } diff --git a/src/utils/inputValidation/preloadedData.ts b/src/utils/inputValidation/preloadedData.ts index 5936eb4b..0b02c6c2 100644 --- a/src/utils/inputValidation/preloadedData.ts +++ b/src/utils/inputValidation/preloadedData.ts @@ -1,27 +1,26 @@ import { isObject, isString, isFiniteNumber } from '../lang'; import { validateSplit } from './split'; -import { logFactory } from '../../logger/sdkLogger'; import { SplitIO } from '../../types'; -const log = logFactory(''); +import { ILogger } from '../../logger/types'; -function validateTimestampData(maybeTimestamp: any, method: string, item: string) { +function validateTimestampData(log: ILogger, maybeTimestamp: any, method: string, item: string) { if (isFiniteNumber(maybeTimestamp) && maybeTimestamp > -1) return true; log.error(`${method}: preloadedData.${item} must be a positive number.`); return false; } -function validateSplitsData(maybeSplitsData: any, method: string) { +function validateSplitsData(log: ILogger, maybeSplitsData: any, method: string) { if (isObject(maybeSplitsData)) { const splitNames = Object.keys(maybeSplitsData); if (splitNames.length === 0) log.warn(`${method}: preloadedData.splitsData doesn't contain split definitions.`); // @TODO in the future, consider handling the possibility of having parsed definitions of splits - if (splitNames.every(splitName => validateSplit(splitName, method) && isString(maybeSplitsData[splitName]))) return true; + if (splitNames.every(splitName => validateSplit(log, splitName, method) && isString(maybeSplitsData[splitName]))) return true; } log.error(`${method}: preloadedData.splitsData must be a map of split names to their serialized definitions.`); return false; } -function validateMySegmentsData(maybeMySegmentsData: any, method: string) { +function validateMySegmentsData(log: ILogger, maybeMySegmentsData: any, method: string) { if (isObject(maybeMySegmentsData)) { const userKeys = Object.keys(maybeMySegmentsData); if (userKeys.every(userKey => { @@ -34,7 +33,7 @@ function validateMySegmentsData(maybeMySegmentsData: any, method: string) { return false; } -function validateSegmentsData(maybeSegmentsData: any, method: string) { +function validateSegmentsData(log: ILogger, maybeSegmentsData: any, method: string) { if (isObject(maybeSegmentsData)) { const segmentNames = Object.keys(maybeSegmentsData); if (segmentNames.every(segmentName => isString(maybeSegmentsData[segmentName]))) return true; @@ -43,15 +42,15 @@ function validateSegmentsData(maybeSegmentsData: any, method: string) { return false; } -export function validatePreloadedData(maybePreloadedData: any, method: string): maybePreloadedData is SplitIO.PreloadedData { +export function validatePreloadedData(log: ILogger, maybePreloadedData: any, method: string): maybePreloadedData is SplitIO.PreloadedData { if (!isObject(maybePreloadedData)) { log.error(`${method}: preloadedData must be an object.`); } else if ( - validateTimestampData(maybePreloadedData.lastUpdated, method, 'lastUpdated') && - validateTimestampData(maybePreloadedData.since, method, 'since') && - validateSplitsData(maybePreloadedData.splitsData, method) && - (!maybePreloadedData.mySegmentsData || validateMySegmentsData(maybePreloadedData.mySegmentsData, method)) && - (!maybePreloadedData.segmentsData || validateSegmentsData(maybePreloadedData.segmentsData, method)) + validateTimestampData(log, maybePreloadedData.lastUpdated, method, 'lastUpdated') && + validateTimestampData(log, maybePreloadedData.since, method, 'since') && + validateSplitsData(log, maybePreloadedData.splitsData, method) && + (!maybePreloadedData.mySegmentsData || validateMySegmentsData(log, maybePreloadedData.mySegmentsData, method)) && + (!maybePreloadedData.segmentsData || validateSegmentsData(log, maybePreloadedData.segmentsData, method)) ) { return true; } diff --git a/src/utils/inputValidation/split.ts b/src/utils/inputValidation/split.ts index 5204bb6f..b7261604 100644 --- a/src/utils/inputValidation/split.ts +++ b/src/utils/inputValidation/split.ts @@ -1,24 +1,25 @@ +import { ERROR_NULL, ERROR_INVALID, WARN_TRIMMING, ERROR_EMPTY } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { isString } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory(''); + // include BOM and nbsp const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/; -export function validateSplit(maybeSplit: any, method: string, item = 'split name'): string | false { +export function validateSplit(log: ILogger, maybeSplit: any, method: string, item = 'split name'): string | false { if (maybeSplit == undefined) { // eslint-disable-line eqeqeq - log.error(`${method}: you passed a null or undefined ${item}, ${item} must be a non-empty string.`); + log.error(ERROR_NULL, [method, item]); } else if (!isString(maybeSplit)) { - log.error(`${method}: you passed an invalid ${item}, ${item} must be a non-empty string.`); + log.error(ERROR_INVALID, [method, item]); } else { if (TRIMMABLE_SPACES_REGEX.test(maybeSplit)) { - log.warn(`${method}: ${item} "${maybeSplit}" has extra whitespace, trimming.`); + log.warn(WARN_TRIMMING, [method, item, maybeSplit]); maybeSplit = maybeSplit.trim(); } if (maybeSplit.length > 0) { return maybeSplit; } else { - log.error(`${method}: you passed an empty ${item}, ${item} must be a non-empty string.`); + log.error(ERROR_EMPTY, [method, item]); } } diff --git a/src/utils/inputValidation/splitExistance.ts b/src/utils/inputValidation/splitExistance.ts index d8d34f58..ddafa767 100644 --- a/src/utils/inputValidation/splitExistance.ts +++ b/src/utils/inputValidation/splitExistance.ts @@ -1,16 +1,16 @@ import { SPLIT_NOT_FOUND } from '../labels'; import { IReadinessManager } from '../../readiness/types'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory(''); +import { ILogger } from '../../logger/types'; +import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants'; /** * This is defined here and in this format mostly because of the logger and the fact that it's considered a validation at product level. * But it's not going to run on the input validation layer. In any case, the most compeling reason to use it as we do is to avoid going to Redis and get a split twice. */ -export function validateSplitExistance(readinessManager: IReadinessManager, splitName: string, labelOrSplitObj: any, method: string): boolean { +export function validateSplitExistance(log: ILogger, readinessManager: IReadinessManager, splitName: string, labelOrSplitObj: any, method: string): boolean { if (readinessManager.isReady()) { // Only if it's ready we validate this, otherwise it may just be that the SDK is not ready yet. if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null) { - log.warn(`${method}: you passed "${splitName}" that does not exist in this environment, please double check what Splits exist in the web console.`); + log.warn(WARN_NOT_EXISTENT_SPLIT, [method, splitName]); return false; } } diff --git a/src/utils/inputValidation/splits.ts b/src/utils/inputValidation/splits.ts index dade5173..ee381210 100644 --- a/src/utils/inputValidation/splits.ts +++ b/src/utils/inputValidation/splits.ts @@ -1,14 +1,14 @@ +import { ERROR_EMPTY_ARRAY } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { uniq } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; import { validateSplit } from './split'; -const log = logFactory(''); -export function validateSplits(maybeSplits: any, method: string, listName = 'split_names', item = 'split name'): string[] | false { +export function validateSplits(log: ILogger, maybeSplits: any, method: string, listName = 'split_names', item = 'split name'): string[] | false { if (Array.isArray(maybeSplits) && maybeSplits.length > 0) { let validatedArray: string[] = []; // Remove invalid values maybeSplits.forEach(maybeSplit => { - const splitName = validateSplit(maybeSplit, method, item); + const splitName = validateSplit(log, maybeSplit, method, item); if (splitName) validatedArray.push(splitName); }); @@ -16,6 +16,6 @@ export function validateSplits(maybeSplits: any, method: string, listName = 'spl if (validatedArray.length) return uniq(validatedArray); } - log.error(`${method}: ${listName} must be a non-empty array.`); + log.error(ERROR_EMPTY_ARRAY, [method, listName]); return false; } diff --git a/src/utils/inputValidation/trafficType.ts b/src/utils/inputValidation/trafficType.ts index 05c9b36b..79f1eb02 100644 --- a/src/utils/inputValidation/trafficType.ts +++ b/src/utils/inputValidation/trafficType.ts @@ -1,20 +1,20 @@ +import { ERROR_NULL, ERROR_INVALID, ERROR_EMPTY, WARN_LOWERCASE_TRAFFIC_TYPE } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { isString } from '../lang'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory(''); const CAPITAL_LETTERS_REGEX = /[A-Z]/; -export function validateTrafficType(maybeTT: any, method: string): string | false { +export function validateTrafficType(log: ILogger, maybeTT: any, method: string): string | false { if (maybeTT == undefined) { // eslint-disable-line eqeqeq - log.error(`${method}: you passed a null or undefined traffic_type_name, traffic_type_name must be a non-empty string.`); + log.error(ERROR_NULL, [method]); } else if (!isString(maybeTT)) { - log.error(`${method}: you passed an invalid traffic_type_name, traffic_type_name must be a non-empty string.`); + log.error(ERROR_INVALID, [method]); } else { if (maybeTT.length === 0) { - log.error(`${method}: you passed an empty traffic_type_name, traffic_type_name must be a non-empty string.`); + log.error(ERROR_EMPTY, [method]); } else { if (CAPITAL_LETTERS_REGEX.test(maybeTT)) { - log.warn(`${method}: traffic_type_name should be all lowercase - converting string to lowercase.`); + log.warn(WARN_LOWERCASE_TRAFFIC_TYPE, [method]); maybeTT = maybeTT.toLowerCase(); } diff --git a/src/utils/inputValidation/trafficTypeExistance.ts b/src/utils/inputValidation/trafficTypeExistance.ts index b54da612..eb64c0d3 100644 --- a/src/utils/inputValidation/trafficTypeExistance.ts +++ b/src/utils/inputValidation/trafficTypeExistance.ts @@ -1,20 +1,20 @@ import thenable from '../promise/thenable'; import { LOCALHOST_MODE } from '../constants'; -import { logFactory } from '../../logger/sdkLogger'; import { ISplitsCacheBase } from '../../storages/types'; import { IReadinessManager } from '../../readiness/types'; import { SDKMode } from '../../types'; import { MaybeThenable } from '../../dtos/types'; -const log = logFactory(''); +import { ILogger } from '../../logger/types'; +import { WARN_NOT_EXISTENT_TT } from '../../logger/constants'; -function logTTExistanceWarning(maybeTT: string, method: string) { - log.warn(`${method}: Traffic Type ${maybeTT} does not have any corresponding Splits in this environment, make sure you're tracking your events to a valid traffic type defined in the Split console.`); +function logTTExistanceWarning(log: ILogger, maybeTT: string, method: string) { + log.warn(WARN_NOT_EXISTENT_TT, [method, maybeTT]); } /** * Separated from the previous method since on some cases it'll be async. */ -export function validateTrafficTypeExistance(readinessManager: IReadinessManager, splitsCache: ISplitsCacheBase, mode: SDKMode, maybeTT: string, method: string): MaybeThenable { +export function validateTrafficTypeExistance(log: ILogger, readinessManager: IReadinessManager, splitsCache: ISplitsCacheBase, mode: SDKMode, maybeTT: string, method: string): MaybeThenable { // If not ready or in localhost mode, we won't run the validation if (!readinessManager.isReady() || mode === LOCALHOST_MODE) return true; @@ -23,12 +23,12 @@ export function validateTrafficTypeExistance(readinessManager: IReadinessManager if (thenable(res)) { res.then(function (isValid) { - if (!isValid) logTTExistanceWarning(maybeTT, method); + if (!isValid) logTTExistanceWarning(log, maybeTT, method); return isValid; // propagate result }); } else { - if (!res) logTTExistanceWarning(maybeTT, method); + if (!res) logTTExistanceWarning(log, maybeTT, method); } return res; diff --git a/src/utils/lang/__tests__/index.spec.ts b/src/utils/lang/__tests__/index.spec.ts index 965153b5..51a62ccc 100644 --- a/src/utils/lang/__tests__/index.spec.ts +++ b/src/utils/lang/__tests__/index.spec.ts @@ -139,7 +139,7 @@ test('LANG UTILS / find', () => { const obj = { myKey: 'myVal', myOtherKey: 'myOtherVal' }; find(obj, spy); - expect(spy.mock.calls.length === 2).toBe(true); // The iteratee should be called as many times as elements we have on the collection. + expect(spy).toBeCalledTimes(2); // The iteratee should be called as many times as elements we have on the collection. expect(spy.mock.calls[0]).toEqual(['myVal', 'myKey', obj]); // When iterating on an object the iteratee should be called with (val, key, collection) expect(spy.mock.calls[1]).toEqual(['myOtherVal', 'myOtherKey', obj]); // When iterating on an object the iteratee should be called with (val, key, collection) @@ -147,7 +147,7 @@ test('LANG UTILS / find', () => { spy.mockClear(); find(arr, spy); - expect(spy.mock.calls.length === 2).toBe(true); // The iteratee should be called as many times as elements we have on the collection. + expect(spy).toBeCalledTimes(2); // The iteratee should be called as many times as elements we have on the collection. expect(spy.mock.calls[0]).toEqual(['one', 0, arr]); // When iterating on an array the iteratee should be called with (val, index, collection) expect(spy.mock.calls[1]).toEqual(['two', 1, arr]); // When iterating on an array the iteratee should be called with (val, index, collection) @@ -486,7 +486,7 @@ test('LANG UTILS / forOwn', () => { const obj = { myKey: 'myVal', myOtherKey: 'myOtherVal' }; forOwn(obj, spy); - expect(spy.mock.calls.length === 2).toBe(true); // The iteratee should be called as many times as elements we have on the object. + expect(spy).toBeCalledTimes(2); // The iteratee should be called as many times as elements we have on the object. expect(spy.mock.calls[0]).toEqual(['myVal', 'myKey', obj]); // When iterating on an object the iteratee should be called with (val, key, collection) expect(spy.mock.calls[1]).toEqual(['myOtherVal', 'myOtherKey', obj]); // When iterating on an object the iteratee should be called with (val, key, collection) diff --git a/src/utils/murmur3/murmur3.ts b/src/utils/murmur3/murmur3.ts index 3bdcbc77..434fcbd3 100644 --- a/src/utils/murmur3/murmur3.ts +++ b/src/utils/murmur3/murmur3.ts @@ -1,7 +1,7 @@ /* eslint-disable no-fallthrough */ import { UTF16ToUTF8, x86Fmix, x86Multiply, x86Rotl } from './commons'; -/*! +/* * +----------------------------------------------------------------------------------+ * | murmurHash3.js v3.0.0 (https://github.com/karanlyons/murmurHash3.js) | * | A TypeScript/JavaScript implementation of MurmurHash3's hashing algorithms. | diff --git a/src/utils/murmur3/murmur3_128.ts b/src/utils/murmur3/murmur3_128.ts index 4d5e3cf7..18140e29 100644 --- a/src/utils/murmur3/murmur3_128.ts +++ b/src/utils/murmur3/murmur3_128.ts @@ -6,7 +6,7 @@ const X86 = 'x86'; const X64_ARCHS = ['arm64', 'ppc64', 'x64', 's390x', 'mipsel']; const isX64 = getArchType() === X64; -/*! +/* * +----------------------------------------------------------------------------------+ * | murmurHash3.js v3.0.0 (https://github.com/karanlyons/murmurHash3.js) | * | A TypeScript/JavaScript implementation of MurmurHash3's hashing algorithms. | diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 8399060b..be0087b5 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { ISettings } from '../../../types'; import { OPTIMIZED, DEBUG } from '../../constants'; @@ -17,7 +18,8 @@ const minimalSettingsParams = { }, version: 'javascript-test', }, - runtime: () => ({ ip: false, hostname: false } as ISettings['runtime']) + runtime: () => ({ ip: false, hostname: false } as ISettings['runtime']), + logger: () => (loggerMock as ISettings['log']) }; describe('settingsValidation', () => { diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 330578b8..23b8dfee 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -1,5 +1,6 @@ import { InMemoryStorageCSFactory } from '../../../storages/inMemory/InMemoryStorageCS'; import { ISettings } from '../../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; export const settingsWithKey = { core: { @@ -7,7 +8,8 @@ export const settingsWithKey = { }, startup: { readyTimeout: 1, - } + }, + log: loggerMock }; export const settingsWithKeyAndTT = { @@ -17,7 +19,8 @@ export const settingsWithKeyAndTT = { }, startup: { readyTimeout: 1, - } + }, + log: loggerMock }; export const settingsWithKeyObject = { @@ -29,7 +32,8 @@ export const settingsWithKeyObject = { }, startup: { readyTimeout: 1, - } + }, + log: loggerMock }; export const fullSettings: ISettings = { @@ -75,7 +79,8 @@ export const fullSettings: ISettings = { sdk: 'sdk', auth: 'auth', streaming: 'streaming' - } + }, + log: loggerMock }; export const settingsSplitApi = { @@ -91,5 +96,6 @@ export const settingsSplitApi = { }, sync: { impressionsMode: 'DEBUG' - } -} as ISettings; + }, + log: loggerMock +}; diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index ea1c52f5..3aa994dd 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -1,4 +1,4 @@ -import { loggerMock, mockClear } from '../../../logger/__tests__/sdkLogger.mock'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { STANDALONE_MODE, CONSUMER_MODE } from '../../constants'; @@ -7,6 +7,7 @@ import { splitFilters, queryStrings, groupedFilters } from '../../../__tests__/m // Test target import { validateSplitFilters } from '../splitFilters'; +import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY } from '../../../logger/constants'; describe('validateSplitFilters', () => { @@ -16,31 +17,23 @@ describe('validateSplitFilters', () => { groupedFilters: { byName: [], byPrefix: [] } }; - test('Returns default output with empty values if `splitFilters` is an invalid object or `mode` is not \'standalone\'', () => { - - expect(validateSplitFilters(undefined, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(validateSplitFilters(null, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(loggerMock.warn.mock.calls.length === 0).toBe(true); - - expect(validateSplitFilters(true, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(loggerMock.warn.mock.calls[0]).toEqual(['Factory instantiation: splitFilters configuration must be a non-empty array of filter objects.']); - - expect(validateSplitFilters(15, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(loggerMock.warn.mock.calls[1]).toEqual(['Factory instantiation: splitFilters configuration must be a non-empty array of filter objects.']); + afterEach(() => { loggerMock.mockClear(); }); - expect(validateSplitFilters('string', STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(loggerMock.warn.mock.calls[2]).toEqual(['Factory instantiation: splitFilters configuration must be a non-empty array of filter objects.']); - - expect(validateSplitFilters([], STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(loggerMock.warn.mock.calls[3]).toEqual(['Factory instantiation: splitFilters configuration must be a non-empty array of filter objects.']); + test('Returns default output with empty values if `splitFilters` is an invalid object or `mode` is not \'standalone\'', () => { - expect(validateSplitFilters([{ type: 'byName', values: ['split_1'] }], CONSUMER_MODE)).toEqual(defaultOutput); - expect(loggerMock.warn.mock.calls[4]).toEqual(["Factory instantiation: split filters have been configured but will have no effect if mode is not 'standalone', since synchronization is being deferred to an external tool."]); + expect(validateSplitFilters(loggerMock, undefined, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, null, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(loggerMock.warn).not.toBeCalled(); - expect(loggerMock.debug.mock.calls.length === 0).toBe(true); - expect(loggerMock.error.mock.calls.length === 0).toBe(true); + expect(validateSplitFilters(loggerMock, true, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, 15, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, 'string', STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, [], STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, [{ type: 'byName', values: ['split_1'] }], CONSUMER_MODE)).toEqual(defaultOutput); // splitFilters ignored if not in 'standalone' mode + expect(loggerMock.warn.mock.calls).toEqual([[WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_IGNORED, ['standalone']]]); - mockClear(); + expect(loggerMock.debug).not.toBeCalled(); + expect(loggerMock.error).not.toBeCalled(); }); test('Returns object with null queryString, if `splitFilters` contains invalid filters or contains filters with no values or invalid values', () => { @@ -54,8 +47,8 @@ describe('validateSplitFilters', () => { queryString: null, groupedFilters: { byName: [], byPrefix: [] } }; - expect(validateSplitFilters(splitFilters, STANDALONE_MODE)).toEqual(output); // filters without values - expect(loggerMock.debug.mock.calls[0]).toEqual(["Factory instantiation: splits filtering criteria is 'null'."]); + expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // filters without values + expect(loggerMock.debug).toBeCalledWith(SETTINGS_SPLITS_FILTER, [null]); loggerMock.debug.mockClear(); splitFilters.push( @@ -64,20 +57,18 @@ describe('validateSplitFilters', () => { { type: null, values: [] }, { type: 'byName', values: [13] }); output.validFilters.push({ type: 'byName', values: [13] }); - expect(validateSplitFilters(splitFilters, STANDALONE_MODE)).toEqual(output); // some filters are invalid - expect(loggerMock.debug.mock.calls).toEqual([["Factory instantiation: splits filtering criteria is 'null'."]]); + expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // some filters are invalid + expect(loggerMock.debug.mock.calls).toEqual([[SETTINGS_SPLITS_FILTER, [null]]]); expect(loggerMock.warn.mock.calls).toEqual([ - ["Factory instantiation: split filter at position '3' is invalid. It must be an object with a valid filter type ('byName' or 'byPrefix') and a list of 'values'."], // invalid value of `type` property - ["Factory instantiation: split filter at position '4' is invalid. It must be an object with a valid filter type ('byName' or 'byPrefix') and a list of 'values'."], // invalid type of `values` property - ["Factory instantiation: split filter at position '5' is invalid. It must be an object with a valid filter type ('byName' or 'byPrefix') and a list of 'values'."] // invalid type of `type` property + [WARN_SPLITS_FILTER_INVALID, [3]], // invalid value of `type` property + [WARN_SPLITS_FILTER_INVALID, [4]], // invalid type of `values` property + [WARN_SPLITS_FILTER_INVALID, [5]] // invalid type of `type` property ]); expect(loggerMock.error.mock.calls).toEqual([ - ['Factory instantiation: you passed an invalid byName filter value, byName filter value must be a non-empty string.'], - ['Factory instantiation: byName filter must be a non-empty array.'] + [ERROR_INVALID, ['settings', 'byName filter value']], + [ERROR_EMPTY_ARRAY, ['settings', 'byName filter']] ]); - - mockClear(); }); test('Returns object with a queryString, if `splitFilters` contains at least a valid `byName` or `byPrefix` filter with at least a valid value', () => { @@ -90,15 +81,13 @@ describe('validateSplitFilters', () => { queryString: queryStrings[i], groupedFilters: groupedFilters[i] }; - expect(validateSplitFilters(splitFilters[i], STANDALONE_MODE)).toEqual(output); // splitFilters #${i} - expect(loggerMock.debug.mock.calls[loggerMock.debug.mock.calls.length - 1]).toEqual([`Factory instantiation: splits filtering criteria is '${queryStrings[i]}'.`]); + expect(validateSplitFilters(loggerMock, splitFilters[i], STANDALONE_MODE)).toEqual(output); // splitFilters #${i} + expect(loggerMock.debug).lastCalledWith(SETTINGS_SPLITS_FILTER, [queryStrings[i]]); } else { // tests where validateSplitFilters throws an exception - expect(() => validateSplitFilters(splitFilters[i], STANDALONE_MODE)).toThrow(queryStrings[i]); + expect(() => validateSplitFilters(loggerMock, splitFilters[i], STANDALONE_MODE)).toThrow(queryStrings[i]); } } - - mockClear(); }); }); diff --git a/src/utils/settingsValidation/impressionsMode.ts b/src/utils/settingsValidation/impressionsMode.ts index cd14e43e..84ae2f14 100644 --- a/src/utils/settingsValidation/impressionsMode.ts +++ b/src/utils/settingsValidation/impressionsMode.ts @@ -1,12 +1,12 @@ +import { ERROR_INVALID_IMPRESSIONS_MODE } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; import { SplitIO } from '../../types'; import { DEBUG, OPTIMIZED } from '../constants'; -import { logFactory } from '../../logger/sdkLogger'; -const log = logFactory('splitio-settings'); -export default function validImpressionsMode(impressionsMode: string): SplitIO.ImpressionsMode { +export default function validImpressionsMode(log: ILogger, impressionsMode: string): SplitIO.ImpressionsMode { impressionsMode = impressionsMode.toUpperCase(); if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) === -1) { - log.error(`You passed an invalid impressionsMode, impressionsMode should be one of the following values: '${DEBUG}' or '${OPTIMIZED}'. Defaulting to '${OPTIMIZED}' mode.`); + log.error(ERROR_INVALID_IMPRESSIONS_MODE, [[DEBUG, OPTIMIZED], OPTIMIZED]); impressionsMode = OPTIMIZED; } diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index a6ae1251..0f07c330 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -1,10 +1,8 @@ import { merge } from '../lang'; import mode from './mode'; import { validateSplitFilters } from './splitFilters'; -import { API } from '../../logger/sdkLogger'; import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE } from '../constants'; import validImpressionsMode from './impressionsMode'; -import { LogLevel } from '../../types'; import { ISettingsInternal, ISettingsValidationParams } from './types'; const base = { @@ -76,25 +74,16 @@ const base = { splitFilters: undefined, // impressions collection mode impressionsMode: OPTIMIZED - } + }, + + // Logger + log: undefined }; function fromSecondsToMillis(n: number) { return Math.round(n * 1000); } -function setupLogger(debugValue: any) { - if (typeof debugValue === 'boolean') { - if (debugValue) { - API.enable(); - } else { - API.disable(); - } - } else if (typeof debugValue === 'string') { - API.setLogLevel(debugValue as LogLevel); - } -} - /** * Validates the given config and use it to build a settings object. * @@ -103,11 +92,16 @@ function setupLogger(debugValue: any) { */ export function settingsValidation(config: unknown, validationParams: ISettingsValidationParams) { - const { defaults, runtime, storage, integrations } = validationParams; + const { defaults, runtime, storage, integrations, logger } = validationParams; // creates a settings object merging base, defaults and config objects. const withDefaults = merge({}, base, defaults, config) as ISettingsInternal; + // ensure a valid logger. + // First thing to validate, since other validators might use the logger. + const log = logger(withDefaults); // @ts-ignore, modify readonly prop + withDefaults.log = log; + // Scheduler periods withDefaults.scheduler.featuresRefreshRate = fromSecondsToMillis(withDefaults.scheduler.featuresRefreshRate); withDefaults.scheduler.segmentsRefreshRate = fromSecondsToMillis(withDefaults.scheduler.segmentsRefreshRate); @@ -122,31 +116,29 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV withDefaults.startup.eventsFirstPushWindow = fromSecondsToMillis(withDefaults.startup.eventsFirstPushWindow); // ensure a valid SDK mode - // @ts-ignore + // @ts-ignore, modify readonly prop withDefaults.mode = mode(withDefaults.core.authorizationKey, withDefaults.mode); // ensure a valid Storage based on mode defined. - // @ts-ignore + // @ts-ignore, modify readonly prop if (storage) withDefaults.storage = storage(withDefaults); - setupLogger(withDefaults.debug); - // Although `key` is mandatory according to TS declaration files, it can be omitted in LOCALHOST mode. In that case, the value `localhost_key` is used. if (withDefaults.mode === LOCALHOST_MODE && withDefaults.core.key === undefined) { withDefaults.core.key = 'localhost_key'; } // Current ip/hostname information - // @ts-ignore + // @ts-ignore, modify readonly prop withDefaults.runtime = runtime(withDefaults); // ensure a valid list of integrations. // `integrations` returns an array of valid integration items. - // @ts-ignore + // @ts-ignore, modify readonly prop if (integrations) withDefaults.integrations = integrations(withDefaults); // validate push options - if (withDefaults.streamingEnabled !== false) { // @ts-ignore + if (withDefaults.streamingEnabled !== false) { // @ts-ignore, modify readonly prop withDefaults.streamingEnabled = true; // Backoff bases. // We are not checking if bases are positive numbers. Thus, we might be reauthenticating immediately (`setTimeout` with NaN or negative number) @@ -154,12 +146,12 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV } // validate the `splitFilters` settings and parse splits query - const splitFiltersValidation = validateSplitFilters(withDefaults.sync.splitFilters, withDefaults.mode); - withDefaults.sync.splitFilters = splitFiltersValidation.validFilters; // @ts-ignore + const splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode); + withDefaults.sync.splitFilters = splitFiltersValidation.validFilters; withDefaults.sync.__splitFiltersValidation = splitFiltersValidation; // ensure a valid impressionsMode - withDefaults.sync.impressionsMode = validImpressionsMode(withDefaults.sync.impressionsMode); + withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode); return withDefaults; } diff --git a/src/utils/settingsValidation/integrations/__tests__/configurable.spec.ts b/src/utils/settingsValidation/integrations/__tests__/configurable.spec.ts index 2c7252b6..5c6989bb 100644 --- a/src/utils/settingsValidation/integrations/__tests__/configurable.spec.ts +++ b/src/utils/settingsValidation/integrations/__tests__/configurable.spec.ts @@ -1,3 +1,4 @@ +import { loggerMock as log } from '../../../../logger/__tests__/sdkLogger.mock'; import { validateConfigurableIntegrations } from '../configurable'; @@ -5,14 +6,14 @@ describe('integration validator for the configurable integrations', () => { // Check different types, since `integrations` param is defined by the user test('Returns an empty array if `integrations` is an invalid object', () => { - expect(validateConfigurableIntegrations({}, ['INT_TYPE'])).toEqual([]); - expect(validateConfigurableIntegrations({ integrations: undefined }, ['INT_TYPE'])).toEqual([]); - expect(validateConfigurableIntegrations({ integrations: true }, ['INT_TYPE'])).toEqual([]); - expect(validateConfigurableIntegrations({ integrations: 123 }, ['INT_TYPE'])).toEqual([]); - expect(validateConfigurableIntegrations({ integrations: 'string' }, ['INT_TYPE'])).toEqual([]); - expect(validateConfigurableIntegrations({ integrations: {} }, ['INT_TYPE'])).toEqual([]); - expect(validateConfigurableIntegrations({ integrations: [] }, ['INT_TYPE'])).toEqual([]); - expect(validateConfigurableIntegrations({ integrations: [false, 0, Infinity, new Error(), () => { }, []] }, ['INT_TYPE'])).toEqual([]); + expect(validateConfigurableIntegrations({ log }, ['INT_TYPE'])).toEqual([]); + expect(validateConfigurableIntegrations({ log, integrations: undefined }, ['INT_TYPE'])).toEqual([]); + expect(validateConfigurableIntegrations({ log, integrations: true }, ['INT_TYPE'])).toEqual([]); + expect(validateConfigurableIntegrations({ log, integrations: 123 }, ['INT_TYPE'])).toEqual([]); + expect(validateConfigurableIntegrations({ log, integrations: 'string' }, ['INT_TYPE'])).toEqual([]); + expect(validateConfigurableIntegrations({ log, integrations: {} }, ['INT_TYPE'])).toEqual([]); + expect(validateConfigurableIntegrations({ log, integrations: [] }, ['INT_TYPE'])).toEqual([]); + expect(validateConfigurableIntegrations({ log, integrations: [false, 0, Infinity, new Error(), () => { }, []] }, ['INT_TYPE'])).toEqual([]); }); test('Filters invalid integrations from `integrations` array', () => { @@ -34,15 +35,15 @@ describe('integration validator for the configurable integrations', () => { }; // All integrations are removed if no `validIntegrationTypes` array is passed - expect(validateConfigurableIntegrations({ integrations: [valid, validWithOptions, invalid] })) + expect(validateConfigurableIntegrations({ log, integrations: [valid, validWithOptions, invalid] })) .toEqual([]); // Integrations that do not have the passed types are removed - expect(validateConfigurableIntegrations({ integrations: [valid, validWithOptions, otherValidWithOptions, invalid] }, ['INT1'])) + expect(validateConfigurableIntegrations({ log, integrations: [valid, validWithOptions, otherValidWithOptions, invalid] }, ['INT1'])) .toEqual([valid, validWithOptions]); // Integrations that do not have the passed types or are invalid objects are removed - expect(validateConfigurableIntegrations({ integrations: [invalid, valid, false, 0, validWithOptions, Infinity, new Error(), otherValidWithOptions, () => { }, [], invalid] }, ['INT1', 'INT2'])) + expect(validateConfigurableIntegrations({ log, integrations: [invalid, valid, false, 0, validWithOptions, Infinity, new Error(), otherValidWithOptions, () => { }, [], invalid] }, ['INT1', 'INT2'])) .toEqual([valid, validWithOptions, otherValidWithOptions]); }); }); diff --git a/src/utils/settingsValidation/integrations/__tests__/plugable.spec.ts b/src/utils/settingsValidation/integrations/__tests__/plugable.spec.ts index d8056e34..a9b76aa1 100644 --- a/src/utils/settingsValidation/integrations/__tests__/plugable.spec.ts +++ b/src/utils/settingsValidation/integrations/__tests__/plugable.spec.ts @@ -1,18 +1,18 @@ import { validatePluggableIntegrations } from '../pluggable'; - +import { loggerMock as log } from '../../../../logger/__tests__/sdkLogger.mock'; describe('integrations validator for pluggable integrations', () => { // Check different types, since `integrations` param is defined by the user test('Returns an empty array if `integrations` is an invalid object', () => { - expect(validatePluggableIntegrations({})).toEqual([]); - expect(validatePluggableIntegrations({ integrations: undefined })).toEqual([]); - expect(validatePluggableIntegrations({ integrations: true })).toEqual([]); - expect(validatePluggableIntegrations({ integrations: 123 })).toEqual([]); - expect(validatePluggableIntegrations({ integrations: 'string' })).toEqual([]); - expect(validatePluggableIntegrations({ integrations: {} })).toEqual([]); - expect(validatePluggableIntegrations({ integrations: [] })).toEqual([]); - expect(validatePluggableIntegrations({ integrations: [false, 0, Infinity, new Error(), []] })).toEqual([]); + expect(validatePluggableIntegrations({ log })).toEqual([]); + expect(validatePluggableIntegrations({ log, integrations: undefined })).toEqual([]); + expect(validatePluggableIntegrations({ log, integrations: true })).toEqual([]); + expect(validatePluggableIntegrations({ log, integrations: 123 })).toEqual([]); + expect(validatePluggableIntegrations({ log, integrations: 'string' })).toEqual([]); + expect(validatePluggableIntegrations({ log, integrations: {} })).toEqual([]); + expect(validatePluggableIntegrations({ log, integrations: [] })).toEqual([]); + expect(validatePluggableIntegrations({ log, integrations: [false, 0, Infinity, new Error(), []] })).toEqual([]); }); test('Filters invalid integration factories from `integrations` array', () => { @@ -21,7 +21,7 @@ describe('integrations validator for pluggable integrations', () => { const invalid = { queue() { } }; // Integration factories that are invalid objects are removed - expect(validatePluggableIntegrations({ integrations: [invalid, validNoopIntFactory, false, 0, validIntFactory, Infinity, new Error(), [], invalid] })) + expect(validatePluggableIntegrations({ log, integrations: [invalid, validNoopIntFactory, false, 0, validIntFactory, Infinity, new Error(), [], invalid] })) .toEqual([validNoopIntFactory, validIntFactory]); }); diff --git a/src/utils/settingsValidation/integrations/common.ts b/src/utils/settingsValidation/integrations/common.ts index 850085b5..95fa1cd4 100644 --- a/src/utils/settingsValidation/integrations/common.ts +++ b/src/utils/settingsValidation/integrations/common.ts @@ -1,5 +1,5 @@ -import { logFactory } from '../../../logger/sdkLogger'; -const log = logFactory('splitio-settings'); +import { WARN_INTEGRATION_INVALID } from '../../../logger/constants'; +import { ILogger } from '../../../logger/types'; /** * This function validates `settings.integrations` object @@ -10,8 +10,8 @@ const log = logFactory('splitio-settings'); * * @returns {Array} array of valid integration items. The array might be empty if `settings` object does not have valid integrations. */ -export function validateIntegrations(settings: any, integrationValidator: (integrationItem: any) => boolean, extraWarning?: string) { - const { integrations } = settings; +export function validateIntegrations(settings: { log: ILogger, integrations?: any }, integrationValidator: (integrationItem: any) => boolean, extraWarning?: string) { + const { integrations, log } = settings; // If integrations is not an array or an empty array, we return an empty array (no integrations). if (!Array.isArray(integrations) || integrations.length === 0) return []; @@ -21,7 +21,7 @@ export function validateIntegrations(settings: any, integrationValidator: (integ // Log a warning if at least one item is invalid const invalids = integrations.length - validIntegrations.length; - if (invalids) log.warn(`${invalids} integration ${invalids === 1 ? 'item' : 'items'} at settings ${invalids === 1 ? 'is' : 'are'} invalid. ${extraWarning || ''}`); + if (invalids) log.warn(WARN_INTEGRATION_INVALID, [invalids, invalids === 1 ? 'item' : 'items', invalids === 1 ? 'is' : 'are', extraWarning || '']); return validIntegrations; } diff --git a/src/utils/settingsValidation/integrations/configurable.ts b/src/utils/settingsValidation/integrations/configurable.ts index b48818be..fb24e35b 100644 --- a/src/utils/settingsValidation/integrations/configurable.ts +++ b/src/utils/settingsValidation/integrations/configurable.ts @@ -1,5 +1,6 @@ import { validateIntegrations } from './common'; import { isString } from '../../lang'; +import { ILogger } from '../../../logger/types'; /** * This function validates `settings.integrations` object that consists of a list of configuration items, used by the isomorphic JS SDK. @@ -9,11 +10,11 @@ import { isString } from '../../lang'; * * @returns {Array} array of valid integration items. The array might be empty if `settings` object does not have valid integrations. */ -export function validateConfigurableIntegrations(settings: any, validIntegrationTypes: string[] = []) { +export function validateConfigurableIntegrations(settings: { log: ILogger, integrations?: any }, validIntegrationTypes: string[] = []) { return validateIntegrations( settings, integration => integration && isString(integration.type) && validIntegrationTypes.indexOf(integration.type) > -1, - 'Integration items must have a valid \'type\' value' + 'Integration items must have a valid "type" value' ); } diff --git a/src/utils/settingsValidation/integrations/pluggable.ts b/src/utils/settingsValidation/integrations/pluggable.ts index aaf8880c..b4a96ee9 100644 --- a/src/utils/settingsValidation/integrations/pluggable.ts +++ b/src/utils/settingsValidation/integrations/pluggable.ts @@ -1,5 +1,6 @@ import { ISettings } from '../../../types'; import { validateIntegrations } from './common'; +import { ILogger } from '../../../logger/types'; /** * This function validates `settings.integrations` object that consists of a list of pluggable integration factories. @@ -8,7 +9,7 @@ import { validateIntegrations } from './common'; * * @returns {Array} array of valid integration factories. The array might be empty if `settings` object does not have valid integrations. */ -export function validatePluggableIntegrations(settings: any): ISettings['integrations'] { +export function validatePluggableIntegrations(settings: { log: ILogger, integrations?: any }): ISettings['integrations'] { return validateIntegrations( settings, diff --git a/src/utils/settingsValidation/logger/__tests__/index.spec.ts b/src/utils/settingsValidation/logger/__tests__/index.spec.ts new file mode 100644 index 00000000..3f91c2db --- /dev/null +++ b/src/utils/settingsValidation/logger/__tests__/index.spec.ts @@ -0,0 +1,55 @@ +import { loggerMock, getLoggerLogLevel } from '../../../../logger/__tests__/sdkLogger.mock'; + +import { validateLogger as pluggableValidateLogger } from '../pluggableLogger'; +import { validateLogger as builtinValidateLogger } from '../builtinLogger'; + +const testTargets = [ + [pluggableValidateLogger], + [builtinValidateLogger] +]; + +describe('logger validators', () => { + + const consoleLogSpy = jest.spyOn(global.console, 'log'); + afterEach(() => { consoleLogSpy.mockClear(); }); + + test.each(testTargets)('returns a NONE logger if `debug` property is not defined or false', (validateLogger) => { // @ts-ignore + expect(getLoggerLogLevel(validateLogger({}))).toBe('NONE'); + expect(getLoggerLogLevel(validateLogger({ debug: undefined }))).toBe('NONE'); + expect(getLoggerLogLevel(validateLogger({ debug: false }))).toBe('NONE'); + + expect(consoleLogSpy).not.toBeCalled(); + }); + + test.each(testTargets)('returns a NONE logger if `debug` property is invalid and logs the error', (validateLogger) => { + expect(getLoggerLogLevel(validateLogger({ debug: null }))).toBe('NONE'); + expect(getLoggerLogLevel(validateLogger({ debug: 10 }))).toBe('NONE'); + expect(getLoggerLogLevel(validateLogger({ debug: {} }))).toBe('NONE'); + + if (validateLogger === builtinValidateLogger) { + // for builtinValidateLogger, a logger cannot be passed as `debug` property + expect(getLoggerLogLevel(validateLogger({ debug: loggerMock }))).toBe('NONE'); + expect(consoleLogSpy).toBeCalledTimes(4); + } else { + expect(consoleLogSpy).toBeCalledTimes(3); + } + }); + + test.each(testTargets)('returns a logger with the provided log level if `debug` property is true or a string log level', (validateLogger) => { + expect(getLoggerLogLevel(validateLogger({ debug: true }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'DEBUG' }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'INFO' }))).toBe('INFO'); + expect(getLoggerLogLevel(validateLogger({ debug: 'WARN' }))).toBe('WARN'); + expect(getLoggerLogLevel(validateLogger({ debug: 'ERROR' }))).toBe('ERROR'); + expect(getLoggerLogLevel(validateLogger({ debug: 'NONE' }))).toBe('NONE'); + + expect(consoleLogSpy).not.toBeCalled(); + }); + + test('pluggable logger validators / returns the provided logger at `debug` property if it is valid', () => { + expect(pluggableValidateLogger({ debug: loggerMock })).toBe(loggerMock); + + expect(consoleLogSpy).not.toBeCalled(); + }); + +}); diff --git a/src/utils/settingsValidation/logger/builtinLogger.ts b/src/utils/settingsValidation/logger/builtinLogger.ts new file mode 100644 index 00000000..5db9cfb0 --- /dev/null +++ b/src/utils/settingsValidation/logger/builtinLogger.ts @@ -0,0 +1,55 @@ +import { isLogLevelString, Logger, LogLevels } from '../../../logger'; +import { ILogger } from '../../../logger/types'; +import { isLocalStorageAvailable } from '../../env/isLocalStorageAvailable'; +import { isNode } from '../../env/isNode'; +import { codesDebug } from '../../../logger/messages/debug'; +import { _Map } from '../../lang/maps'; +import { getLogLevel } from './commons'; +import { LogLevel } from '../../../types'; + +const allCodes = new _Map(codesDebug); + +// @TODO set default debug setting instead of initialLogLevel when integrating in JS and Node packages +const LS_KEY = 'splitio_debug'; +const ENV_VAR_KEY = 'SPLITIO_DEBUG'; + +/** + * Logger initial debug level, that is disabled ('NONE') by default. + * Acceptable values are: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'. + * Other acceptable values are 'on', 'enable' and 'enabled', which are equivalent to 'DEBUG'. + * Any other string value is equivalent to disable. + */ +const initialState = String( + // eslint-disable-next-line no-undef + isNode ? process.env[ENV_VAR_KEY] : isLocalStorageAvailable() ? localStorage.getItem(LS_KEY) : '' +); + + +// By default it starts disabled. +let initialLogLevel = LogLevels.NONE; + +// Kept to avoid a breaking change ('on', 'enable' and 'enabled' are equivalent) +if (/^(enabled?|on)/i.test(initialState)) { + initialLogLevel = LogLevels.DEBUG; +} else if (isLogLevelString(initialState)) { + initialLogLevel = initialState; +} + +/** + * Validates the `debug` property at config and use it to set the log level. + * + * @param settings user config object, with an optional `debug` property of type boolean or string log level. + * @returns a logger instance with the log level at `settings.debug`. If `settings.debug` is invalid or not provided, `initialLogLevel` is used. + */ +export function validateLogger(settings: { debug: unknown }): ILogger { + const { debug } = settings; + + const logLevel: LogLevel | undefined = debug !== undefined ? getLogLevel(debug) : initialLogLevel; + + const log = new Logger({ logLevel: logLevel || initialLogLevel }, allCodes); + + // @ts-ignore // if logLevel is undefined at this point, it means that settings `debug` value is invalid + if (!logLevel) log._log(LogLevels.ERROR, 'Invalid Log Level - No changes to the logs will be applied.'); + + return log; +} diff --git a/src/utils/settingsValidation/logger/commons.ts b/src/utils/settingsValidation/logger/commons.ts new file mode 100644 index 00000000..a51ba991 --- /dev/null +++ b/src/utils/settingsValidation/logger/commons.ts @@ -0,0 +1,24 @@ + +import { LogLevels, isLogLevelString } from '../../../logger'; +import { LogLevel } from '../../../types'; + +/** + * Returns the LogLevel for the given debugValue or undefined if it is invalid, + * i.e., if the debugValue is not a boolean or LogLevel string. + * + * @param debugValue debug value at config + * @returns LogLevel of the given debugValue or undefined if the provided value is invalid + */ +export function getLogLevel(debugValue: unknown): LogLevel | undefined { + if (typeof debugValue === 'boolean') { + if (debugValue) { + return LogLevels.DEBUG; + } else { + return LogLevels.NONE; + } + } else if (typeof debugValue === 'string' && isLogLevelString(debugValue)) { + return debugValue; + } else { + return undefined; + } +} diff --git a/src/utils/settingsValidation/logger/pluggableLogger.ts b/src/utils/settingsValidation/logger/pluggableLogger.ts new file mode 100644 index 00000000..7b149990 --- /dev/null +++ b/src/utils/settingsValidation/logger/pluggableLogger.ts @@ -0,0 +1,35 @@ +import { Logger, LogLevels } from '../../../logger'; +import { ILogger } from '../../../logger/types'; +import { LogLevel } from '../../../types'; +import { getLogLevel } from './commons'; + +function isLogger(log: any): log is ILogger { + return log && typeof log.debug === 'function' && typeof log.info === 'function' && typeof log.warn === 'function' && typeof log.error === 'function' && typeof log.setLogLevel === 'function'; +} + +// By default it starts disabled. +let initialLogLevel = LogLevels.NONE; + +/** + * Validates the `debug` property at config and use it to set the logger. + * + * @param settings user config object, with an optional `debug` property of type boolean, string log level or a Logger object. + * @returns a logger instance, that might be: the provided logger at `settings.debug`, or one with the given `debug` log level, + * or one with NONE log level if `debug` is not defined or invalid. + */ +export function validateLogger(settings: { debug: unknown }): ILogger { + const { debug } = settings; + let logLevel: LogLevel | undefined = initialLogLevel; + + if (debug !== undefined) { + if (isLogger(debug)) return debug; + logLevel = getLogLevel(settings.debug); + } + + const log = new Logger({ logLevel: logLevel || initialLogLevel }); + + // @ts-ignore // `debug` value is invalid if logLevel is undefined at this point + if (!logLevel) log._log(LogLevels.ERROR, 'Invalid `debug` value at config. Logs will be disabled.'); + + return log; +} diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index b62e5a40..7c474952 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -1,9 +1,9 @@ import { STANDALONE_MODE } from '../constants'; import { validateSplits } from '../inputValidation/splits'; -import { logFactory } from '../../logger/sdkLogger'; import { ISplitFiltersValidation } from '../../dtos/types'; import { SplitIO } from '../../types'; -const log = logFactory(''); +import { ILogger } from '../../logger/types'; +import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, logPrefixSettings } from '../../logger/constants'; // Split filters metadata. // Ordered according to their precedency when forming the filter query string: `&names=&prefixes=` @@ -37,9 +37,9 @@ function validateFilterType(maybeFilterType: any): maybeFilterType is SplitIO.Sp * * @throws Error if the sanitized list exceeds the length indicated by `maxLength` */ -function validateSplitFilter(type: SplitIO.SplitFilterType, values: string[], maxLength: number) { +function validateSplitFilter(log: ILogger, type: SplitIO.SplitFilterType, values: string[], maxLength: number) { // validate and remove invalid and duplicated values - let result = validateSplits(values, 'Factory instantiation', `${type} filter`, `${type} filter value`); + let result = validateSplits(log, values, logPrefixSettings, `${type} filter`, `${type} filter value`); if (result) { // check max length @@ -75,6 +75,7 @@ function queryStringBuilder(groupedFilters: Record { - if (res.groupedFilters[type].length > 0) res.groupedFilters[type] = validateSplitFilter(type, res.groupedFilters[type], maxLength); + if (res.groupedFilters[type].length > 0) res.groupedFilters[type] = validateSplitFilter(log, type, res.groupedFilters[type], maxLength); }); // build query string res.queryString = queryStringBuilder(res.groupedFilters); - log.debug(`Factory instantiation: splits filtering criteria is '${res.queryString}'.`); + log.debug(SETTINGS_SPLITS_FILTER, [res.queryString]); return res; } diff --git a/src/utils/settingsValidation/storage/__tests__/storageCS.spec.ts b/src/utils/settingsValidation/storage/__tests__/storageCS.spec.ts index a8dcd3e4..8e1f1484 100644 --- a/src/utils/settingsValidation/storage/__tests__/storageCS.spec.ts +++ b/src/utils/settingsValidation/storage/__tests__/storageCS.spec.ts @@ -1,20 +1,21 @@ import { validateStorageCS } from '../storageCS'; import { InMemoryStorageCSFactory } from '../../../../storages/inMemory/InMemoryStorageCS'; +import { loggerMock as log } from '../../../../logger/__tests__/sdkLogger.mock'; describe('storage validator for pluggable storage (client-side)', () => { // Check different types, since `storage` param is defined by the user test('fallbacks to default InMemory storage if the storage is invalid or not provided', () => { - expect(validateStorageCS({})).toBe(InMemoryStorageCSFactory); - expect(validateStorageCS({ storage: undefined })).toBe(InMemoryStorageCSFactory); - expect(validateStorageCS({ storage: {} })).toBe(InMemoryStorageCSFactory); - expect(validateStorageCS({ storage: 'invalid storage' })).toBe(InMemoryStorageCSFactory); + expect(validateStorageCS({ log })).toBe(InMemoryStorageCSFactory); + expect(validateStorageCS({ log, storage: undefined })).toBe(InMemoryStorageCSFactory); + expect(validateStorageCS({ log, storage: {} })).toBe(InMemoryStorageCSFactory); + expect(validateStorageCS({ log, storage: 'invalid storage' })).toBe(InMemoryStorageCSFactory); }); test('returns the provided storage factory if it is valid', () => { - const mockStorageFactory = () => {}; - expect(validateStorageCS({ storage: mockStorageFactory })).toBe(mockStorageFactory); + const mockStorageFactory = () => { }; + expect(validateStorageCS({ log, storage: mockStorageFactory })).toBe(mockStorageFactory); }); }); diff --git a/src/utils/settingsValidation/storage/storageCS.ts b/src/utils/settingsValidation/storage/storageCS.ts index a0c03a76..d8d1406c 100644 --- a/src/utils/settingsValidation/storage/storageCS.ts +++ b/src/utils/settingsValidation/storage/storageCS.ts @@ -1,7 +1,7 @@ import { InMemoryStorageCSFactory } from '../../../storages/inMemory/InMemoryStorageCS'; import { ISettings } from '../../../types'; -import { logFactory } from '../../../logger/sdkLogger'; -const log = logFactory('splitio-settings'); +import { ILogger } from '../../../logger/types'; +import { WARN_STORAGE_INVALID } from '../../../logger/constants'; /** * This function validates `settings.storage` object @@ -10,14 +10,14 @@ const log = logFactory('splitio-settings'); * * @returns {Object} valid storage factory. It might be the default `InMemoryStorageCSFactory` if the provided storage is invalid. */ -export function validateStorageCS(settings: any): ISettings['storage'] { - const { storage } = settings; +export function validateStorageCS(settings: { log: ILogger, storage?: any }): ISettings['storage'] { + const { storage, log } = settings; // validate storage // @TODO validate its API (Splits cache, MySegments cache, etc) when supporting custom storages if (storage) { if (typeof storage === 'function') return storage; - log.warn('The provided storage is invalid. Fallbacking into default MEMORY storage'); + log.warn(WARN_STORAGE_INVALID); } // return default InMemory storage if provided one is not valid diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index d2845582..5a07379d 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -15,6 +15,8 @@ export interface ISettingsValidationParams { storage?: (settings: ISettings) => ISettings['storage'], /** Integrations validator */ integrations?: (settings: ISettings) => ISettings['integrations'], + /** Logger validator */ + logger: (settings: ISettings) => ISettings['log'], } /** diff --git a/src/utils/timeTracker/__tests__/index.spec.ts b/src/utils/timeTracker/__tests__/index.spec.ts index 7ce7fa84..d9348e27 100644 --- a/src/utils/timeTracker/__tests__/index.spec.ts +++ b/src/utils/timeTracker/__tests__/index.spec.ts @@ -1,5 +1,6 @@ import timer from '../timer'; import tracker from '../index'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('TIMER / should count the time between two tasks', (done) => { const timerDuration = Math.floor(Math.random() * 1000); // In millis @@ -26,9 +27,9 @@ describe('TIME TRACKER', () => { const promise = new Promise(res => { setTimeout(res, 1000); }); - const startNormal = tracker.start(tracker.TaskNames.SDK_READY); - const startNormalFake = tracker.start('fakeTask3'); - const startWithPromise = tracker.start('fakeTask4', undefined, promise) as Promise; + const startNormal = tracker.start(loggerMock, tracker.TaskNames.SDK_READY); + const startNormalFake = tracker.start(loggerMock, 'fakeTask3'); + const startWithPromise = tracker.start(loggerMock, 'fakeTask4', undefined, promise) as Promise; expect(typeof startNormal).toBe('function'); // If we call start without a promise, it will return the stop function, // @ts-expect-error @@ -39,14 +40,14 @@ describe('TIME TRACKER', () => { }); test('stop() / should stop the timer and return the time, if any', () => { - tracker.start('test_task'); + tracker.start(loggerMock, 'test_task'); // creating two tasks with the same task name - const stopFromStart = tracker.start('fakeTask5') as () => number; - const stopFromStart2 = tracker.start('fakeTask5') as () => number; + const stopFromStart = tracker.start(loggerMock, 'fakeTask5') as () => number; + const stopFromStart2 = tracker.start(loggerMock, 'fakeTask5') as () => number; - const stopNotExistentTask = tracker.stop('not_existent'); - const stopNotExistentTaskAndModifier = tracker.stop('test_task', 'mod'); + const stopNotExistentTask = tracker.stop(loggerMock, 'not_existent'); + const stopNotExistentTaskAndModifier = tracker.stop(loggerMock, 'test_task', 'mod'); expect(typeof stopNotExistentTask).toBe('undefined'); // If we try to stop a timer that does not exist, we get undefined. expect(typeof stopNotExistentTaskAndModifier).toBe('undefined'); // If we try to stop a timer that does not exist, we get undefined. diff --git a/src/utils/timeTracker/index.ts b/src/utils/timeTracker/index.ts index fa2dccb3..27dd3896 100644 --- a/src/utils/timeTracker/index.ts +++ b/src/utils/timeTracker/index.ts @@ -1,7 +1,7 @@ import { uniqueId } from '../lang'; -import { Logger } from '../../logger/index'; import timer from './timer'; import thenable from '../promise/thenable'; +import { ILogger } from '../../logger/types'; // Based on ProducerMetricsCollector and ClientCollector classes interface MetricsCollector { @@ -20,11 +20,6 @@ interface MetricsCollector { [method: string]: (ms: number) => void, } -// logger to be used on this module -const logger = new Logger('[TIME TRACKER]', { - showLevel: false -}); - // Map we will use for storing timers data const timers: Record void), @@ -120,20 +115,21 @@ const TrackerAPI = { /** * "Private" method, used to attach count/countException and stop callbacks to a promise. * + * @param {ILogger} log - Logger. * @param {Promise} promise - The promise we want to attach the callbacks. * @param {string} task - The name of the task. * @param {number | string} modifier - (optional) The modifier for the task, if any. */ - __attachToPromise(promise: Promise, task: string, collector: false | MetricsCollector, modifier?: number | string) { + __attachToPromise(log: ILogger, promise: Promise, task: string, collector: false | MetricsCollector, modifier?: number | string) { return promise.then(resp => { - this.stop(task, modifier); + this.stop(log, task, modifier); if (collector && collector.count) collector.count(resp.status); return resp; }) .catch(err => { - this.stop(task, modifier); + this.stop(log, task, modifier); if (collector && collector.countException) collector.countException(); @@ -144,12 +140,13 @@ const TrackerAPI = { * Starts tracking the time for a given task. All tasks tracked are considered "unique" because * there may be multiple SDK instances tracking a "generic" task, making any task non-generic. * + * @param {ILogger} log - Logger. * @param {string} task - The task we are starting. * @param {Object} collectors - The collectors map. * @param {Promise} promise - (optional) The promise we are tracking. * @return {Function | Promise} The stop function for this specific task or the promise received with the callbacks registered. */ - start(task: string, collectors?: Record, promise?: Promise, now?: () => number): Promise | (() => number) { + start(log: ILogger, task: string, collectors?: Record, promise?: Promise, now?: () => number): Promise | (() => number) { const taskUniqueId = uniqueId(); const taskCollector = getCollectorForTask(task, collectors); let result; @@ -157,10 +154,10 @@ const TrackerAPI = { // If we are registering a promise with this task, we should count the status and the exceptions as well // as stopping the task when the promise resolves. Then return the promise if (thenable(promise)) { - result = this.__attachToPromise(promise, task, taskCollector, taskUniqueId); + result = this.__attachToPromise(log, promise, task, taskCollector, taskUniqueId); } else { // If not, we return the stop function, as it will be stopped manually. - result = this.stop.bind(this, task, taskUniqueId); + result = this.stop.bind(this, log, task, taskUniqueId); if (CALLBACKS[task] && !taskCollector) { // and provide a way for a defered setup of the collector, if needed. // @ts-expect-error @@ -196,16 +193,17 @@ const TrackerAPI = { /** * Stops the tracking of a given task. * + * @param {ILogger} log - Logger. * @param {string} task - The task we are starting. * @param {number | string} modifier - (optional) The modifier for that specific task. */ - stop(task: string, modifier?: number | string) { + stop(log: ILogger, task: string, modifier?: number | string) { const timerName = generateTimerKey(task, modifier); const timerData = timers[timerName]; if (timerData) { // Stop the timer and round result for readability. const et = timerData.timer(); - logger.debug(`[${task}] took ${et}ms to finish.`); + log.debug(`[TIME TRACKER]: [${task}] took ${et}ms to finish.`); // Check if we have a tracker callback. if (timerData.cb) {