From 453e332a39df8e1303ab5effbd4726991edf4bff Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 23 Mar 2026 22:07:14 -0700 Subject: [PATCH 1/4] skill(data-viz): add lazy init, data-code separation, color contrast, icon semantics, field validation, pre-delivery checklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .opencode/skills/data-viz/SKILL.md | 15 +++++ .../data-viz/references/component-guide.md | 67 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/.opencode/skills/data-viz/SKILL.md b/.opencode/skills/data-viz/SKILL.md index ad6a021691..44f85fedcc 100644 --- a/.opencode/skills/data-viz/SKILL.md +++ b/.opencode/skills/data-viz/SKILL.md @@ -100,6 +100,8 @@ A single insight might just be one chart with a headline and annotation. Scale c - **Responsive**: `min-h-[VALUE]` on all charts. Grid stacks on mobile - **Animation**: Entry transitions only, `duration-300` to `duration-500`. Never continuous - **Accessibility**: `aria-label` on charts, WCAG AA contrast, don't rely on color alone +- **Dynamic color safety**: When colors come from external sources (brand palettes, category maps, API data, user config), never apply them directly as text color without a contrast check. Dark colors are invisible on dark card backgrounds. Safe pattern: use the external color only for non-text elements (left border, dot, underline); always use the standard text color (white / `var(--text)`) for the label itself. If color-coded text is required, apply a minimum lightness floor: `color: hsl(from brandColor h s max(l, 60%))` +- **Icon semantics**: Verify every icon matches its label's actual meaning, not just its visual shape. Common traps: using a rising-trend icon (📈) for metrics where lower is better (latency, error rate, cost); using achievement icons (🏆) for plain counts. When in doubt, use a neutral descriptive icon over a thematic one that could mislead ### Step 5: Interactivity & Annotations @@ -133,3 +135,16 @@ A single insight might just be one chart with a headline and annotation. Scale c - Pie charts > 5 slices — use horizontal bar - Unlabeled dual y-axes — use two separate charts - Truncated bar axes — always start at zero +- Filtering or mapping over a field not confirmed to exist in the data export — an undefined field in `.filter()` or `.map()` produces empty arrays or NaN silently, not an error; always validate the exported schema matches what the chart code consumes + +## Pre-Delivery Checklist + +Before marking a dashboard complete: + +- [ ] Every tab / view activated — all charts render (no blank canvases, no unexpected 0–1 axes) +- [ ] Every field referenced in chart/filter code confirmed present in the data export +- [ ] All text readable on its background — check explicitly when colors come from external data +- [ ] All icons match their label's meaning +- [ ] Tooltips appear on hover for every chart +- [ ] No chart silently receives an empty dataset — add a visible empty state or console warning +- [ ] Mobile: grid stacks correctly, no body-level horizontal overflow diff --git a/.opencode/skills/data-viz/references/component-guide.md b/.opencode/skills/data-viz/references/component-guide.md index 8f792da07f..28a6ce46d8 100644 --- a/.opencode/skills/data-viz/references/component-guide.md +++ b/.opencode/skills/data-viz/references/component-guide.md @@ -392,3 +392,70 @@ const CalloutLabel = ({ viewBox, label, color = "#1e293b" }: { viewBox?: { x: nu ``` **Rules:** Never overlap data. Use `position: "insideTopRight"/"insideTopLeft"` on labels. Pair annotations with tooltips — annotation names the event, tooltip shows the value. + +--- + +## Multi-Tab Dashboard — Lazy Chart Initialization + +Charts initialized inside a hidden container (`display:none`) render blank. Chart.js, Recharts, and Nivo all read container dimensions at mount time — a hidden container measures as `0×0`. + +**Rule: never initialize a chart until its container is visible.** + +```js +// Vanilla JS pattern +var _inited = {}; + +function activateTab(name) { + // 1. make the tab visible first + document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); + document.getElementById('tab-' + name).classList.add('active'); + // 2. then initialize charts — only on first visit + if (!_inited[name]) { + _inited[name] = true; + initChartsFor(name); + } +} + +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 +- **React conditional rendering**: prefer `visibility:hidden` + `position:absolute` over toggling `display:none` if you want charts to stay mounted and pre-rendered + +--- + +## Programmatic Dashboard Generation — Data-Code Separation + +When generating a standalone HTML dashboard from a script (Python, shell, etc.), never embed JSON data inside a template string that also contains JavaScript. Curly-brace collisions in f-strings / template literals cause silent JS parse failures that are hard to debug. + +**Wrong** — data and JS logic share one f-string, every `{` in JS must be escaped as `{{`: + +```python +html = f""" + +""" +``` + +**Right** — separate data from logic entirely: + +```python +# Step 1: write data to its own file — no template string needed +with open('data.js', 'w') as f: + f.write('const DATA = ' + json.dumps(data) + ';') + +# Step 2: HTML loads both files; app.js is static and never needs escaping +``` + +```html + + +``` + +Benefits: `app.js` is static and independently testable; `data.js` is regenerated without touching logic; no escaping required in either file. From e664d78769a032371784a239fd48aa6cb4c854fa Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Tue, 24 Mar 2026 11:22:03 -0700 Subject: [PATCH 2/4] fix: new user detection race condition + telemetry gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/docs/reference/security-faq.md | 17 +++++++++++++++++ docs/docs/reference/telemetry.md | 14 ++++++++++++++ .../opencode/src/altimate/telemetry/index.ts | 17 ++++++++++++++++- .../opencode/src/cli/cmd/tui/component/tips.tsx | 12 +++++++----- .../opencode/src/cli/cmd/tui/routes/home.tsx | 8 +++++++- packages/opencode/src/cli/welcome.ts | 13 +++++++++++++ 6 files changed, 74 insertions(+), 7 deletions(-) diff --git a/docs/docs/reference/security-faq.md b/docs/docs/reference/security-faq.md index ce5c8ff4e7..71fb872318 100644 --- a/docs/docs/reference/security-faq.md +++ b/docs/docs/reference/security-faq.md @@ -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 `, the CLI fetches `/.well-known/altimate-code` to discover the server's auth command. Before executing anything: diff --git a/docs/docs/reference/telemetry.md b/docs/docs/reference/telemetry.md index 4535ae72b6..e5e8a146ef 100644 --- a/docs/docs/reference/telemetry.md +++ b/docs/docs/reference/telemetry.md @@ -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). @@ -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: diff --git a/packages/opencode/src/altimate/telemetry/index.ts b/packages/opencode/src/altimate/telemetry/index.ts index 25ae604150..659cf70d88 100644 --- a/packages/opencode/src/altimate/telemetry/index.ts +++ b/packages/opencode/src/altimate/telemetry/index.ts @@ -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" @@ -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, }, diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.tsx b/packages/opencode/src/cli/cmd/tui/component/tips.tsx index d3b9d9cc09..c2df1e2513 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/tips.tsx @@ -47,12 +47,13 @@ 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 + const tip = createMemo(() => { + const pool = props.isFirstTime ? BEGINNER_TIPS : TIPS + return parse(pool[Math.floor(Math.random() * pool.length)]) + }) return ( @@ -60,13 +61,14 @@ export function Tips(props: { isFirstTime?: boolean }) { ● Tip{" "} - + {(part) => {part.text}} ) } +// altimate_change end const TIPS = [ "Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files", diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index d16bc5d15a..fbbb8cc7c5 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -38,7 +38,13 @@ 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) + if (sync.status === "loading" || sync.status === "partial") return false + 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 diff --git a/packages/opencode/src/cli/welcome.ts b/packages/opencode/src/cli/welcome.ts index f01e7b9759..dd77c8b773 100644 --- a/packages/opencode/src/cli/welcome.ts +++ b/packages/opencode/src/cli/welcome.ts @@ -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" @@ -41,6 +44,16 @@ export function showWelcomeBannerIfNeeded(): void { // altimate_change end const isUpgrade = installedVersion === currentVersion && installedVersion !== "local" + // 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 + if (!isUpgrade) return const tty = process.stderr.isTTY From 80b5734e7fd49c0433363f2423ad26bb68b2bf86 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Tue, 24 Mar 2026 12:34:23 -0700 Subject: [PATCH 3/4] 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 --- .../src/cli/cmd/tui/component/tips.tsx | 7 +- .../opencode/src/cli/cmd/tui/routes/home.tsx | 7 +- packages/opencode/src/cli/welcome.ts | 11 +-- .../opencode/test/telemetry/telemetry.test.ts | 71 ++++++++++--------- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.tsx b/packages/opencode/src/cli/cmd/tui/component/tips.tsx index c2df1e2513..818edfed01 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/tips.tsx @@ -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 @@ -50,9 +50,12 @@ const BEGINNER_TIPS = [ // altimate_change start — first-time user beginner tips with reactive pool export function Tips(props: { isFirstTime?: boolean }) { const theme = useTheme().theme + // 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(Math.random() * pool.length)]) + return parse(pool[Math.floor(tipIndex * pool.length)]) }) return ( diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index fbbb8cc7c5..a702e3af25 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -41,7 +41,8 @@ export function Home() { // 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) - if (sync.status === "loading" || sync.status === "partial") return false + // Return undefined to represent "loading" state + if (sync.status === "loading" || sync.status === "partial") return undefined return sync.data.session.length === 0 }) // altimate_change end @@ -133,7 +134,7 @@ export function Home() { /> {/* altimate_change start — first-time onboarding hint */} - + Get started: @@ -152,7 +153,7 @@ export function Home() { {/* altimate_change start — pass first-time flag for beginner tips */} - + {/* altimate_change end */} diff --git a/packages/opencode/src/cli/welcome.ts b/packages/opencode/src/cli/welcome.ts index dd77c8b773..7e650a0625 100644 --- a/packages/opencode/src/cli/welcome.ts +++ b/packages/opencode/src/cli/welcome.ts @@ -39,10 +39,13 @@ 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 - const isUpgrade = installedVersion === currentVersion && installedVersion !== "local" // altimate_change start — track first launch for new user counting (privacy-safe: only version + machine_id) Telemetry.track({ @@ -64,7 +67,7 @@ export function showWelcomeBannerIfNeeded(): void { const reset = "\x1b[0m" const bold = "\x1b[1m" - const v = `altimate-code v${currentVersion} installed` + const v = `altimate-code v${installedVersion} installed` const lines = [ "", " Get started:", diff --git a/packages/opencode/test/telemetry/telemetry.test.ts b/packages/opencode/test/telemetry/telemetry.test.ts index b8f4b1fb1a..f1b7990eb9 100644 --- a/packages/opencode/test/telemetry/telemetry.test.ts +++ b/packages/opencode/test/telemetry/telemetry.test.ts @@ -231,8 +231,12 @@ describe("telemetry.event-types", () => { "warehouse_discovery", "warehouse_census", "core_failure", + "first_launch", + "skill_created", + "skill_installed", + "skill_removed", ] - expect(eventTypes.length).toBe(33) + expect(eventTypes.length).toBe(37) }) }) @@ -352,6 +356,10 @@ describe("telemetry.naming-convention", () => { "warehouse_discovery", "warehouse_census", "core_failure", + "first_launch", + "skill_created", + "skill_installed", + "skill_removed", ] for (const t of types) { expect(t).toMatch(/^[a-z][a-z0-9_]*$/) @@ -418,8 +426,7 @@ describe("telemetry.parseConnectionString (indirect)", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "IngestionEndpoint=https://example.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "IngestionEndpoint=https://example.com" await Telemetry.init() Telemetry.track({ @@ -734,8 +741,7 @@ describe("telemetry.flush", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() Telemetry.track({ @@ -770,8 +776,7 @@ describe("telemetry.flush", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() Telemetry.track({ @@ -810,8 +815,7 @@ describe("telemetry.flush", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() Telemetry.track({ @@ -858,8 +862,7 @@ describe("telemetry.flush", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() Telemetry.track({ @@ -894,8 +897,7 @@ describe("telemetry.flush", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() // Fill buffer beyond MAX_BUFFER_SIZE (200) to trigger drops @@ -917,8 +919,8 @@ describe("telemetry.flush", () => { const envelopes = JSON.parse(fetchBodies[0]) // Should include a TelemetryBufferOverflow error event const overflowEvent = envelopes.find( - (e: any) => e.data?.baseData?.name === "error" && - e.data?.baseData?.properties?.error_name === "TelemetryBufferOverflow", + (e: any) => + e.data?.baseData?.name === "error" && e.data?.baseData?.properties?.error_name === "TelemetryBufferOverflow", ) expect(overflowEvent).toBeDefined() expect(overflowEvent.data.baseData.properties.error_message).toContain("10 events dropped") @@ -970,8 +972,7 @@ describe("telemetry.shutdown", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() Telemetry.track({ @@ -1005,8 +1006,7 @@ describe("telemetry.shutdown", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() Telemetry.setContext({ sessionId: "sess-1", projectId: "proj-1" }) @@ -1060,8 +1060,7 @@ describe("telemetry.shutdown", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() await Telemetry.shutdown() @@ -1101,8 +1100,7 @@ describe("telemetry.buffer overflow", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() // Track 250 events — first 50 should be dropped @@ -1158,8 +1156,7 @@ describe("telemetry.buffer overflow", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() // Exactly 205 events — 5 should be dropped @@ -1211,8 +1208,7 @@ describe("telemetry.init with enabled telemetry", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() // If flush timer is set up, tracking + waiting should eventually trigger flush @@ -1316,8 +1312,7 @@ describe("telemetry.init with enabled telemetry", () => { try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" const p1 = Telemetry.init() const p2 = Telemetry.init() @@ -1450,6 +1445,18 @@ describe("telemetry.memory", () => { }) }).not.toThrow() }) + + test("track accepts first_launch event without throwing", () => { + expect(() => { + Telemetry.track({ + type: "first_launch", + timestamp: Date.now(), + session_id: "", + version: "0.5.9", + is_upgrade: false, + }) + }).not.toThrow() + }) }) // --------------------------------------------------------------------------- @@ -1483,8 +1490,7 @@ describe("Telemetry.isEnabled()", () => { spyOn(global, "fetch").mockImplementation(async () => new Response("", { status: 200 })) try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() expect(Telemetry.isEnabled()).toBe(true) } finally { @@ -1501,8 +1507,7 @@ describe("Telemetry.isEnabled()", () => { spyOn(global, "fetch").mockImplementation(async () => new Response("", { status: 200 })) try { delete process.env.ALTIMATE_TELEMETRY_DISABLED - process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = - "InstrumentationKey=k;IngestionEndpoint=https://e.com" + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=k;IngestionEndpoint=https://e.com" await Telemetry.init() expect(Telemetry.isEnabled()).toBe(true) await Telemetry.shutdown() From 2e784ddf7123d0df11b3834049582c89c37d23af Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Tue, 24 Mar 2026 13:45:20 -0700 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?= =?UTF-8?q?=E2=80=94=20Nivo=20description=20+=20marker=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .opencode/skills/data-viz/references/component-guide.md | 2 +- packages/opencode/src/cli/welcome.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.opencode/skills/data-viz/references/component-guide.md b/.opencode/skills/data-viz/references/component-guide.md index 28a6ce46d8..4e27168380 100644 --- a/.opencode/skills/data-viz/references/component-guide.md +++ b/.opencode/skills/data-viz/references/component-guide.md @@ -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 --- diff --git a/packages/opencode/src/cli/welcome.ts b/packages/opencode/src/cli/welcome.ts index 7e650a0625..2b08a79b35 100644 --- a/packages/opencode/src/cli/welcome.ts +++ b/packages/opencode/src/cli/welcome.ts @@ -67,7 +67,9 @@ export function showWelcomeBannerIfNeeded(): void { const reset = "\x1b[0m" const bold = "\x1b[1m" + // 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:",