Skip to content

feat(ingester): at-least-once sink delivery with bounded retry buffer - #13

Merged
sagnik11 merged 3 commits into
mainfrom
posthog/sink-at-least-once-delivery
Aug 18, 2026
Merged

feat(ingester): at-least-once sink delivery with bounded retry buffer#13
sagnik11 merged 3 commits into
mainfrom
posthog/sink-at-least-once-delivery

Conversation

@sagnik11

@sagnik11 sagnik11 commented Aug 17, 2026

Copy link
Copy Markdown
Member

Problem

The sink webhook forward was a single fire-and-forget fetch with a 10 s timeout and a console.warn on failure. Any consumer deploy, network blip, or slow downstream batch permanently lost the delivery — and with it the fingerprinted occurrences that feed the consumer's issue grouping and incident detection. The docs pointed at ClickHouse replay as the recovery story, but consumers couldn't retry safely and nothing implemented replay.

Change

  • SinkForwarder (packages/otlp-ingester/src/sink.ts): batches queue in memory and retry with exponential backoff + full jitter (1 s → 60 s, SINK_MAX_ATTEMPTS = 12 ≈ 8 min — rides out a routine consumer deploy). Network errors, timeouts, 408/429 and 5xx retry; other 4xx are consumer rejections and drop immediately. Concurrency drops to 1 while failing so recovery doesn't stampede the consumer.
  • Bounded buffer: SINK_MAX_BUFFERED_BATCHES = 1000 / SINK_MAX_BUFFERED_MB = 64, oldest-first eviction. Every dropped batch is logged with its signal time range for targeted ClickHouse replay.
  • Additive wire change: each payload now carries a unique batchId so consumers can dedupe re-deliveries (a batch can arrive twice when only the 2xx response was lost).
  • Observability: /healthz exposes delivery counters (sink.queued / delivered / retried / droppedOverflow / droppedPermanent / lastFailureAt …) for missed-batch monitoring; shutdown logs any undelivered batches.
  • Docs: new "Delivery semantics" section in ARCHITECTURE.md, compatibility table in PLAN.md, env table in the ingester README.

The durability boundary is intentional: the retry buffer is memory-only, but nothing is enqueued that wasn't already durably written to ClickHouse (ingest 503s otherwise) — ClickHouse stays the recovery source for anything the buffer cannot save.

Deploy order

Consumers whose sink writes are additive must ship their dedupe before this deploys, since retries can now legitimately deliver the same batch twice. For Autter cloud that's the monorepo's dedupe-ledger + reconciler PR (raised alongside this one).


Created with PostHog Desktop


View code changes stack in Autter

Summary

Summary generated by Autter.
Adds at-least-once delivery for optional sink webhooks in the OTLP ingester. Fingerprinted occurrences, metric rollups, and LLM call batches are forwarded through a bounded in-memory retry buffer with exponential backoff, while ClickHouse remains the durable source for replay after buffer exhaustion or shutdown.

Changes

  • Added SinkForwarder in packages/otlp-ingester/src/sink.ts to:
    • Deliver sink batches with a stable batchId for consumer-side deduplication.
    • Retry failed deliveries with capped exponential backoff.
    • Bound buffered batches and memory usage, dropping the oldest batches first when limits are exceeded.
    • Expose delivery statistics for health checks and operational visibility.
  • Updated createIngesterApp to enqueue sink payloads after successful ClickHouse persistence and expose the forwarder on IngesterApp.
  • Added sink lifecycle handling to the ingester entry point so retries stop cleanly during SIGTERM/SIGINT; undelivered batches are logged as recoverable through ClickHouse replay.
  • Added SINK_MAX_ATTEMPTS, SINK_MAX_BUFFERED_BATCHES, and SINK_MAX_BUFFERED_MB configuration with defaults.
  • Updated the architecture, deployment plan, and ingester README to document at-least-once semantics, retry behavior, buffer bounds, and the new configuration.

Acceptance Criteria

  • When AUTTER_SINK_URL is configured, successfully persisted ingest batches are forwarded with a stable batchId.
  • Transient sink failures retry with capped backoff up to SINK_MAX_ATTEMPTS, without creating an unbounded in-memory queue.
  • Buffer limits are enforced independently by batch count and approximate memory usage, with oldest batches dropped and logged when limits are exceeded.
  • /healthz includes sink delivery statistics when a sink is configured and continues to report ClickHouse health correctly.
  • Shutdown stops new sink retries, closes the HTTP server and ClickHouse store, and logs any undelivered batches as recoverable from ClickHouse.
  • Ingest behavior remains unchanged when AUTTER_SINK_URL is unset.

Test Plan

  • Start the ingester with ClickHouse and a test sink, ingest traces, metrics, and browser data, and verify each persisted batch reaches the sink with a batchId.
  • Make the sink return 5xx responses, then restore it and verify retries occur with increasing delays and eventually deliver the queued batch.
  • Configure small SINK_MAX_ATTEMPTS, SINK_MAX_BUFFERED_BATCHES, and SINK_MAX_BUFFERED_MB values, generate failures, and verify the oldest buffered batches are dropped and logged at the configured bounds.
  • Query /healthz while sink deliveries are succeeding and failing, and verify the reported sink statistics reflect the pending and failed batches.
  • Send SIGTERM or SIGINT with pending sink work and verify shutdown logs the undelivered count and exits without preventing ClickHouse closure.
  • Run the existing CI build and integration workflows, including regression coverage for normalized occurrence payloads and shared RuntimeOccurrence/sink batch types.

Rollback Plan

  • Revert this change and redeploy the ingester image to restore direct sink forwarding behavior.
  • If sink delivery is unhealthy before a redeploy, unset AUTTER_SINK_URL or point it to a disabled sink endpoint; ClickHouse ingestion remains available and can serve as the replay source.
  • After rollback, replay any batches recorded in ClickHouse that were not delivered successfully, using the sink consumer's batchId deduplication.

Related Issues

No linked issue was identified.

Written for commit 26cc0c2. Summary will update on new commits.

The sink webhook was fire-and-forget: one POST with a 10s timeout and a
console.warn on failure. Any consumer deploy, network blip, or slow org-DB
round trip silently lost the batch — and with it the error occurrences
that feed issue grouping and incident detection downstream.

Batches now queue in memory and retry with exponential backoff (1s → 60s,
SINK_MAX_ATTEMPTS tries, ~8 min by default) on network errors, timeouts,
408/429 and 5xx; other 4xx drop immediately. The buffer is bounded
(SINK_MAX_BUFFERED_BATCHES / SINK_MAX_BUFFERED_MB) with oldest-first
eviction, and every drop is logged with its signal time range so the
consumer can replay it from ClickHouse — which stays the recovery source:
nothing is enqueued that wasn't already durably written there.

Each payload additively gains a unique batchId so consumers can dedupe
re-deliveries (a batch can arrive twice when only the 2xx was lost).
/healthz now exposes sink delivery counters for missed-batch monitoring,
and shutdown logs any undelivered batches.

Generated-By: PostHog Desktop
Task-Id: 97b59a29-5efe-4bce-ab15-fec99be87fe5

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Autter review in progress — running security, correctness & dependency checks on this PR. Follow live step-by-step progress on the autter/review-gate check in the merge box. Merge is blocked until the gate completes; Autter approves automatically when the review comes back clean, and releases this hold with a neutral review when it finds non-blocking issues.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter posted 1 finding(s) as review threads below (🟠 1). Each carries a copy-paste AI fix prompt.

Comment thread packages/otlp-ingester/src/sink.ts Outdated
// batch — retrying the same body cannot succeed.
permanent = res.status < 500 && res.status !== 408 && res.status !== 429;
} catch (err) {
failure = err instanceof Error ? err.message : String(err);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 Raw sink exception messages exposed through unauthenticated health checks — Risk: 62/100

When fetchImpl rejects, SinkForwarder.send() stores the raw exception message in lastFailureMessage. GET /healthz returns sink.stats(), exposing those messages to unauthenticated health clients. Store a sanitized failure category/message instead of the raw exception text.

🛠 AI fix prompt (copy & paste into your coding agent)
Do not expose the raw error message in SinkStats or GET /healthz. Store it only in server-side logs, or map failures to a fixed safe category such as "timeout", "connection_error", or "http_error" before returning the health response.

Flagged by Autter security & observability checks.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter posted 1 finding(s) as review threads below (🟡 1). Each carries a copy-paste AI fix prompt.

) {}

