@splitsoftware/splitio-commons 1.6.2-rc.6 → 1.6.2-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.
Files changed (135) hide show
  1. package/CHANGES.txt +3 -0
  2. package/cjs/consent/sdkUserConsent.js +2 -2
  3. package/cjs/listeners/browser.js +2 -1
  4. package/cjs/logger/constants.js +2 -1
  5. package/cjs/sdkClient/sdkClient.js +3 -1
  6. package/cjs/sdkFactory/index.js +26 -5
  7. package/cjs/services/splitApi.js +20 -0
  8. package/cjs/storages/KeyBuilderSS.js +6 -0
  9. package/cjs/storages/inLocalStorage/index.js +4 -0
  10. package/cjs/storages/inMemory/ImpressionCountsCacheInMemory.js +12 -1
  11. package/cjs/storages/inMemory/InMemoryStorage.js +5 -1
  12. package/cjs/storages/inMemory/InMemoryStorageCS.js +5 -1
  13. package/cjs/storages/inMemory/uniqueKeysCacheInMemory.js +73 -0
  14. package/cjs/storages/inMemory/uniqueKeysCacheInMemoryCS.js +78 -0
  15. package/cjs/storages/inRedis/ImpressionCountsCacheInRedis.js +49 -0
  16. package/cjs/storages/inRedis/constants.js +4 -1
  17. package/cjs/storages/inRedis/index.js +15 -1
  18. package/cjs/storages/inRedis/uniqueKeysCacheInRedis.js +56 -0
  19. package/cjs/sync/submitters/submitterManager.js +3 -0
  20. package/cjs/sync/submitters/telemetrySubmitter.js +1 -0
  21. package/cjs/sync/submitters/uniqueKeysSubmitter.js +26 -0
  22. package/cjs/trackers/impressionsTracker.js +22 -41
  23. package/cjs/trackers/strategy/strategyDebug.js +25 -0
  24. package/cjs/trackers/strategy/strategyNone.js +29 -0
  25. package/cjs/trackers/strategy/strategyOptimized.js +35 -0
  26. package/cjs/trackers/uniqueKeysTracker.js +38 -0
  27. package/cjs/utils/constants/index.js +4 -2
  28. package/cjs/utils/settingsValidation/impressionsMode.js +2 -2
  29. package/cjs/utils/settingsValidation/index.js +4 -0
  30. package/esm/consent/sdkUserConsent.js +2 -2
  31. package/esm/listeners/browser.js +3 -2
  32. package/esm/logger/constants.js +1 -0
  33. package/esm/sdkClient/sdkClient.js +3 -1
  34. package/esm/sdkFactory/index.js +26 -5
  35. package/esm/services/splitApi.js +20 -0
  36. package/esm/storages/KeyBuilderSS.js +6 -0
  37. package/esm/storages/inLocalStorage/index.js +5 -1
  38. package/esm/storages/inMemory/ImpressionCountsCacheInMemory.js +12 -1
  39. package/esm/storages/inMemory/InMemoryStorage.js +6 -2
  40. package/esm/storages/inMemory/InMemoryStorageCS.js +6 -2
  41. package/esm/storages/inMemory/uniqueKeysCacheInMemory.js +70 -0
  42. package/esm/storages/inMemory/uniqueKeysCacheInMemoryCS.js +75 -0
  43. package/esm/storages/inRedis/ImpressionCountsCacheInRedis.js +46 -0
  44. package/esm/storages/inRedis/constants.js +3 -0
  45. package/esm/storages/inRedis/index.js +16 -2
  46. package/esm/storages/inRedis/uniqueKeysCacheInRedis.js +53 -0
  47. package/esm/sync/submitters/submitterManager.js +3 -0
  48. package/esm/sync/submitters/telemetrySubmitter.js +2 -1
  49. package/esm/sync/submitters/uniqueKeysSubmitter.js +22 -0
  50. package/esm/trackers/impressionsTracker.js +22 -41
  51. package/esm/trackers/strategy/strategyDebug.js +21 -0
  52. package/esm/trackers/strategy/strategyNone.js +25 -0
  53. package/esm/trackers/strategy/strategyOptimized.js +31 -0
  54. package/esm/trackers/uniqueKeysTracker.js +34 -0
  55. package/esm/utils/constants/index.js +2 -0
  56. package/esm/utils/settingsValidation/impressionsMode.js +3 -3
  57. package/esm/utils/settingsValidation/index.js +4 -0
  58. package/package.json +1 -1
  59. package/src/consent/sdkUserConsent.ts +2 -2
  60. package/src/listeners/browser.ts +3 -2
  61. package/src/logger/constants.ts +1 -0
  62. package/src/sdkClient/sdkClient.ts +3 -1
  63. package/src/sdkFactory/index.ts +29 -5
  64. package/src/sdkFactory/types.ts +7 -4
  65. package/src/services/splitApi.ts +22 -0
  66. package/src/services/types.ts +6 -0
  67. package/src/storages/AbstractSplitsCacheAsync.ts +0 -1
  68. package/src/storages/KeyBuilderSS.ts +8 -0
  69. package/src/storages/inLocalStorage/index.ts +4 -1
  70. package/src/storages/inMemory/ImpressionCountsCacheInMemory.ts +16 -1
  71. package/src/storages/inMemory/InMemoryStorage.ts +5 -2
  72. package/src/storages/inMemory/InMemoryStorageCS.ts +6 -2
  73. package/src/storages/inMemory/uniqueKeysCacheInMemory.ts +82 -0
  74. package/src/storages/inMemory/uniqueKeysCacheInMemoryCS.ts +88 -0
  75. package/src/storages/inRedis/ImpressionCountsCacheInRedis.ts +51 -0
  76. package/src/storages/inRedis/constants.ts +3 -0
  77. package/src/storages/inRedis/index.ts +12 -3
  78. package/src/storages/inRedis/uniqueKeysCacheInRedis.ts +63 -0
  79. package/src/storages/types.ts +30 -8
  80. package/src/sync/submitters/submitterManager.ts +2 -0
  81. package/src/sync/submitters/telemetrySubmitter.ts +4 -3
  82. package/src/sync/submitters/types.ts +20 -1
  83. package/src/sync/submitters/uniqueKeysSubmitter.ts +35 -0
  84. package/src/trackers/impressionsTracker.ts +27 -48
  85. package/src/trackers/strategy/strategyDebug.ts +28 -0
  86. package/src/trackers/strategy/strategyNone.ts +34 -0
  87. package/src/trackers/strategy/strategyOptimized.ts +42 -0
  88. package/src/trackers/types.ts +28 -0
  89. package/src/trackers/uniqueKeysTracker.ts +48 -0
  90. package/src/types.ts +5 -1
  91. package/src/utils/constants/index.ts +2 -0
  92. package/src/utils/settingsValidation/impressionsMode.ts +3 -3
  93. package/src/utils/settingsValidation/index.ts +5 -0
  94. package/types/logger/constants.d.ts +1 -0
  95. package/types/sdkClient/types.d.ts +18 -0
  96. package/types/sdkFactory/types.d.ts +4 -2
  97. package/types/services/types.d.ts +4 -0
  98. package/types/storages/KeyBuilderSS.d.ts +2 -0
  99. package/types/storages/inMemory/CountsCacheInMemory.d.ts +20 -0
  100. package/types/storages/inMemory/ImpressionCountsCacheInMemory.d.ts +5 -1
  101. package/types/storages/inMemory/LatenciesCacheInMemory.d.ts +20 -0
  102. package/types/storages/inMemory/uniqueKeysCacheInMemory.d.ts +35 -0
  103. package/types/storages/inMemory/uniqueKeysCacheInMemoryCS.d.ts +37 -0
  104. package/types/storages/inRedis/CountsCacheInRedis.d.ts +9 -0
  105. package/types/storages/inRedis/ImpressionCountsCacheInRedis.d.ts +14 -0
  106. package/types/storages/inRedis/LatenciesCacheInRedis.d.ts +9 -0
  107. package/types/storages/inRedis/constants.d.ts +3 -0
  108. package/types/storages/inRedis/uniqueKeysCacheInRedis.d.ts +15 -0
  109. package/types/storages/types.d.ts +18 -5
  110. package/types/sync/offline/LocalhostFromFile.d.ts +2 -0
  111. package/types/sync/offline/splitsParser/splitsParserFromFile.d.ts +2 -0
  112. package/types/sync/submitters/eventsSyncTask.d.ts +8 -0
  113. package/types/sync/submitters/impressionCountsSubmitterInRedis.d.ts +5 -0
  114. package/types/sync/submitters/impressionCountsSyncTask.d.ts +13 -0
  115. package/types/sync/submitters/impressionsSyncTask.d.ts +14 -0
  116. package/types/sync/submitters/metricsSyncTask.d.ts +12 -0
  117. package/types/sync/submitters/submitterSyncTask.d.ts +10 -0
  118. package/types/sync/submitters/types.d.ts +18 -1
  119. package/types/sync/submitters/uniqueKeysSubmitter.d.ts +5 -0
  120. package/types/sync/submitters/uniqueKeysSubmitterInRedis.d.ts +5 -0
  121. package/types/sync/syncTaskComposite.d.ts +5 -0
  122. package/types/trackers/filter/bloomFilter.d.ts +10 -0
  123. package/types/trackers/filter/dictionaryFilter.d.ts +8 -0
  124. package/types/trackers/filter/types.d.ts +5 -0
  125. package/types/trackers/impressionsTracker.d.ts +4 -6
  126. package/types/trackers/strategy/strategyDebug.d.ts +9 -0
  127. package/types/trackers/strategy/strategyNone.d.ts +10 -0
  128. package/types/trackers/strategy/strategyOptimized.d.ts +11 -0
  129. package/types/trackers/types.d.ts +23 -0
  130. package/types/trackers/uniqueKeysTracker.d.ts +13 -0
  131. package/types/types.d.ts +5 -1
  132. package/types/utils/constants/index.d.ts +2 -0
  133. package/types/utils/settingsValidation/index.d.ts +1 -0
  134. package/types/utils/timeTracker/index.d.ts +70 -0
  135. package/src/logger/.DS_Store +0 -0
