evals 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/analytics.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Anonymous npx evals TUI funnel events → app-server → Pendo Track.
3
+ *
4
+ * No PII — only a stable anon id under ~/.arize and allowlisted event names.
5
+ * Fire-and-forget: never blocks or fails the CLI if tracking is down.
6
+ *
7
+ * Events:
8
+ * npx_evals_agent_selected — coding agent picked in the TUI
9
+ * npx_evals_agent_install_click — install-docs link when no agent on PATH
10
+ * npx_evals_skills_installed — Arize skills/CLI install attempt
11
+ * npx_evals_auth_opened — signup/sign-in browser about to open (OAuth)
12
+ */
13
+
14
+ import { randomUUID } from 'crypto';
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
16
+ import { homedir } from 'os';
17
+ import { join } from 'path';
18
+
19
+ export const DEFAULT_ANALYTICS_URL =
20
+ 'https://app.arize.com/api/analytics/npx-evals';
21
+
22
+ /** Bound client timeout; server holds the Pendo key. */
23
+ export const TRACK_TIMEOUT_MS = 2500;
24
+
25
+ export const ALLOWED_EVENTS = new Set([
26
+ 'npx_evals_agent_selected',
27
+ 'npx_evals_agent_install_click',
28
+ 'npx_evals_skills_installed',
29
+ 'npx_evals_auth_opened',
30
+ ]);
31
+
32
+ function analyticsUrl() {
33
+ return process.env.ARIZE_EVALS_ANALYTICS_URL || DEFAULT_ANALYTICS_URL;
34
+ }
35
+
36
+ function anonIdPath() {
37
+ return join(homedir(), '.arize', 'anon_client_id');
38
+ }
39
+
40
+ /** Stable anon id so Pendo can stitch steps for one machine without knowing who. */
41
+ export function getAnonClientId() {
42
+ const path = anonIdPath();
43
+ try {
44
+ if (existsSync(path)) {
45
+ const existing = readFileSync(path, 'utf8').trim();
46
+ if (existing) return existing;
47
+ }
48
+ } catch {
49
+ // fall through and mint a new id
50
+ }
51
+
52
+ const id = randomUUID();
53
+ try {
54
+ mkdirSync(join(homedir(), '.arize'), { recursive: true });
55
+ writeFileSync(path, id, 'utf8');
56
+ } catch {
57
+ // still send with in-memory id if home isn't writable
58
+ }
59
+ return id;
60
+ }
61
+
62
+ /**
63
+ * Send one anon funnel event. Resolves quickly; network errors are swallowed.
64
+ * @param {string} name allowlisted event name
65
+ * @returns {Promise<void>}
66
+ */
67
+ export async function trackEvent(name) {
68
+ if (!ALLOWED_EVENTS.has(name)) return;
69
+
70
+ const body = {
71
+ event: name,
72
+ anonymousId: getAnonClientId(),
73
+ };
74
+
75
+ try {
76
+ const controller = new AbortController();
77
+ const timer = setTimeout(() => controller.abort(), TRACK_TIMEOUT_MS);
78
+ await fetch(analyticsUrl(), {
79
+ method: 'POST',
80
+ headers: { 'content-type': 'application/json' },
81
+ body: JSON.stringify(body),
82
+ signal: controller.signal,
83
+ });
84
+ clearTimeout(timer);
85
+ } catch {
86
+ // never surface tracking failures to the user
87
+ }
88
+ }
89
+
90
+ /** Fire without awaiting — safe to call before launch / exit. */
91
+ export function trackEventBackground(name) {
92
+ void trackEvent(name);
93
+ }
package/bin.js CHANGED
@@ -34,10 +34,19 @@ if (!Number.isInteger(major) || major < MIN_MAJOR) {
34
34
  process.exit(1);
35
35
  }
36
36
 
