feat(ingester): at-least-once sink delivery with bounded retry buffer - #13
Conversation
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
There was a problem hiding this comment.
🔴 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.
| // 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); |
There was a problem hiding this comment.
🟠 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.
| ) {} | ||
|
|
||
| /** Queue a batch for delivery. No-op when there is nothing to send. */ | ||
| enqueue( |
There was a problem hiding this comment.
🟡 [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.
🚦 Pre-merge checks ·
|
| Check | Status | Explanation |
|---|---|---|
| Removed observability | 1 potential issue(s) detected (max risk 65/100): packages/otlp-ingester/src/server.ts:208. | |
| Silent exception swallowing | 1 potential issue(s) detected (max risk 70/100): packages/otlp-ingester/src/sink.ts:247. | |
| Stack trace leakage | 1 potential issue(s) detected (max risk 62/100): packages/otlp-ingester/src/sink.ts:258. | |
| Idempotency key not detected | 1 potential issue(s) detected (max risk 48/100): packages/otlp-ingester/src/sink.ts:5. | |
| Possible non-atomic read-modify-write | 1 potential issue(s) detected (max risk 45/100): packages/otlp-ingester/src/sink.ts:247. | |
| Batch size limit not detected | 1 potential issue(s) detected (max risk 48/100): packages/otlp-ingester/src/sink.ts:57. | |
| Missing linked tracker issue | 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 | 1 potential issue(s) detected (max risk 65/100): packages/otlp-ingester/src/config.ts:26. | |
| Missing security-team review on sensitive path | 1 potential issue(s) detected (max risk 60/100): packages/otlp-ingester/src/config.ts:82. | |
| Source changes without matching tests | 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) | 1 potential issue(s) detected (max risk 80/100): packages/otlp-ingester/src/sink.ts:3. | |
| Runtime error risk | 1 finding(s) on changed lines. | |
| Excessive complexity | 2 finding(s) on changed lines. | |
| Dead export (no callers) | 1 finding(s) on changed lines. | |
| Complexity Guard | 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.
🧭 PR hygiene & process suggestionsAutter 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/100This 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 🛠 AI fix prompt (copy & paste into your coding agent)🟠 Source changes without matching tests — Risk: 65/100This 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)🟠 Missing CODEOWNERS approval — Risk: 65/100No 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)🟠 Source changes without matching tests — Risk: 60/100This 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 🛠 AI fix prompt (copy & paste into your coding agent)🟠 Missing security-team review for sink delivery — Risk: 60/100No 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)🟠 Missing linked tracker issue — Risk: 50/100This PR's title and body do not reference any tracker issue (GitHub 🛠 AI fix prompt (copy & paste into your coding agent)🟠 Source changes without matching tests — Risk: 50/100Application 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 🛠 AI fix prompt (copy & paste into your coding agent)🟡 Missing linked tracker issue — Risk: 25/100The 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 🛠 AI fix prompt (copy & paste into your coding agent)Flagged by Autter PR-hygiene checks.
|
There was a problem hiding this comment.
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.
| return; | ||
| } | ||
| forwardToSink(ctx, fingerprinted, metricPoints, llmCalls); | ||
| sink?.enqueue(ctx, fingerprinted, metricPoints, llmCalls); |
There was a problem hiding this comment.
🟠 [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.
| await res.text().catch(() => {}); | ||
| if (res.ok) { | ||
| this.delivered += 1; | ||
| this.consecutiveFailures = 0; |
There was a problem hiding this comment.
🟠 [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.
| RETRY_BASE_MS * 2 ** (batch.attempts - 1), | ||
| ); | ||
| batch.nextAttemptAt = Date.now() + backoff / 2 + Math.random() * (backoff / 2); | ||
| this.queue.push(batch); |
There was a problem hiding this comment.
🟠 [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() } : {}; |
There was a problem hiding this comment.
🟠 [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 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)
The traces handler starts four independent ClickHouse inserts in
|
| // 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 — ` + |
There was a problem hiding this comment.
🟠 [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.
| // ClickHouse, so the consumer's reconciliation replays it. | ||
| console.warn( | ||
| `${pending} sink batch(es) undelivered at shutdown — ` + | ||
| `recoverable via ClickHouse replay`, |
There was a problem hiding this comment.
🟠 [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.
| this.queuedBytes -= dropped.bytes; | ||
| this.droppedOverflow += 1; | ||
| console.warn( | ||
| `sink buffer overflow: dropped batch ${dropped.batchId} ` + |
There was a problem hiding this comment.
🟠 [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.
| console.warn( | ||
| `sink buffer overflow: dropped batch ${dropped.batchId} ` + | ||
| `(signals ${dropped.signalsFrom ?? "?"} .. ${dropped.signalsTo ?? "?"}) — ` + | ||
| `replay this range from ClickHouse`, |
There was a problem hiding this comment.
🟠 [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.
| if (permanent || batch.attempts >= this.config.sinkMaxAttempts) { | ||
| this.droppedPermanent += 1; | ||
| console.error( | ||
| `sink delivery gave up after ${batch.attempts} attempt(s) ` + |
There was a problem hiding this comment.
🟠 [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.
| `sink delivery gave up after ${batch.attempts} attempt(s) ` + | ||
| `(${failure}): batch ${batch.batchId} ` + | ||
| `(signals ${batch.signalsFrom ?? "?"} .. ${batch.signalsTo ?? "?"}) — ` + | ||
| `replay this range from ClickHouse`, |
There was a problem hiding this comment.
🟠 [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.
| | `/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) | |
There was a problem hiding this comment.
🟡 [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.
| | 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` | |
There was a problem hiding this comment.
🟡 [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.
| | `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`) | |
There was a problem hiding this comment.
🟡 [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.
| | `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) | |
There was a problem hiding this comment.
🟡 [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.
| | `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 | |
There was a problem hiding this comment.
🟡 [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'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)
What happened: The How to fix: Add a Build output |
🧪 Autter test runAutter executed the automated checks against No automated test suites were detected in this repository. Change coverage (is each changed file's behavior verified by a test?)
🤖 Coverage-check evidencepackages/otlp-ingester/src/config.ts packages/otlp-ingester/src/index.ts packages/otlp-ingester/src/server.ts packages/otlp-ingester/src/sink.ts Temporary tests are written under Test plan (from the PR description)
🤖 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. ✅ Configure small ✅ Query ⬜ items could not be verified automatically and still need a manual check. |
Autter task list
Generated from PR diff, blast radius, and context. Issues found
🛠 Fix optionsCheck one option and Autter will start a fix run for the unresolved issues above.
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
|
Pushed
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. |
There was a problem hiding this comment.
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.
|
🔄 Re-running my review on the latest commit — fresh findings will appear shortly. 🤖 Replying to your mention — mention @autter again to continue. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
🔴 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.
Problem
The sink webhook forward was a single fire-and-forget
fetchwith a 10 s timeout and aconsole.warnon 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.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.batchIdso consumers can dedupe re-deliveries (a batch can arrive twice when only the 2xx response was lost)./healthzexposes delivery counters (sink.queued/delivered/retried/droppedOverflow/droppedPermanent/lastFailureAt…) for missed-batch monitoring; shutdown logs any undelivered batches.ARCHITECTURE.md, compatibility table inPLAN.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
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
SinkForwarderinpackages/otlp-ingester/src/sink.tsto:batchIdfor consumer-side deduplication.createIngesterAppto enqueue sink payloads after successful ClickHouse persistence and expose the forwarder onIngesterApp.SIGTERM/SIGINT; undelivered batches are logged as recoverable through ClickHouse replay.SINK_MAX_ATTEMPTS,SINK_MAX_BUFFERED_BATCHES, andSINK_MAX_BUFFERED_MBconfiguration with defaults.Acceptance Criteria
AUTTER_SINK_URLis configured, successfully persisted ingest batches are forwarded with a stablebatchId.SINK_MAX_ATTEMPTS, without creating an unbounded in-memory queue./healthzincludes sink delivery statistics when a sink is configured and continues to report ClickHouse health correctly.AUTTER_SINK_URLis unset.Test Plan
batchId.SINK_MAX_ATTEMPTS,SINK_MAX_BUFFERED_BATCHES, andSINK_MAX_BUFFERED_MBvalues, generate failures, and verify the oldest buffered batches are dropped and logged at the configured bounds./healthzwhile sink deliveries are succeeding and failing, and verify the reported sink statistics reflect the pending and failed batches.SIGTERMorSIGINTwith pending sink work and verify shutdown logs the undelivered count and exits without preventing ClickHouse closure.RuntimeOccurrence/sink batch types.Rollback Plan
AUTTER_SINK_URLor point it to a disabled sink endpoint; ClickHouse ingestion remains available and can serve as the replay source.batchIddeduplication.Related Issues
No linked issue was identified.
Written for commit 26cc0c2. Summary will update on new commits.