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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/cli-docs/src/fragments/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ export SENTRY_DISABLE_TIPS=1 # per-session, overrides the stored preference

Only the tips are removed. Pagination hints, dry-run and cancellation notices, and the cache-age footer (`cached · 3m ago · use -f to refresh`) are still shown.

## Telemetry

The CLI sends usage data and error reports about itself to Sentry. To opt out, set `SENTRY_CLI_NO_TELEMETRY=1` or `DO_NOT_TRACK=1`, or run `sentry cli defaults telemetry off`.

When telemetry can't be sent, for example while offline or when a firewall blocks Sentry, the CLI keeps it in its local database and sends up to 10 stored envelopes on each later run. The cache holds at most 100 envelopes and 4 MiB, compressed. When it's full, the oldest usage data goes first, then the oldest error reports. It discards error reports after 30 days and everything else after 4 days. Metrics, such as timings, aren't cached: when they can't be sent, they're dropped.

After the first failed send, the CLI stops sending for 5 minutes, so an unreachable Sentry delays a command's exit by about 3 seconds at most. Long-running commands, such as `sentry mcp` and `sentry local`, then try one send every 5 minutes, and send up to 10 stored envelopes once one succeeds.

Opting out deletes the cache on the next run. The library (`createSentrySDK`) never caches telemetry.

## Global Options

These flags are accepted by every command. They are not shown in individual command `--help` output, but are always available.
Expand Down
27 changes: 25 additions & 2 deletions packages/cli/src/lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import { logger } from "../logger.js";
import { storedCredentialIdentity } from "./identity-hash.js";
import type { Database } from "./sqlite.js";

export const CURRENT_SCHEMA_VERSION = 20;
export const CURRENT_SCHEMA_VERSION = 21;

/** Environment variable to disable auto-repair */
const NO_AUTO_REPAIR_ENV = "SENTRY_CLI_NO_AUTO_REPAIR";

type SqliteType = "TEXT" | "INTEGER";
type SqliteType = "TEXT" | "INTEGER" | "BLOB";

export type ColumnDef = {
type: SqliteType;
Expand Down Expand Up @@ -328,6 +328,23 @@ export const TABLE_SCHEMAS: Record<string, TableSchema> = {
result_count: { type: "INTEGER", notNull: true },
},
},
/**
* Telemetry envelopes that failed to send, kept for a later run to retry
* (`src/lib/telemetry/offline-store.ts`). `envelope` holds the serialized
* envelope bytes; `size` is their length, for the byte cap. `is_error` is 1
* when the envelope has an error event, which is kept longer and dropped
* last over the caps. Rows past their max age are deleted on read. Dropping
* the table only loses telemetry that may never have been delivered anyway.
*/
telemetry_envelope_queue: {
columns: {
id: { type: "INTEGER", primaryKey: true },
created_at: { type: "INTEGER", notNull: true },
is_error: { type: "INTEGER", notNull: true, default: "0" },
size: { type: "INTEGER", notNull: true },
envelope: { type: "BLOB", notNull: true },
},
},
};

/** Generate CREATE TABLE DDL from column definitions */
Expand Down Expand Up @@ -1172,6 +1189,12 @@ export function runMigrations(db: Database): void {
db.exec(EXPECTED_TABLES.completion_cache as string);
}

// Migration 20 -> 21: Add telemetry_envelope_queue for telemetry envelopes
// that failed to send, retried by the next run.
if (currentVersion < 21) {
db.exec(EXPECTED_TABLES.telemetry_envelope_queue as string);
}

