wallet-stack 1.0.0-alpha.125 → 1.0.0-alpha.126
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.
- package/package.json +3 -1
- package/src/account/SecuritySubmenu.tsx +9 -5
- package/src/analytics/AppAnalytics.test.ts +118 -2
- package/src/analytics/AppAnalytics.ts +97 -38
- package/src/analytics/Events.tsx +1 -0
- package/src/analytics/Properties.tsx +3 -0
- package/src/analytics/docs.ts +1 -0
- package/src/analytics/saga.test.ts +22 -1
- package/src/analytics/saga.ts +12 -1
- package/src/app/actions.ts +1 -1
- package/src/config.ts +3 -0
- package/src/public/types.tsx +4 -0
- package/src/qrcode/schema.ts +2 -4
- package/src/utils/serialization.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallet-stack",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.126",
|
|
4
4
|
"author": "Valora Inc",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"expo-camera": "~16.1.11",
|
|
93
93
|
"expo-splash-screen": "~0.30.10",
|
|
94
94
|
"lottie-react-native": "^5.1.6",
|
|
95
|
+
"mixpanel-react-native": "^3.2.1",
|
|
95
96
|
"react": "19.0.0",
|
|
96
97
|
"react-native": "0.79.6",
|
|
97
98
|
"react-native-adjust": "^4.38.1",
|
|
@@ -160,6 +161,7 @@
|
|
|
160
161
|
"is-ip": "^3.1.0",
|
|
161
162
|
"jwt-decode": "^4.0.0",
|
|
162
163
|
"lodash": "^4.17.21",
|
|
164
|
+
"mixpanel-react-native": "^3.2.1",
|
|
163
165
|
"permissionless": "^0.2.57",
|
|
164
166
|
"react-async-hook": "^4.0.0",
|
|
165
167
|
"react-i18next": "^15.4.1",
|
|
@@ -126,11 +126,15 @@ const SecuritySubmenu = ({ route, navigation }: Props) => {
|
|
|
126
126
|
return onPressContinueWithAccountRemoval()
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const handleToggleAnalytics = (
|
|
130
|
-
dispatch
|
|
131
|
-
|
|
132
|
-
enabled
|
|
133
|
-
|
|
129
|
+
const handleToggleAnalytics = (enabled: boolean) => {
|
|
130
|
+
// Fire analytics event either before or after dispatch -- to ensure it is not skipped
|
|
131
|
+
if (enabled) {
|
|
132
|
+
dispatch(setAnalyticsEnabled(enabled))
|
|
133
|
+
AppAnalytics.track(SettingsEvents.settings_analytics, { enabled })
|
|
134
|
+
} else {
|
|
135
|
+
AppAnalytics.track(SettingsEvents.settings_analytics, { enabled })
|
|
136
|
+
dispatch(setAnalyticsEnabled(enabled))
|
|
137
|
+
}
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
const handleRequirePinToggle = (value: boolean) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createClient } from '@segment/analytics-react-native'
|
|
2
2
|
import { PincodeType } from 'src/account/reducer'
|
|
3
3
|
import AppAnalyticsModule from 'src/analytics/AppAnalytics'
|
|
4
|
-
import { OnboardingEvents } from 'src/analytics/Events'
|
|
4
|
+
import { NavigationEvents, OnboardingEvents } from 'src/analytics/Events'
|
|
5
5
|
import * as config from 'src/config'
|
|
6
6
|
import { store } from 'src/redux/store'
|
|
7
7
|
import StatsigClientSingleton from 'src/statsig/client'
|
|
@@ -37,6 +37,29 @@ jest.mock('src/web3/networkConfig', () => {
|
|
|
37
37
|
}
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
const mockMixpanelInit = jest.fn().mockResolvedValue(undefined)
|
|
41
|
+
const mockMixpanelTrack = jest.fn()
|
|
42
|
+
const mockMixpanelIdentify = jest.fn().mockResolvedValue(undefined)
|
|
43
|
+
const mockMixpanelReset = jest.fn()
|
|
44
|
+
const mockMixpanelFlush = jest.fn()
|
|
45
|
+
const mockMixpanelPeopleSet = jest.fn()
|
|
46
|
+
const mockMixpanelOptInTracking = jest.fn()
|
|
47
|
+
const mockMixpanelOptOutTracking = jest.fn()
|
|
48
|
+
const mockMixpanelConstructor = jest.fn()
|
|
49
|
+
|
|
50
|
+
jest.mock('mixpanel-react-native', () => ({
|
|
51
|
+
Mixpanel: mockMixpanelConstructor.mockImplementation(() => ({
|
|
52
|
+
init: mockMixpanelInit,
|
|
53
|
+
track: mockMixpanelTrack,
|
|
54
|
+
identify: mockMixpanelIdentify,
|
|
55
|
+
reset: mockMixpanelReset,
|
|
56
|
+
flush: mockMixpanelFlush,
|
|
57
|
+
getPeople: jest.fn(() => ({ set: mockMixpanelPeopleSet })),
|
|
58
|
+
optInTracking: mockMixpanelOptInTracking,
|
|
59
|
+
optOutTracking: mockMixpanelOptOutTracking,
|
|
60
|
+
})),
|
|
61
|
+
}))
|
|
62
|
+
|
|
40
63
|
const mockDeviceId = 'abc-def-123' // mocked in __mocks__/react-native-device-info.ts (but importing from that file causes weird errors)
|
|
41
64
|
const expectedSessionId = '453e535d43b22002185f316d5b41561010d9224580bfb608da132e74b128227a'
|
|
42
65
|
const mockWalletAddress = '0x12AE66CDc592e10B60f9097a7b0D3C59fce29876' // deliberately using checksummed version here
|
|
@@ -206,6 +229,8 @@ describe('AppAnalytics', () => {
|
|
|
206
229
|
mockConfig.STATSIG_API_KEY = 'statsig-key'
|
|
207
230
|
mockConfig.STATSIG_ENABLED = true
|
|
208
231
|
mockConfig.SEGMENT_API_KEY = 'segment-key'
|
|
232
|
+
mockConfig.MIXPANEL_TOKEN = 'mixpanel-token'
|
|
233
|
+
mockConfig.MIXPANEL_ENABLED = true
|
|
209
234
|
mockConfig.ENABLED_NETWORK_IDS = ['celo-alfajores']
|
|
210
235
|
mockStore.getState.mockImplementation(() => state)
|
|
211
236
|
})
|
|
@@ -238,27 +263,55 @@ describe('AppAnalytics', () => {
|
|
|
238
263
|
await AppAnalytics.init()
|
|
239
264
|
expect(StatsigClientSingleton.initialize).not.toHaveBeenCalled()
|
|
240
265
|
})
|
|
266
|
+
|
|
267
|
+
it('initializes Mixpanel when MIXPANEL_TOKEN is present', async () => {
|
|
268
|
+
await AppAnalytics.init()
|
|
269
|
+
expect(mockMixpanelConstructor).toHaveBeenCalledWith('mixpanel-token', false, true)
|
|
270
|
+
expect(mockMixpanelInit).toHaveBeenCalledWith(false, undefined, undefined)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('does not initialize Mixpanel if MIXPANEL_TOKEN is not present', async () => {
|
|
274
|
+
mockConfig.MIXPANEL_TOKEN = undefined
|
|
275
|
+
mockConfig.MIXPANEL_ENABLED = false
|
|
276
|
+
await AppAnalytics.init()
|
|
277
|
+
expect(mockMixpanelConstructor).not.toHaveBeenCalled()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('initializes Mixpanel with custom API host when provided', async () => {
|
|
281
|
+
mockConfig.MIXPANEL_API_HOST = 'https://api-eu.mixpanel.com'
|
|
282
|
+
await AppAnalytics.init()
|
|
283
|
+
expect(mockMixpanelInit).toHaveBeenCalledWith(false, undefined, 'https://api-eu.mixpanel.com')
|
|
284
|
+
})
|
|
241
285
|
})
|
|
242
286
|
|
|
243
287
|
it('delays identify calls until async init has finished', async () => {
|
|
244
288
|
AppAnalytics.identify('0xUSER', { someUserProp: 'testValue' })
|
|
245
289
|
expect(mockSegmentClient.identify).not.toHaveBeenCalled()
|
|
290
|
+
expect(mockMixpanelConstructor).not.toHaveBeenCalled()
|
|
246
291
|
|
|
247
292
|
await AppAnalytics.init()
|
|
248
293
|
// Now that init has finished identify should have been called
|
|
249
294
|
expect(mockSegmentClient.identify).toHaveBeenCalledWith('0xUSER', { someUserProp: 'testValue' })
|
|
295
|
+
expect(mockMixpanelIdentify).toHaveBeenCalledWith('0xUSER')
|
|
296
|
+
expect(mockMixpanelPeopleSet).toHaveBeenCalledWith({ someUserProp: 'testValue' })
|
|
250
297
|
|
|
251
298
|
// And now test that identify calls go trough directly
|
|
252
299
|
mockSegmentClient.identify.mockClear()
|
|
300
|
+
mockMixpanelIdentify.mockClear()
|
|
301
|
+
mockMixpanelPeopleSet.mockClear()
|
|
253
302
|
AppAnalytics.identify('0xUSER2', { someUserProp: 'testValue2' })
|
|
254
303
|
expect(mockSegmentClient.identify).toHaveBeenCalledWith('0xUSER2', {
|
|
255
304
|
someUserProp: 'testValue2',
|
|
256
305
|
})
|
|
306
|
+
expect(mockMixpanelIdentify).toHaveBeenCalledWith('0xUSER2')
|
|
307
|
+
await mockMixpanelIdentify.mock.results[0].value // await the identify() promise
|
|
308
|
+
expect(mockMixpanelPeopleSet).toHaveBeenCalledWith({ someUserProp: 'testValue2' })
|
|
257
309
|
})
|
|
258
310
|
|
|
259
311
|
it('delays track calls until async init has finished', async () => {
|
|
260
312
|
AppAnalytics.track(OnboardingEvents.pin_invalid, { error: 'some error' })
|
|
261
313
|
expect(mockSegmentClient.track).not.toHaveBeenCalled()
|
|
314
|
+
expect(mockMixpanelConstructor).not.toHaveBeenCalled()
|
|
262
315
|
|
|
263
316
|
await AppAnalytics.init()
|
|
264
317
|
// Now that init has finished track should have been called
|
|
@@ -267,32 +320,52 @@ describe('AppAnalytics', () => {
|
|
|
267
320
|
...defaultProperties,
|
|
268
321
|
error: 'some error',
|
|
269
322
|
})
|
|
323
|
+
expect(mockMixpanelTrack).toHaveBeenCalledTimes(1)
|
|
324
|
+
expect(mockMixpanelTrack).toHaveBeenCalledWith(OnboardingEvents.pin_invalid, {
|
|
325
|
+
...defaultProperties,
|
|
326
|
+
error: 'some error',
|
|
327
|
+
})
|
|
270
328
|
|
|
271
329
|
// And now test that track calls go trough directly
|
|
272
330
|
mockSegmentClient.track.mockClear()
|
|
331
|
+
mockMixpanelTrack.mockClear()
|
|
273
332
|
AppAnalytics.track(OnboardingEvents.pin_invalid, { error: 'some error' })
|
|
274
333
|
expect(mockSegmentClient.track).toHaveBeenCalledTimes(1)
|
|
275
334
|
expect(mockSegmentClient.track).toHaveBeenCalledWith(OnboardingEvents.pin_invalid, {
|
|
276
335
|
...defaultProperties,
|
|
277
336
|
error: 'some error',
|
|
278
337
|
})
|
|
338
|
+
expect(mockMixpanelTrack).toHaveBeenCalledTimes(1)
|
|
339
|
+
expect(mockMixpanelTrack).toHaveBeenCalledWith(OnboardingEvents.pin_invalid, {
|
|
340
|
+
...defaultProperties,
|
|
341
|
+
error: 'some error',
|
|
342
|
+
})
|
|
279
343
|
})
|
|
280
344
|
|
|
281
345
|
it('delays screen calls until async init has finished', async () => {
|
|
282
346
|
AppAnalytics.page('Some Page', { someProp: 'testValue' })
|
|
283
347
|
expect(mockSegmentClient.screen).not.toHaveBeenCalled()
|
|
348
|
+
expect(mockMixpanelConstructor).not.toHaveBeenCalled()
|
|
284
349
|
|
|
285
350
|
await AppAnalytics.init()
|
|
286
|
-
// Now that init has finished
|
|
351
|
+
// Now that init has finished screen should have been called
|
|
287
352
|
expect(mockSegmentClient.screen).toHaveBeenCalledTimes(1)
|
|
288
353
|
expect(mockSegmentClient.screen).toHaveBeenCalledWith('Some Page', {
|
|
289
354
|
...defaultProperties,
|
|
290
355
|
sCurrentScreenId: 'Some Page',
|
|
291
356
|
someProp: 'testValue',
|
|
292
357
|
})
|
|
358
|
+
expect(mockMixpanelTrack).toHaveBeenCalledTimes(1)
|
|
359
|
+
expect(mockMixpanelTrack).toHaveBeenCalledWith(NavigationEvents.screen_viewed, {
|
|
360
|
+
screen_name: 'Some Page',
|
|
361
|
+
...defaultProperties,
|
|
362
|
+
sCurrentScreenId: 'Some Page',
|
|
363
|
+
someProp: 'testValue',
|
|
364
|
+
})
|
|
293
365
|
|
|
294
366
|
// And now test that page calls go trough directly
|
|
295
367
|
mockSegmentClient.screen.mockClear()
|
|
368
|
+
mockMixpanelTrack.mockClear()
|
|
296
369
|
AppAnalytics.page('Some Page2', { someProp: 'testValue2' })
|
|
297
370
|
expect(mockSegmentClient.screen).toHaveBeenCalledTimes(1)
|
|
298
371
|
expect(mockSegmentClient.screen).toHaveBeenCalledWith('Some Page2', {
|
|
@@ -301,6 +374,14 @@ describe('AppAnalytics', () => {
|
|
|
301
374
|
someProp: 'testValue2',
|
|
302
375
|
sPrevScreenId: 'Some Page',
|
|
303
376
|
})
|
|
377
|
+
expect(mockMixpanelTrack).toHaveBeenCalledTimes(1)
|
|
378
|
+
expect(mockMixpanelTrack).toHaveBeenCalledWith(NavigationEvents.screen_viewed, {
|
|
379
|
+
screen_name: 'Some Page2',
|
|
380
|
+
...defaultProperties,
|
|
381
|
+
sCurrentScreenId: 'Some Page2',
|
|
382
|
+
someProp: 'testValue2',
|
|
383
|
+
sPrevScreenId: 'Some Page',
|
|
384
|
+
})
|
|
304
385
|
})
|
|
305
386
|
|
|
306
387
|
it('adds super properties to all tracked events', async () => {
|
|
@@ -311,6 +392,11 @@ describe('AppAnalytics', () => {
|
|
|
311
392
|
...defaultProperties,
|
|
312
393
|
error: 'some error',
|
|
313
394
|
})
|
|
395
|
+
expect(mockMixpanelTrack).toHaveBeenCalledTimes(1)
|
|
396
|
+
expect(mockMixpanelTrack).toHaveBeenCalledWith(OnboardingEvents.pin_invalid, {
|
|
397
|
+
...defaultProperties,
|
|
398
|
+
error: 'some error',
|
|
399
|
+
})
|
|
314
400
|
})
|
|
315
401
|
|
|
316
402
|
it('adds super properties to all screen events', async () => {
|
|
@@ -322,6 +408,36 @@ describe('AppAnalytics', () => {
|
|
|
322
408
|
someProp: 'someValue',
|
|
323
409
|
sCurrentScreenId: 'ScreenA',
|
|
324
410
|
})
|
|
411
|
+
expect(mockMixpanelTrack).toHaveBeenCalledTimes(1)
|
|
412
|
+
expect(mockMixpanelTrack).toHaveBeenCalledWith(NavigationEvents.screen_viewed, {
|
|
413
|
+
screen_name: 'ScreenA',
|
|
414
|
+
...defaultProperties,
|
|
415
|
+
someProp: 'someValue',
|
|
416
|
+
sCurrentScreenId: 'ScreenA',
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('resets both Segment and Mixpanel', async () => {
|
|
421
|
+
await AppAnalytics.init()
|
|
422
|
+
await AppAnalytics.reset()
|
|
423
|
+
expect(mockSegmentClient.flush).toHaveBeenCalled()
|
|
424
|
+
expect(mockSegmentClient.reset).toHaveBeenCalled()
|
|
425
|
+
expect(mockMixpanelFlush).toHaveBeenCalled()
|
|
426
|
+
expect(mockMixpanelReset).toHaveBeenCalled()
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('calls optOutTracking when analytics is disabled', async () => {
|
|
430
|
+
await AppAnalytics.init()
|
|
431
|
+
AppAnalytics.setAnalyticsEnabled(false)
|
|
432
|
+
expect(mockMixpanelOptOutTracking).toHaveBeenCalled()
|
|
433
|
+
expect(mockMixpanelOptInTracking).not.toHaveBeenCalled()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('calls optInTracking when analytics is enabled', async () => {
|
|
437
|
+
await AppAnalytics.init()
|
|
438
|
+
AppAnalytics.setAnalyticsEnabled(true)
|
|
439
|
+
expect(mockMixpanelOptInTracking).toHaveBeenCalled()
|
|
440
|
+
expect(mockMixpanelOptOutTracking).not.toHaveBeenCalled()
|
|
325
441
|
})
|
|
326
442
|
|
|
327
443
|
it('returns a different sessionId if the time is different', async () => {
|
|
@@ -2,12 +2,12 @@ import { createClient, SegmentClient } from '@segment/analytics-react-native'
|
|
|
2
2
|
import { AdjustPlugin } from '@segment/analytics-react-native-plugin-adjust'
|
|
3
3
|
import { DestinationFiltersPlugin } from '@segment/analytics-react-native-plugin-destination-filters'
|
|
4
4
|
import { FirebasePlugin } from '@segment/analytics-react-native-plugin-firebase'
|
|
5
|
-
import
|
|
5
|
+
import { Mixpanel } from 'mixpanel-react-native'
|
|
6
6
|
import { Platform } from 'react-native'
|
|
7
7
|
import DeviceInfo from 'react-native-device-info'
|
|
8
8
|
import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions'
|
|
9
9
|
import { AsyncStoragePersistor } from 'src/analytics/AsyncStoragePersistor'
|
|
10
|
-
import { AppEvents } from 'src/analytics/Events'
|
|
10
|
+
import { AppEvents, NavigationEvents } from 'src/analytics/Events'
|
|
11
11
|
import { InjectTraits } from 'src/analytics/InjectTraits'
|
|
12
12
|
import { AnalyticsPropertiesList } from 'src/analytics/Properties'
|
|
13
13
|
import { getCurrentUserTraits } from 'src/analytics/selectors'
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
DEFAULT_TESTNET,
|
|
16
16
|
FIREBASE_ENABLED,
|
|
17
17
|
isE2EEnv,
|
|
18
|
+
MIXPANEL_API_HOST,
|
|
19
|
+
MIXPANEL_ENABLED,
|
|
20
|
+
MIXPANEL_TOKEN,
|
|
18
21
|
SEGMENT_API_KEY,
|
|
19
22
|
STATSIG_ENABLED,
|
|
20
23
|
STATSIG_ENV,
|
|
@@ -23,6 +26,7 @@ import { store } from 'src/redux/store'
|
|
|
23
26
|
import StatsigClientSingleton from 'src/statsig/client'
|
|
24
27
|
import { ensureError } from 'src/utils/ensureError'
|
|
25
28
|
import Logger from 'src/utils/Logger'
|
|
29
|
+
import { sanitizeProperties } from 'src/utils/serialization'
|
|
26
30
|
import { sha256 } from 'viem'
|
|
27
31
|
|
|
28
32
|
const TAG = 'AppAnalytics'
|
|
@@ -94,6 +98,7 @@ class AppAnalytics {
|
|
|
94
98
|
private currentScreenId: string | undefined
|
|
95
99
|
private prevScreenId: string | undefined
|
|
96
100
|
private segmentClient: SegmentClient | undefined
|
|
101
|
+
private mixpanelClient: Mixpanel | undefined
|
|
97
102
|
|
|
98
103
|
async init() {
|
|
99
104
|
let uniqueID
|
|
@@ -153,6 +158,26 @@ class AppAnalytics {
|
|
|
153
158
|
} else {
|
|
154
159
|
Logger.info(TAG, 'Statsig is not enabled, skipping setup')
|
|
155
160
|
}
|
|
161
|
+
|
|
162
|
+
if (MIXPANEL_ENABLED && MIXPANEL_TOKEN) {
|
|
163
|
+
try {
|
|
164
|
+
const trackLegacyAutomaticEvents = false
|
|
165
|
+
const useNative = true
|
|
166
|
+
this.mixpanelClient = new Mixpanel(MIXPANEL_TOKEN, trackLegacyAutomaticEvents, useNative)
|
|
167
|
+
|
|
168
|
+
const optOutTrackingDefault = !this.isEnabled()
|
|
169
|
+
const superProperties = undefined
|
|
170
|
+
const serverURL = MIXPANEL_API_HOST
|
|
171
|
+
await this.mixpanelClient.init(optOutTrackingDefault, superProperties, serverURL)
|
|
172
|
+
|
|
173
|
+
Logger.info(TAG, 'Mixpanel initialized!')
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const error = ensureError(err)
|
|
176
|
+
Logger.error(TAG, `Mixpanel setup error: ${error.message}\n`, error)
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
Logger.info(TAG, 'Mixpanel token not present, skipping setup')
|
|
180
|
+
}
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
isEnabled() {
|
|
@@ -178,6 +203,16 @@ class AppAnalytics {
|
|
|
178
203
|
return this.sessionId
|
|
179
204
|
}
|
|
180
205
|
|
|
206
|
+
setAnalyticsEnabled(enabled: boolean) {
|
|
207
|
+
if (this.mixpanelClient) {
|
|
208
|
+
if (enabled) {
|
|
209
|
+
this.mixpanelClient.optInTracking()
|
|
210
|
+
} else {
|
|
211
|
+
this.mixpanelClient.optOutTracking()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
181
216
|
track<EventName extends keyof AnalyticsPropertiesList>(
|
|
182
217
|
...args: undefined extends AnalyticsPropertiesList[EventName]
|
|
183
218
|
? [EventName] | [EventName, AnalyticsPropertiesList[EventName]]
|
|
@@ -190,15 +225,10 @@ class AppAnalytics {
|
|
|
190
225
|
return
|
|
191
226
|
}
|
|
192
227
|
|
|
193
|
-
|
|
194
|
-
Logger.debug(TAG, `segmentClient undefined, not tracking event ${eventName}`)
|
|
195
|
-
return
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const props: {} = {
|
|
228
|
+
const props = sanitizeProperties({
|
|
199
229
|
...this.getSuperProps(),
|
|
200
230
|
...eventProperties,
|
|
201
|
-
}
|
|
231
|
+
})
|
|
202
232
|
|
|
203
233
|
if (__DEV__) {
|
|
204
234
|
Logger.debug(TAG, `Tracking event ${eventName} with properties:`, props)
|
|
@@ -206,9 +236,17 @@ class AppAnalytics {
|
|
|
206
236
|
Logger.info(TAG, `Tracking event ${eventName}`)
|
|
207
237
|
}
|
|
208
238
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
// Track to Segment
|
|
240
|
+
if (this.segmentClient) {
|
|
241
|
+
this.segmentClient.track(eventName, props).catch((err) => {
|
|
242
|
+
Logger.error(TAG, `Failed to track event ${eventName} to Segment`, err)
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Track to Mixpanel
|
|
247
|
+
if (this.mixpanelClient) {
|
|
248
|
+
this.mixpanelClient.track(eventName, props)
|
|
249
|
+
}
|
|
212
250
|
}
|
|
213
251
|
|
|
214
252
|
identify(userID: string | null, traits: {}) {
|
|
@@ -222,17 +260,27 @@ class AppAnalytics {
|
|
|
222
260
|
return
|
|
223
261
|
}
|
|
224
262
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
263
|
+
const safeTraits = sanitizeProperties(traits)
|
|
264
|
+
|
|
265
|
+
// Identify in Segment
|
|
266
|
+
if (this.segmentClient) {
|
|
267
|
+
this.segmentClient.identify(userID, safeTraits).catch((err) => {
|
|
268
|
+
Logger.error(TAG, `Failed to identify user ${userID} in Segment`, err)
|
|
269
|
+
throw err
|
|
270
|
+
})
|
|
228
271
|
}
|
|
229
|
-
// The firebase segment plugin can't handle null or undefined values
|
|
230
|
-
const safeTraits = _.omitBy(traits, _.isNil)
|
|
231
272
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
273
|
+
// Identify in Mixpanel
|
|
274
|
+
if (this.mixpanelClient) {
|
|
275
|
+
this.mixpanelClient
|
|
276
|
+
.identify(userID)
|
|
277
|
+
.then(() => {
|
|
278
|
+
this.mixpanelClient!.getPeople().set(safeTraits)
|
|
279
|
+
})
|
|
280
|
+
.catch((err) => {
|
|
281
|
+
Logger.error(TAG, `Failed to identify user ${userID} in Mixpanel`, err)
|
|
282
|
+
})
|
|
283
|
+
}
|
|
236
284
|
}
|
|
237
285
|
|
|
238
286
|
page(screenId: string, eventProperties = {}) {
|
|
@@ -241,36 +289,47 @@ class AppAnalytics {
|
|
|
241
289
|
return
|
|
242
290
|
}
|
|
243
291
|
|
|
244
|
-
if (!this.segmentClient) {
|
|
245
|
-
Logger.debug(TAG, `segmentClient is undefined, not tracking screen ${screenId}`)
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
|
|
249
292
|
if (screenId !== this.currentScreenId) {
|
|
250
293
|
this.prevScreenId = this.currentScreenId
|
|
251
294
|
this.currentScreenId = screenId
|
|
252
295
|
}
|
|
253
296
|
|
|
254
|
-
const props
|
|
297
|
+
const props = sanitizeProperties({
|
|
255
298
|
...this.getSuperProps(),
|
|
256
299
|
...eventProperties,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// Track screen in Segment
|
|
303
|
+
if (this.segmentClient) {
|
|
304
|
+
this.segmentClient.screen(screenId, props).catch((err) => {
|
|
305
|
+
Logger.error(TAG, 'Error tracking page in Segment', err)
|
|
306
|
+
})
|
|
257
307
|
}
|
|
258
308
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
309
|
+
// Track screen in Mixpanel
|
|
310
|
+
if (this.mixpanelClient) {
|
|
311
|
+
this.mixpanelClient.track(NavigationEvents.screen_viewed, {
|
|
312
|
+
screen_name: screenId,
|
|
313
|
+
...props,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
262
316
|
}
|
|
263
317
|
|
|
264
318
|
async reset() {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
319
|
+
// Reset Segment
|
|
320
|
+
if (this.segmentClient) {
|
|
321
|
+
try {
|
|
322
|
+
await this.segmentClient.flush()
|
|
323
|
+
await this.segmentClient.reset()
|
|
324
|
+
} catch (error) {
|
|
325
|
+
Logger.error(TAG, 'Error resetting Segment analytics', error)
|
|
326
|
+
}
|
|
268
327
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
328
|
+
|
|
329
|
+
// Reset Mixpanel
|
|
330
|
+
if (this.mixpanelClient) {
|
|
331
|
+
this.mixpanelClient.flush()
|
|
332
|
+
this.mixpanelClient.reset()
|
|
274
333
|
}
|
|
275
334
|
}
|
|
276
335
|
|
package/src/analytics/Events.tsx
CHANGED
|
@@ -930,6 +930,9 @@ interface PerformanceProperties {
|
|
|
930
930
|
|
|
931
931
|
interface NavigationProperties {
|
|
932
932
|
[NavigationEvents.navigator_not_ready]: undefined
|
|
933
|
+
[NavigationEvents.screen_viewed]: {
|
|
934
|
+
screen_name: string
|
|
935
|
+
}
|
|
933
936
|
}
|
|
934
937
|
|
|
935
938
|
export interface WalletConnect1Properties {
|
package/src/analytics/docs.ts
CHANGED
|
@@ -430,6 +430,7 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
|
|
|
430
430
|
[ContractKitEvents.init_contractkit_finish]: ``,
|
|
431
431
|
[PerformanceEvents.redux_store_size]: ``,
|
|
432
432
|
[NavigationEvents.navigator_not_ready]: ``,
|
|
433
|
+
[NavigationEvents.screen_viewed]: `When a screen is viewed in the app`,
|
|
433
434
|
|
|
434
435
|
// Events related to Points program
|
|
435
436
|
[PointsEvents.points_discover_press]: `when points card is pressed in the discover tab`,
|
|
@@ -2,8 +2,9 @@ import { expectSaga } from 'redux-saga-test-plan'
|
|
|
2
2
|
import { dynamic } from 'redux-saga-test-plan/providers'
|
|
3
3
|
import { select } from 'redux-saga/effects'
|
|
4
4
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
5
|
-
import { updateUserTraits } from 'src/analytics/saga'
|
|
5
|
+
import { handleSetAnalyticsEnabled, updateUserTraits } from 'src/analytics/saga'
|
|
6
6
|
import { getCurrentUserTraits } from 'src/analytics/selectors'
|
|
7
|
+
import { Actions } from 'src/app/actions'
|
|
7
8
|
|
|
8
9
|
jest.mock('src/config', () => ({
|
|
9
10
|
...jest.requireActual('src/config'),
|
|
@@ -55,3 +56,23 @@ describe(updateUserTraits, () => {
|
|
|
55
56
|
})
|
|
56
57
|
})
|
|
57
58
|
})
|
|
59
|
+
|
|
60
|
+
describe(handleSetAnalyticsEnabled, () => {
|
|
61
|
+
it('calls setAnalyticsEnabled with the enabled value', async () => {
|
|
62
|
+
await expectSaga(handleSetAnalyticsEnabled, {
|
|
63
|
+
type: Actions.SET_ANALYTICS_ENABLED,
|
|
64
|
+
enabled: false,
|
|
65
|
+
}).silentRun()
|
|
66
|
+
|
|
67
|
+
expect(AppAnalytics.setAnalyticsEnabled).toHaveBeenCalledWith(false)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('calls setAnalyticsEnabled when analytics is re-enabled', async () => {
|
|
71
|
+
await expectSaga(handleSetAnalyticsEnabled, {
|
|
72
|
+
type: Actions.SET_ANALYTICS_ENABLED,
|
|
73
|
+
enabled: true,
|
|
74
|
+
}).silentRun()
|
|
75
|
+
|
|
76
|
+
expect(AppAnalytics.setAnalyticsEnabled).toHaveBeenCalledWith(true)
|
|
77
|
+
})
|
|
78
|
+
})
|
package/src/analytics/saga.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
2
2
|
import { getCurrentUserTraits } from 'src/analytics/selectors'
|
|
3
|
-
import {
|
|
3
|
+
import { Actions, SetAnalyticsEnabled } from 'src/app/actions'
|
|
4
|
+
import { safely } from 'src/utils/safely'
|
|
5
|
+
import { call, select, spawn, take, takeEvery } from 'typed-redux-saga'
|
|
4
6
|
|
|
5
7
|
export function* updateUserTraits() {
|
|
6
8
|
let prevTraits
|
|
@@ -16,6 +18,15 @@ export function* updateUserTraits() {
|
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
export function* handleSetAnalyticsEnabled(action: SetAnalyticsEnabled) {
|
|
22
|
+
yield* call([AppAnalytics, 'setAnalyticsEnabled'], action.enabled)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function* watchAnalyticsEnabled() {
|
|
26
|
+
yield* takeEvery(Actions.SET_ANALYTICS_ENABLED, safely(handleSetAnalyticsEnabled))
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export function* analyticsSaga() {
|
|
20
30
|
yield* spawn(updateUserTraits)
|
|
31
|
+
yield* spawn(watchAnalyticsEnabled)
|
|
21
32
|
}
|
package/src/app/actions.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -97,6 +97,9 @@ export const ALCHEMY_API_KEY = experimentalConfig.alchemyApiKey
|
|
|
97
97
|
export const STATSIG_API_KEY = appConfig.features?.statsig?.apiKey
|
|
98
98
|
export const STATSIG_ENABLED = !isE2EEnv && !!STATSIG_API_KEY
|
|
99
99
|
export const SEGMENT_API_KEY = appConfig.features?.segment?.apiKey
|
|
100
|
+
export const MIXPANEL_TOKEN = appConfig.features?.mixpanel?.token
|
|
101
|
+
export const MIXPANEL_API_HOST = appConfig.features?.mixpanel?.apiHost
|
|
102
|
+
export const MIXPANEL_ENABLED = !isE2EEnv && !!MIXPANEL_TOKEN
|
|
100
103
|
export const AUTH0_CLIENT_ID =
|
|
101
104
|
DEFAULT_TESTNET === 'mainnet'
|
|
102
105
|
? 'FS2sPfMvDBKy0udOoCbc4ao8HakvAR6b'
|
package/src/public/types.tsx
CHANGED
package/src/qrcode/schema.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { PathReporter } from 'io-ts/lib/PathReporter'
|
|
|
11
11
|
import { DEEP_LINK_URL_SCHEME } from 'src/config'
|
|
12
12
|
import { LocalCurrencyCode } from 'src/localCurrency/consts'
|
|
13
13
|
import { AddressType, E164PhoneNumberType } from 'src/utils/io'
|
|
14
|
+
import { sanitizeProperties } from 'src/utils/serialization'
|
|
14
15
|
import { parse } from 'url'
|
|
15
16
|
|
|
16
17
|
export const UriDataType = ioType({
|
|
@@ -36,10 +37,7 @@ enum UriMethod {
|
|
|
36
37
|
pay = 'pay',
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
// removes undefined parameters for serialization
|
|
40
|
-
export const stripUndefined = (obj: object) => JSON.parse(JSON.stringify(obj))
|
|
41
|
-
|
|
42
40
|
export const urlFromUriData = (data: Partial<UriData>, method: UriMethod = UriMethod.pay) => {
|
|
43
|
-
const params = new URLSearchParams(
|
|
41
|
+
const params = new URLSearchParams(sanitizeProperties(data))
|
|
44
42
|
return encodeURI(`${DEEP_LINK_URL_SCHEME}://wallet/${method.toString()}?${params.toString()}`)
|
|
45
43
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively strips values that are not useful to send through serialization
|
|
3
|
+
* boundaries (e.g. React Native native bridge, URL params, analytics payloads).
|
|
4
|
+
*
|
|
5
|
+
* Removes: `undefined`, `null`, `NaN`, `Infinity`
|
|
6
|
+
*/
|
|
7
|
+
export const sanitizeProperties = <T extends object>(obj: T): Partial<T> =>
|
|
8
|
+
JSON.parse(JSON.stringify(obj), (_key, value) => (value === null ? undefined : value))
|