/** Queue a batch for delivery. No-op when there is nothing to send. */
enqueue(

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] Split batch shaping from queue mutation — Risk: 35/100

enqueue combines eligibility checks, payload shaping and serialization, signal-range calculation, queue mutation, buffer-bound enforcement, and delivery scheduling. Extracting batch construction from queue management would allow retry-buffer behavior and payload shaping to be tested independently; the current code has no functional validation defect.

🛠 AI fix prompt (copy & paste into your coding agent)
Extract pure helpers for building the serialized batch and calculating its signal range, then keep enqueue focused on queue mutation, bounds enforcement, and pump scheduling.

Flagged by Autter security & observability checks.

@autter-dev

autter-dev Bot commented Aug 17, 2026

Copy link
Copy Markdown

🚦 Pre-merge checks · ⚠️ 15 warning, ✅ 154 passed

Needs attention

Check Status Explanation
Removed observability ⚠️ Warning 1 potential issue(s) detected (max risk 65/100): packages/otlp-ingester/src/server.ts:208.
Silent exception swallowing ⚠️ Warning 1 potential issue(s) detected (max risk 70/100): packages/otlp-ingester/src/sink.ts:247.
Stack trace leakage ⚠️ Warning 1 potential issue(s) detected (max risk 62/100): packages/otlp-ingester/src/sink.ts:258.
Idempotency key not detected ⚠️ Warning 1 potential issue(s) detected (max risk 48/100): packages/otlp-ingester/src/sink.ts:5.
Possible non-atomic read-modify-write ⚠️ Warning 1 potential issue(s) detected (max risk 45/100): packages/otlp-ingester/src/sink.ts:247.
Batch size limit not detected ⚠️ Warning 1 potential issue(s) detected (max risk 48/100): packages/otlp-ingester/src/sink.ts:57.
Missing linked tracker issue ⚠️ Warning 2 potential issue(s) detected (max risk 50/100): packages/otlp-ingester/src/sink.ts:1, packages/otlp-ingester/src/server.ts:41.
Missing CODEOWNERS reviewer approval ⚠️ Warning 1 potential issue(s) detected (max risk 65/100): packages/otlp-ingester/src/config.ts:26.
Missing security-team review on sensitive path ⚠️ Warning 1 potential issue(s) detected (max risk 60/100): packages/otlp-ingester/src/config.ts:82.
Source changes without matching tests ⚠️ Warning 4 potential issue(s) detected (max risk 85/100): packages/otlp-ingester/src/index.ts:5, packages/otlp-ingester/src/server.ts:22, packages/otlp-ingester/src/sink.ts:1, packages/otlp-ingester/src/server.ts:41.
Hallucinated import (package not installed) ⚠️ Warning 1 potential issue(s) detected (max risk 80/100): packages/otlp-ingester/src/sink.ts:3.
Runtime error risk ⚠️ Warning 1 finding(s) on changed lines.
Excessive complexity ⚠️ Warning 2 finding(s) on changed lines.
Dead export (no callers) ⚠️ Warning 1 finding(s) on changed lines.
Complexity Guard ⚠️ Warning 1 finding(s) on changed lines.
✅ Passed checks (154)
Check Status Explanation
Too many files changed ✅ Passed Changed 7 file(s), within the limit of 50.
Too many lines changed ✅ Passed Changed 435 line(s), within the limit of 1000.
Too many unrelated chapters ✅ Passed 4 chapter(s) detected, within the limit of 6.
Generated files hiding real changes ✅ Passed Generated-file volume (0 lines) does not obscure the 435 hand-written line(s).
Missing PR context ✅ Passed PR context looks sufficient.
Mixed concerns (refactor + behavior change) ✅ Passed The PR is a focused behavioral reliability change introducing at-least-once sink delivery, retries, buffering, and lifecycle handling; it does not combine a separate no-behavior refactor with that change.
Migration + app logic + UI combined in one PR ✅ Passed The PR contains no database migration files and no user-interface changes; it only changes ingester behavior, configuration, and documentation.
Sensitive data in logs ✅ Passed No sensitive data in logs issues detected.
Log injection ✅ Passed No log injection issues detected.
Missing audit logging ✅ Passed No missing audit logging issues detected.
Unhandled promise rejection ✅ Passed No unhandled promise rejection issues detected.
Circuit breaker not detected ✅ Passed No circuit breaker not detected issues detected.
Multi-write without detected transaction ✅ Passed No multi-write without detected transaction issues detected.
Possible TOCTOU in critical path ✅ Passed No possible toctou in critical path issues detected.
Optimistic locking not detected ✅ Passed No optimistic locking not detected issues detected.
Rate limiting not detected ✅ Passed No rate limiting not detected issues detected.
Rate limiting removed ✅ Passed No rate limiting removed issues detected.
Pagination not detected ✅ Passed No pagination not detected issues detected.
Publicly exposed storage ✅ Passed No publicly exposed storage issues detected.
Over-permissive IAM policy ✅ Passed No over-permissive iam policy issues detected.
Security group open to the internet ✅ Passed No security group open to the internet issues detected.
Unencrypted storage at rest ✅ Passed No unencrypted storage at rest issues detected.
Infrastructure missing access logging ✅ Passed No infrastructure missing access logging issues detected.
Hardcoded secret in IaC ✅ Passed No hardcoded secret in iac issues detected.
Infrastructure misconfiguration ✅ Passed No infrastructure misconfiguration issues detected.
Deprecated Kubernetes API version ✅ Passed No deprecated kubernetes api version issues detected.
Compound IaC attack chain ✅ Passed No compound iac attack chain issues detected.
Prompt injection risk ✅ Passed No LLM/AI-integration code touched by this diff.
LLM output used in a dangerous sink ✅ Passed No LLM/AI-integration code touched by this diff.
Sensitive data in prompt or system-prompt leakage ✅ Passed No LLM/AI-integration code touched by this diff.
Over-privileged LLM tool / excessive agency ✅ Passed No LLM/AI-integration code touched by this diff.
Missing validation on an LLM-driven decision ✅ Passed No LLM/AI-integration code touched by this diff.
Unbounded LLM usage (denial-of-wallet) ✅ Passed No LLM/AI-integration code touched by this diff.
Table exposed without row-level security ✅ Passed No row-level-security-related code touched by this diff.
Over-broad row-level security policy ✅ Passed No row-level-security-related code touched by this diff.
Code path that bypasses row-level security ✅ Passed No row-level-security-related code touched by this diff.
Privileged database credential reachable from the client ✅ Passed No row-level-security-related code touched by this diff.
Privileged query without row-level scoping ✅ Passed No row-level-security-related code touched by this diff.
Template-default gradient styling ✅ Passed No added frontend pages or design-slop markers in this diff.
Interchangeable AI marketing copy ✅ Passed No added frontend pages or design-slop markers in this diff.
Placeholder content shipped to users ✅ Passed No added frontend pages or design-slop markers in this diff.
Emoji standing in for an icon system ✅ Passed No added frontend pages or design-slop markers in this diff.
Call-to-action that goes nowhere ✅ Passed No added frontend pages or design-slop markers in this diff.
Templated page composition ✅ Passed No added frontend pages or design-slop markers in this diff.
Merge-blocking marker left in the change ✅ Passed No pending-work markers added by this diff.
Known-defect marker shipped in code ✅ Passed No pending-work markers added by this diff.
Untracked TODO without an issue reference ✅ Passed No pending-work markers added by this diff.
Test disabled or left pending ✅ Passed No pending-work markers added by this diff.
PII in logs ✅ Passed No pii in logs issues detected.
PII or internals leaked in error response ✅ Passed No pii or internals leaked in error response issues detected.
PII stored without application-level encryption ✅ Passed No pii stored without application-level encryption issues detected.
User data stored without retention controls ✅ Passed No user data stored without retention controls issues detected.
PII sent to external / cross-border destination ✅ Passed No pii sent to external / cross-border destination issues detected.
Lockfile resolution / integrity tampered ✅ Passed No lockfile resolution / integrity tampered issues detected.
Dependency runs install-time lifecycle script ✅ Passed No dependency runs install-time lifecycle script issues detected.
Possible dependency-confusion attack ✅ Passed No possible dependency-confusion attack issues detected.
Lockfile resolves a dependency the manifest does not declare ✅ Passed No lockfile resolves a dependency the manifest does not declare issues detected.
Checked-in build artefact modified without source change ✅ Passed No checked-in build artefact modified without source change issues detected.
Dockerfile build-step is insecure ✅ Passed No dockerfile build-step is insecure issues detected.
External artefact pulled in without integrity pinning ✅ Passed No external artefact pulled in without integrity pinning issues detected.
Changed export, importer not updated ✅ Passed No changed export with an un-updated importer detected.
Migration missing rollback / down step ✅ Passed No migration missing rollback / down step issues detected.
Frontend importing database client directly ✅ Passed No frontend importing database client directly issues detected.
Route handler bypassing service layer ✅ Passed No route handler bypassing service layer issues detected.
Backend service importing UI module ✅ Passed No backend service importing ui module issues detected.
Cross-context internals import ✅ Passed No cross-context internals import issues detected.
Workspace package rule violation ✅ Passed No workspace package rule violation issues detected.
Inconsistent logging pattern ✅ Passed No inconsistent logging pattern issues detected.
Inconsistent error handling ✅ Passed No inconsistent error handling issues detected.
Endpoint missing input validation ✅ Passed No endpoint missing input validation issues detected.
Multi-write without transaction wrapper ✅ Passed No multi-write without transaction wrapper issues detected.
New feature shipped without feature flag ✅ Passed No new feature shipped without feature flag issues detected.
Module placed in the wrong workspace package ✅ Passed No module placed in the wrong workspace package issues detected.
Direct env-var access bypasses config module ✅ Passed No direct env-var access bypasses config module issues detected.
Nonexistent package (not found in registry) ✅ Passed No nonexistent package (not found in registry) issues detected.
Call to function that does not exist ✅ Passed No call to function that does not exist issues detected.
Generic placeholder identifier in production logic ✅ Passed No generic placeholder identifier in production logic issues detected.
Repetitive boilerplate (duplicated block) ✅ Passed No repetitive boilerplate (duplicated block) issues detected.
Overbroad try/catch swallowing all exceptions ✅ Passed No overbroad try/catch swallowing all exceptions issues detected.
TODO / FIXME on critical path ✅ Passed No todo / fixme on critical path issues detected.
Comment contradicts or fabricates code behaviour ✅ Passed No comment contradicts or fabricates code behaviour issues detected.
Abstraction defined but never used ✅ Passed No abstraction defined but never used issues detected.
Code style differs from rest of codebase ✅ Passed No code style differs from rest of codebase issues detected.
Established pattern ignored ✅ Passed No established pattern ignored issues detected.
Unhandled edge case (null / empty / zero / boundary) ✅ Passed No unhandled edge case (null / empty / zero / boundary) issues detected.
Doc-copy code with insecure defaults ✅ Passed No doc-copy code with insecure defaults issues detected.
Dead code (defined but never referenced) ✅ Passed No dead code (defined but never referenced) issues detected.
Deprecated API call ✅ Passed No deprecated api call issues detected.
API pattern from wrong library version ✅ Passed No api pattern from wrong library version issues detected.
API endpoint removed ✅ Passed No api endpoint removed issues detected.
HTTP method changed (GET ↔ POST etc.) ✅ Passed No http method changed (get ↔ post etc.) issues detected.
New required field added to request ✅ Passed No new required field added to request issues detected.
Field removed from response schema ✅ Passed No field removed from response schema issues detected.
Response field type changed ✅ Passed No response field type changed issues detected.
HTTP status code changed ✅ Passed No http status code changed issues detected.
Auth requirement added / removed / changed ✅ Passed No auth requirement added / removed / changed issues detected.
Error response shape changed ✅ Passed No error response shape changed issues detected.
Pagination behaviour changed ✅ Passed No pagination behaviour changed issues detected.
Outbound webhook payload schema changed ✅ Passed No outbound webhook payload schema changed issues detected.
GraphQL field removed without deprecation ✅ Passed No graphql field removed without deprecation issues detected.
GraphQL enum value removed ✅ Passed No graphql enum value removed issues detected.
SQL injection ✅ Passed No sql injection issues detected.
Cross-site scripting (XSS) ✅ Passed No cross-site scripting (xss) issues detected.
Path traversal ✅ Passed No path traversal issues detected.
Command injection ✅ Passed No command injection issues detected.
Insecure deserialization ✅ Passed No insecure deserialization issues detected.
Weak cryptography ✅ Passed No weak cryptography issues detected.
Hardcoded secret ✅ Passed No hardcoded secret issues detected.
Insecure randomness for security material ✅ Passed No insecure randomness for security material issues detected.
Unsafe file upload ✅ Passed No unsafe file upload issues detected.
Missing input validation ✅ Passed No missing input validation issues detected.
Unsafe CORS configuration ✅ Passed No unsafe cors configuration issues detected.
Unsafe / open redirect ✅ Passed No unsafe / open redirect issues detected.
Missing CSRF protection ✅ Passed No missing csrf protection issues detected.
Unsafe cookie / session settings ✅ Passed No unsafe cookie / session settings issues detected.
Sensitive data exposure ✅ Passed No sensitive data exposure issues detected.
API key in source ✅ Passed No api key in source detected.
Access token in source ✅ Passed No access token in source detected.
Private key in source ✅ Passed No private key in source detected.
Database connection URL with embedded credentials ✅ Passed No database connection url with embedded credentials detected.
Cloud credential in source ✅ Passed No cloud credential in source detected.
Webhook signing secret in source ✅ Passed No webhook signing secret in source detected.
OAuth client secret in source ✅ Passed No oauth client secret in source detected.
JWT signing secret in source ✅ Passed No jwt signing secret in source detected.
Hardcoded password ✅ Passed No hardcoded password detected.
Auth middleware removed from route ✅ Passed No auth middleware removed from route issues detected.
Route protection changed (protected → public) ✅ Passed No route protection changed (protected → public) issues detected.
Permission / RBAC check removed ✅ Passed No permission / rbac check removed issues detected.
Required role weakened ✅ Passed No required role weakened issues detected.
Admin-only route exposed to lower privilege ✅ Passed No admin-only route exposed to lower privilege issues detected.
Token validation skipped in middleware chain ✅ Passed No token validation skipped in middleware chain issues detected.
JWT verification weakened or changed ✅ Passed No jwt verification weakened or changed issues detected.
Session expiration / TTL changed ✅ Passed No session expiration / ttl changed issues detected.
Password reset flow changed ✅ Passed No password reset flow changed issues detected.
OAuth callback / redirect handling changed ✅ Passed No oauth callback / redirect handling changed issues detected.
Webhook endpoint missing signature verification ✅ Passed No webhook endpoint missing signature verification issues detected.
Public route touches private/PII data ✅ Passed No public route touches private/pii data issues detected.
Frontend performance issue ✅ Passed No additional explanation was reported.
Frontend security issue ✅ Passed No additional explanation was reported.
Frontend correctness issue ✅ Passed No additional explanation was reported.
Accessibility issue ✅ Passed No additional explanation was reported.
Frontend maintainability issue ✅ Passed No additional explanation was reported.
Code correctness issue ✅ Passed No additional explanation was reported.
Resource leak risk ✅ Passed No additional explanation was reported.
Data integrity risk ✅ Passed No additional explanation was reported.
Maintainability issue ✅ Passed No additional explanation was reported.
Co-change coupling ✅ Passed No additional explanation was reported.
Redundant alias / duplicate import ✅ Passed No additional explanation was reported.
Redundant type construct ✅ Passed No additional explanation was reported.
Simplifiable code ✅ Passed No additional explanation was reported.
Unnecessary type assertion ✅ Passed No additional explanation was reported.
Module smell ✅ Passed No additional explanation was reported.
Code duplication / DRY violation ✅ Passed No additional explanation was reported.
Bundle Size Monitor ✅ Passed No additional explanation was reported.

