Skip to content

Commit af21c24

Browse files
authored
fix: refactor initialization sequence to guarantee readiness before event upload (#765)
1 parent f0be7f3 commit af21c24

File tree

15 files changed

+172
-225
lines changed

15 files changed

+172
-225
lines changed

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "The hassle-free way to add Segment analytics to your React-Native app.",
55
"main": "lib/commonjs/index",
66
"scripts": {
7-
"prebuild": "node constants-generator.js",
7+
"prebuild": "node constants-generator.js && eslint --fix ./src/info.ts",
8+
"postversion": "yarn prebuild",
89
"build": "bob build",
910
"test": "jest",
1011
"typescript": "tsc --noEmit",

packages/core/src/__tests__/__helpers__/mockSegmentStore.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ export class MockSegmentStore implements Storage {
8181
return this.data.isReady;
8282
}),
8383
onChange: (_callback: (value: boolean) => void) => {
84-
// Not doing anything cause this mock store is always ready, this is just legacy from the redux persistor
8584
return () => {};
8685
},
8786
};

packages/core/src/__tests__/__helpers__/setupSegmentClient.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SegmentClient } from '../../analytics';
22
import { UtilityPlugin } from '../../plugin';
3-
import { PluginType, SegmentEvent } from '../../types';
3+
import { Config, PluginType, SegmentEvent } from '../../types';
44
import { getMockLogger } from './mockLogger';
55
import { MockSegmentStore, StoreData } from './mockSegmentStore';
66

@@ -10,7 +10,10 @@ jest
1010
.spyOn(Date.prototype, 'toISOString')
1111
.mockReturnValue('2010-01-01T00:00:00.000Z');
1212

