Setting the file. One moment.
Telemetry Common · Expo Skill Feedback · expo/skills · Skills Docs
ContentsBack to the top of the page scripts/ telemetry_common.cjs
JavaScript · 171 lines · 7 KB
"https"
);
11 const os = require ( "os" );
12 const path = require ( "path" );
13
14 const POSTHOG_HOST = "https://us.i.posthog.com" ;
15
16 // PostHog project API key. This is a *write-only, public* ingestion key — the
17 // same kind embedded in browser snippets — so it is safe to commit. Override
18 // per environment with EXPO_SKILLS_POSTHOG_KEY (e.g. a staging project).
19 // NOTE: never put a PostHog *personal* API key (phx_...) here — those are secret.
20 const POSTHOG_PROJECT_API_KEY =
21 process.env. EXPO_SKILLS_POSTHOG_KEY || "phc_w8xRytdAAwkV3oExnuUozqH64PMzCmDLnyoChpPBcNXs" ;
22
23 const SOURCE = "expo-skills" ;
24 const INSTALLATION_ID_PATH = path. join (os. homedir (), ".expo-skills" , "installation-id" );
25
26 // Opt-in is the model: telemetry is OFF by default and only sends once the user
27 // explicitly enables it. This is the one product knob — flip to true for an opt-out
28 // (on-by-default) model instead.
29 const DEFAULT_ON = false ;
30
31 // Persistent opt-in marker. Its presence turns telemetry on across sessions, regardless
32 // of how the agent was launched (env vars don't always reach hook subprocesses).
33 // Written / removed by scripts/telemetry.cjs --on / --off.
34 const OPT_IN_PATH = path. join (os. homedir (), ".expo-skills" , "opt-in" );
35
36 // CI detection — never emit from automated environments so usage data reflects real
37 // humans. Honors the common CI=true convention plus major providers' signals.
38 function isCI () {
39 const ci = String (process.env. CI || "" ). trim (). toLowerCase ();
40 if (ci && ci !== "0" && ci !== "false" ) return true ;
41 return Boolean (
42 process.env. GITHUB_ACTIONS ||
43 process.env. GITLAB_CI ||
44 process.env. CIRCLECI ||
45 process.env. TRAVIS ||
46 process.env. BUILDKITE ||
47 process.env. JENKINS_URL ||
48 process.env. TEAMCITY_VERSION ||
49 process.env. TF_BUILD
50 );
51 }
52
53 // Explicit on/off intent from env vars: returns "on" | "off" | null.
54 // DO_NOT_TRACK=1 -> off (https://consoledonottrack.com)
55 // EXPO_SKILLS_TELEMETRY=0/off/false/no -> off
56 // EXPO_SKILLS_TELEMETRY=1/on/true/yes -> on
57 function telemetryEnvSignal () {
58 const dnt = String (process.env. DO_NOT_TRACK || "" ). trim (). toLowerCase ();
59 if (dnt && dnt !== "0" && dnt !== "false" ) return "off" ;
60 const flag = String (process.env. EXPO_SKILLS_TELEMETRY || "" ). trim (). toLowerCase ();
61 if ([ "0" , "false" , "off" , "no" ]. includes (flag)) return "off" ;
62 if ([ "1" , "true" , "on" , "yes" ]. includes (flag)) return "on" ;
63 return null ;
64 }
65
66 // Master gate. Nothing is sent unless this returns true. Precedence (highest first):
67 // 1. explicit env off (DO_NOT_TRACK / EXPO_SKILLS_TELEMETRY=0) -> off
68 // 2. CI -> off (never emit from bots)
69 // 3. explicit env on (EXPO_SKILLS_TELEMETRY=1) -> on
70 // 4. persistent opt-in marker (telemetry.cjs --on) -> on
71 // 5. default -> DEFAULT_ON (off = opt-in)
72 // So explicit-off and CI always win; otherwise it's on only when explicitly enabled.
73 function telemetryActive () {
74 const env = telemetryEnvSignal ();
75 if (env === "off" ) return false ;
76 if ( isCI ()) return false ;
77 if (env === "on" ) return true ;
78 try { if (fs. existsSync ( OPT_IN_PATH )) return true ; } catch {}
79 return DEFAULT_ON ;
80 }
81
82 // A real key ships in this file, so this check passes by default. This guard only
83 // makes the scripts inert if someone strips the key (e.g. a fork or private build).
84 function telemetryConfigured () {
85 const key = String ( POSTHOG_PROJECT_API_KEY || "" ). trim ();
86 return key. length > 0 && key !== "phc_REPLACE_ME" ;
87 }
88
89 // Best-effort agent-harness label for the event (default when --agent-harness isn't passed).
90 function detectHarness () {
91 if (process.env. CLAUDECODE ) return "claude-code" ;
92 if (process.env. CODEX_SANDBOX || process.env. CODEX_SANDBOX_NETWORK_DISABLED ||
93 String (process.env. AGENT || "" ). toLowerCase () === "codex" ) return "codex" ;
94 return "unknown" ;
95 }
96
97 // Friendly OS + CPU arch for event properties (non-PII).
98 function platformProps () {
99 const osName = { darwin: "macos" , win32: "windows" }[process.platform] || process.platform;
100 return { os: osName, arch: process.arch };
101 }
102
103 // Random, anonymous, per-install id — created once at 0600, only its hash is ever sent.
104 function readInstallationId ( create = true ) {
105 try {
106 if (fs. existsSync ( INSTALLATION_ID_PATH )) {
107 const existing = fs. readFileSync ( INSTALLATION_ID_PATH , "utf8" ). trim ();
108 if (existing) return existing;
109 }
110 if ( ! create) return null ;
111 fs. mkdirSync (path. dirname ( INSTALLATION_ID_PATH ), { recursive: true , mode: 0o700 });
112 try { fs. chmodSync (path. dirname ( INSTALLATION_ID_PATH ), 0o700 ); } catch {}
113 const installationId = crypto. randomUUID (). replace ( /-/ g , "" );
114 try {
115 // 'wx' = O_CREAT | O_EXCL | O_WRONLY — atomic create, fails if it exists.
116 const fd = fs. openSync ( INSTALLATION_ID_PATH , "wx" , 0o600 );
117 try { fs. writeFileSync (fd, installationId + " \n " ); } finally { fs. closeSync (fd); }
118 return installationId;
119 } catch (err) {
120 if (err && err.code === "EEXIST" ) return fs. readFileSync ( INSTALLATION_ID_PATH , "utf8" ). trim () || null ;
121 throw err;
122 }
123 } catch {
124 return null ;
125 }
126 }
127
128 function telemetryIdentity ( agentHarness , { createInstallation = true } = {}) {
129 const id = readInstallationId (createInstallation);
130 const installHash = id ? crypto. createHash ( "sha256" ). update (id). digest ( "hex" ). slice ( 0 , 32 ) : null ;
131 if (installHash) return [ `expo-skills-installation:${ installHash }` , { installation_id_hash: installHash }];
132 return [ `expo-skills-events:${ agentHarness }` , {}];
133 }
134
135 function sendToPosthog ( payload , { userAgent , timeoutMs }) {
136 return new Promise (( resolve , reject ) => {
137 const url = new URL ( "/i/v0/e/" , POSTHOG_HOST );
138 const body = Buffer. from ( JSON . stringify (payload), "utf8" );
139 const req = https. request (url, {
140 method: "POST" ,
141 headers: { "Content-Type" : "application/json" , "Content-Length" : body. length , "User-Agent" : userAgent },
142 timeout: timeoutMs,
143 }, ( res ) => {
144 const chunks = [];
145 res. on ( "data" , ( c ) => chunks. push (c));
146 res. on ( "end" , () => {
147 const status = res.statusCode || 0 ;
148 if (status >= 200 && status < 300 ) return resolve ();
149 reject ( new Error ( `HTTP ${ status } ${ Buffer . concat ( chunks ). toString ( "utf8" ) }` ));
150 });
151 });
152 req. on ( "timeout" , () => req. destroy ( new Error ( "request timed out" )));
153 req. on ( "error" , reject);
154 req. write (body);
155 req. end ();
156 });
157 }
158
159 module . exports = {
160 POSTHOG_PROJECT_API_KEY,
161 SOURCE,
162 OPT_IN_PATH,
163 telemetryActive,
164 telemetryEnvSignal,
165 telemetryConfigured,
166 detectHarness,
167 isCI,
168 platformProps,
169 telemetryIdentity,
170 sendToPosthog,
171 };