This comment is updated automatically whenever Autter reviews a new PR revision.

@autter-dev

autter-dev Bot commented Aug 17, 2026

Copy link
Copy Markdown

🧭 PR hygiene & process suggestions

Autter has 8 suggestion(s) about the shape of this PR (size, scope, reviewability). These are process guidance — not code defects — so they are consolidated here instead of posted as inline comments on individual files.

🟠 Source changes without matching tests — Risk: 75/100

This source file was changed but no sibling test file is added or modified anywhere in the PR. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions shutdown, IngesterConfig, IngesterApp, createIngesterApp, loadConfig; scopes @autter/otlp-ingester; dependent files packages/otlp-ingester/src/config.ts, packages/otlp-ingester/src/server.ts, ./server.js, ./config.js, ./types.js.

🛠 AI fix prompt (copy & paste into your coding agent)
Add or update a sibling test (`*.test.*`, `*_test.*`, or `__tests__/`) that exercises the new behavior in `packages/otlp-ingester/src/index.ts` around line 5. Cover the happy path AND at least one failure case; without a test, a regression here will only be caught in production. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `shutdown`, `IngesterConfig`, `IngesterApp`, `createIngesterApp`, `loadConfig`; scopes `@autter/otlp-ingester`; dependent files `packages/otlp-ingester/src/config.ts`, `packages/otlp-ingester/src/server.ts`, `./server.js`, `./config.js`, `./types.js`.

