Skip to content

Commit e664d78

Browse files
anandgupta42claude
andcommitted
fix: new user detection race condition + telemetry gaps
UI Fixes: - Guard `isFirstTimeUser` on sync status — don't show beginner UI while sessions are loading (prevents flash on every startup) - Make Tips component reactive — tip pool now updates when `isFirstTime` changes (was locked at render time) Telemetry Fixes (privacy-safe): - Add `first_launch` event — fires once after install, contains only version string and is_upgrade boolean. No PII. Opt-out-able. - Use machine_id as ai.user.id fallback — IMPROVES privacy by giving each anonymous user a distinct random UUID instead of grouping all non-logged-in users under empty string "" Documentation: - telemetry.md: added `first_launch` to event table, new "New User Identification" section, "Data Retention" section - security-faq.md: added "How does Altimate Code identify users?" and "What happens on first launch?" sections All telemetry changes respect existing ALTIMATE_TELEMETRY_DISABLED opt-out. No PII is ever sent — machine_id is crypto.randomUUID(), email is SHA-256 hashed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 453e332 commit e664d78

6 files changed

Lines changed: 74 additions & 7 deletions

File tree

docs/docs/reference/security-faq.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,23 @@ Or via environment variable:
126126
export ALTIMATE_TELEMETRY_DISABLED=true
127127
```
128128

129+
### How does Altimate Code identify users for analytics?
130+
131+
- **Logged-in users:** Your email is SHA-256 hashed before sending. We never see your raw email.
132+
- **Anonymous users:** A random UUID (`crypto.randomUUID()`) is generated on first run and stored at `~/.altimate/machine-id`. This is NOT tied to your hardware, OS, or identity — it's purely random.
133+
- **Both identifiers** are only sent when telemetry is enabled. Disable with `ALTIMATE_TELEMETRY_DISABLED=true`.
134+
- **No fingerprinting:** We do not use browser fingerprinting, hardware IDs, MAC addresses, or IP-based tracking.
135+
136+
### What happens on first launch?
137+
138+
A single `first_launch` event is sent containing only:
139+
140+
- The installed version (e.g., "0.5.9")
141+
- Whether this is a fresh install or upgrade (boolean)
142+
- Your anonymous machine ID (random UUID)
143+
144+
No code, queries, file paths, or personal information is included. This event helps us understand adoption and is fully opt-out-able.
145+
129146
## What happens when I authenticate via a well-known URL?
130147

131148
When you run `altimate auth login <url>`, the CLI fetches `<url>/.well-known/altimate-code` to discover the server's auth command. Before executing anything:

docs/docs/reference/telemetry.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ We collect the following categories of events:
3636
| `skill_used` | A skill is loaded (skill name and source — `builtin`, `global`, or `project` — no skill content) |
3737
| `sql_execute_failure` | A SQL execution fails (warehouse type, query type, error message, PII-masked SQL — no raw values) |
3838
| `core_failure` | An internal tool error occurs (tool name, category, error class, truncated error message, PII-safe input signature, and optionally masked arguments — no raw values or credentials) |
39+
| `first_launch` | Fired once on first CLI run after installation. Contains version and is_upgrade flag. No PII. |
3940

4041
Each event includes a timestamp, anonymous session ID, CLI version, and an anonymous machine ID (a random UUID stored in `~/.altimate/machine-id`, generated once and never tied to any personal information).
4142

@@ -88,6 +89,19 @@ We take your privacy seriously. Altimate Code telemetry **never** collects:
8889

8990
Error messages are truncated to 500 characters and scrubbed of file paths before sending.
9091

92+
### New User Identification
93+
94+
Altimate Code uses two types of anonymous identifiers for analytics, depending on whether you are logged in:
95+
96+
- **Anonymous users (not logged in):** A random UUID is generated using `crypto.randomUUID()` on first run and stored at `~/.altimate/machine-id`. This ID is not tied to your hardware, operating system, or identity — it is purely random and serves only to distinguish one machine from another in aggregate analytics.
97+
- **Logged-in users (OAuth):** Your email address is SHA-256 hashed before sending. The raw email is never transmitted.
98+
99+
Both identifiers are only sent when telemetry is enabled. Disable telemetry entirely with `ALTIMATE_TELEMETRY_DISABLED=true` or the config option above.
100+
101+
### Data Retention
102+
103+
Telemetry data is sent to Azure Application Insights and retained according to [Microsoft's data retention policies](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-retention-configure). We do not maintain a separate data store. To request deletion of your telemetry data, contact privacy@altimate.ai.
104+
91105
## Network
92106

93107
Telemetry data is sent to Azure Application Insights:

packages/opencode/src/altimate/telemetry/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,15 @@ export namespace Telemetry {
350350
skill_source: "builtin" | "global" | "project"
351351
duration_ms: number
352352
}
353+
// altimate_change start — first_launch event for new user counting (privacy-safe: only version + machine_id)
354+
| {
355+
type: "first_launch"
356+
timestamp: number
357+
session_id: string
358+
version: string
359+
is_upgrade: boolean
360+
}
361+
// altimate_change end
353362
// altimate_change start — telemetry for skill management operations
354363
| {
355364
type: "skill_created"
@@ -618,7 +627,13 @@ export namespace Telemetry {
618627
iKey: cfg.iKey,
619628
tags: {
620629
"ai.session.id": sid || "startup",
621-
"ai.user.id": userEmail,
630+
// altimate_change start — use machine_id as fallback for anonymous user identification
631+
// This IMPROVES privacy: previously all anonymous users shared ai.user.id=""
632+
// which made them appear as one mega-user in analytics. Using the random UUID
633+
// (already sent as a custom property) gives each machine a distinct identity
634+
// without any PII. machine_id is a crypto.randomUUID() stored locally.
635+
"ai.user.id": userEmail || machineId || "",
636+
// altimate_change end
622637
"ai.cloud.role": "altimate",
623638
"ai.application.ver": Installation.VERSION,
624639
},

packages/opencode/src/cli/cmd/tui/component/tips.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,26 +47,28 @@ const BEGINNER_TIPS = [
4747
]
4848
// altimate_change end
4949

50-
// altimate_change start — first-time user beginner tips
50+
// altimate_change start — first-time user beginner tips with reactive pool
5151
export function Tips(props: { isFirstTime?: boolean }) {
5252
const theme = useTheme().theme
53-
const pool = props.isFirstTime ? BEGINNER_TIPS : TIPS
54-
const parts = parse(pool[Math.floor(Math.random() * pool.length)])
55-
// altimate_change end
53+
const tip = createMemo(() => {
54+
const pool = props.isFirstTime ? BEGINNER_TIPS : TIPS
55+
return parse(pool[Math.floor(Math.random() * pool.length)])
56+
})
5657

5758
return (
5859
<box flexDirection="row" maxWidth="100%">
5960
<text flexShrink={0} style={{ fg: theme.warning }}>
6061
● Tip{" "}
6162
</text>
6263
<text flexShrink={1}>
63-
<For each={parts}>
64+
<For each={tip()}>
6465
{(part) => <span style={{ fg: part.highlight ? theme.text : theme.textMuted }}>{part.text}</span>}
6566
</For>
6667
</text>
6768
</box>
6869
)
6970
}
71+
// altimate_change end
7072

7173
const TIPS = [
7274
"Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files",

packages/opencode/src/cli/cmd/tui/routes/home.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ export function Home() {
3838
return Object.values(sync.data.mcp).filter((x) => x.status === "connected").length
3939
})
4040

41-
const isFirstTimeUser = createMemo(() => sync.data.session.length === 0)
41+
// altimate_change start — fix race condition: don't show beginner UI until sessions loaded
42+
const isFirstTimeUser = createMemo(() => {
43+
// Don't evaluate until sessions have actually loaded (avoid flash of beginner UI)
44+
if (sync.status === "loading" || sync.status === "partial") return false
45+
return sync.data.session.length === 0
46+
})
47+
// altimate_change end
4248
const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
4349
const showTips = createMemo(() => {
4450
// Always show tips — first-time users need guidance the most

packages/opencode/src/cli/welcome.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import path from "path"
33
import os from "os"
44
import { Installation } from "../installation"
55
import { EOL } from "os"
6+
// altimate_change start — import Telemetry for first_launch event
7+
import { Telemetry } from "../altimate/telemetry"
8+
// altimate_change end
69

710
const APP_NAME = "altimate-code"
811
const MARKER_FILE = ".installed-version"
@@ -41,6 +44,16 @@ export function showWelcomeBannerIfNeeded(): void {
4144
// altimate_change end
4245
const isUpgrade = installedVersion === currentVersion && installedVersion !== "local"
4346

47+
// altimate_change start — track first launch for new user counting (privacy-safe: only version + machine_id)
48+
Telemetry.track({
49+
type: "first_launch",
50+
timestamp: Date.now(),
51+
session_id: "",
52+
version: installedVersion,
53+
is_upgrade: isUpgrade,
54+
})
55+
// altimate_change end
56+
4457
if (!isUpgrade) return
4558

4659
const tty = process.stderr.isTTY

0 commit comments

Comments
 (0)