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 76/** 77 * Log an event, ensuring the user is identified before logging the event. 78 **/ 79export async function logEventAsync( 80 event: Event, 81 properties: Record<string, any> = {} 82): Promise<void> { 83 if (env.EXPO_NO_TELEMETRY) { 84 return; 85 } 86 87 // this has the side effect of calling `setUserData` which fetches the user and populates identifyData 88 try { 89 await getUserAsync(); 90 } catch {} 91 92 identifyIfNotYetIdentified(); 93 94 if (!identifyData) { 95 return; 96 } 97 const { userId, deviceId } = identifyData; 98 const commonEventProperties = { source_version: process.env.__EXPO_VERSION, source: 'expo' }; 99 100 const identity = { userId, anonymousId: deviceId }; 101 getRudderAnalyticsClient().track({ 102 event, 103 properties: { ...properties, ...commonEventProperties }, 104 ...identity, 105 context: getContext(), 106 }); 107} 108 109function identifyIfNotYetIdentified(): void { 110 if (env.EXPO_NO_TELEMETRY || identified || !identifyData) { 111 return; 112 } 113 114 getRudderAnalyticsClient().identify({ 115 userId: identifyData.userId, 116 anonymousId: identifyData.deviceId, 117 traits: identifyData.traits, 118 }); 119 identified = true; 120} 121 122/** Exposed for testing only */ 123export function getContext(): Record<string, any> { 124 const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform(); 125 return { 126 os: { name: platform, version: os.release() }, 127 device: { type: platform, model: platform }, 128 app: { name: 'expo', version: process.env.__EXPO_VERSION }, 129 ci: ciInfo.isCI ? { name: ciInfo.name, isPr: ciInfo.isPR } : undefined, 130 }; 131} 132