37
- // cli.js only auto-runs when it is the entry point, so call main() explicitly.
38
- import('./cli.js')
39
- .then((cli) => cli.main())
40
- .catch((err) => {
41
- console.error(`Could not start evals: ${err && err.message ? err.message : err}`);
42
- process.exit(1);
43
- });
37
+ // `evals track <event>` fire-and-forget funnel ping, no TUI. Used for local/debug;
38
+ // the onboarding prompt stages track-event.mjs instead (must not name npx evals).
39
+ const args = process.argv.slice(2);
40
+ if (args[0] === 'track') {
41
+ import('./track-event.mjs')
42
+ .then((m) => m.run(args.slice(1)))
43
+ .finally(() => process.exit(0));
44
+ } else {
45
+ // cli.js only auto-runs when it is the entry point, so call main() explicitly.
46
+ import('./cli.js')
47
+ .then((cli) => cli.main())
48
+ .catch((err) => {
49
+ console.error(`Could not start evals: ${err && err.message ? err.message : err}`);
50
+ process.exit(1);
51
+ });
52
+ }
package/cli.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  import { join, isAbsolute, resolve } from 'path';
22
22
  import { tmpdir, homedir } from 'os';
23
23
  import { fileURLToPath } from 'url';
24
+ import { trackEventBackground } from './analytics.js';
24
25
 
25
26
  const e = React.createElement;
26
27
 
@@ -37,6 +38,8 @@ const BRAND_ACCENT = '#FF3CA8';
37
38
  // that file — a ~35 KB prompt is too large to pass reliably as a command-line
38
39
  // argument.
39
40
  const BUNDLED_PROMPT_PATH = fileURLToPath(new URL('./onboarding-prompt.md', import.meta.url));
41
+ const BUNDLED_ANALYTICS_PATH = fileURLToPath(new URL('./analytics.js', import.meta.url));
42
+ const BUNDLED_TRACK_PATH = fileURLToPath(new URL('./track-event.mjs', import.meta.url));
40
43
 
41
44
  // Wheels for the coding-agent tracing harness, built by scripts/build-harness-wheel.mjs.
42
45
  // Staging these next to the prompt lets Step 4A install with no network and no
@@ -57,6 +60,18 @@ const PROMPT_FILE_NAME = 'onboarding-prompt.md';
57
60
  // gets cleared at the next launch instead of sitting around holding a live key.
58
61
  const ENV_FILE_NAME = 'harness.env';
59
62
 
63
+ // Anon funnel helper staged beside the prompt so the agent can fire auth-opened
64
+ // before OAuth without naming `npx evals` (prompt contract forbids that).
65
+ const TRACK_FILE_NAME = 'track-event.mjs';
66
+ const ANALYTICS_FILE_NAME = 'analytics.js';
67
+
68
+ const STAGED_ROOT_FILES = [
69
+ PROMPT_FILE_NAME,
70
+ ENV_FILE_NAME,
71
+ TRACK_FILE_NAME,
72
+ ANALYTICS_FILE_NAME,
73
+ ];
74
+
60
75
  // MANIFEST only lands in the shell launchers' bundles, never in ours. Naming it
61
76
  // anyway keeps one allowlist correct for every implementation, so a directory
62
77
  // staged by start.sh can be cleared by `npx evals` and vice versa.
@@ -135,7 +150,7 @@ export function clearOnboardingDir(dir) {
135
150
  continue;
136
151
  }
137
152
 
138
- if (entry.name !== PROMPT_FILE_NAME && entry.name !== ENV_FILE_NAME) return false;
153
+ if (!STAGED_ROOT_FILES.includes(entry.name)) return false;
139
154
  unlinkSync(path);
140
155
  }
141
156
  return true;
