@splitsoftware/splitio-commons 1.2.1-rc.4 → 1.2.1-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/listeners/browser.js +14 -10
- package/cjs/logger/constants.js +6 -3
- package/cjs/logger/messages/debug.js +3 -3
- package/cjs/logger/messages/error.js +3 -2
- package/cjs/logger/messages/info.js +4 -2
- package/cjs/sdkClient/client.js +10 -4
- package/cjs/sdkFactory/index.js +5 -4
- package/cjs/sdkFactory/userConsentProps.js +37 -0
- package/cjs/storages/KeyBuilder.js +1 -5
- package/cjs/storages/KeyBuilderCS.js +11 -1
- package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +23 -3
- package/cjs/sync/streaming/SSEClient/index.js +2 -1
- package/cjs/sync/submitters/eventsSyncTask.js +14 -3
- package/cjs/sync/submitters/impressionsSyncTask.js +6 -2
- package/cjs/sync/syncManagerOnline.js +11 -7
- package/cjs/utils/consent.js +10 -0
- package/cjs/utils/constants/index.js +5 -1
- package/cjs/utils/lang/index.js +8 -1
- package/cjs/utils/settingsValidation/consent.js +16 -0
- package/cjs/utils/settingsValidation/impressionsMode.js +6 -6
- package/cjs/utils/settingsValidation/index.js +4 -1
- package/esm/listeners/browser.js +14 -10
- package/esm/logger/constants.js +4 -1
- package/esm/logger/messages/debug.js +3 -3
- package/esm/logger/messages/error.js +3 -2
- package/esm/logger/messages/info.js +4 -2
- package/esm/sdkClient/client.js +11 -5
- package/esm/sdkFactory/index.js +5 -4
- package/esm/sdkFactory/userConsentProps.js +33 -0
- package/esm/storages/KeyBuilder.js +2 -6
- package/esm/storages/KeyBuilderCS.js +11 -1
- package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +23 -3
- package/esm/sync/streaming/SSEClient/index.js +2 -1
- package/esm/sync/submitters/eventsSyncTask.js +14 -3
- package/esm/sync/submitters/impressionsSyncTask.js +6 -2
- package/esm/sync/syncManagerOnline.js +11 -7
- package/esm/utils/consent.js +6 -0
- package/esm/utils/constants/index.js +4 -0
- package/esm/utils/lang/index.js +6 -0
- package/esm/utils/settingsValidation/consent.js +12 -0
- package/esm/utils/settingsValidation/impressionsMode.js +7 -7
- package/esm/utils/settingsValidation/index.js +4 -1
- package/package.json +1 -1
- package/src/listeners/browser.ts +13 -9
- package/src/logger/constants.ts +4 -1
- package/src/logger/messages/debug.ts +3 -3
- package/src/logger/messages/error.ts +3 -2
- package/src/logger/messages/info.ts +4 -2
- package/src/sdkClient/client.ts +7 -5
- package/src/sdkFactory/index.ts +5 -4
- package/src/sdkFactory/types.ts +2 -0
- package/src/sdkFactory/userConsentProps.ts +40 -0
- package/src/storages/KeyBuilder.ts +2 -6
- package/src/storages/KeyBuilderCS.ts +13 -1
- package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +23 -3
- package/src/sync/streaming/SSEClient/index.ts +2 -1
- package/src/sync/submitters/eventsSyncTask.ts +14 -3
- package/src/sync/submitters/impressionsSyncTask.ts +6 -2
- package/src/sync/syncManagerOnline.ts +13 -7
- package/src/sync/types.ts +4 -1
- package/src/types.ts +6 -0
- package/src/utils/consent.ts +8 -0
- package/src/utils/constants/index.ts +5 -0
- package/src/utils/lang/index.ts +8 -1
- package/src/utils/settingsValidation/consent.ts +16 -0
- package/src/utils/settingsValidation/impressionsMode.ts +8 -8
- package/src/utils/settingsValidation/index.ts +5 -1
- package/src/utils/settingsValidation/types.ts +2 -0
- package/types/logger/constants.d.ts +4 -1
- package/types/sdkFactory/types.d.ts +1 -0
- package/types/sdkFactory/userConsentProps.d.ts +6 -0
- package/types/storages/KeyBuilderCS.d.ts +2 -0
- package/types/sync/types.d.ts +3 -0
- package/types/types.d.ts +6 -0
- package/types/utils/consent.d.ts +2 -0
- package/types/utils/constants/index.d.ts +3 -0
- package/types/utils/lang/index.d.ts +4 -0
- package/types/utils/settingsValidation/consent.d.ts +6 -0
- package/types/utils/settingsValidation/impressionsMode.d.ts +1 -1
- package/types/utils/settingsValidation/types.d.ts +2 -0
- package/types/utils/settingsValidation/userConsent.d.ts +5 -0
package/src/sdkClient/client.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { getMatching, getBucketing } from '../utils/key';
|
|
|
4
4
|
import { validateSplitExistance } from '../utils/inputValidation/splitExistance';
|
|
5
5
|
import { validateTrafficTypeExistance } from '../utils/inputValidation/trafficTypeExistance';
|
|
6
6
|
import { SDK_NOT_READY } from '../utils/labels';
|
|
7
|
-
import { CONTROL } from '../utils/constants';
|
|
7
|
+
import { CONSENT_DECLINED, CONTROL } from '../utils/constants';
|
|
8
8
|
import { IClientFactoryParams } from './types';
|
|
9
9
|
import { IEvaluationResult } from '../evaluator/types';
|
|
10
10
|
import { SplitIO, ImpressionDTO } from '../types';
|
|
@@ -16,13 +16,14 @@ import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants';
|
|
|
16
16
|
*/
|
|
17
17
|
// @TODO missing time tracking to collect telemetry
|
|
18
18
|
export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | SplitIO.IAsyncClient {
|
|
19
|
-
const { sdkReadinessManager: { readinessManager }, storage, settings
|
|
19
|
+
const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker } = params;
|
|
20
|
+
const { log, mode } = settings;
|
|
20
21
|
|
|
21
22
|
function getTreatment(key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, withConfig = false) {
|
|
22
23
|
const wrapUp = (evaluationResult: IEvaluationResult) => {
|
|
23
24
|
const queue: ImpressionDTO[] = [];
|
|
24
25
|
const treatment = processEvaluation(evaluationResult, splitName, key, attributes, withConfig, `getTreatment${withConfig ? 'withConfig' : ''}`, queue);
|
|
25
|
-
impressionsTracker.track(queue, attributes);
|
|
26
|
+
if (settings.userConsent !== CONSENT_DECLINED) impressionsTracker.track(queue, attributes);
|
|
26
27
|
return treatment;
|
|
27
28
|
};
|
|
28
29
|
|
|
@@ -42,7 +43,7 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S
|
|
|
42
43
|
Object.keys(evaluationResults).forEach(splitName => {
|
|
43
44
|
treatments[splitName] = processEvaluation(evaluationResults[splitName], splitName, key, attributes, withConfig, `getTreatments${withConfig ? 'withConfig' : ''}`, queue);
|
|
44
45
|
});
|
|
45
|
-
impressionsTracker.track(queue, attributes);
|
|
46
|
+
if (settings.userConsent !== CONSENT_DECLINED) impressionsTracker.track(queue, attributes);
|
|
46
47
|
return treatments;
|
|
47
48
|
};
|
|
48
49
|
|
|
@@ -115,7 +116,8 @@ export function clientFactory(params: IClientFactoryParams): SplitIO.IClient | S
|
|
|
115
116
|
// 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.
|
|
116
117
|
validateTrafficTypeExistance(log, readinessManager, storage.splits, mode, trafficTypeName, 'track');
|
|
117
118
|
|
|
118
|
-
return eventTracker.track(eventData, size);
|
|
119
|
+
if (settings.userConsent !== CONSENT_DECLINED) return eventTracker.track(eventData, size);
|
|
120
|
+
else return false;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
return {
|
package/src/sdkFactory/index.ts
CHANGED
|
@@ -12,13 +12,14 @@ import { createLoggerAPI } from '../logger/sdkLogger';
|
|
|
12
12
|
import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants';
|
|
13
13
|
import { metadataBuilder } from '../storages/metadataBuilder';
|
|
14
14
|
import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../readiness/constants';
|
|
15
|
+
import { objectAssign } from '../utils/lang/objectAssign';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Modular SDK factory
|
|
18
19
|
*/
|
|
19
20
|
export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.ISDK | SplitIO.IAsyncSDK {
|
|
20
21
|
|
|
21
|
-
const { settings, platform, storageFactory, splitApiFactory,
|
|
22
|
+
const { settings, platform, storageFactory, splitApiFactory, extraProps,
|
|
22
23
|
syncManagerFactory, SignalListener, impressionsObserverFactory, impressionListener,
|
|
23
24
|
integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory } = params;
|
|
24
25
|
const log = settings.log;
|
|
@@ -88,12 +89,12 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
|
|
|
88
89
|
|
|
89
90
|
log.info(NEW_FACTORY);
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
return objectAssign({
|
|
92
94
|
// Split evaluation and event tracking engine
|
|
93
95
|
client: clientMethod,
|
|
94
96
|
|
|
95
97
|
// Manager API to explore available information
|
|
96
|
-
// @ts-ignore
|
|
97
98
|
manager() {
|
|
98
99
|
log.debug(RETRIEVE_MANAGER);
|
|
99
100
|
return managerInstance;
|
|
@@ -103,5 +104,5 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
|
|
|
103
104
|
Logger: createLoggerAPI(settings.log),
|
|
104
105
|
|
|
105
106
|
settings,
|
|
106
|
-
};
|
|
107
|
+
}, extraProps && extraProps(settings, syncManager));
|
|
107
108
|
}
|
package/src/sdkFactory/types.ts
CHANGED
|
@@ -70,4 +70,6 @@ export interface ISdkFactoryParams {
|
|
|
70
70
|
// Impression observer factory. If provided, will be used for impressions dedupe
|
|
71
71
|
impressionsObserverFactory?: () => IImpressionObserver
|
|
72
72
|
|
|
73
|
+
// Optional function to assign additional properties to the factory instance
|
|
74
|
+
extraProps?: (settings: ISettings, syncManager?: ISyncManager) => object
|
|
73
75
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ERROR_NOT_BOOLEAN, USER_CONSENT_UPDATED, USER_CONSENT_NOT_UPDATED } from '../logger/constants';
|
|
2
|
+
import { ISyncManager } from '../sync/types';
|
|
3
|
+
import { ISettings } from '../types';
|
|
4
|
+
import { CONSENT_GRANTED, CONSENT_DECLINED } from '../utils/constants';
|
|
5
|
+
import { isBoolean } from '../utils/lang';
|
|
6
|
+
|
|
7
|
+
// Extend client-side factory instances with user consent getter/setter
|
|
8
|
+
export function userConsentProps(settings: ISettings, syncManager?: ISyncManager) {
|
|
9
|
+
|
|
10
|
+
const log = settings.log;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
setUserConsent(consent: unknown) {
|
|
14
|
+
// validate input param
|
|
15
|
+
if (!isBoolean(consent)) {
|
|
16
|
+
log.warn(ERROR_NOT_BOOLEAN, ['setUserConsent']);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const newConsentStatus = consent ? CONSENT_GRANTED : CONSENT_DECLINED;
|
|
21
|
+
|
|
22
|
+
if (settings.userConsent !== newConsentStatus) { // @ts-ignore, modify readonly prop
|
|
23
|
+
settings.userConsent = newConsentStatus;
|
|
24
|
+
|
|
25
|
+
if (consent) syncManager?.submitter?.start(); // resumes submitters if transitioning to GRANTED
|
|
26
|
+
else syncManager?.submitter?.stop(); // pauses submitters if transitioning to DECLINED
|
|
27
|
+
|
|
28
|
+
log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]);
|
|
29
|
+
} else {
|
|
30
|
+
log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
getUserConsent() {
|
|
37
|
+
return settings.userConsent;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { startsWith } from '../utils/lang';
|
|
2
2
|
|
|
3
3
|
const everythingAtTheEnd = /[^.]+$/;
|
|
4
4
|
|
|
5
5
|
const DEFAULT_PREFIX = 'SPLITIO';
|
|
6
6
|
|
|
7
7
|
export function validatePrefix(prefix: unknown) {
|
|
8
|
-
return prefix
|
|
9
|
-
endsWith(prefix, '.' + DEFAULT_PREFIX) ?
|
|
10
|
-
prefix : // suffix already appended
|
|
11
|
-
prefix + '.' + DEFAULT_PREFIX : // append suffix
|
|
12
|
-
DEFAULT_PREFIX; // use default prefix if none is provided
|
|
8
|
+
return prefix ? prefix + '.SPLITIO' : 'SPLITIO';
|
|
13
9
|
}
|
|
14
10
|
|
|
15
11
|
export class KeyBuilder {
|
|
@@ -16,10 +16,22 @@ export class KeyBuilderCS extends KeyBuilder {
|
|
|
16
16
|
* @override
|
|
17
17
|
*/
|
|
18
18
|
buildSegmentNameKey(segmentName: string) {
|
|
19
|
-
return `${this.
|
|
19
|
+
return `${this.prefix}.${this.matchingKey}.segment.${segmentName}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
extractSegmentName(builtSegmentKeyName: string) {
|
|
23
|
+
const prefix = `${this.prefix}.${this.matchingKey}.segment.`;
|
|
24
|
+
|
|
25
|
+
if (startsWith(builtSegmentKeyName, prefix))
|
|
26
|
+
return builtSegmentKeyName.substr(prefix.length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// @BREAKING: The key used to start with the matching key instead of the prefix, this was changed on version 10.17.3
|
|
30
|
+
buildOldSegmentNameKey(segmentName: string) {
|
|
31
|
+
return `${this.matchingKey}.${this.prefix}.segment.${segmentName}`;
|
|
32
|
+
}
|
|
33
|
+
// @BREAKING: The key used to start with the matching key instead of the prefix, this was changed on version 10.17.3
|
|
34
|
+
extractOldSegmentKey(builtSegmentKeyName: string) {
|
|
23
35
|
const prefix = `${this.matchingKey}.${this.prefix}.segment.`;
|
|
24
36
|
|
|
25
37
|
if (startsWith(builtSegmentKeyName, prefix))
|
|
@@ -67,9 +67,29 @@ export class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync {
|
|
|
67
67
|
|
|
68
68
|
// Scan current values from localStorage
|
|
69
69
|
const storedSegmentNames = Object.keys(localStorage).reduce((accum, key) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
70
|
+
let segmentName = this.keys.extractSegmentName(key);
|
|
71
|
+
|
|
72
|
+
if (segmentName) {
|
|
73
|
+
accum.push(segmentName);
|
|
74
|
+
} else {
|
|
75
|
+
// @BREAKING: This is only to clean up "old" keys. Remove this whole else code block.
|
|
76
|
+
segmentName = this.keys.extractOldSegmentKey(key);
|
|
77
|
+
|
|
78
|
+
if (segmentName) { // this was an old segment key, let's clean up.
|
|
79
|
+
const newSegmentKey = this.keys.buildSegmentNameKey(segmentName);
|
|
80
|
+
try {
|
|
81
|
+
// If the new format key is not there, create it.
|
|
82
|
+
if (!localStorage.getItem(newSegmentKey) && names.indexOf(segmentName) > -1) {
|
|
83
|
+
localStorage.setItem(newSegmentKey, DEFINED);
|
|
84
|
+
// we are migrating a segment, let's track it.
|
|
85
|
+
accum.push(segmentName);
|
|
86
|
+
}
|
|
87
|
+
localStorage.removeItem(key); // we migrated the current key, let's delete it.
|
|
88
|
+
} catch (e) {
|
|
89
|
+
this.log.error(e);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
73
93
|
|
|
74
94
|
return accum;
|
|
75
95
|
}, [] as string[]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IEventSourceConstructor } from '../../../services/types';
|
|
2
2
|
import { ISettings } from '../../../types';
|
|
3
|
+
import { isString } from '../../../utils/lang';
|
|
3
4
|
import { IAuthTokenPushEnabled } from '../AuthClient/types';
|
|
4
5
|
import { ISSEClient, ISseEventHandler } from './types';
|
|
5
6
|
|
|
@@ -15,7 +16,7 @@ const CONTROL_CHANNEL_REGEX = /^control_/;
|
|
|
15
16
|
*/
|
|
16
17
|
function buildSSEHeaders(settings: ISettings) {
|
|
17
18
|
const headers: Record<string, string> = {
|
|
18
|
-
SplitSDKClientKey: settings.core.authorizationKey.slice(-4),
|
|
19
|
+
SplitSDKClientKey: isString(settings.core.authorizationKey) ? settings.core.authorizationKey.slice(-4) : '',
|
|
19
20
|
SplitSDKVersion: settings.version,
|
|
20
21
|
};
|
|
21
22
|
|
|
@@ -22,24 +22,35 @@ export function eventsSyncTaskFactory(
|
|
|
22
22
|
// don't retry events.
|
|
23
23
|
const syncTask = submitterSyncTaskFactory(log, postEventsBulk, eventsCache, eventsPushRate, DATA_NAME, latencyTracker);
|
|
24
24
|
|
|
25
|
-
// Set a timer for the first push of events
|
|
25
|
+
// Set a timer for the first push window of events.
|
|
26
|
+
// Not implemented in the base submitter or sync task, since this feature is only used by the events submitter.
|
|
26
27
|
if (eventsFirstPushWindow > 0) {
|
|
28
|
+
let running = false;
|
|
27
29
|
let stopEventPublisherTimeout: ReturnType<typeof setTimeout>;
|
|
28
30
|
const originalStart = syncTask.start;
|
|
29
31
|
syncTask.start = () => {
|
|
32
|
+
running = true;
|
|
30
33
|
stopEventPublisherTimeout = setTimeout(originalStart, eventsFirstPushWindow);
|
|
31
34
|
};
|
|
32
35
|
const originalStop = syncTask.stop;
|
|
33
36
|
syncTask.stop = () => {
|
|
37
|
+
running = false;
|
|
34
38
|
clearTimeout(stopEventPublisherTimeout);
|
|
35
39
|
originalStop();
|
|
36
40
|
};
|
|
41
|
+
syncTask.isRunning = () => {
|
|
42
|
+
return running;
|
|
43
|
+
};
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
// register events submitter to be executed when events cache is full
|
|
40
47
|
eventsCache.setOnFullQueueCb(() => {
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
if (syncTask.isRunning()) {
|
|
49
|
+
log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]);
|
|
50
|
+
syncTask.execute();
|
|
51
|
+
}
|
|
52
|
+
// If submitter is stopped (e.g., user consent declined or unknown, or app state offline), we don't send the data.
|
|
53
|
+
// Data will be sent when submitter is resumed.
|
|
43
54
|
});
|
|
44
55
|
|
|
45
56
|
return syncTask;
|
|
@@ -57,8 +57,12 @@ export function impressionsSyncTaskFactory(
|
|
|
57
57
|
|
|
58
58
|
// register impressions submitter to be executed when impressions cache is full
|
|
59
59
|
impressionsCache.setOnFullQueueCb(() => {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if (syncTask.isRunning()) {
|
|
61
|
+
log.info(SUBMITTERS_PUSH_FULL_QUEUE, [DATA_NAME]);
|
|
62
|
+
syncTask.execute();
|
|
63
|
+
}
|
|
64
|
+
// If submitter is stopped (e.g., user consent declined or unknown, or app state offline), we don't send the data.
|
|
65
|
+
// Data will be sent when submitter is resumed.
|
|
62
66
|
});
|
|
63
67
|
|
|
64
68
|
return syncTask;
|
|
@@ -6,6 +6,7 @@ import { IPushManager } from './streaming/types';
|
|
|
6
6
|
import { IPollingManager, IPollingManagerCS } from './polling/types';
|
|
7
7
|
import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants';
|
|
8
8
|
import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants';
|
|
9
|
+
import { isConsentGranted } from '../utils/consent';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Online SyncManager factory.
|
|
@@ -25,7 +26,7 @@ export function syncManagerOnlineFactory(
|
|
|
25
26
|
*/
|
|
26
27
|
return function (params: ISyncManagerFactoryParams): ISyncManagerCS {
|
|
27
28
|
|
|
28
|
-
const { log, streamingEnabled } = params
|
|
29
|
+
const { settings, settings: { log, streamingEnabled } } = params;
|
|
29
30
|
|
|
30
31
|
/** Polling Manager */
|
|
31
32
|
const pollingManager = pollingManagerFactory && pollingManagerFactory(params);
|
|
@@ -39,7 +40,6 @@ export function syncManagerOnlineFactory(
|
|
|
39
40
|
// It is not inyected as push and polling managers, because at the moment it is required
|
|
40
41
|
const submitter = submitterManagerFactory(params);
|
|
41
42
|
|
|
42
|
-
|
|
43
43
|
/** Sync Manager logic */
|
|
44
44
|
|
|
45
45
|
function startPolling() {
|
|
@@ -69,12 +69,18 @@ export function syncManagerOnlineFactory(
|
|
|
69
69
|
let startFirstTime = true; // flag to distinguish calling the `start` method for the first time, to support pausing and resuming the synchronization
|
|
70
70
|
|
|
71
71
|
return {
|
|
72
|
+
// Exposed for fine-grained control of synchronization.
|
|
73
|
+
// E.g.: user consent, app state changes (Page hide, Foreground/Background, Online/Offline).
|
|
74
|
+
pollingManager,
|
|
72
75
|
pushManager,
|
|
76
|
+
submitter,
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
79
|
* Method used to start the syncManager for the first time, or resume it after being stopped.
|
|
76
80
|
*/
|
|
77
81
|
start() {
|
|
82
|
+
running = true;
|
|
83
|
+
|
|
78
84
|
// start syncing splits and segments
|
|
79
85
|
if (pollingManager) {
|
|
80
86
|
if (pushManager) {
|
|
@@ -90,21 +96,21 @@ export function syncManagerOnlineFactory(
|
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
// start periodic data recording (events, impressions, telemetry).
|
|
93
|
-
if (
|
|
94
|
-
running = true;
|
|
99
|
+
if (isConsentGranted(settings)) submitter.start();
|
|
95
100
|
},
|
|
96
101
|
|
|
97
102
|
/**
|
|
98
103
|
* Method used to stop/pause the syncManager.
|
|
99
104
|
*/
|
|
100
105
|
stop() {
|
|
106
|
+
running = false;
|
|
107
|
+
|
|
101
108
|
// stop syncing
|
|
102
109
|
if (pushManager) pushManager.stop();
|
|
103
110
|
if (pollingManager && pollingManager.isRunning()) pollingManager.stop();
|
|
104
111
|
|
|
105
112
|
// stop periodic data recording (events, impressions, telemetry).
|
|
106
|
-
|
|
107
|
-
running = false;
|
|
113
|
+
submitter.stop();
|
|
108
114
|
},
|
|
109
115
|
|
|
110
116
|
isRunning() {
|
|
@@ -112,7 +118,7 @@ export function syncManagerOnlineFactory(
|
|
|
112
118
|
},
|
|
113
119
|
|
|
114
120
|
flush() {
|
|
115
|
-
if (
|
|
121
|
+
if (isConsentGranted(settings)) return submitter.execute();
|
|
116
122
|
else return Promise.resolve();
|
|
117
123
|
},
|
|
118
124
|
|
package/src/sync/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { IPlatform } from '../sdkFactory/types';
|
|
|
3
3
|
import { ISplitApi } from '../services/types';
|
|
4
4
|
import { IStorageSync } from '../storages/types';
|
|
5
5
|
import { ISettings } from '../types';
|
|
6
|
+
import { IPollingManager } from './polling/types';
|
|
6
7
|
import { IPushManager } from './streaming/types';
|
|
7
8
|
|
|
8
9
|
export interface ITask<Input extends any[] = []> {
|
|
@@ -43,7 +44,9 @@ export interface ITimeTracker {
|
|
|
43
44
|
|
|
44
45
|
export interface ISyncManager extends ITask {
|
|
45
46
|
flush(): Promise<any>,
|
|
46
|
-
pushManager?: IPushManager
|
|
47
|
+
pushManager?: IPushManager,
|
|
48
|
+
pollingManager?: IPollingManager,
|
|
49
|
+
submitter?: ISyncTask
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export interface ISyncManagerCS extends ISyncManager {
|
package/src/types.ts
CHANGED
|
@@ -54,6 +54,11 @@ type EventConsts = {
|
|
|
54
54
|
* @typedef {string} SDKMode
|
|
55
55
|
*/
|
|
56
56
|
export type SDKMode = 'standalone' | 'consumer' | 'localhost' | 'consumer_partial';
|
|
57
|
+
/**
|
|
58
|
+
* User consent status.
|
|
59
|
+
* @typedef {string} ConsentStatus
|
|
60
|
+
*/
|
|
61
|
+
export type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN';
|
|
57
62
|
/**
|
|
58
63
|
* Settings interface. This is a representation of the settings the SDK expose, that's why
|
|
59
64
|
* most of it's props are readonly. Only features should be rewritten when localhost mode is active.
|
|
@@ -111,6 +116,7 @@ export interface ISettings {
|
|
|
111
116
|
},
|
|
112
117
|
readonly log: ILogger
|
|
113
118
|
readonly impressionListener?: unknown
|
|
119
|
+
readonly userConsent?: ConsentStatus
|
|
114
120
|
}
|
|
115
121
|
/**
|
|
116
122
|
* Log levels.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ISettings } from '../types';
|
|
2
|
+
import { CONSENT_GRANTED } from './constants';
|
|
3
|
+
|
|
4
|
+
export function isConsentGranted(settings: ISettings) {
|
|
5
|
+
const userConsent = settings.userConsent;
|
|
6
|
+
// undefined userConsent is handled as granted (default)
|
|
7
|
+
return !userConsent || userConsent === CONSENT_GRANTED;
|
|
8
|
+
}
|
|
@@ -32,3 +32,8 @@ export const STORAGE_MEMORY: StorageType = 'MEMORY';
|
|
|
32
32
|
export const STORAGE_LOCALSTORAGE: StorageType = 'LOCALSTORAGE';
|
|
33
33
|
export const STORAGE_REDIS: StorageType = 'REDIS';
|
|
34
34
|
export const STORAGE_PLUGGABLE: StorageType = 'PLUGGABLE';
|
|
35
|
+
|
|
36
|
+
// User consent
|
|
37
|
+
export const CONSENT_GRANTED = 'GRANTED'; // The user has granted consent for tracking events and impressions
|
|
38
|
+
export const CONSENT_DECLINED = 'DECLINED'; // The user has declined consent for tracking events and impressions
|
|
39
|
+
export const CONSENT_UNKNOWN = 'UNKNOWN'; // The user has neither granted nor declined consent for tracking events and impressions
|
package/src/utils/lang/index.ts
CHANGED
|
@@ -89,7 +89,7 @@ export function get(obj: any, prop: any, val: any): any {
|
|
|
89
89
|
/**
|
|
90
90
|
* Parses an array into a map of different arrays, grouping by the specified prop value.
|
|
91
91
|
*/
|
|
92
|
-
export function groupBy<T extends Record<string, any
|
|
92
|
+
export function groupBy<T extends Record<string, any>>(source: T[], prop: string): Record<string, T[]> {
|
|
93
93
|
const map: Record<string, any[]> = {};
|
|
94
94
|
|
|
95
95
|
if (Array.isArray(source) && isString(prop)) {
|
|
@@ -164,6 +164,13 @@ export function isString(val: any): val is string {
|
|
|
164
164
|
return typeof val === 'string' || val instanceof String;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* String sanitizer. Returns the provided value converted to uppercase if it is a string.
|
|
169
|
+
*/
|
|
170
|
+
export function stringToUpperCase(val: any) {
|
|
171
|
+
return isString(val) ? val.toUpperCase() : val;
|
|
172
|
+
}
|
|
173
|
+
|
|
167
174
|
/**
|
|
168
175
|
* Deep copy version of Object.assign using recursion.
|
|
169
176
|
* There are some assumptions here. It's for internal use and we don't need verbose errors
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants';
|
|
2
|
+
import { ILogger } from '../../logger/types';
|
|
3
|
+
import { ConsentStatus } from '../../types';
|
|
4
|
+
import { CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN } from '../constants';
|
|
5
|
+
import { stringToUpperCase } from '../lang';
|
|
6
|
+
|
|
7
|
+
const userConsentValues = [CONSENT_DECLINED, CONSENT_GRANTED, CONSENT_UNKNOWN];
|
|
8
|
+
|
|
9
|
+
export function validateConsent({ userConsent, log }: { userConsent?: any, log: ILogger }): ConsentStatus {
|
|
10
|
+
userConsent = stringToUpperCase(userConsent);
|
|
11
|
+
|
|
12
|
+
if (userConsentValues.indexOf(userConsent) > -1) return userConsent;
|
|
13
|
+
|
|
14
|
+
log.error(ERROR_INVALID_CONFIG_PARAM, ['userConsent', userConsentValues, CONSENT_GRANTED]);
|
|
15
|
+
return CONSENT_GRANTED;
|
|
16
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants';
|
|
2
2
|
import { ILogger } from '../../logger/types';
|
|
3
3
|
import { SplitIO } from '../../types';
|
|
4
4
|
import { DEBUG, OPTIMIZED } from '../constants';
|
|
5
|
+
import { stringToUpperCase } from '../lang';
|
|
5
6
|
|
|
6
|
-
export function validImpressionsMode(log: ILogger, impressionsMode:
|
|
7
|
-
impressionsMode = impressionsMode
|
|
8
|
-
if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) === -1) {
|
|
9
|
-
log.error(ERROR_INVALID_IMPRESSIONS_MODE, [[DEBUG, OPTIMIZED], OPTIMIZED]);
|
|
10
|
-
impressionsMode = OPTIMIZED;
|
|
11
|
-
}
|
|
7
|
+
export function validImpressionsMode(log: ILogger, impressionsMode: any): SplitIO.ImpressionsMode {
|
|
8
|
+
impressionsMode = stringToUpperCase(impressionsMode);
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
if ([DEBUG, OPTIMIZED].indexOf(impressionsMode) > -1) return impressionsMode;
|
|
11
|
+
|
|
12
|
+
log.error(ERROR_INVALID_CONFIG_PARAM, ['impressionsMode', [DEBUG, OPTIMIZED], OPTIMIZED]);
|
|
13
|
+
return OPTIMIZED;
|
|
14
14
|
}
|
|
@@ -97,7 +97,7 @@ function fromSecondsToMillis(n: number) {
|
|
|
97
97
|
*/
|
|
98
98
|
export function settingsValidation(config: unknown, validationParams: ISettingsValidationParams) {
|
|
99
99
|
|
|
100
|
-
const { defaults, runtime, storage, integrations, logger, localhost } = validationParams;
|
|
100
|
+
const { defaults, runtime, storage, integrations, logger, localhost, consent } = validationParams;
|
|
101
101
|
|
|
102
102
|
// creates a settings object merging base, defaults and config objects.
|
|
103
103
|
const withDefaults = merge({}, base, defaults, config) as ISettings;
|
|
@@ -161,5 +161,9 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
|
|
|
161
161
|
// ensure a valid impressionsMode
|
|
162
162
|
withDefaults.sync.impressionsMode = validImpressionsMode(log, withDefaults.sync.impressionsMode);
|
|
163
163
|
|
|
164
|
+
// ensure a valid user consent value
|
|
165
|
+
// @ts-ignore, modify readonly prop
|
|
166
|
+
withDefaults.userConsent = consent(withDefaults);
|
|
167
|
+
|
|
164
168
|
return withDefaults;
|
|
165
169
|
}
|
|
@@ -20,4 +20,6 @@ export interface ISettingsValidationParams {
|
|
|
20
20
|
logger: (settings: ISettings) => ISettings['log'],
|
|
21
21
|
/** Localhost mode validator (`settings.sync.localhostMode`) */
|
|
22
22
|
localhost?: (settings: ISettings) => ISettings['sync']['localhostMode'],
|
|
23
|
+
/** User consent validator (`settings.userConsent`) */
|
|
24
|
+
consent: (settings: ISettings) => ISettings['userConsent'],
|
|
23
25
|
}
|
|
@@ -67,6 +67,8 @@ export declare const SYNC_CONTINUE_POLLING = 118;
|
|
|
67
67
|
export declare const SYNC_STOP_POLLING = 119;
|
|
68
68
|
export declare const EVENTS_TRACKER_SUCCESS = 120;
|
|
69
69
|
export declare const IMPRESSIONS_TRACKER_SUCCESS = 121;
|
|
70
|
+
export declare const USER_CONSENT_UPDATED = 122;
|
|
71
|
+
export declare const USER_CONSENT_NOT_UPDATED = 123;
|
|
70
72
|
export declare const ENGINE_VALUE_INVALID = 200;
|
|
71
73
|
export declare const ENGINE_VALUE_NO_ATTRIBUTES = 201;
|
|
72
74
|
export declare const CLIENT_NO_LISTENER = 202;
|
|
@@ -112,10 +114,11 @@ export declare const ERROR_INVALID_KEY_OBJECT = 317;
|
|
|
112
114
|
export declare const ERROR_INVALID = 318;
|
|
113
115
|
export declare const ERROR_EMPTY = 319;
|
|
114
116
|
export declare const ERROR_EMPTY_ARRAY = 320;
|
|
115
|
-
export declare const
|
|
117
|
+
export declare const ERROR_INVALID_CONFIG_PARAM = 321;
|
|
116
118
|
export declare const ERROR_HTTP = 322;
|
|
117
119
|
export declare const ERROR_LOCALHOST_MODULE_REQUIRED = 323;
|
|
118
120
|
export declare const ERROR_STORAGE_INVALID = 324;
|
|
121
|
+
export declare const ERROR_NOT_BOOLEAN = 325;
|
|
119
122
|
export declare const LOG_PREFIX_SETTINGS = "settings";
|
|
120
123
|
export declare const LOG_PREFIX_INSTANTIATION = "Factory instantiation";
|
|
121
124
|
export declare const LOG_PREFIX_ENGINE = "engine";
|
|
@@ -39,4 +39,5 @@ export interface ISdkFactoryParams {
|
|
|
39
39
|
impressionListener?: SplitIO.IImpressionListener;
|
|
40
40
|
integrationsManagerFactory?: (params: IIntegrationFactoryParams) => IIntegrationManager | undefined;
|
|
41
41
|
impressionsObserverFactory?: () => IImpressionObserver;
|
|
42
|
+
extraProps?: (settings: ISettings, syncManager?: ISyncManager) => object;
|
|
42
43
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ISyncManager } from '../sync/types';
|
|
2
|
+
import { ISettings } from '../types';
|
|
3
|
+
export declare function userConsentProps(settings: ISettings, syncManager?: ISyncManager): {
|
|
4
|
+
setUserConsent(consent: unknown): boolean;
|
|
5
|
+
getUserConsent(): import("../types").ConsentStatus | undefined;
|
|
6
|
+
};
|
|
@@ -8,6 +8,8 @@ export declare class KeyBuilderCS extends KeyBuilder {
|
|
|
8
8
|
*/
|
|
9
9
|
buildSegmentNameKey(segmentName: string): string;
|
|
10
10
|
extractSegmentName(builtSegmentKeyName: string): string | undefined;
|
|
11
|
+
buildOldSegmentNameKey(segmentName: string): string;
|
|
12
|
+
extractOldSegmentKey(builtSegmentKeyName: string): string | undefined;
|
|
11
13
|
buildLastUpdatedKey(): string;
|
|
12
14
|
isSplitsCacheKey(key: string): boolean;
|
|
13
15
|
buildSplitsFilterQueryKey(): string;
|
package/types/sync/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { IPlatform } from '../sdkFactory/types';
|
|
|
3
3
|
import { ISplitApi } from '../services/types';
|
|
4
4
|
import { IStorageSync } from '../storages/types';
|
|
5
5
|
import { ISettings } from '../types';
|
|
6
|
+
import { IPollingManager } from './polling/types';
|
|
6
7
|
import { IPushManager } from './streaming/types';
|
|
7
8
|
export interface ITask<Input extends any[] = []> {
|
|
8
9
|
/**
|
|
@@ -39,6 +40,8 @@ export interface ITimeTracker {
|
|
|
39
40
|
export interface ISyncManager extends ITask {
|
|
40
41
|
flush(): Promise<any>;
|
|
41
42
|
pushManager?: IPushManager;
|
|
43
|
+
pollingManager?: IPollingManager;
|
|
44
|
+
submitter?: ISyncTask;
|
|
42
45
|
}
|
|
43
46
|
export interface ISyncManagerCS extends ISyncManager {
|
|
44
47
|
shared(matchingKey: string, readinessManager: IReadinessManager, storage: IStorageSync): ISyncManager | undefined;
|
package/types/types.d.ts
CHANGED
|
@@ -48,6 +48,11 @@ declare type EventConsts = {
|
|
|
48
48
|
* @typedef {string} SDKMode
|
|
49
49
|
*/
|
|
50
50
|
export declare type SDKMode = 'standalone' | 'consumer' | 'localhost' | 'consumer_partial';
|
|
51
|
+
/**
|
|
52
|
+
* User consent status.
|
|
53
|
+
* @typedef {string} ConsentStatus
|
|
54
|
+
*/
|
|
55
|
+
export declare type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN';
|
|
51
56
|
/**
|
|
52
57
|
* Settings interface. This is a representation of the settings the SDK expose, that's why
|
|
53
58
|
* most of it's props are readonly. Only features should be rewritten when localhost mode is active.
|
|
@@ -105,6 +110,7 @@ export interface ISettings {
|
|
|
105
110
|
};
|
|
106
111
|
readonly log: ILogger;
|
|
107
112
|
readonly impressionListener?: unknown;
|
|
113
|
+
readonly userConsent?: ConsentStatus;
|
|
108
114
|
}
|
|
109
115
|
/**
|
|
110
116
|
* Log levels.
|
|
@@ -20,3 +20,6 @@ export declare const STORAGE_MEMORY: StorageType;
|
|
|
20
20
|
export declare const STORAGE_LOCALSTORAGE: StorageType;
|
|
21
21
|
export declare const STORAGE_REDIS: StorageType;
|
|
22
22
|
export declare const STORAGE_PLUGGABLE: StorageType;
|
|
23
|
+
export declare const CONSENT_GRANTED = "GRANTED";
|
|
24
|
+
export declare const CONSENT_DECLINED = "DECLINED";
|
|
25
|
+
export declare const CONSENT_UNKNOWN = "UNKNOWN";
|
|
@@ -62,6 +62,10 @@ export declare function isObject(obj: any): boolean;
|
|
|
62
62
|
* Checks if a given value is a string.
|
|
63
63
|
*/
|
|
64
64
|
export declare function isString(val: any): val is string;
|
|
65
|
+
/**
|
|
66
|
+
* String sanitizer. Returns the provided value converted to uppercase if it is a string.
|
|
67
|
+
*/
|
|
68
|
+
export declare function stringToUpperCase(val: any): any;
|
|
65
69
|
/**
|
|
66
70
|
* Deep copy version of Object.assign using recursion.
|
|
67
71
|
* There are some assumptions here. It's for internal use and we don't need verbose errors
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ILogger } from '../../logger/types';
|
|
2
2
|
import { SplitIO } from '../../types';
|
|
3
|
-
export declare function validImpressionsMode(log: ILogger, impressionsMode:
|
|
3
|
+
export declare function validImpressionsMode(log: ILogger, impressionsMode: any): SplitIO.ImpressionsMode;
|
|
@@ -23,4 +23,6 @@ export interface ISettingsValidationParams {
|
|
|
23
23
|
logger: (settings: ISettings) => ISettings['log'];
|
|
24
24
|
/** Localhost mode validator (`settings.sync.localhostMode`) */
|
|
25
25
|
localhost?: (settings: ISettings) => ISettings['sync']['localhostMode'];
|
|
26
|
+
/** User consent validator (`settings.userConsent`) */
|
|
27
|
+
consent: (settings: ISettings) => ISettings['userConsent'];
|
|
26
28
|
}
|