Skip to content

Commit 77f8dee

Browse files
Refactor strategies and impressionsTracker
1 parent c2f1e62 commit 77f8dee

File tree

10 files changed

+92
-150
lines changed

10 files changed

+92
-150
lines changed

src/sdkFactory/index.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { strategyDebugFactory } from '../trackers/strategy/strategyDebug';
1313
import { strategyOptimizedFactory } from '../trackers/strategy/strategyOptimized';
1414
import { strategyNoneFactory } from '../trackers/strategy/strategyNone';
1515
import { uniqueKeysTrackerFactory } from '../trackers/uniqueKeysTracker';
16-
import { NONE, OPTIMIZED } from '../utils/constants';
16+
import { DEBUG, OPTIMIZED } from '../utils/constants';
1717

1818
/**
1919
* Modular SDK factory
@@ -61,19 +61,14 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
6161
const observer = impressionsObserverFactory();
6262
const uniqueKeysTracker = uniqueKeysTrackerFactory(log, storage.uniqueKeys, filterAdapterFactory && filterAdapterFactory());
6363

64-
let strategy;
65-
switch (impressionsMode) {
66-
case OPTIMIZED:
67-
strategy = strategyOptimizedFactory(observer, storage.impressionCounts);
68-
break;
69-
case NONE:
70-
strategy = strategyNoneFactory(storage.impressionCounts, uniqueKeysTracker);
71-
break;
72-
default:
73-
strategy = strategyDebugFactory(observer);
74-
}
64+
const noneStrategy = strategyNoneFactory(storage.impressionCounts, uniqueKeysTracker);
65+
const strategy = impressionsMode === OPTIMIZED ?
66+
strategyOptimizedFactory(observer, storage.impressionCounts) :
67+
impressionsMode === DEBUG ?
68+
strategyDebugFactory(observer) :
69+
noneStrategy;
7570

76-
const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, strategy, whenInit, integrationsManager, storage.telemetry);
71+
const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, noneStrategy, strategy, whenInit, integrationsManager, storage.telemetry);
7772
const eventTracker = eventTrackerFactory(settings, storage.events, whenInit, integrationsManager, storage.telemetry);
7873

7974
// splitApi is used by SyncManager and Browser signal listener

src/trackers/__tests__/impressionsTracker.spec.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ const fakeSettingsWithListener = {
3636
};
3737
const fakeWhenInit = (cb: () => void) => cb();
3838

39+
// @TODO validate logic with `impression.track` false
40+
const fakeNoneStrategy = {
41+
process: jest.fn(() => false)
42+
};
43+
3944
/* Tests */
4045