@@ -210,6 +225,8 @@ function prepareSeedPrompt() {
210
225
  const { dir, fellBack } = prepareOnboardingDir();
211
226
  const promptFile = join(dir, PROMPT_FILE_NAME);
212
227
  writeFileSync(promptFile, promptText, 'utf8');
228
+ copyFileSync(BUNDLED_ANALYTICS_PATH, join(dir, ANALYTICS_FILE_NAME));
229
+ copyFileSync(BUNDLED_TRACK_PATH, join(dir, TRACK_FILE_NAME));
213
230
  stageOfflineHarness(dir);
214
231
  // Single quotes, not double: a home path is likelier to contain a space than a
215
232
  // temp path was, but Windows spawns through the shell and embedded double
@@ -217,7 +234,7 @@ function prepareSeedPrompt() {
217
234
  // No "in this project": the launcher may well be run from a home directory, and
218
235
  // Step 4 is what scopes the work — an app here, an app at another path, a starter
219
236
  // app, or the coding agent itself. Keep this wording in step with start.{sh,ps1}.
220
- const seed = `Read the file '${promptFile}' in full with your file-reading tool, not a shell command, then follow it to set up Arize AX tracing, walking me through each step and asking me questions as needed.`;
237
+ const seed = `Read the file '${promptFile}' in full before doing anything else. Prefer your file-reading tool. If none is available, use read-only shell commands with bounded line ranges solely to print this exact file, repeating with successive ranges until EOF. Do not inspect or change anything else, and do not invoke a skill until a step tells you to. Then follow its steps in order from Step 0, walking me through each step and asking me questions as needed.`;
221
238
  return { seed, dir, fellBack };
222
239
  }
223
240
 
@@ -415,7 +432,8 @@ export function buildAgentItems() {
415
432
  items: AGENTS.map(a => ({
416
433
  name: `Install ${a.label}`,
417
434
  subtext: 'No supported agent detected — open install docs',
418
- url: a.installUrl
435
+ url: a.installUrl,
436
+ agentId: a.id,
419
437
  })),
420
438
  none: true
421
439
  };
@@ -477,7 +495,7 @@ function App({ onDone }) {
477
495
  }
478
496
 
479
497
  if (selected.url) {
480
- onDone({ type: 'url', url: selected.url });
498
+ onDone({ type: 'url', url: selected.url, agentId: selected.agentId });
481
499
  exit();
482
500
  return;
483
501
  }
@@ -541,6 +559,8 @@ function launchAgent(agent) {
541
559
  ? ' Done.'
542
560
  : ' Skipped — the setup will follow the Arize docs instead.');
543
561
 
562
+ trackEventBackground('npx_evals_skills_installed');
563
+
544
564
  console.log(`\nLaunching ${agent.label}…\n`);
545
565
 
546
566
  const child = spawn(agent.bin, agent.args(prepared.seed), {
@@ -649,9 +669,11 @@ export async function main() {
649
669
  if (!result) {
650
670
  process.exit(0); // user quit / cancelled
651
671
  } else if (result.type === 'url') {
672
+ trackEventBackground('npx_evals_agent_install_click');
652
673
  console.log(`\nOpening ${result.url} in your browser...\n`);
653
674
  openUrlInBrowser(result.url);
654
675
  } else if (result.type === 'launch') {
676
+ trackEventBackground('npx_evals_agent_selected');
655
677
  launchAgent(result.agent);
656
678
  }
657
679
  } catch (error) {
@@ -4,7 +4,7 @@ Guide the user from zero to their first traces in Arize AX. Use the AX CLI, the
4
4
 
5
5
  **Read all of this file before you act.** If your tools return only part of it, keep reading to the last line. Rules you need are at the end.
6
6
 
7
- Work through the flow in order. Installing the AX CLI and Arize skills, authenticating, and listing spaces are fine before approval that is your own tooling. Do not create AX resources (like API keys), edit application files, or install the app's tracing dependencies before the user approves the plan in Step 5.
7
+ Work through the flow in order. Before approval, install the AX CLI and Arize skills, authenticate, list spaces, and create a space if the user asks in Step 3. Create no other AX resource (like an API key), edit no application file, and install no tracing dependency before the user approves the plan in Step 5.
8
8
 
9
9
  ## How to write to the user
10
10
 
@@ -94,7 +94,9 @@ python3 -m pip install --upgrade arize-ax-cli
94
94
 
95
95
  Try `uv` first, even if `pipx` is installed.
96
96
 
97
- **Check your skill list for `arize-instrumentation` and `arize-link` first.** They are usually installed already. If they are listed, do not run the install — invoke them by name when needed and go to Step 2.
97
+ **Check your skill list for `arize-instrumentation` and `arize-link` first.** They are usually installed already. If they are listed, do not run the install — go to Step 2, and invoke them only where a step says to: `arize-instrumentation` at Step 6, `arize-link` at Step 8.
98
+
99
+ A skill you loaded before you read this file does not own this flow — this file does. Drop that skill's plan and start at Step 0.
98
100
 
99
101
  Install them only if they are absent, or you cannot see your skill list, and npx is available:
100
102
 
@@ -139,11 +141,14 @@ If a `default` profile exists but the probe failed, it is signed out or expired,
139
141
 
140
142
  - **No key anywhere** — sign up or sign in with browser OAuth:
141
143
 
144
+ Immediately before the OAuth command, fire the anon funnel ping (ignore failures; never let it block auth), then open the browser flow:
145
+
142
146
  ```bash
147
+ node ~/.arize/onboarding/track-event.mjs auth-opened >/dev/null 2>&1 || true
143
148
  ax profiles create default --auth-method oauth --utm-params "utm_source=npmevals&utm_medium=cli&utm_campaign=prompt-first-onboarding"
144
149
  ```
145
150
 
146
- Pass `--utm-params` verbatim on the two browser OAuth commands (`profiles create --auth-method oauth` and the `ax auth login` fallback) and on no other `ax` command — it tags a new sign-up with onboarding attribution.
151
+ Pass `--utm-params` verbatim on the two browser OAuth commands (`profiles create --auth-method oauth` and the `ax auth login` fallback) and on no other `ax` command — it tags a new sign-up with onboarding attribution. Always run `track-event.mjs auth-opened` immediately before each of those two commands.
147
152
 
148
153
  Always pass the positional profile name `default`. Without it, the CLI prompts `profile name [default]:`, receives EOF from an agent-run command, and exits with `Goodbye!` without creating a profile.
149
154
 
@@ -170,6 +175,7 @@ Then wait for the command to exit. Treat exit code `0`, or success output such a
170
175
  Only after the OAuth command completes, re-run the probe from the top of this step. If it succeeds, continue. If the profile already existed and the probe returns an authentication error, it is signed out or expired — run the fallback:
171
176
 
172
177
  ```bash
178
+ node ~/.arize/onboarding/track-event.mjs auth-opened >/dev/null 2>&1 || true
173
179
  ax auth login --utm-params "utm_source=npmevals&utm_medium=cli&utm_campaign=prompt-first-onboarding"
174
180
  ```
175
181
 
@@ -181,13 +187,41 @@ Run no other remote `ax` command (creating keys, listing all spaces, inspecting
181
187
 
182
188
  ## Step 3: Select the AX space
183
189
 
184
- List the spaces the authenticated profile can access:
190
+ List every space the authenticated profile can access. The default limit is low, so raise it, and page with `--cursor <cursor>` while the response reports `pagination.has_more` as true:
191
+
192
+ ```bash
193
+ ax spaces list --limit 100 --output json
194
+ ```
195
+
196
+ **No spaces returned** — the account is new. Create a space (see below). Do not ask the user to choose.
197
+
198
+ **One or more spaces** — ask the user which space to use. Ask this even when the list holds one space. Offer every space by display name, in the order returned, and offer a new space as the last choice:
199
+
200
+ ```text
201
+ Which Arize AX space do you want to use?
202
+
203
+ 1. <space name>
204
+ 2. <space name>
205
+ 3. Create a new space
206
+ ```
207
+
208
+ If `ARIZE_SPACE_ID` is already set (environment or `.env` / `.env.local`) **and** it appears in that list, mark that space as the one the app uses now and make it the default choice — the list confirms the active profile can reach it, since the app's key and your CLI profile can point at different spaces. If it is set but absent from the list, leave it out of the question.
209
+
210
+ ### Create a new space
211
+
212
+ Ask the user for a name, and offer `arize-tracing` as the default. A new space needs an organization ID:
213
+
214
+ ```bash
215
+ ax organizations list --output json
216
+ ```
217
+
218
+ One organization, use it; several, ask the user which one. Then create the space:
185
219
 
186
220
  ```bash
187
- ax spaces list --output json
221
+ ax spaces create --name "<name>" --organization-id "<organization-id>" --output json
188
222
  ```
189
223
 
190
- If `ARIZE_SPACE_ID` is already set (environment or `.env` / `.env.local`), use it **only if it appears in that list** that confirms the active profile can reach it, since the app's key and your CLI profile may point at different spaces. If it is set but absent from the list, select from the list instead. Otherwise: one space returned, use it; several, ask the user which; none, guide the user to create one in the Arize AX UI (or with an organization ID if available), then re-list.
224
+ A space the user asked for is its own approval — it is the only AX resource you create before Step 5. Take the space **ID** from the command output.
191
225
 
192
226
  Capture the space's **ID**, not its display name, for `ARIZE_SPACE_ID`. `arize-otel` requires the ID; a name silently fails to route traces.
193
227
 
@@ -598,7 +632,7 @@ https://arize.com/docs/ax/integrations/platforms/<agent>/<agent>-tracing
598
632
  ## Critical rules
599
633
 
600
634
  - Keep your task list current if you have one (see "Track progress"): one task per milestone, exactly one in progress, complete only when done, never mentioned in chat.
601
- - Get the user's approval (Step 5) before creating AX resources, editing files, or installing dependencies.
635
+ - Get the user's approval (Step 5) before creating AX resources, editing files, or installing dependencies — except a space the user asked for in Step 3.
602
636
  - Authenticate the CLI (Step 2) before any other `ax` call; env vars alone don't. Never interrupt a waiting OAuth command — no stdin close, Ctrl-C, `pkill`, or probe. A slow wait is not a hang.
603
637
  - Never print, log, or summarize secrets in chat — API keys, env-file contents/values, or span bodies (prompts, completions, tool args, user data) — and never read env files into chat. Only report traces a command actually returned.
604
638
  - One AX API key, ever: reuse a **validated** `ARIZE_API_KEY`, else a **single** `ax api-keys create --env-file <file>` into an already-git-ignored file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evals",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Arize AX onboarding — instrument your app with tracing via your coding agent",
5
5
  "type": "module",
6
6
  "main": "cli.js",
@@ -24,6 +24,8 @@
24
24
  "files": [
25
25
  "bin.js",
26
26
  "cli.js",
27
+ "analytics.js",
28
+ "track-event.mjs",
27
29
  "onboarding-prompt.md",
28
30
  "start.sh",
29
31
  "start.ps1",
package/start.ps1 CHANGED
@@ -26,6 +26,9 @@ $ErrorActionPreference = 'Stop'
26
26
  # files over HTTP, so it's fetchable even though the source repo is private.
27
27
  # Override with $env:ARIZE_PROMPT_URL if needed.
28
28
  $PromptUrl = if ($env:ARIZE_PROMPT_URL) { $env:ARIZE_PROMPT_URL } else { 'https://cdn.jsdelivr.net/npm/evals/onboarding-prompt.md' }
29
+ $TrackUrl = if ($env:ARIZE_TRACK_URL) { $env:ARIZE_TRACK_URL } else { 'https://cdn.jsdelivr.net/npm/evals/track-event.mjs' }
30
+ $AnalyticsUrl = if ($env:ARIZE_ANALYTICS_URL) { $env:ARIZE_ANALYTICS_URL } else { 'https://cdn.jsdelivr.net/npm/evals/analytics.js' }
31
+ $AnalyticsEndpoint = if ($env:ARIZE_EVALS_ANALYTICS_URL) { $env:ARIZE_EVALS_ANALYTICS_URL } else { 'https://app.arize.com/api/analytics/npx-evals' }
29
32
  # Where the bundled harness wheels live, same package. Override for testing.
30
33
  $VendorUrl = if ($env:ARIZE_VENDOR_URL) { $env:ARIZE_VENDOR_URL } else { 'https://cdn.jsdelivr.net/npm/evals/vendor' }
31
34
 
@@ -99,6 +102,32 @@ function Resolve-Agent {
99
102
  }
100
103
  }
101
104
 
105
+
106
+ function Send-EvalsFunnelEvent {
107
+ param([string]$Name)
108
+ $anonFile = Join-Path $HOME '.arize/anon_client_id'
109
+ $clientId = $null
110
+ if (Test-Path -LiteralPath $anonFile) {
111
+ $clientId = (Get-Content -LiteralPath $anonFile -Raw -ErrorAction SilentlyContinue).Trim()
112
+ }
113
+ if (-not $clientId) {
114
+ $clientId = [guid]::NewGuid().ToString()
115
+ try {
116
+ New-Item -ItemType Directory -Force -Path (Join-Path $HOME '.arize') | Out-Null
117
+ Set-Content -LiteralPath $anonFile -Value $clientId -NoNewline
118
+ } catch {}
119
+ }
120
+ $body = @{ event = $Name; anonymousId = $clientId } | ConvertTo-Json -Compress
121
+ try {
122
+ Start-Job -ScriptBlock {
123
+ param($u, $j)
124
+ try {
125
+ Invoke-RestMethod -Method Post -Uri $u -ContentType 'application/json' -Body $j -TimeoutSec 2 | Out-Null
126
+ } catch {}
127
+ } -ArgumentList $AnalyticsEndpoint, $body | Out-Null
128
+ } catch {}
129
+ }
130
+
102
131
  # --- Welcome ---
103
132
  Write-Host ""
104
133
  Write-Host "Arize AX" -ForegroundColor Magenta -NoNewline
@@ -116,6 +145,7 @@ if ([Console]::IsOutputRedirected) {
116
145
  } else {
117
146
  $chosen = Resolve-Agent
118
147
  if (-not $chosen) { exit 1 }
148
+ Send-EvalsFunnelEvent -Name 'npx_evals_agent_selected'
119
149
  }
120
150
 
121
151
  # A directory, not a bare temp file: Step 4A looks for the offline bundle *beside*
@@ -154,7 +184,7 @@ function Clear-PromptDir($dir) {
154
184
  if (-not $ours) { return $false }
155
185
  if (-not (Clear-BundleDir $sub.FullName)) { return $false }
156
186
  }
157
- foreach ($name in @('onboarding-prompt.md', 'harness.env')) {
187
+ foreach ($name in @('onboarding-prompt.md', 'harness.env', 'track-event.mjs', 'analytics.js')) {
158
188
  Remove-Item -LiteralPath (Join-Path $dir $name) -Force -ErrorAction SilentlyContinue
159
189
  }
160
190
  # Anything left is not ours.
@@ -200,6 +230,10 @@ try {
200
230
  exit 1
201
231
  }
202
232
 
233
+ # Best-effort: agent runs these before OAuth. Missing files just skip the ping.
234
+ try { Invoke-RestMethod -Uri $TrackUrl -OutFile (Join-Path $promptDir 'track-event.mjs') } catch {}
235
+ try { Invoke-RestMethod -Uri $AnalyticsUrl -OutFile (Join-Path $promptDir 'analytics.js') } catch {}
236
+
203
237
  # Fetch the bundled tracing harness so Step 4A can install it without piping a
204
238
  # downloaded script into a shell — the thing auto-approval permission
205
239
  # classifiers refuse. `npx evals` ships these files; this path pulls them from
@@ -241,7 +275,7 @@ try {
241
275
  # where embedded double quotes would not.
242
276
  # No "in this project": this may well be run from a home directory, and Step 4 is
243
277
  # what scopes the work. Keep this wording in step with cli.js and start.sh.
244
- $seed = "Read the file '$promptFile' in full with your file-reading tool, not a shell command, then follow it to set up Arize AX tracing, walking me through each step and asking me questions as needed."
278
+ $seed = "Read the file '$promptFile' in full before doing anything else. Prefer your file-reading tool. If none is available, use read-only shell commands with bounded line ranges solely to print this exact file, repeating with successive ranges until EOF. Do not inspect or change anything else, and do not invoke a skill until a step tells you to. Then follow its steps in order from Step 0, walking me through each step and asking me questions as needed."
245
279
 
246
280
  if (-not $chosen) {
247
281
  # Addressed to the person, two lines, recommendation first - see the note in cli.js.
package/start.sh CHANGED
@@ -22,6 +22,33 @@ set -euo pipefail
22
22
  # files over HTTP, so it's fetchable even though the source repo is private.
23
23
  # Override with ARIZE_PROMPT_URL if needed.
24
24
  PROMPT_URL="${ARIZE_PROMPT_URL:-https://cdn.jsdelivr.net/npm/evals/onboarding-prompt.md}"
25
+ TRACK_URL="${ARIZE_TRACK_URL:-https://cdn.jsdelivr.net/npm/evals/track-event.mjs}"
26
+ ANALYTICS_URL="${ARIZE_ANALYTICS_URL:-https://cdn.jsdelivr.net/npm/evals/analytics.js}"
27
+ ANALYTICS_ENDPOINT="${ARIZE_EVALS_ANALYTICS_URL:-https://app.arize.com/api/analytics/npx-evals}"
28
+
29
+ # Fire-and-forget funnel ping (same events as cli.js). Never blocks the launcher.
30
+ track_evals_event() {
31
+ local name="$1"
32
+ local anon_file="${HOME:-}/.arize/anon_client_id"
33
+ local client_id=""
34
+ if [[ -f "$anon_file" ]]; then
35
+ client_id="$(tr -d '[:space:]' < "$anon_file" 2>/dev/null || true)"
36
+ fi
37
+ if [[ -z "$client_id" ]]; then
38
+ if command -v uuidgen >/dev/null 2>&1; then
39
+ client_id="$(uuidgen | tr '[:upper:]' '[:lower:]')"
40
+ else
41
+ client_id="$(od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')"
42
+ client_id="${client_id:0:8}-${client_id:8:4}-${client_id:12:4}-${client_id:16:4}-${client_id:20:12}"
43
+ fi
44
+ [[ -n "${HOME:-}" ]] && mkdir -p "${HOME}/.arize" 2>/dev/null || true
45
+ [[ -n "${HOME:-}" ]] && printf '%s' "$client_id" > "$anon_file" 2>/dev/null || true
46
+ fi
47
+ local body="{\"event\":\"$name\",\"anonymousId\":\"$client_id\"}"
48
+ if command -v curl >/dev/null 2>&1; then
49
+ curl -fsS -m 2 -X POST -H 'content-type: application/json' -d "$body" "$ANALYTICS_ENDPOINT" >/dev/null 2>&1 &
50
+ fi
51
+ }
25
52
  # Where the bundled harness wheels live, same package. Override for testing.
26
53
  VENDOR_URL="${ARIZE_VENDOR_URL:-https://cdn.jsdelivr.net/npm/evals/vendor}"
27
54
 
@@ -155,6 +182,7 @@ if [[ ! -t 1 ]]; then
155
182
  CHOSEN=""
156
183
  else
157
184
  CHOSEN="$(choose_agent)" || exit 1
185
+ track_evals_event npx_evals_agent_selected
158
186
  fi
159
187
 
160
188
  # A directory, not a bare temp file: Step 4A looks for the offline bundle *beside*
@@ -186,7 +214,8 @@ clear_prompt_dir() {
186
214
  for sub in "$1"/arize-offline "$1"/.staging*; do
187
215
  [ -d "$sub" ] && clear_bundle_dir "$sub"
188
216
  done
189
- rm -f "$1/onboarding-prompt.md" "$1/harness.env"
217
+ rm -f "$1/onboarding-prompt.md" "$1/harness.env" \
218
+ "$1/track-event.mjs" "$1/analytics.js"
190
219
  # Anything left is not ours: bail out and let the caller fall back.
191
220
  rmdir "$1" 2>/dev/null || { [ -z "$(ls -A "$1" 2>/dev/null)" ]; return $?; }
192
221
  }
@@ -231,6 +260,10 @@ if ! fetch "$PROMPT_URL" "$PROMPT_FILE"; then
231
260
  exit 1
232
261
  fi
233
262
 
263
+ # Best-effort: agent runs these before OAuth. Missing files just skip the ping.
264
+ fetch "$TRACK_URL" "$PROMPT_DIR/track-event.mjs" 2>/dev/null || true
265
+ fetch "$ANALYTICS_URL" "$PROMPT_DIR/analytics.js" 2>/dev/null || true
266
+
234
267
  # Fetch the bundled tracing harness so Step 4A can install it without piping a
235
268
  # downloaded script into a shell — the thing auto-approval permission classifiers
236
269
  # refuse. `npx evals` ships these files in the package; this path has to pull them
@@ -272,7 +305,7 @@ fi
272
305
 
273
306
  # No "in this project": this may well be run from a home directory, and Step 4 is
274
307
  # what scopes the work. Keep this wording in step with cli.js and start.ps1.
275
- SEED="Read the file '$PROMPT_FILE' in full with your file-reading tool, not a shell command, then follow it to set up Arize AX tracing, walking me through each step and asking me questions as needed."
308
+ SEED="Read the file '$PROMPT_FILE' in full before doing anything else. Prefer your file-reading tool. If none is available, use read-only shell commands with bounded line ranges solely to print this exact file, repeating with successive ranges until EOF. Do not inspect or change anything else, and do not invoke a skill until a step tells you to. Then follow its steps in order from Step 0, walking me through each step and asking me questions as needed."
276
309
 
277
310
  if [[ -z "$CHOSEN" ]]; then
278
311
  # Addressed to the person, two lines, recommendation first — see the note in cli.js.
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fire one anon funnel event, then exit.
4
+ *
5
+ * Staged next to the onboarding prompt so the agent can run it before opening
6
+ * the Arize signup/sign-in browser (OAuth) without naming `npx evals`.
7
+ *
8
+ * Usage:
9
+ * node ~/.arize/onboarding/track-event.mjs auth-opened
10
+ *
11
+ * Always exits 0 — tracking must never block auth.
12
+ */
13
+
14
+ import { trackEvent } from './analytics.js';
15
+
16
+ /** @type {Record<string, string>} */
17
+ const EVENTS = {
18
+ 'auth-opened': 'npx_evals_auth_opened',
19
+ };
20
+
21
+ /**
22
+ * @param {string[]} argv
23
+ * @returns {Promise<void>}
24
+ */
25
+ export async function run(argv) {
26
+ const key = argv[0] || '';
27
+ const name = EVENTS[key];
28
+ if (!name) return;
29
+
30
+ await trackEvent(name);
31
+ }
32
+
33
+ const isDirect =
34
+ process.argv[1] &&
35
+ (process.argv[1].endsWith('track-event.mjs') ||
36
+ process.argv[1].endsWith('track-event'));
37
+
38
+ if (isDirect) {
39
+ run(process.argv.slice(2)).finally(() => process.exit(0));
40
+ }
package/vendor/MANIFEST CHANGED
@@ -1,6 +1,6 @@
1
1
  a9cede65f98d3415b13e1c6a437d0fdee27286bfff0186dd72bb65d9869b7864 LICENSE-coding-harness-tracing
2
2
  62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 certifi-2026.7.22-py3-none-any.whl
3
- b18aa5a0bd311407aced15656803a05bf4019f49287bb6a0b3683ca02c3c8449 coding_harness_tracing-0.1.0-py3-none-any.whl
3
+ 9f44b6e34618b87ab64f6aa48fd300bbfc6122fd4aca195122f68973747cad61 coding_harness_tracing-0.1.0-py3-none-any.whl
4
4
  aca14e09ca99c253d2a142e2d5c5c38c8c7cf90840d3cb10b241c29929871fa4 harness-install.bat
5
5
  a7ba18a98280e236b20ab4cb9f8a4006068b454586a2af50bf23fb0dbe70583d harness-install.sh
6
6
  b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61 python_dotenv-1.2.1-py3-none-any.whl
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "repo": "https://github.com/Arize-ai/coding-harness-tracing",
3
3
  "ref": "main",
4
- "resolvedSha": "a51baf0a3f45b01b1d23677176c90eb851fb3969",
4
+ "resolvedSha": "771ad555aab24ea2fd54a41b22936b6521156b26",
5
5
  "source": "git",
6
6
  "wheel": "coding_harness_tracing-0.1.0-py3-none-any.whl",
7
7
  "wheelVersion": "0.1.0",
8
- "wheelSha256": "b18aa5a0bd311407aced15656803a05bf4019f49287bb6a0b3683ca02c3c8449",
8
+ "wheelSha256": "9f44b6e34618b87ab64f6aa48fd300bbfc6122fd4aca195122f68973747cad61",
9
9
  "files": [
10
10
  "LICENSE-coding-harness-tracing",
11
11
  "MANIFEST",