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