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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,10 @@ new OTLPTraceExporter({
## Design principles

- **Errors are 100%, everything else is sampled or aggregated.** Raw error
occurrences are always kept (14-day TTL); successful traces are expected to
be sampled upstream (0.5–1%); usage is stored as 1-minute rollups (90 days).
occurrences are always kept (14-day TTL); traces containing an error are
retained in full (the Node SDK tail-retains them), so every issue keeps the
trace that explains it; healthy traces are expected to be sampled upstream
(0.5–1%); usage is stored as 1-minute rollups (90 days).
- **Per-repo analysis.** Every row is keyed by `org_id` + `repository_id`.
- **Privacy by construction.** No cookies, no DOM, no request/response bodies,
no emails, no full URLs with query strings.
Expand Down
5 changes: 4 additions & 1 deletion deploy/single-server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ services:
# Not published to the host — only the ingester talks to it, over the
# internal compose network. Never expose 8123/9000 publicly.
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8123/ping"]
# 127.0.0.1, not localhost: in-container localhost resolves to ::1, but
# ClickHouse's IPv6 bind can silently fail (listen_try), leaving only
# 0.0.0.0 listening and the check refused forever.
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8123/ping"]
interval: 10s
timeout: 5s
retries: 10
Expand Down
5 changes: 4 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ services:
ulimits:
nofile: 262144
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8123/ping"]
# 127.0.0.1, not localhost: in-container localhost resolves to ::1, but
# ClickHouse's IPv6 bind can silently fail (listen_try) — e.g. on Docker
# Desktop for Mac — leaving only 0.0.0.0 listening and the check refused.
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8123/ping"]
interval: 5s
retries: 10

Expand Down
23 changes: 18 additions & 5 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ you from zero to seeing data in ClickHouse.
| Unhandled errors / rejections | ✅ automatic | ✅ automatic |
| Handled errors | `captureException(err)` | `captureException(err)` |
| Usage | session pings + `trackEvent()` | request counts/durations per route (automatic) |
| Traces | — (by design; no OTel in the browser) | ~1% sampled (configurable) |
| Traces | — (by design; no OTel in the browser) | ~1% sampled, plus **every erroring trace kept in full** |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [deterministic] markdownlint: MD013 — Risk: 30/100

Line length: Expected: 80; Actual: 108

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at docs/GETTING-STARTED.md:44: Line length: Expected: 80; Actual: 108

Flagged by Autter security & observability checks.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [deterministic] markdownlint: MD013 — Risk: 30/100

Line length: Expected: 80; Actual: 108

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at docs/GETTING-STARTED.md:44: Line length: Expected: 80; Actual: 108

Flagged by Autter security & observability checks.

| LLM usage & cost | — | `withLlmCall()` / Vercel AI SDK telemetry / GenAI semconv — always 100% (model, tokens, cost) |

**What is never sent:** cookies, DOM content, form values, request/response
Expand All @@ -58,6 +58,11 @@ cd autter-runtime
docker compose up # ClickHouse + ingester on :4318, key "dev-key"
```

> **That's all the clone is for.** It runs the ingester — you never add
> code to this checkout. Every step from here on (installing packages,
> creating `instrument.cjs`, …) happens in **your application's
> repository**, the app you want to monitor.

For real deployments, configure keys via env (or point
`AUTTER_KEY_VALIDATOR_URL` at your own key service):

Expand All @@ -78,11 +83,15 @@ Full config reference: [`packages/otlp-ingester`](../packages/otlp-ingester).

## 3. Instrument your backend

In **your app's repository** (not the `autter-runtime` checkout from
step 2):

```bash
npm install @autter/runtime-node
```

Create `instrument.cjs` — it must load **before** your app:
Create `instrument.cjs` in your app's root, next to its entry point — it
must load **before** your app:

```js
const { initAutterServer } = require("@autter/runtime-node");
Expand All @@ -101,8 +110,10 @@ node --require ./instrument.cjs server.js
```

That alone gives you: every incoming HTTP request traced-and-sampled,
request/error/duration rollups per route, and crashes captured. For
handled errors:
request/error/duration rollups per route, crashes captured, and the full
trace of any request that errors — erroring traces are retained even when
the sampler wouldn't have kept them, so every issue keeps the trace that
explains it. For handled errors:

```js
const { captureException } = require("@autter/runtime-node");
Expand Down Expand Up @@ -303,7 +314,9 @@ clickhouse-client --password dev`.
- [ ] `release` is wired to your git SHA in **both** frontend and backend —
it's what powers regression detection ("broke in release X").
- [ ] Keep trace sampling at ~1% (`traceSampleRate`) — errors are always
captured regardless.
captured regardless, and the full trace of an erroring request is
retained (`retainTracesOnError`, on by default), so cheap sampling
doesn't cost you debugging context.
- [ ] The relay route keeps its built-in per-IP rate limit (or your WAF
covers it: `perIpRateLimit: false`).
- [ ] Direct browser ingest: your CSP includes
Expand Down
12 changes: 12 additions & 0 deletions docs/INTEGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,15 @@ Errors are always worth sending; keep successful-trace sampling at ~1%
(`OTEL_TRACES_SAMPLER_ARG=0.01`). Request/usage metrics are derived
server-side from spans and the `http.server.duration` histogram — no extra
setup. LLM/GenAI spans are the exception — send them at 100% (see above).

One consequence of plain head sampling to know about: an error recorded on
an unsampled trace is dropped with it, and even when the error survives
through a separate path, the trace explaining it usually doesn't.
`@autter/runtime-node` handles both automatically — errors ride a dedicated
always-on tracer, and the full trace around an error is tail-retained even
when head sampling said no (`retainTracesOnError`, on by default). On other
stacks, route errors through an always-on tracer provider (same pattern as
the LLM guidance above), and if you want erroring traces kept in full, put
an OTel Collector with the `tail_sampling` processor (a
`status_code = ERROR` policy plus a small `probabilistic` one) in front of
the ingester instead of sampling in the SDK.
16 changes: 6 additions & 10 deletions packages/otlp-ingester/src/normalize-otlp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,16 +325,12 @@
if (llmCall) llmCalls.push(llmCall);

// Server spans fold into 1-minute usage rollups so traffic is
// tracked even when the metrics pipeline isn't wired —
// senders that DO export request metrics mark the resource
// with `autter.metrics_wired` and are skipped here, or every
// request they trace would count twice. The rollup key uses
// the normalized route: `http.route` is already a template,
// but the url.path/http.target fallback is a raw path whose
// id segments would explode the SummingMergeTree key space
// (and never line up with the metric-fed rows for the same
// endpoint).
if (kind === "server" && !resource.metricsWired) {
// tracked even when the metrics pipeline isn't wired. Spans
// exported by error-linked tail retention are skipped: the SDK
// ships those at ~100% alongside a metrics pipeline that already
// counts every request, so folding them in would double-count
// erroring routes.
if (kind === "server" && attrs.get("autter.tail_retained") !== "true") {

Check failure on line 333 in packages/otlp-ingester/src/normalize-otlp.ts

View check run for this annotation

Autter.dev / autter/review-gate

🔴 High · Wired server spans are double-counted in usage rollups

The rollup condition no longer checks `resource.metricsWired`, so non-tail-retained server spans from resources that also export request metrics are folded into span-fed rollups and counted again by the metric pipeline. Restore the metrics-wired exclusion while retaining the tail-retention check. Suggested fix: Keep the `!resource.metricsWired` exclusion for metric-wired resources, and treat the tail-retention marker according to its decoded OTLP type, accepting boolean true (and optionally the string "true" for compatibility). Add tests covering metric-wired regular spans and boolean `autter.tail_retained` spans in `normalizeTraces`. Blast radius — if this defect reaches production it can fail the downstream usage that depends on this file: functions `normalizeTraces`, `addToRollup`, `routeOf`, `spanDurationMs`, `statusCodeOf`, `severityOf`, `AutterServerOptions`, `AutterSeverity`; scopes `@autter/otlp-ingester`; dependent files `./fingerprint.js`, `./llm.js`, `./types.js`.

Check warning on line 333 in packages/otlp-ingester/src/normalize-otlp.ts

View check run for this annotation

Autter.dev / autter/review-gate

🟠 Medium · Source changes without matching tests

normalizeTraces and its addToRollup path now filter spans using autter.tail_retained, but no sibling test file is added or modified. Incorrect filtering could double-count erroring server routes or suppress legitimate usage rollups in the ingester and downstream usage tables. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `normalizeTraces`, `addToRollup`, `routeOf`, `spanDurationMs`, `statusCodeOf`, `severityOf`, `AutterServerOptions`, `AutterSeverity`; scopes `@autter/otlp-ingester`; dependent files `./fingerprint.js`, `./llm.js`, `./types.js`. Suggested fix: Add or modify a sibling test file for normalize-otlp.ts covering tail-retained server spans, metrics-wired resources, ordinary server spans, and preservation of non-server and non-retained rollups. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `normalizeTraces`, `addToRollup`, `routeOf`, `spanDurationMs`, `statusCodeOf`, `severityOf`, `AutterServerOptions`, `AutterSeverity`; scopes `@autter/otlp-ingester`; dependent files `./fingerprint.js`, `./llm.js`, `./types.js`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [ai] Tail-retained requests are dropped from fallback usage rollups — Risk: 78/100

This condition excludes every tail-retained server span from metricPoints, but the trace and metric pipelines are independent: /v1/traces calls normalizeTraces and stores its rollups, while /v1/metrics is a separate request that may be absent or fail. Therefore any unsampled request that later errors is exported with autter.tail_retained and contributes no request or error rollup when metrics are not wired, delayed, or unavailable, even though this code is specifically the fallback that is supposed to track usage without metrics. The change fixes double counting only when a matching metric histogram is actually delivered; without proving that, it causes systematic undercounting of erroring requests in a documented supported configuration.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Dependent files: packages/otlp-ingester/src/normalize-otlp.ts, packages/otlp-ingester/src/server.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Do not suppress the span-derived rollup solely from the tail-retained marker unless the ingester has positively established that the corresponding metric point was received and will be counted. Otherwise retain the fallback rollup, or add an explicit deduplication mechanism keyed by service/environment/release/route/time bucket.

Flagged by Autter security & observability checks.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [ai] Wired server spans are double-counted in usage rollups — Risk: 80/100

The rollup condition no longer checks resource.metricsWired, so non-tail-retained server spans from resources that also export request metrics are folded into span-fed rollups and counted again by the metric pipeline. Restore the metrics-wired exclusion while retaining the tail-retention check.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Functions/symbols: normalizeTraces, addToRollup, routeOf, spanDurationMs, statusCodeOf, severityOf, AutterServerOptions, AutterSeverity
  • Dependent files: ./fingerprint.js, ./llm.js, ./types.js
  • Scopes: @autter/otlp-ingester
🛠 AI fix prompt (copy & paste into your coding agent)
Keep the `!resource.metricsWired` exclusion for metric-wired resources, and treat the tail-retention marker according to its decoded OTLP type, accepting boolean true (and optionally the string "true" for compatibility). Add tests covering metric-wired regular spans and boolean `autter.tail_retained` spans in `normalizeTraces`. Blast radius — if this defect reaches production it can fail the downstream usage that depends on this file: functions `normalizeTraces`, `addToRollup`, `routeOf`, `spanDurationMs`, `statusCodeOf`, `severityOf`, `AutterServerOptions`, `AutterSeverity`; scopes `@autter/otlp-ingester`; dependent files `./fingerprint.js`, `./llm.js`, `./types.js`.

Flagged by Autter security & observability checks.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [ai] Runtime-node requests are double-counted after the metrics-wired guard is removed — Risk: 86/100

This is not safe to ship because the changed server-span branch now adds a span-derived request rollup for every ordinary server span unless it has autter.tail_retained, but the runtime-node caller still marks every resource autter.metrics_wired=true and still exports request histograms. The trace endpoint then persists the new span rollup and the metrics endpoint persists the exact histogram rollup, so each runtime-node request contributes to both runtime_metrics_1m feeds; only tail-retained error spans are excluded, leaving normal sampled requests and non-retained errors duplicated rather than preserving the prior metrics-wired contract.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Dependent files: packages/runtime-node/src/server.ts, packages/otlp-ingester/src/normalize-otlp.ts, packages/otlp-ingester/src/server.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Restore the `!resource.metricsWired` guard for ordinary server spans, or otherwise ensure runtime-node spans are excluded whenever its request-metrics pipeline is enabled while retaining the tail-retention exclusion.

Flagged by Autter security & observability checks.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [ai] Runtime server spans are now double-counted against the metrics pipeline — Risk: 86/100

This condition unconditionally creates a request rollup for every ordinary server span and no longer checks the resource's autter.metrics_wired flag. initAutterServer sets that flag to true and configures a periodic OTLP metric reader, so each sampled server span received from the runtime is accompanied by a metrics-pipeline request point. The ingester therefore adds the same request, error, and duration once from metrics and again from the span, inflating traffic and latency totals for the normal runtime-node deployment. The new tail-retained exclusion does not prevent this for healthy or head-sampled requests.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Dependent files: packages/otlp-ingester/src/normalize-otlp.ts, packages/runtime-node/src/server.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Restore the `!resource.metricsWired` guard for ordinary server-span rollups, or otherwise ensure runtime server spans are not folded when the sender exports request metrics.

Flagged by Autter security & observability checks.

addToRollup(rollups, {
service: resource.service,
environment: resource.environment,
Expand Down
15 changes: 14 additions & 1 deletion packages/runtime-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,23 @@ Defaults (cheap by construction):
| Signal | Default |
| --- | --- |
| Captured/unhandled exceptions | 100% (dedicated always-on tracer) |
| Traces containing an error | 100% (tail retention, `retainTracesOnError`) |
| `withProcessSpan` spans | 100% (same always-on tracer) |
| LLM/GenAI call spans | 100% (`llmTracing`, on by default) |
| Traces | 1% head sampling (`traceSampleRate`) |
| Healthy traces | 1% head sampling (`traceSampleRate`) |
| Request metrics | exported every 60 s |
| Logs | not collected |

**Error-linked trace retention.** Errors export at 100% while traces are
head-sampled — on its own that strands a retained error without the trace
that explains it. So unsampled spans are kept briefly in an in-process
buffer, and the moment a trace shows an error — a 5xx response, a recorded
exception, `captureException`, or an error-severity `captureMessage` — the
whole trace is exported, sampling lottery notwithstanding. The buffer is
bounded (256 spans per trace, 5 000 spans total, dropped as soon as the
request ends healthy, 30 s TTL), degrades to plain head sampling on
overflow, and never blocks. Disable with `retainTracesOnError: false`.

Crashes are observed via `process.uncaughtExceptionMonitor`, which does
**not** change your process's exit behaviour; the final flush is
best-effort. Framework instrumentations are opt-in:
Expand All @@ -102,6 +113,8 @@ Note on usage rollups: requests are counted from the `http.server.duration`
metric (100% accurate) and additionally from sampled server spans. At the
default 1% sampling the span contribution is negligible; if you set
`traceSampleRate: 1` in development, expect request counts roughly doubled.
Tail-retained error traces don't distort this: their spans carry
`autter.tail_retained` and the ingester keeps them out of span-fed rollups.

## 3. LLM tracing

Expand Down
Loading
Loading