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