From 79914402e6c424762ae2985aed7b71d40096619d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 1 Feb 2022 13:21:01 -0300 Subject: [PATCH 01/53] fixed node-fetch vulnerability, and fetch API rejection handling --- package-lock.json | 35 +++++++++++++++++++++++++++++---- package.json | 2 +- src/services/splitHttpClient.ts | 2 +- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7be002a0..00e802b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5031,10 +5031,37 @@ "dev": true }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-int64": { "version": "0.4.0", diff --git a/package.json b/package.json index 9310f51f..9cce3f8a 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "jest-localstorage-mock": "^2.4.3", "js-yaml": "^3.14.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "redis-server": "1.2.2", "rimraf": "^3.0.2", "ts-jest": "^27.0.5", diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index ee031149..d9ec90f7 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -49,7 +49,7 @@ export function splitHttpClientFactory(settings: Pick { - const resp = error.response; + const resp = error && error.response; let msg = ''; if (resp) { // An HTTP error From d8824d2e04be8af1feceffa8e62de9bc6e4bdf97 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Feb 2022 17:12:40 -0300 Subject: [PATCH 02/53] added remaining unit tests and modules from JS SDK --- .../condition/__tests__/engineUtils.spec.ts | 5 - .../__tests__/mySegmentsV2utils.spec.ts | 10 +- src/utils/__tests__/Backoff.spec.ts | 44 +++++ .../{index.spec.ts.spec.ts => index.spec.ts} | 17 +- src/utils/lang/__tests__/binarySearch.spec.ts | 132 ++++++++++++++ src/utils/murmur3/__tests__/legacy.spec.ts | 21 +++ src/utils/murmur3/legacy.ts | 48 ++++++ src/utils/promise/__tests__/thenable.spec.ts | 22 +++ src/utils/promise/__tests__/timeout.spec.ts | 76 ++++++++ src/utils/promise/__tests__/wrapper.spec.ts | 162 ++++++++++++++++++ src/utils/promise/timeout.ts | 2 +- src/utils/time/__tests__/index.spec.ts | 9 + .../timeTracker/now/__tests__/now.spec.ts | 30 ++++ 13 files changed, 558 insertions(+), 20 deletions(-) create mode 100644 src/utils/__tests__/Backoff.spec.ts rename src/utils/key/__tests__/{index.spec.ts.spec.ts => index.spec.ts} (73%) create mode 100644 src/utils/lang/__tests__/binarySearch.spec.ts create mode 100644 src/utils/murmur3/__tests__/legacy.spec.ts create mode 100644 src/utils/murmur3/legacy.ts create mode 100644 src/utils/promise/__tests__/thenable.spec.ts create mode 100644 src/utils/promise/__tests__/timeout.spec.ts create mode 100644 src/utils/promise/__tests__/wrapper.spec.ts create mode 100644 src/utils/time/__tests__/index.spec.ts create mode 100644 src/utils/timeTracker/now/__tests__/now.spec.ts diff --git a/src/evaluator/condition/__tests__/engineUtils.spec.ts b/src/evaluator/condition/__tests__/engineUtils.spec.ts index 8682c851..72753bbd 100644 --- a/src/evaluator/condition/__tests__/engineUtils.spec.ts +++ b/src/evaluator/condition/__tests__/engineUtils.spec.ts @@ -18,7 +18,6 @@ test('ENGINE / should always evaluate to "off"', () => { expect(engineUtils.getTreatment(loggerMock, bucketingKey, seed, treatmentsMock) === 'off').toBe(true); // treatment should be 'off' - let endTime = Date.now(); console.log(`Evaluation takes ${(endTime - startTime) / 1000} seconds`); @@ -38,28 +37,24 @@ test('ENGINE / should always evaluate to "on"', () => { }); test('ENGINE / shouldApplyRollout - trafficAllocation 100', () => { - const shouldApplyRollout = engineUtils.shouldApplyRollout(100, 'asd', 14); expect(shouldApplyRollout).toBe(true); // Should return true as traffic allocation is 100. }); test('ENGINE / shouldApplyRollout - algo murmur | trafficAllocation 53 | bucket 51', () => { - const shouldApplyRollout = engineUtils.shouldApplyRollout(53, 'a', 29); expect(shouldApplyRollout).toBe(true); // Should return true as traffic allocation is 100. }); test('ENGINE / shouldApplyRollout - algo murmur | trafficAllocation 53 | bucket 56', () => { - const shouldApplyRollout = engineUtils.shouldApplyRollout(53, 'a', 31); expect(shouldApplyRollout).toBe(false); // Should return false as bucket is higher than trafficAllocation. }); test('ENGINE / shouldApplyRollout - algo murmur | trafficAllocation 1 | bucket 1', () => { - const shouldApplyRollout = engineUtils.shouldApplyRollout(1, 'aaaaaaklmnbv', -1667452163); expect(shouldApplyRollout).toBe(true); // Should return true as bucket is higher or equal than trafficAllocation. diff --git a/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts b/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts index b2826848..b24c6cad 100644 --- a/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts +++ b/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts @@ -16,19 +16,19 @@ test('parseKeyList', () => { addedUserKeys.forEach(userKey => { const hash = hash64(userKey); expect(added.has(hash.dec)).toBe(true); // key hash belongs to added list - expect(removed.has(hash.dec)).toBe(false); // t belong to removed list + expect(removed.has(hash.dec)).toBe(false); // key hash doesn\'t belong to removed list }); removedUserKeys.forEach(userKey => { const hash = hash64(userKey); - expect(added.has(hash.dec)).toBe(false); // t belong to added list + expect(added.has(hash.dec)).toBe(false); // key hash doesn\'t belong to added list expect(removed.has(hash.dec)).toBe(true); // key hash belongs to removed list }); otherUserKeys.forEach(userKey => { const hash = hash64(userKey); - expect(added.has(hash.dec)).toBe(false); // t belong to added list - expect(removed.has(hash.dec)).toBe(false); // t belong to removed list + expect(added.has(hash.dec)).toBe(false); // key hash doesn\'t belong to added list + expect(removed.has(hash.dec)).toBe(false); // key hash doesn\'t belong to removed list }); }); }); @@ -47,7 +47,7 @@ test('parseBitmap & isInBitmap', () => { falseUserKeys.forEach(userKey => { const hash = hash64(userKey); - expect(isInBitmap(actualBitmap, hash.hex)).toBe(false); // t belong to Bitmap + expect(isInBitmap(actualBitmap, hash.hex)).toBe(false); // key hash doesn\'t belong to Bitmap }); }); }); diff --git a/src/utils/__tests__/Backoff.spec.ts b/src/utils/__tests__/Backoff.spec.ts new file mode 100644 index 00000000..7fd5c083 --- /dev/null +++ b/src/utils/__tests__/Backoff.spec.ts @@ -0,0 +1,44 @@ +import { Backoff } from '../Backoff'; + +test('Backoff', (done) => { + + let start = Date.now(); + let backoff: Backoff; + + let alreadyReset = false; + const callback = () => { + const delta = Date.now() - start; + start += delta; + const expectedMillis = Math.min(backoff.baseMillis * Math.pow(2, backoff.attempts - 1), backoff.maxMillis); + + expect(delta > expectedMillis - 20 && delta < expectedMillis + 20).toBe(true); // executes callback at expected time + if (backoff.attempts <= 3) { + backoff.scheduleCall(); + } else { + backoff.reset(); + expect(backoff.attempts).toBe(0); // restarts attempts when `reset` called + expect(backoff.timeoutID).toBe(undefined); // restarts timeoutId when `reset` called + + // init the schedule cycle or finish the test + if (alreadyReset) { + done(); + } else { + alreadyReset = true; + backoff.scheduleCall(); + } + } + }; + + backoff = new Backoff(callback); + expect(backoff.cb).toBe(callback); // contains given callback + expect(backoff.baseMillis).toBe(Backoff.DEFAULT_BASE_MILLIS); // contains default baseMillis + expect(backoff.maxMillis).toBe(Backoff.DEFAULT_MAX_MILLIS); // contains default maxMillis + + const CUSTOM_BASE = 200; + const CUSTOM_MAX = 700; + backoff = new Backoff(callback, CUSTOM_BASE, CUSTOM_MAX); + expect(backoff.baseMillis).toBe(CUSTOM_BASE); // contains given baseMillis + expect(backoff.maxMillis).toBe(CUSTOM_MAX); // contains given maxMillis + + expect(backoff.scheduleCall()).toBe(backoff.baseMillis); // scheduleCall returns the scheduled delay time +}); diff --git a/src/utils/key/__tests__/index.spec.ts.spec.ts b/src/utils/key/__tests__/index.spec.ts similarity index 73% rename from src/utils/key/__tests__/index.spec.ts.spec.ts rename to src/utils/key/__tests__/index.spec.ts index ba1257f6..cd76e944 100644 --- a/src/utils/key/__tests__/index.spec.ts.spec.ts +++ b/src/utils/key/__tests__/index.spec.ts @@ -1,7 +1,6 @@ -import { SplitIO } from '../../../types'; import { keyParser, getMatching, getBucketing } from '../index'; -test('keyParser / if a string is passed as param, it should return a object', () => { +test('keyParser / if a string is passed as a param, it should return a object', () => { const key = 'some key'; const keyParsed = keyParser(key); @@ -24,7 +23,7 @@ test('keyParser / should return the keys configurations', () => { expect(keyParsed.bucketingKey).toBe(bucketingKey); // matching key should be equal to bucketingKey }); -test('getMatching / if a string is passed as a param it should return a string', () => { +test('getMatching / if a string is passed as a param, it should return a string', () => { const key = 'some key'; const keyParsed = getMatching(key); @@ -33,7 +32,7 @@ test('getMatching / if a string is passed as a param it should return a string', expect(keyParsed).toBe(key); // key parsed should be equal to key }); -test('getMatching / if a object is passed as a param it should return a string', () => { +test('getMatching / if a object is passed as a param, it should return a string', () => { const key = { matchingKey: 'some key', @@ -46,7 +45,7 @@ test('getMatching / if a object is passed as a param it should return a string', expect(keyParsed).toBe(key.matchingKey); // key parsed should be equal to key }); -test('getBucketing / should return undefined if a string is passed as a param and return undefined is set', () => { +test('getBucketing / if a string is passed as a param, it should return undefined', () => { const key = 'simple key'; @@ -55,11 +54,11 @@ test('getBucketing / should return undefined if a string is passed as a param an expect(keyParsed).toBe(undefined); // key parsed should return undefined }); -test('getBucketing / should return undefined if a string is passed as a param and return undefined is set', () => { +test('getBucketing / if a object is passed as a param, it should return the bucketingKey value', () => { - const key = { bucketingKey: 'test_buck_key' }; + const key: any = { bucketingKey: 'test_buck_key' }; - const keyParsed = getBucketing(key as SplitIO.SplitKeyObject); + const keyParsed = getBucketing(key); - expect(keyParsed).toBe('test_buck_key'); // key parsed should return undefined + expect(keyParsed).toBe('test_buck_key'); // key parsed should return the bucketingKey }); diff --git a/src/utils/lang/__tests__/binarySearch.spec.ts b/src/utils/lang/__tests__/binarySearch.spec.ts new file mode 100644 index 00000000..9b7a5510 --- /dev/null +++ b/src/utils/lang/__tests__/binarySearch.spec.ts @@ -0,0 +1,132 @@ +/** +Copyright 2022 Split Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +**/ +import { binarySearch } from '../binarySearch'; + +test('BINARY SEARCH / given [1,3,5,7,10] as dataset look for several elements', () => { + let searchFor = binarySearch.bind(null, [1, 3, 5, 7, 10]); + let index = undefined; + let value = -1; + + index = searchFor(value); + expect(index).toBe(0); // expected value 0 + + value++; // 0 + index = searchFor(value); + expect(index).toBe(0); // expected value 0 + + value++; // 1 + index = searchFor(value); + expect(index).toBe(0); // expected value 0 + + value++; // 2 + index = searchFor(value); + expect(index).toBe(0); // expected value 0 + + value++; // 3 + index = searchFor(value); + expect(index).toBe(1); // expected value 1 + + value++; // 4 + index = searchFor(value); + expect(index).toBe(1); // expected value 1 + + value++; // 5 + index = searchFor(value); + expect(index).toBe(2); // expected value 2 + + value++; // 6 + index = searchFor(value); + expect(index).toBe(2); // expected value 2 + + value++; // 7 + index = searchFor(value); + expect(index).toBe(3); // expected value 3 + + value++; // 8 + index = searchFor(value); + expect(index).toBe(3); // expected value 3 + + value++; // 9 + index = searchFor(value); + expect(index).toBe(3); // expected value 3 + + value++; // 10 + index = searchFor(value); + expect(index).toBe(4); // expected value 4 + + value++; // 11 + index = searchFor(value); + expect(index).toBe(4); // expected value 4 + + value++; // 12 + index = searchFor(value); + expect(index).toBe(4); // expected value 4 +}); + +test('BINARY SEARCH / run test using integer keys', () => { + const KEYS = [ + 1000, 1500, 2250, 3375, 5063, + 7594, 11391, 17086, 25629, 38443, + 57665, 86498, 129746, 194620, 291929, + 437894, 656841, 985261, 1477892, 2216838, + 3325257, 4987885, 7481828 + ]; + + let searchFor = binarySearch.bind(null, KEYS); + + let index = searchFor(10); + expect(index).toBe(0); // expected value 0 + + index = searchFor(1001); + expect(index).toBe(0); // expected value 0 + + index = searchFor(1499); + expect(index).toBe(0); // expected value 0 + + index = searchFor(1500); + expect(index).toBe(1); // expected value 1 + + index = searchFor(2200); + expect(index).toBe(1); // expected value 1 + + index = searchFor(2251); + expect(index).toBe(2); // expected value 2 +}); + +test('BINARY SEARCH / run test using float keys', () => { + const KEYS = [ + 1, 1.5, 2.25, 3.38, 5.06, 7.59, 11.39, 17.09, 25.63, 38.44, + 57.67, 86.5, 129.75, 194.62, 291.93, 437.89, 656.84, 985.26, 1477.89, + 2216.84, 3325.26, 4987.89, 7481.83 + ]; + + let searchFor = binarySearch.bind(null, KEYS); + + let index = searchFor(3.38); + expect(index).toBe(3); // expected value 3 + + index = searchFor(6); + expect(index).toBe(4); // expected value 4 + + index = searchFor(500.55); + expect(index).toBe(15); // expected value 15 + + index = searchFor(7481.83); + expect(index).toBe(22); // expected value 22 + + index = searchFor(80000); + expect(index).toBe(22); // expected value 22 +}); diff --git a/src/utils/murmur3/__tests__/legacy.spec.ts b/src/utils/murmur3/__tests__/legacy.spec.ts new file mode 100644 index 00000000..39ad41cb --- /dev/null +++ b/src/utils/murmur3/__tests__/legacy.spec.ts @@ -0,0 +1,21 @@ +// @ts-nocheck +import * as utils from '../legacy'; +import csv from 'csv-streamify'; +import fs from 'fs'; + +test('ENGINE / validate hashing behavior using sample data', (done) => { + let parser = csv({ objectMode: false }); + + parser.on('data', (line) => { + let [seed, key, hash, bucket] = JSON.parse(line.toString('utf8').trim()); + + seed = parseInt(seed, 10); + hash = parseInt(hash, 10); + bucket = parseInt(bucket, 10); + + expect(utils.hash(key, seed)).toBe(hash); // matching using int32 hash value + expect(utils.bucket(key, seed)).toBe(bucket); // matching using int32 bucket value + }).on('end', done); + + fs.createReadStream(require.resolve('./mocks/small-data.csv')).pipe(parser); +}); diff --git a/src/utils/murmur3/legacy.ts b/src/utils/murmur3/legacy.ts new file mode 100644 index 00000000..1797fb47 --- /dev/null +++ b/src/utils/murmur3/legacy.ts @@ -0,0 +1,48 @@ +// Deprecated hashing function, used for split bucketing. Replaced by murmur3 + +// +// JAVA reference implementation for the hashing function. +// +// int h = 0; +// for (int i = 0; i < key.length(); i++) { +// h = 31 * h + key.charAt(i); +// } +// return h ^ seed; // XOR the hash and seed +// + +function ToInteger(x: number) { + x = Number(x); + return x < 0 ? Math.ceil(x) : Math.floor(x); +} + +function modulo(a: number, b: number) { + return a - Math.floor(a / b) * b; +} + +function ToUint32(x: number) { + return modulo(ToInteger(x), Math.pow(2, 32)); +} + +function ToInt32(x: number) { + let uint32 = ToUint32(x); + + if (uint32 >= Math.pow(2, 31)) { + return uint32 - Math.pow(2, 32); + } else { + return uint32; + } +} + +export function hash(str: string, seed: number): number { + let h = 0; + + for (let i = 0; i < str.length; i++) { + h = ToInt32(ToInt32(31 * h) + str.charCodeAt(i)); + } + + return ToInt32(h ^ seed); +} + +export function bucket(str: string, seed: number): number { + return Math.abs(hash(str, seed) % 100) + 1; +} diff --git a/src/utils/promise/__tests__/thenable.spec.ts b/src/utils/promise/__tests__/thenable.spec.ts new file mode 100644 index 00000000..82dd9ff9 --- /dev/null +++ b/src/utils/promise/__tests__/thenable.spec.ts @@ -0,0 +1,22 @@ +import { thenable } from '../thenable'; + +test('Promise utils / thenable', () => { + const prom = new Promise(() => { }); + const promResolved = Promise.resolve(); + const promRejected = Promise.reject(); + const thenableThing = { then: () => { } }; + const nonThenableThing = { then: 'not a function' }; + + expect(thenable(prom)).toBe(true); // Promises and thenable-like objects should pass the test. + expect(thenable(promResolved)).toBe(true); // Promises and thenable-like objects should pass the test. + expect(thenable(promRejected)).toBe(true); // Promises and thenable-like objects should pass the test. + expect(thenable(thenableThing)).toBe(true); // Promises and thenable-like objects should pass the test. + + expect(thenable(nonThenableThing)).toBe(false); // Non thenable objects should fail the test. + expect(thenable('string')).toBe(false); // Non thenable objects should fail the test. + expect(thenable(123)).toBe(false); // Non thenable objects should fail the test. + expect(thenable({})).toBe(false); // Non thenable objects should fail the test. + expect(thenable({ catch: () => { } })).toBe(false); // Non thenable objects should fail the test. + expect(thenable([prom, promResolved])).toBe(false); // Non thenable objects should fail the test. + expect(thenable(() => { })).toBe(false); // Non thenable objects should fail the test. +}); diff --git a/src/utils/promise/__tests__/timeout.spec.ts b/src/utils/promise/__tests__/timeout.spec.ts new file mode 100644 index 00000000..da6e4446 --- /dev/null +++ b/src/utils/promise/__tests__/timeout.spec.ts @@ -0,0 +1,76 @@ +import { timeout } from '../timeout'; + +const baseTimeoutInMs = 20; +const resolutionValue = 'random_Value'; + +test('Promise utils / timeout - What happens in the event of a timeout or no timeout at all', async () => { + const prom = new Promise(() => { }); + + expect(timeout(0, prom)).toBe(prom); // If we set the timeout with a value less than 1, we just get the original promise (no timeout). + expect(timeout(-1, prom)).toBe(prom); // If we set the timeout with a value less than 1, we just get the original promise (no timeout). + + prom.then( + () => expect('This should not execute').toBeFalsy(), + () => expect('This should execute on timeout expiration.') + ); + + const ts = Date.now(); + const wrapperProm = timeout(baseTimeoutInMs, prom); + + expect(wrapperProm).not.toBe(prom); // If we actually set a timeout it should return a wrapping promise. + + try { + // This should be rejected after 10ms + await wrapperProm; + expect('Should not execute').toBeFalsy(); + } catch (error) { + // The promise was rejected not resolved. Give it an error margin of 10ms since it's not predictable + expect((Date.now() - ts) < baseTimeoutInMs + 20).toBe(true); // The timeout should have rejected the promise. + expect(error.message).toMatch(/^Operation timed out because it exceeded the configured time limit of/); // The timeout should have rejected the promise with a Split Timeout Error. + } +}); + +test('Promise utils / timeout - What happens if the promise resolves before the timeout.', async () => { + let promiseResolver: any = null; + const prom = new Promise(res => { promiseResolver = res; }); + const wrapperProm = timeout(baseTimeoutInMs * 100, prom); + + expect(wrapperProm).not.toBe(prom); // If we actually set a timeout it should return a wrapping promise. + + setTimeout(() => { + // Resolve the promise before the timeout + promiseResolver(resolutionValue); + }, baseTimeoutInMs * 10); + + // This one should not reject but be resolved + try { + // await prom; + const result = await wrapperProm; + + expect(result).toBe(resolutionValue); // The wrapper should resolve to the same value the original promise resolves. + } catch (error) { + expect('Should not execute').toBeFalsy(); + } +}); + +test('Promise utils / timeout - What happens if the promise rejects before the timeout.', async () => { + let promiseRejecter: any = null; + const prom = new Promise((res, rej) => { promiseRejecter = rej; }); + const wrapperProm = timeout(baseTimeoutInMs * 100, prom); + + expect(wrapperProm).not.toBe(prom); // If we actually set a timeout it should return a wrapping promise. + + setTimeout(() => { + // Reject the promise before the timeout + promiseRejecter(resolutionValue); + }, baseTimeoutInMs * 10); + + // This one should not resolve but be rejected + try { + await wrapperProm; + + expect('Should not execute').toBeFalsy(); + } catch (error) { + expect(error).toBe(resolutionValue); // The wrapper should reject to the same error than the original promise. + } +}); diff --git a/src/utils/promise/__tests__/wrapper.spec.ts b/src/utils/promise/__tests__/wrapper.spec.ts new file mode 100644 index 00000000..ca0d418a --- /dev/null +++ b/src/utils/promise/__tests__/wrapper.spec.ts @@ -0,0 +1,162 @@ +// @ts-nocheck +import { promiseWrapper } from '../wrapper'; + +test('Promise utils / promise wrapper', function (done) { + expect.assertions(58); // number of passHandler, passHandlerFinally, passHandlerWithThrow and `hasOnFulfilled` asserts + + const value = 'value'; + const failHandler = (val) => { done.fail(val); }; + const passHandler = (val) => { expect(val).toBe(value); return val; }; + const passHandlerFinally = (val) => { expect(val).toBeUndefined(); }; + const passHandlerWithThrow = (val) => { expect(val).toBe(value); throw val; }; + const createResolvedPromise = () => new Promise((res) => { setTimeout(() => { res(value); }, 100); }); + const createRejectedPromise = () => new Promise((_, rej) => { setTimeout(() => { rej(value); }, 100); }); + + // resolved promises + let wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.finally(passHandlerFinally); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler, failHandler).finally(passHandlerFinally); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler).catch(failHandler).finally(passHandlerFinally); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler).catch(failHandler).then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler).then(passHandler).catch(failHandler).finally(passHandlerFinally).then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler).then(passHandlerWithThrow).catch(passHandler).then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + const wrappedPromise2 = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise2.then(() => { + wrappedPromise2.then(passHandler); + }); + expect(wrappedPromise2.hasOnFulfilled()).toBe(true); + + Promise.all([ + promiseWrapper(createResolvedPromise(), failHandler), + promiseWrapper(createResolvedPromise(), failHandler)] + ).then((val) => { expect(val).toEqual([value, value]); }); + + // rejected promises + wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.catch(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.catch(passHandler).then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + // caveat: setting an `onFinally` handler as the first handler, requires an `onRejected` handler if promise is rejected + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.finally(passHandlerFinally).catch(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); + wrappedPromise.then(undefined, passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); + wrappedPromise.then(failHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler).then(failHandler).catch(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); + wrappedPromise.then(failHandler).then(failHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler, passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler).catch(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler).then(failHandler, passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler).catch(passHandler).then(passHandler).finally(passHandlerFinally); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + const wrappedPromise3 = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise3.catch(() => { + wrappedPromise3.catch(passHandler); + }); + expect(wrappedPromise3.hasOnFulfilled()).toBe(false); + + Promise.all([ + promiseWrapper(createResolvedPromise(), failHandler), + promiseWrapper(createRejectedPromise(), failHandler)]).catch(passHandler); + + setTimeout(() => { + done(); + }, 1000); + +}); + +test('Promise utils / promise wrapper: async/await', async function () { + + expect.assertions(8); // number of passHandler, passHandlerWithThrow and passHandlerFinally + + const value = 'value'; + const failHandler = (val) => { throw val; }; + const passHandler = (val) => { expect(val).toBe(value); return val; }; + const passHandlerFinally = (val) => { expect(val).toBeUndefined(); }; + const passHandlerWithThrow = (val) => { expect(val).toBe(value); throw val; }; + const createResolvedPromise = () => new Promise((res) => { res(value); }); + const createRejectedPromise = () => new Promise((res, rej) => { rej(value); }); + + try { + const result = await promiseWrapper(createResolvedPromise(), failHandler); + passHandler(result); + } catch (result) { + failHandler(result); + } finally { + passHandlerFinally(); + } + + try { + const result = await promiseWrapper(createRejectedPromise(), failHandler); + failHandler(result); + } catch (result) { + passHandler(result); + } + + let result; + try { + result = await promiseWrapper(createResolvedPromise(), failHandler); + passHandler(result); + passHandlerWithThrow(result); + } catch (error) { + result = passHandler(error); + } finally { + passHandlerFinally(); + } + passHandler(result); +}); diff --git a/src/utils/promise/timeout.ts b/src/utils/promise/timeout.ts index 9d3c12ee..5b8bebd7 100644 --- a/src/utils/promise/timeout.ts +++ b/src/utils/promise/timeout.ts @@ -3,7 +3,7 @@ export function timeout(ms: number, promise: Promise): Promise { return new Promise((resolve, reject) => { const tid = setTimeout(() => { - reject(new Error(`Operation timed out because it exceeded the configured time limit of ${ms}ms.`)); + reject(new Error(`Operation timed out because it exceeded the configured time limit of ${ms} ms.`)); }, ms); promise.then((res) => { diff --git a/src/utils/time/__tests__/index.spec.ts b/src/utils/time/__tests__/index.spec.ts new file mode 100644 index 00000000..9bd0c8fe --- /dev/null +++ b/src/utils/time/__tests__/index.spec.ts @@ -0,0 +1,9 @@ +import { truncateTimeFrame } from '..'; + +test('Test truncateTimeFrame', () => { + expect(truncateTimeFrame(new Date(2020, 9, 2, 10, 53, 12).getTime())).toBe(new Date(2020, 9, 2, 10, 0, 0).getTime()); + expect(truncateTimeFrame(new Date(2020, 9, 2, 10, 0, 0).getTime())).toBe(new Date(2020, 9, 2, 10, 0, 0).getTime()); + expect(truncateTimeFrame(new Date(2020, 9, 2, 10, 53, 0).getTime())).toBe(new Date(2020, 9, 2, 10, 0, 0).getTime()); + expect(truncateTimeFrame(new Date(2020, 9, 2, 10, 0, 12).getTime())).toBe(new Date(2020, 9, 2, 10, 0, 0).getTime()); + expect(truncateTimeFrame(new Date(1970, 1, 0, 0, 0, 0).getTime())).toBe(new Date(1970, 1, 0, 0, 0, 0).getTime()); +}); diff --git a/src/utils/timeTracker/now/__tests__/now.spec.ts b/src/utils/timeTracker/now/__tests__/now.spec.ts new file mode 100644 index 00000000..ded2c815 --- /dev/null +++ b/src/utils/timeTracker/now/__tests__/now.spec.ts @@ -0,0 +1,30 @@ +/** +Copyright 2022 Split Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +**/ + +import { now as nowBrowser } from '../browser'; +import { now as nowNode } from '../node'; + +[nowBrowser, nowNode].forEach(now => { + test('NOW / should generate a value each time you call it', () => { + let n1 = now(); + let n2 = now(); + let n3 = now(); + + expect(Number.isFinite(n1)).toBe(true); // is a finite value? + expect(Number.isFinite(n2)).toBe(true); // is a finite value? + expect(Number.isFinite(n3)).toBe(true); // is a finite value? + }); +}); From 3a5141a6f587636bc5362521a6bd2e98f47bb0c6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Feb 2022 17:35:04 -0300 Subject: [PATCH 03/53] added isBrowserClient flag to SDK clients --- src/sdkClient/client.ts | 3 ++- src/sdkClient/clientCS.ts | 4 +++- src/types.ts | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index a572047e..5b0b7de2 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -123,6 +123,7 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S getTreatmentWithConfig, getTreatments, getTreatmentsWithConfig, - track + track, + isBrowserClient: false } as SplitIO.IClient | SplitIO.IAsyncClient; } diff --git a/src/sdkClient/clientCS.ts b/src/sdkClient/clientCS.ts index 8cf0d6a2..16648bc6 100644 --- a/src/sdkClient/clientCS.ts +++ b/src/sdkClient/clientCS.ts @@ -23,6 +23,8 @@ export function clientCSDecorator(log: ILogger, client: SplitIO.IClient, key: Sp getTreatmentsWithConfig: clientCS.getTreatmentsWithConfig.bind(clientCS, key), // Key is bound to the `track` method. Same thing happens with trafficType but only if provided - track: trafficType ? clientCS.track.bind(clientCS, key, trafficType) : clientCS.track.bind(clientCS, key) + track: trafficType ? clientCS.track.bind(clientCS, key, trafficType) : clientCS.track.bind(clientCS, key), + + isBrowserClient: true }) as SplitIO.ICsClient; } diff --git a/src/types.ts b/src/types.ts index 9a94dd45..46451a60 100644 --- a/src/types.ts +++ b/src/types.ts @@ -381,6 +381,10 @@ interface IBasicClient extends IStatusInterface { * @returns {Promise} */ destroy(): Promise + + // Whether the client implements the client-side API, i.e, with bound key, (true), or the server-side API (false). + // Exposed for internal purposes only. Not considered part of the public API, and might be renamed eventually. + isBrowserClient: boolean } /** * Common definitions between SDK instances for different environments interface. @@ -1139,7 +1143,7 @@ export namespace SplitIO { /** * Removes from client's in memory attributes storage the attribute with the given key * @function removeAttribute - * @param {string} attributeName + * @param {string} attributeName * @returns {boolean} true if attribute was removed and false otherways */ removeAttribute(attributeName: string): boolean, From cdd85585940253ea80c141b9d532d5c5f343c3a0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 7 Feb 2022 14:11:41 -0300 Subject: [PATCH 04/53] upgrade node-fetch for vulnerability fix --- package-lock.json | 35 +++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7be002a0..00e802b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5031,10 +5031,37 @@ "dev": true }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-int64": { "version": "0.4.0", diff --git a/package.json b/package.json index 9310f51f..9cce3f8a 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "jest-localstorage-mock": "^2.4.3", "js-yaml": "^3.14.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "redis-server": "1.2.2", "rimraf": "^3.0.2", "ts-jest": "^27.0.5", From f4fbd1a105ecfb5f4b3163c5612f0928dab05dc8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 10 Feb 2022 17:16:16 -0300 Subject: [PATCH 05/53] update push manager and add unit tests --- package-lock.json | 2 +- package.json | 2 +- src/logger/messages/info.ts | 6 +- src/sync/__tests__/syncTask.mock.ts | 9 + src/sync/streaming/AuthClient/index.ts | 3 +- .../streaming/__tests__/pushManager.spec.ts | 157 ++++++++++++++++-- src/sync/streaming/pushManager.ts | 49 +++--- src/sync/syncManagerOnline.ts | 16 +- .../__tests__/settings.mocks.ts | 11 +- 9 files changed, 204 insertions(+), 51 deletions(-) create mode 100644 src/sync/__tests__/syncTask.mock.ts diff --git a/package-lock.json b/package-lock.json index 00e802b2..9456d98a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.0", + "version": "1.2.1-rc.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9cce3f8a..012f2ef0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.0", + "version": "1.2.1-rc.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 42838345..298711bd 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -23,10 +23,10 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.SUBMITTERS_PUSH_FULL_EVENTS_QUEUE, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing full events queue and reseting timer.'], [c.SUBMITTERS_PUSH, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Pushing %s %s.'], [c.STREAMING_REFRESH_TOKEN, c.LOG_PREFIX_SYNC_STREAMING + 'Refreshing streaming token in %s seconds, and connecting streaming in %s seconds.'], - [c.STREAMING_RECONNECT, c.LOG_PREFIX_SYNC_STREAMING + 'Attempting to reconnect in %s seconds.'], - [c.STREAMING_CONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Connecting to streaming.'], + [c.STREAMING_RECONNECT, c.LOG_PREFIX_SYNC_STREAMING + 'Attempting to reconnect streaming in %s seconds.'], + [c.STREAMING_CONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Connecting streaming.'], [c.STREAMING_DISABLED, c.LOG_PREFIX_SYNC_STREAMING + 'Streaming is disabled for given Api key. Switching to polling mode.'], - [c.STREAMING_DISCONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Disconnecting from streaming.'], + [c.STREAMING_DISCONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Disconnecting streaming.'], [c.SYNC_START_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming not available. Starting polling.'], [c.SYNC_CONTINUE_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming couldn\'t connect. Continue polling.'], [c.SYNC_STOP_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming (re)connected. Syncing and stopping polling.'], diff --git a/src/sync/__tests__/syncTask.mock.ts b/src/sync/__tests__/syncTask.mock.ts new file mode 100644 index 00000000..10ef439e --- /dev/null +++ b/src/sync/__tests__/syncTask.mock.ts @@ -0,0 +1,9 @@ +export function syncTaskFactory() { + return { + execute: jest.fn(), + isExecuting: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + isRunning: jest.fn(), + }; +} diff --git a/src/sync/streaming/AuthClient/index.ts b/src/sync/streaming/AuthClient/index.ts index 96ae9ca8..b8d81c55 100644 --- a/src/sync/streaming/AuthClient/index.ts +++ b/src/sync/streaming/AuthClient/index.ts @@ -17,8 +17,7 @@ export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate { * @param {string[] | undefined} userKeys set of user Keys to track MY_SEGMENTS_CHANGES. It is undefined for server-side API. */ return function authenticate(userKeys?: string[]): Promise { - let authPromise = fetchAuth(userKeys); // errors handled by fetchAuth service - return authPromise + return fetchAuth(userKeys) .then(resp => resp.json()) .then(json => { if (json.token) { // empty token when `"pushEnabled": false` diff --git a/src/sync/streaming/__tests__/pushManager.spec.ts b/src/sync/streaming/__tests__/pushManager.spec.ts index 26b63f7d..99b75473 100644 --- a/src/sync/streaming/__tests__/pushManager.spec.ts +++ b/src/sync/streaming/__tests__/pushManager.spec.ts @@ -1,7 +1,10 @@ -import { pushManagerFactory } from '../pushManager'; import { EventEmitter } from '../../../utils/MinEvents'; -import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; +import { fullSettings, fullSettingsServerSide } from '../../../utils/settingsValidation/__tests__/settings.mocks'; import { IPushManagerCS } from '../types'; +import { syncTaskFactory } from '../../__tests__/syncTask.mock'; + +// Test target +import { pushManagerFactory } from '../pushManager'; const platformMock = { getEventSource: jest.fn(() => { return () => { }; }), @@ -18,19 +21,149 @@ test('pushManagerFactory returns undefined if EventSource is not available', () expect(pushManager).toBe(undefined); }); -test('pushManager does not connect to streaming if it is stopped inmediatelly after being started', (done) => { - const fetchAuthMock = jest.fn(); +describe('pushManager in client-side', () => { - // @ts-ignore - const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, fullSettings) as IPushManagerCS; + test('does not connect to streaming if it is stopped inmediatelly after being started', async () => { + const fetchAuthMock = jest.fn(); + + // @ts-ignore + const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, fullSettings) as IPushManagerCS; + + pushManager.start(); + pushManager.start(); + pushManager.stop(); + pushManager.stop(); + pushManager.start(); + pushManager.stop(); + + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).not.toBeCalled(); + }); + + test('re-authenticates asynchronously when it is resumed and when new users are added', async () => { + + // @TODO assert log messages + const fetchAuthMock = jest.fn(() => Promise.reject({ message: 'error' })); + + // @ts-ignore + const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, { + ...fullSettings, + scheduler: { + ...fullSettings.scheduler, + pushRetryBackoffBase: 10 // high authentication backoff + } + }) as IPushManagerCS; + + // calling start multiple times has no effect (authenticates only once) + pushManager.start(); + pushManager.start(); + + // authenticates asynchronously, only once for both users + const mySegmentsSyncTask = syncTaskFactory(); + pushManager.add('user2', mySegmentsSyncTask); + expect(fetchAuthMock).toHaveBeenCalledTimes(0); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenLastCalledWith([fullSettings.core.key, 'user2']); + + // re-authenticates asynchronously due to new users + pushManager.add('user3', mySegmentsSyncTask); + pushManager.add('user4', mySegmentsSyncTask); + expect(fetchAuthMock).toHaveBeenCalledTimes(1); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenLastCalledWith([fullSettings.core.key, 'user2', 'user3', 'user4']); + + // pausing + pushManager.stop(); + pushManager.stop(); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenCalledTimes(2); - pushManager.start(); - pushManager.start(); - pushManager.stop(); - pushManager.stop(); + // re-authenticates asynchronously when resuming + pushManager.start(); + pushManager.start(); + expect(fetchAuthMock).toHaveBeenCalledTimes(2); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenLastCalledWith(['user2', 'user3', 'user4', fullSettings.core.key]); + expect(fetchAuthMock).toHaveBeenCalledTimes(3); - setTimeout(() => { + // doesn't re-authenticate if a user is removed + pushManager.remove('user3'); + expect(fetchAuthMock).toHaveBeenCalledTimes(3); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenCalledTimes(3); + + // re-authenticates asynchronously when resuming, considering the updated list of users + pushManager.stop(); + pushManager.start(); + pushManager.stop(); + pushManager.start(); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenLastCalledWith(['user2', 'user4', fullSettings.core.key]); + expect(fetchAuthMock).toHaveBeenCalledTimes(4); + pushManager.stop(); + }); +}); + +describe('pushManager in server-side', () => { + + test('does not connect to streaming if it is stopped inmediatelly after being started', async () => { + const fetchAuthMock = jest.fn(); + + // @ts-ignore + const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, fullSettingsServerSide) as IPushManagerCS; + + pushManager.start(); + pushManager.start(); + pushManager.stop(); + pushManager.stop(); + pushManager.start(); + pushManager.stop(); + + await new Promise(res => setTimeout(res)); expect(fetchAuthMock).not.toBeCalled(); - done(); }); + + test('re-authenticates asynchronously when it is resumed', async () => { + const fetchAuthMock = jest.fn(() => Promise.reject({ message: 'error' })); + + // @ts-ignore + const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, { + ...fullSettingsServerSide, + scheduler: { + ...fullSettingsServerSide.scheduler, + pushRetryBackoffBase: 10 // high authentication backoff + } + }) as IPushManagerCS; + + // calling start multiple times has no effect (authenticates asynchronously only once) + pushManager.start(); + pushManager.start(); + expect(fetchAuthMock).toHaveBeenCalledTimes(0); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenLastCalledWith(undefined); + + // pausing + pushManager.stop(); + pushManager.stop(); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenCalledTimes(1); + + // re-authenticates asynchronously when resuming + pushManager.start(); + pushManager.start(); + expect(fetchAuthMock).toHaveBeenCalledTimes(1); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenCalledTimes(2); + + // re-authenticates asynchronously when stopping/resuming + pushManager.stop(); + pushManager.stop(); + pushManager.start(); + pushManager.start(); + expect(fetchAuthMock).toHaveBeenCalledTimes(2); + await new Promise(res => setTimeout(res)); + expect(fetchAuthMock).toHaveBeenCalledTimes(3); + pushManager.stop(); + }); + }); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index f5130447..55a67549 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -40,7 +40,7 @@ export function pushManagerFactory( // `userKey` is the matching key of main client in client-side SDK. // It can be used to check if running on client-side or server-side SDK. - const userKey = settings.core.key ? getMatching(settings.core.key) : undefined; // + const userKey = settings.core.key ? getMatching(settings.core.key) : undefined; const log = settings.log; let sseClient: ISSEClient; @@ -59,7 +59,8 @@ export function pushManagerFactory( sseClient.setEventHandler(sseHandler); // init workers - const segmentsUpdateWorker = userKey ? new MySegmentsUpdateWorker(pollingManager.segmentsSyncTask) : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); + // MySegmentsUpdateWorker (client-side) are initiated in `add` method + const segmentsUpdateWorker = userKey ? undefined : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); // For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); @@ -68,11 +69,6 @@ export function pushManagerFactory( // [Only for client-side] map of user keys to their corresponding hash64 and MySegmentsUpdateWorkers. // Hash64 is used to process MY_SEGMENTS_UPDATE_V2 events and dispatch actions to the corresponding MySegmentsUpdateWorker. const clients: Record = {}; - if (userKey) { - const hash = hashUserKey(userKey); - userKeyHashes[hash] = userKey; - clients[userKey] = { hash64: hash64(userKey), worker: segmentsUpdateWorker as MySegmentsUpdateWorker }; - } // [Only for client-side] variable to flag that a new client was added. It is needed to reconnect streaming. let connectForNewClient = false; @@ -176,7 +172,7 @@ export function pushManagerFactory( function stopWorkers() { splitsUpdateWorker.backoff.reset(); if (userKey) forOwn(clients, ({ worker }) => worker.backoff.reset()); - else segmentsUpdateWorker.backoff.reset(); + else (segmentsUpdateWorker as SegmentsUpdateWorker).backoff.reset(); } pushEmitter.on(PUSH_SUBSYSTEM_DOWN, stopWorkers); @@ -306,37 +302,40 @@ export function pushManagerFactory( // Expose Event Emitter functionality and Event constants Object.create(pushEmitter), { - // Expose functionality for starting and stoping push mode: - stop: disconnectPush, // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which starts polling. - + // Stop/pause push mode + stop() { + disconnectPush(); // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which starts polling. + if (userKey) this.remove(userKey); // Necessary to properly resume streaming in client-side (e.g., RN SDK transition to foreground). + }, + // Start/resume push mode start() { // Guard condition to avoid calling `connectPush` again if the `start` method is called multiple times or if push has been disabled. if (disabled || disconnected === false) return; disconnected = false; - // Run in next event-loop cycle for optimization on client-side: if multiple clients are created in the same cycle than the factory, only one authentication is performed. - setTimeout(connectPush); + + if (userKey) this.add(userKey, pollingManager.segmentsSyncTask); // client-side + else setTimeout(connectPush); // server-side runs in next cycle as in client-side, for tests consistency }, // [Only for client-side] add(userKey: string, mySegmentsSyncTask: ISegmentsSyncTask) { - clients[userKey] = { hash64: hash64(userKey), worker: new MySegmentsUpdateWorker(mySegmentsSyncTask) }; - const hash = hashUserKey(userKey); if (!userKeyHashes[hash]) { userKeyHashes[hash] = userKey; + clients[userKey] = { hash64: hash64(userKey), worker: new MySegmentsUpdateWorker(mySegmentsSyncTask) }; connectForNewClient = true; // we must reconnect on start, to listen the channel for the new user key - } - // Reconnects in case of a new client. - // Run in next event-loop cycle to save authentication calls - // in case the user is creating several clients in the current cycle. - setTimeout(function checkForReconnect() { - if (connectForNewClient) { - connectForNewClient = false; - connectPush(); - } - }, 0); + // Reconnects in case of a new client. + // Run in next event-loop cycle to save authentication calls + // in case multiple clients are created in the current cycle. + setTimeout(function checkForReconnect() { + if (connectForNewClient) { + connectForNewClient = false; + connectPush(); + } + }, 0); + } }, // [Only for client-side] remove(userKey: string) { diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index c8264106..1b6ec876 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -49,11 +49,11 @@ export function syncManagerOnlineFactory( /** Sync Manager logic */ function startPolling() { - if (!pollingManager!.isRunning()) { + if (pollingManager!.isRunning()) { + log.info(SYNC_CONTINUE_POLLING); + } else { log.info(SYNC_START_POLLING); pollingManager!.start(); - } else { - log.info(SYNC_CONTINUE_POLLING); } } @@ -81,6 +81,9 @@ export function syncManagerOnlineFactory( * Method used to start the syncManager for the first time, or resume it after being stopped. */ start() { + if (running) return; + running = true; + // start syncing splits and segments if (pollingManager) { if (pushManager) { @@ -96,21 +99,22 @@ export function syncManagerOnlineFactory( } // start periodic data recording (events, impressions, telemetry). - submitter && submitter.start(); - running = true; + if (submitter) submitter.start(); }, /** * Method used to stop/pause the syncManager. */ stop() { + if (!running) return; + running = false; + // stop syncing if (pushManager) pushManager.stop(); if (pollingManager && pollingManager.isRunning()) pollingManager.stop(); // stop periodic data recording (events, impressions, telemetry). if (submitter) submitter.stop(); - running = false; }, isRunning() { diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 80aacabf..80b0392e 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -65,7 +65,7 @@ export const fullSettings: ISettings = { integrations: [() => { }], // A no-op integration mode: 'standalone', debug: false, - streamingEnabled: false, + streamingEnabled: true, sync: { splitFilters: [], impressionsMode: 'OPTIMIZED', @@ -90,6 +90,15 @@ export const fullSettings: ISettings = { log: loggerMock }; +export const fullSettingsServerSide = { + ...fullSettings, + core: { + ...fullSettings.core, + key: undefined, + }, + features: '.split' +}; + export const settingsSplitApi = { core: { authorizationKey: 'api-key' From fbf619226ab0a719e3442e51ab0d592960fa4b96 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 10 Feb 2022 18:31:28 -0300 Subject: [PATCH 06/53] CI update to use node v14, which doesn't close on unhandled promise rejections on tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fac0178..277879c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Set up nodejs uses: actions/setup-node@v2 with: - node-version: 'lts/*' + node-version: '14' cache: 'npm' - name: npm CI From bd923146e1e6da8a918f380c979f38a75ced4b1f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 10 Feb 2022 20:24:40 -0300 Subject: [PATCH 07/53] polishing --- src/sync/__tests__/syncTask.spec.ts | 2 +- src/sync/streaming/__tests__/pushManager.spec.ts | 11 +++++++---- src/sync/streaming/pushManager.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/sync/__tests__/syncTask.spec.ts b/src/sync/__tests__/syncTask.spec.ts index d4a7502b..49ecf67f 100644 --- a/src/sync/__tests__/syncTask.spec.ts +++ b/src/sync/__tests__/syncTask.spec.ts @@ -64,7 +64,7 @@ test('syncTaskFactory', (done) => { syncTask.stop(); expect(asyncTask).toBeCalledTimes(7); - // // Resume periodic execution + // Resume periodic execution syncTask.start(); // Inmediatelly call task syncTask.start(); // No effect expect(asyncTask).toBeCalledTimes(8); diff --git a/src/sync/streaming/__tests__/pushManager.spec.ts b/src/sync/streaming/__tests__/pushManager.spec.ts index 99b75473..824caa63 100644 --- a/src/sync/streaming/__tests__/pushManager.spec.ts +++ b/src/sync/streaming/__tests__/pushManager.spec.ts @@ -41,8 +41,6 @@ describe('pushManager in client-side', () => { }); test('re-authenticates asynchronously when it is resumed and when new users are added', async () => { - - // @TODO assert log messages const fetchAuthMock = jest.fn(() => Promise.reject({ message: 'error' })); // @ts-ignore @@ -54,7 +52,9 @@ describe('pushManager in client-side', () => { } }) as IPushManagerCS; - // calling start multiple times has no effect (authenticates only once) + // calling start again has no effect (authenticates asynchronously only once) + pushManager.start(); + pushManager.stop(); pushManager.start(); pushManager.start(); @@ -135,9 +135,12 @@ describe('pushManager in server-side', () => { } }) as IPushManagerCS; - // calling start multiple times has no effect (authenticates asynchronously only once) + // calling start again has no effect (authenticates asynchronously only once) pushManager.start(); pushManager.start(); + // @TODO pausing & resuming synchronously is not working as expected in server-side + // pushManager.stop(); + // pushManager.start(); expect(fetchAuthMock).toHaveBeenCalledTimes(0); await new Promise(res => setTimeout(res)); expect(fetchAuthMock).toHaveBeenLastCalledWith(undefined); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 55a67549..c004db0c 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -314,7 +314,7 @@ export function pushManagerFactory( disconnected = false; if (userKey) this.add(userKey, pollingManager.segmentsSyncTask); // client-side - else setTimeout(connectPush); // server-side runs in next cycle as in client-side, for tests consistency + else setTimeout(connectPush); // server-side runs in next cycle as in client-side, for consistency with client-side }, // [Only for client-side] From 8821ad11337fdfd971c094ea9d9d2b32b71ba0bf Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 11 Feb 2022 10:22:36 -0300 Subject: [PATCH 08/53] remove guard conditions from SyncManager start/stop. They are not required because polling and push manager operations are idempotent --- src/sync/syncManagerOnline.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 1b6ec876..8c62466d 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -81,9 +81,6 @@ export function syncManagerOnlineFactory( * Method used to start the syncManager for the first time, or resume it after being stopped. */ start() { - if (running) return; - running = true; - // start syncing splits and segments if (pollingManager) { if (pushManager) { @@ -100,21 +97,20 @@ export function syncManagerOnlineFactory( // start periodic data recording (events, impressions, telemetry). if (submitter) submitter.start(); + running = true; }, /** * Method used to stop/pause the syncManager. */ stop() { - if (!running) return; - running = false; - // stop syncing if (pushManager) pushManager.stop(); if (pollingManager && pollingManager.isRunning()) pollingManager.stop(); // stop periodic data recording (events, impressions, telemetry). if (submitter) submitter.stop(); + running = false; }, isRunning() { From fedb5aafb478120412abc03c01c5fafde6f84448 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Sun, 13 Feb 2022 13:29:02 -0300 Subject: [PATCH 09/53] Removed some types and interfaces, to clean up code --- src/evaluator/matchers/ew.ts | 8 +-- .../polling/fetchers/mySegmentsFetcher.ts | 3 +- src/sync/polling/fetchers/types.ts | 1 + src/sync/polling/pollingManagerCS.ts | 9 +-- src/sync/polling/pollingManagerSS.ts | 11 +-- .../polling/syncTasks/mySegmentsSyncTask.ts | 3 +- src/sync/polling/types.ts | 12 ---- .../polling/updaters/mySegmentsUpdater.ts | 3 +- .../UpdateWorkers/SegmentsUpdateWorker.ts | 2 +- .../__tests__/SegmentsUpdateWorker.spec.ts | 2 +- .../streaming/__tests__/pushManager.spec.ts | 72 +++++++++++-------- src/sync/streaming/pushManager.ts | 22 +++--- src/sync/streaming/types.ts | 30 ++------ src/sync/submitters/submitterManager.ts | 12 ++-- src/sync/syncManagerOnline.ts | 30 ++++---- src/utils/inputValidation/eventProperties.ts | 6 +- src/utils/lang/__tests__/index.spec.ts | 7 +- src/utils/lang/index.ts | 14 ---- 18 files changed, 98 insertions(+), 149 deletions(-) diff --git a/src/evaluator/matchers/ew.ts b/src/evaluator/matchers/ew.ts index 851e0e2c..6afdc9e1 100644 --- a/src/evaluator/matchers/ew.ts +++ b/src/evaluator/matchers/ew.ts @@ -1,13 +1,13 @@ import { ENGINE_MATCHER_ENDS_WITH } from '../../logger/constants'; import { ILogger } from '../../logger/types'; -import { endsWith as strEndsWith } from '../../utils/lang'; +import { endsWith } from '../../utils/lang'; export function endsWithMatcherContext(log: ILogger, ruleAttr: string[]) /*: Function */ { return function endsWithMatcher(runtimeAttr: string): boolean { - let endsWith = ruleAttr.some(e => strEndsWith(runtimeAttr, e)); + let strEndsWith = ruleAttr.some(e => endsWith(runtimeAttr, e)); - log.debug(ENGINE_MATCHER_ENDS_WITH, [runtimeAttr, ruleAttr, endsWith]); + log.debug(ENGINE_MATCHER_ENDS_WITH, [runtimeAttr, ruleAttr, strEndsWith]); - return endsWith; + return strEndsWith; }; } diff --git a/src/sync/polling/fetchers/mySegmentsFetcher.ts b/src/sync/polling/fetchers/mySegmentsFetcher.ts index 8ea7a5f4..498132b0 100644 --- a/src/sync/polling/fetchers/mySegmentsFetcher.ts +++ b/src/sync/polling/fetchers/mySegmentsFetcher.ts @@ -6,9 +6,10 @@ import { IMySegmentsFetcher } from './types'; * Factory of MySegments fetcher. * MySegments fetcher is a wrapper around `mySegments` API service that parses the response and handle errors. */ -export function mySegmentsFetcherFactory(fetchMySegments: IFetchMySegments, userMatchingKey: string): IMySegmentsFetcher { +export function mySegmentsFetcherFactory(fetchMySegments: IFetchMySegments): IMySegmentsFetcher { return function mySegmentsFetcher( + userMatchingKey: string, noCache?: boolean, // Optional decorator for `fetchMySegments` promise, such as timeout or time tracker decorator?: (promise: Promise) => Promise diff --git a/src/sync/polling/fetchers/types.ts b/src/sync/polling/fetchers/types.ts index 4d1272f0..e15331fb 100644 --- a/src/sync/polling/fetchers/types.ts +++ b/src/sync/polling/fetchers/types.ts @@ -15,6 +15,7 @@ export type ISegmentChangesFetcher = ( ) => Promise export type IMySegmentsFetcher = ( + userMatchingKey: string, noCache?: boolean, decorator?: (promise: Promise) => Promise ) => Promise diff --git a/src/sync/polling/pollingManagerCS.ts b/src/sync/polling/pollingManagerCS.ts index 138ed003..364829a5 100644 --- a/src/sync/polling/pollingManagerCS.ts +++ b/src/sync/polling/pollingManagerCS.ts @@ -1,26 +1,23 @@ import { ISegmentsSyncTask, ISplitsSyncTask, IPollingManagerCS } from './types'; import { forOwn } from '../../utils/lang'; import { IReadinessManager } from '../../readiness/types'; -import { ISplitApi } from '../../services/types'; import { IStorageSync } from '../../storages/types'; import { mySegmentsSyncTaskFactory } from './syncTasks/mySegmentsSyncTask'; import { splitsSyncTaskFactory } from './syncTasks/splitsSyncTask'; -import { ISettings } from '../../types'; import { getMatching } from '../../utils/key'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants'; +import { ISyncManagerFactoryParams } from '../types'; /** * Expose start / stop mechanism for polling data from services. * For client-side API with multiple clients. */ export function pollingManagerCSFactory( - splitApi: ISplitApi, - storage: IStorageSync, - readiness: IReadinessManager, - settings: ISettings, + params: ISyncManagerFactoryParams ): IPollingManagerCS { + const { splitApi, storage, readiness, settings } = params; const log = settings.log; const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(splitApi.fetchSplitChanges, storage, readiness, settings); diff --git a/src/sync/polling/pollingManagerSS.ts b/src/sync/polling/pollingManagerSS.ts index c1e7edad..2c7d3ec9 100644 --- a/src/sync/polling/pollingManagerSS.ts +++ b/src/sync/polling/pollingManagerSS.ts @@ -1,23 +1,18 @@ import { splitsSyncTaskFactory } from './syncTasks/splitsSyncTask'; import { segmentsSyncTaskFactory } from './syncTasks/segmentsSyncTask'; -import { IStorageSync } from '../../storages/types'; -import { IReadinessManager } from '../../readiness/types'; -import { ISplitApi } from '../../services/types'; -import { ISettings } from '../../types'; import { IPollingManager, ISegmentsSyncTask, ISplitsSyncTask } from './types'; import { thenable } from '../../utils/promise/thenable'; import { POLLING_START, POLLING_STOP, LOG_PREFIX_SYNC_POLLING } from '../../logger/constants'; +import { ISyncManagerFactoryParams } from '../types'; /** * Expose start / stop mechanism for pulling data from services. */ export function pollingManagerSSFactory( - splitApi: ISplitApi, - storage: IStorageSync, - readiness: IReadinessManager, - settings: ISettings + params: ISyncManagerFactoryParams ): IPollingManager { + const { splitApi, storage, readiness, settings } = params; const log = settings.log; const splitsSyncTask: ISplitsSyncTask = splitsSyncTaskFactory(splitApi.fetchSplitChanges, storage, readiness, settings); diff --git a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts index 7b830d21..30f521b0 100644 --- a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts @@ -21,12 +21,13 @@ export function mySegmentsSyncTaskFactory( settings.log, mySegmentsUpdaterFactory( settings.log, - mySegmentsFetcherFactory(fetchMySegments, matchingKey), + mySegmentsFetcherFactory(fetchMySegments), storage.splits, storage.segments, readiness.segments, settings.startup.requestTimeoutBeforeReady, settings.startup.retriesOnFailureBeforeReady, + matchingKey ), settings.scheduler.segmentsRefreshRate, 'mySegmentsUpdater', diff --git a/src/sync/polling/types.ts b/src/sync/polling/types.ts index f3cf3639..3908152a 100644 --- a/src/sync/polling/types.ts +++ b/src/sync/polling/types.ts @@ -1,7 +1,5 @@ import { IReadinessManager } from '../../readiness/types'; -import { ISplitApi } from '../../services/types'; import { IStorageSync } from '../../storages/types'; -import { ISettings } from '../../types'; import { SegmentsData } from '../streaming/SSEHandler/types'; import { ITask, ISyncTask } from '../types'; @@ -23,13 +21,3 @@ export interface IPollingManagerCS extends IPollingManager { remove(matchingKey: string): void; get(matchingKey: string): ISegmentsSyncTask | undefined } - -/** - * Signature of polling manager factory/constructor - */ -export type IPollingManagerFactoryParams = [ - splitApi: ISplitApi, - storage: IStorageSync, - readiness: IReadinessManager, - settings: ISettings, -] diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index a72ee54b..486cbe00 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -23,6 +23,7 @@ export function mySegmentsUpdaterFactory( segmentsEventEmitter: ISegmentsEventEmitter, requestTimeoutBeforeReady: number, retriesOnFailureBeforeReady: number, + matchingKey: string ): IMySegmentsUpdater { let readyOnAlreadyExistentState = true; @@ -69,7 +70,7 @@ export function mySegmentsUpdaterFactory( // If segmentsData is provided, there is no need to fetch mySegments new Promise((res) => { updateSegments(segmentsData); res(true); }) : // If not provided, fetch mySegments - mySegmentsFetcher(noCache, _promiseDecorator).then(segments => { + mySegmentsFetcher(matchingKey, noCache, _promiseDecorator).then(segments => { // Only when we have downloaded segments completely, we should not keep retrying anymore startingUp = false; diff --git a/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts index 5c52872d..03889a25 100644 --- a/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts @@ -19,7 +19,7 @@ export class SegmentsUpdateWorker implements IUpdateWorker { * @param {Object} segmentsCache segments data cache * @param {Object} segmentsSyncTask task for syncing segments data */ - constructor(segmentsCache: ISegmentsCacheSync, segmentsSyncTask: ISegmentsSyncTask) { + constructor(segmentsSyncTask: ISegmentsSyncTask, segmentsCache: ISegmentsCacheSync) { this.segmentsCache = segmentsCache; this.segmentsSyncTask = segmentsSyncTask; this.maxChangeNumbers = {}; diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SegmentsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SegmentsUpdateWorker.spec.ts index 4a494657..e3168593 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SegmentsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SegmentsUpdateWorker.spec.ts @@ -47,7 +47,7 @@ describe('SegmentsUpdateWorker ', () => { cache.addToSegment('mocked_segment_3', ['e']); const segmentsSyncTask = segmentsSyncTaskMock(cache); - const segmentsUpdateWorker = new SegmentsUpdateWorker(cache, segmentsSyncTask); + const segmentsUpdateWorker = new SegmentsUpdateWorker(segmentsSyncTask, cache); segmentsUpdateWorker.backoff.baseMillis = 0; // retry immediately expect(segmentsUpdateWorker.maxChangeNumbers).toEqual({}); // inits with not queued events; diff --git a/src/sync/streaming/__tests__/pushManager.spec.ts b/src/sync/streaming/__tests__/pushManager.spec.ts index 824caa63..72d8c225 100644 --- a/src/sync/streaming/__tests__/pushManager.spec.ts +++ b/src/sync/streaming/__tests__/pushManager.spec.ts @@ -1,24 +1,34 @@ import { EventEmitter } from '../../../utils/MinEvents'; import { fullSettings, fullSettingsServerSide } from '../../../utils/settingsValidation/__tests__/settings.mocks'; -import { IPushManagerCS } from '../types'; import { syncTaskFactory } from '../../__tests__/syncTask.mock'; // Test target import { pushManagerFactory } from '../pushManager'; +import { IPushManager } from '../types'; -const platformMock = { - getEventSource: jest.fn(() => { return () => { }; }), - EventEmitter +const paramsMock = { + platform: { + getEventSource: jest.fn(() => { return () => { }; }), + EventEmitter + }, + settings: fullSettings, + storage: {}, + readiness: {} }; test('pushManagerFactory returns undefined if EventSource is not available', () => { - // @ts-ignore - const pushManager = pushManagerFactory({}, {}, {}, () => { }, { - getEventSource: () => undefined, - EventEmitter - }, fullSettings); - - expect(pushManager).toBe(undefined); + const params = { + ...paramsMock, + platform: { + getEventSource: () => undefined, + EventEmitter + } + }; // @ts-ignore + const pushManagerCS = pushManagerFactory(params, {}); // @ts-ignore + const pushManagerSS = pushManagerFactory(params, {}); + + expect(pushManagerCS).toBe(undefined); + expect(pushManagerSS).toBe(undefined); }); describe('pushManager in client-side', () => { @@ -26,8 +36,9 @@ describe('pushManager in client-side', () => { test('does not connect to streaming if it is stopped inmediatelly after being started', async () => { const fetchAuthMock = jest.fn(); - // @ts-ignore - const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, fullSettings) as IPushManagerCS; + const pushManager = pushManagerFactory({ // @ts-ignore + ...paramsMock, splitApi: { fetchAuth: fetchAuthMock } + }, {}) as IPushManager; pushManager.start(); pushManager.start(); @@ -43,14 +54,15 @@ describe('pushManager in client-side', () => { test('re-authenticates asynchronously when it is resumed and when new users are added', async () => { const fetchAuthMock = jest.fn(() => Promise.reject({ message: 'error' })); - // @ts-ignore - const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, { - ...fullSettings, - scheduler: { - ...fullSettings.scheduler, - pushRetryBackoffBase: 10 // high authentication backoff + const pushManager = pushManagerFactory({ // @ts-ignore + ...paramsMock, splitApi: { fetchAuth: fetchAuthMock }, settings: { + ...fullSettings, + scheduler: { + ...fullSettings.scheduler, + pushRetryBackoffBase: 10 // high authentication backoff + } } - }) as IPushManagerCS; + }, {}) as IPushManager; // calling start again has no effect (authenticates asynchronously only once) pushManager.start(); @@ -109,8 +121,9 @@ describe('pushManager in server-side', () => { test('does not connect to streaming if it is stopped inmediatelly after being started', async () => { const fetchAuthMock = jest.fn(); - // @ts-ignore - const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, fullSettingsServerSide) as IPushManagerCS; + const pushManager = pushManagerFactory({ // @ts-ignore + ...paramsMock, splitApi: { fetchAuth: fetchAuthMock }, settings: fullSettingsServerSide + }, {}) as IPushManager; pushManager.start(); pushManager.start(); @@ -126,14 +139,15 @@ describe('pushManager in server-side', () => { test('re-authenticates asynchronously when it is resumed', async () => { const fetchAuthMock = jest.fn(() => Promise.reject({ message: 'error' })); - // @ts-ignore - const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, { - ...fullSettingsServerSide, - scheduler: { - ...fullSettingsServerSide.scheduler, - pushRetryBackoffBase: 10 // high authentication backoff + const pushManager = pushManagerFactory({ // @ts-ignore + ...paramsMock, splitApi: { fetchAuth: fetchAuthMock }, settings: { + ...fullSettingsServerSide, + scheduler: { + ...fullSettingsServerSide.scheduler, + pushRetryBackoffBase: 10 // high authentication backoff + } } - }) as IPushManagerCS; + }, {}) as IPushManager; // calling start again has no effect (authenticates asynchronously only once) pushManager.start(); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index c004db0c..fc3a6ad2 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -1,7 +1,5 @@ -import { IPushEventEmitter, IPushManagerCS } from './types'; +import { IPushEventEmitter, IPushManager } from './types'; import { ISSEClient } from './SSEClient/types'; -import { IStorageSync } from '../../storages/types'; -import { IReadinessManager } from '../../readiness/types'; import { ISegmentsSyncTask, IPollingManager } from '../polling/types'; import { objectAssign } from '../../utils/lang/objectAssign'; import { Backoff } from '../../utils/Backoff'; @@ -12,17 +10,15 @@ import { SplitsUpdateWorker } from './UpdateWorkers/SplitsUpdateWorker'; import { authenticateFactory, hashUserKey } from './AuthClient'; import { forOwn } from '../../utils/lang'; import { SSEClient } from './SSEClient'; -import { IFetchAuth } from '../../services/types'; -import { ISettings } from '../../types'; import { getMatching } from '../../utils/key'; import { MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; -import { IPlatform } from '../../sdkFactory/types'; import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 } from '../../logger/constants'; import { KeyList, UpdateStrategy } from './SSEHandler/types'; import { isInBitmap, parseBitmap, parseKeyList } from './mySegmentsV2utils'; import { ISet, _Set } from '../../utils/lang/sets'; import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64'; import { IAuthTokenPushEnabled } from './AuthClient/types'; +import { ISyncManagerFactoryParams } from '../types'; /** * PushManager factory: @@ -30,13 +26,11 @@ import { IAuthTokenPushEnabled } from './AuthClient/types'; * - for client-side, with support for multiple clients, if key is provided in settings */ export function pushManagerFactory( + params: ISyncManagerFactoryParams, pollingManager: IPollingManager, - storage: IStorageSync, - readiness: IReadinessManager, - fetchAuth: IFetchAuth, - platform: IPlatform, - settings: ISettings, -): IPushManagerCS | undefined { +): IPushManager | undefined { + + const { settings, storage, splitApi, readiness, platform } = params; // `userKey` is the matching key of main client in client-side SDK. // It can be used to check if running on client-side or server-side SDK. @@ -51,7 +45,7 @@ export function pushManagerFactory( log.warn(STREAMING_FALLBACK, [e]); return; } - const authenticate = authenticateFactory(fetchAuth); + const authenticate = authenticateFactory(splitApi.fetchAuth); // init feedback loop const pushEmitter = new platform.EventEmitter() as IPushEventEmitter; @@ -60,7 +54,7 @@ export function pushManagerFactory( // init workers // MySegmentsUpdateWorker (client-side) are initiated in `add` method - const segmentsUpdateWorker = userKey ? undefined : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); + const segmentsUpdateWorker = userKey ? undefined : new SegmentsUpdateWorker(pollingManager.segmentsSyncTask, storage.segments); // For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index 6832a335..57a4a96a 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -1,11 +1,7 @@ import { IMySegmentsUpdateData, IMySegmentsUpdateV2Data, ISegmentUpdateData, ISplitUpdateData, ISplitKillData } from './SSEHandler/types'; import { ITask } from '../types'; -import { IPollingManager, ISegmentsSyncTask } from '../polling/types'; -import { IReadinessManager } from '../../readiness/types'; -import { IFetchAuth } from '../../services/types'; -import { IStorageSync } from '../../storages/types'; -import { IEventEmitter, ISettings } from '../../types'; -import { IPlatform } from '../../sdkFactory/types'; +import { ISegmentsSyncTask } from '../polling/types'; +import { IEventEmitter } from '../../types'; import { ControlType } from './constants'; // Internal SDK events, subscribed by SyncManager and PushManager @@ -45,26 +41,10 @@ export interface IPushEventEmitter extends IEventEmitter { } /** - * PushManager for server-side + * PushManager */ -export interface IPushManager extends ITask, IPushEventEmitter { } - -/** - * PushManager for client-side with support for multiple clients - */ -export interface IPushManagerCS extends IPushManager { +export interface IPushManager extends ITask, IPushEventEmitter { + // Methods used in client-side, to support multiple clients add(userKey: string, mySegmentsSyncTask: ISegmentsSyncTask): void, remove(userKey: string): void } - -/** - * Signature of push manager factory/constructor - */ -export type IPushManagerFactoryParams = [ - pollingManager: IPollingManager, - storage: IStorageSync, - readiness: IReadinessManager, - fetchAuth: IFetchAuth, - platform: IPlatform, - settings: ISettings -] diff --git a/src/sync/submitters/submitterManager.ts b/src/sync/submitters/submitterManager.ts index 7828d947..51440a25 100644 --- a/src/sync/submitters/submitterManager.ts +++ b/src/sync/submitters/submitterManager.ts @@ -2,15 +2,11 @@ import { syncTaskComposite } from '../syncTaskComposite'; import { eventsSyncTaskFactory } from './eventsSyncTask'; import { impressionsSyncTaskFactory } from './impressionsSyncTask'; import { impressionCountsSyncTaskFactory } from './impressionCountsSyncTask'; -import { ISplitApi } from '../../services/types'; -import { IStorageSync } from '../../storages/types'; -import { ISettings } from '../../types'; +import { ISyncManagerFactoryParams } from '../types'; -export function submitterManagerFactory( - settings: ISettings, - storage: IStorageSync, - splitApi: ISplitApi, -) { +export function submitterManagerFactory(params: ISyncManagerFactoryParams) { + + const { settings, storage, splitApi } = params; const log = settings.log; const submitters = [ impressionsSyncTaskFactory(log, splitApi.postTestImpressionsBulk, storage.impressions, settings.scheduler.impressionsRefreshRate, settings.core.labelsEnabled), diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 8c62466d..1bbfe948 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -2,8 +2,8 @@ import { ISyncManagerCS, ISyncManagerFactoryParams } from './types'; import { submitterManagerFactory } from './submitters/submitterManager'; 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 { IPushManager } from './streaming/types'; +import { IPollingManager, IPollingManagerCS } from './polling/types'; import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants'; import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants'; @@ -16,34 +16,28 @@ import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '.. * @param pushManagerFactory optional to build a SyncManager with or without streaming support */ export function syncManagerOnlineFactory( - pollingManagerFactory?: (...args: IPollingManagerFactoryParams) => IPollingManager, - pushManagerFactory?: (...args: IPushManagerFactoryParams) => IPushManager | undefined + pollingManagerFactory?: (params: ISyncManagerFactoryParams) => IPollingManager, + pushManagerFactory?: (params: ISyncManagerFactoryParams, pollingManager: IPollingManager) => IPushManager | undefined, ): (params: ISyncManagerFactoryParams) => ISyncManagerCS { /** * SyncManager factory for modular SDK */ - return function ({ - settings, - platform, - splitApi, - storage, - readiness - }: ISyncManagerFactoryParams): ISyncManagerCS { + return function (params: ISyncManagerFactoryParams): ISyncManagerCS { - const log = settings.log; + const { log, streamingEnabled } = params.settings; /** Polling Manager */ - const pollingManager = pollingManagerFactory && pollingManagerFactory(splitApi, storage, readiness, settings); + const pollingManager = pollingManagerFactory && pollingManagerFactory(params); /** Push Manager */ - const pushManager = settings.streamingEnabled && pollingManager && pushManagerFactory ? - pushManagerFactory(pollingManager, storage, readiness, splitApi.fetchAuth, platform, settings) : + const pushManager = streamingEnabled && pollingManager && pushManagerFactory ? + pushManagerFactory(params, pollingManager) : undefined; /** Submitter Manager */ // It is not inyected as push and polling managers, because at the moment it is required - const submitter = submitterManagerFactory(settings, storage, splitApi); + const submitter = submitterManagerFactory(params); /** Sync Manager logic */ @@ -141,7 +135,7 @@ export function syncManagerOnlineFactory( // of segments since `syncAll` was already executed when starting the main client mySegmentsSyncTask.execute(); } - (pushManager as IPushManagerCS).add(matchingKey, mySegmentsSyncTask); + pushManager.add(matchingKey, mySegmentsSyncTask); } else { if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); } @@ -151,7 +145,7 @@ export function syncManagerOnlineFactory( const mySegmentsSyncTask = (pollingManager as IPollingManagerCS).get(matchingKey); if (mySegmentsSyncTask) { // stop syncing - if (pushManager) (pushManager as IPushManagerCS).remove(matchingKey); + if (pushManager) pushManager.remove(matchingKey); if (mySegmentsSyncTask.isRunning()) mySegmentsSyncTask.stop(); (pollingManager as IPollingManagerCS).remove(matchingKey); diff --git a/src/utils/inputValidation/eventProperties.ts b/src/utils/inputValidation/eventProperties.ts index b2bf20e6..1fb2984e 100644 --- a/src/utils/inputValidation/eventProperties.ts +++ b/src/utils/inputValidation/eventProperties.ts @@ -1,4 +1,5 @@ -import { isObject, shallowClone, isString, isFiniteNumber, isBoolean } from '../lang'; +import { isObject, isString, isFiniteNumber, isBoolean } from '../lang'; +import { objectAssign } from '../lang/objectAssign'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; import { ERROR_NOT_PLAIN_OBJECT, ERROR_SIZE_EXCEEDED, WARN_SETTING_NULL, WARN_TRIMMING_PROPERTIES } from '../../logger/constants'; @@ -22,7 +23,8 @@ export function validateEventProperties(log: ILogger, maybeProperties: any, meth } const keys = Object.keys(maybeProperties); - const clone = shallowClone(maybeProperties); + // Shallow clone + const clone = objectAssign({}, maybeProperties); // To avoid calculating the size twice we'll return it from here. const output = { properties: clone, diff --git a/src/utils/lang/__tests__/index.spec.ts b/src/utils/lang/__tests__/index.spec.ts index 613ad573..e6e724a1 100644 --- a/src/utils/lang/__tests__/index.spec.ts +++ b/src/utils/lang/__tests__/index.spec.ts @@ -18,11 +18,11 @@ import { toNumber, forOwn, groupBy, - shallowClone, isBoolean } from '../index'; import { getFnName } from '../getFnName'; import { getGlobal } from '../getGlobal'; +import { objectAssign } from '../objectAssign'; test('LANG UTILS / startsWith', () => { expect(startsWith('myStr', 'myS')).toBe(true); @@ -529,7 +529,7 @@ test('LANG UTILS / getFnName', () => { }); -test('LANG UTILS / shallowClone', () => { +test('LANG UTILS / objectAssign', () => { const toClone = { aProperty: 1, another: 'two', @@ -539,7 +539,7 @@ test('LANG UTILS / shallowClone', () => { bool: true }; - const clone = shallowClone(toClone); + const clone = objectAssign({}, toClone); expect(clone).toEqual(toClone); // The structure of the shallow clone should be the same since references are copied too. expect(clone).not.toBe(toClone); // But the reference to the object itself is differente since it is a clone @@ -558,4 +558,3 @@ test('LANG UTILS / isBoolean', () => { [true, false].forEach(val => expect(isBoolean(val)).toBe(true)); }); - diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 340d3ccc..f894f652 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -194,20 +194,6 @@ export function merge(target: { [key: string]: any }, source: { [key: string]: a return res; } -/** - * Shallow clone an object - */ -export function shallowClone(obj: any): any { - const keys = Object.keys(obj); - const output: Record = {}; - - for (let i = 0; i < keys.length; i++) { - output[keys[i]] = obj[keys[i]]; - } - - return output; -} - /** * Checks if the target string starts with the sub string. */ From 67769a349866e3c35bfc5f426c6cbb8571097f28 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 14 Feb 2022 17:21:35 -0300 Subject: [PATCH 10/53] updated dependencies and added runtime validators from JS SDK to reuse in other SDKs --- package-lock.json | 92 ++++++++----------- package.json | 13 +-- src/__tests__/testUtils/fetchMock.ts | 5 +- src/logger/types.ts | 4 + .../splitsParser/splitsParserFromFile.ts | 2 +- .../inputValidation/__tests__/apiKey.spec.ts | 2 +- .../inputValidation/__tests__/key.spec.ts | 2 +- src/utils/promise/__tests__/timeout.spec.ts | 2 +- src/utils/settingsValidation/index.ts | 7 +- .../settingsValidation/runtime/browser.ts | 6 ++ src/utils/settingsValidation/runtime/node.ts | 22 +++++ src/utils/settingsValidation/types.ts | 12 +-- tsconfig.json | 2 +- 13 files changed, 93 insertions(+), 78 deletions(-) create mode 100644 src/utils/settingsValidation/runtime/browser.ts create mode 100644 src/utils/settingsValidation/runtime/node.ts diff --git a/package-lock.json b/package-lock.json index 9456d98a..6dd5545f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.0", + "version": "1.2.1-rc.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -373,6 +373,15 @@ "@babel/helper-plugin-utils": "^7.14.5" } }, + "@babel/runtime": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", + "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "@babel/template": { "version": "7.15.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", @@ -1061,6 +1070,15 @@ "@types/node": "*" } }, + "@types/ip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz", + "integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -1657,30 +1675,6 @@ "babel-preset-current-node-syntax": "^1.0.0" } }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - } - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -2793,12 +2787,13 @@ } }, "fetch-mock": { - "version": "9.10.7", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.10.7.tgz", - "integrity": "sha512-YkiMHSL8CQ0vlWYpqGvlaZjViFk0Kar9jonPjSvaWoztkeHH6DENqUzBIsffzjVKhwchPI74SZRLRpIsEyNcZQ==", + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", + "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", "dev": true, "requires": { - "babel-runtime": "^6.26.0", + "@babel/core": "^7.0.0", + "@babel/runtime": "^7.0.0", "core-js": "^3.0.0", "debug": "^4.1.1", "glob-to-regexp": "^0.4.0", @@ -2809,21 +2804,6 @@ "whatwg-url": "^6.5.0" }, "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -5341,9 +5321,9 @@ "dev": true }, "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", "dev": true }, "react-is": { @@ -5397,6 +5377,12 @@ "promise-queue": "^2.2.5" } }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, "regexpp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", @@ -5939,9 +5925,9 @@ } }, "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tsutils": { "version": "3.17.1", @@ -5991,9 +5977,9 @@ } }, "typescript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz", - "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "dev": true }, "unbox-primitive": { diff --git a/package.json b/package.json index 012f2ef0..5b25d315 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.0", + "version": "1.2.1-rc.2", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", @@ -20,8 +20,8 @@ "check:lint": "eslint src --ext .js,.ts", "check:types": "tsc --noEmit", "build": "npm run build:cjs && npm run build:esm", - "build:esm": "rimraf esm && tsc -m es2015 --outDir esm -d true --declarationDir types --importHelpers", - "build:cjs": "rimraf cjs && tsc -m CommonJS --outDir cjs --importHelpers", + "build:esm": "rimraf esm && tsc -m es2015 --outDir esm -d true --declarationDir types", + "build:cjs": "rimraf cjs && tsc -m CommonJS --outDir cjs", "test": "jest", "test:coverage": "jest --coverage", "publish:rc": "npm run check && npm run test && npm run build && npm publish --tag rc", @@ -44,11 +44,12 @@ "bugs": "https://github.com/splitio/javascript-commons/issues", "homepage": "https://github.com/splitio/javascript-commons#readme", "dependencies": { - "tslib": "^2.1.0" + "tslib": "^2.3.1" }, "devDependencies": { "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", + "@types/ip": "^1.1.0", "@types/jest": "^27.0.0", "@types/lodash": "^4.14.162", "@typescript-eslint/eslint-plugin": "^4.2.0", @@ -58,7 +59,7 @@ "eslint": "^7.32.0", "eslint-plugin-compat": "3.7.0", "eslint-plugin-import": "^2.25.3", - "fetch-mock": "^9.10.7", + "fetch-mock": "^9.11.0", "ioredis": "^4.28.0", "jest": "^27.2.3", "jest-localstorage-mock": "^2.4.3", @@ -68,7 +69,7 @@ "redis-server": "1.2.2", "rimraf": "^3.0.2", "ts-jest": "^27.0.5", - "typescript": "^4.0.2" + "typescript": "4.4.4" }, "sideEffects": false } diff --git a/src/__tests__/testUtils/fetchMock.ts b/src/__tests__/testUtils/fetchMock.ts index 42a0292e..94a614f7 100644 --- a/src/__tests__/testUtils/fetchMock.ts +++ b/src/__tests__/testUtils/fetchMock.ts @@ -1,6 +1,7 @@ -import { sandbox } from 'fetch-mock'; +// http://www.wheresrhys.co.uk/fetch-mock/#usageinstallation +import fetchMockLib from 'fetch-mock'; -const fetchMock = sandbox(); +const fetchMock = fetchMockLib.sandbox(); // config the fetch mock to chain routes (appends the new route to the list of routes) fetchMock.config.overwriteRoutes = false; diff --git a/src/logger/types.ts b/src/logger/types.ts index 05043be4..fcdfa2e0 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -9,11 +9,15 @@ export interface ILoggerOptions { export interface ILogger { setLogLevel(logLevel: LogLevel): void + debug(msg: unknown): void debug(msg: string | number, args?: any[]): void + info(msg: unknown): void info(msg: string | number, args?: any[]): void + warn(msg: unknown): void warn(msg: string | number, args?: any[]): void + error(msg: unknown): void error(msg: string | number, args?: any[]): void } diff --git a/src/sync/offline/splitsParser/splitsParserFromFile.ts b/src/sync/offline/splitsParser/splitsParserFromFile.ts index d65d6a55..0c376918 100644 --- a/src/sync/offline/splitsParser/splitsParserFromFile.ts +++ b/src/sync/offline/splitsParser/splitsParserFromFile.ts @@ -82,7 +82,7 @@ export function splitsParserFromFileFactory(): ISplitsParser { try { data = fs.readFileSync(filePath, 'utf-8'); } catch (e) { - log.error(e.message); + log.error(e && (e as Error).message); return {}; } diff --git a/src/utils/inputValidation/__tests__/apiKey.spec.ts b/src/utils/inputValidation/__tests__/apiKey.spec.ts index 76d18f1f..e9cd1c18 100644 --- a/src/utils/inputValidation/__tests__/apiKey.spec.ts +++ b/src/utils/inputValidation/__tests__/apiKey.spec.ts @@ -8,7 +8,7 @@ const invalidKeys = [ { key: null, msg: ERROR_NULL }, { key: undefined, msg: ERROR_NULL }, { key: () => { }, msg: ERROR_INVALID }, - { key: new Promise(r => r()), 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 }, diff --git a/src/utils/inputValidation/__tests__/key.spec.ts b/src/utils/inputValidation/__tests__/key.spec.ts index 74bdb62e..5535d6f8 100644 --- a/src/utils/inputValidation/__tests__/key.spec.ts +++ b/src/utils/inputValidation/__tests__/key.spec.ts @@ -9,7 +9,7 @@ const invalidKeys = [ { key: null, msg: ERROR_NULL }, { key: undefined, msg: ERROR_NULL }, { key: () => { }, msg: ERROR_INVALID }, - { key: new Promise(r => r()), 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 }, diff --git a/src/utils/promise/__tests__/timeout.spec.ts b/src/utils/promise/__tests__/timeout.spec.ts index da6e4446..5ad9a548 100644 --- a/src/utils/promise/__tests__/timeout.spec.ts +++ b/src/utils/promise/__tests__/timeout.spec.ts @@ -23,7 +23,7 @@ test('Promise utils / timeout - What happens in the event of a timeout or no tim // This should be rejected after 10ms await wrapperProm; expect('Should not execute').toBeFalsy(); - } catch (error) { + } catch (error: any) { // The promise was rejected not resolved. Give it an error margin of 10ms since it's not predictable expect((Date.now() - ts) < baseTimeoutInMs + 20).toBe(true); // The timeout should have rejected the promise. expect(error.message).toMatch(/^Operation timed out because it exceeded the configured time limit of/); // The timeout should have rejected the promise with a Split Timeout Error. diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index e10965a0..3d535d57 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -78,11 +78,6 @@ const base = { localhostMode: undefined }, - runtime: { - ip: false, - hostname: false - }, - // Logger log: undefined }; @@ -139,7 +134,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV // Current ip/hostname information // @ts-ignore, modify readonly prop - if (runtime) withDefaults.runtime = runtime(withDefaults); + withDefaults.runtime = runtime(withDefaults); // ensure a valid list of integrations. // `integrations` returns an array of valid integration items. diff --git a/src/utils/settingsValidation/runtime/browser.ts b/src/utils/settingsValidation/runtime/browser.ts new file mode 100644 index 00000000..560db87c --- /dev/null +++ b/src/utils/settingsValidation/runtime/browser.ts @@ -0,0 +1,6 @@ +export function validateRuntime() { + return { + ip: false, + hostname: false + }; +} diff --git a/src/utils/settingsValidation/runtime/node.ts b/src/utils/settingsValidation/runtime/node.ts new file mode 100644 index 00000000..585ed9b4 --- /dev/null +++ b/src/utils/settingsValidation/runtime/node.ts @@ -0,0 +1,22 @@ +import osFunction from 'os'; +import ipFunction from 'ip'; + +import { UNKNOWN, NA, CONSUMER_MODE } from '../../constants'; +import { ISettings } from '../../../types'; + +export function validateRuntime(settings: ISettings) { + const isIPAddressesEnabled = settings.core.IPAddressesEnabled === true; + const isConsumerMode = settings.mode === CONSUMER_MODE; + + // If the values are not available, default to false (for standalone) or "unknown" (for consumer mode, to be used on Redis keys) + let ip = ipFunction.address() || (isConsumerMode ? UNKNOWN : false); + let hostname = osFunction.hostname() || (isConsumerMode ? UNKNOWN : false); + + if (!isIPAddressesEnabled) { // If IPAddresses setting is not enabled, set as false (for standalone) or "NA" (for consumer mode, to be used on Redis keys) + ip = hostname = isConsumerMode ? NA : false; + } + + return { + ip, hostname + }; +} diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index db801b12..b305d440 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -6,15 +6,15 @@ import { ISettings } from '../../types'; */ export interface ISettingsValidationParams { /** - * Object of values to overwrite default settings. - * Version and startup properties are mandatory, because these values are not part of the base setting. + * Object of values to overwrite base settings. + * Version and startup properties are required, because they are not defined in the base settings. */ defaults: Partial & { version: string } & { startup: ISettings['startup'] }, - /** Function to overwrite runtime values (ip and hostname) which are false by default */ - runtime?: (settings: ISettings) => ISettings['runtime'], - /** Storage validator */ + /** Function to define runtime values (`settings.runtime`) */ + runtime: (settings: ISettings) => ISettings['runtime'], + /** Storage validator (`settings.storage`) */ storage?: (settings: ISettings) => ISettings['storage'], - /** Integrations validator */ + /** Integrations validator (`settings.integrations`) */ integrations?: (settings: ISettings) => ISettings['integrations'], /** Logger validator (`settings.debug`) */ logger: (settings: ISettings) => ISettings['log'], diff --git a/tsconfig.json b/tsconfig.json index 1d719785..20433c1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,7 @@ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "importHelpers": true, /* Import emit helpers from 'tslib', to avoid duplicated helpers in builds. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ From e3c9ebd1f9c085dab272b224983a90c3accd7559 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 14 Feb 2022 21:03:54 -0300 Subject: [PATCH 11/53] update logger types --- src/logger/types.ts | 8 ++++---- src/utils/settingsValidation/runtime/browser.ts | 4 +++- src/utils/settingsValidation/runtime/node.ts | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/logger/types.ts b/src/logger/types.ts index fcdfa2e0..79ec1b07 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -9,15 +9,15 @@ export interface ILoggerOptions { export interface ILogger { setLogLevel(logLevel: LogLevel): void - debug(msg: unknown): void + debug(msg: any): void debug(msg: string | number, args?: any[]): void - info(msg: unknown): void + info(msg: any): void info(msg: string | number, args?: any[]): void - warn(msg: unknown): void + warn(msg: any): void warn(msg: string | number, args?: any[]): void - error(msg: unknown): void + error(msg: any): void error(msg: string | number, args?: any[]): void } diff --git a/src/utils/settingsValidation/runtime/browser.ts b/src/utils/settingsValidation/runtime/browser.ts index 560db87c..8da6ac6a 100644 --- a/src/utils/settingsValidation/runtime/browser.ts +++ b/src/utils/settingsValidation/runtime/browser.ts @@ -1,4 +1,6 @@ -export function validateRuntime() { +import { ISettings } from '../../../types'; + +export function validateRuntime(): ISettings['runtime'] { return { ip: false, hostname: false diff --git a/src/utils/settingsValidation/runtime/node.ts b/src/utils/settingsValidation/runtime/node.ts index 585ed9b4..fa97772d 100644 --- a/src/utils/settingsValidation/runtime/node.ts +++ b/src/utils/settingsValidation/runtime/node.ts @@ -4,7 +4,7 @@ import ipFunction from 'ip'; import { UNKNOWN, NA, CONSUMER_MODE } from '../../constants'; import { ISettings } from '../../../types'; -export function validateRuntime(settings: ISettings) { +export function validateRuntime(settings: ISettings): ISettings['runtime'] { const isIPAddressesEnabled = settings.core.IPAddressesEnabled === true; const isConsumerMode = settings.mode === CONSUMER_MODE; From c9eaafaf55e3fa169d07d60e81dd0a005f7bdefd Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 16 Feb 2022 14:24:51 -0300 Subject: [PATCH 12/53] polish on some log messages and added value to integrations --- src/integrations/ga/GoogleAnalyticsToSplit.ts | 11 +++++---- src/integrations/ga/SplitToGoogleAnalytics.ts | 11 +++++---- src/integrations/types.ts | 5 ++++ src/logger/messages/error.ts | 2 +- src/logger/messages/info.ts | 2 +- .../clientAttributesDecoration.spec.ts | 23 +++++++++++++++---- src/storages/types.ts | 2 +- src/sync/streaming/pushManager.ts | 8 +++++-- 8 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/integrations/ga/GoogleAnalyticsToSplit.ts b/src/integrations/ga/GoogleAnalyticsToSplit.ts index 940447aa..95923713 100644 --- a/src/integrations/ga/GoogleAnalyticsToSplit.ts +++ b/src/integrations/ga/GoogleAnalyticsToSplit.ts @@ -1,11 +1,14 @@ -import { IIntegrationFactoryParams } from '../types'; +import { IIntegrationFactoryParams, IntegrationFactory } from '../types'; import { GaToSplit } from './GaToSplit'; import { GoogleAnalyticsToSplitOptions } from './types'; -export function GoogleAnalyticsToSplit(options: GoogleAnalyticsToSplitOptions) { +export function GoogleAnalyticsToSplit(options: GoogleAnalyticsToSplitOptions): IntegrationFactory { // GaToSplit integration factory - return (params: IIntegrationFactoryParams) => { + function GoogleAnalyticsToSplitFactory(params: IIntegrationFactoryParams) { return GaToSplit(options, params); - }; + } + + GoogleAnalyticsToSplitFactory.type = 'GOOGLE_ANALYTICS_TO_SPLIT'; + return GoogleAnalyticsToSplitFactory; } diff --git a/src/integrations/ga/SplitToGoogleAnalytics.ts b/src/integrations/ga/SplitToGoogleAnalytics.ts index 4ec71177..101df26f 100644 --- a/src/integrations/ga/SplitToGoogleAnalytics.ts +++ b/src/integrations/ga/SplitToGoogleAnalytics.ts @@ -1,11 +1,14 @@ -import { IIntegrationFactoryParams } from '../types'; +import { IIntegrationFactoryParams, IntegrationFactory } from '../types'; import { SplitToGa } from './SplitToGa'; import { SplitToGoogleAnalyticsOptions } from './types'; -export function SplitToGoogleAnalytics(options: SplitToGoogleAnalyticsOptions = {}) { +export function SplitToGoogleAnalytics(options: SplitToGoogleAnalyticsOptions = {}): IntegrationFactory { // SplitToGa integration factory - return (params: IIntegrationFactoryParams) => { + function SplitToGoogleAnalyticsFactory(params: IIntegrationFactoryParams) { return new SplitToGa(params.settings.log, options); - }; + } + + SplitToGoogleAnalyticsFactory.type = 'SPLIT_TO_GOOGLE_ANALYTICS'; + return SplitToGoogleAnalyticsFactory; } diff --git a/src/integrations/types.ts b/src/integrations/types.ts index 02e868f1..0c050879 100644 --- a/src/integrations/types.ts +++ b/src/integrations/types.ts @@ -12,3 +12,8 @@ export interface IIntegrationFactoryParams { storage: { events: IEventsCacheBase } settings: ISettings } + +export type IntegrationFactory = { + readonly type: string + (params: IIntegrationFactoryParams): IIntegration | void +} diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index fee6c423..1d0fe00e 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -13,7 +13,7 @@ export const codesError: [number, string][] = [ [c.ERROR_SYNC_OFFLINE_LOADING, c.LOG_PREFIX_SYNC_OFFLINE + 'There was an issue loading the mock Splits data, no changes will be applied to the current cache. %s'], [c.ERROR_STREAMING_SSE, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to connect or error on streaming connection, with error message: %s'], [c.ERROR_STREAMING_AUTH, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to authenticate for streaming. Error: %s.'], - [c.ERROR_HTTP, ' Response status is not OK. Status: %s. URL: %s. Message: %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.'], diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 298711bd..bf30a97c 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -29,5 +29,5 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.STREAMING_DISCONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Disconnecting streaming.'], [c.SYNC_START_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming not available. Starting polling.'], [c.SYNC_CONTINUE_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming couldn\'t connect. Continue polling.'], - [c.SYNC_STOP_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming (re)connected. Syncing and stopping polling.'], + [c.SYNC_STOP_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming connected. Syncing and stopping polling.'], ]); diff --git a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts index bb6c0df7..f5c49f96 100644 --- a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts +++ b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts @@ -32,10 +32,12 @@ test('ATTRIBUTES DECORATION / storage', () => { expect(client.getAttribute('attributeName1')).toEqual(undefined); // It should throw undefined expect(client.getAttribute('attributeName2')).toEqual('newAttributeValue2'); // It should be equal - client.setAttributes({ + expect(client.setAttributes({ 'attributeName3': 'attributeValue3', 'attributeName4': 'attributeValue4' - }); + })).toEqual(true); // @ts-ignore + expect(client.setAttributes(undefined)).toEqual(false); // @ts-ignore + expect(client.setAttributes(null)).toEqual(false); expect(client.getAttributes()).toEqual({ attributeName2: 'newAttributeValue2', attributeName3: 'attributeValue3', attributeName4: 'attributeValue4' }); // It should be equal @@ -48,7 +50,11 @@ test('ATTRIBUTES DECORATION / storage', () => { describe('ATTRIBUTES DECORATION / validation', () => { - test('Should return true if it is a valid attributes map without logging any errors', () => { + beforeEach(() => { + loggerMock.mockClear(); + }); + + test('Should return true if it is a valid attributes map without logging any errors and warnings', () => { const validAttributes = { amIvalid: 'yes', 'are_you_sure': true, howMuch: 10, 'spell': ['1', '0'] }; expect(client.setAttributes(validAttributes)).toEqual(true); // It should return true if it is valid. @@ -63,6 +69,8 @@ describe('ATTRIBUTES DECORATION / validation', () => { expect(Object.keys(client.getAttributes()).length).toEqual(0); // It should be zero after clearing attributes + expect(loggerMock.error).not.toBeCalled(); // no error logs + expect(loggerMock.warn).not.toBeCalled(); // no warning logs }); test('Should return false if it is an invalid attributes map', () => { @@ -82,12 +90,17 @@ describe('ATTRIBUTES DECORATION / validation', () => { '': 'attributeValue' }; - expect(client.setAttributes(attributes)).toEqual(false); // It should be invalid if the attribute key is not a string + expect(client.setAttributes(attributes)).toEqual(false); // @ts-ignore // It should be invalid if the attribute key is not a string + expect(client.setAttributes(undefined)).toEqual(false); // @ts-ignore // It should be invalid if the attributes param is nullish. Doesn't log an error + expect(client.setAttributes(null)).toEqual(false); // @ts-ignore // It should be invalid if the attributes param is nullish. Doesn't log an error + expect(client.setAttributes('invalid')).toEqual(false); // It should be invalid if the attributes param is not an object expect(Object.keys(client.getAttributes()).length).toEqual(0); // It should be zero after trying to add an invalid attribute expect(client.clearAttributes()).toEqual(true); + expect(loggerMock.error).toBeCalledTimes(1); // error logs + expect(loggerMock.warn).toBeCalledTimes(5); // warning logs }); test('Should return true if attributes map is valid', () => { @@ -114,6 +127,8 @@ describe('ATTRIBUTES DECORATION / validation', () => { expect(client.clearAttributes()).toEqual(true); + expect(loggerMock.error).toBeCalledTimes(0); // no error logs + expect(loggerMock.warn).toBeCalledTimes(0); // no warning logs }); }); diff --git a/src/storages/types.ts b/src/storages/types.ts index 0e3a6001..42e80a06 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -443,7 +443,7 @@ export interface IStorageFactoryParams { export type StorageType = 'MEMORY' | 'LOCALSTORAGE' | 'REDIS' | 'PLUGGABLE'; export type IStorageSyncFactory = { - type: StorageType, + readonly type: StorageType, (params: IStorageFactoryParams): IStorageSync } diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index fc3a6ad2..d07525f7 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -71,6 +71,7 @@ export function pushManagerFactory( // It is used to halt the `connectPush` process if it was in progress. let disconnected: boolean | undefined; // flag that indicates a PUSH_NONRETRYABLE_ERROR, condition with which starting pushManager again is ignored. + // true if STREAMING_DISABLED control event, or 'pushEnabled: false', or non-recoverable SSE or Auth errors. let disabled: boolean | undefined; // `disabled` implies `disconnected === true` /** PushManager functions related to initialization */ @@ -296,12 +297,15 @@ export function pushManagerFactory( // Expose Event Emitter functionality and Event constants Object.create(pushEmitter), { - // Stop/pause push mode + // Stop/pause push mode. + // It doesn't emit events. Neither PUSH_SUBSYSTEM_DOWN to start polling. stop() { disconnectPush(); // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which starts polling. if (userKey) this.remove(userKey); // Necessary to properly resume streaming in client-side (e.g., RN SDK transition to foreground). }, - // Start/resume push mode + + // Start/resume push mode. + // It eventually emits PUSH_SUBSYSTEM_DOWN, that starts polling, or PUSH_SUBSYSTEM_UP, that executes a syncAll start() { // Guard condition to avoid calling `connectPush` again if the `start` method is called multiple times or if push has been disabled. if (disabled || disconnected === false) return; From 34dbeaabbf37c55c967c606803a97a70a2172544 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 16 Feb 2022 18:22:32 -0300 Subject: [PATCH 13/53] added missing method in push manager --- package-lock.json | 2 +- package.json | 2 +- src/sync/streaming/__tests__/pushManager.spec.ts | 8 ++++++++ src/sync/streaming/pushManager.ts | 5 +++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6dd5545f..4dc54c8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.2", + "version": "1.2.1-rc.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5b25d315..6b962dff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.2", + "version": "1.2.1-rc.3", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/streaming/__tests__/pushManager.spec.ts b/src/sync/streaming/__tests__/pushManager.spec.ts index 72d8c225..fb5fc39d 100644 --- a/src/sync/streaming/__tests__/pushManager.spec.ts +++ b/src/sync/streaming/__tests__/pushManager.spec.ts @@ -65,10 +65,12 @@ describe('pushManager in client-side', () => { }, {}) as IPushManager; // calling start again has no effect (authenticates asynchronously only once) + expect(pushManager.isRunning()).toBe(false); pushManager.start(); pushManager.stop(); pushManager.start(); pushManager.start(); + expect(pushManager.isRunning()).toBe(true); // authenticates asynchronously, only once for both users const mySegmentsSyncTask = syncTaskFactory(); @@ -85,7 +87,9 @@ describe('pushManager in client-side', () => { expect(fetchAuthMock).toHaveBeenLastCalledWith([fullSettings.core.key, 'user2', 'user3', 'user4']); // pausing + expect(pushManager.isRunning()).toBe(true); pushManager.stop(); + expect(pushManager.isRunning()).toBe(false); pushManager.stop(); await new Promise(res => setTimeout(res)); expect(fetchAuthMock).toHaveBeenCalledTimes(2); @@ -150,8 +154,10 @@ describe('pushManager in server-side', () => { }, {}) as IPushManager; // calling start again has no effect (authenticates asynchronously only once) + expect(pushManager.isRunning()).toBe(false); pushManager.start(); pushManager.start(); + expect(pushManager.isRunning()).toBe(true); // @TODO pausing & resuming synchronously is not working as expected in server-side // pushManager.stop(); // pushManager.start(); @@ -160,8 +166,10 @@ describe('pushManager in server-side', () => { expect(fetchAuthMock).toHaveBeenLastCalledWith(undefined); // pausing + expect(pushManager.isRunning()).toBe(true); pushManager.stop(); pushManager.stop(); + expect(pushManager.isRunning()).toBe(false); await new Promise(res => setTimeout(res)); expect(fetchAuthMock).toHaveBeenCalledTimes(1); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index d07525f7..ec5ff916 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -315,6 +315,11 @@ export function pushManagerFactory( else setTimeout(connectPush); // server-side runs in next cycle as in client-side, for consistency with client-side }, + // true/false if start or stop was called last respectively + isRunning(){ + return disconnected === false; + }, + // [Only for client-side] add(userKey: string, mySegmentsSyncTask: ISegmentsSyncTask) { const hash = hashUserKey(userKey); From b6fe47d3d76375b3649f67022b01efa7b5cd8454 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 17 Feb 2022 12:11:14 -0300 Subject: [PATCH 14/53] implementation and unit tests --- src/logger/constants.ts | 2 +- src/logger/messages/info.ts | 2 +- src/sdkFactory/index.ts | 1 + src/storages/inLocalStorage/index.ts | 2 +- .../inMemory/ImpressionsCacheInMemory.ts | 23 +++++++- src/storages/inMemory/InMemoryStorage.ts | 2 +- src/storages/inMemory/InMemoryStorageCS.ts | 2 +- .../ImpressionsCacheInMemory.spec.ts | 53 +++++++++++++++++-- src/storages/pluggable/index.ts | 4 +- src/storages/types.ts | 6 ++- src/sync/submitters/eventsSyncTask.ts | 10 ++-- src/sync/submitters/impressionsSyncTask.ts | 13 ++++- src/types.ts | 15 ++++++ .../__tests__/settings.mocks.ts | 1 + src/utils/settingsValidation/index.ts | 2 + 15 files changed, 119 insertions(+), 19 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index ab711cac..ada74e13 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -61,7 +61,7 @@ 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_FULL_QUEUE = 115; export const SUBMITTERS_PUSH = 116; export const SYNC_START_POLLING = 117; export const SYNC_CONTINUE_POLLING = 118; diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index bf30a97c..a8746dec 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -20,7 +20,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.POLLING_START, c.LOG_PREFIX_SYNC_POLLING + 'Starting polling'], [c.POLLING_STOP, c.LOG_PREFIX_SYNC_POLLING + 'Stopping polling'], [c.SYNC_SPLITS_FETCH_RETRY, c.LOG_PREFIX_SYNC_SPLITS + 'Retrying download of splits #%s. Reason: %s'], - [c.SUBMITTERS_PUSH_FULL_EVENTS_QUEUE, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing full events queue and reseting timer.'], + [c.SUBMITTERS_PUSH_FULL_QUEUE, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing full %s queue and reseting timer.'], [c.SUBMITTERS_PUSH, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Pushing %s %s.'], [c.STREAMING_REFRESH_TOKEN, c.LOG_PREFIX_SYNC_STREAMING + 'Refreshing streaming token in %s seconds, and connecting streaming in %s seconds.'], [c.STREAMING_RECONNECT, c.LOG_PREFIX_SYNC_STREAMING + 'Attempting to reconnect streaming in %s seconds.'], diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 61433a5e..5290d5c7 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -33,6 +33,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // @TODO consider passing the settings object, so that each storage access only what it needs const storageFactoryParams: IStorageFactoryParams = { + impressionsQueueSize: settings.scheduler.impressionsQueueSize, eventsQueueSize: settings.scheduler.eventsQueueSize, optimize: shouldBeOptimized(settings), diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 0d35341b..68604e30 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -40,7 +40,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn return { splits: new SplitsCacheInLocal(log, keys, expirationTimestamp, params.splitFiltersValidation), segments: new MySegmentsCacheInLocal(log, keys), - impressions: new ImpressionsCacheInMemory(), + impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), diff --git a/src/storages/inMemory/ImpressionsCacheInMemory.ts b/src/storages/inMemory/ImpressionsCacheInMemory.ts index dcbf6b7a..57621c71 100644 --- a/src/storages/inMemory/ImpressionsCacheInMemory.ts +++ b/src/storages/inMemory/ImpressionsCacheInMemory.ts @@ -3,13 +3,34 @@ import { ImpressionDTO } from '../../types'; export class ImpressionsCacheInMemory implements IImpressionsCacheSync { - private queue: ImpressionDTO[] = []; + private onFullQueue?: () => void; + private readonly maxQueue: number; + private queue: ImpressionDTO[]; + + /** + * + * @param impressionsQueueSize number of queued impressions to call onFullQueueCb. + * Default value is 0, that means no maximum value, in case we want to avoid this being triggered. + */ + constructor(impressionsQueueSize: number = 0) { + this.maxQueue = impressionsQueueSize; + this.queue = []; + } + + setOnFullQueueCb(cb: () => void) { + this.onFullQueue = cb; + } /** * Store impressions in sequential order */ track(data: ImpressionDTO[]) { this.queue.push(...data); + + // Check if the cache queue is full and we need to flush it. + if (this.maxQueue > 0 && this.queue.length >= this.maxQueue && this.onFullQueue) { + this.onFullQueue(); + } } /** diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index 00d666fc..6f446f20 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -16,7 +16,7 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS return { splits: new SplitsCacheInMemory(), segments: new SegmentsCacheInMemory(), - impressions: new ImpressionsCacheInMemory(), + impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), diff --git a/src/storages/inMemory/InMemoryStorageCS.ts b/src/storages/inMemory/InMemoryStorageCS.ts index 7ac34ea8..4fa9b962 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -16,7 +16,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag return { splits: new SplitsCacheInMemory(), segments: new MySegmentsCacheInMemory(), - impressions: new ImpressionsCacheInMemory(), + impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize), impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined, events: new EventsCacheInMemory(params.eventsQueueSize), diff --git a/src/storages/inMemory/__tests__/ImpressionsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/ImpressionsCacheInMemory.spec.ts index e5f7ed1b..4a50eaf8 100644 --- a/src/storages/inMemory/__tests__/ImpressionsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/ImpressionsCacheInMemory.spec.ts @@ -1,13 +1,56 @@ - +// @ts-nocheck import { ImpressionsCacheInMemory } from '../ImpressionsCacheInMemory'; -test('IMPRESSIONS CACHE IN MEMORY / should incrementally store values', () => { +test('IMPRESSIONS CACHE IN MEMORY / should incrementally store values, clear the queue, and tell if it is empty', () => { const c = new ImpressionsCacheInMemory(); - // @ts-expect-error - c.track([0]); // @ts-expect-error - c.track([1, 2]); // @ts-expect-error + // queue is initially empty + expect(c.state()).toEqual([]); + expect(c.isEmpty()).toBe(true); + + + c.track([0]); + c.track([1, 2]); c.track([3]); expect(c.state()).toEqual([0, 1, 2, 3]); // all the items should be stored in sequential order + expect(c.isEmpty()).toBe(false); + + // should empty the queue + c.clear(); + expect(c.state()).toEqual([]); + expect(c.isEmpty()).toBe(true); +}); + +test('IMPRESSIONS CACHE IN MEMORY / Should call "onFullQueueCb" when the queue is full.', () => { + let cbCalled = 0; + const cache = new ImpressionsCacheInMemory(3); // small impressionsQueueSize to be reached + cache.setOnFullQueueCb(() => { cbCalled++; cache.clear(); }); + + cache.track([0]); + cache.track([0]); + expect(cbCalled).toBe(0); // if the queue is not full, it will not run the callback. + cache.track([0]); + expect(cbCalled).toBe(1); // if we had the queue full, it should flush events. + cache.track([1]); + expect(cbCalled).toBe(1); // After that, while the queue is below max size, it should not try to flush it. + cache.track([2]); + expect(cbCalled).toBe(1); // After that, while the queue is below max size, it should not try to flush it. + cache.track([3]); + expect(cbCalled).toBe(2); // Once we get to the max size, it should try to flush it. + cache.track([4]); + expect(cbCalled).toBe(2); // And it should not flush again, + cache.track([5]); + expect(cbCalled).toBe(2); // And it should not flush again, + cache.track([6]); + expect(cbCalled).toBe(3); // Until the queue is filled with events again. +}); + +test('IMPRESSIONS CACHE IN MEMORY / Should not throw if the "onFullQueueCb" callback was not provided.', () => { + const cache = new ImpressionsCacheInMemory(3); // small eventsQueueSize to be reached + + cache.track([0]); + cache.track([1]); // Cache still not full, + expect(cache.track.bind(cache, [2])).not.toThrow(); // but when it is full, as 'onFullQueueCb' was not provided, nothing happens but no exceptions are thrown. + expect(cache.track.bind(cache, [3])).not.toThrow(); // but when it is full, as 'onFullQueueCb' was not provided, nothing happens but no exceptions are thrown. }); diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index fef04aa6..e4062132 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -63,7 +63,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn const prefix = validatePrefix(options.prefix); - function PluggableStorageFactory({ log, metadata, onReadyCb, mode, eventsQueueSize, optimize }: IStorageFactoryParams): IStorageAsync { + function PluggableStorageFactory({ log, metadata, onReadyCb, mode, eventsQueueSize, impressionsQueueSize, optimize }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const wrapper = wrapperAdapter(log, options.wrapper); const isPartialConsumer = mode === CONSUMER_PARTIAL_MODE; @@ -74,7 +74,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn return { splits: new SplitsCachePluggable(log, keys, wrapper), segments: new SegmentsCachePluggable(log, keys, wrapper), - impressions: isPartialConsumer ? new ImpressionsCacheInMemory() : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), + impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), impressionCounts: optimize ? new ImpressionCountsCacheInMemory() : undefined, events: isPartialConsumer ? promisifyEventsTrack(new EventsCacheInMemory(eventsQueueSize)) : new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata), // @TODO add telemetry cache when required diff --git a/src/storages/types.ts b/src/storages/types.ts index 42e80a06..e699561b 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -298,7 +298,7 @@ export interface IRecorderCacheProducerSync { // @TODO names are inconsistent with spec /* Checks if cache is empty. Returns true if the cache was just created or cleared */ isEmpty(): boolean - /* clears cache data */ + /* Clears cache data */ clear(): void /* Gets cache data */ state(): T @@ -307,10 +307,13 @@ export interface IRecorderCacheProducerSync { export interface IImpressionsCacheSync extends IImpressionsCacheBase, IRecorderCacheProducerSync { track(data: ImpressionDTO[]): void + /* Registers callback for full queue */ + setOnFullQueueCb(cb: () => void): void } export interface IEventsCacheSync extends IEventsCacheBase, IRecorderCacheProducerSync { track(data: SplitIO.EventData, size?: number): boolean + /* Registers callback for full queue */ setOnFullQueueCb(cb: () => void): void } @@ -423,6 +426,7 @@ export type DataLoader = (storage: IStorageSync, matchingKey: string) => void export interface IStorageFactoryParams { log: ILogger, + impressionsQueueSize?: number, eventsQueueSize?: number, optimize?: boolean /* whether create the `impressionCounts` cache (OPTIMIZED impression mode) or not (DEBUG impression mode) */, diff --git a/src/sync/submitters/eventsSyncTask.ts b/src/sync/submitters/eventsSyncTask.ts index 3a15a6e0..151607ea 100644 --- a/src/sync/submitters/eventsSyncTask.ts +++ b/src/sync/submitters/eventsSyncTask.ts @@ -3,7 +3,9 @@ import { IPostEventsBulk } from '../../services/types'; import { ISyncTask, ITimeTracker } from '../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; import { ILogger } from '../../logger/types'; -import { SUBMITTERS_PUSH_FULL_EVENTS_QUEUE } from '../../logger/constants'; +import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; + +const DATA_NAME = 'events'; /** * Sync task that periodically posts tracked events @@ -18,7 +20,7 @@ export function eventsSyncTaskFactory( ): ISyncTask { // don't retry events. - const syncTask = submitterSyncTaskFactory(log, postEventsBulk, eventsCache, eventsPushRate, 'queued events', latencyTracker); + const syncTask = submitterSyncTaskFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME, latencyTracker); // Set a timer for the first push of events, if (eventsFirstPushWindow > 0) { @@ -34,9 +36,9 @@ export function eventsSyncTaskFactory( }; } - // register eventsSubmitter to be executed when events cache is full + // register events submitter to be executed when events cache is full eventsCache.setOnFullQueueCb(() => { - log.info(SUBMITTERS_PUSH_FULL_EVENTS_QUEUE); + log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); syncTask.execute(); }); diff --git a/src/sync/submitters/impressionsSyncTask.ts b/src/sync/submitters/impressionsSyncTask.ts index cf6b6068..5947c40c 100644 --- a/src/sync/submitters/impressionsSyncTask.ts +++ b/src/sync/submitters/impressionsSyncTask.ts @@ -6,6 +6,9 @@ import { ImpressionDTO } from '../../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; import { ImpressionsPayload } from './types'; import { ILogger } from '../../logger/types'; +import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; + +const DATA_NAME = 'impressions'; /** * Converts `impressions` data from cache into request payload. @@ -50,5 +53,13 @@ export function impressionsSyncTaskFactory( ): ISyncTask { // retry impressions only once. - return submitterSyncTaskFactory(log, postTestImpressionsBulk, impressionsCache, impressionsRefreshRate, 'impressions', latencyTracker, fromImpressionsCollector.bind(undefined, sendLabels), 1); + const syncTask = submitterSyncTaskFactory(log, postTestImpressionsBulk, impressionsCache, impressionsRefreshRate, DATA_NAME, latencyTracker, fromImpressionsCollector.bind(undefined, sendLabels), 1); + + // register impressions submitter to be executed when impressions cache is full + impressionsCache.setOnFullQueueCb(() => { + log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); + syncTask.execute(); + }); + + return syncTask; } diff --git a/src/types.ts b/src/types.ts index 46451a60..1e3654a8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,6 +73,7 @@ export interface ISettings { readonly scheduler: { featuresRefreshRate: number, impressionsRefreshRate: number, + impressionsQueueSize: number, metricsRefreshRate: number, segmentsRefreshRate: number, offlineRefreshRate: number, @@ -255,6 +256,13 @@ interface INodeBasicSettings extends ISharedSettings { * @default 300 */ impressionsRefreshRate?: number, + /** + * The maximum number of impression items we want to queue. If we queue more values, it will trigger a flush and reset the timer. + * If you use a 0 here, the queue will have no maximum size. + * @property {number} impressionsQueueSize + * @default 30000 + */ + impressionsQueueSize?: number, /** * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. * @property {number} metricsRefreshRate @@ -769,6 +777,13 @@ export namespace SplitIO { * @default 60 */ impressionsRefreshRate?: number, + /** + * The maximum number of impression items we want to queue. If we queue more values, it will trigger a flush and reset the timer. + * If you use a 0 here, the queue will have no maximum size. + * @property {number} impressionsQueueSize + * @default 30000 + */ + impressionsQueueSize?: number, /** * The SDK sends diagnostic metrics to Split servers. This parameters controls this metric flush period in seconds. * @property {number} metricsRefreshRate diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 80b0392e..3cb458cb 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -52,6 +52,7 @@ export const fullSettings: ISettings = { offlineRefreshRate: 1, eventsPushRate: 1, eventsQueueSize: 1, + impressionsQueueSize: 1, pushRetryBackoffBase: 1 }, startup: { diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 3d535d57..20bcd1bc 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -38,6 +38,8 @@ const base = { eventsPushRate: 60, // how many events will be queued before flushing eventsQueueSize: 500, + // how many impressions will be queued before flushing + impressionsQueueSize: 30000, // backoff base seconds to wait before re attempting to connect to push notifications pushRetryBackoffBase: 1, }, From 2478bbc24c8bc3d78d86378efaba31a8b191da17 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 17 Feb 2022 14:05:25 -0300 Subject: [PATCH 15/53] move node runtime validator to JS SDK --- package-lock.json | 9 -------- package.json | 1 - src/utils/settingsValidation/runtime.ts | 9 ++++++++ .../settingsValidation/runtime/browser.ts | 8 ------- src/utils/settingsValidation/runtime/node.ts | 22 ------------------- 5 files changed, 9 insertions(+), 40 deletions(-) create mode 100644 src/utils/settingsValidation/runtime.ts delete mode 100644 src/utils/settingsValidation/runtime/browser.ts delete mode 100644 src/utils/settingsValidation/runtime/node.ts diff --git a/package-lock.json b/package-lock.json index 4dc54c8b..c8abb07d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1070,15 +1070,6 @@ "@types/node": "*" } }, - "@types/ip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz", - "integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", diff --git a/package.json b/package.json index 6b962dff..2bed38b7 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "devDependencies": { "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", - "@types/ip": "^1.1.0", "@types/jest": "^27.0.0", "@types/lodash": "^4.14.162", "@typescript-eslint/eslint-plugin": "^4.2.0", diff --git a/src/utils/settingsValidation/runtime.ts b/src/utils/settingsValidation/runtime.ts new file mode 100644 index 00000000..2f53ac54 --- /dev/null +++ b/src/utils/settingsValidation/runtime.ts @@ -0,0 +1,9 @@ +import { ISettings } from '../../types'; + +// For client-side SDKs, machine IP and Hostname are not captured and sent to Split backend. +export function validateRuntime(): ISettings['runtime'] { + return { + ip: false, + hostname: false + }; +} diff --git a/src/utils/settingsValidation/runtime/browser.ts b/src/utils/settingsValidation/runtime/browser.ts deleted file mode 100644 index 8da6ac6a..00000000 --- a/src/utils/settingsValidation/runtime/browser.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ISettings } from '../../../types'; - -export function validateRuntime(): ISettings['runtime'] { - return { - ip: false, - hostname: false - }; -} diff --git a/src/utils/settingsValidation/runtime/node.ts b/src/utils/settingsValidation/runtime/node.ts deleted file mode 100644 index fa97772d..00000000 --- a/src/utils/settingsValidation/runtime/node.ts +++ /dev/null @@ -1,22 +0,0 @@ -import osFunction from 'os'; -import ipFunction from 'ip'; - -import { UNKNOWN, NA, CONSUMER_MODE } from '../../constants'; -import { ISettings } from '../../../types'; - -export function validateRuntime(settings: ISettings): ISettings['runtime'] { - const isIPAddressesEnabled = settings.core.IPAddressesEnabled === true; - const isConsumerMode = settings.mode === CONSUMER_MODE; - - // If the values are not available, default to false (for standalone) or "unknown" (for consumer mode, to be used on Redis keys) - let ip = ipFunction.address() || (isConsumerMode ? UNKNOWN : false); - let hostname = osFunction.hostname() || (isConsumerMode ? UNKNOWN : false); - - if (!isIPAddressesEnabled) { // If IPAddresses setting is not enabled, set as false (for standalone) or "NA" (for consumer mode, to be used on Redis keys) - ip = hostname = isConsumerMode ? NA : false; - } - - return { - ip, hostname - }; -} From 0e67f8ae03fe7daa8e883dbdaab021fc8537a2a6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 23 Feb 2022 13:00:53 -0300 Subject: [PATCH 16/53] implementation and unit tests --- src/logger/constants.ts | 1 + src/logger/messages/warn.ts | 1 + src/sdkClient/__tests__/client.spec.ts | 39 ++++++++++++++++++ src/sdkClient/client.ts | 12 +++--- src/sync/__tests__/syncManagerOnline.spec.ts | 43 ++++++++++++++++++++ src/sync/submitters/eventsSyncTask.ts | 11 +++-- src/sync/submitters/impressionsSyncTask.ts | 11 +++-- src/sync/syncManagerOnline.ts | 22 +++++++--- src/sync/types.ts | 5 ++- src/utils/constants/index.ts | 5 +++ 10 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 src/sdkClient/__tests__/client.spec.ts create mode 100644 src/sync/__tests__/syncManagerOnline.spec.ts diff --git a/src/logger/constants.ts b/src/logger/constants.ts index ada74e13..f9e5041b 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -93,6 +93,7 @@ export const WARN_SPLITS_FILTER_INVALID = 220; export const WARN_SPLITS_FILTER_EMPTY = 221; export const WARN_API_KEY = 222; export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 223; +export const SUBMITTERS_PUSH_FULL_QUEUE_DROPPED = 224; export const ERROR_ENGINE_COMBINER_IFELSEIF = 300; export const ERROR_LOGLEVEL_INVALID = 301; diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index f6a66eec..ccda06b8 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -13,6 +13,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.STREAMING_FALLBACK, c.LOG_PREFIX_SYNC_STREAMING + 'Falling back to polling mode. Reason: %s'], [c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Droping %s %s after retry. Reason: %s.'], [c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s %s, keeping data to retry on next iteration. Reason: %s.'], + [c.SUBMITTERS_PUSH_FULL_QUEUE_DROPPED, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Droping %s from full queue. Reason: user consent declined.'], // 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.'], diff --git a/src/sdkClient/__tests__/client.spec.ts b/src/sdkClient/__tests__/client.spec.ts new file mode 100644 index 00000000..f447d6d3 --- /dev/null +++ b/src/sdkClient/__tests__/client.spec.ts @@ -0,0 +1,39 @@ +import { ISettings } from '../../types'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; +import { clientFactory } from '../client'; + +test('client should track or not events and impressions depending on user consent status', () => { + const sdkReadinessManager = { readinessManager: { isReady: () => true } }; + const storage = { splits: { trafficTypeExists: () => true } }; + + const settings: ISettings = { ...fullSettings, userConsent: undefined }; + const eventTracker = { track: jest.fn() }; + const impressionsTracker = { track: jest.fn() }; + + // @ts-ignore + const client = clientFactory({ settings, eventTracker, impressionsTracker, sdkReadinessManager, storage }); + + client.getTreatment('some_key', 'some_split'); + expect(impressionsTracker.track).toBeCalledTimes(1); // impression should be tracked if userConsent is undefined + client.track('some_key', 'some_tt', 'some_event'); + expect(eventTracker.track).toBeCalledTimes(1); // event should be tracked if userConsent is undefined + + settings.userConsent = 'unknown'; + client.getTreatment('some_key', 'some_split'); + expect(impressionsTracker.track).toBeCalledTimes(2); // impression should be tracked if userConsent is unknown + client.track('some_key', 'some_tt', 'some_event'); + expect(eventTracker.track).toBeCalledTimes(2); // event should be tracked if userConsent is unknown + + settings.userConsent = 'granted'; + client.getTreatment('some_key', 'some_split'); + expect(impressionsTracker.track).toBeCalledTimes(3); // impression should be tracked if userConsent is granted + client.track('some_key', 'some_tt', 'some_event'); + expect(eventTracker.track).toBeCalledTimes(3); // event should be tracked if userConsent is granted + + settings.userConsent = 'declined'; + client.getTreatment('some_key', 'some_split'); + expect(impressionsTracker.track).toBeCalledTimes(3); // impression should not be tracked if userConsent is declined + client.track('some_key', 'some_tt', 'some_event'); + expect(eventTracker.track).toBeCalledTimes(3); // event should not be tracked if userConsent is declined + +}); diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 5b0b7de2..7d5ab72c 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -4,7 +4,7 @@ import { getMatching, getBucketing } from '../utils/key'; import { validateSplitExistance } from '../utils/inputValidation/splitExistance'; import { validateTrafficTypeExistance } from '../utils/inputValidation/trafficTypeExistance'; import { SDK_NOT_READY } from '../utils/labels'; -import { CONTROL } from '../utils/constants'; +import { CONSENT_DECLINED, CONTROL } from '../utils/constants'; import { IClientFactoryParams } from './types'; import { IEvaluationResult } from '../evaluator/types'; import { SplitIO, ImpressionDTO } from '../types'; @@ -16,13 +16,14 @@ import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants'; */ // @TODO missing time tracking to collect telemetry export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | SplitIO.IAsyncClient { - const { sdkReadinessManager: { readinessManager }, storage, settings: { log, mode }, impressionsTracker, eventTracker } = params; + const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker } = params; + const { log, mode } = settings; function getTreatment(key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, withConfig = false) { const wrapUp = (evaluationResult: IEvaluationResult) => { const queue: ImpressionDTO[] = []; const treatment = processEvaluation(evaluationResult, splitName, key, attributes, withConfig, `getTreatment${withConfig ? 'withConfig' : ''}`, queue); - impressionsTracker.track(queue, attributes); + if (settings.userConsent !== CONSENT_DECLINED) impressionsTracker.track(queue, attributes); return treatment; }; @@ -42,7 +43,7 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S Object.keys(evaluationResults).forEach(splitName => { treatments[splitName] = processEvaluation(evaluationResults[splitName], splitName, key, attributes, withConfig, `getTreatments${withConfig ? 'withConfig' : ''}`, queue); }); - impressionsTracker.track(queue, attributes); + if (settings.userConsent !== CONSENT_DECLINED) impressionsTracker.track(queue, attributes); return treatments; }; @@ -115,7 +116,8 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S // 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(log, readinessManager, storage.splits, mode, trafficTypeName, 'track'); - return eventTracker.track(eventData, size); + if (settings.userConsent !== CONSENT_DECLINED) return eventTracker.track(eventData, size); + else return false; } return { diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts new file mode 100644 index 00000000..0e8f2a64 --- /dev/null +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -0,0 +1,43 @@ +import { ISettings } from '../../types'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; +import { syncTaskFactory } from './syncTask.mock'; + +jest.mock('../submitters/submitterManager', () => { + return { + submitterManagerFactory: syncTaskFactory + }; +}); + +import { syncManagerOnlineFactory } from '../syncManagerOnline'; + +test('syncManagerOnline should start or not the submitter depending on user consent status', () => { + const settings: ISettings = { ...fullSettings, userConsent: undefined }; + + // @ts-ignore + const syncManager = syncManagerOnlineFactory()({ settings }); + const submitter = syncManager.submitter!; + + syncManager.start(); + expect(submitter.start).toBeCalledTimes(1); // Submitter should be started if userConsent is undefined + syncManager.stop(); + expect(submitter.stop).toBeCalledTimes(1); + + settings.userConsent = 'unknown'; + syncManager.start(); + expect(submitter.start).toBeCalledTimes(1); // Submitter should not be started if userConsent is unknown + syncManager.stop(); + expect(submitter.stop).toBeCalledTimes(2); + + settings.userConsent = 'granted'; + syncManager.start(); + expect(submitter.start).toBeCalledTimes(2); // Submitter should be started if userConsent is granted + syncManager.stop(); + expect(submitter.stop).toBeCalledTimes(3); + + settings.userConsent = 'declined'; + syncManager.start(); + expect(submitter.start).toBeCalledTimes(2); // Submitter should not be started if userConsent is declined + syncManager.stop(); + expect(submitter.stop).toBeCalledTimes(4); + +}); diff --git a/src/sync/submitters/eventsSyncTask.ts b/src/sync/submitters/eventsSyncTask.ts index 151607ea..fcd5b3b8 100644 --- a/src/sync/submitters/eventsSyncTask.ts +++ b/src/sync/submitters/eventsSyncTask.ts @@ -3,7 +3,7 @@ import { IPostEventsBulk } from '../../services/types'; import { ISyncTask, ITimeTracker } from '../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; import { ILogger } from '../../logger/types'; -import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; +import { SUBMITTERS_PUSH_FULL_QUEUE, SUBMITTERS_PUSH_FULL_QUEUE_DROPPED } from '../../logger/constants'; const DATA_NAME = 'events'; @@ -38,8 +38,13 @@ export function eventsSyncTaskFactory( // register events submitter to be executed when events cache is full eventsCache.setOnFullQueueCb(() => { - log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); - syncTask.execute(); + if (syncTask.isRunning()) { + log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); + syncTask.execute(); + } else { // If submitter is stopped (e.g., user consent declined, or app state offline), we drop items + log.warn(SUBMITTERS_PUSH_FULL_QUEUE_DROPPED); + eventsCache.clear(); + } }); return syncTask; diff --git a/src/sync/submitters/impressionsSyncTask.ts b/src/sync/submitters/impressionsSyncTask.ts index 5947c40c..9f225fdd 100644 --- a/src/sync/submitters/impressionsSyncTask.ts +++ b/src/sync/submitters/impressionsSyncTask.ts @@ -6,7 +6,7 @@ import { ImpressionDTO } from '../../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; import { ImpressionsPayload } from './types'; import { ILogger } from '../../logger/types'; -import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; +import { SUBMITTERS_PUSH_FULL_QUEUE, SUBMITTERS_PUSH_FULL_QUEUE_DROPPED } from '../../logger/constants'; const DATA_NAME = 'impressions'; @@ -57,8 +57,13 @@ export function impressionsSyncTaskFactory( // register impressions submitter to be executed when impressions cache is full impressionsCache.setOnFullQueueCb(() => { - log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); - syncTask.execute(); + if (syncTask.isRunning()) { + log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); + syncTask.execute(); + } else { // If submitter is stopped (e.g., user consent declined, or app state offline), we drop items + log.warn(SUBMITTERS_PUSH_FULL_QUEUE_DROPPED); + impressionsCache.clear(); + } }); return syncTask; diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 1bbfe948..425b9d9f 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -6,6 +6,7 @@ import { IPushManager } from './streaming/types'; import { IPollingManager, IPollingManagerCS } from './polling/types'; import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants'; import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants'; +import { CONSENT_GRANTED } from '../utils/constants'; /** * Online SyncManager factory. @@ -39,6 +40,11 @@ export function syncManagerOnlineFactory( // It is not inyected as push and polling managers, because at the moment it is required const submitter = submitterManagerFactory(params); + function isUserConsentGranted() { + const userConsent = params.settings.userConsent; + // undefined userConsent is handled as granted to avoid a breaking change with users that don't use this features + return !userConsent || userConsent === CONSENT_GRANTED; + } /** Sync Manager logic */ @@ -69,12 +75,18 @@ export function syncManagerOnlineFactory( let startFirstTime = true; // flag to distinguish calling the `start` method for the first time, to support pausing and resuming the synchronization return { + // Exposed for fine-grained control of synchronization. + // E.g.: user consent, app state changes (Page hide, Foreground/Background, Online/Offline). + pollingManager, pushManager, + submitter, /** * Method used to start the syncManager for the first time, or resume it after being stopped. */ start() { + running = true; + // start syncing splits and segments if (pollingManager) { if (pushManager) { @@ -90,21 +102,21 @@ export function syncManagerOnlineFactory( } // start periodic data recording (events, impressions, telemetry). - if (submitter) submitter.start(); - running = true; + if (isUserConsentGranted()) submitter.start(); }, /** * Method used to stop/pause the syncManager. */ stop() { + running = false; + // stop syncing if (pushManager) pushManager.stop(); if (pollingManager && pollingManager.isRunning()) pollingManager.stop(); // stop periodic data recording (events, impressions, telemetry). - if (submitter) submitter.stop(); - running = false; + submitter.stop(); }, isRunning() { @@ -112,7 +124,7 @@ export function syncManagerOnlineFactory( }, flush() { - if (submitter) return submitter.execute(); + if (isUserConsentGranted()) return submitter.execute(); else return Promise.resolve(); }, diff --git a/src/sync/types.ts b/src/sync/types.ts index 0f0588d9..1a70a5c6 100644 --- a/src/sync/types.ts +++ b/src/sync/types.ts @@ -3,6 +3,7 @@ import { IPlatform } from '../sdkFactory/types'; import { ISplitApi } from '../services/types'; import { IStorageSync } from '../storages/types'; import { ISettings } from '../types'; +import { IPollingManager } from './polling/types'; import { IPushManager } from './streaming/types'; export interface ITask { @@ -43,7 +44,9 @@ export interface ITimeTracker { export interface ISyncManager extends ITask { flush(): Promise, - pushManager?: IPushManager + pushManager?: IPushManager, + pollingManager?: IPollingManager, + submitter?: ISyncTask } export interface ISyncManagerCS extends ISyncManager { diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 4a4d8303..9b3412a5 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -32,3 +32,8 @@ export const STORAGE_MEMORY: StorageType = 'MEMORY'; export const STORAGE_LOCALSTORAGE: StorageType = 'LOCALSTORAGE'; export const STORAGE_REDIS: StorageType = 'REDIS'; export const STORAGE_PLUGGABLE: StorageType = 'PLUGGABLE'; + +// User consent +export const CONSENT_GRANTED = 'granted'; // The user has granted consent for tracking events and impressions +export const CONSENT_DECLINED = 'declined'; // The user has declined consent for tracking events and impressions +export const CONSENT_UNKNOWN = 'unknown'; // The user has neither granted nor declined consent for tracking events and impressions From 58620798732acbb4c2828612ec32819dfe472a3d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 23 Feb 2022 13:29:14 -0300 Subject: [PATCH 17/53] updated settings interface --- src/types.ts | 6 ++++++ src/utils/settingsValidation/__tests__/settings.mocks.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 1e3654a8..9f67b027 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,6 +54,11 @@ type EventConsts = { * @typedef {string} SDKMode */ export type SDKMode = 'standalone' | 'consumer' | 'localhost' | 'consumer_partial'; +/** + * User consent status. + * @typedef {string} ConsentStatus + */ +export type ConsentStatus = 'granted' | 'declined' | 'unknown'; /** * Settings interface. This is a representation of the settings the SDK expose, that's why * most of it's props are readonly. Only features should be rewritten when localhost mode is active. @@ -111,6 +116,7 @@ export interface ISettings { }, readonly log: ILogger readonly impressionListener?: unknown + userConsent?: ConsentStatus } /** * Log levels. diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 3cb458cb..ff160297 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -88,7 +88,8 @@ export const fullSettings: ISettings = { auth: 'auth', streaming: 'streaming' }, - log: loggerMock + log: loggerMock, + userConsent: undefined }; export const fullSettingsServerSide = { From b2dfd2e494993eea48f0da4fdaf6c11cd2394579 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 23 Feb 2022 14:11:29 -0300 Subject: [PATCH 18/53] update submitters to not drop data if queues are full but consent has not been granted yet --- src/logger/constants.ts | 1 - src/logger/messages/warn.ts | 1 - src/sync/submitters/eventsSyncTask.ts | 7 +++---- src/sync/submitters/impressionsSyncTask.ts | 7 +++---- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index f9e5041b..ada74e13 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -93,7 +93,6 @@ export const WARN_SPLITS_FILTER_INVALID = 220; export const WARN_SPLITS_FILTER_EMPTY = 221; export const WARN_API_KEY = 222; export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 223; -export const SUBMITTERS_PUSH_FULL_QUEUE_DROPPED = 224; export const ERROR_ENGINE_COMBINER_IFELSEIF = 300; export const ERROR_LOGLEVEL_INVALID = 301; diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index ccda06b8..f6a66eec 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -13,7 +13,6 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.STREAMING_FALLBACK, c.LOG_PREFIX_SYNC_STREAMING + 'Falling back to polling mode. Reason: %s'], [c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Droping %s %s after retry. Reason: %s.'], [c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s %s, keeping data to retry on next iteration. Reason: %s.'], - [c.SUBMITTERS_PUSH_FULL_QUEUE_DROPPED, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Droping %s from full queue. Reason: user consent declined.'], // 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.'], diff --git a/src/sync/submitters/eventsSyncTask.ts b/src/sync/submitters/eventsSyncTask.ts index fcd5b3b8..b87ee24e 100644 --- a/src/sync/submitters/eventsSyncTask.ts +++ b/src/sync/submitters/eventsSyncTask.ts @@ -3,7 +3,7 @@ import { IPostEventsBulk } from '../../services/types'; import { ISyncTask, ITimeTracker } from '../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; import { ILogger } from '../../logger/types'; -import { SUBMITTERS_PUSH_FULL_QUEUE, SUBMITTERS_PUSH_FULL_QUEUE_DROPPED } from '../../logger/constants'; +import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; const DATA_NAME = 'events'; @@ -41,10 +41,9 @@ export function eventsSyncTaskFactory( if (syncTask.isRunning()) { log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); syncTask.execute(); - } else { // If submitter is stopped (e.g., user consent declined, or app state offline), we drop items - log.warn(SUBMITTERS_PUSH_FULL_QUEUE_DROPPED); - eventsCache.clear(); } + // If submitter is stopped (e.g., user consent declined or unknown, or app state offline), we don't send the data. + // Data will be sent when submitter is resumed. }); return syncTask; diff --git a/src/sync/submitters/impressionsSyncTask.ts b/src/sync/submitters/impressionsSyncTask.ts index 9f225fdd..f3fdb20c 100644 --- a/src/sync/submitters/impressionsSyncTask.ts +++ b/src/sync/submitters/impressionsSyncTask.ts @@ -6,7 +6,7 @@ import { ImpressionDTO } from '../../types'; import { submitterSyncTaskFactory } from './submitterSyncTask'; import { ImpressionsPayload } from './types'; import { ILogger } from '../../logger/types'; -import { SUBMITTERS_PUSH_FULL_QUEUE, SUBMITTERS_PUSH_FULL_QUEUE_DROPPED } from '../../logger/constants'; +import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants'; const DATA_NAME = 'impressions'; @@ -60,10 +60,9 @@ export function impressionsSyncTaskFactory( if (syncTask.isRunning()) { log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]); syncTask.execute(); - } else { // If submitter is stopped (e.g., user consent declined, or app state offline), we drop items - log.warn(SUBMITTERS_PUSH_FULL_QUEUE_DROPPED); - impressionsCache.clear(); } + // If submitter is stopped (e.g., user consent declined or unknown, or app state offline), we don't send the data. + // Data will be sent when submitter is resumed. }); return syncTask; From 73423894a45b59e946883d8790dd808b2b5ead8d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 23 Feb 2022 16:14:52 -0300 Subject: [PATCH 19/53] Modify browser listener flow to avoid flushing data without consent --- src/listeners/__tests__/browser.spec.ts | 36 +++++++++++++++++++++++-- src/listeners/browser.ts | 22 ++++++++------- src/sync/syncManagerOnline.ts | 14 +++------- src/utils/consent.ts | 8 ++++++ 4 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 src/utils/consent.ts diff --git a/src/listeners/__tests__/browser.spec.ts b/src/listeners/__tests__/browser.spec.ts index 8af13e8b..730c19de 100644 --- a/src/listeners/__tests__/browser.spec.ts +++ b/src/listeners/__tests__/browser.spec.ts @@ -85,11 +85,12 @@ beforeAll(() => { }); }); -// clean mocks -afterEach(() => { +// clear mocks +beforeEach(() => { (global.window.addEventListener as jest.Mock).mockClear(); (global.window.removeEventListener as jest.Mock).mockClear(); if (global.window.navigator.sendBeacon) (global.window.navigator.sendBeacon as jest.Mock).mockClear(); + Object.values(fakeSplitApi).forEach(method => method.mockClear()); }); // delete mocks from global @@ -227,3 +228,34 @@ test('Browser JS listener / standalone mode / Impressions debug mode without sen // restore sendBeacon API global.navigator.sendBeacon = sendBeacon; }); + +test('Browser JS listener / standalone mode / user consent status', () => { + const syncManagerMock = {}; + const settings = { ...fullSettings }; + + // @ts-expect-error + const listener = new BrowserSignalListener(syncManagerMock, settings, fakeStorageOptimized as IStorageSync, fakeSplitApi); + + listener.start(); + + settings.userConsent = 'unknown'; + triggerUnloadEvent(); + settings.userConsent = 'declined'; + triggerUnloadEvent(); + + // Unload event was triggered when user consent was unknown and declined. Thus sendBeacon and post services should not be called + expect(global.window.navigator.sendBeacon).toBeCalledTimes(0); + expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); + expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); + expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); + + settings.userConsent = 'granted'; + triggerUnloadEvent(); + settings.userConsent = undefined; + triggerUnloadEvent(); + + // Unload event was triggered when user consent was granted and undefined. Thus sendBeacon should be called 6 times (3 times per event in optimized mode). + expect(global.window.navigator.sendBeacon).toBeCalledTimes(6); + + listener.stop(); +}); diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index 9705af2c..502c4e43 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -11,6 +11,7 @@ import { OPTIMIZED, DEBUG } from '../utils/constants'; import { objectAssign } from '../utils/lang/objectAssign'; import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; import { ISyncManager } from '../sync/types'; +import { isConsentGranted } from '../utils/consent'; // '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'; @@ -65,15 +66,18 @@ export class BrowserSignalListener implements ISignalListener { flushData() { if (!this.syncManager) return; // In consumer mode there is not sync manager and data to flush - const eventsUrl = this.settings.urls.events; - const extraMetadata = { - // sim stands for Sync/Split Impressions Mode - sim: this.settings.sync.impressionsMode === OPTIMIZED ? OPTIMIZED : DEBUG - }; + // Flush data if there is user consent + if (isConsentGranted(this.settings)) { + const eventsUrl = this.settings.urls.events; + const extraMetadata = { + // sim stands for Sync/Split Impressions Mode + sim: this.settings.sync.impressionsMode === OPTIMIZED ? OPTIMIZED : DEBUG + }; - this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata); - this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk); - if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector); + this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata); + this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk); + if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector); + } // Close streaming connection if (this.syncManager.pushManager) this.syncManager.pushManager.stop(); @@ -84,7 +88,7 @@ export class BrowserSignalListener implements ISignalListener { if (!cache.isEmpty()) { const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.state()) : cache.state(); if (!this._sendBeacon(url, dataPayload, extraMetadata)) { - postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op just to catch a possible exceptions + postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op just to catch a possible exception } cache.clear(); } diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 425b9d9f..b95be139 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -6,7 +6,7 @@ import { IPushManager } from './streaming/types'; import { IPollingManager, IPollingManagerCS } from './polling/types'; import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants'; import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants'; -import { CONSENT_GRANTED } from '../utils/constants'; +import { isConsentGranted } from '../utils/consent'; /** * Online SyncManager factory. @@ -26,7 +26,7 @@ export function syncManagerOnlineFactory( */ return function (params: ISyncManagerFactoryParams): ISyncManagerCS { - const { log, streamingEnabled } = params.settings; + const { settings, settings: { log, streamingEnabled } } = params; /** Polling Manager */ const pollingManager = pollingManagerFactory && pollingManagerFactory(params); @@ -40,12 +40,6 @@ export function syncManagerOnlineFactory( // It is not inyected as push and polling managers, because at the moment it is required const submitter = submitterManagerFactory(params); - function isUserConsentGranted() { - const userConsent = params.settings.userConsent; - // undefined userConsent is handled as granted to avoid a breaking change with users that don't use this features - return !userConsent || userConsent === CONSENT_GRANTED; - } - /** Sync Manager logic */ function startPolling() { @@ -102,7 +96,7 @@ export function syncManagerOnlineFactory( } // start periodic data recording (events, impressions, telemetry). - if (isUserConsentGranted()) submitter.start(); + if (isConsentGranted(settings)) submitter.start(); }, /** @@ -124,7 +118,7 @@ export function syncManagerOnlineFactory( }, flush() { - if (isUserConsentGranted()) return submitter.execute(); + if (isConsentGranted(settings)) return submitter.execute(); else return Promise.resolve(); }, diff --git a/src/utils/consent.ts b/src/utils/consent.ts new file mode 100644 index 00000000..20ca745c --- /dev/null +++ b/src/utils/consent.ts @@ -0,0 +1,8 @@ +import { ISettings } from '../types'; +import { CONSENT_GRANTED } from './constants'; + +export function isConsentGranted(settings: ISettings) { + const userConsent = settings.userConsent; + // undefined userConsent is handled as granted (default) + return !userConsent || userConsent === CONSENT_GRANTED; +} From bae3e5ee12695c7a77ef2b869a4d1eb9450e1091 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 24 Feb 2022 16:25:56 -0300 Subject: [PATCH 20/53] added getter & setter for user consent in factory --- src/logger/constants.ts | 2 + src/logger/messages/error.ts | 1 + src/logger/messages/info.ts | 1 + src/sdkClient/__tests__/client.spec.ts | 2 +- .../__tests__/userConsentProps.spec.ts | 39 +++++++++++++++++++ src/sdkFactory/index.ts | 9 +++-- src/sdkFactory/types.ts | 2 + src/sdkFactory/userConsentProps.ts | 37 ++++++++++++++++++ src/sync/__tests__/syncManagerOnline.spec.ts | 2 +- 9 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/sdkFactory/__tests__/userConsentProps.spec.ts create mode 100644 src/sdkFactory/userConsentProps.ts diff --git a/src/logger/constants.ts b/src/logger/constants.ts index ada74e13..92428e5b 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -68,6 +68,7 @@ export const SYNC_CONTINUE_POLLING = 118; export const SYNC_STOP_POLLING = 119; export const EVENTS_TRACKER_SUCCESS = 120; export const IMPRESSIONS_TRACKER_SUCCESS = 121; +export const USER_CONSENT_UPDATED = 122; export const ENGINE_VALUE_INVALID = 200; export const ENGINE_VALUE_NO_ATTRIBUTES = 201; @@ -119,6 +120,7 @@ export const ERROR_INVALID_IMPRESSIONS_MODE = 321; export const ERROR_HTTP = 322; export const ERROR_LOCALHOST_MODULE_REQUIRED = 323; export const ERROR_STORAGE_INVALID = 324; +export const ERROR_NOT_BOOLEAN = 325; // Log prefixes (a.k.a. tags or categories) export const LOG_PREFIX_SETTINGS = 'settings'; diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 1d0fe00e..4c2b3d70 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -28,6 +28,7 @@ export const codesError: [number, string][] = [ [c.ERROR_INVALID, '%s: you passed an invalid %s. It must be a non-empty string.'], [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'], [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'], + [c.ERROR_NOT_BOOLEAN, '%s: param must be boolean.'], // initialization / settings validation [c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'], [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'], diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index a8746dec..bac83ea9 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -14,6 +14,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.NEW_FACTORY, ' New Split SDK instance created.'], [c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'], [c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'], + [c.USER_CONSENT_UPDATED, ' User consent changed from %s to %s.'], // synchronizer [c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'], diff --git a/src/sdkClient/__tests__/client.spec.ts b/src/sdkClient/__tests__/client.spec.ts index f447d6d3..a46be232 100644 --- a/src/sdkClient/__tests__/client.spec.ts +++ b/src/sdkClient/__tests__/client.spec.ts @@ -6,7 +6,7 @@ test('client should track or not events and impressions depending on user consen const sdkReadinessManager = { readinessManager: { isReady: () => true } }; const storage = { splits: { trafficTypeExists: () => true } }; - const settings: ISettings = { ...fullSettings, userConsent: undefined }; + const settings: ISettings = { ...fullSettings }; const eventTracker = { track: jest.fn() }; const impressionsTracker = { track: jest.fn() }; diff --git a/src/sdkFactory/__tests__/userConsentProps.spec.ts b/src/sdkFactory/__tests__/userConsentProps.spec.ts new file mode 100644 index 00000000..7a173387 --- /dev/null +++ b/src/sdkFactory/__tests__/userConsentProps.spec.ts @@ -0,0 +1,39 @@ +import { userConsentProps } from '../userConsentProps'; +import { syncTaskFactory } from '../../sync/__tests__/syncTask.mock'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; + +test('userConsentProps', () => { + const settings = { ...fullSettings }; + const syncManager = { submitter: syncTaskFactory() }; + + // @ts-ignore + const props = userConsentProps(settings, syncManager); + + // getUserConsent returns settings.userConsent + expect(props.getUserConsent()).toBe(settings.userConsent); + + // setting user consent to 'granted' + expect(props.setUserConsent(true)).toBe(true); + expect(props.setUserConsent(true)).toBe(true); // calling again has no affect + expect(syncManager.submitter.start).toBeCalledTimes(1); // submitter resumed + expect(syncManager.submitter.stop).toBeCalledTimes(0); + expect(props.getUserConsent()).toBe('granted'); + + // setting user consent to 'declined' + expect(props.setUserConsent(false)).toBe(true); + expect(props.setUserConsent(false)).toBe(true); // calling again has no affect + expect(syncManager.submitter.start).toBeCalledTimes(1); + expect(syncManager.submitter.stop).toBeCalledTimes(1); // submitter paused + expect(props.getUserConsent()).toBe('declined'); + + // Invalid values have no effect + expect(props.setUserConsent('declined')).toBe(false); // strings are not valid + expect(props.setUserConsent('granted')).toBe(false); + expect(props.setUserConsent(undefined)).toBe(false); + expect(props.setUserConsent({})).toBe(false); + + expect(syncManager.submitter.start).toBeCalledTimes(1); + expect(syncManager.submitter.stop).toBeCalledTimes(1); + expect(props.getUserConsent()).toBe('declined'); + +}); diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 5290d5c7..2d645796 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -12,13 +12,14 @@ import { createLoggerAPI } from '../logger/sdkLogger'; import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants'; import { metadataBuilder } from '../storages/metadataBuilder'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../readiness/constants'; +import { objectAssign } from '../utils/lang/objectAssign'; /** * Modular SDK factory */ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.ISDK | SplitIO.IAsyncSDK { - const { settings, platform, storageFactory, splitApiFactory, + const { settings, platform, storageFactory, splitApiFactory, extraProps, syncManagerFactory, SignalListener, impressionsObserverFactory, impressionListener, integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory } = params; const log = settings.log; @@ -88,12 +89,12 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. log.info(NEW_FACTORY); - return { + // @ts-ignore + return objectAssign({ // Split evaluation and event tracking engine client: clientMethod, // Manager API to explore available information - // @ts-ignore manager() { log.debug(RETRIEVE_MANAGER); return managerInstance; @@ -103,5 +104,5 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. Logger: createLoggerAPI(settings.log), settings, - }; + }, extraProps && extraProps(settings, syncManager)); } diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index d907d2d0..842a6068 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -70,4 +70,6 @@ export interface ISdkFactoryParams { // Impression observer factory. If provided, will be used for impressions dedupe impressionsObserverFactory?: () => IImpressionObserver + // Optional function to assign additional properties to the factory instance + extraProps?: (settings: ISettings, syncManager?: ISyncManager) => object } diff --git a/src/sdkFactory/userConsentProps.ts b/src/sdkFactory/userConsentProps.ts new file mode 100644 index 00000000..b9498360 --- /dev/null +++ b/src/sdkFactory/userConsentProps.ts @@ -0,0 +1,37 @@ +import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED } from '../logger/constants'; +import { ISyncManager } from '../sync/types'; +import { ISettings } from '../types'; +import { CONSENT_GRANTED, CONSENT_DECLINED } from '../utils/constants'; + +// Extend client-side factory instances with user consent getter/setter +export function userConsentProps(settings: ISettings, syncManager?: ISyncManager) { + + const log = settings.log; + + return { + setUserConsent(consent: unknown) { + // validate input param + if (typeof consent !== 'boolean') { + log.error(ERROR_NOT_BOOLEAN, ['setUserConsent']); + return false; + } + + const newConsentStatus = consent ? CONSENT_GRANTED : CONSENT_DECLINED; + + if (settings.userConsent !== newConsentStatus) { + // resume/pause submitters + if (consent) syncManager?.submitter?.start(); + else syncManager?.submitter?.stop(); + + log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); + settings.userConsent = newConsentStatus; + } + + return true; + }, + + getUserConsent() { + return settings.userConsent; + } + }; +} diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 0e8f2a64..cd94c9bd 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -11,7 +11,7 @@ jest.mock('../submitters/submitterManager', () => { import { syncManagerOnlineFactory } from '../syncManagerOnline'; test('syncManagerOnline should start or not the submitter depending on user consent status', () => { - const settings: ISettings = { ...fullSettings, userConsent: undefined }; + const settings: ISettings = { ...fullSettings }; // @ts-ignore const syncManager = syncManagerOnlineFactory()({ settings }); From 961f56c5bdcf5e7a82bf18a80852e5728858666a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 24 Feb 2022 16:35:12 -0300 Subject: [PATCH 21/53] constants to uppercase --- src/sdkClient/__tests__/client.spec.ts | 9 ++++----- src/sync/__tests__/syncManagerOnline.spec.ts | 9 ++++----- src/types.ts | 4 ++-- src/utils/constants/index.ts | 6 +++--- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/sdkClient/__tests__/client.spec.ts b/src/sdkClient/__tests__/client.spec.ts index f447d6d3..4112d7d5 100644 --- a/src/sdkClient/__tests__/client.spec.ts +++ b/src/sdkClient/__tests__/client.spec.ts @@ -1,4 +1,3 @@ -import { ISettings } from '../../types'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; import { clientFactory } from '../client'; @@ -6,7 +5,7 @@ test('client should track or not events and impressions depending on user consen const sdkReadinessManager = { readinessManager: { isReady: () => true } }; const storage = { splits: { trafficTypeExists: () => true } }; - const settings: ISettings = { ...fullSettings, userConsent: undefined }; + const settings = { ...fullSettings }; const eventTracker = { track: jest.fn() }; const impressionsTracker = { track: jest.fn() }; @@ -18,19 +17,19 @@ test('client should track or not events and impressions depending on user consen client.track('some_key', 'some_tt', 'some_event'); expect(eventTracker.track).toBeCalledTimes(1); // event should be tracked if userConsent is undefined - settings.userConsent = 'unknown'; + settings.userConsent = 'UNKNOWN'; client.getTreatment('some_key', 'some_split'); expect(impressionsTracker.track).toBeCalledTimes(2); // impression should be tracked if userConsent is unknown client.track('some_key', 'some_tt', 'some_event'); expect(eventTracker.track).toBeCalledTimes(2); // event should be tracked if userConsent is unknown - settings.userConsent = 'granted'; + settings.userConsent = 'GRANTED'; client.getTreatment('some_key', 'some_split'); expect(impressionsTracker.track).toBeCalledTimes(3); // impression should be tracked if userConsent is granted client.track('some_key', 'some_tt', 'some_event'); expect(eventTracker.track).toBeCalledTimes(3); // event should be tracked if userConsent is granted - settings.userConsent = 'declined'; + settings.userConsent = 'DECLINED'; client.getTreatment('some_key', 'some_split'); expect(impressionsTracker.track).toBeCalledTimes(3); // impression should not be tracked if userConsent is declined client.track('some_key', 'some_tt', 'some_event'); diff --git a/src/sync/__tests__/syncManagerOnline.spec.ts b/src/sync/__tests__/syncManagerOnline.spec.ts index 0e8f2a64..c47cad86 100644 --- a/src/sync/__tests__/syncManagerOnline.spec.ts +++ b/src/sync/__tests__/syncManagerOnline.spec.ts @@ -1,4 +1,3 @@ -import { ISettings } from '../../types'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; import { syncTaskFactory } from './syncTask.mock'; @@ -11,7 +10,7 @@ jest.mock('../submitters/submitterManager', () => { import { syncManagerOnlineFactory } from '../syncManagerOnline'; test('syncManagerOnline should start or not the submitter depending on user consent status', () => { - const settings: ISettings = { ...fullSettings, userConsent: undefined }; + const settings = { ...fullSettings }; // @ts-ignore const syncManager = syncManagerOnlineFactory()({ settings }); @@ -22,19 +21,19 @@ test('syncManagerOnline should start or not the submitter depending on user cons syncManager.stop(); expect(submitter.stop).toBeCalledTimes(1); - settings.userConsent = 'unknown'; + settings.userConsent = 'UNKNOWN'; syncManager.start(); expect(submitter.start).toBeCalledTimes(1); // Submitter should not be started if userConsent is unknown syncManager.stop(); expect(submitter.stop).toBeCalledTimes(2); - settings.userConsent = 'granted'; + settings.userConsent = 'GRANTED'; syncManager.start(); expect(submitter.start).toBeCalledTimes(2); // Submitter should be started if userConsent is granted syncManager.stop(); expect(submitter.stop).toBeCalledTimes(3); - settings.userConsent = 'declined'; + settings.userConsent = 'DECLINED'; syncManager.start(); expect(submitter.start).toBeCalledTimes(2); // Submitter should not be started if userConsent is declined syncManager.stop(); diff --git a/src/types.ts b/src/types.ts index 9f67b027..c449ff37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,7 +58,7 @@ export type SDKMode = 'standalone' | 'consumer' | 'localhost' | 'consumer_partia * User consent status. * @typedef {string} ConsentStatus */ -export type ConsentStatus = 'granted' | 'declined' | 'unknown'; +export type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN'; /** * Settings interface. This is a representation of the settings the SDK expose, that's why * most of it's props are readonly. Only features should be rewritten when localhost mode is active. @@ -116,7 +116,7 @@ export interface ISettings { }, readonly log: ILogger readonly impressionListener?: unknown - userConsent?: ConsentStatus + readonly userConsent?: ConsentStatus } /** * Log levels. diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 9b3412a5..243accd6 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -34,6 +34,6 @@ export const STORAGE_REDIS: StorageType = 'REDIS'; export const STORAGE_PLUGGABLE: StorageType = 'PLUGGABLE'; // User consent -export const CONSENT_GRANTED = 'granted'; // The user has granted consent for tracking events and impressions -export const CONSENT_DECLINED = 'declined'; // The user has declined consent for tracking events and impressions -export const CONSENT_UNKNOWN = 'unknown'; // The user has neither granted nor declined consent for tracking events and impressions +export const CONSENT_GRANTED = 'GRANTED'; // The user has granted consent for tracking events and impressions +export const CONSENT_DECLINED = 'DECLINED'; // The user has declined consent for tracking events and impressions +export const CONSENT_UNKNOWN = 'UNKNOWN'; // The user has neither granted nor declined consent for tracking events and impressions From 37cc9fb076ffe2c75597e049efa97f2704eb785b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 24 Feb 2022 16:42:35 -0300 Subject: [PATCH 22/53] constants to uppercase --- src/listeners/__tests__/browser.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/listeners/__tests__/browser.spec.ts b/src/listeners/__tests__/browser.spec.ts index 730c19de..5ec91eff 100644 --- a/src/listeners/__tests__/browser.spec.ts +++ b/src/listeners/__tests__/browser.spec.ts @@ -238,9 +238,9 @@ test('Browser JS listener / standalone mode / user consent status', () => { listener.start(); - settings.userConsent = 'unknown'; + settings.userConsent = 'UNKNOWN'; triggerUnloadEvent(); - settings.userConsent = 'declined'; + settings.userConsent = 'DECLINED'; triggerUnloadEvent(); // Unload event was triggered when user consent was unknown and declined. Thus sendBeacon and post services should not be called @@ -249,7 +249,7 @@ test('Browser JS listener / standalone mode / user consent status', () => { expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); - settings.userConsent = 'granted'; + settings.userConsent = 'GRANTED'; triggerUnloadEvent(); settings.userConsent = undefined; triggerUnloadEvent(); From 9669ad87d1af146b713aa9fe8f0fea0c48b19f43 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 24 Feb 2022 16:51:46 -0300 Subject: [PATCH 23/53] constants to uppercase --- src/logger/messages/error.ts | 2 +- src/logger/messages/info.ts | 2 +- src/sdkFactory/__tests__/userConsentProps.spec.ts | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 4c2b3d70..caa2936d 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -28,7 +28,7 @@ export const codesError: [number, string][] = [ [c.ERROR_INVALID, '%s: you passed an invalid %s. It must be a non-empty string.'], [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'], [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'], - [c.ERROR_NOT_BOOLEAN, '%s: param must be boolean.'], + [c.ERROR_NOT_BOOLEAN, '%s: you must provide a boolean param.'], // initialization / settings validation [c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'], [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'], diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index bac83ea9..4facaf24 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -14,7 +14,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.NEW_FACTORY, ' New Split SDK instance created.'], [c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'], [c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'], - [c.USER_CONSENT_UPDATED, ' User consent changed from %s to %s.'], + [c.USER_CONSENT_UPDATED, 'User consent changed from %s to %s.'], // synchronizer [c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'], diff --git a/src/sdkFactory/__tests__/userConsentProps.spec.ts b/src/sdkFactory/__tests__/userConsentProps.spec.ts index 7a173387..e7f4d0fc 100644 --- a/src/sdkFactory/__tests__/userConsentProps.spec.ts +++ b/src/sdkFactory/__tests__/userConsentProps.spec.ts @@ -12,28 +12,28 @@ test('userConsentProps', () => { // getUserConsent returns settings.userConsent expect(props.getUserConsent()).toBe(settings.userConsent); - // setting user consent to 'granted' + // setting user consent to 'GRANTED' expect(props.setUserConsent(true)).toBe(true); expect(props.setUserConsent(true)).toBe(true); // calling again has no affect expect(syncManager.submitter.start).toBeCalledTimes(1); // submitter resumed expect(syncManager.submitter.stop).toBeCalledTimes(0); - expect(props.getUserConsent()).toBe('granted'); + expect(props.getUserConsent()).toBe('GRANTED'); - // setting user consent to 'declined' + // setting user consent to 'DECLINED' expect(props.setUserConsent(false)).toBe(true); expect(props.setUserConsent(false)).toBe(true); // calling again has no affect expect(syncManager.submitter.start).toBeCalledTimes(1); expect(syncManager.submitter.stop).toBeCalledTimes(1); // submitter paused - expect(props.getUserConsent()).toBe('declined'); + expect(props.getUserConsent()).toBe('DECLINED'); // Invalid values have no effect - expect(props.setUserConsent('declined')).toBe(false); // strings are not valid - expect(props.setUserConsent('granted')).toBe(false); + expect(props.setUserConsent('DECLINED')).toBe(false); // strings are not valid + expect(props.setUserConsent('GRANTED')).toBe(false); expect(props.setUserConsent(undefined)).toBe(false); expect(props.setUserConsent({})).toBe(false); expect(syncManager.submitter.start).toBeCalledTimes(1); expect(syncManager.submitter.stop).toBeCalledTimes(1); - expect(props.getUserConsent()).toBe('declined'); + expect(props.getUserConsent()).toBe('DECLINED'); }); From fb7f3f02837e9aaee0a316f221b48f23c3926bd9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 24 Feb 2022 18:40:51 -0300 Subject: [PATCH 24/53] validate config.userConsent --- src/logger/constants.ts | 2 +- src/logger/messages/error.ts | 2 +- src/sdkFactory/userConsentProps.ts | 2 +- src/utils/settingsValidation/impressionsMode.ts | 15 +++++++-------- src/utils/settingsValidation/index.ts | 6 +++++- src/utils/settingsValidation/types.ts | 2 ++ src/utils/settingsValidation/userConsent.ts | 14 ++++++++++++++ 7 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/utils/settingsValidation/userConsent.ts diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 92428e5b..36002715 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -116,7 +116,7 @@ export const ERROR_INVALID_KEY_OBJECT = 317; export const ERROR_INVALID = 318; export const ERROR_EMPTY = 319; export const ERROR_EMPTY_ARRAY = 320; -export const ERROR_INVALID_IMPRESSIONS_MODE = 321; +export const ERROR_INVALID_CONFIG_PARAM = 321; export const ERROR_HTTP = 322; export const ERROR_LOCALHOST_MODULE_REQUIRED = 323; export const ERROR_STORAGE_INVALID = 324; diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index caa2936d..db97a25a 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -30,7 +30,7 @@ export const codesError: [number, string][] = [ [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'], [c.ERROR_NOT_BOOLEAN, '%s: you must provide a boolean param.'], // initialization / settings validation - [c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'], + [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It must be one of the following values: %s. Defaulting to "%s".'], [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'], [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': The provided storage is invalid.%s Fallbacking into default MEMORY storage'], ]; diff --git a/src/sdkFactory/userConsentProps.ts b/src/sdkFactory/userConsentProps.ts index b9498360..027cef57 100644 --- a/src/sdkFactory/userConsentProps.ts +++ b/src/sdkFactory/userConsentProps.ts @@ -23,7 +23,7 @@ export function userConsentProps(settings: ISettings, syncManager?: ISyncManager if (consent) syncManager?.submitter?.start(); else syncManager?.submitter?.stop(); - log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); + log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); // @ts-ignore, modify readonly prop settings.userConsent = newConsentStatus; } diff --git a/src/utils/settingsValidation/impressionsMode.ts b/src/utils/settingsValidation/impressionsMode.ts index ddcda23d..891f92b9 100644 --- a/src/utils/settingsValidation/impressionsMode.ts +++ b/src/utils/settingsValidation/impressionsMode.ts @@ -1,14 +1,13 @@ -import { ERROR_INVALID_IMPRESSIONS_MODE } from '../../logger/constants'; +import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants'; import { ILogger } from '../../logger/types'; import { SplitIO } from '../../types'; import { DEBUG, OPTIMIZED } from '../constants'; -export function validImpressionsMode(log: ILogger, impressionsMode: string): SplitIO.ImpressionsMode { - impressionsMode = impressionsMode.toUpperCase(); - if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) === -1) { - log.error(ERROR_INVALID_IMPRESSIONS_MODE, [[DEBUG, OPTIMIZED], OPTIMIZED]); - impressionsMode = OPTIMIZED; - } +export function validImpressionsMode(log: ILogger, impressionsMode: any): SplitIO.ImpressionsMode { + if (typeof impressionsMode === 'string') impressionsMode = impressionsMode.toUpperCase(); - return impressionsMode as SplitIO.ImpressionsMode; + if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) > -1) return impressionsMode; + + log.error(ERROR_INVALID_CONFIG_PARAM, ['impressionsMode', [DEBUG, OPTIMIZED], OPTIMIZED]); + return OPTIMIZED; } diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 20bcd1bc..75c0872e 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -97,7 +97,7 @@ function fromSecondsToMillis(n: number) { */ export function settingsValidation(config: unknown, validationParams: ISettingsValidationParams) { - const { defaults, runtime, storage, integrations, logger, localhost } = validationParams; + const { defaults, runtime, storage, integrations, logger, localhost, userConsent } = validationParams; // creates a settings object merging base, defaults and config objects. const withDefaults = merge({}, base, defaults, config) as ISettings; @@ -161,5 +161,9 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV // ensure a valid impressionsMode withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode); + // ensure a valid user consent value + // @ts-ignore, modify readonly prop + withDefaults.userConsent = userConsent(withDefaults); + return withDefaults; } diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index b305d440..e8f954b5 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -20,4 +20,6 @@ export interface ISettingsValidationParams { logger: (settings: ISettings) => ISettings['log'], /** Localhost mode validator (`settings.sync.localhostMode`) */ localhost?: (settings: ISettings) => ISettings['sync']['localhostMode'], + /** User consent validator (`settings.userConsent`) */ + userConsent: (settings: ISettings) => ISettings['userConsent'], } diff --git a/src/utils/settingsValidation/userConsent.ts b/src/utils/settingsValidation/userConsent.ts new file mode 100644 index 00000000..7f16b351 --- /dev/null +++ b/src/utils/settingsValidation/userConsent.ts @@ -0,0 +1,14 @@ +import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants'; +import { ILogger } from '../../logger/types'; +import { CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN } from '../constants'; + +const userConsentValues = [CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN]; + +export function validateUserConsent({ userConsent, log }: { userConsent: any, log: ILogger }) { + if (typeof userConsent === 'string') userConsent = userConsent.toUpperCase(); + + if (userConsentValues.indexOf(userConsent) > -1) return userConsent; + + log.error(ERROR_INVALID_CONFIG_PARAM, ['userConsent', userConsentValues, CONSENT_GRANTED]); + return CONSENT_GRANTED; +} From 14105d8a96ad7c273c0ec5cb25b7b365d9eff7f5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 24 Feb 2022 18:44:54 -0300 Subject: [PATCH 25/53] fixed type check issue --- src/sdkFactory/userConsentProps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdkFactory/userConsentProps.ts b/src/sdkFactory/userConsentProps.ts index b9498360..027cef57 100644 --- a/src/sdkFactory/userConsentProps.ts +++ b/src/sdkFactory/userConsentProps.ts @@ -23,7 +23,7 @@ export function userConsentProps(settings: ISettings, syncManager?: ISyncManager if (consent) syncManager?.submitter?.start(); else syncManager?.submitter?.stop(); - log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); + log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); // @ts-ignore, modify readonly prop settings.userConsent = newConsentStatus; } From 9e71ee2af4da837cb41f28ff4ee385d038bbb182 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 24 Feb 2022 19:10:52 -0300 Subject: [PATCH 26/53] fixed test --- src/utils/settingsValidation/__tests__/index.spec.ts | 3 ++- src/utils/settingsValidation/{userConsent.ts => consent.ts} | 2 +- src/utils/settingsValidation/index.ts | 4 ++-- src/utils/settingsValidation/types.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) rename src/utils/settingsValidation/{userConsent.ts => consent.ts} (85%) diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index be0087b5..50b960a9 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -19,7 +19,8 @@ const minimalSettingsParams = { version: 'javascript-test', }, runtime: () => ({ ip: false, hostname: false } as ISettings['runtime']), - logger: () => (loggerMock as ISettings['log']) + logger: () => (loggerMock as ISettings['log']), + consent: () => undefined }; describe('settingsValidation', () => { diff --git a/src/utils/settingsValidation/userConsent.ts b/src/utils/settingsValidation/consent.ts similarity index 85% rename from src/utils/settingsValidation/userConsent.ts rename to src/utils/settingsValidation/consent.ts index 7f16b351..1cf9233b 100644 --- a/src/utils/settingsValidation/userConsent.ts +++ b/src/utils/settingsValidation/consent.ts @@ -4,7 +4,7 @@ import { CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN } from '../constants const userConsentValues = [CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN]; -export function validateUserConsent({ userConsent, log }: { userConsent: any, log: ILogger }) { +export function validateConsent({ userConsent, log }: { userConsent: any, log: ILogger }) { if (typeof userConsent === 'string') userConsent = userConsent.toUpperCase(); if (userConsentValues.indexOf(userConsent) > -1) return userConsent; diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 75c0872e..d7a31c10 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -97,7 +97,7 @@ function fromSecondsToMillis(n: number) { */ export function settingsValidation(config: unknown, validationParams: ISettingsValidationParams) { - const { defaults, runtime, storage, integrations, logger, localhost, userConsent } = validationParams; + const { defaults, runtime, storage, integrations, logger, localhost, consent } = validationParams; // creates a settings object merging base, defaults and config objects. const withDefaults = merge({}, base, defaults, config) as ISettings; @@ -163,7 +163,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV // ensure a valid user consent value // @ts-ignore, modify readonly prop - withDefaults.userConsent = userConsent(withDefaults); + withDefaults.userConsent = consent(withDefaults); return withDefaults; } diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index e8f954b5..40cd155c 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -21,5 +21,5 @@ export interface ISettingsValidationParams { /** Localhost mode validator (`settings.sync.localhostMode`) */ localhost?: (settings: ISettings) => ISettings['sync']['localhostMode'], /** User consent validator (`settings.userConsent`) */ - userConsent: (settings: ISettings) => ISettings['userConsent'], + consent: (settings: ISettings) => ISettings['userConsent'], } From 3dc3638150df006358395c1bc1f5fc38f02484a6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 25 Feb 2022 19:20:53 -0300 Subject: [PATCH 27/53] localstorage update --- src/storages/KeyBuilderCS.ts | 14 +++++++- .../inLocalStorage/MySegmentsCacheInLocal.ts | 26 +++++++++++++-- .../__tests__/MySegmentsCacheInLocal.spec.ts | 32 +++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index d4f9e902..58c19fbb 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -16,10 +16,22 @@ export class KeyBuilderCS extends KeyBuilder { * @override */ buildSegmentNameKey(segmentName: string) { - return `${this.matchingKey}.${this.prefix}.segment.${segmentName}`; + return `${this.prefix}.${this.matchingKey}.segment.${segmentName}`; } extractSegmentName(builtSegmentKeyName: string) { + const prefix = `${this.prefix}.${this.matchingKey}.segment.`; + + if (startsWith(builtSegmentKeyName, prefix)) + return builtSegmentKeyName.substr(prefix.length); + } + + // @BREAKING: The key used to start with the matching key instead of the prefix, this was changed on version 10.17.3 + buildOldSegmentNameKey(segmentName: string) { + return `${this.matchingKey}.${this.prefix}.segment.${segmentName}`; + } + // @BREAKING: The key used to start with the matching key instead of the prefix, this was changed on version 10.17.3 + extractOldSegmentKey(builtSegmentKeyName: string) { const prefix = `${this.matchingKey}.${this.prefix}.segment.`; if (startsWith(builtSegmentKeyName, prefix)) diff --git a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts index 7bc6690a..627d1fcf 100644 --- a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts @@ -67,9 +67,29 @@ export class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { // Scan current values from localStorage const storedSegmentNames = Object.keys(localStorage).reduce((accum, key) => { - const name = this.keys.extractSegmentName(key); - - if (name) accum.push(name); + let segmentName = this.keys.extractSegmentName(key); + + if (segmentName) { + accum.push(segmentName); + } else { + // @BREAKING: This is only to clean up "old" keys. Remove this whole else code block. + segmentName = this.keys.extractOldSegmentKey(key); + + if (segmentName) { // this was an old segment key, let's clean up. + const newSegmentKey = this.keys.buildSegmentNameKey(segmentName); + try { + // If the new format key is not there, create it. + if (!localStorage.getItem(newSegmentKey) && names.indexOf(segmentName) > -1) { + localStorage.setItem(newSegmentKey, DEFINED); + // we are migrating a segment, let's track it. + accum.push(segmentName); + } + localStorage.removeItem(key); // we migrated the current key, let's delete it. + } catch (e) { + this.log.error(e); + } + } + } return accum; }, [] as string[]); diff --git a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts index 36072550..67c9efcc 100644 --- a/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/MySegmentsCacheInLocal.spec.ts @@ -17,3 +17,35 @@ test('SEGMENT CACHE / in LocalStorage', () => { expect(cache.isInSegment('mocked-segment') === false).toBe(true); }); + +// @BREAKING: REMOVE when removing this backwards compatibility. +test('SEGMENT CACHE / in LocalStorage migration for mysegments keys', () => { + + const keys = new KeyBuilderCS('LS_BC_test.SPLITIO', 'test_nico'); + const cache = new MySegmentsCacheInLocal(loggerMock, keys); + + const oldKey1 = 'test_nico.LS_BC_test.SPLITIO.segment.segment1'; + const oldKey2 = 'test_nico.LS_BC_test.SPLITIO.segment.segment2'; + const newKey1 = keys.buildSegmentNameKey('segment1'); + const newKey2 = keys.buildSegmentNameKey('segment2'); + + cache.clear(); // cleanup before starting. + + // Not adding a full suite for LS keys now, testing here + expect(oldKey1).toBe(keys.buildOldSegmentNameKey('segment1')); + expect('segment1').toBe(keys.extractOldSegmentKey(oldKey1)); + + // add two segments, one we don't want to send on reset, should only be cleared, other one will be migrated. + localStorage.setItem(oldKey1, '1'); + localStorage.setItem(oldKey2, '1'); + expect(localStorage.getItem(newKey1)).toBe(null); // control assertion + + cache.resetSegments(['segment1']); + + expect(localStorage.getItem(newKey1)).toBe('1'); // The segment key for segment1, as is part of the new list, should be migrated. + expect(localStorage.getItem(newKey2)).toBe(null); // The segment key for segment2 should not be migrated. + expect(localStorage.getItem(oldKey1)).toBe(null); // Old keys are removed. + expect(localStorage.getItem(oldKey2)).toBe(null); // Old keys are removed. + + cache.clear(); +}); From a16b6032517792aeb88fb057c18e8a246fcf96f2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 2 Mar 2022 13:26:59 -0300 Subject: [PATCH 28/53] prepare rc --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8abb07d..ea2edb24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.3", + "version": "1.2.1-rc.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2bed38b7..719eb3f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.3", + "version": "1.2.1-rc.5", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From a4bf0faf04d1d2645eed9193d53723dc375ea94f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Mar 2022 12:14:44 -0300 Subject: [PATCH 29/53] update validatePrefix function, for consistency with JS SDK --- src/storages/KeyBuilder.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 080ed3d9..3c54d403 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -1,15 +1,11 @@ -import { endsWith, startsWith } from '../utils/lang'; +import { startsWith } from '../utils/lang'; const everythingAtTheEnd = /[^.]+$/; const DEFAULT_PREFIX = 'SPLITIO'; export function validatePrefix(prefix: unknown) { - return prefix && typeof prefix === 'string' ? - endsWith(prefix, '.' + DEFAULT_PREFIX) ? - prefix : // suffix already appended - prefix + '.' + DEFAULT_PREFIX : // append suffix - DEFAULT_PREFIX; // use default prefix if none is provided + return prefix ? prefix + '.SPLITIO' : 'SPLITIO'; } export class KeyBuilder { From 0e36a90df20e0d3afde09f6b35f8d6913a8bf43c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Mar 2022 16:55:38 -0300 Subject: [PATCH 30/53] feedback and polishing --- src/logger/constants.ts | 1 + src/logger/messages/debug.ts | 6 +++--- src/logger/messages/error.ts | 4 ++-- src/logger/messages/info.ts | 7 ++++--- src/sdkFactory/userConsentProps.ts | 21 +++++++++++-------- src/utils/lang/index.ts | 9 +++++++- src/utils/settingsValidation/consent.ts | 3 ++- .../settingsValidation/impressionsMode.ts | 3 ++- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 36002715..68259646 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -69,6 +69,7 @@ export const SYNC_STOP_POLLING = 119; export const EVENTS_TRACKER_SUCCESS = 120; export const IMPRESSIONS_TRACKER_SUCCESS = 121; export const USER_CONSENT_UPDATED = 122; +export const USER_CONSENT_NOT_UPDATED = 123; export const ENGINE_VALUE_INVALID = 200; export const ENGINE_VALUE_NO_ATTRIBUTES = 201; diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts index ed8281ed..c57ea6e0 100644 --- a/src/logger/messages/debug.ts +++ b/src/logger/messages/debug.ts @@ -31,9 +31,9 @@ export const codesDebug: [number, string][] = codesInfo.concat([ // SDK [c.CLEANUP_REGISTERING, c.LOG_PREFIX_CLEANUP + 'Registering cleanup handler %s'], [c.CLEANUP_DEREGISTERING, c.LOG_PREFIX_CLEANUP + 'Deregistering cleanup handler %s'], - [c.RETRIEVE_CLIENT_DEFAULT, ' Retrieving default SDK client.'], - [c.RETRIEVE_CLIENT_EXISTING, ' Retrieving existing SDK client.'], - [c.RETRIEVE_MANAGER, ' Retrieving manager instance.'], + [c.RETRIEVE_CLIENT_DEFAULT, 'Retrieving default SDK client.'], + [c.RETRIEVE_CLIENT_EXISTING, 'Retrieving existing SDK client.'], + [c.RETRIEVE_MANAGER, 'Retrieving manager instance.'], // synchronizer [c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Splits data: \n%s'], [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up split update using since = %s'], diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 555bd1d6..8ef9065b 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -5,7 +5,7 @@ export const codesError: [number, string][] = [ [c.ERROR_ENGINE_COMBINER_IFELSEIF, c.LOG_PREFIX_ENGINE_COMBINER + 'Invalid Split, no valid rules found'], // SDK [c.ERROR_LOGLEVEL_INVALID, 'logger: Invalid Log Level - No changes to the logs will be applied.'], - [c.ERROR_CLIENT_CANNOT_GET_READY, ' The SDK will not get ready. Reason: %s'], + [c.ERROR_CLIENT_CANNOT_GET_READY, 'The SDK will not get ready. Reason: %s'], [c.ERROR_IMPRESSIONS_TRACKER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Could not store impressions bulk with %s impression(s). Error: %s'], [c.ERROR_IMPRESSIONS_LISTENER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Impression listener logImpression method threw: %s.'], [c.ERROR_EVENTS_TRACKER, c.LOG_PREFIX_EVENTS_TRACKER + 'Failed to queue %s'], @@ -28,7 +28,7 @@ export const codesError: [number, string][] = [ [c.ERROR_INVALID, '%s: you passed an invalid %s. It must be a non-empty string.'], [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'], [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'], - [c.ERROR_NOT_BOOLEAN, '%s: you must provide a boolean param.'], + [c.ERROR_NOT_BOOLEAN, '%s: provided param must be a boolean value.'], // initialization / settings validation [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'], [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'], diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 4facaf24..9f4a4254 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -10,11 +10,12 @@ export const codesInfo: [number, string][] = codesWarn.concat([ // SDK [c.IMPRESSION, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Split: %s. Key: %s. Evaluation: %s. Label: %s'], [c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Queueing corresponding impression.'], - [c.NEW_SHARED_CLIENT, ' New shared client instance created.'], - [c.NEW_FACTORY, ' New Split SDK instance created.'], + [c.NEW_SHARED_CLIENT, 'New shared client instance created.'], + [c.NEW_FACTORY, 'New Split SDK instance created.'], [c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'], [c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'], - [c.USER_CONSENT_UPDATED, 'User consent changed from %s to %s.'], + [c.USER_CONSENT_UPDATED, 'setUserConsent: consent status changed from %s to %s.'], + [c.USER_CONSENT_NOT_UPDATED, 'setUserConsent: call had no effect because it was the current consent status (%s).'], // synchronizer [c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'], diff --git a/src/sdkFactory/userConsentProps.ts b/src/sdkFactory/userConsentProps.ts index 027cef57..da715c33 100644 --- a/src/sdkFactory/userConsentProps.ts +++ b/src/sdkFactory/userConsentProps.ts @@ -1,7 +1,8 @@ -import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED } from '../logger/constants'; +import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED, USER_CONSENT_NOT_UPDATED } from '../logger/constants'; import { ISyncManager } from '../sync/types'; import { ISettings } from '../types'; import { CONSENT_GRANTED, CONSENT_DECLINED } from '../utils/constants'; +import { isBoolean } from '../utils/lang'; // Extend client-side factory instances with user consent getter/setter export function userConsentProps(settings: ISettings, syncManager?: ISyncManager) { @@ -11,20 +12,22 @@ export function userConsentProps(settings: ISettings, syncManager?: ISyncManager return { setUserConsent(consent: unknown) { // validate input param - if (typeof consent !== 'boolean') { - log.error(ERROR_NOT_BOOLEAN, ['setUserConsent']); + if (!isBoolean(consent)) { + log.warn(ERROR_NOT_BOOLEAN, ['setUserConsent']); return false; } const newConsentStatus = consent ? CONSENT_GRANTED : CONSENT_DECLINED; - if (settings.userConsent !== newConsentStatus) { - // resume/pause submitters - if (consent) syncManager?.submitter?.start(); - else syncManager?.submitter?.stop(); - - log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); // @ts-ignore, modify readonly prop + if (settings.userConsent !== newConsentStatus) { // @ts-ignore, modify readonly prop settings.userConsent = newConsentStatus; + + if (consent) syncManager?.submitter?.start(); // resumes submitters if transitioning to GRANTED + else syncManager?.submitter?.stop(); // pauses submitters if transitioning to DECLINED + + log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); + } else { + log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]); } return true; diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index f894f652..2a7332ff 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -89,7 +89,7 @@ export function get(obj: any, prop: any, val: any): any { /** * Parses an array into a map of different arrays, grouping by the specified prop value. */ -export function groupBy >(source: T[], prop: string): Record { +export function groupBy>(source: T[], prop: string): Record { const map: Record = {}; if (Array.isArray(source) && isString(prop)) { @@ -164,6 +164,13 @@ export function isString(val: any): val is string { return typeof val === 'string' || val instanceof String; } +/** + * String sanitizer. Returns the provided value converted to uppercase if it is a string. + */ +export function stringToUpperCase(val: any) { + return isString(val) ? val.toUpperCase() : val; +} + /** * Deep copy version of Object.assign using recursion. * There are some assumptions here. It's for internal use and we don't need verbose errors diff --git a/src/utils/settingsValidation/consent.ts b/src/utils/settingsValidation/consent.ts index 1cf9233b..c68cf222 100644 --- a/src/utils/settingsValidation/consent.ts +++ b/src/utils/settingsValidation/consent.ts @@ -1,11 +1,12 @@ import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants'; import { ILogger } from '../../logger/types'; import { CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN } from '../constants'; +import { stringToUpperCase } from '../lang'; const userConsentValues = [CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN]; export function validateConsent({ userConsent, log }: { userConsent: any, log: ILogger }) { - if (typeof userConsent === 'string') userConsent = userConsent.toUpperCase(); + userConsent = stringToUpperCase(userConsent); if (userConsentValues.indexOf(userConsent) > -1) return userConsent; diff --git a/src/utils/settingsValidation/impressionsMode.ts b/src/utils/settingsValidation/impressionsMode.ts index 891f92b9..9fdc3e09 100644 --- a/src/utils/settingsValidation/impressionsMode.ts +++ b/src/utils/settingsValidation/impressionsMode.ts @@ -2,9 +2,10 @@ import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants'; import { ILogger } from '../../logger/types'; import { SplitIO } from '../../types'; import { DEBUG, OPTIMIZED } from '../constants'; +import { stringToUpperCase } from '../lang'; export function validImpressionsMode(log: ILogger, impressionsMode: any): SplitIO.ImpressionsMode { - if (typeof impressionsMode === 'string') impressionsMode = impressionsMode.toUpperCase(); + impressionsMode = stringToUpperCase(impressionsMode); if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) > -1) return impressionsMode; From 002912f2678c57490a1e100e5511f1157d94a5de Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 4 Mar 2022 16:26:23 -0300 Subject: [PATCH 31/53] fixed issue with event submitter push window --- package-lock.json | 2 +- package.json | 2 +- .../__tests__/eventsSyncTask.spec.ts | 61 +++++++++++++++++++ src/sync/submitters/eventsSyncTask.ts | 9 ++- 4 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/sync/submitters/__tests__/eventsSyncTask.spec.ts diff --git a/package-lock.json b/package-lock.json index ea2edb24..9c0208f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.5", + "version": "1.2.1-rc.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 719eb3f6..3bef4bea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.5", + "version": "1.2.1-rc.6", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/submitters/__tests__/eventsSyncTask.spec.ts b/src/sync/submitters/__tests__/eventsSyncTask.spec.ts new file mode 100644 index 00000000..59b647c1 --- /dev/null +++ b/src/sync/submitters/__tests__/eventsSyncTask.spec.ts @@ -0,0 +1,61 @@ +import { eventsSyncTaskFactory } from '../eventsSyncTask'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; + + + +describe('Events submitter (eventsSyncTask)', () => { + + let __onFullQueueCb: () => void; + const postEventsBulkMock = jest.fn(); + const eventsCacheMock = { + isEmpty: jest.fn(() => true), + setOnFullQueueCb: jest.fn(function (onFullQueueCb) { __onFullQueueCb = onFullQueueCb; }) + }; + + beforeEach(() => { + eventsCacheMock.isEmpty.mockClear(); + }); + + test('with eventsFirstPushWindow', async () => { + const eventsFirstPushWindow = 20; // @ts-ignore + const eventsSubmitter = eventsSyncTaskFactory(loggerMock, postEventsBulkMock, eventsCacheMock, 30000, eventsFirstPushWindow); + + eventsSubmitter.start(); + expect(eventsSubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running + expect(eventsSubmitter.isExecuting()).toEqual(false); // but not executed immediatelly if there is a push window + expect(eventsCacheMock.isEmpty).not.toBeCalled(); + + // If queue is full, submitter should be executed + __onFullQueueCb(); + expect(eventsSubmitter.isExecuting()).toEqual(true); + expect(eventsCacheMock.isEmpty).toBeCalledTimes(1); + + // Await first push window + await new Promise(res => setTimeout(res, eventsFirstPushWindow + 10)); + expect(eventsCacheMock.isEmpty).toBeCalledTimes(2); // after the push window, submitter should have been executed + + expect(eventsSubmitter.isRunning()).toEqual(true); + eventsSubmitter.stop(); + expect(eventsSubmitter.isRunning()).toEqual(false); + }); + + test('without eventsFirstPushWindow', async () => { + // @ts-ignore + const eventsSubmitter = eventsSyncTaskFactory(loggerMock, postEventsBulkMock, eventsCacheMock, 30000); + + eventsSubmitter.start(); + expect(eventsSubmitter.isRunning()).toEqual(true); // Submitter should be flagged as running + expect(eventsSubmitter.isExecuting()).toEqual(true); // and executes immediatelly if there isn't a push window + expect(eventsCacheMock.isEmpty).toBeCalledTimes(1); + + // If queue is full, submitter should be executed + __onFullQueueCb(); + expect(eventsSubmitter.isExecuting()).toEqual(true); + expect(eventsCacheMock.isEmpty).toBeCalledTimes(2); + + expect(eventsSubmitter.isRunning()).toEqual(true); + eventsSubmitter.stop(); + expect(eventsSubmitter.isRunning()).toEqual(false); + }); + +}); diff --git a/src/sync/submitters/eventsSyncTask.ts b/src/sync/submitters/eventsSyncTask.ts index b87ee24e..7c84374b 100644 --- a/src/sync/submitters/eventsSyncTask.ts +++ b/src/sync/submitters/eventsSyncTask.ts @@ -22,18 +22,25 @@ export function eventsSyncTaskFactory( // don't retry events. const syncTask = submitterSyncTaskFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME, latencyTracker); - // Set a timer for the first push of events, + // Set a timer for the first push window of events. + // Not implemented in the base submitter or sync task, since this feature is only used by the events submitter. if (eventsFirstPushWindow > 0) { + let running = false; let stopEventPublisherTimeout: ReturnType; const originalStart = syncTask.start; syncTask.start = () => { + running = true; stopEventPublisherTimeout = setTimeout(originalStart, eventsFirstPushWindow); }; const originalStop = syncTask.stop; syncTask.stop = () => { + running = false; clearTimeout(stopEventPublisherTimeout); originalStop(); }; + syncTask.isRunning = () => { + return running; + }; } // register events submitter to be executed when events cache is full From 8255eaeab8075a606d264318b7449aff7116f8de Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 8 Mar 2022 12:08:38 -0300 Subject: [PATCH 32/53] validate undefined authorizationKey in SSEClient --- package-lock.json | 2 +- package.json | 2 +- src/sync/streaming/SSEClient/index.ts | 3 ++- src/utils/settingsValidation/consent.ts | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c0208f7..36876610 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.6", + "version": "1.2.1-rc.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3bef4bea..03f9ec7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.6", + "version": "1.2.1-rc.7", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/streaming/SSEClient/index.ts b/src/sync/streaming/SSEClient/index.ts index 378ac9c0..7c0dfb6e 100644 --- a/src/sync/streaming/SSEClient/index.ts +++ b/src/sync/streaming/SSEClient/index.ts @@ -1,5 +1,6 @@ import { IEventSourceConstructor } from '../../../services/types'; import { ISettings } from '../../../types'; +import { isString } from '../../../utils/lang'; import { IAuthTokenPushEnabled } from '../AuthClient/types'; import { ISSEClient, ISseEventHandler } from './types'; @@ -15,7 +16,7 @@ const CONTROL_CHANNEL_REGEX = /^control_/; */ function buildSSEHeaders(settings: ISettings) { const headers: Record = { - SplitSDKClientKey: settings.core.authorizationKey.slice(-4), + SplitSDKClientKey: isString(settings.core.authorizationKey) ? settings.core.authorizationKey.slice(-4) : '', SplitSDKVersion: settings.version, }; diff --git a/src/utils/settingsValidation/consent.ts b/src/utils/settingsValidation/consent.ts index c68cf222..98b4112b 100644 --- a/src/utils/settingsValidation/consent.ts +++ b/src/utils/settingsValidation/consent.ts @@ -1,11 +1,12 @@ import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants'; import { ILogger } from '../../logger/types'; +import { ConsentStatus } from '../../types'; import { CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN } from '../constants'; import { stringToUpperCase } from '../lang'; const userConsentValues = [CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN]; -export function validateConsent({ userConsent, log }: { userConsent: any, log: ILogger }) { +export function validateConsent({ userConsent, log }: { userConsent?: any, log: ILogger }): ConsentStatus { userConsent = stringToUpperCase(userConsent); if (userConsentValues.indexOf(userConsent) > -1) return userConsent; From 9ab79d2b2dc478787df0c38cea11c4742771cc3c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 8 Mar 2022 17:04:03 -0300 Subject: [PATCH 33/53] move consent check from client to trackers --- package-lock.json | 2 +- package.json | 2 +- src/sdkClient/__tests__/client.spec.ts | 38 -------- src/sdkClient/client.ts | 9 +- src/sdkClient/clientInputValidation.ts | 15 +-- src/sdkClient/sdkClient.ts | 7 +- src/sdkFactory/index.ts | 6 +- src/sdkFactory/types.ts | 1 - src/trackers/__tests__/eventTracker.spec.ts | 93 +++++++++++-------- .../__tests__/impressionsTracker.spec.ts | 89 +++++++++++------- src/trackers/eventTracker.ts | 14 ++- src/trackers/impressionObserver/utils.ts | 9 +- src/trackers/impressionsTracker.ts | 15 ++- 13 files changed, 154 insertions(+), 146 deletions(-) delete mode 100644 src/sdkClient/__tests__/client.spec.ts diff --git a/package-lock.json b/package-lock.json index 36876610..0a014819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.7", + "version": "1.2.1-rc.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 03f9ec7a..63b45dc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.7", + "version": "1.2.1-rc.8", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkClient/__tests__/client.spec.ts b/src/sdkClient/__tests__/client.spec.ts deleted file mode 100644 index 4112d7d5..00000000 --- a/src/sdkClient/__tests__/client.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; -import { clientFactory } from '../client'; - -test('client should track or not events and impressions depending on user consent status', () => { - const sdkReadinessManager = { readinessManager: { isReady: () => true } }; - const storage = { splits: { trafficTypeExists: () => true } }; - - const settings = { ...fullSettings }; - const eventTracker = { track: jest.fn() }; - const impressionsTracker = { track: jest.fn() }; - - // @ts-ignore - const client = clientFactory({ settings, eventTracker, impressionsTracker, sdkReadinessManager, storage }); - - client.getTreatment('some_key', 'some_split'); - expect(impressionsTracker.track).toBeCalledTimes(1); // impression should be tracked if userConsent is undefined - client.track('some_key', 'some_tt', 'some_event'); - expect(eventTracker.track).toBeCalledTimes(1); // event should be tracked if userConsent is undefined - - settings.userConsent = 'UNKNOWN'; - client.getTreatment('some_key', 'some_split'); - expect(impressionsTracker.track).toBeCalledTimes(2); // impression should be tracked if userConsent is unknown - client.track('some_key', 'some_tt', 'some_event'); - expect(eventTracker.track).toBeCalledTimes(2); // event should be tracked if userConsent is unknown - - settings.userConsent = 'GRANTED'; - client.getTreatment('some_key', 'some_split'); - expect(impressionsTracker.track).toBeCalledTimes(3); // impression should be tracked if userConsent is granted - client.track('some_key', 'some_tt', 'some_event'); - expect(eventTracker.track).toBeCalledTimes(3); // event should be tracked if userConsent is granted - - settings.userConsent = 'DECLINED'; - client.getTreatment('some_key', 'some_split'); - expect(impressionsTracker.track).toBeCalledTimes(3); // impression should not be tracked if userConsent is declined - client.track('some_key', 'some_tt', 'some_event'); - expect(eventTracker.track).toBeCalledTimes(3); // event should not be tracked if userConsent is declined - -}); diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 7d5ab72c..3ef41ec6 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -4,7 +4,7 @@ import { getMatching, getBucketing } from '../utils/key'; import { validateSplitExistance } from '../utils/inputValidation/splitExistance'; import { validateTrafficTypeExistance } from '../utils/inputValidation/trafficTypeExistance'; import { SDK_NOT_READY } from '../utils/labels'; -import { CONSENT_DECLINED, CONTROL } from '../utils/constants'; +import { CONTROL } from '../utils/constants'; import { IClientFactoryParams } from './types'; import { IEvaluationResult } from '../evaluator/types'; import { SplitIO, ImpressionDTO } from '../types'; @@ -23,7 +23,7 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S const wrapUp = (evaluationResult: IEvaluationResult) => { const queue: ImpressionDTO[] = []; const treatment = processEvaluation(evaluationResult, splitName, key, attributes, withConfig, `getTreatment${withConfig ? 'withConfig' : ''}`, queue); - if (settings.userConsent !== CONSENT_DECLINED) impressionsTracker.track(queue, attributes); + impressionsTracker.track(queue, attributes); return treatment; }; @@ -43,7 +43,7 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S Object.keys(evaluationResults).forEach(splitName => { treatments[splitName] = processEvaluation(evaluationResults[splitName], splitName, key, attributes, withConfig, `getTreatments${withConfig ? 'withConfig' : ''}`, queue); }); - if (settings.userConsent !== CONSENT_DECLINED) impressionsTracker.track(queue, attributes); + impressionsTracker.track(queue, attributes); return treatments; }; @@ -116,8 +116,7 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S // 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(log, readinessManager, storage.splits, mode, trafficTypeName, 'track'); - if (settings.userConsent !== CONSENT_DECLINED) return eventTracker.track(eventData, size); - else return false; + return eventTracker.track(eventData, size); } return { diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index df0eaebc..de9f3594 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -15,14 +15,17 @@ import { startsWith } from '../utils/lang'; 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'; +import { ISettings, SplitIO } from '../types'; +import { isStorageSync } from '../trackers/impressionObserver/utils'; /** * 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 function clientInputValidationDecorator(log: ILogger, client: TClient, readinessManager: IReadinessManager, isStorageSync = false): TClient { +export function clientInputValidationDecorator(settings: ISettings, client: TClient, readinessManager: IReadinessManager): TClient { + + const log = settings.log; + const isSync = isStorageSync(settings); /** * Avoid repeating this validations code @@ -47,8 +50,7 @@ export function clientInputValidationDecorator(value: T): MaybeThenable { - if (isStorageSync) return value; - return Promise.resolve(value); + return isSync ? value : Promise.resolve(value); } function getTreatment(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) { @@ -108,8 +110,7 @@ export function clientInputValidationDecorator ISignalListener, // Used by BrowserSignalListener // @TODO review impressionListener and integrations interfaces. What about handling impressionListener as an integration ? - impressionListener?: SplitIO.IImpressionListener, integrationsManagerFactory?: (params: IIntegrationFactoryParams) => IIntegrationManager | undefined, // Impression observer factory. If provided, will be used for impressions dedupe diff --git a/src/trackers/__tests__/eventTracker.spec.ts b/src/trackers/__tests__/eventTracker.spec.ts index aa2cd234..d770d899 100644 --- a/src/trackers/__tests__/eventTracker.spec.ts +++ b/src/trackers/__tests__/eventTracker.spec.ts @@ -1,5 +1,5 @@ -import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import { SplitIO } from '../../types'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; import { eventTrackerFactory } from '../eventTracker'; /* Mocks */ @@ -12,30 +12,30 @@ const fakeIntegrationsManager = { handleEvent: jest.fn() }; +const fakeEvent = { + eventTypeId: 'eventTypeId', + trafficTypeName: 'trafficTypeName', + value: 0, + timestamp: Date.now(), + key: 'matchingKey', + properties: { + prop1: 'prop1', + prop2: 0, + } +}; + /* Tests */ 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(loggerMock, fakeEventsCache, fakeIntegrationsManager); + const instance = eventTrackerFactory(fullSettings, fakeEventsCache, fakeIntegrationsManager); expect(typeof instance.track).toBe('function'); // The instance should implement the track method. }); - test('Propagate the event into the event cache and integrations manager, and return its result (a boolean or a promise that resolves to boolean)', (done) => { - const fakeEvent = { - eventTypeId: 'eventTypeId', - trafficTypeName: 'trafficTypeName', - value: 0, - timestamp: Date.now(), - key: 'matchingKey', - properties: { - prop1: 'prop1', - prop2: 0, - } - }; - + test('Propagate the event into the event cache and integrations manager, and return its result (a boolean or a promise that resolves to boolean)', async () => { fakeEventsCache.track.mockImplementation((eventData: SplitIO.EventData, size: number) => { if (eventData === fakeEvent) { switch (size) { @@ -46,46 +46,61 @@ describe('Event Tracker', () => { } }); - const tracker = eventTrackerFactory(loggerMock, fakeEventsCache, fakeIntegrationsManager); + const tracker = eventTrackerFactory(fullSettings, 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).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(() => { - expect(fakeIntegrationsManager.handleEvent.mock.calls[0]).toEqual([fakeEvent]); // A copy of the tracked event should be sent to integration manager after the timeout wrapping make it to the queue stack. - expect(fakeIntegrationsManager.handleEvent.mock.calls[0][0]).not.toBe(fakeEvent); // Should not send the original event. + await new Promise(res => setTimeout(res)); + expect(fakeIntegrationsManager.handleEvent.mock.calls[0]).toEqual([fakeEvent]); // A copy of the tracked event should be sent to integration manager after the timeout wrapping make it to the queue stack. + expect(fakeIntegrationsManager.handleEvent.mock.calls[0][0]).not.toBe(fakeEvent); // Should not send the original event. + + const result2 = tracker.track(fakeEvent, 2) as Promise; - const result2 = tracker.track(fakeEvent, 2) as Promise; + expect(fakeEventsCache.track.mock.calls[1]).toEqual([fakeEvent, 2]); // Should be present in the event cache. - expect(fakeEventsCache.track.mock.calls[1]).toEqual([fakeEvent, 2]); // Should be present in the event cache. + let tracked = await result2; + expect(tracked).toBe(false); // Should return the value of the event cache resolved promise. - result2.then(tracked => { - expect(tracked).toBe(false); // Should return the value of the event cache resolved promise. + await new Promise(res => setTimeout(res)); + expect(fakeIntegrationsManager.handleEvent).toBeCalledTimes(1); // Untracked event should not be sent to integration manager. - setTimeout(() => { - 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. + + tracked = await result3; + 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. + + await new Promise(res => setTimeout(res)); + expect(fakeIntegrationsManager.handleEvent.mock.calls[1]).toEqual([fakeEvent]); // A copy of tracked event should be sent to integration manager after the timeout wrapping make it to the queue stack. + expect(fakeIntegrationsManager.handleEvent.mock.calls[1][0]).not.toBe(fakeEvent); // Should not send the original event. + + }); - const result3 = tracker.track(fakeEvent, 3) as Promise; + test('Should track or not events depending on user consent status', () => { + const settings = { ...fullSettings }; + const fakeEventsCache = { track: jest.fn(() => true) }; - expect(fakeEventsCache.track.mock.calls[2]).toEqual([fakeEvent, 3]); // Should be present in the event cache. + const tracker = eventTrackerFactory(settings, fakeEventsCache); - result3.then(tracked => { - 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. + expect(tracker.track(fakeEvent)).toBe(true); + expect(fakeEventsCache.track).toBeCalledTimes(1); // event should be tracked if userConsent is undefined - setTimeout(() => { - expect(fakeIntegrationsManager.handleEvent.mock.calls[1]).toEqual([fakeEvent]); // A copy of tracked event should be sent to integration manager after the timeout wrapping make it to the queue stack. - expect(fakeIntegrationsManager.handleEvent.mock.calls[1][0]).not.toBe(fakeEvent); // Should not send the original event. + settings.userConsent = 'UNKNOWN'; + expect(tracker.track(fakeEvent)).toBe(true); + expect(fakeEventsCache.track).toBeCalledTimes(2); // event should be tracked if userConsent is unknown - done(); - }, 0); - }); + settings.userConsent = 'GRANTED'; + expect(tracker.track(fakeEvent)).toBe(true); + expect(fakeEventsCache.track).toBeCalledTimes(3); // event should be tracked if userConsent is granted - }, 0); - }); - }, 0); + settings.userConsent = 'DECLINED'; + expect(tracker.track(fakeEvent)).toBe(false); + expect(fakeEventsCache.track).toBeCalledTimes(3); // event should not be tracked if userConsent is declined }); }); diff --git a/src/trackers/__tests__/impressionsTracker.spec.ts b/src/trackers/__tests__/impressionsTracker.spec.ts index fe763d67..0a35db12 100644 --- a/src/trackers/__tests__/impressionsTracker.spec.ts +++ b/src/trackers/__tests__/impressionsTracker.spec.ts @@ -3,48 +3,51 @@ import { ImpressionCountsCacheInMemory } from '../../storages/inMemory/Impressio import { impressionObserverSSFactory } from '../impressionObserver/impressionObserverSS'; import { impressionObserverCSFactory } from '../impressionObserver/impressionObserverCS'; import { ImpressionDTO } from '../../types'; -import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; /* Mocks */ -function generateMocks() { - const fakeImpressionsCache = { - track: jest.fn() - }; - const fakeSettings = { - runtime: { - hostname: 'fake-hostname', - ip: 'fake-ip' - }, - version: 'jest-test', - }; - const fakeListener = { - logImpression: jest.fn() - }; - const fakeIntegrationsManager = { - handleImpression: jest.fn() - }; - - return { - fakeImpressionsCache, fakeSettings, fakeListener, fakeIntegrationsManager - }; -} +const fakeImpressionsCache = { + track: jest.fn() +}; +const fakeListener = { + logImpression: jest.fn() +}; +const fakeIntegrationsManager = { + handleImpression: jest.fn() +}; +const fakeSettings = { + ...fullSettings, + runtime: { + hostname: 'fake-hostname', + ip: 'fake-ip' + }, + version: 'jest-test' +}; +const fakeSettingsWithListener = { + ...fakeSettings, + impressionListener: fakeListener +}; /* Tests */ describe('Impressions Tracker', () => { + beforeEach(() => { + fakeImpressionsCache.track.mockClear(); + fakeListener.logImpression.mockClear(); + fakeIntegrationsManager.handleImpression.mockClear(); + }); + test('Tracker API', () => { expect(typeof impressionsTrackerFactory).toBe('function'); // The module should return a function which acts as a factory. - const { fakeImpressionsCache, fakeSettings } = generateMocks(); - const instance = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings); + const instance = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache); 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, fakeSettings } = generateMocks(); - const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings); + const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache); const imp1 = { feature: '10', @@ -64,8 +67,7 @@ describe('Impressions Tracker', () => { }); test('Tracked impressions should be sent to impression listener and integration manager when we invoke .track()', (done) => { - const { fakeImpressionsCache, fakeSettings, fakeListener, fakeIntegrationsManager } = generateMocks(); - const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings, fakeListener, fakeIntegrationsManager); + const tracker = impressionsTrackerFactory(fakeSettingsWithListener, fakeImpressionsCache, fakeIntegrationsManager); const fakeImpression = { feature: 'impression' @@ -135,13 +137,12 @@ describe('Impressions Tracker', () => { } as ImpressionDTO; test('Should track 3 impressions with Previous Time.', () => { - const { fakeImpressionsCache, fakeSettings } = generateMocks(); impression.time = impression2.time = 123456789; impression3.time = 1234567891; const trackers = [ - impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings, undefined, undefined, impressionObserverSSFactory()), - impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings, undefined, undefined, impressionObserverCSFactory()) + impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, undefined, impressionObserverSSFactory()), + impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, undefined, impressionObserverCSFactory()) ]; expect(fakeImpressionsCache.track).not.toBeCalled(); // storage method should not be called until impressions are tracked. @@ -163,13 +164,12 @@ describe('Impressions Tracker', () => { }); test('Should track 2 impressions in OPTIMIZED mode (providing ImpressionCountsCache).', () => { - const { fakeImpressionsCache, fakeSettings } = generateMocks(); impression.time = Date.now(); impression2.time = Date.now(); impression3.time = Date.now(); const impressionCountsCache = new ImpressionCountsCacheInMemory(); - const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings, undefined, undefined, impressionObserverCSFactory(), impressionCountsCache); + const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, undefined, impressionObserverCSFactory(), impressionCountsCache); expect(fakeImpressionsCache.track).not.toBeCalled(); // cache method should not be called by just creating a tracker @@ -187,4 +187,25 @@ describe('Impressions Tracker', () => { expect(Object.keys(impressionCountsCache.state()).length).toBe(2); }); + test('Should track or not impressions depending on user consent status', () => { + const settings = { ...fullSettings }; + + const tracker = impressionsTrackerFactory(settings, fakeImpressionsCache); + + tracker.track([impression]); + expect(fakeImpressionsCache.track).toBeCalledTimes(1); // impression should be tracked if userConsent is undefined + + settings.userConsent = 'UNKNOWN'; + tracker.track([impression]); + expect(fakeImpressionsCache.track).toBeCalledTimes(2); // impression should be tracked if userConsent is unknown + + settings.userConsent = 'GRANTED'; + tracker.track([impression]); + expect(fakeImpressionsCache.track).toBeCalledTimes(3); // impression should be tracked if userConsent is granted + + settings.userConsent = 'DECLINED'; + tracker.track([impression]); + expect(fakeImpressionsCache.track).toBeCalledTimes(3); // impression should not be tracked if userConsent is declined + }); + }); diff --git a/src/trackers/eventTracker.ts b/src/trackers/eventTracker.ts index b3d63f6e..7ac851a9 100644 --- a/src/trackers/eventTracker.ts +++ b/src/trackers/eventTracker.ts @@ -2,9 +2,10 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { thenable } from '../utils/promise/thenable'; import { IEventsCacheBase } from '../storages/types'; import { IEventsHandler, IEventTracker } from './types'; -import { SplitIO } from '../types'; -import { ILogger } from '../logger/types'; +import { ISettings, SplitIO } from '../types'; import { EVENTS_TRACKER_SUCCESS, ERROR_EVENTS_TRACKER } from '../logger/constants'; +import { CONSENT_DECLINED } from '../utils/constants'; +import { isStorageSync } from './impressionObserver/utils'; /** * Event tracker stores events in cache and pass them to the integrations manager if provided. @@ -13,11 +14,14 @@ import { EVENTS_TRACKER_SUCCESS, ERROR_EVENTS_TRACKER } from '../logger/constant * @param integrationsManager optional event handler used for integrations */ export function eventTrackerFactory( - log: ILogger, + settings: ISettings, eventsCache: IEventsCacheBase, integrationsManager?: IEventsHandler ): IEventTracker { + const log = settings.log; + const isSync = isStorageSync(settings); + function queueEventsCallback(eventData: SplitIO.EventData, tracked: boolean) { const { eventTypeId, trafficTypeName, key, value, timestamp, properties } = eventData; // Logging every prop would be too much. @@ -44,6 +48,10 @@ export function eventTrackerFactory( return { track(eventData: SplitIO.EventData, size?: number) { + if (settings.userConsent === CONSENT_DECLINED) { + return isSync ? false : Promise.resolve(false); + } + const tracked = eventsCache.track(eventData, size); if (thenable(tracked)) { diff --git a/src/trackers/impressionObserver/utils.ts b/src/trackers/impressionObserver/utils.ts index 7fb73b85..4ecccbe3 100644 --- a/src/trackers/impressionObserver/utils.ts +++ b/src/trackers/impressionObserver/utils.ts @@ -1,4 +1,4 @@ -import { CONSUMER_PARTIAL_MODE, OPTIMIZED, PRODUCER_MODE, STANDALONE_MODE } from '../../utils/constants'; +import { CONSUMER_MODE, CONSUMER_PARTIAL_MODE, OPTIMIZED, PRODUCER_MODE, STANDALONE_MODE } from '../../utils/constants'; import { ISettings } from '../../types'; /** @@ -15,3 +15,10 @@ export function shouldBeOptimized(settings: ISettings) { if (!shouldAddPt(settings)) return false; return settings.sync.impressionsMode === OPTIMIZED ? true : false; } + +/** + * Storage is async if mode is consumer or partial consumer + */ +export function isStorageSync(settings: ISettings) { + return [CONSUMER_MODE, CONSUMER_PARTIAL_MODE].indexOf(settings.mode) === -1 ? true : false; +} diff --git a/src/trackers/impressionsTracker.ts b/src/trackers/impressionsTracker.ts index c7c18e5b..ecd40a5d 100644 --- a/src/trackers/impressionsTracker.ts +++ b/src/trackers/impressionsTracker.ts @@ -5,8 +5,8 @@ import { IImpressionCountsCacheSync, IImpressionsCacheBase } from '../storages/t import { IImpressionsHandler, IImpressionsTracker } from './types'; import { SplitIO, ImpressionDTO, ISettings } from '../types'; import { IImpressionObserver } from './impressionObserver/types'; -import { ILogger } from '../logger/types'; import { IMPRESSIONS_TRACKER_SUCCESS, ERROR_IMPRESSIONS_TRACKER, ERROR_IMPRESSIONS_LISTENER } from '../logger/constants'; +import { CONSENT_DECLINED } from '../utils/constants'; /** * Impressions tracker stores impressions in cache and pass them to the listener and integrations manager if provided. @@ -19,22 +19,21 @@ import { IMPRESSIONS_TRACKER_SUCCESS, ERROR_IMPRESSIONS_TRACKER, ERROR_IMPRESSIO * @param countsCache optional cache to save impressions count. If provided, impressions will be deduped (OPTIMIZED mode) */ export function impressionsTrackerFactory( - log: ILogger, + settings: ISettings, impressionsCache: IImpressionsCacheBase, - - // @TODO consider passing only an optional integrationsManager to handle impressions - { runtime: { ip, hostname }, version }: Pick, - impressionListener?: SplitIO.IImpressionListener, integrationsManager?: IImpressionsHandler, - // if observer is provided, it implies `shouldAddPreviousTime` flag (i.e., if impressions previous time should be added or not) observer?: IImpressionObserver, // if countsCache is provided, it implies `isOptimized` flag (i.e., if impressions should be deduped or not) countsCache?: IImpressionCountsCacheSync ): IImpressionsTracker { + const { log, impressionListener, runtime: { ip, hostname }, version } = settings; + return { track(impressions: ImpressionDTO[], attributes?: SplitIO.Attributes) { + if (settings.userConsent === CONSENT_DECLINED) return; + const impressionsCount = impressions.length; const impressionsToStore: ImpressionDTO[] = []; // Track only the impressions that are going to be stored @@ -85,7 +84,7 @@ export function impressionsTrackerFactory( // integrationsManager.handleImpression does not throw errors if (integrationsManager) integrationsManager.handleImpression(impressionData); - try { // An exception on the listeners should not break the SDK. + try { // @ts-ignore. An exception on the listeners should not break the SDK. if (impressionListener) impressionListener.logImpression(impressionData); } catch (err) { log.error(ERROR_IMPRESSIONS_LISTENER, [err]); From 6bff1209cf177a8e08daf624bc63ba9732402f6f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 9 Mar 2022 11:17:59 -0300 Subject: [PATCH 34/53] fixed map, to use ponyfill on browsers with partial support of Map constructor, like ie11 --- src/utils/lang/__tests__/maps.spec.ts | 16 ++++++++++++++++ src/utils/lang/maps.ts | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/utils/lang/__tests__/maps.spec.ts diff --git a/src/utils/lang/__tests__/maps.spec.ts b/src/utils/lang/__tests__/maps.spec.ts new file mode 100644 index 00000000..02312d50 --- /dev/null +++ b/src/utils/lang/__tests__/maps.spec.ts @@ -0,0 +1,16 @@ +import { __getMapConstructor, MapPoly } from '../maps'; + +test('__getMapConstructor', () => { + + // should return global Map constructor if available + expect(__getMapConstructor()).toBe(global.Map); + + const originalMap = global.Map; // @ts-ignore + global.Map = undefined; // overwrite global Map + + // should return Map polyfill if global Map constructor is not available + expect(__getMapConstructor()).toBe(MapPoly); + + global.Map = originalMap; // restore original global Map + +}); diff --git a/src/utils/lang/maps.ts b/src/utils/lang/maps.ts index 25461615..a0f09cff 100644 --- a/src/utils/lang/maps.ts +++ b/src/utils/lang/maps.ts @@ -79,4 +79,18 @@ interface IMapConstructor { readonly prototype: IMap; } -export const _Map: IMapConstructor = typeof Map !== 'undefined' ? Map : MapPoly; +/** + * return the Map constructor to use. If native Map is not available or it doesn't support the required features (e.g., IE11), + * a ponyfill with minimal features is returned instead. + * + * Exported for testing purposes only. + */ +export function __getMapConstructor(): IMapConstructor { + // eslint-disable-next-line compat/compat + if (typeof Array.from === 'function' && typeof Map === 'function' && Map.prototype && Map.prototype.values) { + return Map; + } + return MapPoly; +} + +export const _Map = __getMapConstructor(); From bbaed1f4c60b5b719493e69261249d0ce9698ecc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 14 Mar 2022 10:53:51 -0300 Subject: [PATCH 35/53] updated isClientSide flag --- src/sdkClient/client.ts | 2 +- src/sdkClient/clientCS.ts | 2 +- src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 3ef41ec6..15ea6193 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -125,6 +125,6 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S getTreatments, getTreatmentsWithConfig, track, - isBrowserClient: false + isClientSide: false } as SplitIO.IClient | SplitIO.IAsyncClient; } diff --git a/src/sdkClient/clientCS.ts b/src/sdkClient/clientCS.ts index 16648bc6..c66d1e04 100644 --- a/src/sdkClient/clientCS.ts +++ b/src/sdkClient/clientCS.ts @@ -25,6 +25,6 @@ export function clientCSDecorator(log: ILogger, client: SplitIO.IClient, key: Sp // Key is bound to the `track` method. Same thing happens with trafficType but only if provided track: trafficType ? clientCS.track.bind(clientCS, key, trafficType) : clientCS.track.bind(clientCS, key), - isBrowserClient: true + isClientSide: true }) as SplitIO.ICsClient; } diff --git a/src/types.ts b/src/types.ts index c449ff37..7d523223 100644 --- a/src/types.ts +++ b/src/types.ts @@ -398,7 +398,7 @@ interface IBasicClient extends IStatusInterface { // Whether the client implements the client-side API, i.e, with bound key, (true), or the server-side API (false). // Exposed for internal purposes only. Not considered part of the public API, and might be renamed eventually. - isBrowserClient: boolean + isClientSide: boolean } /** * Common definitions between SDK instances for different environments interface. From 6eb52fc0d381015709a6b4db6eb0d2a930037cd7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Mar 2022 11:56:15 -0300 Subject: [PATCH 36/53] code syntax refactor --- src/integrations/pluggable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integrations/pluggable.ts b/src/integrations/pluggable.ts index aed6806c..df4ccd21 100644 --- a/src/integrations/pluggable.ts +++ b/src/integrations/pluggable.ts @@ -29,10 +29,10 @@ export function pluggableIntegrationsManagerFactory( // Exception safe methods: each integration module is responsable for handling errors return { - handleImpression: function (impressionData: SplitIO.ImpressionData) { + handleImpression(impressionData: SplitIO.ImpressionData) { listeners.forEach(listener => listener.queue({ type: SPLIT_IMPRESSION, payload: impressionData })); }, - handleEvent: function (eventData: SplitIO.EventData) { + handleEvent(eventData: SplitIO.EventData) { listeners.forEach(listener => listener.queue({ type: SPLIT_EVENT, payload: eventData })); } }; From 7620e08f87e4a37d155ba532de7d13eb0049148d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Mar 2022 16:23:08 -0300 Subject: [PATCH 37/53] in-transit encryption support added --- src/storages/inRedis/RedisAdapter.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index 05ef94f7..c07d9bb0 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -164,6 +164,9 @@ export class RedisAdapter extends ioredis { } else { // If it IS the string URL, that'll be the first param for ioredis. result.unshift(options.url); } + if (options.tls) { + merge(opts, { tls: options.tls }); + } return result; } @@ -171,9 +174,9 @@ export class RedisAdapter extends ioredis { /** * Parses the options into what we care about. */ - static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass }: Record) { + static _defineOptions({ connectionTimeout, operationTimeout, url, host, port, db, pass, tls }: Record) { const parsedOptions = { - connectionTimeout, operationTimeout, url, host, port, db, pass + connectionTimeout, operationTimeout, url, host, port, db, pass, tls }; return merge({}, DEFAULT_OPTIONS, parsedOptions); From 05637d0224e48d24b043b92f41bd0cf5ed2760af Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 17 Mar 2022 16:45:41 -0300 Subject: [PATCH 38/53] forwarding connectionTimeout to ioredis connectTimeout --- src/storages/inRedis/RedisAdapter.ts | 3 +++ .../inRedis/__tests__/RedisAdapter.spec.ts | 17 ++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index c07d9bb0..0c374f23 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -164,6 +164,9 @@ export class RedisAdapter extends ioredis { } else { // If it IS the string URL, that'll be the first param for ioredis. result.unshift(options.url); } + if (options.connectionTimeout) { + merge(opts, { connectTimeout: options.connectionTimeout }); + } if (options.tls) { merge(opts, { tls: options.tls }); } diff --git a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts index c9e789f6..1e9fd512 100644 --- a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts +++ b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts @@ -93,33 +93,36 @@ describe('STORAGE Redis Adapter', () => { new RedisAdapter(loggerMock, { url: redisUrl, connectionTimeout: 123, - operationTimeout: 124 + operationTimeout: 124, + tls: { ca: 'ca' } }); // 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. + expect(constructorParams[1]).toEqual({ enableOfflineQueue: false, connectTimeout: 123, lazyConnect: false, tls: { ca: 'ca' } }); // and the second parameter would be the default settings for the lib. new RedisAdapter(loggerMock, { ...redisParams, connectionTimeout: 123, - operationTimeout: 124 + operationTimeout: 124, + tls: { ca: 'ca' } }); 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. + expect(constructorParams[0]).toEqual({ host: redisParams.host, port: redisParams.port, db: redisParams.db, password: redisParams.pass, enableOfflineQueue: false, connectTimeout: 123, lazyConnect: false, tls: { ca: 'ca' } }); // 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 + // Using default connectionTimeout + operationTimeout: 124, + tls: { ca: 'ca' } }); 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[1]).toEqual({ enableOfflineQueue: false, connectTimeout: 10000, lazyConnect: false, tls: { ca: 'ca' } }); // and the second parameter would be the default settings for the lib. }); test('static method - _defineOptions', () => { From e1e6698e83128b86924affae97b7e3ab78dd4421 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 25 Mar 2022 19:06:33 -0300 Subject: [PATCH 39/53] implementation and tests --- package-lock.json | 8 ++-- package.json | 2 +- .../__tests__/sdkClientMethodCS.spec.ts | 25 ------------- src/sdkClient/sdkClientMethodCS.ts | 7 +--- src/sdkClient/sdkClientMethodCSWithTT.ts | 13 +------ .../__tests__/index.spec.ts | 37 +++++++++++++++++++ src/utils/settingsValidation/index.ts | 24 ++++++++++-- src/utils/settingsValidation/types.ts | 4 +- 8 files changed, 68 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a014819..46d193a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.8", + "version": "1.2.1-rc.9", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4984,9 +4984,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "ms": { diff --git a/package.json b/package.json index 63b45dc7..609120dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.8", + "version": "1.2.1-rc.9", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts index b5b6b21e..4e445d6d 100644 --- a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts @@ -220,31 +220,6 @@ describe('sdkClientMethodCSFactory', () => { if (!ignoresTT) expect(() => sdkClientMethod('valid-key', ['invalid-TT'])).toThrow('Shared Client needs a valid traffic type or no traffic type at all.'); }); - test.each(testTargets)('invalid key/TT binds a false key/TT in the default client', (sdkClientMethodCSFactory, ignoresTT) => { - const paramsWithInvalidKeyAndTT = { - ...params, - settings: { - ...params.settings, - core: { - key: true, // invalid key - trafficType: '' // invalid TT - } - } - }; - - (clientCSDecoratorSpy as jest.Mock).mockClear(); - // @ts-expect-error - const sdkClientMethod = sdkClientMethodCSFactory(paramsWithInvalidKeyAndTT); - - // calling the function should return a client instance - const client = sdkClientMethod(); - assertClientApi(client, params.sdkReadinessManager.sdkStatus); - - // but with false as binded key and TT - if (ignoresTT) expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), false); - else expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), false, false); - }); - test.each(testTargets)('attributes binding - main client', (sdkClientMethodCSFactory) => { // @ts-expect-error const sdkClientMethod = sdkClientMethodCSFactory(params); diff --git a/src/sdkClient/sdkClientMethodCS.ts b/src/sdkClient/sdkClientMethodCS.ts index 2abd7a20..32f6f3fb 100644 --- a/src/sdkClient/sdkClientMethodCS.ts +++ b/src/sdkClient/sdkClientMethodCS.ts @@ -23,15 +23,10 @@ const method = 'Client instantiation'; export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?: SplitIO.SplitKey) => SplitIO.ICsClient { 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(log, key, method); - const mainClientInstance = clientCSDecorator( log, sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore - validKey + key ); const parsedDefaultKey = keyParser(key); diff --git a/src/sdkClient/sdkClientMethodCSWithTT.ts b/src/sdkClient/sdkClientMethodCSWithTT.ts index 7c5427e2..22a49ac6 100644 --- a/src/sdkClient/sdkClientMethodCSWithTT.ts +++ b/src/sdkClient/sdkClientMethodCSWithTT.ts @@ -25,20 +25,11 @@ const method = 'Client instantiation'; export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?: SplitIO.SplitKey, trafficType?: string) => SplitIO.ICsClient { 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(log, key, method); - let validTrafficType; - if (trafficType !== undefined) { - validTrafficType = validateTrafficType(log, trafficType, method); - } - const mainClientInstance = clientCSDecorator( log, sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore - validKey, - validTrafficType + key, + trafficType ); const parsedDefaultKey = keyParser(key); diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 50b960a9..7f110c86 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -189,6 +189,43 @@ describe('settingsValidation', () => { expect(settings.integrations).toBe(integrationsValidatorResult); expect(integrationsValidatorMock).toBeCalledWith(settings); }); + + test('validates and sanitizes key and traffic type in client-side', () => { + const clientSideValidationParams = { ...minimalSettingsParams, isClientSide: true }; + + const samples = [{ + key: ' valid-key ', settingsKey: 'valid-key', // key string is trimmed + trafficType: 'VALID-TT', settingsTrafficType: 'valid-tt', // TT is converted to lowercase + }, { + key: undefined, settingsKey: false, // undefined key is not valid in client-side + trafficType: undefined, settingsTrafficType: undefined, + }, { + key: null, settingsKey: false, + trafficType: null, settingsTrafficType: false, + }, { + key: true, settingsKey: false, + trafficType: true, settingsTrafficType: false, + }, { + key: 1.5, settingsKey: '1.5', // finite number as key is parsed + trafficType: 100, settingsTrafficType: false, + }, { + key: { matchingKey: 100, bucketingKey: ' BUCK ' }, settingsKey: { matchingKey: '100', bucketingKey: 'BUCK' }, + trafficType: {}, settingsTrafficType: false, + }]; + + samples.forEach(({ key, trafficType, settingsKey, settingsTrafficType }) => { + const settings = settingsValidation({ + core: { + authorizationKey: 'dummy token', + key, + trafficType + } + }, clientSideValidationParams); + + expect(settings.core.key).toEqual(settingsKey); + expect(settings.core.trafficType).toEqual(settingsTrafficType); + }); + }); }); test('SETTINGS / urls should be correctly assigned', () => { diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index d7a31c10..c4de8fbc 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -5,6 +5,8 @@ import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE } from '../constants'; import { validImpressionsMode } from './impressionsMode'; import { ISettingsValidationParams } from './types'; import { ISettings } from '../../types'; +import { validateKey } from '../inputValidation/key'; +import { validateTrafficType } from '../inputValidation/trafficType'; const base = { // Define which kind of object you want to retrieve from SplitFactory @@ -97,7 +99,7 @@ function fromSecondsToMillis(n: number) { */ export function settingsValidation(config: unknown, validationParams: ISettingsValidationParams) { - const { defaults, runtime, storage, integrations, logger, localhost, consent } = validationParams; + const { defaults, isClientSide, runtime, storage, integrations, logger, localhost, consent } = validationParams; // creates a settings object merging base, defaults and config objects. const withDefaults = merge({}, base, defaults, config) as ISettings; @@ -129,9 +131,23 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV // @ts-ignore, modify readonly prop if (storage) withDefaults.storage = storage(withDefaults); - // 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'; + // In client-side, validate key and TT + if (isClientSide) { + const maybeKey = withDefaults.core.key; + // Although `key` is required in client-side, it can be omitted in LOCALHOST mode. In that case, the value `localhost_key` is used. + if (withDefaults.mode === LOCALHOST_MODE && maybeKey === undefined) { + withDefaults.core.key = 'localhost_key'; + } else { + // Keeping same behaviour than JS SDK: if settings key or TT are invalid, + // `false` value is used as binded key/TT of the default client, which leads to some issues. + // @ts-ignore, @TODO handle invalid keys as a non-recoverable error? + withDefaults.core.key = validateKey(log, maybeKey, 'Client instantiation'); + } + + const maybeTT = withDefaults.core.trafficType; + if (maybeTT !== undefined) { // @ts-ignore, assigning false + withDefaults.core.trafficType = validateTrafficType(log, maybeTT, 'Client instantiation'); + } } // Current ip/hostname information diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index 40cd155c..f6ead8e4 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -10,7 +10,9 @@ export interface ISettingsValidationParams { * Version and startup properties are required, because they are not defined in the base settings. */ defaults: Partial & { version: string } & { startup: ISettings['startup'] }, - /** Function to define runtime values (`settings.runtime`) */ + /** If true, validates core.key and core.trafficType */ + isClientSide?: boolean, + /** Define runtime values (`settings.runtime`) */ runtime: (settings: ISettings) => ISettings['runtime'], /** Storage validator (`settings.storage`) */ storage?: (settings: ISettings) => ISettings['storage'], From c5aee70c4a58512583587d520519854648fb321e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 25 Mar 2022 19:13:08 -0300 Subject: [PATCH 40/53] fixed type issue --- src/sdkClient/__tests__/sdkClientMethodCS.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts index 4e445d6d..7eabeb7c 100644 --- a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts @@ -2,10 +2,6 @@ import { sdkClientMethodCSFactory as sdkClientMethodCSWithTTFactory } from '../s import { sdkClientMethodCSFactory } from '../sdkClientMethodCS'; import { assertClientApi } from './testUtils'; -/** Mocks */ -import * as clientCS from '../clientCS'; -const clientCSDecoratorSpy = jest.spyOn(clientCS, 'clientCSDecorator'); - import { settingsWithKey, settingsWithKeyAndTT, settingsWithKeyObject } from '../../utils/settingsValidation/__tests__/settings.mocks'; const partialStorages: { destroy: jest.Mock }[] = []; From 4621c01afac2f425a6a699919f22fb222786f9cc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 28 Mar 2022 16:17:18 -0300 Subject: [PATCH 41/53] updated isObject util function --- src/utils/lang/__tests__/index.spec.ts | 34 +++++++++++++------------- src/utils/lang/index.ts | 30 ++++++++++++++++++++--- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/utils/lang/__tests__/index.spec.ts b/src/utils/lang/__tests__/index.spec.ts index e6e724a1..145d4fed 100644 --- a/src/utils/lang/__tests__/index.spec.ts +++ b/src/utils/lang/__tests__/index.spec.ts @@ -231,26 +231,26 @@ test('LANG UTILS / isIntegerNumber', () => { test('LANG UTILS / isObject', () => { // positive - expect(isObject({})).toBe(true); // Should return true for map objects. - expect(isObject({ a: true })).toBe(true); // Should return true for map objects. - expect(isObject(new Object())).toBe(true); // Should return true for map objects. - expect(isObject(Object.create({}))).toBe(true); // Should return true for map objects. - expect(isObject(Object.create(Object.prototype))).toBe(true); // Should return true for map objects. + expect(isObject({})).toBe(true); // Should return true for plain objects (objects created by the Object built-in constructor). + expect(isObject({ a: true })).toBe(true); // Should return true for plain objects. + expect(isObject(new Object())).toBe(true); // Should return true for plain objects. + expect(isObject(Object.create({}))).toBe(true); // Should return true for plain objects. + expect(isObject(Object.create(Object.prototype))).toBe(true); // Should return true for plain objects. // negative - expect(isObject([])).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(() => { })).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(true)).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(false)).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(null)).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(undefined)).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(1)).toBe(false); // Should return false for anything that is not a map object. - expect(isObject('asd')).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(function () { })).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(Symbol('test'))).toBe(false); // Should return false for anything that is not a map object. - expect(isObject(new Promise(res => res()))).toBe(false); // Should return false for anything that is not a map object. + expect(isObject([])).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(() => { })).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(true)).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(false)).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(null)).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(undefined)).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(1)).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject('asd')).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(function () { })).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(Symbol('test'))).toBe(false); // Should return false for anything that is not a plain object. + expect(isObject(new Promise(res => res()))).toBe(false); // Should return false for anything that is not a plain object. // Object.create(null) creates an object with no prototype which may be tricky to handle. Filtering that out too. - expect(isObject(Object.create(null))).toBe(false); // Should return false for anything that is not a map object. + expect(isObject(Object.create(null))).toBe(false); // Should return false for anything that is not a plain object. }); diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 2a7332ff..94efa40d 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -150,11 +150,35 @@ export function isNaNNumber(val: any): boolean { return val !== val; } +function _isObject(o: any) { + return Object.prototype.toString.call(o) === '[object Object]'; +} + /** - * Validates if a value is an object with the Object prototype (map object). + * Validates if a value is an object created by the Object constructor (plain object). + * Adapted from: + * + * is-plain-object + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. */ -export function isObject(obj: any): boolean { - return obj !== null && typeof obj === 'object' && obj.constructor === Object; +export function isObject(o: any): boolean { + if (_isObject(o) === false) return false; + + // If has modified constructor + const ctor = o.constructor; + if (ctor === undefined) return false; // `Object.create(null)` + + // If has modified prototype + const prot = ctor.prototype; + if (_isObject(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) return false; + + // Most likely a plain Object + return true; } /** From ec387c21bed536256fe21e7725b8eeab4846c9ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:53:03 +0000 Subject: [PATCH 42/53] Bump minimist from 1.2.5 to 1.2.6 Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c8abb07d..c6b7b5b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4984,9 +4984,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "ms": { From cb32d4e836064128de9b3b3b93702cb3e2f19106 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 29 Mar 2022 12:43:27 -0300 Subject: [PATCH 43/53] performance improvement --- src/utils/inputValidation/attributes.ts | 3 +-- src/utils/lang/__tests__/index.spec.ts | 7 +++++++ src/utils/lang/index.ts | 15 ++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/utils/inputValidation/attributes.ts b/src/utils/inputValidation/attributes.ts index 7a06ee90..e9824113 100644 --- a/src/utils/inputValidation/attributes.ts +++ b/src/utils/inputValidation/attributes.ts @@ -6,7 +6,7 @@ import { ERROR_NOT_PLAIN_OBJECT } from '../../logger/constants'; 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 + if (maybeAttrs == undefined || isObject(maybeAttrs)) // eslint-disable-line eqeqeq return maybeAttrs; log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'attributes']); @@ -23,5 +23,4 @@ export function validateAttributesDeep(log: ILogger, maybeAttributes: Record { // Object.create(null) creates an object with no prototype which may be tricky to handle. Filtering that out too. expect(isObject(Object.create(null))).toBe(false); // Should return false for anything that is not a plain object. + // validate on a different VM context + const ctx = vm.createContext({ isObject }); + vm.runInContext('var plainObjectResult = isObject({}); var arrayResult = isObject([]); var nullResult = isObject(null);', ctx); + expect(ctx.plainObjectResult).toBe(true); + expect(ctx.arrayResult).toBe(false); + expect(ctx.nullResult).toBe(false); }); test('LANG UTILS / uniqueId', () => { diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 94efa40d..83a35f6a 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -154,16 +154,13 @@ function _isObject(o: any) { return Object.prototype.toString.call(o) === '[object Object]'; } -/** - * Validates if a value is an object created by the Object constructor (plain object). - * Adapted from: - * +/* * is-plain-object * * Copyright (c) 2014-2017, Jon Schlinkert. * Released under the MIT License. */ -export function isObject(o: any): boolean { +function _isPlainObject(o: any) { if (_isObject(o) === false) return false; // If has modified constructor @@ -181,6 +178,14 @@ export function isObject(o: any): boolean { return true; } +/** + * Validates if a value is an object created by the Object constructor (plain object). + * It uses `_isPlainObject` to avoid false negatives when validating values on a separate VM context. + */ +export function isObject(obj: any): boolean { + return obj !== null && typeof obj === 'object' && (obj.constructor === Object || _isPlainObject(obj)); +} + /** * Checks if a given value is a string. */ From b9161b2559792012cce0a6d9d9dc27658d1471d4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 29 Mar 2022 12:44:50 -0300 Subject: [PATCH 44/53] size improvement --- src/utils/lang/__tests__/index.spec.ts | 21 ++++++++++++--- src/utils/lang/index.ts | 37 +++++--------------------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/utils/lang/__tests__/index.spec.ts b/src/utils/lang/__tests__/index.spec.ts index b4c3ae24..ae502e86 100644 --- a/src/utils/lang/__tests__/index.spec.ts +++ b/src/utils/lang/__tests__/index.spec.ts @@ -255,10 +255,23 @@ test('LANG UTILS / isObject', () => { // validate on a different VM context const ctx = vm.createContext({ isObject }); - vm.runInContext('var plainObjectResult = isObject({}); var arrayResult = isObject([]); var nullResult = isObject(null);', ctx); - expect(ctx.plainObjectResult).toBe(true); - expect(ctx.arrayResult).toBe(false); - expect(ctx.nullResult).toBe(false); + vm.runInContext(` + var positives = + isObject({}) && + isObject({ a: true }) && + isObject(new Object()) && + isObject(Object.create({})) && + isObject(Object.create(Object.prototype)); + var negatives = + isObject([]) || + isObject(() => { }) || + isObject(null) || + isObject(undefined) || + isObject(new Promise(res => res())) || + isObject(Object.create(null)); + `, ctx); + expect(ctx.positives).toBe(true); + expect(ctx.negatives).toBe(false); }); test('LANG UTILS / uniqueId', () => { diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 83a35f6a..bd7c7a51 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -150,40 +150,15 @@ export function isNaNNumber(val: any): boolean { return val !== val; } -function _isObject(o: any) { - return Object.prototype.toString.call(o) === '[object Object]'; -} - -/* - * is-plain-object - * - * Copyright (c) 2014-2017, Jon Schlinkert. - * Released under the MIT License. - */ -function _isPlainObject(o: any) { - if (_isObject(o) === false) return false; - - // If has modified constructor - const ctor = o.constructor; - if (ctor === undefined) return false; // `Object.create(null)` - - // If has modified prototype - const prot = ctor.prototype; - if (_isObject(prot) === false) return false; - - // If constructor does not have an Object-specific method - if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) return false; - - // Most likely a plain Object - return true; -} - /** * Validates if a value is an object created by the Object constructor (plain object). - * It uses `_isPlainObject` to avoid false negatives when validating values on a separate VM context. + * It checks `constructor.name` to avoid false negatives when validating values on a separate VM context, which has its own global built-ins. */ -export function isObject(obj: any): boolean { - return obj !== null && typeof obj === 'object' && (obj.constructor === Object || _isPlainObject(obj)); +export function isObject(obj: any) { + return obj !== null && typeof obj === 'object' && ( + obj.constructor === Object || + (obj.constructor != null && obj.constructor.name === 'Object') + ); } /** From 5bcf761fb6f700ada2191f2dabf94e2debe1b946 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 29 Mar 2022 15:06:33 -0300 Subject: [PATCH 45/53] updated some type signatures --- src/evaluator/parser/index.ts | 2 +- src/evaluator/types.ts | 4 ++-- src/evaluator/value/index.ts | 4 ++-- src/evaluator/value/sanitize.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/evaluator/parser/index.ts b/src/evaluator/parser/index.ts index 8e53a321..25c1e7f1 100644 --- a/src/evaluator/parser/index.ts +++ b/src/evaluator/parser/index.ts @@ -31,7 +31,7 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt const matcher = matcherFactory(log, matcherDto, storage); // Evaluator function. - return (key: string, attributes: SplitIO.Attributes, splitEvaluator: ISplitEvaluator) => { + return (key: string, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator) => { const value = sanitizeValue(log, key, matcherDto, attributes); const result = value !== undefined && matcher ? matcher(value, splitEvaluator) : false; diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index ccab4db8..54078b5b 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -6,7 +6,7 @@ import { ILogger } from '../logger/types'; export interface IDependencyMatcherValue { key: SplitIO.SplitKey, - attributes: SplitIO.Attributes + attributes?: SplitIO.Attributes } export interface IMatcherDto { @@ -27,7 +27,7 @@ export interface IEvaluation { export type IEvaluationResult = IEvaluation & { treatment: string } -export type ISplitEvaluator = (log: ILogger, 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 | undefined, 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 319e05e2..c564a68f 100644 --- a/src/evaluator/value/index.ts +++ b/src/evaluator/value/index.ts @@ -4,7 +4,7 @@ import { ILogger } from '../../logger/types'; import { sanitize } from './sanitize'; import { ENGINE_VALUE, ENGINE_VALUE_NO_ATTRIBUTES, ENGINE_VALUE_INVALID } from '../../logger/constants'; -function parseValue(log: ILogger, 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) { @@ -23,7 +23,7 @@ function parseValue(log: ILogger, key: string, attributeName: string | null, att /** * Defines value to be matched (key / attribute). */ -export function sanitizeValue(log: ILogger, key: string, matcherDto: IMatcherDto, attributes: SplitIO.Attributes) { +export function sanitizeValue(log: ILogger, key: string, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) { const attributeName = matcherDto.attribute; const valueToMatch = parseValue(log, key, attributeName, attributes); const sanitizedValue = sanitize(log, matcherDto.type, valueToMatch, matcherDto.dataType, attributes); diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 6a36a59d..d12de8ed 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -41,7 +41,7 @@ function sanitizeBoolean(val: any): boolean | undefined { return undefined; } -function dependencyProcessor(sanitizedValue: string, attributes: SplitIO.Attributes): IDependencyMatcherValue { +function dependencyProcessor(sanitizedValue: string, attributes?: SplitIO.Attributes): IDependencyMatcherValue { return { key: sanitizedValue, attributes @@ -69,7 +69,7 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) { /** * Sanitize matcher value */ -export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | undefined, dataType: string, attributes: SplitIO.Attributes) { +export 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; From 5020990f796239893ddb40ba99cb9c7918be941b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 29 Mar 2022 15:13:58 -0300 Subject: [PATCH 46/53] handle key and TT validation separately --- package-lock.json | 2 +- package.json | 2 +- .../settingsValidation/__tests__/index.spec.ts | 15 ++++++++++++++- src/utils/settingsValidation/index.ts | 14 ++++++++------ src/utils/settingsValidation/types.ts | 6 ++++-- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46d193a6..1e0f7f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.9", + "version": "1.2.1-rc.10", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 609120dc..c74fbf0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.9", + "version": "1.2.1-rc.10", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/utils/settingsValidation/__tests__/index.spec.ts b/src/utils/settingsValidation/__tests__/index.spec.ts index 7f110c86..3f799970 100644 --- a/src/utils/settingsValidation/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/__tests__/index.spec.ts @@ -191,7 +191,7 @@ describe('settingsValidation', () => { }); test('validates and sanitizes key and traffic type in client-side', () => { - const clientSideValidationParams = { ...minimalSettingsParams, isClientSide: true }; + const clientSideValidationParams = { ...minimalSettingsParams, acceptKey: true, acceptTT: true }; const samples = [{ key: ' valid-key ', settingsKey: 'valid-key', // key string is trimmed @@ -226,6 +226,19 @@ describe('settingsValidation', () => { expect(settings.core.trafficType).toEqual(settingsTrafficType); }); }); + + test('validates and sanitizes key, while traffic type is ignored', () => { + const settings = settingsValidation({ + core: { + authorizationKey: 'dummy token', + key: true, + trafficType: true + } + }, { ...minimalSettingsParams, acceptKey: true }); + + expect(settings.core.key).toEqual(false); // key is validated + expect(settings.core.trafficType).toEqual(true); // traffic type is ignored + }); }); test('SETTINGS / urls should be correctly assigned', () => { diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index c4de8fbc..5bb1125b 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -99,7 +99,7 @@ function fromSecondsToMillis(n: number) { */ export function settingsValidation(config: unknown, validationParams: ISettingsValidationParams) { - const { defaults, isClientSide, runtime, storage, integrations, logger, localhost, consent } = validationParams; + const { defaults, runtime, storage, integrations, logger, localhost, consent } = validationParams; // creates a settings object merging base, defaults and config objects. const withDefaults = merge({}, base, defaults, config) as ISettings; @@ -131,8 +131,8 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV // @ts-ignore, modify readonly prop if (storage) withDefaults.storage = storage(withDefaults); - // In client-side, validate key and TT - if (isClientSide) { + // Validate key and TT (for client-side) + if (validationParams.acceptKey) { const maybeKey = withDefaults.core.key; // Although `key` is required in client-side, it can be omitted in LOCALHOST mode. In that case, the value `localhost_key` is used. if (withDefaults.mode === LOCALHOST_MODE && maybeKey === undefined) { @@ -144,9 +144,11 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV withDefaults.core.key = validateKey(log, maybeKey, 'Client instantiation'); } - const maybeTT = withDefaults.core.trafficType; - if (maybeTT !== undefined) { // @ts-ignore, assigning false - withDefaults.core.trafficType = validateTrafficType(log, maybeTT, 'Client instantiation'); + if (validationParams.acceptTT) { + const maybeTT = withDefaults.core.trafficType; + if (maybeTT !== undefined) { // @ts-ignore + withDefaults.core.trafficType = validateTrafficType(log, maybeTT, 'Client instantiation'); + } } } diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index f6ead8e4..2322d042 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -10,8 +10,10 @@ export interface ISettingsValidationParams { * Version and startup properties are required, because they are not defined in the base settings. */ defaults: Partial & { version: string } & { startup: ISettings['startup'] }, - /** If true, validates core.key and core.trafficType */ - isClientSide?: boolean, + /** If true, validates core.key */ + acceptKey?: boolean, + /** If true, validates core.trafficType */ + acceptTT?: boolean, /** Define runtime values (`settings.runtime`) */ runtime: (settings: ISettings) => ISettings['runtime'], /** Storage validator (`settings.storage`) */ From 0ca9ff5fca2a36a367a66f53838acfde9b8cd78b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 31 Mar 2022 13:01:03 -0300 Subject: [PATCH 47/53] fixed issue with a info log --- package-lock.json | 2 +- package.json | 2 +- src/sdkFactory/userConsentProps.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e0f7f3d..ebe428e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.10", + "version": "1.2.1-rc.11", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c74fbf0e..8338e84a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.10", + "version": "1.2.1-rc.11", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkFactory/userConsentProps.ts b/src/sdkFactory/userConsentProps.ts index da715c33..893a10ca 100644 --- a/src/sdkFactory/userConsentProps.ts +++ b/src/sdkFactory/userConsentProps.ts @@ -19,13 +19,12 @@ export function userConsentProps(settings: ISettings, syncManager?: ISyncManager const newConsentStatus = consent ? CONSENT_GRANTED : CONSENT_DECLINED; - if (settings.userConsent !== newConsentStatus) { // @ts-ignore, modify readonly prop + if (settings.userConsent !== newConsentStatus) { + log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); // @ts-ignore, modify readonly prop settings.userConsent = newConsentStatus; if (consent) syncManager?.submitter?.start(); // resumes submitters if transitioning to GRANTED else syncManager?.submitter?.stop(); // pauses submitters if transitioning to DECLINED - - log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); } else { log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]); } From f053eaf4163ac2e96378702758d6cba75787a77e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 31 Mar 2022 13:40:46 -0300 Subject: [PATCH 48/53] added info log for initial user consent --- src/logger/constants.ts | 1 + src/logger/messages/info.ts | 1 + src/sdkFactory/userConsentProps.ts | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 68259646..10b45254 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -70,6 +70,7 @@ export const EVENTS_TRACKER_SUCCESS = 120; export const IMPRESSIONS_TRACKER_SUCCESS = 121; export const USER_CONSENT_UPDATED = 122; export const USER_CONSENT_NOT_UPDATED = 123; +export const USER_CONSENT_INITIAL = 124; export const ENGINE_VALUE_INVALID = 200; export const ENGINE_VALUE_NO_ATTRIBUTES = 201; diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 9f4a4254..96703540 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -16,6 +16,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'], [c.USER_CONSENT_UPDATED, 'setUserConsent: consent status changed from %s to %s.'], [c.USER_CONSENT_NOT_UPDATED, 'setUserConsent: call had no effect because it was the current consent status (%s).'], + [c.USER_CONSENT_INITIAL, 'Starting the SDK with %s user consent. No data will be sent.'], // synchronizer [c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'], diff --git a/src/sdkFactory/userConsentProps.ts b/src/sdkFactory/userConsentProps.ts index 893a10ca..b275406c 100644 --- a/src/sdkFactory/userConsentProps.ts +++ b/src/sdkFactory/userConsentProps.ts @@ -1,6 +1,7 @@ -import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED, USER_CONSENT_NOT_UPDATED } from '../logger/constants'; +import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED, USER_CONSENT_NOT_UPDATED, USER_CONSENT_INITIAL } from '../logger/constants'; import { ISyncManager } from '../sync/types'; import { ISettings } from '../types'; +import { isConsentGranted } from '../utils/consent'; import { CONSENT_GRANTED, CONSENT_DECLINED } from '../utils/constants'; import { isBoolean } from '../utils/lang'; @@ -9,6 +10,8 @@ export function userConsentProps(settings: ISettings, syncManager?: ISyncManager const log = settings.log; + if (!isConsentGranted(settings)) log.info(USER_CONSENT_INITIAL, [settings.userConsent]); + return { setUserConsent(consent: unknown) { // validate input param From a03092f4e70a13ae22268385502b463401be8ae8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 4 Apr 2022 14:03:07 -0300 Subject: [PATCH 49/53] prepare release v1.3.0 --- CHANGES.txt | 14 ++++++++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 892a6b9d..16af0c07 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,17 @@ +1.3.0 (April 4, 2022) + - Added support for user consent, a way to control if the SDK tracks impressions and events or not, based on users granting or rejecting consent for it. + - Added support for `scheduler.impressionsQueueSize` property on SDK configuration. + - Added support to Redis storage for NodeJS to accept TLS configuration options. + - Updated format for MySegments keys in LocalStorage, keeping backwards compatibility (issue https://github.com/splitio/javascript-client/issues/638). + - Updated some modules due to general polishing and refactors, including updates on some log messages. + - Updated some dependencies for vulnerability fixes. + - Bugfixing - Fixed internal isObject utility function, to avoid false negatives on runtimes using multiple VM contexts, like NuxtJS dev server. + - Bugfixing - Fixed validation of `core.key` SDK configuration param, to parse it into a string and log a warning when passing a number (Related to issue https://github.com/splitio/react-native-client/issues/19). + - Bugfixing - Fixed validation of `sync.impressionsMode` SDK configuration param, to avoid an exception on SplitFactory instantiation when passing a non-string value. + - Bugfixing - Fixed an issue with `connectionTimeout` options params of Redis storage, that was being ignored and not passed down to the underlying ioredis client. + - Bugfixing - Fixed streaming synchronization issue with multiple clients. + - Bugfixing - Fixed issue with internal Map ponyfill that result in logger not working on IE11 browser. + 1.2.0 (January 19, 2022) - Added support to SDK clients on browser to optionally bind attributes to the client, keeping these loaded within the SDK along with the user ID, for easier usage when requesting flag. diff --git a/package-lock.json b/package-lock.json index ebe428e0..a53eec0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.11", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8338e84a..074589a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.11", + "version": "1.3.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 23454193bad788ce2f0316356055905d86374a79 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 5 Apr 2022 16:16:30 -0300 Subject: [PATCH 50/53] refactors regarding user consent API --- package-lock.json | 2 +- package.json | 2 +- src/consent/__tests__/sdkUserConsent.spec.ts | 45 ++++++++++++++ src/{utils/consent.ts => consent/index.ts} | 2 +- src/consent/sdkUserConsent.ts | 58 +++++++++++++++++++ src/listeners/browser.ts | 2 +- src/sdkClient/client.ts | 5 +- src/sdkClient/sdkClient.ts | 8 +-- src/sdkClient/sdkClientMethod.ts | 4 +- src/sdkClient/sdkClientMethodCS.ts | 9 ++- src/sdkClient/sdkClientMethodCSWithTT.ts | 9 ++- src/sdkClient/types.ts | 21 ------- .../__tests__/userConsentProps.spec.ts | 39 ------------- src/sdkFactory/index.ts | 5 +- src/sdkFactory/types.ts | 16 ++++- src/sdkFactory/userConsentProps.ts | 42 -------------- src/sync/syncManagerOnline.ts | 2 +- 17 files changed, 140 insertions(+), 131 deletions(-) create mode 100644 src/consent/__tests__/sdkUserConsent.spec.ts rename src/{utils/consent.ts => consent/index.ts} (82%) create mode 100644 src/consent/sdkUserConsent.ts delete mode 100644 src/sdkClient/types.ts delete mode 100644 src/sdkFactory/__tests__/userConsentProps.spec.ts delete mode 100644 src/sdkFactory/userConsentProps.ts diff --git a/package-lock.json b/package-lock.json index ebe428e0..e09226c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.11", + "version": "1.2.1-rc.12", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8338e84a..66a87c36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.2.1-rc.11", + "version": "1.2.1-rc.12", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/consent/__tests__/sdkUserConsent.spec.ts b/src/consent/__tests__/sdkUserConsent.spec.ts new file mode 100644 index 00000000..706b6ca9 --- /dev/null +++ b/src/consent/__tests__/sdkUserConsent.spec.ts @@ -0,0 +1,45 @@ +import { createUserConsentAPI } from '../sdkUserConsent'; +import { syncTaskFactory } from '../../sync/__tests__/syncTask.mock'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; + +test('createUserConsentAPI', () => { + const settings = { ...fullSettings, userConsent: 'UNKNOWN' }; + const syncManager = { submitter: syncTaskFactory() }; + const storage = { + events: { clear: jest.fn() }, + impressions: { clear: jest.fn() } + }; + + // @ts-ignore + const props = createUserConsentAPI({ settings, syncManager, storage }); + + // getUserConsent returns settings.userConsent + expect(props.getStatus()).toBe(settings.userConsent); + expect(props.getStatus()).toBe(props.Status.UNKNOWN); + + // setting user consent to 'GRANTED' + expect(props.setStatus(true)).toBe(true); + expect(props.setStatus(true)).toBe(true); // calling again has no affect + expect(syncManager.submitter.start).toBeCalledTimes(1); // submitter resumed + expect(syncManager.submitter.stop).toBeCalledTimes(0); + expect(props.getStatus()).toBe(props.Status.GRANTED); + + // setting user consent to 'DECLINED' + expect(props.setStatus(false)).toBe(true); + expect(props.setStatus(false)).toBe(true); // calling again has no affect + expect(syncManager.submitter.start).toBeCalledTimes(1); + expect(syncManager.submitter.stop).toBeCalledTimes(1); // submitter paused + expect(props.getStatus()).toBe(props.Status.DECLINED); + expect(storage.events.clear).toBeCalledTimes(1); // storage tracked data dropped + expect(storage.impressions.clear).toBeCalledTimes(1); + + // Invalid values have no effect + expect(props.setStatus('DECLINED')).toBe(false); // strings are not valid + expect(props.setStatus('GRANTED')).toBe(false); + expect(props.setStatus(undefined)).toBe(false); + expect(props.setStatus({})).toBe(false); + + expect(syncManager.submitter.start).toBeCalledTimes(1); + expect(syncManager.submitter.stop).toBeCalledTimes(1); + expect(props.getStatus()).toBe(props.Status.DECLINED); +}); diff --git a/src/utils/consent.ts b/src/consent/index.ts similarity index 82% rename from src/utils/consent.ts rename to src/consent/index.ts index 20ca745c..3de8d7d5 100644 --- a/src/utils/consent.ts +++ b/src/consent/index.ts @@ -1,5 +1,5 @@ import { ISettings } from '../types'; -import { CONSENT_GRANTED } from './constants'; +import { CONSENT_GRANTED } from '../utils/constants'; export function isConsentGranted(settings: ISettings) { const userConsent = settings.userConsent; diff --git a/src/consent/sdkUserConsent.ts b/src/consent/sdkUserConsent.ts new file mode 100644 index 00000000..cd7d387f --- /dev/null +++ b/src/consent/sdkUserConsent.ts @@ -0,0 +1,58 @@ +import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED, USER_CONSENT_NOT_UPDATED, USER_CONSENT_INITIAL } from '../logger/constants'; +import { isConsentGranted } from './index'; +import { CONSENT_GRANTED, CONSENT_DECLINED, CONSENT_UNKNOWN } from '../utils/constants'; +import { isBoolean } from '../utils/lang'; +import { ISdkFactoryContext } from '../sdkFactory/types'; + +// User consent enum +const ConsentStatus = { + GRANTED: CONSENT_GRANTED, + DECLINED: CONSENT_DECLINED, + UNKNOWN: CONSENT_UNKNOWN, +}; + +/** + * The public user consent API exposed via SplitFactory, used to control if the SDK tracks and sends impressions and events or not. + */ +export function createUserConsentAPI(params: ISdkFactoryContext) { + const { settings, settings: { log }, syncManager, storage: { events, impressions, impressionCounts } } = params; + + if (!isConsentGranted(settings)) log.info(USER_CONSENT_INITIAL, [settings.userConsent]); + + return { + setStatus(consent: unknown) { + // validate input param + if (!isBoolean(consent)) { + log.warn(ERROR_NOT_BOOLEAN, ['setUserConsent']); + return false; + } + + const newConsentStatus = consent ? CONSENT_GRANTED : CONSENT_DECLINED; + + if (settings.userConsent !== newConsentStatus) { + log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); // @ts-ignore, modify readonly prop + settings.userConsent = newConsentStatus; + + if (consent) { // resumes submitters if transitioning to GRANTED + syncManager?.submitter?.start(); + } else { // pauses submitters and drops tracked data if transitioning to DECLINED + syncManager?.submitter?.stop(); + // @ts-ignore, clear method is present in storage for standalone and partial consumer mode + if (events.clear) events.clear(); // @ts-ignore + if (impressions.clear) impressions.clear(); + if (impressionCounts) impressionCounts.clear(); + } + } else { + log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]); + } + + return true; + }, + + getStatus() { + return settings.userConsent; + }, + + Status: ConsentStatus + }; +} diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index 502c4e43..5bbd3496 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -11,7 +11,7 @@ import { OPTIMIZED, DEBUG } from '../utils/constants'; import { objectAssign } from '../utils/lang/objectAssign'; import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; import { ISyncManager } from '../sync/types'; -import { isConsentGranted } from '../utils/consent'; +import { isConsentGranted } from '../consent'; // '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'; diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 15ea6193..44501624 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -5,17 +5,16 @@ import { validateSplitExistance } from '../utils/inputValidation/splitExistance' import { validateTrafficTypeExistance } from '../utils/inputValidation/trafficTypeExistance'; import { SDK_NOT_READY } from '../utils/labels'; import { CONTROL } from '../utils/constants'; -import { IClientFactoryParams } from './types'; import { IEvaluationResult } from '../evaluator/types'; import { SplitIO, ImpressionDTO } from '../types'; import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants'; - +import { ISdkFactoryContext } from '../sdkFactory/types'; /** * Creator of base client with getTreatments and track methods. */ // @TODO missing time tracking to collect telemetry -export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | SplitIO.IAsyncClient { +export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | SplitIO.IAsyncClient { const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker } = params; const { log, mode } = settings; diff --git a/src/sdkClient/sdkClient.ts b/src/sdkClient/sdkClient.ts index acf03f96..1ff86ba3 100644 --- a/src/sdkClient/sdkClient.ts +++ b/src/sdkClient/sdkClient.ts @@ -3,13 +3,13 @@ import { IStatusInterface, SplitIO } from '../types'; import { releaseApiKey } from '../utils/inputValidation/apiKey'; import { clientFactory } from './client'; import { clientInputValidationDecorator } from './clientInputValidation'; -import { ISdkClientFactoryParams } from './types'; +import { ISdkFactoryContext } from '../sdkFactory/types'; /** * Creates an Sdk client, i.e., a base client with status and destroy interface */ -export function sdkClientFactory(params: ISdkClientFactoryParams): SplitIO.IClient | SplitIO.IAsyncClient { - const { sdkReadinessManager, syncManager, storage, signalListener, settings, sharedClient } = params; +export function sdkClientFactory(params: ISdkFactoryContext, isMainClient = true): SplitIO.IClient | SplitIO.IAsyncClient { + const { sdkReadinessManager, syncManager, storage, signalListener, settings } = params; return objectAssign( // Proto-linkage of the readiness Event Emitter @@ -35,7 +35,7 @@ export function sdkClientFactory(params: ISdkClientFactoryParams): SplitIO.IClie signalListener && signalListener.stop(); // Release the API Key if it is the main client - if (!sharedClient) releaseApiKey(settings.core.authorizationKey); + if (isMainClient) releaseApiKey(settings.core.authorizationKey); // Cleanup storage return storage.destroy(); diff --git a/src/sdkClient/sdkClientMethod.ts b/src/sdkClient/sdkClientMethod.ts index 82d37435..9cd117ea 100644 --- a/src/sdkClient/sdkClientMethod.ts +++ b/src/sdkClient/sdkClientMethod.ts @@ -1,12 +1,12 @@ -import { ISdkClientFactoryParams } from './types'; import { SplitIO } from '../types'; import { sdkClientFactory } from './sdkClient'; import { RETRIEVE_CLIENT_DEFAULT } from '../logger/constants'; +import { ISdkFactoryContext } from '../sdkFactory/types'; /** * Factory of client method for server-side SDKs (ISDK and IAsyncSDK) */ -export function sdkClientMethodFactory(params: ISdkClientFactoryParams): () => SplitIO.IClient | SplitIO.IAsyncClient { +export function sdkClientMethodFactory(params: ISdkFactoryContext): () => SplitIO.IClient | SplitIO.IAsyncClient { const log = params.settings.log; const clientInstance = sdkClientFactory(params); diff --git a/src/sdkClient/sdkClientMethodCS.ts b/src/sdkClient/sdkClientMethodCS.ts index 32f6f3fb..3e6d6bdd 100644 --- a/src/sdkClient/sdkClientMethodCS.ts +++ b/src/sdkClient/sdkClientMethodCS.ts @@ -1,5 +1,4 @@ import { clientCSDecorator } from './clientCS'; -import { ISdkClientFactoryParams } from './types'; import { SplitIO } from '../types'; import { validateKey } from '../utils/inputValidation/key'; import { getMatching, keyParser } from '../utils/key'; @@ -8,6 +7,7 @@ import { ISyncManagerCS } from '../sync/types'; import { objectAssign } from '../utils/lang/objectAssign'; import { RETRIEVE_CLIENT_DEFAULT, NEW_SHARED_CLIENT, RETRIEVE_CLIENT_EXISTING } from '../logger/constants'; import { SDK_SEGMENTS_ARRIVED } from '../readiness/constants'; +import { ISdkFactoryContext } from '../sdkFactory/types'; function buildInstanceId(key: SplitIO.SplitKey) { // @ts-ignore @@ -20,12 +20,12 @@ 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 { +export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: SplitIO.SplitKey) => SplitIO.ICsClient { const { storage, syncManager, sdkReadinessManager, settings: { core: { key }, startup: { readyTimeout }, log } } = params; const mainClientInstance = clientCSDecorator( log, - sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore + sdkClientFactory(params) as SplitIO.IClient, key ); @@ -76,8 +76,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? storage: sharedStorage || storage, syncManager: sharedSyncManager, signalListener: undefined, // only the main client "destroy" method stops the signal listener - sharedClient: true - })) as SplitIO.IClient, + }), false) as SplitIO.IClient, validKey ); diff --git a/src/sdkClient/sdkClientMethodCSWithTT.ts b/src/sdkClient/sdkClientMethodCSWithTT.ts index 22a49ac6..cdbe99cd 100644 --- a/src/sdkClient/sdkClientMethodCSWithTT.ts +++ b/src/sdkClient/sdkClientMethodCSWithTT.ts @@ -1,5 +1,4 @@ import { clientCSDecorator } from './clientCS'; -import { ISdkClientFactoryParams } from './types'; import { SplitIO } from '../types'; import { validateKey } from '../utils/inputValidation/key'; import { validateTrafficType } from '../utils/inputValidation/trafficType'; @@ -9,6 +8,7 @@ import { ISyncManagerCS } from '../sync/types'; import { objectAssign } from '../utils/lang/objectAssign'; import { RETRIEVE_CLIENT_DEFAULT, NEW_SHARED_CLIENT, RETRIEVE_CLIENT_EXISTING } from '../logger/constants'; import { SDK_SEGMENTS_ARRIVED } from '../readiness/constants'; +import { ISdkFactoryContext } from '../sdkFactory/types'; function buildInstanceId(key: SplitIO.SplitKey, trafficType?: string) { // @ts-ignore @@ -22,12 +22,12 @@ const method = 'Client instantiation'; * 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 { +export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: SplitIO.SplitKey, trafficType?: string) => SplitIO.ICsClient { const { storage, syncManager, sdkReadinessManager, settings: { core: { key, trafficType }, startup: { readyTimeout }, log } } = params; const mainClientInstance = clientCSDecorator( log, - sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore + sdkClientFactory(params) as SplitIO.IClient, key, trafficType ); @@ -86,8 +86,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? storage: sharedStorage || storage, syncManager: sharedSyncManager, signalListener: undefined, // only the main client "destroy" method stops the signal listener - sharedClient: true - })) as SplitIO.IClient, + }), false) as SplitIO.IClient, validKey, validTrafficType ); diff --git a/src/sdkClient/types.ts b/src/sdkClient/types.ts deleted file mode 100644 index d5e3b3b8..00000000 --- a/src/sdkClient/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ISignalListener } from '../listeners/types'; -import { ISdkReadinessManager } from '../readiness/types'; -import { IStorageAsync, IStorageSync } from '../storages/types'; -import { ISyncManager } from '../sync/types'; -import { IEventTracker, IImpressionsTracker } from '../trackers/types'; -import { ISettings } from '../types'; - -export interface IClientFactoryParams { - storage: IStorageSync | IStorageAsync, - sdkReadinessManager: ISdkReadinessManager, - settings: ISettings - impressionsTracker: IImpressionsTracker, - eventTracker: IEventTracker, - // @TODO add time tracker and metricCollectors (a.k.a metricTracker)? -} - -export interface ISdkClientFactoryParams extends IClientFactoryParams { - signalListener?: ISignalListener - syncManager?: ISyncManager, - sharedClient?: boolean -} diff --git a/src/sdkFactory/__tests__/userConsentProps.spec.ts b/src/sdkFactory/__tests__/userConsentProps.spec.ts deleted file mode 100644 index e7f4d0fc..00000000 --- a/src/sdkFactory/__tests__/userConsentProps.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { userConsentProps } from '../userConsentProps'; -import { syncTaskFactory } from '../../sync/__tests__/syncTask.mock'; -import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; - -test('userConsentProps', () => { - const settings = { ...fullSettings }; - const syncManager = { submitter: syncTaskFactory() }; - - // @ts-ignore - const props = userConsentProps(settings, syncManager); - - // getUserConsent returns settings.userConsent - expect(props.getUserConsent()).toBe(settings.userConsent); - - // setting user consent to 'GRANTED' - expect(props.setUserConsent(true)).toBe(true); - expect(props.setUserConsent(true)).toBe(true); // calling again has no affect - expect(syncManager.submitter.start).toBeCalledTimes(1); // submitter resumed - expect(syncManager.submitter.stop).toBeCalledTimes(0); - expect(props.getUserConsent()).toBe('GRANTED'); - - // setting user consent to 'DECLINED' - expect(props.setUserConsent(false)).toBe(true); - expect(props.setUserConsent(false)).toBe(true); // calling again has no affect - expect(syncManager.submitter.start).toBeCalledTimes(1); - expect(syncManager.submitter.stop).toBeCalledTimes(1); // submitter paused - expect(props.getUserConsent()).toBe('DECLINED'); - - // Invalid values have no effect - expect(props.setUserConsent('DECLINED')).toBe(false); // strings are not valid - expect(props.setUserConsent('GRANTED')).toBe(false); - expect(props.setUserConsent(undefined)).toBe(false); - expect(props.setUserConsent({})).toBe(false); - - expect(syncManager.submitter.start).toBeCalledTimes(1); - expect(syncManager.submitter.stop).toBeCalledTimes(1); - expect(props.getUserConsent()).toBe('DECLINED'); - -}); diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 898bb1a0..e28bd1e2 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -81,7 +81,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const signalListener = SignalListener && new SignalListener(syncManager, settings, storage, splitApi); // Sdk client and manager - const clientMethod = sdkClientMethodFactory({ eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }); + const ctx = { eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }; + const clientMethod = sdkClientMethodFactory(ctx); const managerInstance = sdkManagerFactory(log, storage.splits, sdkReadinessManager); syncManager && syncManager.start(); @@ -104,5 +105,5 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. Logger: createLoggerAPI(settings.log), settings, - }, extraProps && extraProps(settings, syncManager)); + }, extraProps && extraProps(ctx)); } diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 2689af9a..21968c46 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -2,13 +2,23 @@ import { IIntegrationManager, IIntegrationFactoryParams } from '../integrations/ import { ISignalListener } from '../listeners/types'; import { ILogger } from '../logger/types'; import { ISdkReadinessManager } from '../readiness/types'; -import { ISdkClientFactoryParams } from '../sdkClient/types'; import { IFetch, ISplitApi, IEventSourceConstructor } from '../services/types'; import { IStorageAsync, IStorageSync, ISplitsCacheSync, ISplitsCacheAsync, IStorageFactoryParams } from '../storages/types'; import { ISyncManager, ISyncManagerFactoryParams } from '../sync/types'; import { IImpressionObserver } from '../trackers/impressionObserver/types'; +import { IImpressionsTracker, IEventTracker } from '../trackers/types'; import { SplitIO, ISettings, IEventEmitter } from '../types'; +export interface ISdkFactoryContext { + storage: IStorageSync | IStorageAsync, + sdkReadinessManager: ISdkReadinessManager, + settings: ISettings + impressionsTracker: IImpressionsTracker, + eventTracker: IEventTracker, + signalListener?: ISignalListener + syncManager?: ISyncManager, +} + /** * Environment related dependencies. * These getters are called a fixed number of times per factory instantiation. @@ -53,7 +63,7 @@ export interface ISdkFactoryParams { // Sdk client method factory (ISDK::client method). // It Allows to distinguish SDK clients with the client-side API (`ICsSDK`) or server-side API (`ISDK` or `IAsyncSDK`). - sdkClientMethodFactory: (params: ISdkClientFactoryParams) => ({ (): SplitIO.ICsClient; (key: SplitIO.SplitKey, trafficType?: string | undefined): SplitIO.ICsClient; } | (() => SplitIO.IClient) | (() => SplitIO.IAsyncClient)) + sdkClientMethodFactory: (params: ISdkFactoryContext) => ({ (): SplitIO.ICsClient; (key: SplitIO.SplitKey, trafficType?: string | undefined): SplitIO.ICsClient; } | (() => SplitIO.IClient) | (() => SplitIO.IAsyncClient)) // Optional signal listener constructor. Used to handle special app states, like shutdown, app paused or resumed. // Pass only if `syncManager` (used by Node listener) and `splitApi` (used by Browser listener) are passed. @@ -70,5 +80,5 @@ export interface ISdkFactoryParams { impressionsObserverFactory?: () => IImpressionObserver // Optional function to assign additional properties to the factory instance - extraProps?: (settings: ISettings, syncManager?: ISyncManager) => object + extraProps?: (params: ISdkFactoryContext) => object } diff --git a/src/sdkFactory/userConsentProps.ts b/src/sdkFactory/userConsentProps.ts deleted file mode 100644 index b275406c..00000000 --- a/src/sdkFactory/userConsentProps.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED, USER_CONSENT_NOT_UPDATED, USER_CONSENT_INITIAL } from '../logger/constants'; -import { ISyncManager } from '../sync/types'; -import { ISettings } from '../types'; -import { isConsentGranted } from '../utils/consent'; -import { CONSENT_GRANTED, CONSENT_DECLINED } from '../utils/constants'; -import { isBoolean } from '../utils/lang'; - -// Extend client-side factory instances with user consent getter/setter -export function userConsentProps(settings: ISettings, syncManager?: ISyncManager) { - - const log = settings.log; - - if (!isConsentGranted(settings)) log.info(USER_CONSENT_INITIAL, [settings.userConsent]); - - return { - setUserConsent(consent: unknown) { - // validate input param - if (!isBoolean(consent)) { - log.warn(ERROR_NOT_BOOLEAN, ['setUserConsent']); - return false; - } - - const newConsentStatus = consent ? CONSENT_GRANTED : CONSENT_DECLINED; - - if (settings.userConsent !== newConsentStatus) { - log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); // @ts-ignore, modify readonly prop - settings.userConsent = newConsentStatus; - - if (consent) syncManager?.submitter?.start(); // resumes submitters if transitioning to GRANTED - else syncManager?.submitter?.stop(); // pauses submitters if transitioning to DECLINED - } else { - log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]); - } - - return true; - }, - - getUserConsent() { - return settings.userConsent; - } - }; -} diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index b95be139..235c18c8 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -6,7 +6,7 @@ import { IPushManager } from './streaming/types'; import { IPollingManager, IPollingManagerCS } from './polling/types'; import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants'; import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants'; -import { isConsentGranted } from '../utils/consent'; +import { isConsentGranted } from '../consent'; /** * Online SyncManager factory. From dea639bc1e19f555cf38d6860bb839afb413ebf6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 5 Apr 2022 16:20:13 -0300 Subject: [PATCH 51/53] update in param --- src/sdkClient/sdkClient.ts | 4 ++-- src/sdkClient/sdkClientMethodCS.ts | 2 +- src/sdkClient/sdkClientMethodCSWithTT.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sdkClient/sdkClient.ts b/src/sdkClient/sdkClient.ts index 1ff86ba3..c8ad8dc2 100644 --- a/src/sdkClient/sdkClient.ts +++ b/src/sdkClient/sdkClient.ts @@ -8,7 +8,7 @@ import { ISdkFactoryContext } from '../sdkFactory/types'; /** * Creates an Sdk client, i.e., a base client with status and destroy interface */ -export function sdkClientFactory(params: ISdkFactoryContext, isMainClient = true): SplitIO.IClient | SplitIO.IAsyncClient { +export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: boolean): SplitIO.IClient | SplitIO.IAsyncClient { const { sdkReadinessManager, syncManager, storage, signalListener, settings } = params; return objectAssign( @@ -35,7 +35,7 @@ export function sdkClientFactory(params: ISdkFactoryContext, isMainClient = true signalListener && signalListener.stop(); // Release the API Key if it is the main client - if (isMainClient) releaseApiKey(settings.core.authorizationKey); + if (!isSharedClient) releaseApiKey(settings.core.authorizationKey); // Cleanup storage return storage.destroy(); diff --git a/src/sdkClient/sdkClientMethodCS.ts b/src/sdkClient/sdkClientMethodCS.ts index 3e6d6bdd..284c59ae 100644 --- a/src/sdkClient/sdkClientMethodCS.ts +++ b/src/sdkClient/sdkClientMethodCS.ts @@ -76,7 +76,7 @@ export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: Spl storage: sharedStorage || storage, syncManager: sharedSyncManager, signalListener: undefined, // only the main client "destroy" method stops the signal listener - }), false) as SplitIO.IClient, + }), true) as SplitIO.IClient, validKey ); diff --git a/src/sdkClient/sdkClientMethodCSWithTT.ts b/src/sdkClient/sdkClientMethodCSWithTT.ts index cdbe99cd..7abb36d8 100644 --- a/src/sdkClient/sdkClientMethodCSWithTT.ts +++ b/src/sdkClient/sdkClientMethodCSWithTT.ts @@ -86,7 +86,7 @@ export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: Spl storage: sharedStorage || storage, syncManager: sharedSyncManager, signalListener: undefined, // only the main client "destroy" method stops the signal listener - }), false) as SplitIO.IClient, + }), true) as SplitIO.IClient, validKey, validTrafficType ); From 50fbef4a2041a2d08e47415325cdd1ff47440653 Mon Sep 17 00:00:00 2001 From: Matias Melograno Date: Tue, 5 Apr 2022 17:27:05 -0300 Subject: [PATCH 52/53] added gha for updating license year --- .github/workflows/update-license-year.yml | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/update-license-year.yml diff --git a/.github/workflows/update-license-year.yml b/.github/workflows/update-license-year.yml new file mode 100644 index 00000000..c8fd1684 --- /dev/null +++ b/.github/workflows/update-license-year.yml @@ -0,0 +1,49 @@ +name: Update License Year + +on: + schedule: + - cron: "0 3 1 1 *" # 03:00 AM on January 1 + push: + branches: + - main + - master + +permissions: + contents: write + pull-requests: write + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set Current year + run: "echo CURRENT=$(date +%Y) >> $GITHUB_ENV" + + - name: Set Previous Year + run: "echo PREVIOUS=$(($CURRENT-1)) >> $GITHUB_ENV" + + - name: Update LICENSE + uses: jacobtomlinson/gha-find-replace@v2 + with: + find: ${{ env.PREVIOUS }} + replace: ${{ env.CURRENT }} + include: "LICENSE" + regex: false + + - name: Commit files + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git commit -m "Updated License Year" -a + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: Update License Year + branch: update-license From b849b8f12ecb322203f1e94369bf08627d92287d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 6 Apr 2022 12:01:45 -0300 Subject: [PATCH 53/53] update changelog --- CHANGES.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 16af0c07..7bf86465 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,16 +1,16 @@ -1.3.0 (April 4, 2022) - - Added support for user consent, a way to control if the SDK tracks impressions and events or not, based on users granting or rejecting consent for it. - - Added support for `scheduler.impressionsQueueSize` property on SDK configuration. - - Added support to Redis storage for NodeJS to accept TLS configuration options. +1.3.0 (April 6, 2022) + - Added user consent feature to allow delaying or disabling the data tracking from SDK until user consent is explicitly granted or declined. Read more in our docs. + - Added `scheduler.impressionsQueueSize` property to SDK configuration to limit the amount of impressions tracked in memory. Read more in our docs. + - Added support to accept TLS configuration options to the Redis storage in NodeJS. Read more in our docs. - Updated format for MySegments keys in LocalStorage, keeping backwards compatibility (issue https://github.com/splitio/javascript-client/issues/638). - - Updated some modules due to general polishing and refactors, including updates on some log messages. + - Updated some modules due to general polishing and refactors, including updates in some log messages. - Updated some dependencies for vulnerability fixes. - - Bugfixing - Fixed internal isObject utility function, to avoid false negatives on runtimes using multiple VM contexts, like NuxtJS dev server. + - Bugfixing - Updated internal isObject utility function, to avoid unexpected behaviors on frameworks and libraries that uses multiple VM contexts, like NuxtJS dev server. - Bugfixing - Fixed validation of `core.key` SDK configuration param, to parse it into a string and log a warning when passing a number (Related to issue https://github.com/splitio/react-native-client/issues/19). - Bugfixing - Fixed validation of `sync.impressionsMode` SDK configuration param, to avoid an exception on SplitFactory instantiation when passing a non-string value. - Bugfixing - Fixed an issue with `connectionTimeout` options params of Redis storage, that was being ignored and not passed down to the underlying ioredis client. - Bugfixing - Fixed streaming synchronization issue with multiple clients. - - Bugfixing - Fixed issue with internal Map ponyfill that result in logger not working on IE11 browser. + - Bugfixing - Fixed issue with internal Map ponyfill that results in logger not working properly on IE11 browser. 1.2.0 (January 19, 2022) - Added support to SDK clients on browser to optionally bind attributes to the client, keeping these loaded within the SDK along with the user ID, for easier usage when requesting flag.