🟠 Source changes without matching tests — Risk: 65/100

This source file was changed but no sibling test file is added or modified anywhere in the PR.

🛠 AI fix prompt (copy & paste into your coding agent)
Add or update a sibling test (`*.test.*`, `*_test.*`, or `__tests__/`) that exercises the new behavior in `packages/otlp-ingester/src/sink.ts` around line 1. Cover the happy path AND at least one failure case; without a test, a regression here will only be caught in production.

🟠 Missing CODEOWNERS approval — Risk: 65/100

No approving CODEOWNERS review is present for this change; the only listed review requests changes and codeownersText is empty.

🛠 AI fix prompt (copy & paste into your coding agent)
Configure or restore CODEOWNERS entries for the changed scopes and obtain an approving review from at least one matching owner for every changed path, including packages/otlp-ingester/src/config.ts, server.ts, index.ts, sink.ts, and the documentation files. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `IngesterConfig`, `loadConfig`, `parseIngestKeys`, `intEnv`, `IngesterApp`, `createIngesterApp`; scopes `@autter/otlp-ingester`; dependent files `packages/otlp-ingester/src/index.ts`.

🟠 Source changes without matching tests — Risk: 60/100

This source file was changed but no sibling test file is added or modified anywhere in the PR. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions IngesterApp, createIngesterApp, authenticate, forwardToSink, storageError, fingerprintAll, IngesterConfig, loadConfig; scopes @autter/otlp-ingester; dependent files packages/otlp-ingester/src/index.ts, ./auth.js, ./clickhouse.js, ./config.js, ./fingerprint.js, ./normalize-browser.js, ./normalize-otlp.js, ./otlp-proto.js.

🛠 AI fix prompt (copy & paste into your coding agent)
Add or update a sibling test (`*.test.*`, `*_test.*`, or `__tests__/`) that exercises the new behavior in `packages/otlp-ingester/src/server.ts` around line 22. Cover the happy path AND at least one failure case; without a test, a regression here will only be caught in production. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `IngesterApp`, `createIngesterApp`, `authenticate`, `forwardToSink`, `storageError`, `fingerprintAll`, `IngesterConfig`, `loadConfig`; scopes `@autter/otlp-ingester`; dependent files `packages/otlp-ingester/src/index.ts`, `./auth.js`, `./clickhouse.js`, `./config.js`, `./fingerprint.js`, `./normalize-browser.js`, `./normalize-otlp.js`, `./otlp-proto.js`.

🟠 Missing security-team review for sink delivery — Risk: 60/100

No security-team approval is present for the new sink delivery path, which handles AUTTER_SINK_TOKEN and buffered forwarding of data from ingest endpoints.

🛠 AI fix prompt (copy & paste into your coding agent)
Request and obtain approval from the configured security team for the sink webhook and credential-handling changes, covering loadConfig, SinkForwarder, createIngesterApp, and the /v1/traces, /v1/metrics, and /v1/browser enqueue paths. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `IngesterConfig`, `loadConfig`, `parseIngestKeys`, `intEnv`, `IngesterApp`, `createIngesterApp`; scopes `@autter/otlp-ingester`; dependent files `packages/otlp-ingester/src/index.ts`.

🟠 Missing linked tracker issue — Risk: 50/100

This PR's title and body do not reference any tracker issue (GitHub #123, Jira/Linear KEY-123, or Fixes/Closes/Resolves).

🛠 AI fix prompt (copy & paste into your coding agent)
In the PR description, add a reference to the tracker issue this change implements (GitHub `#123`, Jira/Linear `PROJ-456`, or a `Fixes/Closes/Resolves` marker). Reviewers anchor on `packages/otlp-ingester/src/sink.ts` around line 1 need that context to understand why this change exists and what success looks like. Why it matters: reviewers and on-call engineers need the linked issue to understand the why behind a change months from now.

🟠 Source changes without matching tests — Risk: 50/100

Application source changes add SinkForwarder construction in createIngesterApp, alter enqueue behavior for the OTLP ingest handlers, and extend /healthz, but no sibling test file was added or modified. Failures could cause persisted batches to be omitted, duplicated, or misreported to downstream sink consumers. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions IngesterApp, createIngesterApp, authenticate, forwardToSink, storageError, fingerprintAll, IngesterConfig, loadConfig; scopes @autter/otlp-ingester; dependent files packages/otlp-ingester/src/index.ts, ./auth.js, ./clickhouse.js, ./config.js, ./fingerprint.js, ./normalize-browser.js, ./normalize-otlp.js, ./otlp-proto.js.

🛠 AI fix prompt (copy & paste into your coding agent)
Add or modify a sibling test file for the otlp-ingester source, covering createIngesterApp, the /healthz response, successful ClickHouse persistence followed by sink enqueue, and the /v1/traces, /v1/metrics, and /v1/browser delivery paths. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `IngesterApp`, `createIngesterApp`, `authenticate`, `forwardToSink`, `storageError`, `fingerprintAll`, `IngesterConfig`, `loadConfig`; scopes `@autter/otlp-ingester`; dependent files `packages/otlp-ingester/src/index.ts`, `./auth.js`, `./clickhouse.js`, `./config.js`, `./fingerprint.js`, `./normalize-browser.js`, `./normalize-otlp.js`, `./otlp-proto.js`.

🟡 Missing linked tracker issue — Risk: 25/100

The PR description does not reference a tracker issue. The change affects createIngesterApp, SinkForwarder delivery, and the /healthz, /v1/traces, /v1/metrics, and /v1/browser ingest flows, so it needs traceability for downstream sink consumers and ClickHouse replay operations. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions IngesterApp, createIngesterApp, authenticate, forwardToSink, storageError, fingerprintAll, IngesterConfig, loadConfig; scopes @autter/otlp-ingester; dependent files packages/otlp-ingester/src/index.ts, ./auth.js, ./clickhouse.js, ./config.js, ./fingerprint.js, ./normalize-browser.js, ./normalize-otlp.js, ./otlp-proto.js.

🛠 AI fix prompt (copy & paste into your coding agent)
Add a GitHub, Jira, or Linear issue reference to the PR description, using a supported format such as #123, KEY-123, or Fixes #123. Blast radius — skipping this guardrail cascades to the downstream usage that depends on this file: functions `IngesterApp`, `createIngesterApp`, `authenticate`, `forwardToSink`, `storageError`, `fingerprintAll`, `IngesterConfig`, `loadConfig`; scopes `@autter/otlp-ingester`; dependent files `packages/otlp-ingester/src/index.ts`, `./auth.js`, `./clickhouse.js`, `./config.js`, `./fingerprint.js`, `./normalize-browser.js`, `./normalize-otlp.js`, `./otlp-proto.js`.

Flagged by Autter PR-hygiene checks.


⚠️ 2 unconfirmed finding(s) — flagged by a detector but not proven by Autter's verification pass