@@ -7,7 +7,7 @@ import { fromImpressionCountsCollector } from '../sync/submitters/impressionCoun
7
7
  import { IResponse, ISplitApi } from '../services/types';
8
8
  import { ImpressionDTO, ISettings } from '../types';
9
9
  import { ImpressionsPayload } from '../sync/submitters/types';
10
- import { OPTIMIZED, DEBUG } from '../utils/constants';
10
+ import { OPTIMIZED, DEBUG, NONE } from '../utils/constants';
11
11
  import { objectAssign } from '../utils/lang/objectAssign';
12
12
  import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants';
13
13
  import { ISyncManager } from '../sync/types';
@@ -88,9 +88,10 @@ export class BrowserSignalListener implements ISignalListener {
88
88
  // Flush impressions & events data if there is user consent
89
89
  if (isConsentGranted(this.settings)) {
90
90
  const eventsUrl = this.settings.urls.events;
91
+ const sim = this.settings.sync.impressionsMode;
91
92
  const extraMetadata = {
92
93
  // sim stands for Sync/Split Impressions Mode
93
- sim: this.settings.sync.impressionsMode === OPTIMIZED ? OPTIMIZED : DEBUG
94
+ sim: sim === OPTIMIZED ? OPTIMIZED : sim === DEBUG ? DEBUG : NONE
94
95
  };
95
96
 
96
97
  this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
@@ -143,4 +143,5 @@ export const LOG_PREFIX_SYNC_POLLING = LOG_PREFIX_SYNC + ':polling-manager: ';
143
143
  export const LOG_PREFIX_SYNC_SUBMITTERS = LOG_PREFIX_SYNC + ':submitter: ';
144
144
  export const LOG_PREFIX_IMPRESSIONS_TRACKER = 'impressions-tracker: ';
145
145
  export const LOG_PREFIX_EVENTS_TRACKER = 'events-tracker: ';
146
+ export const LOG_PREFIX_UNIQUE_KEYS_TRACKER = 'unique-keys-tracker: ';
146
147
  export const LOG_PREFIX_CLEANUP = 'cleanup: ';
@@ -9,7 +9,7 @@ import { ISdkFactoryContext } from '../sdkFactory/types';
9
9
  * Creates an Sdk client, i.e., a base client with status and destroy interface
10
10
  */
11
11
  export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: boolean): SplitIO.IClient | SplitIO.IAsyncClient {
12
- const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker } = params;
12
+ const { sdkReadinessManager, syncManager, storage, signalListener, settings, telemetryTracker, uniqueKeysTracker } = params;
13
13
 
14
14
  return objectAssign(
15
15
  // Proto-linkage of the readiness Event Emitter
@@ -39,6 +39,8 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo
39
39
 
40
40
  // Release the API Key if it is the main client
41
41
  if (!isSharedClient) releaseApiKey(settings.core.authorizationKey);
42
+
43
+ if (uniqueKeysTracker) uniqueKeysTracker.stop();
42
44
 
43
45
  // Cleanup storage
44
46
  return storage.destroy();
@@ -13,6 +13,11 @@ 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
15
  import { objectAssign } from '../utils/lang/objectAssign';
16
+ import { strategyDebugFactory } from '../trackers/strategy/strategyDebug';
17
+ import { strategyOptimizedFactory } from '../trackers/strategy/strategyOptimized';
18
+ import { strategyNoneFactory } from '../trackers/strategy/strategyNone';
19
+ import { uniqueKeysTrackerFactory } from '../trackers/uniqueKeysTracker';
20
+ import { NONE, OPTIMIZED } from '../utils/constants';
16
21
 
17
22
  /**
18
23
  * Modular SDK factory
@@ -21,7 +26,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
21
26
 
22
27
  const { settings, platform, storageFactory, splitApiFactory, extraProps,
23
28
  syncManagerFactory, SignalListener, impressionsObserverFactory,
24
- integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory } = params;
29
+ integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory,
30
+ filterAdapterFactory } = params;
25
31
  const log = settings.log;
26
32
 
27
33
  // @TODO handle non-recoverable errors, such as, global `fetch` not available, invalid API Key, etc.
@@ -37,6 +43,10 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
37
43
  const storageFactoryParams: IStorageFactoryParams = {
38
44
  impressionsQueueSize: settings.scheduler.impressionsQueueSize,
39
45
  eventsQueueSize: settings.scheduler.eventsQueueSize,
46
+ uniqueKeysCacheSize: settings.scheduler.uniqueKeysCacheSize,
47
+ impressionCountsQueueSize: settings.scheduler.impressionCountsQueueSize,
48
+ impressionCountsRefreshRate: settings.scheduler.impressionCountsRefreshRate,
49
+ uniqueKeysRefreshRate: settings.scheduler.uniqueKeysRefreshRate,
40
50
  optimize: shouldBeOptimized(settings),
41
51
 
42
52
  // ATM, only used by InLocalStorage
@@ -45,6 +55,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
45
55
 
46
56
  // ATM, only used by PluggableStorage
47
57
  mode: settings.mode,
58
+ impressionsMode: settings.sync.impressionsMode,
48
59
 
49
60
  // Callback used to emit SDK_READY in consumer mode, where `syncManagerFactory` is undefined,
50
61
  // or partial consumer mode, where it only has submitters, and therefore it doesn't emit readiness events.
@@ -63,15 +74,28 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
63
74
  const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now);
64
75
  const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker });
65
76
 
66
- // trackers
67
- const observer = impressionsObserverFactory && impressionsObserverFactory();
68
- const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, integrationsManager, observer, storage.impressionCounts, storage.telemetry);
77
+ const observer = impressionsObserverFactory();
78
+ const uniqueKeysTracker = storageFactoryParams.impressionsMode === NONE ? uniqueKeysTrackerFactory(log, storage.uniqueKeys!, filterAdapterFactory && filterAdapterFactory()) : undefined;
79
+
80
+ let strategy;
81
+ switch (storageFactoryParams.impressionsMode) {
82
+ case OPTIMIZED:
83
+ strategy = strategyOptimizedFactory(observer, storage.impressionCounts!);
84
+ break;
85
+ case NONE:
86
+ strategy = strategyNoneFactory(storage.impressionCounts!, uniqueKeysTracker!);
87
+ break;
88
+ default:
89
+ strategy = strategyDebugFactory(observer);
90
+ }
91
+
92
+ const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, strategy, integrationsManager, storage.telemetry);
69
93
  const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager, storage.telemetry);
70
94
 
71
95
  // splitApi is used by SyncManager and Browser signal listener
72
96
  const splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker);
73
97
 
74
- const ctx: ISdkFactoryContext = { splitApi, eventTracker, impressionsTracker, telemetryTracker, sdkReadinessManager, readiness, settings, storage, platform };
98
+ const ctx: ISdkFactoryContext = { splitApi, eventTracker, impressionsTracker, telemetryTracker, uniqueKeysTracker, sdkReadinessManager, readiness, settings, storage, platform };
75
99
 
76
100
  const syncManager = syncManagerFactory && syncManagerFactory(ctx as ISdkFactoryContextSync);
77
101
  ctx.syncManager = syncManager;
@@ -6,7 +6,7 @@ import { IFetch, ISplitApi, IEventSourceConstructor } from '../services/types';
6
6
  import { IStorageAsync, IStorageSync, ISplitsCacheSync, ISplitsCacheAsync, IStorageFactoryParams } from '../storages/types';
7
7
  import { ISyncManager } from '../sync/types';
8
8
  import { IImpressionObserver } from '../trackers/impressionObserver/types';
9
- import { IImpressionsTracker, IEventTracker, ITelemetryTracker } from '../trackers/types';
9
+ import { IImpressionsTracker, IEventTracker, ITelemetryTracker, IFilterAdapter, IUniqueKeysTracker } from '../trackers/types';
10
10
  import { SplitIO, ISettings, IEventEmitter } from '../types';
11
11
 
12
12
  /**
@@ -44,6 +44,7 @@ export interface ISdkFactoryContext {
44
44
  eventTracker: IEventTracker,
45
45
  telemetryTracker: ITelemetryTracker,
46
46
  storage: IStorageSync | IStorageAsync,
47
+ uniqueKeysTracker?: IUniqueKeysTracker,
47
48
  signalListener?: ISignalListener
48
49
  splitApi?: ISplitApi
49
50
  syncManager?: ISyncManager,
@@ -96,6 +97,11 @@ export interface ISdkFactoryParams {
96
97
  // It Allows to distinguish SDK clients with the client-side API (`ICsSDK`) or server-side API (`ISDK` or `IAsyncSDK`).
97
98
  sdkClientMethodFactory: (params: ISdkFactoryContext) => ({ (): SplitIO.ICsClient; (key: SplitIO.SplitKey, trafficType?: string | undefined): SplitIO.ICsClient; } | (() => SplitIO.IClient) | (() => SplitIO.IAsyncClient))
98
99
 
100
+ // Impression observer factory. If provided, will be used for impressions dedupe
101
+ impressionsObserverFactory: () => IImpressionObserver
102
+
103
+ filterAdapterFactory?: () => IFilterAdapter
104
+
99
105
  // Optional signal listener constructor. Used to handle special app states, like shutdown, app paused or resumed.
100
106
  // Pass only if `syncManager` (used by Node listener) and `splitApi` (used by Browser listener) are passed.
101
107
  SignalListener?: new (
@@ -107,9 +113,6 @@ export interface ISdkFactoryParams {
107
113
  // @TODO review impressionListener and integrations interfaces. What about handling impressionListener as an integration ?
108
114
  integrationsManagerFactory?: (params: IIntegrationFactoryParams) => IIntegrationManager | undefined,
109
115
 
110
- // Impression observer factory. If provided, will be used for impressions dedupe
111
- impressionsObserverFactory?: () => IImpressionObserver
112
-
113
116
  // Optional function to assign additional properties to the factory instance
114
117
  extraProps?: (params: ISdkFactoryContext) => object
115
118
  }
@@ -106,6 +106,28 @@ export function splitApiFactory(
106
106
  const url = `${urls.events}/testImpressions/count`;
107
107
  return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(IMPRESSIONS_COUNT));
108
108
  },
109
+
110
+ /**
111
+ * Post unique keys for client side.
112
+ *
113
+ * @param body unique keys payload
114
+ * @param headers Optionals headers to overwrite default ones. For example, it is used in producer mode to overwrite metadata headers.
115
+ */
116
+ postUniqueKeysBulkCs(body: string, headers?: Record<string, string>) {
117
+ const url = `${urls.telemetry}/v1/keys/cs`;
118
+ return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY));
119
+ },
120
+
121
+ /**
122
+ * Post unique keys for server side.
123
+ *
124
+ * @param body unique keys payload
125
+ * @param headers Optionals headers to overwrite default ones. For example, it is used in producer mode to overwrite metadata headers.
126
+ */
127
+ postUniqueKeysBulkSs(body: string, headers?: Record<string, string>) {
128
+ const url = `${urls.telemetry}/v1/keys/ss`;
129
+ return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY));
130
+ },
109
131
 
