1import RudderAnalytics from '@expo/rudder-sdk-node'; 2import * as ciInfo from 'ci-info'; 3import os from 'os'; 4 5import UserSettings from '../../api/user/UserSettings'; 6import { getUserAsync } from '../../api/user/user'; 7import { env } from '../env'; 8 9const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = { 10 darwin: 'Mac', 11 win32: 'Windows', 12 linux: 'Linux', 13}; 14 15let client: RudderAnalytics | null = null; 16let identified = false; 17let identifyData: { 18 userId: string; 19 deviceId: string; 20 traits: Record<string, any>; 21} | null = null; 22 23export function resetInternalStateForTesting() { 24 identified = false; 25 identifyData = null; 26 client = null; 27} 28 29export function getRudderAnalyticsClient(): RudderAnalytics { 30 if (client) { 31 return client; 32 } 33 34 client = new RudderAnalytics( 35 env.EXPO_STAGING || env.EXPO_LOCAL 36 ? '24TKICqYKilXM480mA7ktgVDdea' 37 : '24TKR7CQAaGgIrLTgu3Fp4OdOkI', // expo unified 38 'https://cdp.expo.dev/v1/batch', 39 { 40 flushInterval: 300, 41 } 42 ); 43 44 // Install flush on exit... 45 process.on('SIGINT', () => client?.flush?.()); 46 process.on('SIGTERM', () => client?.flush?.()); 47 48 return client; 49} 50 51export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> { 52 if (env.EXPO_NO_TELEMETRY) { 53 return; 54 } 55 56 const deviceId = await UserSettings.getAnonymousIdentifierAsync(); 57 58 identifyData = { 59 userId, 60 deviceId, 61 traits, 62 }; 63 64 identifyIfNotYetIdentified(); 65} 66 67type Event = 68 | 'action' 69 | 'Open Url on Device' 70 | 'Start Project' 71 | 'Serve Manifest' 72 | 'Serve Expo Updates Manifest' 73 | 'dev client start command' 74 | 'dev client run command' 75 | 'metro config' 76 | 'metro debug'; 77 78/** 79 * Log an event, ensuring the user is identified before logging the event. 80 **/ 81export async function logEventAsync( 82 event: Event, 83 properties: Record<string, any> = {} 84): Promise<void> { 85 if (env.EXPO_NO_TELEMETRY) { 86 return; 87 } 88 89 // this has the side effect of calling `setUserData` which fetches the user and populates identifyData 90 try { 91 await getUserAsync(); 92 } catch {} 93 94 identifyIfNotYetIdentified(); 95 96 if (!identifyData) { 97 return; 98 } 99 const { userId, deviceId } = identifyData; 100 const commonEventProperties = { source_version: process.env.__EXPO_VERSION, source: 'expo' }; 101 102 const identity = { userId, anonymousId: deviceId }; 103 getRudderAnalyticsClient().track({ 104 event, 105 properties: { ...properties, ...commonEventProperties }, 106 ...identity, 107 context: getContext(), 108 }); 109} 110 111function identifyIfNotYetIdentified(): void { 112 if (env.EXPO_NO_TELEMETRY || identified || !identifyData) { 113 return; 114 } 115 116 getRudderAnalyticsClient().identify({ 117 userId: identifyData.userId, 118 anonymousId: identifyData.deviceId, 119 traits: identifyData.traits, 120 }); 121 identified = true; 122} 123 124/** Exposed for testing only */ 125export function getContext(): Record<string, any> { 126 const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform(); 127 return { 128 os: { name: platform, version: os.release() }, 129 device: { type: platform, model: platform }, 130 app: { name: 'expo', version: process.env.__EXPO_VERSION }, 131 ci: ciInfo.isCI ? { name: ciInfo.name, isPr: ciInfo.isPR } : undefined, 132 }; 133} 134