/** * Dev-server connection-phase metrics. * * Records counters and histograms for the dev-server endpoints that the editor * and SABS hit during a live-edit connection: `/_sb_health`, `/_sb_connect`, * `/_sb_activate`, `/_sb_disconnect`, `/_sb_status`, `/_sb_activity`, * `/_sb_update_token`, plus WebSocket upgrades, Vite eager-init, and * warm-standby activation. * * Metrics are scoped to the dev-server's existing OTel pipeline. When telemetry * has not yet initialized (warm-standby pre-activation), record calls are * buffered and replayed by `flush()` once `configureTelemetry` completes, so * warm-standby observations are not silently dropped into the no-op meter. */ /** * Normalize an npm/pnpm error code into a bounded metric-label value. Known * codes pass through (keeping the diagnostic facet); everything else collapses * to `"other"` (or `"none"` when absent) so the * `dev_server_initial_install_failure_total` series count stays bounded. */ export declare function normalizeNpmErrorCodeTag(code: string | undefined): string; /** * Normalize the CLI parent's `SUPERBLOCKS_RESTART_REASON` env into a bounded * `boot_kind` metric label for `dev_server_boot_total`: * - absent/empty/whitespace -> `"cold_start"` (first boot or a non-restart * respawn; the parent only stamps the reason on a post-flip respawn) * - `RESTART_REASON_FLIP_RESTART` -> passes through (the post-flip in-place * restart we want to prove actually booted) * - anything else -> `"other"` * * The reason comes from process env, which an operator or a stale value could * set to anything, so the unknown branch collapses to `"other"` to keep the * `boot_kind` series count bounded (cardinality guard). */ export declare function normalizeBootKindTag(restartReason: string | undefined): string; /** * Where a recoverable shutdown error was caught and suppressed instead of * exiting the process. A closed enum so the `handler` label on * `dev_server_recoverable_shutdown_error_total` stays low-cardinality. */ export type RecoverableShutdownHandler = "uncaughtException" | "unhandledRejection" | "vite_teardown"; /** * Normalize a recoverable-shutdown error's `code` into a bounded metric-label * value: known close codes pass through (keeping the diagnostic facet), * anything else collapses to `"other"`, and a missing/non-primitive code maps * to `"none"`. */ export declare function normalizeRecoverableShutdownCodeTag(code: unknown): string; /** Dev-server endpoints that produce per-request metrics. */ export type DevServerEndpoint = "_sb_health" | "_sb_connect" | "_sb_activate" | "_sb_disconnect" | "_sb_status" | "_sb_activity" | "_sb_update_token" | "_sb_persist" | "_sb_reload_file" | "_sb_ready"; /** * Coarse failure type for dev-server endpoint outcomes. Distinct from raw HTTP * status so dashboards can filter by reason without high-cardinality labels. */ export type DevServerFailureType = "none" | "auth" | "validation" | "branch_mismatch" | "vite_init" | "internal"; /** * Outcome of the warm-standby activation handshake. * * accepted - 200 OK, transitioning to full server * rejected - 400/413 (validation, body too large) * error - JSON parse / unexpected exception */ export type WarmActivationOutcome = "accepted" | "rejected" | "error"; /** * Records dev-server connection-phase metrics. * * Calls made before `configureTelemetry` completes are queued and replayed by * `flush()` once telemetry is online. Without this, warm-standby observations * are routed through `getMeter()`'s no-op fallback meter and silently dropped * — defeating the metric's purpose, since warm activation is exactly the path * we want to observe. The buffer is bounded to bound memory; instruments are * still NOT cached, so each replayed call resolves a fresh meter. */ declare class DevServerMetrics { /** * Pre-init replay queue. Entries are bound `() => void` recorders that close * over their args at call time, so labels are captured even if the buffer is * drained much later. */ private bufferedRecorders; /** Set once `recordBoot` has been called, so the boot metric is emitted at * most once per process regardless of how many init paths invoke it. */ private booted; /** * Routes a recorder to the live OTel meter when telemetry is up, or buffers * it for replay when telemetry has not yet initialized. */ private record; /** * Replays buffered recorders against the (now-initialized) meter and clears * the queue. Idempotent — safe to call multiple times. Callers should invoke * this immediately after `configureTelemetry()` resolves. * * No-ops if telemetry did not actually come up (e.g., `configureTelemetry` * returned `false` due to a missing config or an internal init failure). * Draining the buffer through a still-no-op meter would silently lose every * observation AND clear the queue, so a later successful re-init via * `onTokenReceived` could not recover them. Keeping the buffer intact lets * a subsequent `flush()` call replay them once telemetry is genuinely live. */ flush(): void; /** * Records a dev-server process boot, tagged by `boot_kind`. * * Emitted once per process at startup. Pass the raw * `SUPERBLOCKS_RESTART_REASON` env value (the CLI parent stamps * `RESTART_REASON_FLIP_RESTART` on a post-flip in-place respawn and leaves it * unset otherwise); it is normalized to a bounded `boot_kind` label * (`cold_start` | `flip_restart` | `other`). * * This is the completion half of the in-place-restart telemetry: the * server-side decision metric proves we *chose* to self-restart, and a * `dev_server_boot_total{boot_kind="flip_restart"}` increment proves the * child actually booted afterward, so the two can be compared in Datadog. */ recordBoot(restartReason: string | undefined): void; /** * Records the outcome of a dev-server HTTP endpoint request. */ recordEndpoint(endpoint: DevServerEndpoint, method: string, status: number, durationMs: number, failureType?: DevServerFailureType): void; /** * Records a WebSocket upgrade attempt against the dev-server's HTTP socket. */ recordSocketUpgrade(outcome: "accepted" | "rejected"): void; /** * Records the duration and outcome of Vite eager-init (the call that creates * the user-facing Vite server during /_sb_connect ramp-up). */ recordViteEagerInit(durationMs: number, outcome: "success" | "error"): void; /** * Records a warm-standby activation request outcome. */ recordWarmActivation(outcome: WarmActivationOutcome): void; /** * Records the time from receiving a successful /_sb_activate POST to the * full Express dev-server being attached to the warm HTTP server (the * "no-port-gap" handoff window — gateways see 502s if this stretches). */ recordWarmHandoff(durationMs: number): void; /** * Emits a dependency-failure counter with the standard low-cardinality tags. * Shared by the initial-install and auto-upgrade degrade paths, which differ * only in counter name + description. * * All tags are low-cardinality: * - `category` is a closed enum from the matching error type. * - `npm_error_code` is a bounded npm error code string (e.g. "E401", * "ERESOLVE") or `"none"` when absent. * - `has_any_registry_configured` is `"true"` / `"false"` / `"unknown"`. * - `outcome`, when the caller supplies one, is a closed enum owned by that * caller (see `recordUpgradeFailure`). * * Every label a caller passes must be destructured here AND placed into * `labels`. This helper is the only thing that reaches `counter.add`, so a * field accepted by a public wrapper but omitted here is silently dropped on * the way to the exporter — and TypeScript cannot catch it, because `labels` * is a typed variable rather than a fresh object literal at the call site. * That is exactly how `outcome` went missing. * * Do NOT pass raw package names, registry hostnames, or rawError — those are * high-cardinality or contain customer-infra identifiers. */ private recordDependencyFailure; /** Records a degraded startup caused by a failed initial dependency install. */ recordInitialInstallFailure(labels: { category: string; npmErrorCode?: string; hasAnyRegistryConfigured?: boolean; }): void; /** Records a degraded startup where a best-effort CLI/library auto-upgrade * failed and the pod kept serving the currently installed version. */ recordUpgradeFailure(labels: { category: string; npmErrorCode?: string; hasAnyRegistryConfigured?: boolean; /** * What the failure did to this process: `degrade` kept the current version * running, `exit` terminated so the sandbox is replaced. Without this the * two outcomes are indistinguishable, so a crash-loop of exiting pods looks * identical to a fleet of pods quietly serving the older version. * Low-cardinality: a closed two-value enum. */ outcome?: "degrade" | "exit"; }): void; /** * Records a recoverable shutdown error that was suppressed (logged at warn, * process left alive) instead of triggering a fatal exit. This is the * alertable replacement for the `dev_server_fatal_exit` log token the old * fatal path emitted: without it, the only way to see these errors (or notice * their rate regressing) is a log search. * * Both labels are low-cardinality: `handler` is a closed enum, and the error's * `code` is normalized to the known socket / Vite close codes (or * `other`/`none`). */ recordRecoverableShutdownError(handler: RecoverableShutdownHandler, reason: unknown): void; /** * Records a Vite/esbuild build error suppressed instead of exiting. This is * the alertable replacement for the `dev_server_fatal_exit` log token when * optimizeDeps or transform failures would previously crash the pod. */ recordRecoverableBuildError(labels: { category: string; handler: RecoverableShutdownHandler; }): void; /** * Records an ENOSPC disk-recovery attempt and its outcome. Lets dashboards * and alerts answer "how often is the live-edit PVC filling up, and is * recovery actually rescuing the install?" without depending on log * ingestion (the PVC is destroyed at teardown, so logs can be lost). * * `recovered` is the only label — bounded cardinality (true/false). */ recordDiskRecovery({ recovered }: { recovered: boolean; }): void; } /** Process-wide dev-server metrics singleton. */ export declare const devServerMetrics: DevServerMetrics; export {}; //# sourceMappingURL=dev-server-metrics.d.mts.map