110
132
  postMetricsConfig(body: string) {
111
133
  const url = `${urls.telemetry}/v1/metrics/config`;
@@ -43,6 +43,10 @@ export type IFetchMySegments = (userMatchingKey: string, noCache?: boolean) => P
43
43
 
44
44
  export type IPostEventsBulk = (body: string, headers?: Record<string, string>) => Promise<IResponse>
45
45
 
46
+ export type IPostUniqueKeysBulkCs = (body: string, headers?: Record<string, string>) => Promise<IResponse>
47
+
48
+ export type IPostUniqueKeysBulkSs = (body: string, headers?: Record<string, string>) => Promise<IResponse>
49
+
46
50
  export type IPostTestImpressionsBulk = (body: string, headers?: Record<string, string>) => Promise<IResponse>
47
51
 
48
52
  export type IPostTestImpressionsCount = (body: string, headers?: Record<string, string>) => Promise<IResponse>
@@ -59,6 +63,8 @@ export interface ISplitApi {
59
63
  fetchSegmentChanges: IFetchSegmentChanges
60
64
  fetchMySegments: IFetchMySegments
61
65
  postEventsBulk: IPostEventsBulk
66
+ postUniqueKeysBulkCs: IPostUniqueKeysBulkCs
67
+ postUniqueKeysBulkSs: IPostUniqueKeysBulkSs
62
68
  postTestImpressionsBulk: IPostTestImpressionsBulk
63
69
  postTestImpressionsCount: IPostTestImpressionsCount
64
70
  postMetricsConfig: IPostMetricsConfig
@@ -8,7 +8,6 @@ import { objectAssign } from '../utils/lang/objectAssign';
8
8
  */
9
9
  export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync {
10
10
 
11
- // Implement addSplits and removeSplits here and others like AbstractSplitsCacheSync, to reuse code between redis and pluggable implementations
12
11
  abstract addSplit(name: string, split: ISplit): Promise<boolean>
13
12
  abstract addSplits(entries: [string, ISplit][]): Promise<boolean[] | void>
14
13
  abstract removeSplits(names: string[]): Promise<boolean[] | void>
@@ -27,6 +27,14 @@ export class KeyBuilderSS extends KeyBuilder {
27
27
  return `${this.prefix}.impressions`;
28
28
  }
29
29
 
30
+ buildImpressionsCountKey() {
31
+ return `${this.prefix}.impressions.count`;
32
+ }
33
+
34
+ buildUniqueKeysKey() {
35
+ return `${this.prefix}.uniquekeys`;
36
+ }
37
+
30
38
  buildEventsKey() {
31
39
  return `${this.prefix}.events`;
32
40
  }
@@ -12,8 +12,9 @@ import { SplitsCacheInMemory } from '../inMemory/SplitsCacheInMemory';
12
12
  import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser';
13
13
  import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS';
14
14
  import { LOG_PREFIX } from './constants';
15
- import { LOCALHOST_MODE, STORAGE_LOCALSTORAGE } from '../../utils/constants';
15
+ import { LOCALHOST_MODE, NONE, STORAGE_LOCALSTORAGE } from '../../utils/constants';
16
16
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory';
17
+ import { UniqueKeysCacheInMemoryCS } from '../inMemory/uniqueKeysCacheInMemoryCS';
17
18
 
18
19
  export interface InLocalStorageOptions {
19
20
  prefix?: string
@@ -45,6 +46,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
45
46
  impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined,
46
47
  events: new EventsCacheInMemory(params.eventsQueueSize),
47
48
  telemetry: params.mode !== LOCALHOST_MODE && shouldRecordTelemetry() ? new TelemetryCacheInMemory() : undefined,
49
+ uniqueKeys: params.impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,
48
50
 
49
51
  destroy() {
50
52
  this.splits = new SplitsCacheInMemory();
@@ -52,6 +54,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
52
54
  this.impressions.clear();
53
55
  this.impressionCounts && this.impressionCounts.clear();
54
56
  this.events.clear();
57
+ this.uniqueKeys?.clear();
55
58
  },
56
59
 
57
60
  // When using shared instanciation with MEMORY we reuse everything but segments (they are customer per key).
@@ -1,8 +1,16 @@
1
1
  import { truncateTimeFrame } from '../../utils/time';
2
+ import { DEFAULT_CACHE_SIZE } from '../inRedis/constants';
2
3
  import { IImpressionCountsCacheSync } from '../types';
3
4
 
4
5
  export class ImpressionCountsCacheInMemory implements IImpressionCountsCacheSync {
5
- private cache: Record<string, number> = {};
6
+ protected cache: Record<string, number> = {};
7
+ private readonly maxStorage: number;
8
+ protected onFullQueue?: () => void;
9
+ private cacheSize = 0;
10
+
11
+ constructor(impressionCountsCacheSize: number = DEFAULT_CACHE_SIZE) {
12
+ this.maxStorage = impressionCountsCacheSize;
13
+ }
6
14
 
7
15
  /**
8
16
  * Builds key to be stored in the cache with the featureName and the timeFrame truncated.
@@ -18,6 +26,13 @@ export class ImpressionCountsCacheInMemory implements IImpressionCountsCacheSync
18
26
  const key = this._makeKey(featureName, timeFrame);
19
27
  const currentAmount = this.cache[key];
20
28
  this.cache[key] = currentAmount ? currentAmount + amount : amount;
29
+ if (this.onFullQueue) {
30
+ this.cacheSize = this.cacheSize + amount;
31
+ if (this.cacheSize >= this.maxStorage) {
32
+ this.onFullQueue();
33
+ this.cacheSize = 0;
34
+ }
35
+ }
21
36
  }
22
37
 
23
38
 
@@ -4,8 +4,9 @@ import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory';
4
4
  import { EventsCacheInMemory } from './EventsCacheInMemory';
5
5
  import { IStorageFactoryParams, IStorageSync } from '../types';
6
6
  import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
7
- import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
7
+ import { DEBUG, LOCALHOST_MODE, NONE, STORAGE_MEMORY } from '../../utils/constants';
8
8
  import { TelemetryCacheInMemory } from './TelemetryCacheInMemory';
9
+ import { UniqueKeysCacheInMemory } from './uniqueKeysCacheInMemory';
9
10
 
10
11
  /**
11
12
  * InMemory storage factory for standalone server-side SplitFactory
@@ -18,9 +19,10 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS
18
19
  splits: new SplitsCacheInMemory(),
19
20
  segments: new SegmentsCacheInMemory(),
20
21
  impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize),
21
- impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined,
22
+ impressionCounts: params.impressionsMode !== DEBUG ? new ImpressionCountsCacheInMemory() : undefined,
22
23
  events: new EventsCacheInMemory(params.eventsQueueSize),
23
24
  telemetry: params.mode !== LOCALHOST_MODE ? new TelemetryCacheInMemory() : undefined, // Always track telemetry in standalone mode on server-side
25
+ uniqueKeys: params.impressionsMode === NONE ? new UniqueKeysCacheInMemory(params.uniqueKeysCacheSize) : undefined,
24
26
 
25
27
  // When using MEMORY we should clean all the caches to leave them empty
26
28
  destroy() {
@@ -29,6 +31,7 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS
29
31
  this.impressions.clear();
30
32
  this.impressionCounts && this.impressionCounts.clear();
31
33
  this.events.clear();
34
+ this.uniqueKeys?.clear();
32
35
  }
33
36
  };
34
37
  }
@@ -4,8 +4,9 @@ import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory';
4
4
  import { EventsCacheInMemory } from './EventsCacheInMemory';
5
5
  import { IStorageSync, IStorageFactoryParams } from '../types';
6
6
  import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
7
- import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
7
+ import { DEBUG, LOCALHOST_MODE, NONE, STORAGE_MEMORY } from '../../utils/constants';
8
8
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
9
+ import { UniqueKeysCacheInMemoryCS } from './uniqueKeysCacheInMemoryCS';
9
10
 
10
11
  /**
11
12
  * InMemory storage factory for standalone client-side SplitFactory
@@ -18,9 +19,11 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
18
19
  splits: new SplitsCacheInMemory(),
19
20
  segments: new MySegmentsCacheInMemory(),
20
21
  impressions: new ImpressionsCacheInMemory(params.impressionsQueueSize),
21
- impressionCounts: params.optimize ? new ImpressionCountsCacheInMemory() : undefined,
22
+ impressionCounts: params.impressionsMode !== DEBUG ? new ImpressionCountsCacheInMemory() : undefined,
22
23
  events: new EventsCacheInMemory(params.eventsQueueSize),
23
24
  telemetry: params.mode !== LOCALHOST_MODE && shouldRecordTelemetry() ? new TelemetryCacheInMemory() : undefined,
25
+ uniqueKeys: params.impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS(params.uniqueKeysCacheSize) : undefined,
26
+
24
27
 
25
28
  // When using MEMORY we should clean all the caches to leave them empty
26
29
  destroy() {
@@ -29,6 +32,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
29
32
  this.impressions.clear();
30
33
  this.impressionCounts && this.impressionCounts.clear();
31
34
  this.events.clear();
35
+ this.uniqueKeys?.clear();
32
36
  },
33
37
 
34
38
  // When using shared instanciation with MEMORY we reuse everything but segments (they are unique per key)
@@ -0,0 +1,82 @@
1
+ import { IUniqueKeysCacheBase } from '../types';
2
+ import { ISet, setToArray, _Set } from '../../utils/lang/sets';
3
+ import { UniqueKeysPayloadSs } from '../../sync/submitters/types';
4
+ import { DEFAULT_CACHE_SIZE } from '../inRedis/constants';
5
+
6
+ export class UniqueKeysCacheInMemory implements IUniqueKeysCacheBase {
7
+
8
+ protected onFullQueue?: () => void;
9
+ private readonly maxStorage: number;
10
+ private uniqueTrackerSize = 0;
11
+ protected uniqueKeysTracker: { [keys: string]: ISet<string> };
12
+
13
+ constructor(uniqueKeysQueueSize: number = DEFAULT_CACHE_SIZE) {
14
+ this.maxStorage = uniqueKeysQueueSize;
15
+ this.uniqueKeysTracker = {};
16
+ }
17
+
18
+ setOnFullQueueCb(cb: () => void) {
19
+ this.onFullQueue = cb;
20
+ }
21
+
22
+ /**
23
+ * Store unique keys in sequential order
24
+ * key: string = feature name.
25
+ * value: Set<string> = set of unique keys.
26
+ */
27
+ track(key: string, featureName: string) {
28
+ if (!this.uniqueKeysTracker[featureName]) this.uniqueKeysTracker[featureName] = new _Set();
29
+ const tracker = this.uniqueKeysTracker[featureName];
30
+ if (!tracker.has(key)) {
31
+ tracker.add(key);
32
+ this.uniqueTrackerSize++;
33
+ }
34
+ if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
35
+ this.uniqueTrackerSize = 0;
36
+ this.onFullQueue();
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Clear the data stored on the cache.
42
+ */
43
+ clear() {
44
+ this.uniqueKeysTracker = {};
45
+ }
46
+
47
+ /**
48
+ * Pop the collected data, used as payload for posting.
49
+ */
50
+ pop() {
51
+ const data = this.uniqueKeysTracker;
52
+ this.uniqueKeysTracker = {};
53
+ return this.fromUniqueKeysCollector(data);
54
+ }
55
+
56
+ /**
57
+ * Check if the cache is empty.
58
+ */
59
+ isEmpty() {
60
+ return Object.keys(this.uniqueKeysTracker).length === 0;
61
+ }
62
+
63
+ /**
64
+ * Converts `uniqueKeys` data from cache into request payload for SS.
65
+ */
66
+ private fromUniqueKeysCollector(uniqueKeys: { [featureName: string]: ISet<string> }): UniqueKeysPayloadSs {
67
+ const payload = [];
68
+ const featureNames = Object.keys(uniqueKeys);
69
+ for (let i = 0; i < featureNames.length; i++) {
70
+ const featureName = featureNames[i];
71
+ const featureKeys = setToArray(uniqueKeys[featureName]);
72
+ const uniqueKeysPayload = {
73
+ f: featureName,
74
+ ks: featureKeys
75
+ };
76
+
77
+ payload.push(uniqueKeysPayload);
78
+ }
79
+ return { keys: payload };
80
+ }
81
+
82
+ }
@@ -0,0 +1,88 @@
1
+ import { IUniqueKeysCacheBase } from '../types';
2
+ import { ISet, setToArray, _Set } from '../../utils/lang/sets';
3
+ import { UniqueKeysPayloadCs } from '../../sync/submitters/types';
4
+ import { DEFAULT_CACHE_SIZE } from '../inRedis/constants';
5
+
6
+ export class UniqueKeysCacheInMemoryCS implements IUniqueKeysCacheBase {
7
+
8
+ private onFullQueue?: () => void;
9
+ private readonly maxStorage: number;
10
+ private uniqueTrackerSize = 0;
11
+ private uniqueKeysTracker: { [keys: string]: ISet<string> };
12
+
13
+ /**
14
+ *
15
+ * @param impressionsQueueSize number of queued impressions to call onFullQueueCb.
16
+ * Default value is 0, that means no maximum value, in case we want to avoid this being triggered.
17
+ */
18
+ constructor(uniqueKeysQueueSize: number = DEFAULT_CACHE_SIZE) {
19
+ this.maxStorage = uniqueKeysQueueSize;
20
+ this.uniqueKeysTracker = {};
21
+ }
22
+
23
+ setOnFullQueueCb(cb: () => void) {
24
+ this.onFullQueue = cb;
25
+ }
26
+
27
+ /**
28
+ * Store unique keys in sequential order
29
+ * key: string = key.
30
+ * value: HashSet<string> = set of split names.
31
+ */
32
+ track(key: string, featureName: string) {
33
+
34
+ if (!this.uniqueKeysTracker[key]) this.uniqueKeysTracker[key] = new _Set();
35
+ const tracker = this.uniqueKeysTracker[key];
36
+ if (!tracker.has(featureName)) {
37
+ tracker.add(featureName);
38
+ this.uniqueTrackerSize++;
39
+ }
40
+ if (this.uniqueTrackerSize >= this.maxStorage && this.onFullQueue) {
41
+ this.uniqueTrackerSize = 0;
42
+ this.onFullQueue();
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Clear the data stored on the cache.
48
+ */
49
+ clear() {
50
+ this.uniqueKeysTracker = {};
51
+ }
52
+
53
+ /**
54
+ * Pop the collected data, used as payload for posting.
55
+ */
56
+ pop() {
57
+ const data = this.uniqueKeysTracker;
58
+ this.uniqueKeysTracker = {};
59
+ return this.fromUniqueKeysCollector(data);
60
+ }
61
+
62
+ /**
63
+ * Check if the cache is empty.
64
+ */
65
+ isEmpty() {
66
+ return Object.keys(this.uniqueKeysTracker).length === 0;
67
+ }
68
+
69
+ /**
70
+ * Converts `uniqueKeys` data from cache into request payload.
71
+ */
72
+ private fromUniqueKeysCollector(uniqueKeys: { [featureName: string]: ISet<string> }): UniqueKeysPayloadCs {
73
+ const payload = [];
74
+ const featureKeys = Object.keys(uniqueKeys);
75
+ for (let k = 0; k < featureKeys.length; k++) {
76
+ const featureKey = featureKeys[k];
77
+ const featureNames = setToArray(uniqueKeys[featureKey]);
78
+ const uniqueKeysPayload = {
79
+ k: featureKey,
80
+ fs: featureNames
81
+ };
82
+
83
+ payload.push(uniqueKeysPayload);
84
+ }
85
+ return { keys: payload };
86
+ }
87
+
88
+ }
@@ -0,0 +1,51 @@
1
+ import { Redis } from 'ioredis';
2
+ import { ILogger } from '../../logger/types';
3
+ import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory';
4
+ import { LOG_PREFIX, REFRESH_RATE, TTL_REFRESH } from './constants';
5
+
6
+ export class ImpressionCountsCacheInRedis extends ImpressionCountsCacheInMemory {
7
+
8
+ private readonly log: ILogger;
9
+ private readonly key: string;
10
+ private readonly redis: Redis;
11
+ private readonly refreshRate: number;
12
+ private intervalId: any;
13
+
14
+ constructor(log: ILogger, key: string, redis: Redis, impressionCountsCacheSize?: number, refreshRate: number = REFRESH_RATE) {
15
+ super(impressionCountsCacheSize);
16
+ this.log = log;
17
+ this.key = key;
18
+ this.redis = redis;
19
+ this.refreshRate = refreshRate;
20
+ this.onFullQueue = () => { this.postImpressionCountsInRedis(); };
21
+ }
22
+
23
+ postImpressionCountsInRedis(){
24
+ const counts = this.pop();
25
+ if (!counts) return false;
26
+ const keys = Object.keys(counts);
27
+ const pipeline = this.redis.pipeline();
28
+ keys.forEach(key => {
29
+ pipeline.hincrby(this.key, key, counts[key]);
30
+ });
31
+ return pipeline.exec()
32
+ .then(data => {
33
+ // If this is the creation of the key on Redis, set the expiration for it in 3600 seconds.
34
+ if (data.length && data.length === keys.length) {
35
+ return this.redis.expire(this.key, TTL_REFRESH);
36
+ }
37
+ })
38
+ .catch(err => {
39
+ this.log.error(`${LOG_PREFIX}Error in impression counts pipeline: ${err}.`);
40
+ return false;
41
+ });
42
+ }
43
+
44
+ start() {
45
+ this.intervalId = setInterval(this.postImpressionCountsInRedis.bind(this), this.refreshRate);
46
+ }
47
+
48
+ stop() {
49
+ clearInterval(this.intervalId);
50
+ }
51
+ }
@@ -1 +1,4 @@
1
1
  export const LOG_PREFIX = 'storage:redis: ';
2
+ export const DEFAULT_CACHE_SIZE = 30000;
3
+ export const REFRESH_RATE = 300000; // 300.000 ms = start after 5 mins
4
+ export const TTL_REFRESH = 3600; // 1hr
@@ -6,8 +6,10 @@ import { SplitsCacheInRedis } from './SplitsCacheInRedis';
6
6
  import { SegmentsCacheInRedis } from './SegmentsCacheInRedis';
7
7
  import { ImpressionsCacheInRedis } from './ImpressionsCacheInRedis';
8
8
  import { EventsCacheInRedis } from './EventsCacheInRedis';
9
- import { STORAGE_REDIS } from '../../utils/constants';
9
+ import { DEBUG, NONE, STORAGE_REDIS } from '../../utils/constants';
10
10
  import { TelemetryCacheInRedis } from './TelemetryCacheInRedis';
11
+ import { UniqueKeysCacheInRedis } from './uniqueKeysCacheInRedis';
12
+ import { ImpressionCountsCacheInRedis } from './ImpressionCountsCacheInRedis';
11
13
 
12
14
  export interface InRedisStorageOptions {
13
15
  prefix?: string
@@ -22,15 +24,18 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
22
24
 
23
25
  const prefix = validatePrefix(options.prefix);
24
26
 
25
- function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync {
26
-
27
+ function InRedisStorageFactory({ log, metadata, onReadyCb, impressionsMode, impressionCountsQueueSize, impressionCountsRefreshRate, uniqueKeysCacheSize, uniqueKeysRefreshRate }: IStorageFactoryParams): IStorageAsync {
27
28
  const keys = new KeyBuilderSS(prefix, metadata);
28
29
  const redisClient = new RedisAdapter(log, options.options || {});
29
30
  const telemetry = new TelemetryCacheInRedis(log, keys, redisClient);
31
+ const impressionCountsCache = impressionsMode !== DEBUG ? new ImpressionCountsCacheInRedis(log, keys.buildImpressionsCountKey(), redisClient, impressionCountsQueueSize, impressionCountsRefreshRate) : undefined;
32
+ const uniqueKeysCache = impressionsMode === NONE ? new UniqueKeysCacheInRedis(log, keys.buildUniqueKeysKey(), redisClient, uniqueKeysCacheSize, uniqueKeysRefreshRate) : undefined;
30
33
 
31
34
  // subscription to Redis connect event in order to emit SDK_READY event on consumer mode
32
35
  redisClient.on('connect', () => {
33
36
  onReadyCb();
37
+ if (impressionCountsCache) impressionCountsCache.start();
38
+ if (uniqueKeysCache) uniqueKeysCache.start();
34
39
 
35
40
  // Synchronize config
36
41
  telemetry.recordConfig();
@@ -40,13 +45,17 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy
40
45
  splits: new SplitsCacheInRedis(log, keys, redisClient),
41
46
  segments: new SegmentsCacheInRedis(log, keys, redisClient),
42
47
  impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata),
48
+ impressionCounts: impressionCountsCache,
43
49
  events: new EventsCacheInRedis(log, keys.buildEventsKey(), redisClient, metadata),
44
50
  telemetry,
51
+ uniqueKeys: uniqueKeysCache,
45
52
 
46
53
  // When using REDIS we should:
47
54
  // 1- Disconnect from the storage
48
55
  destroy() {
49
56
  redisClient.disconnect();
57
+ if (impressionCountsCache) impressionCountsCache.stop();
58
+ if (uniqueKeysCache) uniqueKeysCache.stop();
50
59
  // @TODO check that caches works as expected when redisClient is disconnected
51
60
  }
52
61
  };