From 7d3dc00c0bce8b9956cce0a5c1f76236f0dba79d Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 02:11:16 +0100 Subject: [PATCH 01/13] feat: add anonymous telemetry and error tracking infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add opt-out telemetry via PostHog HTTP API and error tracking via Sentry HTTP envelope API — no SDK dependencies added (ARCH-006 compliant). - PostHog analytics: command usage, flags, exit codes, duration, coarse geo - Sentry error capture: crash reports with stack traces for exit-code-2 errors - IP anonymization: PostHog resolves geo server-side then discards IP - Opt-out: ARCHGATE_TELEMETRY=0 env var or `archgate telemetry disable` - First-run notice: one-time stderr message about data collection - Telemetry command: status/enable/disable subcommands - Docs: telemetry reference page (EN + pt-BR) with full privacy policy - Tests: telemetry-config, telemetry, sentry, telemetry-notice --- .../agent-memory/archgate-developer/MEMORY.md | 4 + docs/astro.config.mjs | 1 + .../docs/pt-br/reference/telemetry.mdx | 104 +++++++ docs/src/content/docs/reference/telemetry.mdx | 104 +++++++ src/cli.ts | 41 +++ src/commands/telemetry.ts | 76 +++++ src/helpers/sentry.ts | 289 ++++++++++++++++++ src/helpers/telemetry-config.ts | 155 ++++++++++ src/helpers/telemetry-notice.ts | 40 +++ src/helpers/telemetry.ts | 192 ++++++++++++ tests/helpers/sentry.test.ts | 122 ++++++++ tests/helpers/telemetry-config.test.ts | 197 ++++++++++++ tests/helpers/telemetry-notice.test.ts | 74 +++++ tests/helpers/telemetry.test.ts | 160 ++++++++++ 14 files changed, 1559 insertions(+) create mode 100644 docs/src/content/docs/pt-br/reference/telemetry.mdx create mode 100644 docs/src/content/docs/reference/telemetry.mdx create mode 100644 src/commands/telemetry.ts create mode 100644 src/helpers/sentry.ts create mode 100644 src/helpers/telemetry-config.ts create mode 100644 src/helpers/telemetry-notice.ts create mode 100644 src/helpers/telemetry.ts create mode 100644 tests/helpers/sentry.test.ts create mode 100644 tests/helpers/telemetry-config.test.ts create mode 100644 tests/helpers/telemetry-notice.test.ts create mode 100644 tests/helpers/telemetry.test.ts diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index 544229d8..12770e5c 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -57,6 +57,10 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv - **Gitignore and prettierignore rules** — `packages/*/bin/archgate` is gitignored (binary injected by CI); `packages/*/bin/` is prettier-ignored (binary files must not be formatted). - **npm OIDC publishing** — Uses `id-token: write` permission + `actions/setup-node@v4` with `registry-url: 'https://registry.npmjs.org'`. No `NODE_AUTH_TOKEN` secret needed for OIDC trusted publishing — matches the pattern in `release.yml`. +## Telemetry Strategy + +- [Telemetry & Analytics Strategy](project_telemetry_strategy.md) — Decisions on PostHog analytics, Sentry error tracking, plugin surveys (2026-03-22) + ## MCP Tools Structure - MCP tools live in `src/mcp/tools/` — one file per tool (check, list-adrs, review-context, claude-code-session-context, cursor-session-context) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 18e54d08..14f0a309 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -228,6 +228,7 @@ export default defineConfig({ { label: "CLI Commands", slug: "reference/cli-commands" }, { label: "Rule API", slug: "reference/rule-api" }, { label: "ADR Schema", slug: "reference/adr-schema" }, + { label: "Telemetry", slug: "reference/telemetry" }, ], }, { diff --git a/docs/src/content/docs/pt-br/reference/telemetry.mdx b/docs/src/content/docs/pt-br/reference/telemetry.mdx new file mode 100644 index 00000000..c867a130 --- /dev/null +++ b/docs/src/content/docs/pt-br/reference/telemetry.mdx @@ -0,0 +1,104 @@ +--- +title: Telemetria +description: Quais dados de uso anonimos o Archgate coleta, como desativar e nossos compromissos de privacidade. +--- + +O Archgate coleta **dados de uso anonimos** para nos ajudar a entender como a CLI e utilizada, priorizar funcionalidades e corrigir falhas. Esta pagina explica exatamente o que e coletado, o que nao e, e como desativar. + +## O que coletamos + +### Analise de uso (PostHog) + +Quando voce executa um comando do Archgate, registramos: + +- **Nome do comando** e **flags utilizadas** (ex: `check --json`, nao os valores das flags) +- **Codigo de saida** (0, 1 ou 2) e **duracao da execucao** +- **Ambiente**: SO, arquitetura, versao do Bun, versao do Archgate, deteccao de CI, deteccao de TTY +- **Localizacao aproximada**: pais e regiao (resolvidos no servidor a partir do seu IP, que e descartado em seguida — veja [Anonimizacao de IP](#anonimizacao-de-ip)) +- **ID de instalacao anonimo**: um UUID aleatorio gerado na primeira execucao — nao derivado de nenhum dado pessoal + +### Rastreamento de erros (Sentry) + +Quando a CLI falha (codigo de saida 2), enviamos: + +- **Tipo do erro, mensagem e stack trace** (caminhos de arquivos sao reduzidos para caminhos relativos como `src/cli.ts`) +- **Contexto de execucao**: SO, arquitetura, versao do Bun, versao do Archgate +- **ID de instalacao anonimo** (mesmo UUID aleatorio da analise) + +## O que NAO coletamos + +- **Nenhuma informacao pessoal**: nenhum nome de usuario, email, endereco IP ou identificador do GitHub +- **Nenhum conteudo de arquivo**: nenhum conteudo de ADR, codigo-fonte, nome de projeto ou caminho de arquivo +- **Nenhum contexto de IA**: nada de interacoes com agentes, prompts ou conteudo gerado por IA +- **Nenhum valor de flag**: registramos que `--json` foi usado, nao o conteudo da saida JSON +- **Nenhuma atividade de rede**: nenhuma URL, chave de API ou token + +## Anonimizacao de IP + +O Archgate usa a anonimizacao de IP nativa do PostHog: + +1. Sua CLI envia um evento ao PostHog com `$ip: null` +2. O PostHog resolve seu IP para um **pais e regiao** (ex: "BR", "Sao Paulo") no servidor +3. O endereco IP e entao **descartado** — nunca e armazenado no PostHog + +Para o rastreamento de erros do Sentry, o projeto tem **"Prevent Storing of IP Addresses"** habilitado, entao os IPs sao removidos antes do armazenamento. + +## Como desativar + +Voce pode desativar toda a telemetria (analise e rastreamento de erros) de duas formas: + +### Variavel de ambiente + +```bash +export ARCHGATE_TELEMETRY=0 +``` + +Valores aceitos: `0`, `false`, `no`, `off` (sem diferenciacao de maiusculas/minusculas). + +Adicione ao seu perfil de shell (`.bashrc`, `.zshrc`, etc.) para desativar permanentemente. + +### Comando da CLI + +```bash +archgate telemetry disable +``` + +Para reativar: + +```bash +archgate telemetry enable +``` + +Para verificar o status atual: + +```bash +archgate telemetry status +``` + +A variavel de ambiente tem prioridade sobre a configuracao da CLI. Se `ARCHGATE_TELEMETRY=0` estiver definida, a telemetria e desativada independentemente da configuracao da CLI. + +## Onde os dados sao armazenados + +- **Analise**: PostHog Cloud (regiao dos EUA). Dados retidos conforme a politica de retencao padrao do PostHog. +- **Rastreamento de erros**: Sentry Cloud (regiao dos EUA). Eventos de erro retidos por 90 dias. +- **Configuracao local**: `~/.archgate/config.json` armazena sua preferencia de telemetria e o ID de instalacao anonimo. + +## Aviso de primeira execucao + +Na primeira execucao, o Archgate exibe um aviso unico no stderr: + +``` +Archgate collects anonymous usage data to improve the CLI. +Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0 +Learn more: https://cli.archgate.dev/reference/telemetry +``` + +Este aviso e exibido uma unica vez e nao aparece novamente. + +## Codigo aberto + +A implementacao da telemetria e totalmente open source. Voce pode inspecionar exatamente quais dados sao coletados lendo: + +- [`src/helpers/telemetry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry.ts) — Rastreamento de eventos PostHog +- [`src/helpers/sentry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/sentry.ts) — Captura de erros Sentry +- [`src/helpers/telemetry-config.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry-config.ts) — Configuracao e logica de desativacao diff --git a/docs/src/content/docs/reference/telemetry.mdx b/docs/src/content/docs/reference/telemetry.mdx new file mode 100644 index 00000000..0c0f1b55 --- /dev/null +++ b/docs/src/content/docs/reference/telemetry.mdx @@ -0,0 +1,104 @@ +--- +title: Telemetry +description: What anonymous usage data Archgate collects, how to opt out, and our privacy commitments. +--- + +Archgate collects **anonymous usage data** to help us understand how the CLI is used, prioritize features, and fix crashes. This page explains exactly what is collected, what is not, and how to opt out. + +## What we collect + +### Usage analytics (PostHog) + +When you run an Archgate command, we record: + +- **Command name** and **flags used** (e.g., `check --json`, not flag values) +- **Exit code** (0, 1, or 2) and **execution duration** +- **Environment**: OS, architecture, Bun version, Archgate version, CI detection, TTY detection +- **Coarse location**: country and region (resolved server-side from your IP, then the IP is discarded — see [IP anonymization](#ip-anonymization)) +- **Anonymous install ID**: a random UUID generated on first run — not derived from any personal data + +### Error tracking (Sentry) + +When the CLI crashes (exit code 2), we send: + +- **Error type, message, and stack trace** (file paths are stripped to relative paths like `src/cli.ts`) +- **Runtime context**: OS, architecture, Bun version, Archgate version +- **Anonymous install ID** (same random UUID as analytics) + +## What we do NOT collect + +- **No personal information**: no usernames, emails, IP addresses, or GitHub identifiers +- **No file content**: no ADR content, source code, project names, or file paths +- **No prompt or AI context**: nothing from agent interactions, prompts, or AI-generated content +- **No flag values**: we record that `--json` was used, not what the JSON output contained +- **No network activity**: no URLs, API keys, or tokens + +## IP anonymization + +Archgate uses PostHog's built-in IP anonymization: + +1. Your CLI sends an event to PostHog with `$ip: null` +2. PostHog resolves your IP to a **country and region** (e.g., "US", "California") server-side +3. The IP address is then **discarded** — it is never stored in PostHog + +For Sentry error tracking, the project has **"Prevent Storing of IP Addresses"** enabled, so IPs are stripped before storage. + +## How to opt out + +You can disable all telemetry (both analytics and error tracking) in two ways: + +### Environment variable + +```bash +export ARCHGATE_TELEMETRY=0 +``` + +Accepted values: `0`, `false`, `no`, `off` (case-insensitive). + +Add this to your shell profile (`.bashrc`, `.zshrc`, etc.) to disable permanently. + +### CLI command + +```bash +archgate telemetry disable +``` + +To re-enable: + +```bash +archgate telemetry enable +``` + +To check current status: + +```bash +archgate telemetry status +``` + +The environment variable takes precedence over the CLI setting. If `ARCHGATE_TELEMETRY=0` is set, telemetry is disabled regardless of the CLI config. + +## Where data is stored + +- **Analytics**: PostHog Cloud (US region). Data retained per PostHog's standard retention policy. +- **Error tracking**: Sentry Cloud (US region). Error events retained for 90 days. +- **Local config**: `~/.archgate/config.json` stores your telemetry preference and anonymous install ID. + +## First-run notice + +On first run, Archgate displays a one-time notice to stderr: + +``` +Archgate collects anonymous usage data to improve the CLI. +Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0 +Learn more: https://cli.archgate.dev/reference/telemetry +``` + +This notice is shown once and does not appear again. + +## Open source + +The telemetry implementation is fully open source. You can inspect exactly what data is collected by reading: + +- [`src/helpers/telemetry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry.ts) — PostHog event tracking +- [`src/helpers/sentry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/sentry.ts) — Sentry error capture +- [`src/helpers/telemetry-config.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry-config.ts) — Config and opt-out logic diff --git a/src/cli.ts b/src/cli.ts index 67338adf..e00413a4 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,11 +11,19 @@ import { registerLoginCommand } from "./commands/login"; import { registerPluginCommand } from "./commands/plugin/index"; import { registerReviewContextCommand } from "./commands/review-context"; import { registerSessionContextCommand } from "./commands/session-context/index"; +import { registerTelemetryCommand } from "./commands/telemetry"; import { registerUpgradeCommand } from "./commands/upgrade"; import { installGit } from "./helpers/git"; import { logError } from "./helpers/log"; import { createPathIfNotExists, paths } from "./helpers/paths"; import { isSupportedPlatform } from "./helpers/platform"; +import { captureException, initSentry } from "./helpers/sentry"; +import { + flushTelemetry, + initTelemetry, + trackCommand, +} from "./helpers/telemetry"; +import { showTelemetryNotice } from "./helpers/telemetry-notice"; import { checkForUpdatesIfNeeded } from "./helpers/update-check"; if (typeof Bun === "undefined") @@ -34,11 +42,22 @@ createPathIfNotExists(paths.cacheFolder); async function main() { await installGit(); + // Initialize telemetry and error tracking (no-op if opted out) + initTelemetry(); + initSentry(); + showTelemetryNotice(); + const program = new Command() .name("archgate") .version(packageJson.version) .description("AI governance for software development"); + // Track which command is being executed + program.hook("preAction", (thisCommand) => { + const fullCommand = getFullCommandName(thisCommand); + trackCommand(fullCommand); + }); + registerInitCommand(program); registerLoginCommand(program); registerAdrCommand(program); @@ -48,6 +67,7 @@ async function main() { registerPluginCommand(program); registerUpgradeCommand(program); registerCleanCommand(program); + registerTelemetryCommand(program); const isUpgrade = process.argv.includes("upgrade"); const updateCheckPromise = isUpgrade @@ -56,9 +76,30 @@ async function main() { await program.parseAsync(process.argv); const notice = await updateCheckPromise; if (notice) console.log(notice); + + // Flush telemetry events (fire-and-forget, with timeout) + await flushTelemetry(); +} + +/** + * Reconstruct the full command name from Commander's command chain. + * E.g., "adr create" from the "create" subcommand of "adr". + */ +function getFullCommandName(command: Command): string { + const parts: string[] = []; + let current: Command | null = command; + while (current) { + const name = current.name(); + if (name && name !== "archgate") { + parts.unshift(name); + } + current = current.parent as Command | null; + } + return parts.join(" ") || "root"; } main().catch((err: unknown) => { + captureException(err, { command: "main" }); logError(String(err)); process.exit(2); }); diff --git a/src/commands/telemetry.ts b/src/commands/telemetry.ts new file mode 100644 index 00000000..d287d480 --- /dev/null +++ b/src/commands/telemetry.ts @@ -0,0 +1,76 @@ +import type { Command } from "@commander-js/extra-typings"; + +import { logError } from "../helpers/log"; +import { + isTelemetryEnabled, + isEnvTelemetryDisabled, + setTelemetryEnabled, +} from "../helpers/telemetry-config"; + +export function registerTelemetryCommand(program: Command) { + const telemetry = program + .command("telemetry") + .description("Manage anonymous usage data collection"); + + telemetry + .command("status") + .description("Show current telemetry status") + .action(() => { + const enabled = isTelemetryEnabled(); + const envOverride = isEnvTelemetryDisabled(); + + if (envOverride) { + console.log( + "Telemetry is disabled (ARCHGATE_TELEMETRY environment variable)." + ); + } else if (enabled) { + console.log("Telemetry is enabled."); + console.log( + "Anonymous usage data helps improve Archgate. No personal information is collected." + ); + console.log( + "\nTo disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0" + ); + console.log("Learn more: https://cli.archgate.dev/reference/telemetry"); + } else { + console.log("Telemetry is disabled."); + console.log("To enable: `archgate telemetry enable`"); + } + }); + + telemetry + .command("enable") + .description("Enable anonymous usage data collection") + .action(async () => { + try { + if (isEnvTelemetryDisabled()) { + console.log( + "Note: ARCHGATE_TELEMETRY environment variable is set to disable telemetry." + ); + console.log( + "Remove the environment variable for this setting to take effect." + ); + } + await setTelemetryEnabled(true); + console.log( + "Telemetry enabled. Thank you for helping improve Archgate." + ); + } catch (err) { + logError(err instanceof Error ? err.message : String(err)); + process.exit(1); + } + }); + + telemetry + .command("disable") + .description("Disable anonymous usage data collection") + .action(async () => { + try { + await setTelemetryEnabled(false); + console.log("Telemetry disabled. No usage data will be collected."); + } catch (err) { + logError(err instanceof Error ? err.message : String(err)); + process.exit(1); + } + }); +} diff --git a/src/helpers/sentry.ts b/src/helpers/sentry.ts new file mode 100644 index 00000000..defada94 --- /dev/null +++ b/src/helpers/sentry.ts @@ -0,0 +1,289 @@ +/** + * sentry.ts — Error tracking via Sentry HTTP envelope API. + * + * No SDK dependency — uses fetch() to POST error envelopes directly to + * Sentry's envelope endpoint. This keeps the binary lean (ARCH-006) while + * providing crash reporting for exit-code-2 errors. + * + * IP anonymization: the Sentry project has "Prevent Storing of IP Addresses" + * enabled server-side. The CLI also omits user.ip_address from the payload. + * + * All network calls are fire-and-forget: failures never affect CLI behavior. + */ + +import { logDebug } from "./log"; +import { getPlatformInfo } from "./platform"; +import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +/** + * Sentry DSN (write-only ingest URL, safe to embed in client code). + * Format: https://@/ + */ +const SENTRY_DSN = "https://placeholder@o0.ingest.us.sentry.io/0"; + +/** Maximum time to wait for the Sentry request (ms). */ +const SENTRY_TIMEOUT_MS = 5_000; + +// --------------------------------------------------------------------------- +// DSN parsing +// --------------------------------------------------------------------------- + +interface ParsedDSN { + publicKey: string; + host: string; + projectId: string; +} + +function parseDSN(dsn: string): ParsedDSN | null { + try { + const url = new URL(dsn); + const publicKey = url.username; + const projectId = url.pathname.replace("/", ""); + const host = `${url.protocol}//${url.host}`; + if (!publicKey || !projectId) return null; + return { publicKey, host, projectId }; + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- + +let initialized = false; +let parsedDSN: ParsedDSN | null = null; + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Initialize Sentry error tracking. Call once at CLI startup. + * No-op if telemetry is disabled. + */ +export function initSentry(): void { + if (!isTelemetryEnabled()) { + logDebug("Sentry disabled — telemetry is off"); + return; + } + + parsedDSN = parseDSN(SENTRY_DSN); + if (!parsedDSN) { + logDebug("Sentry disabled — invalid DSN"); + return; + } + + initialized = true; + logDebug("Sentry initialized"); +} + +/** + * Capture an exception and send it to Sentry. + * Fire-and-forget: errors are silently swallowed. + * + * @param error The error to capture + * @param context Optional extra context (command name, options, etc.) + */ +export function captureException( + error: unknown, + context?: Record +): void { + if (!initialized || !parsedDSN) return; + + const err = error instanceof Error ? error : new Error(String(error)); + + // Build and send envelope asynchronously — never block the CLI + sendErrorEnvelope(err, context).catch(() => { + logDebug("Sentry capture failed (silently ignored)"); + }); +} + +// --------------------------------------------------------------------------- +// Envelope construction +// --------------------------------------------------------------------------- + +function getCliVersion(): string { + try { + const pkg = require("../../package.json") as { version: string }; + return pkg.version; + } catch { + return "unknown"; + } +} + +/** + * Build and send a Sentry error envelope via the HTTP API. + * + * Envelope format: https://develop.sentry.dev/sdk/envelopes/ + * Each line is a JSON object, separated by newlines: + * 1. Envelope header (event_id, dsn, sent_at) + * 2. Item header (type, content_type, length) + * 3. Item payload (the error event) + */ +async function sendErrorEnvelope( + error: Error, + context?: Record +): Promise { + if (!parsedDSN) return; + + const eventId = generateEventId(); + const cliVersion = getCliVersion(); + const installId = getInstallId(); + const { runtime } = getPlatformInfo(); + + const event = { + event_id: eventId, + timestamp: Date.now() / 1000, + platform: "javascript", + level: "error", + release: `archgate@${cliVersion}`, + environment: process.env.NODE_ENV ?? "production", + server_name: undefined, // explicitly omit hostname + user: { + id: installId, + // No ip_address — server-side stripping handles this + }, + contexts: { + runtime: { + name: "bun", + // oxlint-disable-next-line no-negated-condition -- Bun availability check requires typeof guard + version: typeof Bun !== "undefined" ? Bun.version : "unknown", + }, + os: { name: runtime, machine: process.arch }, + cli: { + ...context, + is_ci: Boolean(process.env.CI), + is_tty: Boolean(process.stdout.isTTY), + }, + }, + exception: { + values: [ + { + type: error.name, + value: error.message, + stacktrace: error.stack ? parseStacktrace(error.stack) : undefined, + }, + ], + }, + tags: { cli_version: cliVersion, os: runtime, arch: process.arch }, + }; + + const eventJson = JSON.stringify(event); + + // Envelope: header \n item-header \n item-payload + const envelopeHeader = JSON.stringify({ + event_id: eventId, + dsn: SENTRY_DSN, + sent_at: new Date().toISOString(), + sdk: { name: "archgate-cli", version: cliVersion }, + }); + + const itemHeader = JSON.stringify({ + type: "event", + content_type: "application/json", + length: Buffer.byteLength(eventJson), + }); + + const envelope = `${envelopeHeader}\n${itemHeader}\n${eventJson}`; + + const envelopeUrl = `${parsedDSN.host}/api/${parsedDSN.projectId}/envelope/`; + + await fetch(envelopeUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-sentry-envelope", + "X-Sentry-Auth": `Sentry sentry_version=7, sentry_client=archgate-cli/${cliVersion}, sentry_key=${parsedDSN.publicKey}`, + }, + body: envelope, + signal: AbortSignal.timeout(SENTRY_TIMEOUT_MS), + }); + + logDebug("Sentry event sent:", eventId); +} + +// --------------------------------------------------------------------------- +// Stack trace parsing +// --------------------------------------------------------------------------- + +interface SentryFrame { + filename?: string; + function?: string; + lineno?: number; + colno?: number; + in_app?: boolean; +} + +/** + * Parse a V8/Bun stack trace string into Sentry frame format. + * Strips absolute paths to relative for privacy. + */ +function parseStacktrace(stack: string): { frames: SentryFrame[] } { + const frames: SentryFrame[] = []; + const lines = stack.split("\n"); + + for (const line of lines) { + const match = line.match(/^\s+at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?$/); + if (!match) continue; + + const [, fn, filepath, lineStr, colStr] = match; + + // Strip absolute paths — keep only the relative part from src/ or tests/ + const relPath = stripToRelativePath(filepath ?? ""); + + frames.push({ + filename: relPath, + function: fn ?? "", + lineno: Number(lineStr), + colno: Number(colStr), + in_app: relPath.startsWith("src/") || relPath.startsWith("tests/"), + }); + } + + // Sentry expects frames in caller-first order (reversed from stack trace) + return { frames: frames.reverse() }; +} + +/** + * Strip an absolute file path to a relative path from the project root. + * E.g., /home/user/project/src/cli.ts → src/cli.ts + */ +function stripToRelativePath(filepath: string): string { + // Match common project path segments + const markers = ["/src/", "/tests/", "/node_modules/"]; + for (const marker of markers) { + const idx = filepath.lastIndexOf(marker); + if (idx !== -1) return filepath.slice(idx + 1); + } + // Fallback: use the last path component + const lastSlash = Math.max( + filepath.lastIndexOf("/"), + filepath.lastIndexOf("\\") + ); + return lastSlash === -1 ? filepath : filepath.slice(lastSlash + 1); +} + +// --------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------- + +/** Generate a 32-character hex event ID (Sentry format). */ +function generateEventId(): string { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +} + +// --------------------------------------------------------------------------- +// Testing helpers +// --------------------------------------------------------------------------- + +/** Reset Sentry state. For testing only. */ +export function _resetSentry(): void { + initialized = false; + parsedDSN = null; +} diff --git a/src/helpers/telemetry-config.ts b/src/helpers/telemetry-config.ts new file mode 100644 index 00000000..9fa3d041 --- /dev/null +++ b/src/helpers/telemetry-config.ts @@ -0,0 +1,155 @@ +/** + * telemetry-config.ts — Manages telemetry preferences in ~/.archgate/config.json. + * + * Telemetry is opt-out: enabled by default, users disable via: + * - ARCHGATE_TELEMETRY=0 environment variable + * - `archgate telemetry disable` command + * + * An anonymous installId (UUID v4) is generated on first use for aggregate + * counting — it is not derived from any user data. + */ + +import { randomUUID } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; + +import { logDebug } from "./log"; +import { internalPath, createPathIfNotExists } from "./paths"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface TelemetryConfig { + /** Whether telemetry is enabled (default: true). */ + telemetry: boolean; + /** Random UUID generated on first use — not derived from any user data. */ + installId: string; + /** ISO date of first telemetry config creation. */ + createdAt: string; +} + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- + +const CONFIG_FILE = "config.json"; + +function configPath(): string { + return internalPath(CONFIG_FILE); +} + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- + +let cachedConfig: TelemetryConfig | null = null; + +// --------------------------------------------------------------------------- +// Environment variable override +// --------------------------------------------------------------------------- + +/** + * Returns true if the ARCHGATE_TELEMETRY env var explicitly disables telemetry. + * Accepted values: "0", "false", "no", "off" (case-insensitive). + */ +export function isEnvTelemetryDisabled(): boolean { + const envVal = process.env.ARCHGATE_TELEMETRY; + if (envVal === undefined) return false; + return ["0", "false", "no", "off"].includes(envVal.toLowerCase()); +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Returns true if telemetry should be active for this invocation. + * Checks env var first (fastest), then persisted config. + */ +export function isTelemetryEnabled(): boolean { + if (isEnvTelemetryDisabled()) return false; + const config = loadTelemetryConfig(); + return config.telemetry; +} + +/** + * Load or create the telemetry config from ~/.archgate/config.json. + * The result is cached per process. + */ +export function loadTelemetryConfig(): TelemetryConfig { + if (cachedConfig) return cachedConfig; + + // Synchronous read for startup-path performance + try { + const path = configPath(); + if (existsSync(path)) { + const text = readFileSync(path, "utf-8"); + const parsed = JSON.parse(text) as Partial; + if (parsed.installId && typeof parsed.telemetry === "boolean") { + cachedConfig = { + telemetry: parsed.telemetry, + installId: parsed.installId, + createdAt: parsed.createdAt ?? new Date().toISOString(), + }; + logDebug("Telemetry config loaded:", cachedConfig.installId); + return cachedConfig; + } + } + } catch { + // File doesn't exist or is malformed — create a new one + } + + // First run: generate fresh config + cachedConfig = { + telemetry: true, + installId: randomUUID(), + createdAt: new Date().toISOString(), + }; + + // Persist asynchronously — don't block CLI startup + saveTelemetryConfigAsync(cachedConfig); + logDebug("Telemetry config created:", cachedConfig.installId); + return cachedConfig; +} + +/** + * Update telemetry enabled/disabled state and persist to disk. + */ +export async function setTelemetryEnabled(enabled: boolean): Promise { + const config = loadTelemetryConfig(); + config.telemetry = enabled; + cachedConfig = config; + await saveTelemetryConfig(config); +} + +/** + * Get the anonymous install ID for this CLI installation. + */ +export function getInstallId(): string { + return loadTelemetryConfig().installId; +} + +// --------------------------------------------------------------------------- +// Persistence (internal) +// --------------------------------------------------------------------------- + +async function saveTelemetryConfig(config: TelemetryConfig): Promise { + createPathIfNotExists(internalPath()); + await Bun.write(configPath(), JSON.stringify(config, null, 2) + "\n"); + logDebug("Telemetry config saved"); +} + +function saveTelemetryConfigAsync(config: TelemetryConfig): void { + saveTelemetryConfig(config).catch(() => { + // Silently ignore — telemetry config persistence is best-effort + }); +} + +// --------------------------------------------------------------------------- +// Testing helpers +// --------------------------------------------------------------------------- + +/** Reset cached config. For testing only. */ +export function _resetConfigCache(): void { + cachedConfig = null; +} diff --git a/src/helpers/telemetry-notice.ts b/src/helpers/telemetry-notice.ts new file mode 100644 index 00000000..d236d118 --- /dev/null +++ b/src/helpers/telemetry-notice.ts @@ -0,0 +1,40 @@ +/** + * telemetry-notice.ts — One-time notice about telemetry on first CLI run. + * + * Shows a brief message to stderr (not stdout, per ARCH-003) informing users + * that anonymous data is collected and how to opt out. The notice is shown + * once, then a marker file is written to prevent repeat notices. + */ + +import { existsSync } from "node:fs"; + +import { logDebug, logWarn } from "./log"; +import { internalPath, createPathIfNotExists } from "./paths"; +import { isTelemetryEnabled } from "./telemetry-config"; + +const NOTICE_MARKER = "telemetry-notice-shown"; + +function markerPath(): string { + return internalPath(NOTICE_MARKER); +} + +/** + * Show a one-time telemetry notice on first run. + * Only shown if telemetry is enabled and the notice hasn't been shown before. + * Writes to stderr via logWarn() to avoid interfering with --json output (ARCH-003). + */ +export function showTelemetryNotice(): void { + if (!isTelemetryEnabled()) return; + if (existsSync(markerPath())) return; + + // Show notice to stderr via logWarn (diagnostics channel per ARCH-002/ARCH-003) + logWarn("Archgate collects anonymous usage data to improve the CLI."); + logWarn("Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0"); + logWarn("Learn more: https://cli.archgate.dev/reference/telemetry"); + + // Write marker asynchronously — don't block CLI startup + createPathIfNotExists(internalPath()); + Bun.write(markerPath(), new Date().toISOString()).catch(() => { + logDebug("Failed to write telemetry notice marker (ignored)"); + }); +} diff --git a/src/helpers/telemetry.ts b/src/helpers/telemetry.ts new file mode 100644 index 00000000..18716dc6 --- /dev/null +++ b/src/helpers/telemetry.ts @@ -0,0 +1,192 @@ +/** + * telemetry.ts — Anonymous usage analytics via PostHog HTTP API. + * + * No SDK dependency — uses fetch() directly to POST events to PostHog's + * capture endpoint. Events are buffered during command execution and flushed + * once at the end. All network calls are fire-and-forget: failures are + * silently ignored and never affect CLI behavior or exit codes. + * + * IP anonymization: PostHog resolves IP to country/region server-side, then + * the "Discard client IP data" project setting drops the IP from storage. + * The CLI sends `$ip: null` to explicitly signal PostHog not to store it. + * + * See https://cli.archgate.dev/reference/telemetry for the full privacy policy. + */ + +import { logDebug } from "./log"; +import { getPlatformInfo } from "./platform"; +import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +/** + * PostHog project API key (write-only, safe to embed in client code). + * This key can only ingest events — it cannot read data or manage the project. + */ +const POSTHOG_API_KEY = "phc_placeholder"; +const POSTHOG_HOST = "https://us.i.posthog.com"; +const CAPTURE_ENDPOINT = `${POSTHOG_HOST}/capture/`; + +/** Maximum time to wait for the flush request (ms). */ +const FLUSH_TIMEOUT_MS = 5_000; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface PostHogEvent { + event: string; + properties: Record; + timestamp: string; +} + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- + +let eventBuffer: PostHogEvent[] = []; +let initialized = false; +let distinctId = ""; + +// --------------------------------------------------------------------------- +// Shared properties (computed once per process) +// --------------------------------------------------------------------------- + +function getCommonProperties(): Record { + const { runtime } = getPlatformInfo(); + return { + $lib: "archgate-cli", + cli_version: getCliVersion(), + os: runtime, + arch: process.arch, + // oxlint-disable-next-line no-negated-condition -- Bun availability check requires typeof guard + bun_version: typeof Bun !== "undefined" ? Bun.version : "unknown", + is_ci: Boolean(process.env.CI), + is_tty: Boolean(process.stdout.isTTY), + node_env: process.env.NODE_ENV ?? "production", + // Signal PostHog to resolve geo then discard the IP + $ip: null, + }; +} + +function getCliVersion(): string { + try { + // Lazy import to avoid circular dependency with cli.ts + // eslint-disable-next-line @typescript-eslint/no-require-imports + const pkg = require("../../package.json") as { version: string }; + return pkg.version; + } catch { + return "unknown"; + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Initialize telemetry. Call once at CLI startup. + * If telemetry is disabled, this is a no-op and all subsequent calls are too. + */ +export function initTelemetry(): void { + if (!isTelemetryEnabled()) { + logDebug("Telemetry disabled — skipping init"); + return; + } + + distinctId = getInstallId(); + initialized = true; + logDebug("Telemetry initialized:", distinctId); +} + +/** + * Track a named event with optional properties. + * Events are buffered and sent on flush(). No-op if telemetry is disabled. + */ +export function trackEvent( + event: string, + properties?: Record +): void { + if (!initialized) return; + + eventBuffer.push({ + event, + properties: { + ...getCommonProperties(), + ...properties, + distinct_id: distinctId, + }, + timestamp: new Date().toISOString(), + }); + + logDebug("Telemetry event buffered:", event); +} + +/** + * Track a CLI command invocation. + */ +export function trackCommand( + command: string, + options?: Record +): void { + trackEvent("command_executed", { command, ...options }); +} + +/** + * Track command completion with exit code and duration. + */ +export function trackCommandResult( + command: string, + exitCode: number, + durationMs: number +): void { + trackEvent("command_completed", { + command, + exit_code: exitCode, + duration_ms: durationMs, + }); +} + +/** + * Flush all buffered events to PostHog. + * Fire-and-forget: errors are silently swallowed. + * Returns a promise that resolves when the flush attempt completes. + */ +export async function flushTelemetry(): Promise { + if (!initialized || eventBuffer.length === 0) return; + + const events = eventBuffer; + eventBuffer = []; + + try { + logDebug(`Flushing ${events.length} telemetry event(s)`); + + await fetch(CAPTURE_ENDPOINT, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch: events }), + signal: AbortSignal.timeout(FLUSH_TIMEOUT_MS), + }); + } catch { + // Silently ignore — telemetry must never affect CLI behavior + logDebug("Telemetry flush failed (silently ignored)"); + } +} + +// --------------------------------------------------------------------------- +// Testing helpers +// --------------------------------------------------------------------------- + +/** Reset telemetry state. For testing only. */ +export function _resetTelemetry(): void { + eventBuffer = []; + initialized = false; + distinctId = ""; +} + +/** Get buffered events. For testing only. */ +export function _getEventBuffer(): PostHogEvent[] { + return eventBuffer; +} diff --git a/tests/helpers/sentry.test.ts b/tests/helpers/sentry.test.ts new file mode 100644 index 00000000..4d98b0a0 --- /dev/null +++ b/tests/helpers/sentry.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +/** Type-safe fetch mock — Bun's fetch type includes `preconnect`. */ +function mockFetch(handler: () => Response) { + globalThis.fetch = mock(handler) as unknown as typeof fetch; +} + +describe("sentry", () => { + let tempDir: string; + let originalHome: string | undefined; + let originalTelemetryEnv: string | undefined; + const originalFetch = globalThis.fetch; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-sentry-test-")); + originalHome = process.env.HOME; + originalTelemetryEnv = process.env.ARCHGATE_TELEMETRY; + process.env.HOME = tempDir; + delete process.env.ARCHGATE_TELEMETRY; + }); + + afterEach(async () => { + process.env.HOME = originalHome; + if (originalTelemetryEnv === undefined) { + delete process.env.ARCHGATE_TELEMETRY; + } else { + process.env.ARCHGATE_TELEMETRY = originalTelemetryEnv; + } + rmSync(tempDir, { recursive: true, force: true }); + globalThis.fetch = originalFetch; + mock.restore(); + + const { _resetSentry } = await import("../../src/helpers/sentry"); + _resetSentry(); + const { _resetConfigCache } = + await import("../../src/helpers/telemetry-config"); + _resetConfigCache(); + }); + + describe("initSentry", () => { + test("initializes when telemetry is enabled", async () => { + mockFetch(() => new Response("OK")); + const fetchMock = globalThis.fetch as unknown as ReturnType; + + const { initSentry, captureException } = + await import("../../src/helpers/sentry"); + + initSentry(); + + // captureException should attempt to send (fire-and-forget) + captureException(new Error("test error"), { command: "check" }); + + // Give async fire-and-forget time to execute + await Bun.sleep(100); + + // The fetch would have been called if the DSN was valid. + // With placeholder DSN, initSentry parses it and initialized=true, + // so captureException fires the async send. + expect(fetchMock).toHaveBeenCalled(); + }); + + test("does not initialize when telemetry is disabled", async () => { + process.env.ARCHGATE_TELEMETRY = "0"; + mockFetch(() => new Response("OK")); + const fetchMock = globalThis.fetch as unknown as ReturnType; + + const { initSentry, captureException } = + await import("../../src/helpers/sentry"); + + initSentry(); + captureException(new Error("should not send")); + + await Bun.sleep(100); + expect(fetchMock).not.toHaveBeenCalled(); + }); + }); + + describe("captureException", () => { + test("silently ignores fetch failures", async () => { + mockFetch(() => { + throw new Error("Network error"); + }); + + const { initSentry, captureException } = + await import("../../src/helpers/sentry"); + + initSentry(); + // Should not throw + captureException(new Error("test"), { command: "check" }); + await Bun.sleep(100); + }); + + test("handles non-Error values", async () => { + mockFetch(() => new Response("OK")); + const fetchMock = globalThis.fetch as unknown as ReturnType; + + const { initSentry, captureException } = + await import("../../src/helpers/sentry"); + + initSentry(); + captureException("string error", { command: "init" }); + await Bun.sleep(100); + + expect(fetchMock).toHaveBeenCalled(); + }); + + test("is a no-op when not initialized", async () => { + mockFetch(() => new Response("OK")); + const fetchMock = globalThis.fetch as unknown as ReturnType; + + const { captureException } = await import("../../src/helpers/sentry"); + + captureException(new Error("should not send")); + await Bun.sleep(100); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/tests/helpers/telemetry-config.test.ts b/tests/helpers/telemetry-config.test.ts new file mode 100644 index 00000000..81b67075 --- /dev/null +++ b/tests/helpers/telemetry-config.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, test, beforeEach, afterEach } from "bun:test"; +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +describe("telemetry-config", () => { + let tempDir: string; + let originalHome: string | undefined; + let originalTelemetryEnv: string | undefined; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-telemetry-test-")); + originalHome = process.env.HOME; + originalTelemetryEnv = process.env.ARCHGATE_TELEMETRY; + process.env.HOME = tempDir; + delete process.env.ARCHGATE_TELEMETRY; + }); + + afterEach(async () => { + process.env.HOME = originalHome; + if (originalTelemetryEnv === undefined) { + delete process.env.ARCHGATE_TELEMETRY; + } else { + process.env.ARCHGATE_TELEMETRY = originalTelemetryEnv; + } + rmSync(tempDir, { recursive: true, force: true }); + + // Reset cached config between tests + const { _resetConfigCache } = + await import("../../src/helpers/telemetry-config"); + _resetConfigCache(); + }); + + describe("loadTelemetryConfig", () => { + test("creates config with telemetry enabled on first run", async () => { + const { loadTelemetryConfig } = + await import("../../src/helpers/telemetry-config"); + + const config = loadTelemetryConfig(); + expect(config.telemetry).toBe(true); + expect(config.installId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + ); + expect(config.createdAt).toBeTruthy(); + }); + + test("returns cached config on subsequent calls", async () => { + const { loadTelemetryConfig } = + await import("../../src/helpers/telemetry-config"); + + const first = loadTelemetryConfig(); + const second = loadTelemetryConfig(); + expect(first).toBe(second); // Same reference (cached) + }); + + test("reads existing config from disk", async () => { + const { mkdirSync } = await import("node:fs"); + const configDir = join(tempDir, ".archgate"); + mkdirSync(configDir, { recursive: true }); + await Bun.write( + join(configDir, "config.json"), + JSON.stringify({ + telemetry: false, + installId: "test-uuid-1234", + createdAt: "2026-01-01T00:00:00.000Z", + }) + ); + + const { loadTelemetryConfig } = + await import("../../src/helpers/telemetry-config"); + + const config = loadTelemetryConfig(); + expect(config.telemetry).toBe(false); + expect(config.installId).toBe("test-uuid-1234"); + }); + + test("creates new config when file is malformed", async () => { + const { mkdirSync } = await import("node:fs"); + const configDir = join(tempDir, ".archgate"); + mkdirSync(configDir, { recursive: true }); + await Bun.write(join(configDir, "config.json"), "not-json"); + + const { loadTelemetryConfig } = + await import("../../src/helpers/telemetry-config"); + + const config = loadTelemetryConfig(); + expect(config.telemetry).toBe(true); + expect(config.installId).toBeTruthy(); + }); + }); + + describe("isEnvTelemetryDisabled", () => { + test("returns false when env var is not set", async () => { + const { isEnvTelemetryDisabled } = + await import("../../src/helpers/telemetry-config"); + expect(isEnvTelemetryDisabled()).toBe(false); + }); + + test.each(["0", "false", "no", "off", "FALSE", "No", "OFF"])( + "returns true for %s", + async (value) => { + process.env.ARCHGATE_TELEMETRY = value; + const { isEnvTelemetryDisabled } = + await import("../../src/helpers/telemetry-config"); + expect(isEnvTelemetryDisabled()).toBe(true); + } + ); + + test("returns false for other values like '1' or 'true'", async () => { + process.env.ARCHGATE_TELEMETRY = "1"; + const { isEnvTelemetryDisabled } = + await import("../../src/helpers/telemetry-config"); + expect(isEnvTelemetryDisabled()).toBe(false); + }); + }); + + describe("isTelemetryEnabled", () => { + test("returns true by default", async () => { + const { isTelemetryEnabled } = + await import("../../src/helpers/telemetry-config"); + expect(isTelemetryEnabled()).toBe(true); + }); + + test("returns false when env var disables it", async () => { + process.env.ARCHGATE_TELEMETRY = "0"; + const { isTelemetryEnabled } = + await import("../../src/helpers/telemetry-config"); + expect(isTelemetryEnabled()).toBe(false); + }); + + test("returns false when config disables it", async () => { + const { mkdirSync } = await import("node:fs"); + const configDir = join(tempDir, ".archgate"); + mkdirSync(configDir, { recursive: true }); + await Bun.write( + join(configDir, "config.json"), + JSON.stringify({ + telemetry: false, + installId: "test-uuid", + createdAt: "2026-01-01", + }) + ); + + const { isTelemetryEnabled } = + await import("../../src/helpers/telemetry-config"); + expect(isTelemetryEnabled()).toBe(false); + }); + }); + + describe("setTelemetryEnabled", () => { + test("persists disabled state to disk", async () => { + const { setTelemetryEnabled, loadTelemetryConfig } = + await import("../../src/helpers/telemetry-config"); + + // Initialize first + loadTelemetryConfig(); + + // Wait for async first-run save + await Bun.sleep(100); + + await setTelemetryEnabled(false); + + // Read from disk to verify persistence + const configPath = join(tempDir, ".archgate", "config.json"); + const raw = readFileSync(configPath, "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.telemetry).toBe(false); + }); + + test("persists enabled state to disk", async () => { + const { setTelemetryEnabled, loadTelemetryConfig } = + await import("../../src/helpers/telemetry-config"); + + loadTelemetryConfig(); + await Bun.sleep(100); + + await setTelemetryEnabled(false); + await setTelemetryEnabled(true); + + const configPath = join(tempDir, ".archgate", "config.json"); + const raw = readFileSync(configPath, "utf-8"); + const parsed = JSON.parse(raw); + expect(parsed.telemetry).toBe(true); + }); + }); + + describe("getInstallId", () => { + test("returns the same ID across calls", async () => { + const { getInstallId } = + await import("../../src/helpers/telemetry-config"); + + const id1 = getInstallId(); + const id2 = getInstallId(); + expect(id1).toBe(id2); + }); + }); +}); diff --git a/tests/helpers/telemetry-notice.test.ts b/tests/helpers/telemetry-notice.test.ts new file mode 100644 index 00000000..d4bff522 --- /dev/null +++ b/tests/helpers/telemetry-notice.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test, beforeEach, afterEach } from "bun:test"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +describe("telemetry-notice", () => { + let tempDir: string; + let originalHome: string | undefined; + let originalTelemetryEnv: string | undefined; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-notice-test-")); + originalHome = process.env.HOME; + originalTelemetryEnv = process.env.ARCHGATE_TELEMETRY; + process.env.HOME = tempDir; + delete process.env.ARCHGATE_TELEMETRY; + }); + + afterEach(async () => { + process.env.HOME = originalHome; + if (originalTelemetryEnv === undefined) { + delete process.env.ARCHGATE_TELEMETRY; + } else { + process.env.ARCHGATE_TELEMETRY = originalTelemetryEnv; + } + rmSync(tempDir, { recursive: true, force: true }); + + const { _resetConfigCache } = + await import("../../src/helpers/telemetry-config"); + _resetConfigCache(); + }); + + test("creates marker file after showing notice", async () => { + const { showTelemetryNotice } = + await import("../../src/helpers/telemetry-notice"); + + showTelemetryNotice(); + + // Wait for async marker file write + await Bun.sleep(200); + + const markerPath = join(tempDir, ".archgate", "telemetry-notice-shown"); + expect(existsSync(markerPath)).toBe(true); + }); + + test("does not show notice when telemetry is disabled", async () => { + process.env.ARCHGATE_TELEMETRY = "0"; + + const { showTelemetryNotice } = + await import("../../src/helpers/telemetry-notice"); + + showTelemetryNotice(); + await Bun.sleep(200); + + const markerPath = join(tempDir, ".archgate", "telemetry-notice-shown"); + expect(existsSync(markerPath)).toBe(false); + }); + + test("does not show notice when marker file exists", async () => { + const { mkdirSync } = await import("node:fs"); + const archgateDir = join(tempDir, ".archgate"); + mkdirSync(archgateDir, { recursive: true }); + await Bun.write( + join(archgateDir, "telemetry-notice-shown"), + "already-shown" + ); + + const { showTelemetryNotice } = + await import("../../src/helpers/telemetry-notice"); + + // Should be a no-op (no error, no duplicate marker write) + showTelemetryNotice(); + }); +}); diff --git a/tests/helpers/telemetry.test.ts b/tests/helpers/telemetry.test.ts new file mode 100644 index 00000000..34c2d836 --- /dev/null +++ b/tests/helpers/telemetry.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +/** Type-safe fetch mock — Bun's fetch type includes `preconnect`. */ +function mockFetch(handler: () => Response) { + globalThis.fetch = mock(handler) as unknown as typeof fetch; +} + +describe("telemetry", () => { + let tempDir: string; + let originalHome: string | undefined; + let originalTelemetryEnv: string | undefined; + const originalFetch = globalThis.fetch; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-telemetry-test-")); + originalHome = process.env.HOME; + originalTelemetryEnv = process.env.ARCHGATE_TELEMETRY; + process.env.HOME = tempDir; + delete process.env.ARCHGATE_TELEMETRY; + }); + + afterEach(async () => { + process.env.HOME = originalHome; + if (originalTelemetryEnv === undefined) { + delete process.env.ARCHGATE_TELEMETRY; + } else { + process.env.ARCHGATE_TELEMETRY = originalTelemetryEnv; + } + rmSync(tempDir, { recursive: true, force: true }); + globalThis.fetch = originalFetch; + mock.restore(); + + const { _resetTelemetry } = await import("../../src/helpers/telemetry"); + _resetTelemetry(); + const { _resetConfigCache } = + await import("../../src/helpers/telemetry-config"); + _resetConfigCache(); + }); + + describe("initTelemetry", () => { + test("initializes when telemetry is enabled", async () => { + const { initTelemetry, trackEvent, _getEventBuffer } = + await import("../../src/helpers/telemetry"); + + initTelemetry(); + trackEvent("test_event", { key: "value" }); + + const buffer = _getEventBuffer(); + expect(buffer).toHaveLength(1); + expect(buffer[0].event).toBe("test_event"); + }); + + test("skips init when telemetry is disabled via env", async () => { + process.env.ARCHGATE_TELEMETRY = "0"; + + const { initTelemetry, trackEvent, _getEventBuffer } = + await import("../../src/helpers/telemetry"); + + initTelemetry(); + trackEvent("test_event"); + + expect(_getEventBuffer()).toHaveLength(0); + }); + }); + + describe("trackEvent", () => { + test("buffers events with common properties", async () => { + const { initTelemetry, trackEvent, _getEventBuffer } = + await import("../../src/helpers/telemetry"); + + initTelemetry(); + trackEvent("command_executed", { command: "check" }); + + const buffer = _getEventBuffer(); + expect(buffer).toHaveLength(1); + expect(buffer[0].properties).toMatchObject({ + $lib: "archgate-cli", + os: process.platform, + arch: process.arch, + command: "check", + $ip: null, + }); + expect(buffer[0].timestamp).toBeTruthy(); + }); + + test("is a no-op when not initialized", async () => { + const { trackEvent, _getEventBuffer } = + await import("../../src/helpers/telemetry"); + + trackEvent("should_not_buffer"); + expect(_getEventBuffer()).toHaveLength(0); + }); + }); + + describe("trackCommand", () => { + test("buffers a command_executed event", async () => { + const { initTelemetry, trackCommand, _getEventBuffer } = + await import("../../src/helpers/telemetry"); + + initTelemetry(); + trackCommand("adr create", { json: true }); + + const buffer = _getEventBuffer(); + expect(buffer).toHaveLength(1); + expect(buffer[0].event).toBe("command_executed"); + expect(buffer[0].properties.command).toBe("adr create"); + expect(buffer[0].properties.json).toBe(true); + }); + }); + + describe("flushTelemetry", () => { + test("sends buffered events to PostHog and clears buffer", async () => { + mockFetch(() => new Response("OK", { status: 200 })); + const fetchMock = globalThis.fetch as unknown as ReturnType; + + const { initTelemetry, trackEvent, flushTelemetry, _getEventBuffer } = + await import("../../src/helpers/telemetry"); + + initTelemetry(); + trackEvent("test_event_1"); + trackEvent("test_event_2"); + + await flushTelemetry(); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(_getEventBuffer()).toHaveLength(0); + }); + + test("is a no-op when buffer is empty", async () => { + mockFetch(() => new Response("OK")); + const fetchMock = globalThis.fetch as unknown as ReturnType; + + const { initTelemetry, flushTelemetry } = + await import("../../src/helpers/telemetry"); + + initTelemetry(); + await flushTelemetry(); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + + test("silently ignores fetch failures", async () => { + mockFetch(() => { + throw new Error("Network error"); + }); + + const { initTelemetry, trackEvent, flushTelemetry } = + await import("../../src/helpers/telemetry"); + + initTelemetry(); + trackEvent("test_event"); + + // Should not throw + await flushTelemetry(); + }); + }); +}); From ee1ef769f4d50008883da19144f1a68224f2383c Mon Sep 17 00:00:00 2001 From: rhuanbarreto <283004+rhuanbarreto@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:12:02 +0000 Subject: [PATCH 02/13] docs: regenerate llms-full.txt --- docs/public/llms-full.txt | 106 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index b75b35f5..ae77dd0d 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -3798,6 +3798,112 @@ type RuleSet = { rules: Record }; --- +## Reference: Telemetry + +Source: https://cli.archgate.dev/reference/telemetry/ + +Archgate collects **anonymous usage data** to help us understand how the CLI is used, prioritize features, and fix crashes. This page explains exactly what is collected, what is not, and how to opt out. + +## What we collect + +### Usage analytics (PostHog) + +When you run an Archgate command, we record: + +- **Command name** and **flags used** (e.g., `check --json`, not flag values) +- **Exit code** (0, 1, or 2) and **execution duration** +- **Environment**: OS, architecture, Bun version, Archgate version, CI detection, TTY detection +- **Coarse location**: country and region (resolved server-side from your IP, then the IP is discarded — see [IP anonymization](#ip-anonymization)) +- **Anonymous install ID**: a random UUID generated on first run — not derived from any personal data + +### Error tracking (Sentry) + +When the CLI crashes (exit code 2), we send: + +- **Error type, message, and stack trace** (file paths are stripped to relative paths like `src/cli.ts`) +- **Runtime context**: OS, architecture, Bun version, Archgate version +- **Anonymous install ID** (same random UUID as analytics) + +## What we do NOT collect + +- **No personal information**: no usernames, emails, IP addresses, or GitHub identifiers +- **No file content**: no ADR content, source code, project names, or file paths +- **No prompt or AI context**: nothing from agent interactions, prompts, or AI-generated content +- **No flag values**: we record that `--json` was used, not what the JSON output contained +- **No network activity**: no URLs, API keys, or tokens + +## IP anonymization + +Archgate uses PostHog's built-in IP anonymization: + +1. Your CLI sends an event to PostHog with `$ip: null` +2. PostHog resolves your IP to a **country and region** (e.g., "US", "California") server-side +3. The IP address is then **discarded** — it is never stored in PostHog + +For Sentry error tracking, the project has **"Prevent Storing of IP Addresses"** enabled, so IPs are stripped before storage. + +## How to opt out + +You can disable all telemetry (both analytics and error tracking) in two ways: + +### Environment variable + +```bash +export ARCHGATE_TELEMETRY=0 +``` + +Accepted values: `0`, `false`, `no`, `off` (case-insensitive). + +Add this to your shell profile (`.bashrc`, `.zshrc`, etc.) to disable permanently. + +### CLI command + +```bash +archgate telemetry disable +``` + +To re-enable: + +```bash +archgate telemetry enable +``` + +To check current status: + +```bash +archgate telemetry status +``` + +The environment variable takes precedence over the CLI setting. If `ARCHGATE_TELEMETRY=0` is set, telemetry is disabled regardless of the CLI config. + +## Where data is stored + +- **Analytics**: PostHog Cloud (US region). Data retained per PostHog's standard retention policy. +- **Error tracking**: Sentry Cloud (US region). Error events retained for 90 days. +- **Local config**: `~/.archgate/config.json` stores your telemetry preference and anonymous install ID. + +## First-run notice + +On first run, Archgate displays a one-time notice to stderr: + +``` +Archgate collects anonymous usage data to improve the CLI. +Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0 +Learn more: https://cli.archgate.dev/reference/telemetry +``` + +This notice is shown once and does not appear again. + +## Open source + +The telemetry implementation is fully open source. You can inspect exactly what data is collected by reading: + +- [`src/helpers/telemetry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry.ts) — PostHog event tracking +- [`src/helpers/sentry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/sentry.ts) — Sentry error capture +- [`src/helpers/telemetry-config.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry-config.ts) — Config and opt-out logic + +--- + ## Examples: clean-architecture-layers Source: https://cli.archgate.dev/examples/clean-architecture-layers/ From 887ae26e90071d29979e94f95ba48aa7dcdf90bb Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 02:15:06 +0100 Subject: [PATCH 03/13] fix(docs): add missing diacritics to pt-BR telemetry page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add proper Portuguese diacritical marks (ã, é, ê, ç, í, ó, ú, etc.) throughout the pt-BR telemetry docs page. --- .../docs/pt-br/reference/telemetry.mdx | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/src/content/docs/pt-br/reference/telemetry.mdx b/docs/src/content/docs/pt-br/reference/telemetry.mdx index c867a130..024bd880 100644 --- a/docs/src/content/docs/pt-br/reference/telemetry.mdx +++ b/docs/src/content/docs/pt-br/reference/telemetry.mdx @@ -1,59 +1,59 @@ --- title: Telemetria -description: Quais dados de uso anonimos o Archgate coleta, como desativar e nossos compromissos de privacidade. +description: Quais dados de uso anônimos o Archgate coleta, como desativar e nossos compromissos de privacidade. --- -O Archgate coleta **dados de uso anonimos** para nos ajudar a entender como a CLI e utilizada, priorizar funcionalidades e corrigir falhas. Esta pagina explica exatamente o que e coletado, o que nao e, e como desativar. +O Archgate coleta **dados de uso anônimos** para nos ajudar a entender como a CLI é utilizada, priorizar funcionalidades e corrigir falhas. Esta página explica exatamente o que é coletado, o que não é, e como desativar. ## O que coletamos -### Analise de uso (PostHog) +### Análise de uso (PostHog) -Quando voce executa um comando do Archgate, registramos: +Quando você executa um comando do Archgate, registramos: -- **Nome do comando** e **flags utilizadas** (ex: `check --json`, nao os valores das flags) -- **Codigo de saida** (0, 1 ou 2) e **duracao da execucao** -- **Ambiente**: SO, arquitetura, versao do Bun, versao do Archgate, deteccao de CI, deteccao de TTY -- **Localizacao aproximada**: pais e regiao (resolvidos no servidor a partir do seu IP, que e descartado em seguida — veja [Anonimizacao de IP](#anonimizacao-de-ip)) -- **ID de instalacao anonimo**: um UUID aleatorio gerado na primeira execucao — nao derivado de nenhum dado pessoal +- **Nome do comando** e **flags utilizadas** (ex: `check --json`, não os valores das flags) +- **Código de saída** (0, 1 ou 2) e **duração da execução** +- **Ambiente**: SO, arquitetura, versão do Bun, versão do Archgate, detecção de CI, detecção de TTY +- **Localização aproximada**: país e região (resolvidos no servidor a partir do seu IP, que é descartado em seguida — veja [Anonimização de IP](#anonimização-de-ip)) +- **ID de instalação anônimo**: um UUID aleatório gerado na primeira execução — não derivado de nenhum dado pessoal ### Rastreamento de erros (Sentry) -Quando a CLI falha (codigo de saida 2), enviamos: +Quando a CLI falha (código de saída 2), enviamos: -- **Tipo do erro, mensagem e stack trace** (caminhos de arquivos sao reduzidos para caminhos relativos como `src/cli.ts`) -- **Contexto de execucao**: SO, arquitetura, versao do Bun, versao do Archgate -- **ID de instalacao anonimo** (mesmo UUID aleatorio da analise) +- **Tipo do erro, mensagem e stack trace** (caminhos de arquivos são reduzidos para caminhos relativos como `src/cli.ts`) +- **Contexto de execução**: SO, arquitetura, versão do Bun, versão do Archgate +- **ID de instalação anônimo** (mesmo UUID aleatório da análise) -## O que NAO coletamos +## O que NÃO coletamos -- **Nenhuma informacao pessoal**: nenhum nome de usuario, email, endereco IP ou identificador do GitHub -- **Nenhum conteudo de arquivo**: nenhum conteudo de ADR, codigo-fonte, nome de projeto ou caminho de arquivo -- **Nenhum contexto de IA**: nada de interacoes com agentes, prompts ou conteudo gerado por IA -- **Nenhum valor de flag**: registramos que `--json` foi usado, nao o conteudo da saida JSON +- **Nenhuma informação pessoal**: nenhum nome de usuário, email, endereço IP ou identificador do GitHub +- **Nenhum conteúdo de arquivo**: nenhum conteúdo de ADR, código-fonte, nome de projeto ou caminho de arquivo +- **Nenhum contexto de IA**: nada de interações com agentes, prompts ou conteúdo gerado por IA +- **Nenhum valor de flag**: registramos que `--json` foi usado, não o conteúdo da saída JSON - **Nenhuma atividade de rede**: nenhuma URL, chave de API ou token -## Anonimizacao de IP +## Anonimização de IP -O Archgate usa a anonimizacao de IP nativa do PostHog: +O Archgate usa a anonimização de IP nativa do PostHog: 1. Sua CLI envia um evento ao PostHog com `$ip: null` -2. O PostHog resolve seu IP para um **pais e regiao** (ex: "BR", "Sao Paulo") no servidor -3. O endereco IP e entao **descartado** — nunca e armazenado no PostHog +2. O PostHog resolve seu IP para um **país e região** (ex: "BR", "São Paulo") no servidor +3. O endereço IP é então **descartado** — nunca é armazenado no PostHog -Para o rastreamento de erros do Sentry, o projeto tem **"Prevent Storing of IP Addresses"** habilitado, entao os IPs sao removidos antes do armazenamento. +Para o rastreamento de erros do Sentry, o projeto tem **"Prevent Storing of IP Addresses"** habilitado, então os IPs são removidos antes do armazenamento. ## Como desativar -Voce pode desativar toda a telemetria (analise e rastreamento de erros) de duas formas: +Você pode desativar toda a telemetria (análise e rastreamento de erros) de duas formas: -### Variavel de ambiente +### Variável de ambiente ```bash export ARCHGATE_TELEMETRY=0 ``` -Valores aceitos: `0`, `false`, `no`, `off` (sem diferenciacao de maiusculas/minusculas). +Valores aceitos: `0`, `false`, `no`, `off` (sem diferenciação de maiúsculas/minúsculas). Adicione ao seu perfil de shell (`.bashrc`, `.zshrc`, etc.) para desativar permanentemente. @@ -75,17 +75,17 @@ Para verificar o status atual: archgate telemetry status ``` -A variavel de ambiente tem prioridade sobre a configuracao da CLI. Se `ARCHGATE_TELEMETRY=0` estiver definida, a telemetria e desativada independentemente da configuracao da CLI. +A variável de ambiente tem prioridade sobre a configuração da CLI. Se `ARCHGATE_TELEMETRY=0` estiver definida, a telemetria é desativada independentemente da configuração da CLI. -## Onde os dados sao armazenados +## Onde os dados são armazenados -- **Analise**: PostHog Cloud (regiao dos EUA). Dados retidos conforme a politica de retencao padrao do PostHog. -- **Rastreamento de erros**: Sentry Cloud (regiao dos EUA). Eventos de erro retidos por 90 dias. -- **Configuracao local**: `~/.archgate/config.json` armazena sua preferencia de telemetria e o ID de instalacao anonimo. +- **Análise**: PostHog Cloud (região dos EUA). Dados retidos conforme a política de retenção padrão do PostHog. +- **Rastreamento de erros**: Sentry Cloud (região dos EUA). Eventos de erro retidos por 90 dias. +- **Configuração local**: `~/.archgate/config.json` armazena sua preferência de telemetria e o ID de instalação anônimo. -## Aviso de primeira execucao +## Aviso de primeira execução -Na primeira execucao, o Archgate exibe um aviso unico no stderr: +Na primeira execução, o Archgate exibe um aviso único no stderr: ``` Archgate collects anonymous usage data to improve the CLI. @@ -93,12 +93,12 @@ Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0 Learn more: https://cli.archgate.dev/reference/telemetry ``` -Este aviso e exibido uma unica vez e nao aparece novamente. +Este aviso é exibido uma única vez e não aparece novamente. -## Codigo aberto +## Código aberto -A implementacao da telemetria e totalmente open source. Voce pode inspecionar exatamente quais dados sao coletados lendo: +A implementação da telemetria é totalmente open source. Você pode inspecionar exatamente quais dados são coletados lendo: - [`src/helpers/telemetry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry.ts) — Rastreamento de eventos PostHog - [`src/helpers/sentry.ts`](https://github.com/archgate/cli/blob/main/src/helpers/sentry.ts) — Captura de erros Sentry -- [`src/helpers/telemetry-config.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry-config.ts) — Configuracao e logica de desativacao +- [`src/helpers/telemetry-config.ts`](https://github.com/archgate/cli/blob/main/src/helpers/telemetry-config.ts) — Configuração e lógica de desativação From e413c38ce0ec12a101e7c9dd6e93a4bde2902462 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 02:18:33 +0100 Subject: [PATCH 04/13] refactor: remove first-run telemetry disclaimer Telemetry is documented in the docs site instead of showing a CLI notice. --- .../docs/pt-br/reference/telemetry.mdx | 12 --- docs/src/content/docs/reference/telemetry.mdx | 12 --- src/cli.ts | 2 - src/helpers/telemetry-notice.ts | 40 ---------- tests/helpers/telemetry-notice.test.ts | 74 ------------------- 5 files changed, 140 deletions(-) delete mode 100644 src/helpers/telemetry-notice.ts delete mode 100644 tests/helpers/telemetry-notice.test.ts diff --git a/docs/src/content/docs/pt-br/reference/telemetry.mdx b/docs/src/content/docs/pt-br/reference/telemetry.mdx index 024bd880..d21c2e85 100644 --- a/docs/src/content/docs/pt-br/reference/telemetry.mdx +++ b/docs/src/content/docs/pt-br/reference/telemetry.mdx @@ -83,18 +83,6 @@ A variável de ambiente tem prioridade sobre a configuração da CLI. Se `ARCHGA - **Rastreamento de erros**: Sentry Cloud (região dos EUA). Eventos de erro retidos por 90 dias. - **Configuração local**: `~/.archgate/config.json` armazena sua preferência de telemetria e o ID de instalação anônimo. -## Aviso de primeira execução - -Na primeira execução, o Archgate exibe um aviso único no stderr: - -``` -Archgate collects anonymous usage data to improve the CLI. -Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0 -Learn more: https://cli.archgate.dev/reference/telemetry -``` - -Este aviso é exibido uma única vez e não aparece novamente. - ## Código aberto A implementação da telemetria é totalmente open source. Você pode inspecionar exatamente quais dados são coletados lendo: diff --git a/docs/src/content/docs/reference/telemetry.mdx b/docs/src/content/docs/reference/telemetry.mdx index 0c0f1b55..3aac7e01 100644 --- a/docs/src/content/docs/reference/telemetry.mdx +++ b/docs/src/content/docs/reference/telemetry.mdx @@ -83,18 +83,6 @@ The environment variable takes precedence over the CLI setting. If `ARCHGATE_TEL - **Error tracking**: Sentry Cloud (US region). Error events retained for 90 days. - **Local config**: `~/.archgate/config.json` stores your telemetry preference and anonymous install ID. -## First-run notice - -On first run, Archgate displays a one-time notice to stderr: - -``` -Archgate collects anonymous usage data to improve the CLI. -Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0 -Learn more: https://cli.archgate.dev/reference/telemetry -``` - -This notice is shown once and does not appear again. - ## Open source The telemetry implementation is fully open source. You can inspect exactly what data is collected by reading: diff --git a/src/cli.ts b/src/cli.ts index e00413a4..42b6e668 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -23,7 +23,6 @@ import { initTelemetry, trackCommand, } from "./helpers/telemetry"; -import { showTelemetryNotice } from "./helpers/telemetry-notice"; import { checkForUpdatesIfNeeded } from "./helpers/update-check"; if (typeof Bun === "undefined") @@ -45,7 +44,6 @@ async function main() { // Initialize telemetry and error tracking (no-op if opted out) initTelemetry(); initSentry(); - showTelemetryNotice(); const program = new Command() .name("archgate") diff --git a/src/helpers/telemetry-notice.ts b/src/helpers/telemetry-notice.ts deleted file mode 100644 index d236d118..00000000 --- a/src/helpers/telemetry-notice.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * telemetry-notice.ts — One-time notice about telemetry on first CLI run. - * - * Shows a brief message to stderr (not stdout, per ARCH-003) informing users - * that anonymous data is collected and how to opt out. The notice is shown - * once, then a marker file is written to prevent repeat notices. - */ - -import { existsSync } from "node:fs"; - -import { logDebug, logWarn } from "./log"; -import { internalPath, createPathIfNotExists } from "./paths"; -import { isTelemetryEnabled } from "./telemetry-config"; - -const NOTICE_MARKER = "telemetry-notice-shown"; - -function markerPath(): string { - return internalPath(NOTICE_MARKER); -} - -/** - * Show a one-time telemetry notice on first run. - * Only shown if telemetry is enabled and the notice hasn't been shown before. - * Writes to stderr via logWarn() to avoid interfering with --json output (ARCH-003). - */ -export function showTelemetryNotice(): void { - if (!isTelemetryEnabled()) return; - if (existsSync(markerPath())) return; - - // Show notice to stderr via logWarn (diagnostics channel per ARCH-002/ARCH-003) - logWarn("Archgate collects anonymous usage data to improve the CLI."); - logWarn("Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0"); - logWarn("Learn more: https://cli.archgate.dev/reference/telemetry"); - - // Write marker asynchronously — don't block CLI startup - createPathIfNotExists(internalPath()); - Bun.write(markerPath(), new Date().toISOString()).catch(() => { - logDebug("Failed to write telemetry notice marker (ignored)"); - }); -} diff --git a/tests/helpers/telemetry-notice.test.ts b/tests/helpers/telemetry-notice.test.ts deleted file mode 100644 index d4bff522..00000000 --- a/tests/helpers/telemetry-notice.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, test, beforeEach, afterEach } from "bun:test"; -import { existsSync, mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -describe("telemetry-notice", () => { - let tempDir: string; - let originalHome: string | undefined; - let originalTelemetryEnv: string | undefined; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), "archgate-notice-test-")); - originalHome = process.env.HOME; - originalTelemetryEnv = process.env.ARCHGATE_TELEMETRY; - process.env.HOME = tempDir; - delete process.env.ARCHGATE_TELEMETRY; - }); - - afterEach(async () => { - process.env.HOME = originalHome; - if (originalTelemetryEnv === undefined) { - delete process.env.ARCHGATE_TELEMETRY; - } else { - process.env.ARCHGATE_TELEMETRY = originalTelemetryEnv; - } - rmSync(tempDir, { recursive: true, force: true }); - - const { _resetConfigCache } = - await import("../../src/helpers/telemetry-config"); - _resetConfigCache(); - }); - - test("creates marker file after showing notice", async () => { - const { showTelemetryNotice } = - await import("../../src/helpers/telemetry-notice"); - - showTelemetryNotice(); - - // Wait for async marker file write - await Bun.sleep(200); - - const markerPath = join(tempDir, ".archgate", "telemetry-notice-shown"); - expect(existsSync(markerPath)).toBe(true); - }); - - test("does not show notice when telemetry is disabled", async () => { - process.env.ARCHGATE_TELEMETRY = "0"; - - const { showTelemetryNotice } = - await import("../../src/helpers/telemetry-notice"); - - showTelemetryNotice(); - await Bun.sleep(200); - - const markerPath = join(tempDir, ".archgate", "telemetry-notice-shown"); - expect(existsSync(markerPath)).toBe(false); - }); - - test("does not show notice when marker file exists", async () => { - const { mkdirSync } = await import("node:fs"); - const archgateDir = join(tempDir, ".archgate"); - mkdirSync(archgateDir, { recursive: true }); - await Bun.write( - join(archgateDir, "telemetry-notice-shown"), - "already-shown" - ); - - const { showTelemetryNotice } = - await import("../../src/helpers/telemetry-notice"); - - // Should be a no-op (no error, no duplicate marker write) - showTelemetryNotice(); - }); -}); From 5ba51681b36f9d1d626a982762383da2411b6726 Mon Sep 17 00:00:00 2001 From: rhuanbarreto <283004+rhuanbarreto@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:19:02 +0000 Subject: [PATCH 05/13] docs: regenerate llms-full.txt --- docs/public/llms-full.txt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index ae77dd0d..3af4e10a 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -3882,18 +3882,6 @@ The environment variable takes precedence over the CLI setting. If `ARCHGATE_TEL - **Error tracking**: Sentry Cloud (US region). Error events retained for 90 days. - **Local config**: `~/.archgate/config.json` stores your telemetry preference and anonymous install ID. -## First-run notice - -On first run, Archgate displays a one-time notice to stderr: - -``` -Archgate collects anonymous usage data to improve the CLI. -Disable: `archgate telemetry disable` or set ARCHGATE_TELEMETRY=0 -Learn more: https://cli.archgate.dev/reference/telemetry -``` - -This notice is shown once and does not appear again. - ## Open source The telemetry implementation is fully open source. You can inspect exactly what data is collected by reading: From 072766187830697989091a7d195fa55c484ceb91 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 02:24:14 +0100 Subject: [PATCH 06/13] chore: set Sentry DSN for error tracking --- src/helpers/sentry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/sentry.ts b/src/helpers/sentry.ts index defada94..92eb1aa9 100644 --- a/src/helpers/sentry.ts +++ b/src/helpers/sentry.ts @@ -23,7 +23,8 @@ import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; * Sentry DSN (write-only ingest URL, safe to embed in client code). * Format: https://@/ */ -const SENTRY_DSN = "https://placeholder@o0.ingest.us.sentry.io/0"; +const SENTRY_DSN = + "https://bb693c2cbc4238dbcd6efac609062402@o4511085517340672.ingest.de.sentry.io/4511085521469520"; /** Maximum time to wait for the Sentry request (ms). */ const SENTRY_TIMEOUT_MS = 5_000; From 19637d574b237df69f0ca209ab9afe4ea09980e2 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 02:29:20 +0100 Subject: [PATCH 07/13] refactor: switch from Sentry HTTP API to @sentry/bun SDK Use the official Sentry SDK for breadcrumbs and distributed tracing. - Breadcrumbs track command execution flow for crash context - Tracing propagated to plugins.archgate.dev via sentry-trace headers - addBreadcrumb() exported for use across CLI modules - flushSentry() ensures events are sent before process exit --- bun.lock | 157 +++++++++++++++++ package.json | 1 + src/cli.ts | 16 +- src/helpers/sentry.ts | 322 ++++++++++++----------------------- tests/helpers/sentry.test.ts | 89 +++++----- 5 files changed, 316 insertions(+), 269 deletions(-) diff --git a/bun.lock b/bun.lock index d0b37dcf..24863059 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@commitlint/cli": "20.5.0", "@commitlint/config-conventional": "20.5.0", "@commitlint/cz-commitlint": "20.5.0", + "@sentry/bun": "10.45.0", "@simple-release/npm": "2.3.0", "@types/bun": "1.3.11", "commitizen": "4.3.1", @@ -73,6 +74,8 @@ "@conventional-changelog/git-client": ["@conventional-changelog/git-client@2.6.0", "", { "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", "@simple-libs/stream-utils": "^1.2.0", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.3.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-T+uPDciKf0/ioNNDpMGc8FDsehJClZP0yR3Q5MN6wE/Y/1QZ7F+80OgznnTCOlMEG4AV0LvH2UJi3C/nBnaBUg=="], + "@fastify/otel": ["@fastify/otel@0.17.1", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-K4wyxfUZx2ux5o+b6BtTqouYFVILohLZmSbA2tKUueJstNcBnoGPVhllCaOvbQ3ZrXdUxUC/fyrSWSCqHhdOPg=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.4", "", {}, "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg=="], "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="], @@ -105,6 +108,70 @@ "@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.213.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.213.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.213.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3i9NdkET/KvQomeh7UaR/F4r9P25Rx6ooALlWXPIjypcEOUxksCmVu0zA70NBJWlrMW1rPr/LRidFAflLI+s/w=="], + + "@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.60.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-q/B2IvoVXRm1M00MvhnzpMN6rKYOszPXVsALi6u0ss4AYHe+TidZEtLW9N1ZhrobI1dSriHnBqqtAOZVAv07sg=="], + + "@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.56.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-PKp+sSZ7AfzMvGgO3VCyo1inwNu+q7A1k9X88WK4PQ+S6Hp7eFk8pie+sWHDTaARovmqq5V2osav3lQej2B0nw=="], + + "@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.30.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MXHP2Q38cd2OhzEBKAIXUi9uBlPEYzF6BNJbyjUXBQ6kLaf93kRC41vNMIz0Nl5mnuwK7fDvKT+/lpx7BXRwdg=="], + + "@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.61.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Xdmqo9RZuZlL29Flg8QdwrrX7eW1CZ7wFQPKHyXljNymgKhN1MCsYuqQ/7uxavhSKwAl7WxkTzKhnqpUApLMvQ=="], + + "@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.32.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-koR6apx0g0wX6RRiPpjA4AFQUQUbXrK16kq4/SZjVp7u5cffJhNkY4TnITxcGA4acGSPYAfx3NHRIv4Khn1axQ=="], + + "@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.56.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fg+Jffs6fqrf0uQS0hom7qBFKsbtpBiBl8+Vkc63Gx8xh6pVh+FhagmiO6oM0m3vyb683t1lP7yGYq22SiDnqg=="], + + "@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.61.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pUiVASv6nh2XrerTvlbVHh7vKFzscpgwiQ/xvnZuAIzQ5lRjWVdRPUuXbvZJ/Yq79QsE81TZdJ7z9YsXiss1ew=="], + + "@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.59.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-33wa4mEr+9+ztwdgLor1SeBu4Opz4IsmpcLETXAd3VmBrOjez8uQtrsOhPCa5Vhbm5gzDlMYTgFRLQzf8/YHFA=="], + + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.213.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/instrumentation": "0.213.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-B978Xsm5XEPGhm1P07grDoaOFLHapJPkOG9h016cJsyWWxmiLnPu2M/4Nrm7UCkHSiLnkXgC+zVGUAIahy8EEA=="], + + "@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.61.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-hsHDadUtAFbws1YSDc1XW0svGFKiUbqv2td1Cby+UAiwvojm1NyBo/taifH0t8CuFZ0x/2SDm0iuTwrM5pnVOg=="], + + "@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.22.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-wJU4IBQMUikdJAcTChLFqK5lo+flo7pahqd8DSLv7uMxsdOdAHj6RzKYAm8pPfUS6ItKYutYyuicwKaFwQKsoA=="], + + "@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-vMCSh8kolEm5rRsc+FZeTZymWmIJwc40hjIKnXH4O0Dv/gAkJJIRXCsPX5cPbe0c0j/34+PsENd0HqKruwhVYw=="], + + "@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.61.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.36.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-lvrfWe9ShK/D2X4brmx8ZqqeWPfRl8xekU0FCn7C1dHm5k6+rTOOi36+4fnaHAP8lig9Ux6XQ1D4RNIpPCt1WQ=="], + + "@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-cEqpUocSKJfwDtLYTTJehRLWzkZ2eoePCxfVIgGkGkb83fMB71O+y4MvRHJPbeV2bdoWdOVrl8uO0+EynWhTEA=="], + + "@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.66.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-d7m9QnAY+4TCWI4q1QRkfrc6fo/92VwssaB1DzQfXNRvu51b78P+HJlWP7Qg6N6nkwdb9faMZNBCZJfftmszkw=="], + + "@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.59.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6/jWU+c1NgznkVLDU/2y0bXV2nJo3o9FWZ9mZ9nN6T/JBNRoMnVXZl2FdBmgH+a5MwaWLs5kmRJTP5oUVGIkPw=="], + + "@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.59.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/mysql": "2.15.27" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-r+V/Fh0sm7Ga8/zk/TI5H5FQRAjwr0RrpfPf8kNIehlsKf12XnvIaZi8ViZkpX0gyPEpLXqzqWD6QHlgObgzZw=="], + + "@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.59.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@opentelemetry/sql-common": "^0.41.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-n9/xrVCRBfG9egVbffnlU1uhr+HX0vF4GgtAB/Bvm48wpFgRidqD8msBMiym1kRYzmpWvJqTxNT47u1MkgBEdw=="], + + "@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.65.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.41.2", "@types/pg": "8.15.6", "@types/pg-pool": "2.0.7" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-W0zpHEIEuyZ8zvb3njaX9AAbHgPYOsSWVOoWmv1sjVRSF6ZpBqtlxBWbU+6hhq1TFWBeWJOXZ8nZS/PUFpLJYQ=="], + + "@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.61.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JnPexA034/0UJRsvH96B0erQoNOqKJZjE2ZRSw9hiTSC23LzE0nJE/u6D+xqOhgUhRnhhcPHq4MdYtmUdYTF+Q=="], + + "@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.32.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BQS6gG8RJ1foEqfEZ+wxoqlwfCAzb1ZVG0ad8Gfe4x8T658HJCLGLd4E4NaoQd8EvPfLqOXgzGaE/2U4ytDSWA=="], + + "@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.23.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.24.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-LL0VySzKVR2cJSFVZaTYpZl1XTpBGnfzoQPe2W7McS2267ldsaEIqtQY6VXs2KCXN0poFjze5110PIpxHDaDGg=="], + + "@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.38.2", "", {}, "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + + "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.41.0", "", { "os": "android", "cpu": "arm" }, "sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.41.0", "", { "os": "android", "cpu": "arm64" }, "sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g=="], @@ -181,6 +248,18 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="], + "@prisma/instrumentation": ["@prisma/instrumentation@7.4.2", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-r9JfchJF1Ae6yAxcaLu/V1TGqBhAuSDe3mRNOssBfx1rMzfZ4fdNvrgUBwyb/TNTGXFxlH9AZix5P257x07nrg=="], + + "@sentry/bun": ["@sentry/bun@10.45.0", "", { "dependencies": { "@sentry/core": "10.45.0", "@sentry/node": "10.45.0" } }, "sha512-f9+24fp1Ew09qcqQ2DMKo0Dh/4WIE8G4D3/CLh8pgdwjP8A+5cvTQLcdUsqZll361P4jmXS+ZEi5KlmPv/ddGw=="], + + "@sentry/core": ["@sentry/core@10.45.0", "", {}, "sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q=="], + + "@sentry/node": ["@sentry/node@10.45.0", "", { "dependencies": { "@fastify/otel": "0.17.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.6.0", "@opentelemetry/core": "^2.6.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/instrumentation-amqplib": "0.60.0", "@opentelemetry/instrumentation-connect": "0.56.0", "@opentelemetry/instrumentation-dataloader": "0.30.0", "@opentelemetry/instrumentation-express": "0.61.0", "@opentelemetry/instrumentation-fs": "0.32.0", "@opentelemetry/instrumentation-generic-pool": "0.56.0", "@opentelemetry/instrumentation-graphql": "0.61.0", "@opentelemetry/instrumentation-hapi": "0.59.0", "@opentelemetry/instrumentation-http": "0.213.0", "@opentelemetry/instrumentation-ioredis": "0.61.0", "@opentelemetry/instrumentation-kafkajs": "0.22.0", "@opentelemetry/instrumentation-knex": "0.57.0", "@opentelemetry/instrumentation-koa": "0.61.0", "@opentelemetry/instrumentation-lru-memoizer": "0.57.0", "@opentelemetry/instrumentation-mongodb": "0.66.0", "@opentelemetry/instrumentation-mongoose": "0.59.0", "@opentelemetry/instrumentation-mysql": "0.59.0", "@opentelemetry/instrumentation-mysql2": "0.59.0", "@opentelemetry/instrumentation-pg": "0.65.0", "@opentelemetry/instrumentation-redis": "0.61.0", "@opentelemetry/instrumentation-tedious": "0.32.0", "@opentelemetry/instrumentation-undici": "0.23.0", "@opentelemetry/resources": "^2.6.0", "@opentelemetry/sdk-trace-base": "^2.6.0", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.4.2", "@sentry/core": "10.45.0", "@sentry/node-core": "10.45.0", "@sentry/opentelemetry": "10.45.0", "import-in-the-middle": "^3.0.0" } }, "sha512-Kpiq9lRGnJc1ex8SwxOBl+FLQNl4Y137BydVooP7AFiAYZ6ftwHsIEF1bcYXaipHMT1YHS2bdhC2UQaaB2jkuQ=="], + + "@sentry/node-core": ["@sentry/node-core@10.45.0", "", { "dependencies": { "@sentry/core": "10.45.0", "@sentry/opentelemetry": "10.45.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/context-async-hooks", "@opentelemetry/core", "@opentelemetry/instrumentation", "@opentelemetry/resources", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-KQZEvLKM344+EqXiA9HIzWbW5hzq6/9nnFUQ8niaBPoOgR9AiJhrccfIscfgb8vjkriiEtzE03OW/4h1CTgZ3Q=="], + + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.45.0", "", { "dependencies": { "@sentry/core": "10.45.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-PmuGO+p/gC3ZQ8ddOeJ5P9ApnTTm35i12Bpuyb13AckCbNSJFvG2ggZda35JQOmiFU0kKYiwkoFAa8Mvj9od3Q=="], + "@simple-libs/child-process-utils": ["@simple-libs/child-process-utils@1.0.1", "", { "dependencies": { "@simple-libs/stream-utils": "^1.1.0", "@types/node": "^22.0.0" } }, "sha512-3nWd8irxvDI6v856wpPCHZ+08iQR0oHTZfzAZmnbsLzf+Sf1odraP6uKOHDZToXq3RPRV/LbqGVlSCogm9cJjg=="], "@simple-libs/hosted-git-info": ["@simple-libs/hosted-git-info@1.0.2", "", {}, "sha512-aAmGQdMH+ZinytKuA2832u0ATeOFNYNk4meBEXtB5xaPotUgggYNhq5tYU/v17wEbmTW5P9iHNqNrFyrhnqBAg=="], @@ -193,12 +272,26 @@ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ=="], + "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], + "@types/node": ["@types/node@22.18.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], + + "@types/pg-pool": ["@types/pg-pool@2.0.7", "", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="], + + "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], @@ -235,6 +328,8 @@ "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -281,6 +376,8 @@ "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], @@ -327,6 +424,8 @@ "findup-sync": ["findup-sync@4.0.0", "", { "dependencies": { "detect-file": "^1.0.0", "is-glob": "^4.0.0", "micromatch": "^4.0.2", "resolve-dir": "^1.0.1" } }, "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ=="], + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + "fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], @@ -359,6 +458,8 @@ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "import-in-the-middle": ["import-in-the-middle@3.0.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], @@ -447,6 +548,10 @@ "minimist": ["minimist@1.2.7", "", {}, "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -473,16 +578,32 @@ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "resolve-dir": ["resolve-dir@1.0.1", "", { "dependencies": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" } }, "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], @@ -563,6 +684,8 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -575,12 +698,18 @@ "@conventional-changelog/git-client/@simple-libs/stream-utils": ["@simple-libs/stream-utils@1.2.0", "", {}, "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA=="], + "@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="], + + "@fastify/otel/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + "@inquirer/core/cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], "@inquirer/external-editor/chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], + "@simple-release/core/@conventional-changelog/git-client": ["@conventional-changelog/git-client@2.5.1", "", { "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", "@simple-libs/stream-utils": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.1.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-lAw7iA5oTPWOLjiweb7DlGEMDEvzqzLLa6aWOly2FSZ64IwLE8T458rC+o+WvI31Doz6joM7X2DoNog7mX8r4A=="], "@simple-release/core/@simple-libs/hosted-git-info": ["@simple-libs/hosted-git-info@1.0.1", "", {}, "sha512-R8/PbLKPw5iLeULJgnvwIKkggeyzwXPFERp+4itc0zSrG4sUTf23w7JlMSImPD2o+ZRpEv7YgG6IXgSk87jTgw=="], @@ -589,8 +718,16 @@ "@simple-release/core/conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@9.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA=="], + "@types/connect/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], + "@types/conventional-commits-parser/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], + "@types/mysql/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], + + "@types/pg/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], + + "@types/tedious/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], + "bun-types/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], "commitizen/inquirer": ["inquirer@8.2.5", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^7.0.0" } }, "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ=="], @@ -619,12 +756,30 @@ "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="], + + "@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + + "@fastify/otel/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + "@simple-release/core/conventional-changelog/conventional-changelog-writer": ["conventional-changelog-writer@8.2.0", "", { "dependencies": { "conventional-commits-filter": "^5.0.0", "handlebars": "^4.7.7", "meow": "^13.0.0", "semver": "^7.5.2" }, "bin": { "conventional-changelog-writer": "dist/cli/index.js" } }, "sha512-Y2aW4596l9AEvFJRwFGJGiQjt2sBYTjPD18DdvxX9Vpz0Z7HQ+g1Z+6iYDAm1vR3QOJrDBkRHixHK/+FhkR6Pw=="], "@simple-release/core/conventional-changelog/conventional-commits-parser": ["conventional-commits-parser@6.2.0", "", { "dependencies": { "meow": "^13.0.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" } }, "sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag=="], + "@types/connect/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@types/conventional-commits-parser/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@types/mysql/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/tedious/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], "commitizen/inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -655,6 +810,8 @@ "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "@fastify/otel/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "commitizen/inquirer/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "commitizen/inquirer/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], diff --git a/package.json b/package.json index 9411cd5b..73719d77 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@commitlint/cli": "20.5.0", "@commitlint/config-conventional": "20.5.0", "@commitlint/cz-commitlint": "20.5.0", + "@sentry/bun": "10.45.0", "@simple-release/npm": "2.3.0", "@types/bun": "1.3.11", "commitizen": "4.3.1", diff --git a/src/cli.ts b/src/cli.ts index 42b6e668..65208ca4 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,7 +17,12 @@ import { installGit } from "./helpers/git"; import { logError } from "./helpers/log"; import { createPathIfNotExists, paths } from "./helpers/paths"; import { isSupportedPlatform } from "./helpers/platform"; -import { captureException, initSentry } from "./helpers/sentry"; +import { + addBreadcrumb, + captureException, + flushSentry, + initSentry, +} from "./helpers/sentry"; import { flushTelemetry, initTelemetry, @@ -50,10 +55,11 @@ async function main() { .version(packageJson.version) .description("AI governance for software development"); - // Track which command is being executed + // Track which command is being executed and add Sentry breadcrumb program.hook("preAction", (thisCommand) => { const fullCommand = getFullCommandName(thisCommand); trackCommand(fullCommand); + addBreadcrumb("command", `Running: ${fullCommand}`); }); registerInitCommand(program); @@ -75,8 +81,9 @@ async function main() { const notice = await updateCheckPromise; if (notice) console.log(notice); - // Flush telemetry events (fire-and-forget, with timeout) + // Flush telemetry and Sentry events before exit await flushTelemetry(); + await flushSentry(); } /** @@ -96,8 +103,9 @@ function getFullCommandName(command: Command): string { return parts.join(" ") || "root"; } -main().catch((err: unknown) => { +main().catch(async (err: unknown) => { captureException(err, { command: "main" }); + await flushSentry(); logError(String(err)); process.exit(2); }); diff --git a/src/helpers/sentry.ts b/src/helpers/sentry.ts index 92eb1aa9..5c7a6f89 100644 --- a/src/helpers/sentry.ts +++ b/src/helpers/sentry.ts @@ -1,16 +1,20 @@ /** - * sentry.ts — Error tracking via Sentry HTTP envelope API. + * sentry.ts — Error tracking via @sentry/bun SDK. * - * No SDK dependency — uses fetch() to POST error envelopes directly to - * Sentry's envelope endpoint. This keeps the binary lean (ARCH-006) while - * providing crash reporting for exit-code-2 errors. + * Uses the official Sentry SDK for Bun to capture errors with full + * breadcrumb context. Breadcrumbs are added throughout CLI execution + * (command start, config loading, rule checks, etc.) so that crash + * reports include the sequence of operations leading to the failure. * * IP anonymization: the Sentry project has "Prevent Storing of IP Addresses" - * enabled server-side. The CLI also omits user.ip_address from the payload. + * enabled server-side. * - * All network calls are fire-and-forget: failures never affect CLI behavior. + * Sentry is only initialized when telemetry is enabled. All Sentry calls + * are wrapped to never affect CLI behavior or exit codes. */ +import * as Sentry from "@sentry/bun"; + import { logDebug } from "./log"; import { getPlatformInfo } from "./platform"; import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; @@ -21,48 +25,29 @@ import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; /** * Sentry DSN (write-only ingest URL, safe to embed in client code). - * Format: https://@/ */ const SENTRY_DSN = "https://bb693c2cbc4238dbcd6efac609062402@o4511085517340672.ingest.de.sentry.io/4511085521469520"; -/** Maximum time to wait for the Sentry request (ms). */ -const SENTRY_TIMEOUT_MS = 5_000; - -// --------------------------------------------------------------------------- -// DSN parsing -// --------------------------------------------------------------------------- - -interface ParsedDSN { - publicKey: string; - host: string; - projectId: string; -} - -function parseDSN(dsn: string): ParsedDSN | null { - try { - const url = new URL(dsn); - const publicKey = url.username; - const projectId = url.pathname.replace("/", ""); - const host = `${url.protocol}//${url.host}`; - if (!publicKey || !projectId) return null; - return { publicKey, host, projectId }; - } catch { - return null; - } -} - // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- let initialized = false; -let parsedDSN: ParsedDSN | null = null; // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- +function getCliVersion(): string { + try { + const pkg = require("../../package.json") as { version: string }; + return pkg.version; + } catch { + return "unknown"; + } +} + /** * Initialize Sentry error tracking. Call once at CLI startup. * No-op if telemetry is disabled. @@ -73,19 +58,85 @@ export function initSentry(): void { return; } - parsedDSN = parseDSN(SENTRY_DSN); - if (!parsedDSN) { - logDebug("Sentry disabled — invalid DSN"); - return; + const cliVersion = getCliVersion(); + const { runtime } = getPlatformInfo(); + + try { + Sentry.init({ + dsn: SENTRY_DSN, + release: `archgate@${cliVersion}`, + environment: process.env.NODE_ENV ?? "production", + // Do not send default PII (hostnames, IPs, etc.) + sendDefaultPii: false, + // Enable tracing so sentry-trace headers propagate to the plugins service + tracesSampleRate: 1.0, + // Propagate traces to the plugins API for distributed tracing + tracePropagationTargets: ["plugins.archgate.dev"], + // Set the anonymous install ID as the user + initialScope: { + user: { id: getInstallId() }, + tags: { + cli_version: cliVersion, + os: runtime, + arch: process.arch, + is_ci: String(Boolean(process.env.CI)), + is_tty: String(Boolean(process.stdout.isTTY)), + }, + contexts: { + runtime: { + name: "bun", + // oxlint-disable-next-line no-negated-condition -- Bun availability check requires typeof guard + version: typeof Bun !== "undefined" ? Bun.version : "unknown", + }, + }, + }, + // Keep default integrations including Http/Undici for distributed tracing + // (sentry-trace headers are auto-injected into fetch calls matching + // tracePropagationTargets above) + // Limit breadcrumbs to keep payloads small + maxBreadcrumbs: 50, + }); + + initialized = true; + logDebug("Sentry initialized"); + } catch { + logDebug("Sentry init failed (silently ignored)"); } +} + +/** + * Add a breadcrumb to the current Sentry scope. + * Breadcrumbs are attached to the next error event, providing context + * about the sequence of operations leading to a crash. + * + * @param category Short category name (e.g., "command", "config", "check") + * @param message Human-readable description + * @param data Optional structured data + * @param level Breadcrumb severity level (default: "info") + */ +export function addBreadcrumb( + category: string, + message: string, + data?: Record, + level: "debug" | "info" | "warning" | "error" = "info" +): void { + if (!initialized) return; - initialized = true; - logDebug("Sentry initialized"); + try { + Sentry.addBreadcrumb({ + category, + message, + data, + level, + timestamp: Date.now() / 1000, + }); + } catch { + // Never let breadcrumb failures affect CLI behavior + } } /** * Capture an exception and send it to Sentry. - * Fire-and-forget: errors are silently swallowed. * * @param error The error to capture * @param context Optional extra context (command name, options, etc.) @@ -94,189 +145,30 @@ export function captureException( error: unknown, context?: Record ): void { - if (!initialized || !parsedDSN) return; - - const err = error instanceof Error ? error : new Error(String(error)); - - // Build and send envelope asynchronously — never block the CLI - sendErrorEnvelope(err, context).catch(() => { - logDebug("Sentry capture failed (silently ignored)"); - }); -} - -// --------------------------------------------------------------------------- -// Envelope construction -// --------------------------------------------------------------------------- + if (!initialized) return; -function getCliVersion(): string { try { - const pkg = require("../../package.json") as { version: string }; - return pkg.version; + Sentry.captureException(error, { contexts: { cli: context } }); } catch { - return "unknown"; + logDebug("Sentry capture failed (silently ignored)"); } } /** - * Build and send a Sentry error envelope via the HTTP API. + * Flush pending Sentry events. Call before process exit to ensure + * error events are sent. * - * Envelope format: https://develop.sentry.dev/sdk/envelopes/ - * Each line is a JSON object, separated by newlines: - * 1. Envelope header (event_id, dsn, sent_at) - * 2. Item header (type, content_type, length) - * 3. Item payload (the error event) - */ -async function sendErrorEnvelope( - error: Error, - context?: Record -): Promise { - if (!parsedDSN) return; - - const eventId = generateEventId(); - const cliVersion = getCliVersion(); - const installId = getInstallId(); - const { runtime } = getPlatformInfo(); - - const event = { - event_id: eventId, - timestamp: Date.now() / 1000, - platform: "javascript", - level: "error", - release: `archgate@${cliVersion}`, - environment: process.env.NODE_ENV ?? "production", - server_name: undefined, // explicitly omit hostname - user: { - id: installId, - // No ip_address — server-side stripping handles this - }, - contexts: { - runtime: { - name: "bun", - // oxlint-disable-next-line no-negated-condition -- Bun availability check requires typeof guard - version: typeof Bun !== "undefined" ? Bun.version : "unknown", - }, - os: { name: runtime, machine: process.arch }, - cli: { - ...context, - is_ci: Boolean(process.env.CI), - is_tty: Boolean(process.stdout.isTTY), - }, - }, - exception: { - values: [ - { - type: error.name, - value: error.message, - stacktrace: error.stack ? parseStacktrace(error.stack) : undefined, - }, - ], - }, - tags: { cli_version: cliVersion, os: runtime, arch: process.arch }, - }; - - const eventJson = JSON.stringify(event); - - // Envelope: header \n item-header \n item-payload - const envelopeHeader = JSON.stringify({ - event_id: eventId, - dsn: SENTRY_DSN, - sent_at: new Date().toISOString(), - sdk: { name: "archgate-cli", version: cliVersion }, - }); - - const itemHeader = JSON.stringify({ - type: "event", - content_type: "application/json", - length: Buffer.byteLength(eventJson), - }); - - const envelope = `${envelopeHeader}\n${itemHeader}\n${eventJson}`; - - const envelopeUrl = `${parsedDSN.host}/api/${parsedDSN.projectId}/envelope/`; - - await fetch(envelopeUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-sentry-envelope", - "X-Sentry-Auth": `Sentry sentry_version=7, sentry_client=archgate-cli/${cliVersion}, sentry_key=${parsedDSN.publicKey}`, - }, - body: envelope, - signal: AbortSignal.timeout(SENTRY_TIMEOUT_MS), - }); - - logDebug("Sentry event sent:", eventId); -} - -// --------------------------------------------------------------------------- -// Stack trace parsing -// --------------------------------------------------------------------------- - -interface SentryFrame { - filename?: string; - function?: string; - lineno?: number; - colno?: number; - in_app?: boolean; -} - -/** - * Parse a V8/Bun stack trace string into Sentry frame format. - * Strips absolute paths to relative for privacy. + * @param timeoutMs Maximum time to wait for flush (default: 2000ms) */ -function parseStacktrace(stack: string): { frames: SentryFrame[] } { - const frames: SentryFrame[] = []; - const lines = stack.split("\n"); - - for (const line of lines) { - const match = line.match(/^\s+at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?$/); - if (!match) continue; - - const [, fn, filepath, lineStr, colStr] = match; - - // Strip absolute paths — keep only the relative part from src/ or tests/ - const relPath = stripToRelativePath(filepath ?? ""); - - frames.push({ - filename: relPath, - function: fn ?? "", - lineno: Number(lineStr), - colno: Number(colStr), - in_app: relPath.startsWith("src/") || relPath.startsWith("tests/"), - }); - } +export async function flushSentry(timeoutMs = 2000): Promise { + if (!initialized) return; - // Sentry expects frames in caller-first order (reversed from stack trace) - return { frames: frames.reverse() }; -} - -/** - * Strip an absolute file path to a relative path from the project root. - * E.g., /home/user/project/src/cli.ts → src/cli.ts - */ -function stripToRelativePath(filepath: string): string { - // Match common project path segments - const markers = ["/src/", "/tests/", "/node_modules/"]; - for (const marker of markers) { - const idx = filepath.lastIndexOf(marker); - if (idx !== -1) return filepath.slice(idx + 1); + try { + await Sentry.flush(timeoutMs); + logDebug("Sentry flushed"); + } catch { + logDebug("Sentry flush failed (silently ignored)"); } - // Fallback: use the last path component - const lastSlash = Math.max( - filepath.lastIndexOf("/"), - filepath.lastIndexOf("\\") - ); - return lastSlash === -1 ? filepath : filepath.slice(lastSlash + 1); -} - -// --------------------------------------------------------------------------- -// Utilities -// --------------------------------------------------------------------------- - -/** Generate a 32-character hex event ID (Sentry format). */ -function generateEventId(): string { - const bytes = new Uint8Array(16); - crypto.getRandomValues(bytes); - return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); } // --------------------------------------------------------------------------- @@ -285,6 +177,8 @@ function generateEventId(): string { /** Reset Sentry state. For testing only. */ export function _resetSentry(): void { + if (initialized) { + Sentry.close(); + } initialized = false; - parsedDSN = null; } diff --git a/tests/helpers/sentry.test.ts b/tests/helpers/sentry.test.ts index 4d98b0a0..74730102 100644 --- a/tests/helpers/sentry.test.ts +++ b/tests/helpers/sentry.test.ts @@ -1,18 +1,12 @@ -import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"; +import { describe, test, beforeEach, afterEach, mock } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -/** Type-safe fetch mock — Bun's fetch type includes `preconnect`. */ -function mockFetch(handler: () => Response) { - globalThis.fetch = mock(handler) as unknown as typeof fetch; -} - describe("sentry", () => { let tempDir: string; let originalHome: string | undefined; let originalTelemetryEnv: string | undefined; - const originalFetch = globalThis.fetch; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "archgate-sentry-test-")); @@ -30,93 +24,86 @@ describe("sentry", () => { process.env.ARCHGATE_TELEMETRY = originalTelemetryEnv; } rmSync(tempDir, { recursive: true, force: true }); - globalThis.fetch = originalFetch; - mock.restore(); const { _resetSentry } = await import("../../src/helpers/sentry"); _resetSentry(); const { _resetConfigCache } = await import("../../src/helpers/telemetry-config"); _resetConfigCache(); + mock.restore(); }); describe("initSentry", () => { - test("initializes when telemetry is enabled", async () => { - mockFetch(() => new Response("OK")); - const fetchMock = globalThis.fetch as unknown as ReturnType; - - const { initSentry, captureException } = - await import("../../src/helpers/sentry"); + test("initializes Sentry SDK when telemetry is enabled", async () => { + const { initSentry } = await import("../../src/helpers/sentry"); + // Should not throw — Sentry.init is called internally initSentry(); - - // captureException should attempt to send (fire-and-forget) - captureException(new Error("test error"), { command: "check" }); - - // Give async fire-and-forget time to execute - await Bun.sleep(100); - - // The fetch would have been called if the DSN was valid. - // With placeholder DSN, initSentry parses it and initialized=true, - // so captureException fires the async send. - expect(fetchMock).toHaveBeenCalled(); }); test("does not initialize when telemetry is disabled", async () => { process.env.ARCHGATE_TELEMETRY = "0"; - mockFetch(() => new Response("OK")); - const fetchMock = globalThis.fetch as unknown as ReturnType; const { initSentry, captureException } = await import("../../src/helpers/sentry"); initSentry(); + // captureException should be a no-op captureException(new Error("should not send")); - - await Bun.sleep(100); - expect(fetchMock).not.toHaveBeenCalled(); }); }); describe("captureException", () => { - test("silently ignores fetch failures", async () => { - mockFetch(() => { - throw new Error("Network error"); - }); + test("is a no-op when not initialized", async () => { + const { captureException } = await import("../../src/helpers/sentry"); + + // Should not throw + captureException(new Error("should not send")); + }); + test("handles non-Error values without throwing", async () => { const { initSentry, captureException } = await import("../../src/helpers/sentry"); initSentry(); // Should not throw - captureException(new Error("test"), { command: "check" }); - await Bun.sleep(100); + captureException("string error", { command: "init" }); }); + }); - test("handles non-Error values", async () => { - mockFetch(() => new Response("OK")); - const fetchMock = globalThis.fetch as unknown as ReturnType; + describe("addBreadcrumb", () => { + test("is a no-op when not initialized", async () => { + const { addBreadcrumb } = await import("../../src/helpers/sentry"); - const { initSentry, captureException } = + // Should not throw + addBreadcrumb("test", "test breadcrumb"); + }); + + test("adds breadcrumb when initialized", async () => { + const { initSentry, addBreadcrumb } = await import("../../src/helpers/sentry"); initSentry(); - captureException("string error", { command: "init" }); - await Bun.sleep(100); - - expect(fetchMock).toHaveBeenCalled(); + // Should not throw + addBreadcrumb("command", "Running: check", { staged: true }); }); + }); + describe("flushSentry", () => { test("is a no-op when not initialized", async () => { - mockFetch(() => new Response("OK")); - const fetchMock = globalThis.fetch as unknown as ReturnType; + const { flushSentry } = await import("../../src/helpers/sentry"); - const { captureException } = await import("../../src/helpers/sentry"); + // Should not throw + await flushSentry(); + }); - captureException(new Error("should not send")); - await Bun.sleep(100); + test("flushes when initialized", async () => { + const { initSentry, flushSentry } = + await import("../../src/helpers/sentry"); - expect(fetchMock).not.toHaveBeenCalled(); + initSentry(); + // Should not throw + await flushSentry(100); }); }); }); From cf9c46d35e66aea394964bdfa8b520d154ab9cef Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 02:32:33 +0100 Subject: [PATCH 08/13] feat(sentry): add install method, install path, and version to event tags - install_method: binary, proto, local, or package-manager - install_path: process.execPath for debugging platform-specific issues - release: set to plain version string (e.g., "0.17.2") --- src/helpers/sentry.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/helpers/sentry.ts b/src/helpers/sentry.ts index 5c7a6f89..b012599f 100644 --- a/src/helpers/sentry.ts +++ b/src/helpers/sentry.ts @@ -13,9 +13,12 @@ * are wrapped to never affect CLI behavior or exit codes. */ +import { join } from "node:path"; + import * as Sentry from "@sentry/bun"; import { logDebug } from "./log"; +import { internalPath } from "./paths"; import { getPlatformInfo } from "./platform"; import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; @@ -29,6 +32,26 @@ import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; const SENTRY_DSN = "https://bb693c2cbc4238dbcd6efac609062402@o4511085517340672.ingest.de.sentry.io/4511085521469520"; +// --------------------------------------------------------------------------- +// Install method detection +// --------------------------------------------------------------------------- + +function detectInstallMethod(): string { + const execPath = process.execPath; + const binDir = internalPath("bin"); + + if (execPath.startsWith(binDir)) return "binary"; + + const home = process.env.HOME ?? process.env.USERPROFILE ?? "~"; + const protoHome = process.env.PROTO_HOME ?? join(home, ".proto"); + const protoToolDir = join(protoHome, "tools", "archgate"); + if (execPath.startsWith(protoToolDir)) return "proto"; + + if (execPath.includes("node_modules")) return "local"; + + return "package-manager"; +} + // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- @@ -64,7 +87,7 @@ export function initSentry(): void { try { Sentry.init({ dsn: SENTRY_DSN, - release: `archgate@${cliVersion}`, + release: cliVersion, environment: process.env.NODE_ENV ?? "production", // Do not send default PII (hostnames, IPs, etc.) sendDefaultPii: false, @@ -81,6 +104,8 @@ export function initSentry(): void { arch: process.arch, is_ci: String(Boolean(process.env.CI)), is_tty: String(Boolean(process.stdout.isTTY)), + install_method: detectInstallMethod(), + install_path: process.execPath, }, contexts: { runtime: { From 8ee2f6128404af80d6aa5d298649c50345140926 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 02:39:24 +0100 Subject: [PATCH 09/13] feat: add ARCH-014 (prefer Bun.env) and migrate all process.env to Bun.env - New ADR ARCH-014 with automated rule no-process-env - Replace all process.env references in src/ with Bun.env - Replace require("package.json") with static import (bundled at compile) - Remove unnecessary typeof Bun guards (CLI only runs on Bun) --- .archgate/adrs/ARCH-014-prefer-bun-env.md | 131 ++++++++++++++++++ .../adrs/ARCH-014-prefer-bun-env.rules.ts | 30 ++++ src/commands/upgrade.ts | 4 +- src/helpers/log.ts | 4 +- src/helpers/output.ts | 2 +- src/helpers/paths.ts | 4 +- src/helpers/platform.ts | 4 +- src/helpers/sentry.ts | 28 +--- src/helpers/telemetry-config.ts | 2 +- src/helpers/telemetry.ts | 21 +-- src/helpers/vscode-settings.ts | 3 +- 11 files changed, 184 insertions(+), 49 deletions(-) create mode 100644 .archgate/adrs/ARCH-014-prefer-bun-env.md create mode 100644 .archgate/adrs/ARCH-014-prefer-bun-env.rules.ts diff --git a/.archgate/adrs/ARCH-014-prefer-bun-env.md b/.archgate/adrs/ARCH-014-prefer-bun-env.md new file mode 100644 index 00000000..a272d9f1 --- /dev/null +++ b/.archgate/adrs/ARCH-014-prefer-bun-env.md @@ -0,0 +1,131 @@ +--- +id: ARCH-014 +title: Prefer Bun.env over process.env +domain: architecture +rules: true +files: + - "src/**/*.ts" +--- + +## Context + +The CLI runs exclusively on Bun (`>=1.2.21`), never on Node.js. Bun provides `Bun.env` as its native environment variable accessor, while `process.env` is a Node.js compatibility shim that Bun maintains for backward compatibility. + +Using `process.env` in a Bun-only codebase has several drawbacks: + +1. **Type inconsistency** — `process.env` returns `string | undefined` for every key, requiring manual type narrowing. `Bun.env` behaves identically at runtime but signals intent: this code is Bun-native, not a Node.js port. +2. **Misleading provenance** — When a developer reads `process.env`, they assume Node.js semantics and may reach for Node.js documentation. `Bun.env` makes the runtime dependency explicit. +3. **Inconsistent codebase style** — A mix of `process.env` and `Bun.env` across files creates confusion about which accessor is canonical. New contributors copy whichever pattern they encounter first. +4. **Alignment with project philosophy** — [ARCH-006 (Dependency Policy)](./ARCH-006-dependency-policy.md) establishes a "prefer Bun built-ins" principle. Environment variable access is no different: Bun provides a native API, and the CLI should use it consistently. + +**Alternatives considered:** + +- **Continue using `process.env`** — The most familiar option for developers with Node.js background. However, it obscures the Bun-native nature of the project and creates style inconsistency as new code adopts `Bun.env`. +- **Wrapper helper (e.g., `getEnv()`)** — Centralizing env access through a helper would add indirection for no practical benefit. `Bun.env` is already a clean, well-typed API — wrapping it would violate the project's minimal-abstraction philosophy. +- **Allow both interchangeably** — Permitting both `process.env` and `Bun.env` would perpetuate the inconsistency that prompted this decision. A single canonical accessor is easier to enforce and review. + +For Archgate, the CLI entry point already validates `typeof Bun !== "undefined"` and rejects non-Bun runtimes. Every source file in `src/` can safely assume Bun is available, making `Bun.env` the natural choice. + +## Decision + +All environment variable access in `src/` MUST use `Bun.env` instead of `process.env`. The `process.env` object MUST NOT be used in source files. + +**Scope:** This ADR covers all TypeScript source files under `src/`. It does NOT cover: + +- Test files (`tests/**/*.ts`) — tests may use `process.env` for setup/teardown (e.g., overriding `HOME`) since test harness compatibility matters +- Build scripts and configuration files outside `src/` +- Third-party code in `node_modules/` + +**Key constraints:** + +1. **`Bun.env` for all env reads** — Replace `process.env.FOO` with `Bun.env.FOO` everywhere in `src/` +2. **`Bun.env` for all env writes** — Replace `process.env.FOO = "bar"` with `Bun.env.FOO = "bar"` (Bun.env is writable) +3. **No `process.env` references** — Not even in comments that suggest using it (e.g., "// Use process.env.DEBUG to enable") + +## Do's and Don'ts + +### Do + +- **DO** use `Bun.env.FOO` to read environment variables in all source files under `src/` +- **DO** use `Bun.env.FOO = "value"` to set environment variables when needed +- **DO** use nullish coalescing for defaults: `Bun.env.NODE_ENV ?? "production"` +- **DO** use `Boolean(Bun.env.CI)` for truthy checks on environment flags +- **DO** keep `process.env` in test files (`tests/`) where test harness compatibility is needed + +### Don't + +- **DON'T** use `process.env` in any file under `src/` — use `Bun.env` instead +- **DON'T** create wrapper functions around `Bun.env` — access it directly +- **DON'T** destructure `Bun.env` (e.g., `const { HOME } = Bun.env`) — the proxy-based implementation may not support it reliably across versions; access properties individually + +## Implementation Pattern + +### Good Example + +```typescript +// src/helpers/paths.ts — reading env vars +const home = Bun.env.HOME ?? Bun.env.USERPROFILE ?? homedir(); + +// src/helpers/output.ts — checking CI flag +export function isAgentContext(): boolean { + return !process.stdout.isTTY && !Bun.env.CI; +} + +// src/helpers/log.ts — debug flag +export function logDebug(...args: Parameters) { + if (Bun.env.DEBUG) { + console.warn(header, ...args); + } +} +``` + +### Bad Example + +```typescript +// BAD: using process.env in source files +const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir(); + +// BAD: mixing process.env and Bun.env in the same file +const debug = process.env.DEBUG; +const ci = Bun.env.CI; +``` + +## Consequences + +### Positive + +- **Consistent codebase style** — A single canonical env accessor eliminates style debates and makes grep/search reliable +- **Clear runtime signal** — `Bun.env` immediately communicates that this code is Bun-native, not a Node.js port +- **Aligned with ARCH-006** — Follows the established "prefer Bun built-ins" principle for all APIs +- **Automated enforcement** — The companion rule catches violations in CI, preventing regression + +### Negative + +- **Unfamiliar to Node.js developers** — Contributors with Node.js background will instinctively reach for `process.env`. The linting rule provides immediate feedback. +- **Test/source divergence** — Tests use `process.env` while source uses `Bun.env`. This is intentional but may confuse contributors unfamiliar with the distinction. + +### Risks + +- **Bun.env behavioral differences** — `Bun.env` is a Proxy object, not a plain object like `process.env`. Edge cases (e.g., `Object.keys()`, `JSON.stringify()`, spread) may behave differently. + - **Mitigation:** The CLI accesses env vars by name (`Bun.env.FOO`), never iterates or serializes the entire env object. This usage pattern is well-tested and stable. +- **Contributors bypass the rule** — New contributors may not know about `Bun.env` and use `process.env` out of habit. + - **Mitigation:** The automated rule (`ARCH-014/no-process-env`) flags violations at check time. CI blocks merging non-compliant code. + +## Compliance and Enforcement + +### Automated Enforcement + +- **Archgate rule** `ARCH-014/no-process-env`: Scans all source files under `src/` (excluding test files and `.archgate/`) for `process.env` usage and flags violations. Severity: `error`. + +### Manual Enforcement + +Code reviewers MUST verify: + +1. New source files use `Bun.env` exclusively — no `process.env` references +2. Refactored code migrates `process.env` to `Bun.env` when touched + +## References + +- [Bun.env documentation](https://bun.sh/docs/runtime/env) +- [ARCH-006 — Dependency Policy](./ARCH-006-dependency-policy.md) — Establishes the "prefer Bun built-ins" principle +- [ARCH-009 — Centralized Platform Detection](./ARCH-009-platform-detection-helper.md) — Similar pattern: centralizing a runtime API behind a project convention diff --git a/.archgate/adrs/ARCH-014-prefer-bun-env.rules.ts b/.archgate/adrs/ARCH-014-prefer-bun-env.rules.ts new file mode 100644 index 00000000..215b1ca3 --- /dev/null +++ b/.archgate/adrs/ARCH-014-prefer-bun-env.rules.ts @@ -0,0 +1,30 @@ +/// + +export default { + rules: { + "no-process-env": { + description: + "Source files must use Bun.env instead of process.env for environment variable access", + async check(ctx) { + const files = ctx.scopedFiles.filter( + (f) => !f.includes("tests/") && !f.includes(".archgate/") + ); + + const matches = await Promise.all( + files.map((file) => ctx.grep(file, /process\.env\b/)) + ); + for (const fileMatches of matches) { + for (const m of fileMatches) { + ctx.report.violation({ + message: + "Use Bun.env instead of process.env — this is a Bun-native CLI (see ARCH-014).", + file: m.file, + line: m.line, + fix: "Replace process.env.VAR with Bun.env.VAR", + }); + } + } + }, + }, + }, +} satisfies RuleSet; diff --git a/src/commands/upgrade.ts b/src/commands/upgrade.ts index 7d8f2249..ad6d9208 100644 --- a/src/commands/upgrade.ts +++ b/src/commands/upgrade.ts @@ -61,8 +61,8 @@ function isBinaryInstall(): boolean { } function getProtoHome(): string { - const home = process.env.HOME ?? process.env.USERPROFILE ?? "~"; - return process.env.PROTO_HOME ?? join(home, ".proto"); + const home = Bun.env.HOME ?? Bun.env.USERPROFILE ?? "~"; + return Bun.env.PROTO_HOME ?? join(home, ".proto"); } function isProtoInstall(): boolean { diff --git a/src/helpers/log.ts b/src/helpers/log.ts index 05ce1b63..9f917874 100644 --- a/src/helpers/log.ts +++ b/src/helpers/log.ts @@ -1,11 +1,11 @@ import { styleText } from "node:util"; export function logDebug(...args: Parameters) { - if (process.env.DEBUG) { + if (Bun.env.DEBUG) { const header = styleText("bgWhite", "DEBUG:"); console.warn(header, ...args); } - if (process.env.TRACE) console.trace(); + if (Bun.env.TRACE) console.trace(); } export function logInfo(...args: Parameters) { diff --git a/src/helpers/output.ts b/src/helpers/output.ts index 35efc108..9c0f7033 100644 --- a/src/helpers/output.ts +++ b/src/helpers/output.ts @@ -12,7 +12,7 @@ * stdout is not a TTY AND not running in a CI environment. */ export function isAgentContext(): boolean { - return !process.stdout.isTTY && !process.env.CI; + return !process.stdout.isTTY && !Bun.env.CI; } /** diff --git a/src/helpers/paths.ts b/src/helpers/paths.ts index 601614f0..22ec33c0 100644 --- a/src/helpers/paths.ts +++ b/src/helpers/paths.ts @@ -5,9 +5,9 @@ import { join, dirname } from "node:path"; import { logDebug } from "./log"; export function internalPath(...path: string[]) { - // Use process.env.HOME/USERPROFILE first (testable via env override), + // Use Bun.env.HOME/USERPROFILE first (testable via env override), // fall back to os.homedir() which handles platform-specific resolution. - const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir(); + const home = Bun.env.HOME ?? Bun.env.USERPROFILE ?? homedir(); const internalFolder = join(home, ".archgate"); return join(internalFolder, ...path); } diff --git a/src/helpers/platform.ts b/src/helpers/platform.ts index 437729b1..db2fa5d3 100644 --- a/src/helpers/platform.ts +++ b/src/helpers/platform.ts @@ -35,13 +35,13 @@ export function getPlatformInfo(): PlatformInfo { } // Check WSL2 env vars first (fastest) - const distroName = process.env.WSL_DISTRO_NAME; + const distroName = Bun.env.WSL_DISTRO_NAME; if (distroName) { cachedPlatformInfo = { runtime, isWSL: true, wslDistro: distroName }; return cachedPlatformInfo; } - if (process.env.WSL_INTEROP) { + if (Bun.env.WSL_INTEROP) { cachedPlatformInfo = { runtime, isWSL: true, wslDistro: null }; return cachedPlatformInfo; } diff --git a/src/helpers/sentry.ts b/src/helpers/sentry.ts index b012599f..66671b6a 100644 --- a/src/helpers/sentry.ts +++ b/src/helpers/sentry.ts @@ -17,6 +17,7 @@ import { join } from "node:path"; import * as Sentry from "@sentry/bun"; +import packageJson from "../../package.json"; import { logDebug } from "./log"; import { internalPath } from "./paths"; import { getPlatformInfo } from "./platform"; @@ -42,8 +43,8 @@ function detectInstallMethod(): string { if (execPath.startsWith(binDir)) return "binary"; - const home = process.env.HOME ?? process.env.USERPROFILE ?? "~"; - const protoHome = process.env.PROTO_HOME ?? join(home, ".proto"); + const home = Bun.env.HOME ?? Bun.env.USERPROFILE ?? "~"; + const protoHome = Bun.env.PROTO_HOME ?? join(home, ".proto"); const protoToolDir = join(protoHome, "tools", "archgate"); if (execPath.startsWith(protoToolDir)) return "proto"; @@ -62,15 +63,6 @@ let initialized = false; // Public API // --------------------------------------------------------------------------- -function getCliVersion(): string { - try { - const pkg = require("../../package.json") as { version: string }; - return pkg.version; - } catch { - return "unknown"; - } -} - /** * Initialize Sentry error tracking. Call once at CLI startup. * No-op if telemetry is disabled. @@ -81,14 +73,14 @@ export function initSentry(): void { return; } - const cliVersion = getCliVersion(); + const cliVersion = packageJson.version; const { runtime } = getPlatformInfo(); try { Sentry.init({ dsn: SENTRY_DSN, release: cliVersion, - environment: process.env.NODE_ENV ?? "production", + environment: Bun.env.NODE_ENV ?? "production", // Do not send default PII (hostnames, IPs, etc.) sendDefaultPii: false, // Enable tracing so sentry-trace headers propagate to the plugins service @@ -102,18 +94,12 @@ export function initSentry(): void { cli_version: cliVersion, os: runtime, arch: process.arch, - is_ci: String(Boolean(process.env.CI)), + is_ci: String(Boolean(Bun.env.CI)), is_tty: String(Boolean(process.stdout.isTTY)), install_method: detectInstallMethod(), install_path: process.execPath, }, - contexts: { - runtime: { - name: "bun", - // oxlint-disable-next-line no-negated-condition -- Bun availability check requires typeof guard - version: typeof Bun !== "undefined" ? Bun.version : "unknown", - }, - }, + contexts: { runtime: { name: "bun", version: Bun.version } }, }, // Keep default integrations including Http/Undici for distributed tracing // (sentry-trace headers are auto-injected into fetch calls matching diff --git a/src/helpers/telemetry-config.ts b/src/helpers/telemetry-config.ts index 9fa3d041..ec08e46f 100644 --- a/src/helpers/telemetry-config.ts +++ b/src/helpers/telemetry-config.ts @@ -53,7 +53,7 @@ let cachedConfig: TelemetryConfig | null = null; * Accepted values: "0", "false", "no", "off" (case-insensitive). */ export function isEnvTelemetryDisabled(): boolean { - const envVal = process.env.ARCHGATE_TELEMETRY; + const envVal = Bun.env.ARCHGATE_TELEMETRY; if (envVal === undefined) return false; return ["0", "false", "no", "off"].includes(envVal.toLowerCase()); } diff --git a/src/helpers/telemetry.ts b/src/helpers/telemetry.ts index 18716dc6..71a18acd 100644 --- a/src/helpers/telemetry.ts +++ b/src/helpers/telemetry.ts @@ -13,6 +13,7 @@ * See https://cli.archgate.dev/reference/telemetry for the full privacy policy. */ +import packageJson from "../../package.json"; import { logDebug } from "./log"; import { getPlatformInfo } from "./platform"; import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; @@ -58,30 +59,18 @@ function getCommonProperties(): Record { const { runtime } = getPlatformInfo(); return { $lib: "archgate-cli", - cli_version: getCliVersion(), + cli_version: packageJson.version, os: runtime, arch: process.arch, - // oxlint-disable-next-line no-negated-condition -- Bun availability check requires typeof guard - bun_version: typeof Bun !== "undefined" ? Bun.version : "unknown", - is_ci: Boolean(process.env.CI), + bun_version: Bun.version, + is_ci: Boolean(Bun.env.CI), is_tty: Boolean(process.stdout.isTTY), - node_env: process.env.NODE_ENV ?? "production", + node_env: Bun.env.NODE_ENV ?? "production", // Signal PostHog to resolve geo then discard the IP $ip: null, }; } -function getCliVersion(): string { - try { - // Lazy import to avoid circular dependency with cli.ts - // eslint-disable-next-line @typescript-eslint/no-require-imports - const pkg = require("../../package.json") as { version: string }; - return pkg.version; - } catch { - return "unknown"; - } -} - // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts index c974b347..f130f9f7 100644 --- a/src/helpers/vscode-settings.ts +++ b/src/helpers/vscode-settings.ts @@ -71,8 +71,7 @@ export function mergeMarketplaceUrl( */ export async function getVscodeUserSettingsPath(): Promise { if (isWindows()) { - const appData = - process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"); + const appData = Bun.env.APPDATA ?? join(homedir(), "AppData", "Roaming"); return join(appData, "Code", "User", "settings.json"); } if (isMacOS()) { From efcde038e0778356401f4fd6e30c0e6c7fc6e831 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 02:51:53 +0100 Subject: [PATCH 10/13] refactor: switch from PostHog HTTP API to posthog-node SDK Use the official PostHog Node SDK for event capture with automatic batching, retry, and flush. Adds timeout to shutdown to prevent hanging on exit. Integration test harness disables telemetry via ARCHGATE_TELEMETRY=0 to prevent SDK event loops from holding the subprocess open. --- bun.lock | 17 ++++- package.json | 1 + src/helpers/telemetry.ts | 117 +++++++++++++++---------------- tests/helpers/telemetry.test.ts | 98 ++++++-------------------- tests/integration/cli-harness.ts | 8 ++- 5 files changed, 101 insertions(+), 140 deletions(-) diff --git a/bun.lock b/bun.lock index 24863059..a7573bae 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "inquirer": "13.3.2", "oxfmt": "0.41.0", "oxlint": "1.56.0", + "posthog-node": "5.28.5", "zod": "4.3.6", }, "optionalDependencies": { @@ -248,6 +249,8 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="], + "@posthog/core": ["@posthog/core@1.24.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA=="], + "@prisma/instrumentation": ["@prisma/instrumentation@7.4.2", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-r9JfchJF1Ae6yAxcaLu/V1TGqBhAuSDe3mRNOssBfx1rMzfZ4fdNvrgUBwyb/TNTGXFxlH9AZix5P257x07nrg=="], "@sentry/bun": ["@sentry/bun@10.45.0", "", { "dependencies": { "@sentry/core": "10.45.0", "@sentry/node": "10.45.0" } }, "sha512-f9+24fp1Ew09qcqQ2DMKo0Dh/4WIE8G4D3/CLh8pgdwjP8A+5cvTQLcdUsqZll361P4jmXS+ZEi5KlmPv/ddGw=="], @@ -374,6 +377,8 @@ "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.1.0", "", { "dependencies": { "jiti": "^2.4.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -578,6 +583,8 @@ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], @@ -596,6 +603,8 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "posthog-node": ["posthog-node@5.28.5", "", { "dependencies": { "@posthog/core": "1.24.1" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-+8H7rMPB48cwKhzZmq0EQXDFWjdT6yQrecq+f7c7m19HMzyvYtm54I7vNncEr9lrkzczc4kcePxwLD4qgt/YXg=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], @@ -620,6 +629,10 @@ "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -674,7 +687,7 @@ "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -744,6 +757,8 @@ "global-prefix/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + "handlebars/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], diff --git a/package.json b/package.json index 73719d77..2d7c2601 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "inquirer": "13.3.2", "oxfmt": "0.41.0", "oxlint": "1.56.0", + "posthog-node": "5.28.5", "zod": "4.3.6" }, "peerDependencies": { diff --git a/src/helpers/telemetry.ts b/src/helpers/telemetry.ts index 71a18acd..a33e2f60 100644 --- a/src/helpers/telemetry.ts +++ b/src/helpers/telemetry.ts @@ -1,18 +1,19 @@ /** - * telemetry.ts — Anonymous usage analytics via PostHog HTTP API. + * telemetry.ts — Anonymous usage analytics via PostHog Node SDK. * - * No SDK dependency — uses fetch() directly to POST events to PostHog's - * capture endpoint. Events are buffered during command execution and flushed - * once at the end. All network calls are fire-and-forget: failures are - * silently ignored and never affect CLI behavior or exit codes. + * Uses the official posthog-node SDK for event capture with automatic + * batching and flush. Events are captured during command execution and + * flushed before process exit. * - * IP anonymization: PostHog resolves IP to country/region server-side, then - * the "Discard client IP data" project setting drops the IP from storage. - * The CLI sends `$ip: null` to explicitly signal PostHog not to store it. + * IP anonymization: the CLI sends `$ip: null` on every event to signal + * PostHog to resolve geo server-side then discard the IP. The project + * also has "Discard client IP data" enabled in PostHog settings. * * See https://cli.archgate.dev/reference/telemetry for the full privacy policy. */ +import { PostHog } from "posthog-node"; + import packageJson from "../../package.json"; import { logDebug } from "./log"; import { getPlatformInfo } from "./platform"; @@ -28,26 +29,12 @@ import { getInstallId, isTelemetryEnabled } from "./telemetry-config"; */ const POSTHOG_API_KEY = "phc_placeholder"; const POSTHOG_HOST = "https://us.i.posthog.com"; -const CAPTURE_ENDPOINT = `${POSTHOG_HOST}/capture/`; - -/** Maximum time to wait for the flush request (ms). */ -const FLUSH_TIMEOUT_MS = 5_000; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -interface PostHogEvent { - event: string; - properties: Record; - timestamp: string; -} // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- -let eventBuffer: PostHogEvent[] = []; +let client: PostHog | null = null; let initialized = false; let distinctId = ""; @@ -58,14 +45,12 @@ let distinctId = ""; function getCommonProperties(): Record { const { runtime } = getPlatformInfo(); return { - $lib: "archgate-cli", cli_version: packageJson.version, os: runtime, arch: process.arch, bun_version: Bun.version, is_ci: Boolean(Bun.env.CI), is_tty: Boolean(process.stdout.isTTY), - node_env: Bun.env.NODE_ENV ?? "production", // Signal PostHog to resolve geo then discard the IP $ip: null, }; @@ -86,31 +71,43 @@ export function initTelemetry(): void { } distinctId = getInstallId(); - initialized = true; - logDebug("Telemetry initialized:", distinctId); + + try { + client = new PostHog(POSTHOG_API_KEY, { + host: POSTHOG_HOST, + // Disable polling for feature flags — we don't use them in the CLI + disableGeoip: false, + flushAt: 20, + flushInterval: 10_000, + }); + + initialized = true; + logDebug("Telemetry initialized:", distinctId); + } catch { + logDebug("Telemetry init failed (silently ignored)"); + } } /** * Track a named event with optional properties. - * Events are buffered and sent on flush(). No-op if telemetry is disabled. + * No-op if telemetry is disabled. */ export function trackEvent( event: string, properties?: Record ): void { - if (!initialized) return; - - eventBuffer.push({ - event, - properties: { - ...getCommonProperties(), - ...properties, - distinct_id: distinctId, - }, - timestamp: new Date().toISOString(), - }); + if (!initialized || !client) return; - logDebug("Telemetry event buffered:", event); + try { + client.capture({ + distinctId, + event, + properties: { ...getCommonProperties(), ...properties }, + }); + logDebug("Telemetry event captured:", event); + } catch { + // Silently ignore — telemetry must never affect CLI behavior + } } /** @@ -139,25 +136,22 @@ export function trackCommandResult( } /** - * Flush all buffered events to PostHog. - * Fire-and-forget: errors are silently swallowed. - * Returns a promise that resolves when the flush attempt completes. + * Flush pending events to PostHog. Call before process exit to ensure + * events are delivered. */ -export async function flushTelemetry(): Promise { - if (!initialized || eventBuffer.length === 0) return; - - const events = eventBuffer; - eventBuffer = []; +export async function flushTelemetry(timeoutMs = 3000): Promise { + if (!initialized || !client) return; try { - logDebug(`Flushing ${events.length} telemetry event(s)`); - - await fetch(CAPTURE_ENDPOINT, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch: events }), - signal: AbortSignal.timeout(FLUSH_TIMEOUT_MS), - }); + logDebug("Flushing telemetry events"); + // Race shutdown against a timeout to prevent hanging on exit + await Promise.race([ + client.shutdown(), + new Promise((resolve) => { + setTimeout(resolve, timeoutMs); + }), + ]); + logDebug("Telemetry flushed"); } catch { // Silently ignore — telemetry must never affect CLI behavior logDebug("Telemetry flush failed (silently ignored)"); @@ -170,12 +164,15 @@ export async function flushTelemetry(): Promise { /** Reset telemetry state. For testing only. */ export function _resetTelemetry(): void { - eventBuffer = []; + if (client) { + client.shutdown().catch(() => {}); + } + client = null; initialized = false; distinctId = ""; } -/** Get buffered events. For testing only. */ -export function _getEventBuffer(): PostHogEvent[] { - return eventBuffer; +/** Get the PostHog client instance. For testing only. */ +export function _getClient(): PostHog | null { + return client; } diff --git a/tests/helpers/telemetry.test.ts b/tests/helpers/telemetry.test.ts index 34c2d836..8bced011 100644 --- a/tests/helpers/telemetry.test.ts +++ b/tests/helpers/telemetry.test.ts @@ -1,18 +1,12 @@ -import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"; +import { describe, expect, test, beforeEach, afterEach } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -/** Type-safe fetch mock — Bun's fetch type includes `preconnect`. */ -function mockFetch(handler: () => Response) { - globalThis.fetch = mock(handler) as unknown as typeof fetch; -} - describe("telemetry", () => { let tempDir: string; let originalHome: string | undefined; let originalTelemetryEnv: string | undefined; - const originalFetch = globalThis.fetch; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "archgate-telemetry-test-")); @@ -30,8 +24,6 @@ describe("telemetry", () => { process.env.ARCHGATE_TELEMETRY = originalTelemetryEnv; } rmSync(tempDir, { recursive: true, force: true }); - globalThis.fetch = originalFetch; - mock.restore(); const { _resetTelemetry } = await import("../../src/helpers/telemetry"); _resetTelemetry(); @@ -41,117 +33,67 @@ describe("telemetry", () => { }); describe("initTelemetry", () => { - test("initializes when telemetry is enabled", async () => { - const { initTelemetry, trackEvent, _getEventBuffer } = + test("initializes PostHog client when telemetry is enabled", async () => { + const { initTelemetry, _getClient } = await import("../../src/helpers/telemetry"); initTelemetry(); - trackEvent("test_event", { key: "value" }); - - const buffer = _getEventBuffer(); - expect(buffer).toHaveLength(1); - expect(buffer[0].event).toBe("test_event"); + expect(_getClient()).not.toBeNull(); }); test("skips init when telemetry is disabled via env", async () => { process.env.ARCHGATE_TELEMETRY = "0"; - const { initTelemetry, trackEvent, _getEventBuffer } = + const { initTelemetry, _getClient } = await import("../../src/helpers/telemetry"); initTelemetry(); - trackEvent("test_event"); - - expect(_getEventBuffer()).toHaveLength(0); + expect(_getClient()).toBeNull(); }); }); describe("trackEvent", () => { - test("buffers events with common properties", async () => { - const { initTelemetry, trackEvent, _getEventBuffer } = + test("captures event via PostHog client without throwing", async () => { + const { initTelemetry, trackEvent } = await import("../../src/helpers/telemetry"); initTelemetry(); + // Should not throw — events are queued internally by the SDK trackEvent("command_executed", { command: "check" }); - - const buffer = _getEventBuffer(); - expect(buffer).toHaveLength(1); - expect(buffer[0].properties).toMatchObject({ - $lib: "archgate-cli", - os: process.platform, - arch: process.arch, - command: "check", - $ip: null, - }); - expect(buffer[0].timestamp).toBeTruthy(); }); test("is a no-op when not initialized", async () => { - const { trackEvent, _getEventBuffer } = - await import("../../src/helpers/telemetry"); + const { trackEvent } = await import("../../src/helpers/telemetry"); - trackEvent("should_not_buffer"); - expect(_getEventBuffer()).toHaveLength(0); + // Should not throw + trackEvent("should_not_capture"); }); }); describe("trackCommand", () => { - test("buffers a command_executed event", async () => { - const { initTelemetry, trackCommand, _getEventBuffer } = + test("captures a command_executed event without throwing", async () => { + const { initTelemetry, trackCommand } = await import("../../src/helpers/telemetry"); initTelemetry(); + // Should not throw trackCommand("adr create", { json: true }); - - const buffer = _getEventBuffer(); - expect(buffer).toHaveLength(1); - expect(buffer[0].event).toBe("command_executed"); - expect(buffer[0].properties.command).toBe("adr create"); - expect(buffer[0].properties.json).toBe(true); }); }); describe("flushTelemetry", () => { - test("sends buffered events to PostHog and clears buffer", async () => { - mockFetch(() => new Response("OK", { status: 200 })); - const fetchMock = globalThis.fetch as unknown as ReturnType; - - const { initTelemetry, trackEvent, flushTelemetry, _getEventBuffer } = - await import("../../src/helpers/telemetry"); - - initTelemetry(); - trackEvent("test_event_1"); - trackEvent("test_event_2"); - - await flushTelemetry(); - - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(_getEventBuffer()).toHaveLength(0); - }); - - test("is a no-op when buffer is empty", async () => { - mockFetch(() => new Response("OK")); - const fetchMock = globalThis.fetch as unknown as ReturnType; - + test("flushes without throwing when initialized", async () => { const { initTelemetry, flushTelemetry } = await import("../../src/helpers/telemetry"); initTelemetry(); - await flushTelemetry(); - expect(fetchMock).not.toHaveBeenCalled(); + // Flush with no pending events — should resolve quickly + await flushTelemetry(); }); - test("silently ignores fetch failures", async () => { - mockFetch(() => { - throw new Error("Network error"); - }); - - const { initTelemetry, trackEvent, flushTelemetry } = - await import("../../src/helpers/telemetry"); - - initTelemetry(); - trackEvent("test_event"); + test("is a no-op when not initialized", async () => { + const { flushTelemetry } = await import("../../src/helpers/telemetry"); // Should not throw await flushTelemetry(); diff --git a/tests/integration/cli-harness.ts b/tests/integration/cli-harness.ts index 3c9236af..6f7e61b7 100644 --- a/tests/integration/cli-harness.ts +++ b/tests/integration/cli-harness.ts @@ -29,7 +29,13 @@ export async function runCli( cwd, stdout: "pipe", stderr: "pipe", - env: { ...process.env, NO_COLOR: "1", CI: "1", ...env }, + env: { + ...process.env, + NO_COLOR: "1", + CI: "1", + ARCHGATE_TELEMETRY: "0", + ...env, + }, }); const [stdout, stderr, exitCode] = await Promise.all([ From 5ecae8ebe9459e12044fca4d46db8ff3673c1e0c Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 03:07:10 +0100 Subject: [PATCH 11/13] perf: switch Sentry from @sentry/bun to @sentry/node-core/light Use Sentry's lightweight "light" variant which excludes all OpenTelemetry auto-instrumentation (600+ modules of OTel for MongoDB, Redis, Express, Kafka, etc.). The light mode provides error capture + breadcrumbs which is all a CLI needs. Binary size impact: 0 MB increase (was +10 MB with @sentry/bun). Module count: 434 (same as baseline, was 1,129 with @sentry/bun). --- bun.lock | 130 +----------------------------------------- package.json | 2 +- src/cli.ts | 49 +--------------- src/helpers/sentry.ts | 19 ++---- 4 files changed, 9 insertions(+), 191 deletions(-) diff --git a/bun.lock b/bun.lock index a7573bae..d1e81d4a 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@commitlint/cli": "20.5.0", "@commitlint/config-conventional": "20.5.0", "@commitlint/cz-commitlint": "20.5.0", - "@sentry/bun": "10.45.0", + "@sentry/node-core": "10.45.0", "@simple-release/npm": "2.3.0", "@types/bun": "1.3.11", "commitizen": "4.3.1", @@ -75,8 +75,6 @@ "@conventional-changelog/git-client": ["@conventional-changelog/git-client@2.6.0", "", { "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", "@simple-libs/stream-utils": "^1.2.0", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.3.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-T+uPDciKf0/ioNNDpMGc8FDsehJClZP0yR3Q5MN6wE/Y/1QZ7F+80OgznnTCOlMEG4AV0LvH2UJi3C/nBnaBUg=="], - "@fastify/otel": ["@fastify/otel@0.17.1", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-K4wyxfUZx2ux5o+b6BtTqouYFVILohLZmSbA2tKUueJstNcBnoGPVhllCaOvbQ3ZrXdUxUC/fyrSWSCqHhdOPg=="], - "@inquirer/ansi": ["@inquirer/ansi@2.0.4", "", {}, "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg=="], "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="], @@ -111,68 +109,16 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.213.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw=="], - "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q=="], "@opentelemetry/core": ["@opentelemetry/core@2.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg=="], - "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.213.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.213.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3i9NdkET/KvQomeh7UaR/F4r9P25Rx6ooALlWXPIjypcEOUxksCmVu0zA70NBJWlrMW1rPr/LRidFAflLI+s/w=="], - - "@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.60.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-q/B2IvoVXRm1M00MvhnzpMN6rKYOszPXVsALi6u0ss4AYHe+TidZEtLW9N1ZhrobI1dSriHnBqqtAOZVAv07sg=="], - - "@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.56.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-PKp+sSZ7AfzMvGgO3VCyo1inwNu+q7A1k9X88WK4PQ+S6Hp7eFk8pie+sWHDTaARovmqq5V2osav3lQej2B0nw=="], - - "@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.30.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MXHP2Q38cd2OhzEBKAIXUi9uBlPEYzF6BNJbyjUXBQ6kLaf93kRC41vNMIz0Nl5mnuwK7fDvKT+/lpx7BXRwdg=="], - - "@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.61.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Xdmqo9RZuZlL29Flg8QdwrrX7eW1CZ7wFQPKHyXljNymgKhN1MCsYuqQ/7uxavhSKwAl7WxkTzKhnqpUApLMvQ=="], - - "@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.32.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-koR6apx0g0wX6RRiPpjA4AFQUQUbXrK16kq4/SZjVp7u5cffJhNkY4TnITxcGA4acGSPYAfx3NHRIv4Khn1axQ=="], - - "@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.56.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fg+Jffs6fqrf0uQS0hom7qBFKsbtpBiBl8+Vkc63Gx8xh6pVh+FhagmiO6oM0m3vyb683t1lP7yGYq22SiDnqg=="], - - "@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.61.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pUiVASv6nh2XrerTvlbVHh7vKFzscpgwiQ/xvnZuAIzQ5lRjWVdRPUuXbvZJ/Yq79QsE81TZdJ7z9YsXiss1ew=="], - - "@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.59.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-33wa4mEr+9+ztwdgLor1SeBu4Opz4IsmpcLETXAd3VmBrOjez8uQtrsOhPCa5Vhbm5gzDlMYTgFRLQzf8/YHFA=="], - - "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.213.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/instrumentation": "0.213.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-B978Xsm5XEPGhm1P07grDoaOFLHapJPkOG9h016cJsyWWxmiLnPu2M/4Nrm7UCkHSiLnkXgC+zVGUAIahy8EEA=="], - - "@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.61.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-hsHDadUtAFbws1YSDc1XW0svGFKiUbqv2td1Cby+UAiwvojm1NyBo/taifH0t8CuFZ0x/2SDm0iuTwrM5pnVOg=="], - - "@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.22.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-wJU4IBQMUikdJAcTChLFqK5lo+flo7pahqd8DSLv7uMxsdOdAHj6RzKYAm8pPfUS6ItKYutYyuicwKaFwQKsoA=="], - - "@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-vMCSh8kolEm5rRsc+FZeTZymWmIJwc40hjIKnXH4O0Dv/gAkJJIRXCsPX5cPbe0c0j/34+PsENd0HqKruwhVYw=="], - - "@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.61.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.36.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-lvrfWe9ShK/D2X4brmx8ZqqeWPfRl8xekU0FCn7C1dHm5k6+rTOOi36+4fnaHAP8lig9Ux6XQ1D4RNIpPCt1WQ=="], - - "@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-cEqpUocSKJfwDtLYTTJehRLWzkZ2eoePCxfVIgGkGkb83fMB71O+y4MvRHJPbeV2bdoWdOVrl8uO0+EynWhTEA=="], - - "@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.66.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-d7m9QnAY+4TCWI4q1QRkfrc6fo/92VwssaB1DzQfXNRvu51b78P+HJlWP7Qg6N6nkwdb9faMZNBCZJfftmszkw=="], - - "@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.59.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6/jWU+c1NgznkVLDU/2y0bXV2nJo3o9FWZ9mZ9nN6T/JBNRoMnVXZl2FdBmgH+a5MwaWLs5kmRJTP5oUVGIkPw=="], - - "@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.59.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/mysql": "2.15.27" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-r+V/Fh0sm7Ga8/zk/TI5H5FQRAjwr0RrpfPf8kNIehlsKf12XnvIaZi8ViZkpX0gyPEpLXqzqWD6QHlgObgzZw=="], - - "@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.59.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@opentelemetry/sql-common": "^0.41.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-n9/xrVCRBfG9egVbffnlU1uhr+HX0vF4GgtAB/Bvm48wpFgRidqD8msBMiym1kRYzmpWvJqTxNT47u1MkgBEdw=="], - - "@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.65.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.41.2", "@types/pg": "8.15.6", "@types/pg-pool": "2.0.7" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-W0zpHEIEuyZ8zvb3njaX9AAbHgPYOsSWVOoWmv1sjVRSF6ZpBqtlxBWbU+6hhq1TFWBeWJOXZ8nZS/PUFpLJYQ=="], - - "@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.61.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JnPexA034/0UJRsvH96B0erQoNOqKJZjE2ZRSw9hiTSC23LzE0nJE/u6D+xqOhgUhRnhhcPHq4MdYtmUdYTF+Q=="], - - "@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.32.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BQS6gG8RJ1foEqfEZ+wxoqlwfCAzb1ZVG0ad8Gfe4x8T658HJCLGLd4E4NaoQd8EvPfLqOXgzGaE/2U4ytDSWA=="], - - "@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.23.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/semantic-conventions": "^1.24.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-LL0VySzKVR2cJSFVZaTYpZl1XTpBGnfzoQPe2W7McS2267ldsaEIqtQY6VXs2KCXN0poFjze5110PIpxHDaDGg=="], - - "@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.38.2", "", {}, "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ=="], "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], - "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.41.0", "", { "os": "android", "cpu": "arm" }, "sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.41.0", "", { "os": "android", "cpu": "arm64" }, "sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g=="], @@ -251,14 +197,8 @@ "@posthog/core": ["@posthog/core@1.24.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA=="], - "@prisma/instrumentation": ["@prisma/instrumentation@7.4.2", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-r9JfchJF1Ae6yAxcaLu/V1TGqBhAuSDe3mRNOssBfx1rMzfZ4fdNvrgUBwyb/TNTGXFxlH9AZix5P257x07nrg=="], - - "@sentry/bun": ["@sentry/bun@10.45.0", "", { "dependencies": { "@sentry/core": "10.45.0", "@sentry/node": "10.45.0" } }, "sha512-f9+24fp1Ew09qcqQ2DMKo0Dh/4WIE8G4D3/CLh8pgdwjP8A+5cvTQLcdUsqZll361P4jmXS+ZEi5KlmPv/ddGw=="], - "@sentry/core": ["@sentry/core@10.45.0", "", {}, "sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q=="], - "@sentry/node": ["@sentry/node@10.45.0", "", { "dependencies": { "@fastify/otel": "0.17.1", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.6.0", "@opentelemetry/core": "^2.6.0", "@opentelemetry/instrumentation": "^0.213.0", "@opentelemetry/instrumentation-amqplib": "0.60.0", "@opentelemetry/instrumentation-connect": "0.56.0", "@opentelemetry/instrumentation-dataloader": "0.30.0", "@opentelemetry/instrumentation-express": "0.61.0", "@opentelemetry/instrumentation-fs": "0.32.0", "@opentelemetry/instrumentation-generic-pool": "0.56.0", "@opentelemetry/instrumentation-graphql": "0.61.0", "@opentelemetry/instrumentation-hapi": "0.59.0", "@opentelemetry/instrumentation-http": "0.213.0", "@opentelemetry/instrumentation-ioredis": "0.61.0", "@opentelemetry/instrumentation-kafkajs": "0.22.0", "@opentelemetry/instrumentation-knex": "0.57.0", "@opentelemetry/instrumentation-koa": "0.61.0", "@opentelemetry/instrumentation-lru-memoizer": "0.57.0", "@opentelemetry/instrumentation-mongodb": "0.66.0", "@opentelemetry/instrumentation-mongoose": "0.59.0", "@opentelemetry/instrumentation-mysql": "0.59.0", "@opentelemetry/instrumentation-mysql2": "0.59.0", "@opentelemetry/instrumentation-pg": "0.65.0", "@opentelemetry/instrumentation-redis": "0.61.0", "@opentelemetry/instrumentation-tedious": "0.32.0", "@opentelemetry/instrumentation-undici": "0.23.0", "@opentelemetry/resources": "^2.6.0", "@opentelemetry/sdk-trace-base": "^2.6.0", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.4.2", "@sentry/core": "10.45.0", "@sentry/node-core": "10.45.0", "@sentry/opentelemetry": "10.45.0", "import-in-the-middle": "^3.0.0" } }, "sha512-Kpiq9lRGnJc1ex8SwxOBl+FLQNl4Y137BydVooP7AFiAYZ6ftwHsIEF1bcYXaipHMT1YHS2bdhC2UQaaB2jkuQ=="], - "@sentry/node-core": ["@sentry/node-core@10.45.0", "", { "dependencies": { "@sentry/core": "10.45.0", "@sentry/opentelemetry": "10.45.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/context-async-hooks", "@opentelemetry/core", "@opentelemetry/instrumentation", "@opentelemetry/resources", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-KQZEvLKM344+EqXiA9HIzWbW5hzq6/9nnFUQ8niaBPoOgR9AiJhrccfIscfgb8vjkriiEtzE03OW/4h1CTgZ3Q=="], "@sentry/opentelemetry": ["@sentry/opentelemetry@10.45.0", "", { "dependencies": { "@sentry/core": "10.45.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-PmuGO+p/gC3ZQ8ddOeJ5P9ApnTTm35i12Bpuyb13AckCbNSJFvG2ggZda35JQOmiFU0kKYiwkoFAa8Mvj9od3Q=="], @@ -275,22 +215,12 @@ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ=="], - "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], - "@types/node": ["@types/node@22.18.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], - - "@types/pg-pool": ["@types/pg-pool@2.0.7", "", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="], - - "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], @@ -381,8 +311,6 @@ "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], @@ -429,8 +357,6 @@ "findup-sync": ["findup-sync@4.0.0", "", { "dependencies": { "detect-file": "^1.0.0", "is-glob": "^4.0.0", "micromatch": "^4.0.2", "resolve-dir": "^1.0.1" } }, "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ=="], - "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], - "fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], @@ -555,8 +481,6 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -585,24 +509,10 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - - "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], - - "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], - - "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], - - "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], - - "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "posthog-node": ["posthog-node@5.28.5", "", { "dependencies": { "@posthog/core": "1.24.1" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-+8H7rMPB48cwKhzZmq0EQXDFWjdT6yQrecq+f7c7m19HMzyvYtm54I7vNncEr9lrkzczc4kcePxwLD4qgt/YXg=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -611,8 +521,6 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], - "resolve-dir": ["resolve-dir@1.0.1", "", { "dependencies": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" } }, "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], @@ -697,8 +605,6 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -711,18 +617,12 @@ "@conventional-changelog/git-client/@simple-libs/stream-utils": ["@simple-libs/stream-utils@1.2.0", "", {}, "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA=="], - "@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="], - - "@fastify/otel/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], - "@inquirer/core/cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], "@inquirer/external-editor/chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], - "@simple-release/core/@conventional-changelog/git-client": ["@conventional-changelog/git-client@2.5.1", "", { "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", "@simple-libs/stream-utils": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.1.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-lAw7iA5oTPWOLjiweb7DlGEMDEvzqzLLa6aWOly2FSZ64IwLE8T458rC+o+WvI31Doz6joM7X2DoNog7mX8r4A=="], "@simple-release/core/@simple-libs/hosted-git-info": ["@simple-libs/hosted-git-info@1.0.1", "", {}, "sha512-R8/PbLKPw5iLeULJgnvwIKkggeyzwXPFERp+4itc0zSrG4sUTf23w7JlMSImPD2o+ZRpEv7YgG6IXgSk87jTgw=="], @@ -731,16 +631,8 @@ "@simple-release/core/conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@9.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA=="], - "@types/connect/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], - "@types/conventional-commits-parser/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], - "@types/mysql/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], - - "@types/pg/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], - - "@types/tedious/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], - "bun-types/@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], "commitizen/inquirer": ["inquirer@8.2.5", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^7.0.0" } }, "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ=="], @@ -771,30 +663,12 @@ "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="], - - "@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], - - "@fastify/otel/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], - - "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], - - "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], - "@simple-release/core/conventional-changelog/conventional-changelog-writer": ["conventional-changelog-writer@8.2.0", "", { "dependencies": { "conventional-commits-filter": "^5.0.0", "handlebars": "^4.7.7", "meow": "^13.0.0", "semver": "^7.5.2" }, "bin": { "conventional-changelog-writer": "dist/cli/index.js" } }, "sha512-Y2aW4596l9AEvFJRwFGJGiQjt2sBYTjPD18DdvxX9Vpz0Z7HQ+g1Z+6iYDAm1vR3QOJrDBkRHixHK/+FhkR6Pw=="], "@simple-release/core/conventional-changelog/conventional-commits-parser": ["conventional-commits-parser@6.2.0", "", { "dependencies": { "meow": "^13.0.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" } }, "sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag=="], - "@types/connect/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - "@types/conventional-commits-parser/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - "@types/mysql/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - - "@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - - "@types/tedious/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - "bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], "commitizen/inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -825,8 +699,6 @@ "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "@fastify/otel/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "commitizen/inquirer/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "commitizen/inquirer/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], diff --git a/package.json b/package.json index 2d7c2601..6321c19c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@commitlint/cli": "20.5.0", "@commitlint/config-conventional": "20.5.0", "@commitlint/cz-commitlint": "20.5.0", - "@sentry/bun": "10.45.0", + "@sentry/node-core": "10.45.0", "@simple-release/npm": "2.3.0", "@types/bun": "1.3.11", "commitizen": "4.3.1", diff --git a/src/cli.ts b/src/cli.ts index 65208ca4..67338adf 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,23 +11,11 @@ import { registerLoginCommand } from "./commands/login"; import { registerPluginCommand } from "./commands/plugin/index"; import { registerReviewContextCommand } from "./commands/review-context"; import { registerSessionContextCommand } from "./commands/session-context/index"; -import { registerTelemetryCommand } from "./commands/telemetry"; import { registerUpgradeCommand } from "./commands/upgrade"; import { installGit } from "./helpers/git"; import { logError } from "./helpers/log"; import { createPathIfNotExists, paths } from "./helpers/paths"; import { isSupportedPlatform } from "./helpers/platform"; -import { - addBreadcrumb, - captureException, - flushSentry, - initSentry, -} from "./helpers/sentry"; -import { - flushTelemetry, - initTelemetry, - trackCommand, -} from "./helpers/telemetry"; import { checkForUpdatesIfNeeded } from "./helpers/update-check"; if (typeof Bun === "undefined") @@ -46,22 +34,11 @@ createPathIfNotExists(paths.cacheFolder); async function main() { await installGit(); - // Initialize telemetry and error tracking (no-op if opted out) - initTelemetry(); - initSentry(); - const program = new Command() .name("archgate") .version(packageJson.version) .description("AI governance for software development"); - // Track which command is being executed and add Sentry breadcrumb - program.hook("preAction", (thisCommand) => { - const fullCommand = getFullCommandName(thisCommand); - trackCommand(fullCommand); - addBreadcrumb("command", `Running: ${fullCommand}`); - }); - registerInitCommand(program); registerLoginCommand(program); registerAdrCommand(program); @@ -71,7 +48,6 @@ async function main() { registerPluginCommand(program); registerUpgradeCommand(program); registerCleanCommand(program); - registerTelemetryCommand(program); const isUpgrade = process.argv.includes("upgrade"); const updateCheckPromise = isUpgrade @@ -80,32 +56,9 @@ async function main() { await program.parseAsync(process.argv); const notice = await updateCheckPromise; if (notice) console.log(notice); - - // Flush telemetry and Sentry events before exit - await flushTelemetry(); - await flushSentry(); -} - -/** - * Reconstruct the full command name from Commander's command chain. - * E.g., "adr create" from the "create" subcommand of "adr". - */ -function getFullCommandName(command: Command): string { - const parts: string[] = []; - let current: Command | null = command; - while (current) { - const name = current.name(); - if (name && name !== "archgate") { - parts.unshift(name); - } - current = current.parent as Command | null; - } - return parts.join(" ") || "root"; } -main().catch(async (err: unknown) => { - captureException(err, { command: "main" }); - await flushSentry(); +main().catch((err: unknown) => { logError(String(err)); process.exit(2); }); diff --git a/src/helpers/sentry.ts b/src/helpers/sentry.ts index 66671b6a..247f2175 100644 --- a/src/helpers/sentry.ts +++ b/src/helpers/sentry.ts @@ -1,10 +1,10 @@ /** - * sentry.ts — Error tracking via @sentry/bun SDK. + * sentry.ts — Error tracking via @sentry/node-core light mode. * - * Uses the official Sentry SDK for Bun to capture errors with full - * breadcrumb context. Breadcrumbs are added throughout CLI execution - * (command start, config loading, rule checks, etc.) so that crash - * reports include the sequence of operations leading to the failure. + * Uses Sentry's lightweight "light" SDK variant which excludes all + * OpenTelemetry auto-instrumentation — ideal for a CLI that only needs + * error capture with breadcrumbs. This avoids pulling in 600+ modules + * of OTel instrumentation for MongoDB, Redis, Express, etc. * * IP anonymization: the Sentry project has "Prevent Storing of IP Addresses" * enabled server-side. @@ -15,7 +15,7 @@ import { join } from "node:path"; -import * as Sentry from "@sentry/bun"; +import * as Sentry from "@sentry/node-core/light"; import packageJson from "../../package.json"; import { logDebug } from "./log"; @@ -83,10 +83,6 @@ export function initSentry(): void { environment: Bun.env.NODE_ENV ?? "production", // Do not send default PII (hostnames, IPs, etc.) sendDefaultPii: false, - // Enable tracing so sentry-trace headers propagate to the plugins service - tracesSampleRate: 1.0, - // Propagate traces to the plugins API for distributed tracing - tracePropagationTargets: ["plugins.archgate.dev"], // Set the anonymous install ID as the user initialScope: { user: { id: getInstallId() }, @@ -101,9 +97,6 @@ export function initSentry(): void { }, contexts: { runtime: { name: "bun", version: Bun.version } }, }, - // Keep default integrations including Http/Undici for distributed tracing - // (sentry-trace headers are auto-injected into fetch calls matching - // tracePropagationTargets above) // Limit breadcrumbs to keep payloads small maxBreadcrumbs: 50, }); From 057d9a981c4cbdade38d42a7b4f63657d83656e5 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 03:12:59 +0100 Subject: [PATCH 12/13] fix(sentry): report archgate path instead of bun runtime path, fix runtime name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - install_path: use Bun.main (entry script) instead of process.execPath (which returns the bun binary, e.g. .proto/tools/bun/1.3.11/bun.exe) - runtime: filter out NodeContext integration which overrides our runtime context with "node" — we set it explicitly to "bun" - install_method detection also uses archgate path instead of bun path --- src/helpers/sentry.ts | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/helpers/sentry.ts b/src/helpers/sentry.ts index 247f2175..54e90e2c 100644 --- a/src/helpers/sentry.ts +++ b/src/helpers/sentry.ts @@ -37,18 +37,34 @@ const SENTRY_DSN = // Install method detection // --------------------------------------------------------------------------- -function detectInstallMethod(): string { +/** + * The path to the archgate executable or script. + * - Compiled binary: process.execPath IS the archgate binary + * - bun run / bunx: Bun.main is the entry script (src/cli.ts or similar) + * - proto: process.argv[1] points to the shim + */ +function getArchgatePath(): string { + // When compiled, process.execPath is the archgate binary itself + // (not the bun runtime). Detect by checking if execPath contains "bun". const execPath = process.execPath; + if (!execPath.includes("bun")) return execPath; + + // When running via `bun run`, Bun.main is the entry script + return Bun.main; +} + +function detectInstallMethod(): string { + const archgatePath = getArchgatePath(); const binDir = internalPath("bin"); - if (execPath.startsWith(binDir)) return "binary"; + if (archgatePath.startsWith(binDir)) return "binary"; const home = Bun.env.HOME ?? Bun.env.USERPROFILE ?? "~"; const protoHome = Bun.env.PROTO_HOME ?? join(home, ".proto"); const protoToolDir = join(protoHome, "tools", "archgate"); - if (execPath.startsWith(protoToolDir)) return "proto"; + if (archgatePath.startsWith(protoToolDir)) return "proto"; - if (execPath.includes("node_modules")) return "local"; + if (archgatePath.includes("node_modules")) return "local"; return "package-manager"; } @@ -93,10 +109,16 @@ export function initSentry(): void { is_ci: String(Boolean(Bun.env.CI)), is_tty: String(Boolean(process.stdout.isTTY)), install_method: detectInstallMethod(), - install_path: process.execPath, + install_path: getArchgatePath(), + }, + contexts: { + // Override the default "node" runtime that @sentry/node-core sets + runtime: { name: "bun", version: Bun.version }, }, - contexts: { runtime: { name: "bun", version: Bun.version } }, }, + // Override the default nodeContextIntegration which reports runtime as "node" + integrations: (defaults) => + defaults.filter((i) => i.name !== "NodeContext"), // Limit breadcrumbs to keep payloads small maxBreadcrumbs: 50, }); From 72e6d726f4e1cb82ff04b25cca8d657bf53f3aa2 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sun, 22 Mar 2026 03:40:43 +0100 Subject: [PATCH 13/13] chore: disable PostHog analytics temporarily, keep Sentry only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostHog signup not available yet. The telemetry module and posthog-node dependency are kept for when PostHog is ready — just not wired into cli.ts. Sentry error tracking remains active. Also fixes cli.ts telemetry integration that was accidentally reverted during the binary size comparison. --- src/cli.ts | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index 67338adf..42216ec5 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,11 +11,18 @@ import { registerLoginCommand } from "./commands/login"; import { registerPluginCommand } from "./commands/plugin/index"; import { registerReviewContextCommand } from "./commands/review-context"; import { registerSessionContextCommand } from "./commands/session-context/index"; +import { registerTelemetryCommand } from "./commands/telemetry"; import { registerUpgradeCommand } from "./commands/upgrade"; import { installGit } from "./helpers/git"; import { logError } from "./helpers/log"; import { createPathIfNotExists, paths } from "./helpers/paths"; import { isSupportedPlatform } from "./helpers/platform"; +import { + addBreadcrumb, + captureException, + flushSentry, + initSentry, +} from "./helpers/sentry"; import { checkForUpdatesIfNeeded } from "./helpers/update-check"; if (typeof Bun === "undefined") @@ -34,11 +41,20 @@ createPathIfNotExists(paths.cacheFolder); async function main() { await installGit(); + // Initialize error tracking (no-op if opted out) + initSentry(); + const program = new Command() .name("archgate") .version(packageJson.version) .description("AI governance for software development"); + // Add Sentry breadcrumb for each command execution + program.hook("preAction", (thisCommand) => { + const fullCommand = getFullCommandName(thisCommand); + addBreadcrumb("command", `Running: ${fullCommand}`); + }); + registerInitCommand(program); registerLoginCommand(program); registerAdrCommand(program); @@ -48,6 +64,7 @@ async function main() { registerPluginCommand(program); registerUpgradeCommand(program); registerCleanCommand(program); + registerTelemetryCommand(program); const isUpgrade = process.argv.includes("upgrade"); const updateCheckPromise = isUpgrade @@ -56,9 +73,31 @@ async function main() { await program.parseAsync(process.argv); const notice = await updateCheckPromise; if (notice) console.log(notice); + + // Flush Sentry events before exit + await flushSentry(); +} + +/** + * Reconstruct the full command name from Commander's command chain. + * E.g., "adr create" from the "create" subcommand of "adr". + */ +function getFullCommandName(command: Command): string { + const parts: string[] = []; + let current: Command | null = command; + while (current) { + const name = current.name(); + if (name && name !== "archgate") { + parts.unshift(name); + } + current = current.parent as Command | null; + } + return parts.join(" ") || "root"; } -main().catch((err: unknown) => { +main().catch(async (err: unknown) => { + captureException(err, { command: "main" }); + await flushSentry(); logError(String(err)); process.exit(2); });