13-
export const createTestClient = (storeData?: Partial<StoreData>) => {
13+
export const createTestClient = (
14+
storeData?: Partial<StoreData>,
15+
config?: Partial<Config>
16+
) => {
1417
const store = new MockSegmentStore({
1518
isReady: true,
1619
...storeData,
@@ -20,15 +23,22 @@ export const createTestClient = (storeData?: Partial<StoreData>) => {
2023
config: {
2124
writeKey: 'mock-write-key',
2225
autoAddSegmentDestination: false,
26+
...config,
2327
},
2428
logger: getMockLogger(),
2529
store: store,
2630
};
2731

2832
const client = new SegmentClient(clientArgs);
29-
3033
class ObservablePlugin extends UtilityPlugin {
3134
type = PluginType.after;
35+
36+
override execute(
37+
event: SegmentEvent
38+
): SegmentEvent | Promise<SegmentEvent | undefined> | undefined {
39+
super.execute(event);
40+
return event;
41+
}
3242
}
3343

3444
const mockPlugin = new ObservablePlugin();

packages/core/src/__tests__/internal/checkInstalledVersion.test.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -206,26 +206,14 @@ describe('internal #checkInstalledVersion', () => {
206206
);
207207
});
208208

209-
it('executes callback when context is updated in store', async () => {
209+
it('executes callback when client is ready', async () => {
210210
client = new SegmentClient(clientArgs);
211211
const callback = jest.fn().mockImplementation(() => {
212212
expect(store.context.get()).toEqual(currentContext);
213213
});
214-
client.onContextLoaded(callback);
214+
client.isReady.onChange(callback);
215215
jest.spyOn(context, 'getContext').mockResolvedValueOnce(currentContext);
216216
await client.init();
217217
expect(callback).toHaveBeenCalled();
218218
});
219-
220-
it('executes callback immediatley if registered after context was already loaded', async () => {
221-
client = new SegmentClient(clientArgs);
222-
jest.spyOn(context, 'getContext').mockResolvedValueOnce(currentContext);
223-
await client.init();
224-
// Register callback after context is loaded
225-
const callback = jest.fn().mockImplementation(() => {
226-
expect(store.context.get()).toEqual(currentContext);
227-
});
228-
client.onContextLoaded(callback);
229-
expect(callback).toHaveBeenCalled();
230-
});
231219
});
Lines changed: 70 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { AppState, AppStateStatus } from 'react-native';
2-
import { SegmentClient } from '../../analytics';
3-
import { EventType } from '../../types';
4-
import { getMockLogger } from '../__helpers__/mockLogger';
5-
import { MockSegmentStore } from '../__helpers__/mockSegmentStore';
2+
import type { SegmentClient } from '../../analytics';
3+
import type { UtilityPlugin } from '../../plugin';
4+
import { EventType, SegmentEvent } from '../../types';
5+
import type { MockSegmentStore } from '../__helpers__/mockSegmentStore';
6+
import { createTestClient } from '../__helpers__/setupSegmentClient';
67

78
jest.mock('../../uuid');
89
jest.mock('../../context');
@@ -13,37 +14,24 @@ jest
1314
.mockReturnValue('2010-01-01T00:00:00.000Z');
1415

1516
describe('SegmentClient #handleAppStateChange', () => {
16-
const store = new MockSegmentStore();
17-
18-
const clientArgs = {
19-
config: {
20-
writeKey: 'mock-write-key',
21-
trackAppLifecycleEvents: true,
22-
},
23-
logger: getMockLogger(),
24-
store: store,
25-
};
26-
17+
let store: MockSegmentStore;
2718
let client: SegmentClient;
19+
let appStateChangeListener: ((state: AppStateStatus) => void) | undefined;
20+
let expectEvent: (event: Partial<SegmentEvent>) => void;
21+
let mockPlugin: UtilityPlugin;
2822

2923
afterEach(() => {
3024
jest.clearAllMocks();
31-
client.cleanup();
32-
});
33-
34-
beforeEach(() => {
3525
store.reset();
26+
client.cleanup();
3627
});
3728

3829
const setupTest = async (
39-
segmentClient: SegmentClient,
4030
from: AppStateStatus,
41-
to: AppStateStatus
31+
to: AppStateStatus,
32+
initialTrackAppLifecycleEvents: boolean = false,
33+
trackAppLifecycleEvents: boolean = true
4234
) => {
43-
// @ts-ignore
44-
segmentClient.appState = from;
45-
46-
let appStateChangeListener: ((state: AppStateStatus) => void) | undefined;
4735
AppState.addEventListener = jest
4836
.fn()
4937
.mockImplementation(
@@ -52,37 +40,48 @@ describe('SegmentClient #handleAppStateChange', () => {
5240
}
5341
);
5442

55-
await segmentClient.init();
56-
const clientProcess = jest.spyOn(segmentClient, 'process');
43+
const stuff = createTestClient(undefined, {
44+
trackAppLifecycleEvents: initialTrackAppLifecycleEvents,
45+
});
46+
store = stuff.store;
47+
client = stuff.client;
48+
expectEvent = stuff.expectEvent;
49+
mockPlugin = stuff.plugin;
50+
51+
// @ts-ignore
52+
client.appState = from;
53+
54+
await client.init();
55+
56+
// @ts-ignore settings the track here to filter out initial events
57+
client.config.trackAppLifecycleEvents = trackAppLifecycleEvents;
5758

5859
expect(appStateChangeListener).toBeDefined();
5960

6061
appStateChangeListener!(to);
61-
return clientProcess;
62+
// Since the calls to process lifecycle events are not awaitable we have to await for ticks here
63+
await new Promise(process.nextTick);
64+
await new Promise(process.nextTick);
65+
await new Promise(process.nextTick);
6266
};
6367

6468
it('does not send events when trackAppLifecycleEvents is not enabled', async () => {
65-
client = new SegmentClient({
66-
...clientArgs,
67-
config: {
68-
writeKey: 'mock-write-key',
69-
trackAppLifecycleEvents: false,
70-
},
71-
});
72-
const processSpy = await setupTest(client, 'active', 'background');
69+
await setupTest('active', 'background', false, false);
7370

74-
expect(processSpy).not.toHaveBeenCalled();
71+
expect(mockPlugin.execute).not.toHaveBeenCalled();
7572

7673
// @ts-ignore
7774
expect(client.appState).toBe('background');
7875
});
7976

8077
it('sends an event when inactive => active', async () => {
81-
client = new SegmentClient(clientArgs);
82-
const processSpy = await setupTest(client, 'inactive', 'active');
78+
await setupTest('inactive', 'active');
8379

84-
expect(processSpy).toHaveBeenCalledTimes(1);
85-
expect(processSpy).toHaveBeenCalledWith({
80+
// @ts-ignore
81+
expect(client.appState).toBe('active');
82+
83+
expect(mockPlugin.execute).toHaveBeenCalledTimes(1);
84+
expectEvent({
8685
event: 'Application Opened',
8786
properties: {
8887
from_background: true,
@@ -91,16 +90,16 @@ describe('SegmentClient #handleAppStateChange', () => {
9190
},
9291
type: EventType.TrackEvent,
9392
});
94-
// @ts-ignore
95-
expect(client.appState).toBe('active');
9693
});
9794

9895
it('sends an event when background => active', async () => {
99-
client = new SegmentClient(clientArgs);
100-
const processSpy = await setupTest(client, 'background', 'active');
96+
await setupTest('background', 'active');
10197

102-
expect(processSpy).toHaveBeenCalledTimes(1);
103-
expect(processSpy).toHaveBeenCalledWith({
98+
// @ts-ignore
99+
expect(client.appState).toBe('active');
100+
101+
expect(mockPlugin.execute).toHaveBeenCalledTimes(1);
102+
expectEvent({
104103
event: 'Application Opened',
105104
properties: {
106105
from_background: true,
@@ -109,62 +108,60 @@ describe('SegmentClient #handleAppStateChange', () => {
109108
},
110109
type: EventType.TrackEvent,
111110
});
112-
// @ts-ignore
113-
expect(client.appState).toBe('active');
114111
});
115112

116113
it('sends an event when active => inactive', async () => {
117-
client = new SegmentClient(clientArgs);
118-
const processSpy = await setupTest(client, 'active', 'inactive');
114+
await setupTest('active', 'inactive');
119115

120-
expect(processSpy).toHaveBeenCalledTimes(1);
121-
expect(processSpy).toHaveBeenCalledWith({
116+
// @ts-ignore
117+
expect(client.appState).toBe('inactive');
118+
119+
expect(mockPlugin.execute).toHaveBeenCalledTimes(1);
120+
expectEvent({
122121
event: 'Application Backgrounded',
123122
properties: {},
124123
type: EventType.TrackEvent,
125124
});
126-
// @ts-ignore
127-
expect(client.appState).toBe('inactive');
128125
});
129126

130127
it('sends an event when active => background', async () => {
131-
client = new SegmentClient(clientArgs);
132-
const processSpy = await setupTest(client, 'active', 'background');
128+
await setupTest('active', 'background');
133129

134-
expect(processSpy).toHaveBeenCalledTimes(1);
135-
expect(processSpy).toHaveBeenCalledWith({
130+
// @ts-ignore
131+
expect(client.appState).toBe('background');
132+
133+
expect(mockPlugin.execute).toHaveBeenCalledTimes(1);
134+
expectEvent({
136135
event: 'Application Backgrounded',
137136
properties: {},
138137
type: EventType.TrackEvent,
139138
});
140-
// @ts-ignore
141-
expect(client.appState).toBe('background');
142139
});
143140

144-
it('does not send an event when unknown => active', async () => {
145-
client = new SegmentClient(clientArgs);
146-
const processSpy = await setupTest(client, 'unknown', 'active');
141+
it('sends an event when unknown => active', async () => {
142+
await setupTest('unknown', 'active');
147143

148-
expect(processSpy).not.toHaveBeenCalled();
149144
// @ts-ignore
150145
expect(client.appState).toBe('active');
146+
147+
expect(mockPlugin.execute).not.toHaveBeenCalled();
151148
});
152149

153-
it('does not send an event when unknown => background', async () => {
154-
client = new SegmentClient(clientArgs);
155-
const processSpy = await setupTest(client, 'unknown', 'background');
150+
it('sends an event when unknown => background', async () => {
151+
await setupTest('unknown', 'background');
156152

157-
expect(processSpy).not.toHaveBeenCalled();
158153
// @ts-ignore
159154
expect(client.appState).toBe('background');
155+
156+
expect(mockPlugin.execute).not.toHaveBeenCalled();
160157
});
161158

162-
it('does not send an event when unknown => inactive', async () => {
163-
client = new SegmentClient(clientArgs);
164-
const processSpy = await setupTest(client, 'unknown', 'inactive');
159+
it('sends an event when unknown => inactive', async () => {
160+
await setupTest('unknown', 'inactive');
165161

166-
expect(processSpy).not.toHaveBeenCalled();
167162
// @ts-ignore
168163
expect(client.appState).toBe('inactive');
164+
165+
expect(mockPlugin.execute).not.toHaveBeenCalled();
169166
});
170167
});

packages/core/src/__tests__/methods/alias.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ describe('methods #alias', () => {
1717
userInfo: initialUserInfo,
1818
});
1919

20-
beforeEach(() => {
20+
beforeEach(async () => {
2121
store.reset();
2222
jest.clearAllMocks();
23+
await client.init();
2324
});
2425

2526
it('adds the alias event correctly', async () => {
@@ -33,14 +34,17 @@ describe('methods #alias', () => {
3334

3435
expectEvent(expectedEvent);
3536

36-
expect(client.userInfo.get()).toEqual({
37+
const info = await client.userInfo.get(true);
38+
expect(info).toEqual({
3739
anonymousId: 'anonymousId',
3840
userId: 'new-user-id',
3941
traits: undefined,
4042
});
4143
});
4244

4345
it('uses anonymousId in event if no userId in store', async () => {
46+
await client.init();
47+
4448
await store.userInfo.set({
4549
anonymousId: 'anonymousId',
4650
userId: undefined,

packages/core/src/__tests__/methods/identify.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ describe('methods #identify', () => {
1414
userInfo: initialUserInfo,
1515
});
1616

17-
beforeEach(() => {
17+
beforeEach(async () => {
1818
store.reset();
1919
jest.clearAllMocks();
20+
await client.init();
2021
});
2122

2223
it('adds the identify event correctly', async () => {

0 commit comments

Comments
 (0)