4146
describe('Impressions Tracker', () => {
@@ -51,12 +56,12 @@ describe('Impressions Tracker', () => {
5156
test('Tracker API', () => {
5257
expect(typeof impressionsTrackerFactory).toBe('function'); // The module should return a function which acts as a factory.
5358

54-
const instance = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategy, fakeWhenInit);
59+
const instance = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy,strategy, fakeWhenInit);
5560
expect(typeof instance.track).toBe('function'); // The instance should implement the track method which will actually track queued impressions.
5661
});
5762

5863
test('Should be able to track impressions (in DEBUG mode without Previous Time).', () => {
59-
const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategy, fakeWhenInit);
64+
const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit);
6065

6166
const imp1 = {
6267
feature: '10',
@@ -76,7 +81,7 @@ describe('Impressions Tracker', () => {
7681
});
7782

7883
test('Tracked impressions should be sent to impression listener and integration manager when we invoke .track()', (done) => {
79-
const tracker = impressionsTrackerFactory(fakeSettingsWithListener, fakeImpressionsCache, strategy, fakeWhenInit, fakeIntegrationsManager);
84+
const tracker = impressionsTrackerFactory(fakeSettingsWithListener, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit, fakeIntegrationsManager);
8085

8186
const fakeImpression = {
8287
feature: 'impression'
@@ -150,8 +155,8 @@ describe('Impressions Tracker', () => {
150155
impression3.time = 1234567891;
151156

152157
const trackers = [
153-
impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategyDebugFactory(impressionObserverSSFactory()), fakeWhenInit, undefined),
154-
impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategyDebugFactory(impressionObserverCSFactory()), fakeWhenInit, undefined)
158+
impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyDebugFactory(impressionObserverSSFactory()), fakeWhenInit, undefined),
159+
impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyDebugFactory(impressionObserverCSFactory()), fakeWhenInit, undefined)
155160
];
156161

157162
expect(fakeImpressionsCache.track).not.toBeCalled(); // storage method should not be called until impressions are tracked.
@@ -178,7 +183,7 @@ describe('Impressions Tracker', () => {
178183
impression3.time = Date.now();
179184

180185
const impressionCountsCache = new ImpressionCountsCacheInMemory();
181-
const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategyOptimizedFactory(impressionObserverCSFactory(), impressionCountsCache), fakeWhenInit, undefined, fakeTelemetryCache as any);
186+
const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyOptimizedFactory(impressionObserverCSFactory(), impressionCountsCache), fakeWhenInit, undefined, fakeTelemetryCache as any);
182187

183188
expect(fakeImpressionsCache.track).not.toBeCalled(); // cache method should not be called by just creating a tracker
184189

@@ -201,7 +206,7 @@ describe('Impressions Tracker', () => {
201206
test('Should track or not impressions depending on user consent status', () => {
202207
const settings = { ...fullSettings };
203208

204-
const tracker = impressionsTrackerFactory(settings, fakeImpressionsCache, strategy, fakeWhenInit);
209+
const tracker = impressionsTrackerFactory(settings, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit);
205210

206211
tracker.track([impression]);
207212
expect(fakeImpressionsCache.track).toBeCalledTimes(1); // impression should be tracked if userConsent is undefined

src/trackers/impressionsTracker.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,11 @@ import SplitIO from '../../types/splitio';
99

1010
/**
1111
* Impressions tracker stores impressions in cache and pass them to the listener and integrations manager if provided.
12-
*
13-
* @param impressionsCache - cache to save impressions
14-
* @param metadata - runtime metadata (ip, hostname and version)
15-
* @param impressionListener - optional impression listener
16-
* @param integrationsManager - optional integrations manager
17-
* @param strategy - strategy for impressions tracking.
1812
*/
1913
export function impressionsTrackerFactory(
2014
settings: ISettings,
2115
impressionsCache: IImpressionsCacheBase,
16+
noneStrategy: IStrategy,
2217
strategy: IStrategy,
2318
whenInit: (cb: () => void) => void,
2419
integrationsManager?: IImpressionsHandler,
@@ -31,37 +26,41 @@ export function impressionsTrackerFactory(
3126
track(impressions: SplitIO.ImpressionDTO[], attributes?: SplitIO.Attributes) {
3227
if (settings.userConsent === CONSENT_DECLINED) return;
3328

34-
const impressionsCount = impressions.length;
35-
const { impressionsToStore, impressionsToListener, deduped } = strategy.process(impressions);
29+
const impressionsToStore = impressions.filter((impression) => {
30+
return impression.track === false ?
31+
noneStrategy.process(impression) :
32+
strategy.process(impression);
33+
});
3634

37-
const impressionsToListenerCount = impressionsToListener.length;
35+
const impressionsLength = impressions.length;
36+
const impressionsToStoreLength = impressionsToStore.length;
3837

39-
if (impressionsToStore.length > 0) {
38+
if (impressionsToStoreLength) {
4039
const res = impressionsCache.track(impressionsToStore);
4140

4241
// If we're on an async storage, handle error and log it.
4342
if (thenable(res)) {
4443
res.then(() => {
45-
log.info(IMPRESSIONS_TRACKER_SUCCESS, [impressionsCount]);
44+
log.info(IMPRESSIONS_TRACKER_SUCCESS, [impressionsLength]);
4645
}).catch(err => {
47-
log.error(ERROR_IMPRESSIONS_TRACKER, [impressionsCount, err]);
46+
log.error(ERROR_IMPRESSIONS_TRACKER, [impressionsLength, err]);
4847
});
4948
} else {
5049
// Record when impressionsCache is sync only (standalone mode)
5150
// @TODO we are not dropping impressions on full queue yet, so DROPPED stats are not recorded
5251
if (telemetryCache) {
53-
(telemetryCache as ITelemetryCacheSync).recordImpressionStats(QUEUED, impressionsToStore.length);
54-
(telemetryCache as ITelemetryCacheSync).recordImpressionStats(DEDUPED, deduped);
52+
(telemetryCache as ITelemetryCacheSync).recordImpressionStats(QUEUED, impressionsToStoreLength);
53+
(telemetryCache as ITelemetryCacheSync).recordImpressionStats(DEDUPED, impressionsLength - impressionsToStoreLength);
5554
}
5655
}
5756
}
5857

5958
// @TODO next block might be handled by the integration manager. In that case, the metadata object doesn't need to be passed in the constructor
6059
if (impressionListener || integrationsManager) {
61-
for (let i = 0; i < impressionsToListenerCount; i++) {
60+
for (let i = 0; i < impressionsLength; i++) {
6261
const impressionData: SplitIO.ImpressionData = {
6362
// copy of impression, to avoid unexpected behaviour if modified by integrations or impressionListener
64-
impression: objectAssign({}, impressionsToListener[i]),
63+
impression: objectAssign({}, impressions[i]),
6564
attributes,
6665
ip,
6766
hostname,

src/trackers/strategy/__tests__/strategyDebug.spec.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,21 @@ import { impressionObserverCSFactory } from '../../impressionObserver/impression
33
import { strategyDebugFactory } from '../strategyDebug';
44
import { impression1, impression2 } from './testUtils';
55

6-
test('strategyDebug', () => {
6+
test.each([
7+
impressionObserverSSFactory(),
8+
impressionObserverCSFactory()]
9+
)('strategyDebug', (impressionObserver) => {
710

8-
let augmentedImp1 = { ...impression1, pt: undefined };
9-
let augmentedImp12 = { ...impression1, pt: impression1.time };
10-
let augmentedImp2 = { ...impression2, pt: undefined };
11+
const strategyDebug = strategyDebugFactory(impressionObserver);
1112

12-
let impressions = [impression1, impression2, {...impression1}];
13-
let augmentedImpressions = [augmentedImp1, augmentedImp2, augmentedImp12];
13+
const impressions = [{ ...impression1 }, { ...impression2 }, { ...impression1 }];
1414

15-
const strategyDebugSS = strategyDebugFactory(impressionObserverSSFactory());
15+
expect(strategyDebug.process(impressions[0])).toBe(true);
16+
expect(impressions[0]).toEqual({ ...impression1, pt: undefined });
1617

17-
let { impressionsToStore, impressionsToListener, deduped } = strategyDebugSS.process(impressions);
18-
19-
expect(impressionsToStore).toStrictEqual(augmentedImpressions);
20-
expect(impressionsToListener).toStrictEqual(augmentedImpressions);
21-
expect(deduped).toStrictEqual(0);
22-
23-
const strategyDebugCS = strategyDebugFactory(impressionObserverCSFactory());
24-
25-
({ impressionsToStore, impressionsToListener, deduped } = strategyDebugCS.process(impressions));
26-
27-
expect(impressionsToStore).toStrictEqual(augmentedImpressions);
28-
expect(impressionsToListener).toStrictEqual(augmentedImpressions);
29-
expect(deduped).toStrictEqual(0);
18+
expect(strategyDebug.process(impressions[1])).toBe(true);
19+
expect(impressions[1]).toEqual({ ...impression2, pt: undefined });
3020

21+
expect(strategyDebug.process(impressions[2])).toBe(true);
22+
expect(impressions[2]).toEqual({ ...impression1, pt: impression1.time });
3123
});

src/trackers/strategy/__tests__/strategyNone.spec.ts

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,9 @@ test('strategyNone - Client side', () => {
2828

2929
const strategyNone = strategyNoneFactory(impressionCountsCache, uniqueKeysTracker);
3030

31-
const {
32-
impressionsToStore: impressionsToStoreCs,
33-
impressionsToListener: impressionsToListenerCs,
34-
deduped: dedupedCs
35-
} = strategyNone.process(impressions);
31+
impressions.forEach(impression => {
32+
expect(strategyNone.process(impression)).toBe(false);
33+
});
3634

3735
expect(uniqueKeysCacheCS.pop()).toStrictEqual({
3836
keys: [
@@ -52,11 +50,6 @@ test('strategyNone - Client side', () => {
5250
});
5351

5452
expect(uniqueKeysCacheCS.pop()).toStrictEqual({ keys: [] });
55-
56-
expect(impressionsToStoreCs).toStrictEqual([]);
57-
expect(impressionsToListenerCs).toStrictEqual(impressions);
58-
expect(dedupedCs).toStrictEqual(0);
59-
6053
});
6154

6255
test('strategyNone - Server side', () => {
@@ -67,11 +60,9 @@ test('strategyNone - Server side', () => {
6760

6861
const strategyNone = strategyNoneFactory(impressionCountsCache, uniqueKeysTracker);
6962

70-
const {
71-
impressionsToStore: impressionsToStoreSs,
72-
impressionsToListener: impressionsToListenerSs,
73-
deduped: dedupedSs
74-
} = strategyNone.process(impressions);
63+
impressions.forEach(impression => {
64+
expect(strategyNone.process(impression)).toBe(false);
65+
});
7566

7667
expect(uniqueKeysCache.pop()).toStrictEqual({
7768
keys: [
@@ -90,9 +81,4 @@ test('strategyNone - Server side', () => {
9081
]
9182
});
9283
expect(uniqueKeysCache.pop()).toStrictEqual({ keys: [] });
93-
94-
expect(impressionsToStoreSs).toStrictEqual([]);
95-
expect(impressionsToListenerSs).toStrictEqual(impressions);
96-
expect(dedupedSs).toStrictEqual(0);
97-
9884
});

src/trackers/strategy/__tests__/strategyOptimized.spec.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,25 @@ import { strategyOptimizedFactory } from '../strategyOptimized';
44
import { ImpressionCountsCacheInMemory } from '../../../storages/inMemory/ImpressionCountsCacheInMemory';
55
import { impression1, impression2 } from './testUtils';
66

7-
test('strategyOptimized', () => {
8-
9-
let augmentedImp1 = { ...impression1, pt: undefined };
10-
let augmentedImp12 = { ...impression1, pt: impression1.time };
11-
let augmentedImp13 = { ...impression1, pt: impression1.time };
12-
let augmentedImp2 = { ...impression2, pt: undefined };
7+
test.each([
8+
impressionObserverSSFactory(),
9+
impressionObserverCSFactory()
10+
])('strategyOptimized', () => {
1311

1412
const impressionCountsCache = new ImpressionCountsCacheInMemory();
15-
const impressions = [impression1, impression2, {...impression1}, {...impression1}];
16-
const augmentedImpressions = [augmentedImp1, augmentedImp2, augmentedImp12, augmentedImp13];
17-
1813
const strategyOptimizedSS = strategyOptimizedFactory(impressionObserverSSFactory(), impressionCountsCache);
1914

20-
let { impressionsToStore, impressionsToListener, deduped } = strategyOptimizedSS.process(impressions);
21-
22-
expect(impressionsToStore).toStrictEqual([augmentedImp1, augmentedImp2]);
23-
expect(impressionsToListener).toStrictEqual(augmentedImpressions);
24-
expect(deduped).toStrictEqual(2);
15+
const impressions = [{...impression1}, {...impression2}, {...impression1}, {...impression1}];
2516

26-
const strategyOptimizedCS = strategyOptimizedFactory(impressionObserverCSFactory(), impressionCountsCache);
17+
expect(strategyOptimizedSS.process(impressions[0])).toBe(true);
18+
expect(impressions[0]).toEqual({ ...impression1, pt: undefined });
2719

28-
({ impressionsToStore, impressionsToListener, deduped } = strategyOptimizedCS.process(impressions));
20+
expect(strategyOptimizedSS.process(impressions[1])).toBe(true);
21+
expect(impressions[1]).toEqual({ ...impression2, pt: undefined });
2922

30-
expect(impressionsToStore).toStrictEqual([augmentedImp1, augmentedImp2]);
31-
expect(impressionsToListener).toStrictEqual(augmentedImpressions);
32-
expect(deduped).toStrictEqual(2);
23+
expect(strategyOptimizedSS.process(impressions[2])).toBe(false);
24+
expect(impressions[2]).toEqual({ ...impression1, pt: impression1.time });
3325

26+
expect(strategyOptimizedSS.process(impressions[3])).toBe(false);
27+
expect(impressions[3]).toEqual({ ...impression1, pt: impression1.time });
3428
});

src/trackers/strategy/strategyDebug.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,16 @@ import { IStrategy } from '../types';
66
* Debug strategy for impressions tracker. Wraps impressions to store and adds previousTime if it corresponds
77
*
88
* @param impressionsObserver - impression observer. Previous time (pt property) is included in impression instances
9-
* @returns IStrategyResult
9+
* @returns Debug strategy
1010
*/
1111
export function strategyDebugFactory(
1212
impressionsObserver: IImpressionObserver
1313
): IStrategy {
1414

1515
return {
16-
process(impressions: SplitIO.ImpressionDTO[]) {
17-
impressions.forEach((impression) => {
18-
// Adds previous time if it is enabled
19-
impression.pt = impressionsObserver.testAndSet(impression);
20-
});
21-
return {
22-
impressionsToStore: impressions,
23-
impressionsToListener: impressions,
24-
deduped: 0
25-
};
16+
process(impression: SplitIO.ImpressionDTO) {
17+
impression.pt = impressionsObserver.testAndSet(impression);
18+
return true;
2619
}
2720
};
2821
}

src/trackers/strategy/strategyNone.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,24 @@ import { IStrategy, IUniqueKeysTracker } from '../types';
55
/**
66
* None strategy for impressions tracker.
77
*
8-
* @param impressionsCounter - cache to save impressions count. impressions will be deduped (OPTIMIZED mode)
8+
* @param impressionCounts - cache to save impressions count. impressions will be deduped (OPTIMIZED mode)
99
* @param uniqueKeysTracker - unique keys tracker in charge of tracking the unique keys per split.
10-
* @returns IStrategyResult
10+
* @returns None strategy
1111
*/
1212
export function strategyNoneFactory(
13-
impressionsCounter: IImpressionCountsCacheBase,
13+
impressionCounts: IImpressionCountsCacheBase,
1414
uniqueKeysTracker: IUniqueKeysTracker
1515
): IStrategy {
1616

1717
return {
18-
process(impressions: SplitIO.ImpressionDTO[]) {
19-
impressions.forEach((impression) => {
20-
const now = Date.now();
21-
// Increments impression counter per featureName
22-
impressionsCounter.track(impression.feature, now, 1);
23-
// Keep track by unique key
24-
uniqueKeysTracker.track(impression.keyName, impression.feature);
25-
});
26-
27-
return {
28-
impressionsToStore: [],
29-
impressionsToListener: impressions,
30-
deduped: 0
31-
};
18+
process(impression: SplitIO.ImpressionDTO) {
19+
const now = Date.now();
20+
// Increments impression counter per featureName
21+
impressionCounts.track(impression.feature, now, 1);
22+
// Keep track by unique key
23+
uniqueKeysTracker.track(impression.keyName, impression.feature);
24+
// Do not store impressions
25+
return false;
3226
}
3327
};
3428
}

0 commit comments

Comments
 (0)