Skip to content
This repository was archived by the owner on Jun 27, 2023. It is now read-only.

Commit a302215

Browse files
authored
fix: refactoring redux event subscriptions and locking upload (segmentio#376)
* fix: refactoring redux event subscriptions and locking upload * fix: refactoring redux event subscriptions and locking upload * fix: adding try/finally to flushRetry
1 parent 9003545 commit a302215

File tree

15 files changed

+353
-228
lines changed

15 files changed

+353
-228
lines changed

packages/core/src/__tests__/analytics.test.ts

Lines changed: 75 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
import { combineReducers, configureStore } from '@reduxjs/toolkit';
2+
import type { AppStateStatus } from 'react-native';
3+
import * as ReactNative from 'react-native';
4+
import { EventType, IdentifyEventType } from '..';
15
import { SegmentClient } from '../analytics';
6+
import * as checkInstalledVersion from '../internal/checkInstalledVersion';
7+
import * as flushRetry from '../internal/flushRetry';
8+
import * as handleAppStateChange from '../internal/handleAppStateChange';
9+
import * as trackDeepLinks from '../internal/trackDeepLinks';
210
import { Logger } from '../logger';
3-
import * as ReactNative from 'react-native';
411
import * as alias from '../methods/alias';
12+
import * as flush from '../methods/flush';
513
import * as group from '../methods/group';
614
import * as identify from '../methods/identify';
715
import * as screen from '../methods/screen';
816
import * as track from '../methods/track';
9-
import * as flush from '../methods/flush';
10-
import * as flushRetry from '../internal/flushRetry';
11-
import * as checkInstalledVersion from '../internal/checkInstalledVersion';
12-
import * as handleAppStateChange from '../internal/handleAppStateChange';
13-
import * as trackDeepLinks from '../internal/trackDeepLinks';
14-
import type { AppStateStatus } from 'react-native';
17+
import { actions, Store } from '../store';
18+
import mainSlice from '../store/main';
19+
import systemSlice from '../store/system';
20+
import userInfo from '../store/userInfo';
1521
import { getMockStore } from './__helpers__/mockStore';
1622

1723
jest.mock('redux-persist', () => {
@@ -109,7 +115,8 @@ describe('SegmentClient initialise', () => {
109115

110116
segmentClient.setupStoreSubscribe();
111117

112-
expect(clientArgs.store.subscribe).toHaveBeenCalledTimes(1);
118+
// Each watcher generates a subscription so we just check that it has subscribed at least once
119+
expect(clientArgs.store.subscribe).toHaveBeenCalled();
113120
});
114121
});
115122

@@ -147,7 +154,8 @@ describe('SegmentClient initialise', () => {
147154
const segmentClient = new SegmentClient(clientArgs);
148155
// @ts-ignore actual value is irrelevant
149156
segmentClient.interval = 'INTERVAL';
150-
segmentClient.unsubscribe = jest.fn();
157+
const unsubscribe = jest.fn();
158+
segmentClient.watchers = [unsubscribe];
151159
// @ts-ignore actual value is irrelevant
152160
segmentClient.refreshTimeout = 'TIMEOUT';
153161
segmentClient.appStateSubscription = {
@@ -158,7 +166,7 @@ describe('SegmentClient initialise', () => {
158166
expect(segmentClient.destroyed).toBe(true);
159167
expect(clearInterval).toHaveBeenCalledTimes(1);
160168
expect(clearInterval).toHaveBeenCalledWith('INTERVAL');
161-
expect(segmentClient.unsubscribe).toHaveBeenCalledTimes(1);
169+
expect(unsubscribe).toHaveBeenCalledTimes(1);
162170
expect(clearTimeout).toHaveBeenCalledTimes(1);
163171
expect(clearTimeout).toHaveBeenCalledWith('TIMEOUT');
164172
expect(segmentClient.appStateSubscription.remove).toHaveBeenCalledTimes(
@@ -349,152 +357,98 @@ describe('SegmentClient #onUpdateStore', () => {
349357
actions: {},
350358
};
351359

360+
const sampleEvent: IdentifyEventType = {
361+
userId: 'user-123',
362+
anonymousId: 'eWpqvL-EHSHLWoiwagN-T',
363+
type: EventType.IdentifyEvent,
364+
integrations: {},
365+
timestamp: '2000-01-01T00:00:00.000Z',
366+
traits: {
367+
foo: 'bar',
368+
},
369+
messageId: 'iDMkR2-I7c2_LCsPPlvwH',
370+
};
371+
372+
const rootReducer = combineReducers({
373+
main: mainSlice.reducer,
374+
system: systemSlice.reducer,
375+
userInfo: userInfo.reducer,
376+
});
377+
let mockStore = configureStore({ reducer: rootReducer }) as Store;
378+
352379
beforeEach(() => {
353380
jest.useFakeTimers();
381+
// Reset the Redux store to a clean state
382+
mockStore = configureStore({ reducer: rootReducer }) as Store;
354383
});
355384

356385
afterEach(() => {
357386
jest.clearAllTimers();
358387
jest.clearAllMocks();
359388
});
360389

361-
it('calls flush when there are unsent events', () => {
390+
/**
391+
* Creates a client wired up with store subscriptions and flush mocks for testing automatic flushes
392+
*/
393+
const setupClient = (flushAt: number): SegmentClient => {
362394
const args = {
363395
...clientArgs,
364396
config: {
365397
...clientArgs.config,
366-
flushAt: 1,
367-
},
368-
store: {
369-
...clientArgs.store,
370-
getState: jest.fn().mockReturnValue({
371-
main: {
372-
events: [{ messageId: '1' }],
373-
eventsToRetry: [],
374-
},
375-
system: {
376-
settings: {},
377-
},
378-
}),
398+
flushAt,
379399
},
400+
store: mockStore,
401+
actions: actions,
380402
};
381403
const client = new SegmentClient(args);
404+
// It is important to setup the flush spy before setting up the subscriptions so that it tracks the calls in the closure
382405
jest.spyOn(client, 'flush').mockResolvedValueOnce();
383-
client.onUpdateStore();
406+
jest.spyOn(client, 'flushRetry').mockResolvedValueOnce();
407+
client.setupStoreSubscribe();
408+
return client;
409+
};
384410

411+
it('calls flush when there are unsent events', () => {
412+
const client = setupClient(1);
413+
mockStore.dispatch(mainSlice.actions.addEvent({ event: sampleEvent }));
385414
expect(client.flush).toHaveBeenCalledTimes(1);
386415
});
387416

388417
it('does not flush when number of events does not exceed the flush threshold', () => {
389-
const args = {
390-
...clientArgs,
391-
config: {
392-
...clientArgs.config,
393-
flushAt: 2,
394-
},
395-
store: {
396-
...clientArgs.store,
397-
getState: jest.fn().mockReturnValue({
398-
main: {
399-
events: [{ messageId: '1' }],
400-
eventsToRetry: [],
401-
},
402-
system: {
403-
settings: {},
404-
},
405-
}),
406-
},
407-
};
408-
const client = new SegmentClient(args);
409-
jest.spyOn(client, 'flush').mockResolvedValueOnce();
410-
client.onUpdateStore();
411-
418+
const client = setupClient(2);
419+
mockStore.dispatch(mainSlice.actions.addEvent({ event: sampleEvent }));
412420
expect(client.flush).not.toHaveBeenCalled();
413421
});
414422

415423
it('does not call flush when there are no events to send', () => {
416-
const args = {
417-
...clientArgs,
418-
config: {
419-
...clientArgs.config,
420-
flushAt: 1,
421-
},
422-
store: {
423-
...clientArgs.store,
424-
getState: jest.fn().mockReturnValue({
425-
main: {
426-
events: [],
427-
eventsToRetry: [],
428-
},
429-
system: {
430-
settings: {},
431-
},
432-
}),
433-
},
434-
};
435-
const client = new SegmentClient(args);
436-
jest.spyOn(client, 'flush').mockResolvedValueOnce();
437-
jest.spyOn(client, 'flushRetry').mockResolvedValueOnce();
438-
client.onUpdateStore();
439-
424+
const client = setupClient(1);
440425
expect(client.flush).not.toHaveBeenCalled();
441426
expect(client.flushRetry).not.toHaveBeenCalled();
442427
});
443428

444429
it('flushes retry queue when it is non-empty', () => {
445-
const args = {
446-
...clientArgs,
447-
config: {
448-
...clientArgs.config,
449-
flushAt: 2,
450-
},
451-
store: {
452-
...clientArgs.store,
453-
getState: jest.fn().mockReturnValue({
454-
main: {
455-
events: [],
456-
eventsToRetry: [{ messageId: '1' }],
457-
},
458-
system: {
459-
settings: {},
460-
},
461-
}),
462-
},
463-
};
464-
const client = new SegmentClient(args);
465-
jest.spyOn(client, 'flush').mockResolvedValueOnce();
466-
client.onUpdateStore();
430+
const client = setupClient(2);
467431

468-
expect(setTimeout).toHaveBeenLastCalledWith(
469-
expect.any(Function),
470-
args.config.retryInterval! * 1000
432+
mockStore.dispatch(
433+
mainSlice.actions.addEventsToRetry({
434+
events: [sampleEvent],
435+
config: { ...clientArgs.config },
436+
})
471437
);
472-
expect(client.refreshTimeout).not.toBeNull();
438+
439+
expect(client.flushRetry).toHaveBeenCalledTimes(1);
473440
});
474441

475442
it('does not flush the retry queue when the refreshTimeout is not null', () => {
476-
const args = {
477-
...clientArgs,
478-
config: {
479-
...clientArgs.config,
480-
flushAt: 2,
481-
},
482-
store: {
483-
...clientArgs.store,
484-
getState: jest.fn().mockReturnValue({
485-
main: {
486-
events: [],
487-
eventsToRetry: [{ messageId: '1' }],
488-
},
489-
system: {
490-
settings: {},
491-
},
492-
}),
493-
},
494-
};
495-
const client = new SegmentClient(args);
443+
const client = setupClient(2);
496444
client.refreshTimeout = jest.fn() as any;
497-
client.onUpdateStore();
445+
446+
mockStore.dispatch(
447+
mainSlice.actions.addEventsToRetry({
448+
events: [sampleEvent],
449+
config: { ...clientArgs.config },
450+
})
451+
);
498452

499453
expect(setTimeout).not.toHaveBeenCalled();
500454
});
@@ -579,8 +533,10 @@ describe('SegmentClient #flushRetry', () => {
579533
it('calls the screen method', async () => {
580534
const flushRetrySpy = jest.spyOn(flushRetry, 'default').mockResolvedValue();
581535
const client = new SegmentClient(clientArgs);
536+
client.setupStoreSubscribe();
582537

583538
await client.flushRetry();
539+
jest.runAllTimers();
584540

585541
expect(flushRetrySpy).toHaveBeenCalledTimes(1);
586542
});

packages/core/src/__tests__/store.test.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1+
import { combineReducers, configureStore } from '@reduxjs/toolkit';
2+
3+
import { actions, getStoreWatcher, initializeStore, Store } from '../store';
4+
import {
5+
default as mainSlice,
6+
initialState as mainInitialState,
7+
} from '../store/main';
8+
import {
9+
default as systemSlice,
10+
initialState as systemInitialState,
11+
} from '../store/system';
12+
import {
13+
default as userInfo,
14+
initialState as userInfoInitialState,
15+
} from '../store/userInfo';
116
import {
217
Context,
318
EventType,
419
IdentifyEventType,
520
ScreenEventType,
621
TrackEventType,
722
} from '../types';
8-
import { initializeStore, actions } from '../store';
9-
import { initialState as mainInitialState } from '../store/main';
10-
import { initialState as systemInitialState } from '../store/system';
11-
import { initialState as userInfoInitialState } from '../store/userInfo';
1223

1324
const initialState = {
1425
main: mainInitialState,
@@ -406,4 +417,47 @@ describe('#initializeStore', () => {
406417
});
407418
});
408419
});
420+
421+
describe('getStoreWatcher', () => {
422+
const event = {
423+
userId: 'user-123',
424+
anonymousId: 'eWpqvL-EHSHLWoiwagN-T',
425+
type: EventType.IdentifyEvent,
426+
integrations: {},
427+
timestamp: '2000-01-01T00:00:00.000Z',
428+
traits: {
429+
foo: 'bar',
430+
},
431+
messageId: 'iDMkR2-I7c2_LCsPPlvwH',
432+
} as IdentifyEventType;
433+
434+
const rootReducer = combineReducers({
435+
main: mainSlice.reducer,
436+
system: systemSlice.reducer,
437+
userInfo: userInfo.reducer,
438+
});
439+
let mockStore = configureStore({ reducer: rootReducer }) as Store;
440+
441+
beforeEach(() => {
442+
jest.useFakeTimers();
443+
// Reset the Redux store to a clean state
444+
mockStore = configureStore({ reducer: rootReducer }) as Store;
445+
});
446+
447+
it('subscribes to changes in the selected objects', () => {
448+
const subscription = jest.fn();
449+
const watcher = getStoreWatcher(mockStore);
450+
watcher((state) => state.main.events, subscription);
451+
mockStore.dispatch(mainSlice.actions.addEvent({ event }));
452+
expect(subscription).toHaveBeenCalledTimes(1);
453+
});
454+
455+
it('no trigger for changes in non-selected objects', () => {
456+
const subscription = jest.fn();
457+
const watcher = getStoreWatcher(mockStore);
458+
watcher((state) => state.main.eventsToRetry, subscription);
459+
mockStore.dispatch(mainSlice.actions.addEvent({ event }));
460+
expect(subscription).toHaveBeenCalledTimes(0);
461+
});
462+
});
409463
});

0 commit comments

Comments
 (0)