Skip to content

Commit 3022614

Browse files
anandgupta42claude
andauthored
fix: new user detection race condition + telemetry gaps (#445)
* skill(data-viz): add lazy init, data-code separation, color contrast, icon semantics, field validation, pre-delivery checklist Six improvements derived from session learnings — all general, none task-specific: - component-guide: lazy chart initialization pattern for multi-tab dashboards (Chart.js/Recharts/Nivo all render blank in display:none containers) - component-guide: data-code separation for programmatic HTML generation (f-string + JS curly braces cause silent parse failures) - SKILL.md Design Principles: dynamic color safety rule for external/brand colors - SKILL.md Design Principles: icon semantics check - SKILL.md Anti-Patterns: warn against filtering on unvalidated data fields - SKILL.md: pre-delivery checklist (tabs, fields, contrast, icons, tooltips, mobile) * 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> * fix: address code review feedback on new user detection and telemetry - use `~/.altimate/machine-id` existence for robust `is_upgrade` flag - fix 3-state logic in `isFirstTimeUser` memo to prevent suppressed beginner UI - prevent tip re-randomization on prop change in `tips.tsx` - add missing `first_launch` event to telemetry tests - remove unused import * fix: address CodeRabbit review — Nivo description + marker guard - Correct Nivo `Responsive*` behavior: `ResizeObserver` does re-fire when container becomes visible, not "never re-fires on show" - Add `altimate_change` marker around `installedVersion` banner line Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 12ed190 commit 3022614

File tree

8 files changed

+129
-48
lines changed

8 files changed

+129
-48
lines changed

.opencode/skills/data-viz/references/component-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ activateTab('overview'); // init the default visible tab on page load
422422
Library-specific notes:
423423
- **Chart.js**: canvas reads as `0×0` inside `display:none` — bars/lines never appear
424424
- **Recharts `ResponsiveContainer`**: reads `clientWidth = 0` — chart collapses to nothing
425-
- **Nivo `Responsive*`**: uses `ResizeObserver` — fires once at `0×0`, never re-fires on show
425+
- **Nivo `Responsive*`**: uses `ResizeObserver` via `useMeasure`/`useDimensions` in `@nivo/core` — initially measures `0×0` when hidden and skips rendering; re-measures and re-renders correctly when container becomes visible, but the initial blank frame can cause a flash
426426
- **React conditional rendering**: prefer `visibility:hidden` + `position:absolute` over toggling `display:none` if you want charts to stay mounted and pre-rendered
427427

428428
---

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: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createMemo, createSignal, For } from "solid-js"
1+
import { createMemo, For } from "solid-js"
22
import { DEFAULT_THEMES, useTheme } from "@tui/context/theme"
33

44
const themeCount = Object.keys(DEFAULT_THEMES).length
@@ -47,26 +47,31 @@ 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+
// Pick random tip index once on mount instead of recalculating randomly when props change
54+
// Use useMemo without dependencies so it only evaluates once
55+
const tipIndex = Math.random()
56+
const tip = createMemo(() => {
57+
const pool = props.isFirstTime ? BEGINNER_TIPS : TIPS
58+
return parse(pool[Math.floor(tipIndex * pool.length)])
59+
})
5660

5761
return (
5862
<box flexDirection="row" maxWidth="100%">
5963
<text flexShrink={0} style={{ fg: theme.warning }}>
6064
● Tip{" "}
6165
</text>
6266
<text flexShrink={1}>
63-
<For each={parts}>
67+
<For each={tip()}>
6468
{(part) => <span style={{ fg: part.highlight ? theme.text : theme.textMuted }}>{part.text}</span>}
6569
</For>
6670
</text>
6771
</box>
6872
)
6973
}
74+
// altimate_change end
7075

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

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ 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+
// Return undefined to represent "loading" state
45+
if (sync.status === "loading" || sync.status === "partial") return undefined
46+
return sync.data.session.length === 0
47+
})
48+
// altimate_change end
4249
const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
4350
const showTips = createMemo(() => {
4451
// Always show tips — first-time users need guidance the most
@@ -127,7 +134,7 @@ export function Home() {
127134
/>
128135
</box>
129136
{/* altimate_change start — first-time onboarding hint */}
130-
<Show when={isFirstTimeUser()}>
137+
<Show when={isFirstTimeUser() === true}>
131138
<box width="100%" maxWidth={75} paddingTop={1} flexShrink={0}>
132139
<text>
133140
<span style={{ fg: theme.textMuted }}>Get started: </span>
@@ -146,7 +153,7 @@ export function Home() {
146153
<box height={4} minHeight={0} width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
147154
<Show when={showTips()}>
148155
{/* altimate_change start — pass first-time flag for beginner tips */}
149-
<Tips isFirstTime={isFirstTimeUser()} />
156+
<Tips isFirstTime={isFirstTimeUser() === true} />
150157
{/* altimate_change end */}
151158
</Show>
152159
</box>

packages/opencode/src/cli/welcome.ts

Lines changed: 22 additions & 4 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"
@@ -36,10 +39,23 @@ export function showWelcomeBannerIfNeeded(): void {
3639
// Remove marker first to avoid showing twice even if display fails
3740
fs.unlinkSync(markerPath)
3841

39-
// altimate_change start — VERSION is already normalized (no "v" prefix)
40-
const currentVersion = Installation.VERSION
42+
// altimate_change start — use ~/.altimate/machine-id existence as a proxy for upgrade vs fresh install
43+
// Since postinstall.mjs always writes the current version to the marker file, we can't reliably
44+
// use installedVersion !== currentVersion for release builds. Instead, if machine-id exists,
45+
// they've run the CLI before.
46+
const machineIdPath = path.join(os.homedir(), ".altimate", "machine-id")
47+
const isUpgrade = fs.existsSync(machineIdPath)
48+
// altimate_change end
49+
50+
// altimate_change start — track first launch for new user counting (privacy-safe: only version + machine_id)
51+
Telemetry.track({
52+
type: "first_launch",
53+
timestamp: Date.now(),
54+
session_id: "",
55+
version: installedVersion,
56+
is_upgrade: isUpgrade,
57+
})
4158
// altimate_change end
42-
const isUpgrade = installedVersion === currentVersion && installedVersion !== "local"
4359

4460
if (!isUpgrade) return
4561

@@ -51,7 +67,9 @@ export function showWelcomeBannerIfNeeded(): void {
5167
const reset = "\x1b[0m"
5268
const bold = "\x1b[1m"
5369

54-
const v = `altimate-code v${currentVersion} installed`
70+
// altimate_change start — use installedVersion (from marker) instead of currentVersion for accurate banner
71+
const v = `altimate-code v${installedVersion} installed`
72+
// altimate_change end
5573
const lines = [
5674
"",
5775
" Get started:",

0 commit comments

Comments
 (0)