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