Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .opencode/skills/data-viz/references/component-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ activateTab('overview'); // init the default visible tab on page load
Library-specific notes:
- **Chart.js**: canvas reads as `0×0` inside `display:none` — bars/lines never appear
- **Recharts `ResponsiveContainer`**: reads `clientWidth = 0` — chart collapses to nothing
- **Nivo `Responsive*`**: uses `ResizeObserver` — fires once at `0×0`, never re-fires on show
- **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
- **React conditional rendering**: prefer `visibility:hidden` + `position:absolute` over toggling `display:none` if you want charts to stay mounted and pre-rendered

---
Expand Down
17 changes: 17 additions & 0 deletions docs/docs/reference/security-faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ Or via environment variable:
export ALTIMATE_TELEMETRY_DISABLED=true
```

### How does Altimate Code identify users for analytics?

- **Logged-in users:** Your email is SHA-256 hashed before sending. We never see your raw email.
- **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.
- **Both identifiers** are only sent when telemetry is enabled. Disable with `ALTIMATE_TELEMETRY_DISABLED=true`.
- **No fingerprinting:** We do not use browser fingerprinting, hardware IDs, MAC addresses, or IP-based tracking.

### What happens on first launch?

A single `first_launch` event is sent containing only:

- The installed version (e.g., "0.5.9")
- Whether this is a fresh install or upgrade (boolean)
- Your anonymous machine ID (random UUID)

No code, queries, file paths, or personal information is included. This event helps us understand adoption and is fully opt-out-able.

## What happens when I authenticate via a well-known URL?

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:
Expand Down
14 changes: 14 additions & 0 deletions docs/docs/reference/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ We collect the following categories of events:
| `skill_used` | A skill is loaded (skill name and source — `builtin`, `global`, or `project` — no skill content) |
| `sql_execute_failure` | A SQL execution fails (warehouse type, query type, error message, PII-masked SQL — no raw values) |
| `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) |
| `first_launch` | Fired once on first CLI run after installation. Contains version and is_upgrade flag. No PII. |

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).

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

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

### New User Identification

Altimate Code uses two types of anonymous identifiers for analytics, depending on whether you are logged in:

- **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.
- **Logged-in users (OAuth):** Your email address is SHA-256 hashed before sending. The raw email is never transmitted.

Both identifiers are only sent when telemetry is enabled. Disable telemetry entirely with `ALTIMATE_TELEMETRY_DISABLED=true` or the config option above.

### Data Retention

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.

## Network

