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