Keep PostHog off the request path; point backend at PostHog US - #574
Open
kcarnold wants to merge 1 commit into
Open
Keep PostHog off the request path; point backend at PostHog US#574kcarnold wants to merge 1 commit into
kcarnold wants to merge 1 commit into
Conversation
A transient 526 from the analytics reverse proxy took the whole backend
down and failed a deploy. posthogMiddleware awaited posthog.flush() after
every request, on every route ('*'), and flush() rejects on any non-2xx —
so an ingestion failure became a 500 from a request that had already
succeeded, health probe and static frontend included.
It also hung: posthog-node's defaults are a 10s timeout x 4 attempts with
3s between them, and flush() serializes behind the previous flush, so
concurrent requests queued up multiplicatively. The onError path then paid
it a second time via captureException's own await flush().
Ingestion now rides posthog-node's background batching, which swallows its
own errors; shutdownPosthog() still drains the tail on SIGTERM. Retries and
timeouts are capped so a dead host costs seconds, not ~49s. /api/ping is no
longer captured — health probes aren't product analytics.
POSTHOG_HOST now defaults to PostHog US directly. The e.thoughtful-ai.com
proxy exists to get the browser SDK past ad blockers; server-side it only
adds a hop that can fail. The frontend and experiment app still use it.
NOTE: the deployment sets POSTHOG_HOST explicitly, so it must be removed
there for this default to take effect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A transient 526 from
POSTHOG_HOST(thee.thoughtful-ai.comreverse proxy) brought the backend down and failed a deploy. The 526 was the trigger; the cause is that telemetry sat on the request path.What actually happened
posthogMiddlewaredidawait posthog.flush()after every request, mounted on'*':flush()rejects on any non-2xx from the host. That rejection propagated out of the middleware intoapp.onError, which returns a 500 — for a request that had already succeeded./api/ping, the Better Auth routes, the OpenAI proxy, and the static frontend catch-all. A total outage of the container, not a degraded analytics path. The readiness probe could never pass, so the rollout failed.requestTimeout: 10sx 4 attempts with3sbetween (a 526 passesretryCheck), so ~9s minimum and ~49s worst case per request. Worse,flush()serializes behind the previousflushPromise, so with N concurrent requests the Nth waited on all N−1 preceding failed flushes.onError→captureException→ which also awaitedflush(), chained behind the one that had just failed.Not a crash loop:
flushBackground()catches its own errors andcaptureExceptionhad atry/catch, so nothing could take the process down via an unhandled rejection. The blast radius was 500s and hangs.Changes
await flush()calls (middleware andcaptureException). Ingestion rides posthog-node's background batching (20 events or 10s), which swallows its own errors;shutdownPosthog()still drains the tail on SIGTERM, so nothing is lost on a normal rollout.requestTimeout: 3000,fetchRetryCount: 1,fetchRetryDelay: 1000. A dead host now costs seconds of background time./api/pingis no longer captured — health probes aren't product analytics, and they queued an event every few seconds forever.POSTHOG_HOSTdefaults tohttps://us.i.posthog.com, in code and in both compose files. The proxy earns its keep for the browser SDK (ad blockers); server-side it's pure added failure surface. The frontend and theexperimentservice are unchanged.backend/src/__tests__/posthog.test.ts: runs a real local HTTP server as a hostile PostHog (526, plus a variant that accepts and never responds) and asserts requests stay 200 and fast. Verified non-vacuous — restoringawait posthog.flush()fails 3 of its 4 tests. It uses a real server rather than a mockedfetchbecause posthog-node resolves the globalfetchonce at its own module load, outside Vitest's module registry, sovi.stubGlobaldoesn't reliably reach it.Deploy note
POSTHOG_HOSTexplicitly, so the new default does nothing until that env var is removed there (or set tohttps://us.i.posthog.com). Nothing in this PR changes the deployed value.Follow-ups not done here
${method} ${path}as the event name, so with the static catch-all every asset URL mints a distinct event — high-cardinality noise plus ingestion cost. Narrowing to/api/*, or capturing the matched route pattern, would help; it changes the analytics data, so it's a separate call.Tests: 110 passed,
tsc --noEmitclean.🤖 Generated with Claude Code