Skip to content

Commit e14acff

Browse files
Merge pull request #374 from splitio/impressions_toggle_strategy_refactors
[Impressions toggle] Strategy refactors
2 parents 5255f06 + 7e13306 commit e14acff

File tree

14 files changed

+109
-171
lines changed

14 files changed

+109
-171
lines changed

src/sdkClient/__tests__/sdkClientMethod.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const paramMocks = [
1616
signalListener: undefined,
1717
settings: { mode: CONSUMER_MODE, log: loggerMock, core: { authorizationKey: 'sdk key '} },
1818
telemetryTracker: telemetryTrackerFactory(),
19-
clients: {}
19+
clients: {},
20+
uniqueKeysTracker: { start: jest.fn(), stop: jest.fn() }
2021
},
2122
// SyncManager (i.e., Sync SDK) and Signal listener
2223
{
@@ -26,7 +27,8 @@ const paramMocks = [
2627
signalListener: { stop: jest.fn() },
2728
settings: { mode: STANDALONE_MODE, log: loggerMock, core: { authorizationKey: 'sdk key '} },
2829
telemetryTracker: telemetryTrackerFactory(),
29-
clients: {}
30+
clients: {},
31+
uniqueKeysTracker: { start: jest.fn(), stop: jest.fn() }
3032
}
3133
];
3234

@@ -70,6 +72,7 @@ test.each(paramMocks)('sdkClientMethodFactory', (params, done: any) => {
7072
client.destroy().then(() => {
7173
expect(params.sdkReadinessManager.readinessManager.destroy).toBeCalledTimes(1);
7274
expect(params.storage.destroy).toBeCalledTimes(1);
75+
expect(params.uniqueKeysTracker.stop).toBeCalledTimes(1);
7376

7477
if (params.syncManager) {
7578
expect(params.syncManager.stop).toBeCalledTimes(1);

src/sdkClient/__tests__/sdkClientMethodCS.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const params = {
4646
settings: settingsWithKey,
4747
telemetryTracker: telemetryTrackerFactory(),
4848
clients: {},
49+
uniqueKeysTracker: { start: jest.fn(), stop: jest.fn() }
4950
};
5051

5152
const invalidAttributes = [
@@ -95,6 +96,7 @@ describe('sdkClientMethodCSFactory', () => {
9596
expect(params.syncManager.stop).toBeCalledTimes(1);
9697
expect(params.syncManager.flush).toBeCalledTimes(1);
9798
expect(params.signalListener.stop).toBeCalledTimes(1);
99+
expect(params.uniqueKeysTracker.stop).toBeCalledTimes(1);
98100
});
99101

100102
});

src/sdkClient/sdkClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo
6161
releaseApiKey(settings.core.authorizationKey);
6262
telemetryTracker.sessionLength();
6363
signalListener && signalListener.stop();
64-
uniqueKeysTracker && uniqueKeysTracker.stop();
64+
uniqueKeysTracker.stop();
6565
}
6666

6767
// Stop background jobs

src/sdkFactory/index.ts

Lines changed: 11 additions & 16 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
@@ -59,21 +59,16 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
5959
const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker });
6060

6161
const observer = impressionsObserverFactory();
62-
const uniqueKeysTracker = impressionsMode === NONE ? uniqueKeysTrackerFactory(log, storage.uniqueKeys!, filterAdapterFactory && filterAdapterFactory()) : undefined;
63-
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-
}
62+
const uniqueKeysTracker = uniqueKeysTrackerFactory(log, storage.uniqueKeys, filterAdapterFactory && filterAdapterFactory());
63+
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
@@ -99,7 +94,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
9994
// We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle.
10095
validateAndTrackApiKey(log, settings.core.authorizationKey);
10196
readiness.init();
102-
uniqueKeysTracker && uniqueKeysTracker.start();
97+
uniqueKeysTracker.start();
10398
syncManager && syncManager.start();
10499
signalListener && signalListener.start();
105100