Telemetry data is sent to Azure Application Insights:
Expand Down
17 changes: 16 additions & 1 deletion packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,15 @@ export namespace Telemetry {
skill_source: "builtin" | "global" | "project"
duration_ms: number
}
// altimate_change start — first_launch event for new user counting (privacy-safe: only version + machine_id)
| {
type: "first_launch"
timestamp: number
session_id: string
version: string
is_upgrade: boolean
}
// altimate_change end
// altimate_change start — telemetry for skill management operations
| {
type: "skill_created"
Expand Down Expand Up @@ -618,7 +627,13 @@ export namespace Telemetry {
iKey: cfg.iKey,
tags: {
"ai.session.id": sid || "startup",
"ai.user.id": userEmail,
// altimate_change start — use machine_id as fallback for anonymous user identification
// This IMPROVES privacy: previously all anonymous users shared ai.user.id=""
// which made them appear as one mega-user in analytics. Using the random UUID
// (already sent as a custom property) gives each machine a distinct identity
// without any PII. machine_id is a crypto.randomUUID() stored locally.
"ai.user.id": userEmail || machineId || "",
// altimate_change end
"ai.cloud.role": "altimate",
"ai.application.ver": Installation.VERSION,
},
Expand Down
17 changes: 11 additions & 6 deletions packages/opencode/src/cli/cmd/tui/component/tips.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createMemo, createSignal, For } from "solid-js"
import { createMemo, For } from "solid-js"
import { DEFAULT_THEMES, useTheme } from "@tui/context/theme"

const themeCount = Object.keys(DEFAULT_THEMES).length
Expand Down Expand Up @@ -47,26 +47,31 @@ const BEGINNER_TIPS = [
]
// altimate_change end

// altimate_change start — first-time user beginner tips
// altimate_change start — first-time user beginner tips with reactive pool
export function Tips(props: { isFirstTime?: boolean }) {
const theme = useTheme().theme
const pool = props.isFirstTime ? BEGINNER_TIPS : TIPS
const parts = parse(pool[Math.floor(Math.random() * pool.length)])
// altimate_change end
// Pick random tip index once on mount instead of recalculating randomly when props change
// Use useMemo without dependencies so it only evaluates once
const tipIndex = Math.random()
const tip = createMemo(() => {
const pool = props.isFirstTime ? BEGINNER_TIPS : TIPS
return parse(pool[Math.floor(tipIndex * pool.length)])
})

return (
<box flexDirection="row" maxWidth="100%">
<text flexShrink={0} style={{ fg: theme.warning }}>
● Tip{" "}
</text>
<text flexShrink={1}>
<For each={parts}>
<For each={tip()}>
{(part) => <span style={{ fg: part.highlight ? theme.text : theme.textMuted }}>{part.text}</span>}
</For>
</text>
</box>
)
}
// altimate_change end

const TIPS = [
"Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files",
Expand Down
13 changes: 10 additions & 3 deletions packages/opencode/src/cli/cmd/tui/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ export function Home() {
return Object.values(sync.data.mcp).filter((x) => x.status === "connected").length
})

const isFirstTimeUser = createMemo(() => sync.data.session.length === 0)
// altimate_change start — fix race condition: don't show beginner UI until sessions loaded
const isFirstTimeUser = createMemo(() => {
// Don't evaluate until sessions have actually loaded (avoid flash of beginner UI)
// Return undefined to represent "loading" state
if (sync.status === "loading" || sync.status === "partial") return undefined
return sync.data.session.length === 0
})
// altimate_change end
const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
const showTips = createMemo(() => {
// Always show tips — first-time users need guidance the most
Expand Down Expand Up @@ -127,7 +134,7 @@ export function Home() {
/>
</box>
{/* altimate_change start — first-time onboarding hint */}
<Show when={isFirstTimeUser()}>
<Show when={isFirstTimeUser() === true}>
<box width="100%" maxWidth={75} paddingTop={1} flexShrink={0}>
<text>
<span style={{ fg: theme.textMuted }}>Get started: </span>
Expand All @@ -146,7 +153,7 @@ export function Home() {
<box height={4} minHeight={0} width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
<Show when={showTips()}>
{/* altimate_change start — pass first-time flag for beginner tips */}
<Tips isFirstTime={isFirstTimeUser()} />
<Tips isFirstTime={isFirstTimeUser() === true} />
{/* altimate_change end */}
</Show>
</box>
Expand Down
26 changes: 22 additions & 4 deletions packages/opencode/src/cli/welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import path from "path"
import os from "os"
import { Installation } from "../installation"
import { EOL } from "os"
// altimate_change start — import Telemetry for first_launch event
import { Telemetry } from "../altimate/telemetry"
// altimate_change end

const APP_NAME = "altimate-code"
const MARKER_FILE = ".installed-version"
Expand Down Expand Up @@ -36,10 +39,23 @@ export function showWelcomeBannerIfNeeded(): void {
// Remove marker first to avoid showing twice even if display fails
fs.unlinkSync(markerPath)

// altimate_change start — VERSION is already normalized (no "v" prefix)
const currentVersion = Installation.VERSION
// altimate_change start — use ~/.altimate/machine-id existence as a proxy for upgrade vs fresh install
// Since postinstall.mjs always writes the current version to the marker file, we can't reliably
// use installedVersion !== currentVersion for release builds. Instead, if machine-id exists,
// they've run the CLI before.
const machineIdPath = path.join(os.homedir(), ".altimate", "machine-id")
const isUpgrade = fs.existsSync(machineIdPath)
// altimate_change end

// altimate_change start — track first launch for new user counting (privacy-safe: only version + machine_id)
Telemetry.track({
type: "first_launch",
timestamp: Date.now(),
session_id: "",
version: installedVersion,
is_upgrade: isUpgrade,
})
// altimate_change end
const isUpgrade = installedVersion === currentVersion && installedVersion !== "local"

if (!isUpgrade) return

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

const v = `altimate-code v${currentVersion} installed`
// altimate_change start — use installedVersion (from marker) instead of currentVersion for accurate banner
const v = `altimate-code v${installedVersion} installed`
// altimate_change end
const lines = [
"",
" Get started:",
Expand Down
Loading
Loading