Inline comments are reserved for findings that survived verification. These are plausible but could not be confirmed from the available context, so they are listed here as FYIs instead — review the ones that look real to you.

  • 🟡 Removed observability (risk 49/100) — packages/otlp-ingester/src/server.ts:208 — The server removes its direct console.warn on sink failures, but replaces the fire-and-forget call with SinkForwarder.enqueue() and exposes sink.stats() through health checks. The imported sink.ts implementation is not provided, so it is not possible to establish whether failure logging or equivalent observability is preserved there.
  • 🟡 Dead export (no callers) (risk 20/100) — packages/otlp-ingester/src/index.ts:49 — The repository graph shows no in-repository consumer, but that does not establish that the export is unnecessary: SinkForwarder and SinkStats may intentionally form the package's public sink contract for external consumers. The provided files contain no documentation or API policy proving this export is dead.
🔇 8 finding(s) suppressed as likely false positives by Autter's verification pass

These were flagged by a detector but a second, full-file verification judged them not to be real issues. Listed here for transparency — review if you disagree.

  • 🟡 Idempotency key not detected (risk 1/100) — packages/otlp-ingester/src/sink.ts:5 — The code handles telemetry batches and forwards occurrences, metrics, and LLM calls to a sink webhook; it is not a payment or order mutation operation. Each retried batch already carries a unique batchId for consumer deduplication, so the finding's financial-state/idempotency claim does not apply.
  • 🟡 Possible non-atomic read-modify-write (risk 1/100) — packages/otlp-ingester/src/sink.ts:247 — The flagged updates are in-memory queue bookkeeping and delivery statistics, not a persisted counter, balance, or quota. JavaScript execution is single-threaded, and the queue mutations are performed synchronously; there is no database read-modify-write requiring a row lock or atomic increment.
  • 🟡 Batch size limit not detected (risk 48/100) — packages/otlp-ingester/src/sink.ts:57 — The reachable HTTP handlers apply Express body limits via config.maxBodyBytes (default 1 MiB), so request arrays cannot be unbounded in size. Trace normalization also explicitly caps processed spans at MAX_SPANS_PER_REQUEST = 5000, and the sink's queued batch count and total bytes are bounded by sinkMaxBufferedBatches and sinkMaxBufferedMb.
  • 🟠 Silent exception swallowing (risk 70/100) — packages/otlp-ingester/src/sink.ts:247 — The exception is not silently swallowed. send() catches fetch failures, records failure stats, increments the failure counter, and either retries the batch with bounded backoff or permanently drops it after the configured attempt limit while logging a replay range. The surrounding finally also decrements in-flight state and repumps the queue.
  • 🔴 Hallucinated import (package not installed) (risk 80/100) — packages/otlp-ingester/src/sink.ts:3 — type is the TypeScript type-only import modifier in import type { IngesterConfig } from "./config.js"; the actual target is the internal relative module ./config.js, not an undeclared npm package.
  • 🟠 Runtime error risk (risk 64/100) — packages/otlp-ingester/src/sink.ts:247 — _The catch in send() intentionally converts fetch/timeout exceptions into delivery failures: it records consecutiveFailures, lastFailureAt, and lastFailureMessage, retries within the configured bound, and logs when retrying or permanently dropping the batch. The fire-and-forget caller uses finally() to decrement inFlight and resume pumping, so this is an intentional background delivery _
  • 🟡 Excessive complexity (risk 40/100) — packages/otlp-ingester/src/sink.ts:188 — The referenced line is not a condition packing six branching operators; the nearby buffer-bound condition contains only || and &&, and the surrounding logic is already separated into enforceBounds() and pump().
  • 🟡 Excessive complexity (risk 40/100) — packages/otlp-ingester/src/sink.ts:270 — The delivery failure handling is split across status classification and the final retry/permanent-drop check; no single referenced line contains six branching or short-circuit operators. The relevant condition uses only ||.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter completed PR review for #13: 12 finding(s) remain below the merge-blocking bar, so this review stays neutral rather than approving. See the findings below; the task checklist follows as the review's final comment.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter posted 1 finding(s) as review threads below (🟠 1). Each carries a copy-paste AI fix prompt.

return;
}
forwardToSink(ctx, fingerprinted, metricPoints, llmCalls);
sink?.enqueue(ctx, fingerprinted, metricPoints, llmCalls);

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] Trace ingestion can amplify sink traffic without a per-tenant egress limit — Risk: 76/100

POST /v1/traces is authenticated and capped at the generic 300 requests/minute per-key limit, but this change makes every accepted trace batch enqueue work for asynchronous delivery. SinkForwarder retries failed deliveries up to 12 times and permits a 1000-batch/64 MB shared in-memory queue, while its four concurrent senders immediately POST to the configured sink. There is no per-key or per-tenant cap on queued bytes, retries, or sink requests, so a valid but abusive tenant can continuously submit maximum-size trace payloads, consume the shared retry buffer, evict other tenants' batches, and amplify traffic to an unavailable or rate-limiting sink. The existing request limiter bounds ingress requests only and does not bound this new expensive external work.

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

  • Dependent files: packages/otlp-ingester/src/server.ts, packages/otlp-ingester/src/sink.ts, packages/otlp-ingester/src/config.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Add a sink delivery budget keyed by authenticated orgId/repositoryId (and preferably a global circuit breaker), covering queued bytes/batches and retry/egress rate. Reject or shed new ingest work with an explicit 429/503 before ClickHouse acceptance when that tenant's sink budget is exhausted, and ensure one tenant cannot evict other tenants from the retry buffer.

Flagged by Autter security & observability checks.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter posted 1 finding(s) as review threads below (🟠 1). Each carries a copy-paste AI fix prompt.

Comment thread packages/otlp-ingester/src/sink.ts Outdated
await res.text().catch(() => {});
if (res.ok) {
this.delivered += 1;
this.consecutiveFailures = 0;

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] Successful concurrent requests incorrectly reopen full sink concurrency while failures remain pending — Risk: 64/100

The pump starts four requests while the circuit is healthy, but each successful request independently resets consecutiveFailures to zero. If one of those requests fails while another succeeds, the success can clear the failure state before the failed batch is retried; the completion callback then calls pump() and launches up to four more requests even though a failed batch is still queued. A transient sink outage can therefore create a retry stampede and continue parallel deliveries contrary to the stated serial-while-failing behavior, increasing overload and duplicate-delivery risk precisely during recovery.

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

  • Dependent files: packages/otlp-ingester/src/sink.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Track failure state for the active retry episode rather than resetting it from any concurrent success, or otherwise gate healthy concurrency until all requests from the failure episode have recovered.

Flagged by Autter security & observability checks.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter posted 2 finding(s) as review threads below (🟠 2). Each carries a copy-paste AI fix prompt.

Comment thread packages/otlp-ingester/src/sink.ts Outdated
RETRY_BASE_MS * 2 ** (batch.attempts - 1),
);
batch.nextAttemptAt = Date.now() + backoff / 2 + Math.random() * (backoff / 2);
this.queue.push(batch);

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] Retrying a batch moves it behind newer batches and defeats oldest-first overflow eviction — Risk: 58/100

enforceBounds() removes this.queue.shift() and is documented/configured as oldest-first eviction, but failed batches are appended with this.queue.push(batch) when requeued. A batch that has already waited and failed is thus placed after every newer batch; when the bounded buffer overflows, newer batches are shifted out first and the older failed batch is retained. Under sustained sink failure this reverses the intended drop policy and can retain stale telemetry while dropping fresher signals, so the documented bounded-buffer behavior is not implemented.

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

  • Dependent files: packages/otlp-ingester/src/sink.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Preserve queue ordering by re-inserting retries according to enqueue age (or use a separate priority/order structure) before applying oldest-first eviction.

Flagged by Autter security & observability checks.

});