src/sdkFactory/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface ISdkFactoryContext {
4646
eventTracker: IEventTracker,
4747
telemetryTracker: ITelemetryTracker,
4848
storage: IStorageSync | IStorageAsync,
49-
uniqueKeysTracker?: IUniqueKeysTracker,
49+
uniqueKeysTracker: IUniqueKeysTracker,
5050
signalListener?: ISignalListener
5151
splitApi?: ISplitApi
5252
syncManager?: ISyncManager,

src/trackers/__tests__/impressionsTracker.spec.ts

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

39+
const fakeNoneStrategy = {
40+
process: jest.fn(() => false)
41+
};
42+
3943
/* Tests */
4044

4145
describe('Impressions Tracker', () => {
@@ -48,15 +52,8 @@ describe('Impressions Tracker', () => {
4852

4953
const strategy = strategyDebugFactory(impressionObserverCSFactory());
5054

51-
test('Tracker API', () => {
52-
expect(typeof impressionsTrackerFactory).toBe('function'); // The module should return a function which acts as a factory.
53-
54-
const instance = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategy, fakeWhenInit);
55-
expect(typeof instance.track).toBe('function'); // The instance should implement the track method which will actually track queued impressions.
56-
});
57-
5855
test('Should be able to track impressions (in DEBUG mode without Previous Time).', () => {
59-
const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategy, fakeWhenInit);
56+
const tracker = impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit);
6057

6158
const imp1 = {
6259
feature: '10',
@@ -70,13 +67,13 @@ describe('Impressions Tracker', () => {
7067

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

73-
tracker.track([{ imp: imp1 }, { imp: imp2 }, { imp: imp3 }]);
70+
tracker.track([{ imp: imp1 }, { imp: imp2, track: true }, { imp: imp3, track: false }]);
7471

75-
expect(fakeImpressionsCache.track.mock.calls[0][0]).toEqual([imp1, imp2, imp3]); // Should call the storage track method once we invoke .track() method, passing queued params in a sequence.
72+
expect(fakeImpressionsCache.track.mock.calls[0][0]).toEqual([imp1, imp2]); // Should call the storage track method once we invoke .track() method, passing impressions with `track` enabled
7673
});
7774

7875
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);
76+
const tracker = impressionsTrackerFactory(fakeSettingsWithListener, fakeImpressionsCache, fakeNoneStrategy, strategy, fakeWhenInit, fakeIntegrationsManager);
8077

8178
const fakeImpression = {
8279
feature: 'impression'
@@ -93,9 +90,9 @@ describe('Impressions Tracker', () => {
9390
expect(fakeIntegrationsManager.handleImpression).not.toBeCalled(); // The integrations manager handleImpression method should not be invoked if we haven't tracked impressions.
9491

9592
// We signal that we actually want to track the queued impressions.
96-
tracker.track([{ imp: fakeImpression }, { imp: fakeImpression2 }], fakeAttributes);
93+
tracker.track([{ imp: fakeImpression }, { imp: fakeImpression2, track: false }], fakeAttributes);
9794

98-
expect(fakeImpressionsCache.track.mock.calls[0][0]).toEqual([fakeImpression, fakeImpression2]); // Even with a listener, impression should be sent to the cache
95+
expect(fakeImpressionsCache.track.mock.calls[0][0]).toEqual([fakeImpression]); // Even with a listener, impressions (with `track` enabled) should be sent to the cache
9996
expect(fakeListener.logImpression).not.toBeCalled(); // The listener should not be executed synchronously.
10097
expect(fakeIntegrationsManager.handleImpression).not.toBeCalled(); // The integrations manager handleImpression method should not be executed synchronously.
10198

@@ -150,14 +147,14 @@ describe('Impressions Tracker', () => {
150147
impression3.time = 1234567891;
151148

152149
const trackers = [
153-
impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategyDebugFactory(impressionObserverSSFactory()), fakeWhenInit, undefined),
154-
impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, strategyDebugFactory(impressionObserverCSFactory()), fakeWhenInit, undefined)
150+
impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyDebugFactory(impressionObserverSSFactory()), fakeWhenInit, undefined),
151+
impressionsTrackerFactory(fakeSettings, fakeImpressionsCache, fakeNoneStrategy, strategyDebugFactory(impressionObserverCSFactory()), fakeWhenInit, undefined)
155152
];
156153

157154
expect(fakeImpressionsCache.track).not.toBeCalled(); // storage method should not be called until impressions are tracked.
158155

159156
trackers.forEach(tracker => {
160-
tracker.track([{ imp: impression }, { imp: impression2 }, { imp: impression3 }]);
157+
tracker.track([{ imp: impression, track: true }, { imp: impression2 }, { imp: impression3 }]);
161158

162159
const lastArgs = fakeImpressionsCache.track.mock.calls[fakeImpressionsCache.track.mock.calls.length - 1];
163160

@@ -178,7 +175,7 @@ describe('Impressions Tracker', () => {
178175
impression3.time = Date.now();
179176

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

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

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

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

206203
tracker.track([{ imp: impression }]);
207204
expect(fakeImpressionsCache.track).toBeCalledTimes(1); // impression should be tracked if userConsent is undefined

src/trackers/impressionsTracker.ts

Lines changed: 17 additions & 19 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,
@@ -28,41 +23,44 @@ export function impressionsTrackerFactory(
2823
const { log, impressionListener, runtime: { ip, hostname }, version } = settings;
2924

3025
return {
31-
track(imps: ImpressionDecorated[], attributes?: SplitIO.Attributes) {
26+
track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes) {
3227
if (settings.userConsent === CONSENT_DECLINED) return;
3328

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

38-
const impressionsToListenerCount = impressionsToListener.length;
35+
const impressionsLength = impressions.length;
36+
const impressionsToStoreLength = impressionsToStore.length;
3937

40-
if (impressionsToStore.length > 0) {
41-
const res = impressionsCache.track(impressionsToStore);
38+
if (impressionsToStoreLength) {
39+
const res = impressionsCache.track(impressionsToStore.map((item) => item.imp));
4240

4341
// If we're on an async storage, handle error and log it.
4442
if (thenable(res)) {
4543
res.then(() => {
46-
log.info(IMPRESSIONS_TRACKER_SUCCESS, [impressionsCount]);
44+
log.info(IMPRESSIONS_TRACKER_SUCCESS, [impressionsLength]);
4745
}).catch(err => {
48-
log.error(ERROR_IMPRESSIONS_TRACKER, [impressionsCount, err]);
46+
log.error(ERROR_IMPRESSIONS_TRACKER, [impressionsLength, err]);
4947
});
5048
} else {
5149
// Record when impressionsCache is sync only (standalone mode)
5250
// @TODO we are not dropping impressions on full queue yet, so DROPPED stats are not recorded
5351
if (telemetryCache) {
54-
(telemetryCache as ITelemetryCacheSync).recordImpressionStats(QUEUED, impressionsToStore.length);
55-
(telemetryCache as ITelemetryCacheSync).recordImpressionStats(DEDUPED, deduped);
52+
(telemetryCache as ITelemetryCacheSync).recordImpressionStats(QUEUED, impressionsToStoreLength);
53+
(telemetryCache as ITelemetryCacheSync).recordImpressionStats(DEDUPED, impressionsLength - impressionsToStoreLength);
5654
}
5755
}
5856
}
5957

6058
// @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
6159
if (impressionListener || integrationsManager) {
62-
for (let i = 0; i < impressionsToListenerCount; i++) {
60+
for (let i = 0; i < impressionsLength; i++) {
6361
const impressionData: SplitIO.ImpressionData = {
6462
// copy of impression, to avoid unexpected behaviour if modified by integrations or impressionListener
65-
impression: objectAssign({}, impressionsToListener[i]),
63+
impression: objectAssign({}, impressions[i].imp),
6664
attributes,
6765
ip,
6866
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
});

0 commit comments

Comments
 (0)