if (currentVersion < CURRENT_SCHEMA_VERSION) {
db.query("UPDATE schema_version SET version = ?").run(
CURRENT_SCHEMA_VERSION,
Expand Down
28 changes: 26 additions & 2 deletions packages/cli/src/lib/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import { CLI_VERSION, SENTRY_CLI_DSN } from "../constants.js";
import { logger } from "../logger.js";
import { isTelemetryEnabled } from "../telemetry.js";
import { redactTelemetryEnvelope } from "../telemetry/credential-redaction.js";
import { clearStoredEnvelopes } from "../telemetry/offline-store.js";
import {
makeStoringTransport,
STORED_ENVELOPES_PER_RUN,
type StoringTransport,
} from "../telemetry/offline-transport.js";
import { makeNodeTransportWithDeadline } from "../telemetry/zstd-transport.js";
import type { McpServerConfig } from "./config.js";
import { reportCliError } from "../error-reporting.js";
Expand Down Expand Up @@ -245,6 +251,13 @@ export function initMcpTelemetry(config: McpServerConfig): void {
"app.upstream.host": config.sentryHost,
"app.url.full": config.mcpUrl,
};
// Unsent telemetry shares the CLI's store, so opting out clears it, as in
// `withTelemetry()`.
const storeTelemetry = isTelemetryEnabled();
if (!storeTelemetry) {
clearStoredEnvelopes();
}
Comment thread
sentry-junior[bot] marked this conversation as resolved.
Resolved
let storingTransport: StoringTransport | undefined;

// Always the CLI's own project, never a DSN from the environment: an MCP
// server started from a project shell or IDE inherits the app's SENTRY_DSN.
Expand All @@ -269,9 +282,17 @@ export function initMcpTelemetry(config: McpServerConfig): void {
release: CLI_VERSION,
// Same credential redaction and request deadline as the CLI's own
// telemetry: a stalled ingest socket must not keep the process alive
// after the client disconnects (#1956).
// after the client disconnects (#1956). Envelopes that fail to send are
// stored like the CLI's own.
transport: (transportOptions) => {
const transport = makeNodeTransportWithDeadline(transportOptions);
if (storeTelemetry) {
storingTransport = makeStoringTransport(
transportOptions,
makeNodeTransportWithDeadline,
);
}
const transport =
storingTransport ?? makeNodeTransportWithDeadline(transportOptions);
return {
send: (envelope) => transport.send(redactTelemetryEnvelope(envelope)),
flush: (timeout) => transport.flush(timeout),
Expand All @@ -292,6 +313,9 @@ export function initMcpTelemetry(config: McpServerConfig): void {
if (client) {
setCurrentClient(client);
}
// Retry what earlier runs failed to send; `close()` on disconnect waits
// for it. The breaker sends another batch whenever ingest recovers.
storingTransport?.sendStored(STORED_ENVELOPES_PER_RUN);
}

/**
Expand Down
27 changes: 26 additions & 1 deletion packages/cli/src/lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ import {
} from "@sentry/toolkit-core/games";
import { getSentryBaseUrl, isSentrySaasUrl } from "./sentry-urls.js";
import { redactTelemetryEnvelope } from "./telemetry/credential-redaction.js";
import { clearStoredEnvelopes } from "./telemetry/offline-store.js";
import {
makeStoringTransport,
STORED_ENVELOPES_PER_RUN,
type StoringTransport,
} from "./telemetry/offline-transport.js";
import { makeCompressedTransport } from "./telemetry/zstd-transport.js";

export type { Span } from "@sentry/core";
Expand Down Expand Up @@ -215,9 +221,18 @@ export async function withTelemetry<T>(
const enabled = isTelemetryEnabled();
const client = initSentry(enabled, options);
if (!client?.getOptions().enabled) {
// Opting out also discards telemetry stored by earlier runs. Library mode
// never touches the store: it belongs to the CLI, not the host.
if (!(enabled || options?.libraryMode)) {
clearStoredEnvelopes();
}
return await callback(undefined);
}

// Retry telemetry that earlier runs failed to send. The sends run in the
// background; the exit flush in createBeforeExitHandler waits for them.
storingTransport?.sendStored(STORED_ENVELOPES_PER_RUN);

// Initialize user and instance context
await initTelemetryContext();

Expand Down Expand Up @@ -494,6 +509,9 @@ const hasGetSystemErrorMap = (() => {
/** Current beforeExit handler, tracked so it can be replaced on re-init */
let currentBeforeExitHandler: (() => void) | null = null;

/** Transport of the current client, unless library mode left the store out. */
let storingTransport: StoringTransport | undefined;

let manualHttpTracing = false;

/**
Expand Down Expand Up @@ -665,8 +683,15 @@ export function initSentry(
// smaller payloads, faster compress/decompress on both sides.
// Automatic gzip fallback when running on Node < 22.15, where
// `node:zlib`'s zstd support is unavailable.
// Outside library mode, envelopes that fail to send are stored for the
// next run (telemetry/offline-transport.ts). Library mode must not write
// the host's disk or add work to its process.
transport: (transportOptions) => {
const transport = makeCompressedTransport(transportOptions);
storingTransport = libraryMode
? undefined
: makeStoringTransport(transportOptions);
const transport =
storingTransport ?? makeCompressedTransport(transportOptions);
return {
// The SDK adds log scope attributes after beforeSendLog and skips
// beforeSend for internal errors. Redact at the final delivery boundary.
Expand Down
Loading
Loading