app.get("/healthz", async (_req, res) => {
const sinkStats = sink ? { sink: sink.stats() } : {};

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] Unauthenticated health checks expose raw sink transport errors — Risk: 58/100

When the sink fetch rejects, SinkForwarder.send stores the exception message in lastFailureMessage; /healthz then spreads the complete sink stats object into its unauthenticated response. A routine DNS, TLS, proxy, or connection failure can therefore expose internal endpoint/transport details to any health-check caller, and the response persists until another failure updates it. This is not safe to ship as an externally reachable health endpoint because failure diagnostics are being leaked rather than kept in logs or reduced to an operationally safe code/status.

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

  • Dependent files: packages/otlp-ingester/src/sink.ts, packages/otlp-ingester/src/server.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Keep raw sink failure messages out of the public health response. Return only bounded operational fields such as counters and timestamps, or expose detailed diagnostics only behind authenticated/admin access; preserve the raw message in server logs/telemetry if needed.

Flagged by Autter security & observability checks.

@autter-dev

autter-dev Bot commented Aug 17, 2026

Copy link
Copy Markdown

Autter found 1 issue(s) it could not attach to the current diff (the anchor line is not part of a diff hunk, or the PR advanced during the review):

🟠 [ai] Concurrent ClickHouse writes are not atomic, so a failed ingest can be partially committed and never forwarded (risk 76/100)

packages/otlp-ingester/src/server.ts:203 · silent_exception_swallowing

The traces handler starts four independent ClickHouse inserts in Promise.all and returns 503 when any one rejects, but ClickHouse has no transaction or compensating cleanup across these tables. If, for example, occurrence and span inserts commit and the metric or LLM insert fails, the first request has already persisted a subset, yet it never reaches sink.enqueue at line 213, while the exporter retries the whole request: the retry can duplicate committed rows and forward a second set of newly fingerprinted occurrences. The browser path has the same two-write failure window. Thus a storage failure can produce half-applied telemetry plus duplicate/replayed signals, contrary to the handler's single success/failure contract.

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

  • Dependent files: packages/otlp-ingester/src/server.ts, packages/otlp-ingester/src/clickhouse.ts

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter posted 6 finding(s) as review threads below (🟠 6). Each carries a copy-paste AI fix prompt.

Comment thread packages/otlp-ingester/src/index.ts Outdated
// The retry buffer is memory-only; everything in it is already in
// ClickHouse, so the consumer's reconciliation replays it.
console.warn(
`${pending} sink batch(es) undelivered at shutdown — ` +

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] Biome: lint/style/useTemplate — Risk: 55/100

Template literals are preferred over string concatenation.

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the Biome `lint/style/useTemplate` issue at packages/otlp-ingester/src/index.ts:33: Template literals are preferred over string concatenation.

Flagged by Autter security & observability checks.

Comment thread packages/otlp-ingester/src/index.ts Outdated
// ClickHouse, so the consumer's reconciliation replays it.
console.warn(
`${pending} sink batch(es) undelivered at shutdown — ` +
`recoverable via ClickHouse replay`,

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] Biome: lint/style/noUnusedTemplateLiteral — Risk: 55/100

Do not use template literals if interpolation and special-character handling are not needed.

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the Biome `lint/style/noUnusedTemplateLiteral` issue at packages/otlp-ingester/src/index.ts:34: Do not use template literals if interpolation and special-character handling are not needed.

Flagged by Autter security & observability checks.

Comment thread packages/otlp-ingester/src/sink.ts Outdated
this.queuedBytes -= dropped.bytes;
this.droppedOverflow += 1;
console.warn(
`sink buffer overflow: dropped batch ${dropped.batchId} ` +

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] Biome: lint/style/useTemplate — Risk: 55/100

Template literals are preferred over string concatenation.

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the Biome `lint/style/useTemplate` issue at packages/otlp-ingester/src/sink.ts:187: Template literals are preferred over string concatenation.

Flagged by Autter security & observability checks.

Comment thread packages/otlp-ingester/src/sink.ts Outdated
console.warn(
`sink buffer overflow: dropped batch ${dropped.batchId} ` +
`(signals ${dropped.signalsFrom ?? "?"} .. ${dropped.signalsTo ?? "?"}) — ` +
`replay this range from ClickHouse`,

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] Biome: lint/style/noUnusedTemplateLiteral — Risk: 55/100

Do not use template literals if interpolation and special-character handling are not needed.

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the Biome `lint/style/noUnusedTemplateLiteral` issue at packages/otlp-ingester/src/sink.ts:189: Do not use template literals if interpolation and special-character handling are not needed.

Flagged by Autter security & observability checks.

Comment thread packages/otlp-ingester/src/sink.ts Outdated
if (permanent || batch.attempts >= this.config.sinkMaxAttempts) {
this.droppedPermanent += 1;
console.error(
`sink delivery gave up after ${batch.attempts} attempt(s) ` +

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] Biome: lint/style/useTemplate — Risk: 55/100

Template literals are preferred over string concatenation.

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the Biome `lint/style/useTemplate` issue at packages/otlp-ingester/src/sink.ts:268: Template literals are preferred over string concatenation.

Flagged by Autter security & observability checks.

Comment thread packages/otlp-ingester/src/sink.ts Outdated
`sink delivery gave up after ${batch.attempts} attempt(s) ` +
`(${failure}): batch ${batch.batchId} ` +
`(signals ${batch.signalsFrom ?? "?"} .. ${batch.signalsTo ?? "?"}) — ` +
`replay this range from ClickHouse`,

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] Biome: lint/style/noUnusedTemplateLiteral — Risk: 55/100

Do not use template literals if interpolation and special-character handling are not needed.

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the Biome `lint/style/noUnusedTemplateLiteral` issue at packages/otlp-ingester/src/sink.ts:271: Do not use template literals if interpolation and special-character handling are not needed.

Flagged by Autter security & observability checks.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter posted 5 finding(s) as review threads below (🟡 5). Each carries a copy-paste AI fix prompt.

Comment thread docs/PLAN.md Outdated
| `/v1/browser` payload (`version: 1`) | additive-only changes |
| ClickHouse table schemas | additive-only; TTLs configurable via env |
| Sink webhook payload (`version: 1`) | additive-only changes (`llmCalls` added additively) |
| Sink webhook payload (`version: 1`) | additive-only changes (`llmCalls`, then `batchId`, added additively) |

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: 110

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at docs/PLAN.md:133: Line length: Expected: 80; Actual: 110

Flagged by Autter security & observability checks.

Comment thread docs/PLAN.md Outdated
| ClickHouse table schemas | additive-only; TTLs configurable via env |
| Sink webhook payload (`version: 1`) | additive-only changes (`llmCalls` added additively) |
| Sink webhook payload (`version: 1`) | additive-only changes (`llmCalls`, then `batchId`, added additively) |
| Sink webhook delivery | at-least-once with bounded retries — consumers must dedupe on `batchId`/`occurrenceId` |

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: 114

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at docs/PLAN.md:134: Line length: Expected: 80; Actual: 114

Flagged by Autter security & observability checks.

Comment thread packages/otlp-ingester/README.md Outdated
| `AUTTER_KEY_VALIDATOR_URL` | — | Webhook: `POST {key}` → `{orgId, repositoryId}` (60 s cache) |
| `AUTTER_KEY_VALIDATOR_TOKEN` | — | Bearer token sent to the validator |
| `AUTTER_SINK_URL` | — | Webhook receiving fingerprinted occurrences for issue grouping |
| `AUTTER_SINK_URL` | — | Webhook receiving fingerprinted occurrences for issue grouping (at-least-once delivery — see `docs/ARCHITECTURE.md`) |

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: 144

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at packages/otlp-ingester/README.md:57: Line length: Expected: 80; Actual: 144

Flagged by Autter security & observability checks.

Comment thread packages/otlp-ingester/README.md Outdated
| `AUTTER_SINK_URL` | — | Webhook receiving fingerprinted occurrences for issue grouping |
| `AUTTER_SINK_URL` | — | Webhook receiving fingerprinted occurrences for issue grouping (at-least-once delivery — see `docs/ARCHITECTURE.md`) |
| `AUTTER_SINK_TOKEN` | — | Bearer token sent to the sink |
| `SINK_MAX_ATTEMPTS` | `12` | Delivery attempts per sink batch (1 s → 60 s backoff, ~8 min total) |

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: 100

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at packages/otlp-ingester/README.md:59: Line length: Expected: 80; Actual: 100

Flagged by Autter security & observability checks.

Comment thread packages/otlp-ingester/README.md Outdated
| `AUTTER_SINK_URL` | — | Webhook receiving fingerprinted occurrences for issue grouping (at-least-once delivery — see `docs/ARCHITECTURE.md`) |
| `AUTTER_SINK_TOKEN` | — | Bearer token sent to the sink |
| `SINK_MAX_ATTEMPTS` | `12` | Delivery attempts per sink batch (1 s → 60 s backoff, ~8 min total) |
| `SINK_MAX_BUFFERED_BATCHES` | `1000` | Sink retry buffer cap (batches); oldest drop first, logged with their time range |

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: 123

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at packages/otlp-ingester/README.md:60: Line length: Expected: 80; Actual: 123

Flagged by Autter security & observability checks.

@autter-dev

autter-dev Bot commented Aug 17, 2026

Copy link
Copy Markdown

Autter's deep review traced 1 finding(s) to file(s) this PR does not change — they can't be shown as inline comments, but the change still affects them:

🔴 [deterministic] Build failed: example-express-app (risk 80/100)

examples/express-app/package.json:1 · build_failure

npm run build failed in examples/express-app — this PR breaks the example-express-app build.

What happened: The example-express-app workspace does not define a build script in its package.json, so npm run build cannot run.

How to fix: Add a build script to examples/express-app/package.json if this example requires a build step, or update the CI command to use an existing script listed by npm run --workspace=example-express-app@0.0.0.

Build output
npm error Lifecycle script `build` failed with error:
npm error workspace example-express-app@0.0.0
npm error location /tmp/autter-agentic-Gjs6sr/examples/express-app
npm error Missing script: "build"
npm error
npm error To see a list of scripts, run:
npm error   npm run --workspace=example-express-app@0.0.0

@autter-dev

autter-dev Bot commented Aug 17, 2026

Copy link
Copy Markdown

🧪 Autter test run

Autter executed the automated checks against 26cc0c22.

No automated test suites were detected in this repository.

Change coverage (is each changed file's behavior verified by a test?)

Changed file Related test Result
packages/otlp-ingester/src/config.ts ✅ no declared test — temporary test passed
packages/otlp-ingester/src/index.ts ✅ no declared test — temporary test passed
packages/otlp-ingester/src/server.ts ✅ no declared test — temporary test passed
packages/otlp-ingester/src/sink.ts ✅ no declared test — temporary test passed
🤖 Coverage-check evidence

packages/otlp-ingester/src/config.ts
Ran: npx tsx .autter/scratch/config.test.ts

config scratch test passed

packages/otlp-ingester/src/index.ts
Ran: npx tsx .autter/scratch/index.test.ts

index scratch test passed; subprocess started on a random port and exited 0 after SIGTERM

packages/otlp-ingester/src/server.ts
Ran: npx tsx .autter/scratch/server.test.ts

server scratch test passed; /healthz returned 200 with clickhouse=unconfigured and zeroed sink stats

packages/otlp-ingester/src/sink.ts
Ran: npx tsx .autter/scratch/sink.test.ts

sink scratch test passed; one batch was POSTed, batchId was UUID-shaped, LLM timestamp was serialized, and delivered=1

Temporary tests are written under .autter/scratch/ for verification only — they are never committed to the repository.

Test plan (from the PR description)

  • ⬜ Start the ingester with ClickHouse and a test sink, ingest traces, metrics, and browser data, and verify each persisted batch reaches the sink with a batchId. — needs manual verification
  • ✅ Make the sink return 5xx responses, then restore it and verify retries occur with increasing delays and eventually deliver the queued batch. — verified by agent execution
  • ✅ Configure small SINK_MAX_ATTEMPTS, SINK_MAX_BUFFERED_BATCHES, and SINK_MAX_BUFFERED_MB values, generate failures, and verify the oldest buffered batches are dropped and logged at the configured bounds. — verified by agent execution
  • ✅ Query /healthz while sink deliveries are succeeding and failing, and verify the reported sink statistics reflect the pending and failed batches. — verified by agent execution
  • ⬜ Send SIGTERM or SIGINT with pending sink work and verify shutdown logs the undelivered count and exits without preventing ClickHouse closure. — needs manual verification
  • ⬜ Run the existing CI build and integration workflows, including regression coverage for normalized occurrence payloads and shared RuntimeOccurrence/sink batch types. — needs manual verification
🤖 Agent-executed checks

✅ Make the sink return 5xx responses, then restore it and verify retries occur with increasing delays and eventually deliver the queued batch.
Ran: timeout 20s npx tsx --tsconfig packages/otlp-ingester/tsconfig.json .autter/scratch/verify-sink.ts

Real localhost sink returned 503, then 204. Output: retry requests=4, intervals=[893,1463] ms, retried=2, delivered=1, droppedPermanent=0.

✅ Configure small SINK_MAX_ATTEMPTS, SINK_MAX_BUFFERED_BATCHES, and SINK_MAX_BUFFERED_MB values, generate failures, and verify the oldest buffered batches are dropped and logged at the configured bounds.
Ran: timeout 20s npx tsx --tsconfig packages/otlp-ingester/tsconfig.json .autter/scratch/verify-sink.ts

With sinkMaxBufferedBatches=2 and sinkMaxBufferedMb=1, output showed queued=2, droppedOverflow=4; stderr logged four ‘sink buffer overflow: dropped batch ...’ messages with replay ranges.

✅ Query /healthz while sink deliveries are succeeding and failing, and verify the reported sink statistics reflect the pending and failed batches.
Ran: timeout 20s npx tsx --tsconfig packages/otlp-ingester/tsconfig.json .autter/scratch/verify-sink.ts

Real Express /healthz returned 200 with healthy sink stats delivered=0, queued=0; failing sink query returned 200 and reported queued=1, retried=1, consecutiveFailures=1, lastFailureMessage="fetch failed".

⬜ items could not be verified automatically and still need a manual check.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter blocked this PR after its agentic checks (build/test/deep scans) completed: build_failure; 1 confirmed correctness/runtime finding(s). See the findings below and the full PR review for details.

@autter-dev

autter-dev Bot commented Aug 17, 2026

Copy link
Copy Markdown

Autter task list

  • @sagnik11 Restore the example Express app build (examples/express-app/package.json) - sagnik11: Add a valid build script to examples/express-app/package.json and run the workspace build to confirm the example remains CI-compatible.
  • @sagnik11 Enforce per-tenant sink egress limits (packages/otlp-ingester/src/server.ts, packages/otlp-ingester/src/config.ts, packages/otlp-ingester/src/sink.ts) - sagnik11: Add configurable per-tenant or per-key limits for sink-enqueued batches and retry traffic, enforce them alongside the existing request limiter, and test that one tenant cannot exhaust the shared retry buffer.
  • @sagnik11 Make persistence and forwarding recoverable after partial writes (packages/otlp-ingester/src/server.ts, packages/otlp-ingester/src/clickhouse.ts, packages/otlp-ingester/src/types.ts) - sagnik11: Redesign trace persistence so the four ClickHouse writes have an atomic or durable recovery boundary, and ensure any partially committed batch is discoverable for sink replay instead of returning 503 and losing forwarding eligibility.
  • @sagnik11 Correct retry ordering and concurrent circuit state (packages/otlp-ingester/src/sink.ts) - sagnik11: Update SinkForwarder so failed batches retain oldest-first queue order, concurrent successes cannot reset failure state for unrelated failures, and retry scheduling remains bounded and deterministic.
  • @sagnik11 Sanitize health failure details and restore delivery observability (packages/otlp-ingester/src/sink.ts, packages/otlp-ingester/src/server.ts, packages/otlp-ingester/src/index.ts) - sagnik11: Replace raw fetch exception text with safe failure categories in SinkStats, restore structured logging or metrics around persistence, enqueue, retry, drop, and shutdown paths, and verify /healthz exposes only operationally safe data.
  • @sagnik11 Add comprehensive ingester delivery and lifecycle tests (packages/otlp-ingester/src/sink.ts, packages/otlp-ingester/src/server.ts, packages/otlp-ingester/src/index.ts) - sagnik11: Add unit and integration coverage for SinkForwarder bounds, retry backoff, stable batch IDs, FIFO eviction, concurrent failures, sanitized stats, createIngesterApp enqueue behavior, all ingest routes, health checks, shutdown, and ClickHouse failure recovery.
  • @sagnik11 Complete review, lint, documentation, and API cleanup (packages/otlp-ingester/src/config.ts, packages/otlp-ingester/src/index.ts, packages/otlp-ingester/src/sink.ts) - sagnik11: Obtain CODEOWNERS and security-team approval, link a tracker issue, fix Biome and markdownlint violations, and remove the unused SinkStats public export or add an intentional consumer before rerunning CI.

Generated from PR diff, blast radius, and context.

Issues found

  1. Build failed: example-express-app · risk 80/100 · examples/express-app/package.json:1
  2. Trace ingestion can amplify sink traffic without a per-tenant egress limit · risk 76/100 · packages/otlp-ingester/src/server.ts:213
  3. Concurrent ClickHouse writes are not atomic, so a failed ingest can be partially committed and never forwarded · risk 76/100 · packages/otlp-ingester/src/server.ts:203
  4. Source changes without matching tests · risk 75/100 · packages/otlp-ingester/src/index.ts:5
  5. Source changes without matching tests · risk 65/100 · packages/otlp-ingester/src/sink.ts:1
  6. Missing CODEOWNERS approval · risk 65/100 · packages/otlp-ingester/src/config.ts:26
  7. Successful concurrent requests incorrectly reopen full sink concurrency while failures remain pending · risk 64/100 · packages/otlp-ingester/src/sink.ts:250
  8. Raw sink exception messages exposed through unauthenticated health checks · risk 62/100 · packages/otlp-ingester/src/sink.ts:258
  9. Source changes without matching tests · risk 60/100 · packages/otlp-ingester/src/server.ts:22
  10. Missing security-team review for sink delivery · risk 60/100 · packages/otlp-ingester/src/config.ts:82
  11. Retrying a batch moves it behind newer batches and defeats oldest-first overflow eviction · risk 58/100 · packages/otlp-ingester/src/sink.ts:283
  12. Unauthenticated health checks expose raw sink transport errors · risk 58/100 · packages/otlp-ingester/src/server.ts:92
  13. Biome: lint/style/useTemplate · risk 55/100 · packages/otlp-ingester/src/sink.ts:268
  14. Biome: lint/style/noUnusedTemplateLiteral · risk 55/100 · packages/otlp-ingester/src/sink.ts:271
  15. Biome: lint/style/useTemplate · risk 55/100 · packages/otlp-ingester/src/index.ts:33
  16. Biome: lint/style/noUnusedTemplateLiteral · risk 55/100 · packages/otlp-ingester/src/index.ts:34
  17. Biome: lint/style/useTemplate · risk 55/100 · packages/otlp-ingester/src/sink.ts:187
  18. Biome: lint/style/noUnusedTemplateLiteral · risk 55/100 · packages/otlp-ingester/src/sink.ts:189
  19. Missing linked tracker issue · risk 50/100 · packages/otlp-ingester/src/sink.ts:1
  20. Source changes without matching tests · risk 50/100 · packages/otlp-ingester/src/server.ts:41
  21. Removed observability · risk 49/100 · packages/otlp-ingester/src/server.ts:208
  22. Split batch shaping from queue mutation · risk 35/100 · packages/otlp-ingester/src/sink.ts:83
  23. markdownlint: MD013 · risk 30/100 · docs/PLAN.md:133
  24. markdownlint: MD013 · risk 30/100 · docs/PLAN.md:134
  25. markdownlint: MD013 · risk 30/100 · packages/otlp-ingester/README.md:57
  26. markdownlint: MD013 · risk 30/100 · packages/otlp-ingester/README.md:59
  27. markdownlint: MD013 · risk 30/100 · packages/otlp-ingester/README.md:60
  28. Missing linked tracker issue · risk 25/100 · packages/otlp-ingester/src/server.ts:41
  29. Dead export (no callers) · risk 20/100 · packages/otlp-ingester/src/index.ts:49

🛠 Fix options

Check one option and Autter will start a fix run for the unresolved issues above.

  • One PR with all unresolved fixes
  • One independent PR per unresolved issue

Checking a box triggers the fix run immediately — Autter comments back with the issues being fixed and the branch created for each.

…viction, sanitized health stats

Addresses the Autter review findings on the at-least-once sink delivery PR:

- occurrenceId is now content-derived (occurrenceIdFor), not a fresh UUID
  per request. OTLP exporters retry whole batches after a 503 from a
  partially-committed multi-table ClickHouse write (or a lost 2xx); with
  random ids every such retry became an undetectable duplicate downstream.
  Deterministic ids make transport retries idempotent end-to-end: the
  consumer's per-occurrence ledger dedupes them and duplicated ClickHouse
  rows stay identifiable by sharing an id.
- Buffer overflow now evicts the oldest batch of the org holding the most
  buffered bytes, so one flooding tenant cannot evict everyone else; a
  single batch larger than the whole buffer is dropped alone instead of
  flushing the queue.
- Retrying batches re-enter the queue at their enqueue-age position
  (seq-sorted), keeping the documented oldest-first eviction true under
  sustained failure.
- A success only clears the failure episode if no failure occurred after
  that request started — a concurrent stale success can no longer reopen
  full concurrency mid-outage (retry stampede).
- /healthz failure detail is a fixed category (timeout, connection_error,
  http_<status>, error); raw transport/exception text stays in server logs
  only.
- scheduleWake no longer spins 0 ms timers while all delivery slots are
  busy; completions re-pump instead.
- First test suite: sink delivery/retry/eviction/episode semantics and
  occurrence-id determinism (npm test -w @autter/otlp-ingester, no network).
- example-express-app gets a build script (node --check) so workspace-wide
  builds pass; Biome template-literal lints and markdownlint MD013 fixed.

Generated-By: PostHog Desktop
Task-Id: c03f7b14-557a-4204-b347-c0ef8d567d3b
@sagnik11

Copy link
Copy Markdown
Member Author

Pushed d6fb5ef addressing the review findings:

  • Partial ClickHouse writes / non-atomic multi-table insertoccurrenceId is now content-derived (occurrenceIdFor in fingerprint.ts), not a per-request UUID. An exporter that retries after a 503 from a partially-committed write (or a lost 2xx) reproduces the same ids, so the consumer's per-occurrence dedupe ledger absorbs the retry and duplicated ClickHouse rows stay identifiable (the consumer reconciler counts distinct ids). Recovery boundary documented at the insert site and in ARCHITECTURE.md.
  • Per-tenant egress amplification — buffer overflow now evicts the oldest batch of the org holding the most buffered bytes, so one flooding tenant can't evict everyone else; a single batch larger than the whole buffer drops alone instead of flushing the queue.
  • Retry ordering — retrying batches re-enter the queue at their enqueue-age position (seq-sorted), keeping oldest-first eviction true under sustained failure.
  • Concurrent success resetting failure state — a success only clears the episode if no failure occurred after that request started; stale successes can't reopen full concurrency mid-outage. Also fixed a 0 ms-timer spin in scheduleWake when all delivery slots are busy.
  • Health endpoint leakage/healthz now reports sink.lastFailureReason as a fixed category (timeout, connection_error, http_<status>, error); raw exception text stays in server logs.
  • Build failureexamples/express-app has a build script (node --check server.js).
  • Tests — first suite in the repo: 12 tests covering delivery shape, retry/backoff, permanent drops, episode semantics, eviction fairness/ordering, oversized batches, sanitized stats, and occurrence-id determinism (npm test -w @autter/otlp-ingester, no network needed). Test files are excluded from the published build.
  • Biome template-literal lints and markdownlint MD013 fixed; enqueue split into buildBatch/signalRange helpers.

Merge order note: land the consumer side (Autter-dev/autter-monorepo#247, dedupe ledger + reconciler) before or together with this PR — a retrying ingester pointed at a ledger-less consumer would double-count redelivered batches.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter could not complete PR review for #13. Autter hit an internal error and could not complete this review. Push a new revision to re-run it.

@autter-dev

autter-dev Bot commented Aug 17, 2026

Copy link
Copy Markdown

🔄 Re-running my review on the latest commit — fresh findings will appear shortly.

🤖 Replying to your mention — mention @autter again to continue.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Autter could not complete PR review for #13. Autter hit an internal error and could not complete this review. Push a new revision to re-run it.

@sagnik11
sagnik11 merged commit 67f1740 into main Aug 18, 2026
1 check passed

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Autter review in progress — running security, correctness & dependency checks on this PR. Follow live step-by-step progress on the autter/review-gate check in the merge box. Merge is blocked until the gate completes; Autter approves automatically when the review comes back clean, and releases this hold with a neutral review when it finds non-blocking issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant