{"record":{"id":"b095364d856e649a","repo":"neondatabase/neon","slug":"failed-to-capture-events","errorCode":null,"errorMessage":"Failed to capture events: {}, {}","messagePattern":"Failed to capture events: (.+?), (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"libs/posthog_client_lite/src/lib.rs","lineNumber":627,"sourceCode":"    ) -> anyhow::Result<()> {\n        // PUBLIC_URL/capture/\n        let url = format!(\"{}/capture/\", self.config.public_api_url);\n        let response = self\n            .client\n            .post(url)\n            .body(serde_json::to_string(&json!({\n                \"api_key\": self.config.client_api_key,\n                \"distinct_id\": distinct_id,\n                \"event\": event,\n                \"properties\": properties,\n            }))?)\n            .send()\n            .await?;\n        let status = response.status();\n        let body = response.text().await?;\n        if !status.is_success() {\n            return Err(anyhow::anyhow!(\n                \"Failed to capture events: {}, {}\",\n                status,\n                body\n            ));\n        }\n        Ok(())\n    }\n\n    pub async fn capture_event_batch(&self, events: &[CaptureEvent]) -> anyhow::Result<()> {\n        // PUBLIC_URL/batch/\n        let url = format!(\"{}/batch/\", self.config.public_api_url);\n        let response = self\n            .client\n            .post(url)\n            .body(serde_json::to_string(&json!({\n                \"api_key\": self.config.client_api_key,\n                \"batch\": events,\n            }))?)\n            .send()","sourceCodeStart":609,"sourceCodeEnd":645,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/posthog_client_lite/src/lib.rs#L609-L645","documentation":"capture_event POSTs a single event {api_key, distinct_id, event, properties} to {public_api_url}/capture/ using the client_api_key. Any non-2xx response produces this error with the HTTP status and PostHog's response body. The body typically names the concrete problem: invalid API key, invalid JSON/property types, or payload-size limits.","triggerScenarios":"401 'invalid API key' when client_api_key is wrong or from a different project; 400 when properties contain unserializable/wrong-typed values or the event name is empty; 413 when a huge properties payload exceeds the capture limit; 5xx during PostHog incidents.","commonSituations":"Using the server/personal key where the project API key is required (or vice versa); events sent to the wrong region (EU vs US public_api_url); oversized batch-like properties on a single event; proxy/WAF rejecting the JSON POST.","solutions":["Check the embedded body text: fix the client_api_key for 401, the properties shape for 400","Ensure public_api_url points at the capture endpoint of the correct PostHog region/project","Shrink or split very large property payloads (move big blobs out of event properties)","Treat 5xx as transient: retry with backoff or drop the event (it is telemetry), but log the body"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Only send events you know are well-formed; cheap client-side checks avoid 400s:\nfn event_is_well_formed(event: &str, distinct_id: &str, props: &serde_json::Value) -> bool {\n    !event.is_empty()\n        && !distinct_id.is_empty()\n        && props.is_object() // posthog expects an object of properties\n        && serde_json::to_string(props).map(|s| s.len() < 512 * 1024).unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"// Telemetry must never take down the caller: retry transient, swallow-and-log the rest.\nlet mut bo = ExponentialBackoff::default();\nloop {\n    match client.capture_event(event, distinct_id, &props).await {\n        Ok(()) => break,\n        Err(e) => {\n            let msg = format!(\"{e:#}\");\n            let transient = msg.contains(\" 5\") || msg.contains(\"timeout\") || msg.contains(\" 429, \");\n            if !transient || bo.next_backoff().is_none() {\n                tracing::warn!(\"posthog capture dropped: {msg}\");\n                break; // analytics is best-effort\n            }\n        }\n    }\n}","preventionTips":["Treat capture as best-effort: wrap calls so failures log-and-drop instead of propagating","Unit-test event producers for non-empty names/ids and JSON-serializable properties","Keep properties small and primitive; never embed blobs, logs, or payloads in events","Alert on capture error rate, not on individual errors -- a rising 401 signals key rotation"],"tags":["rust","posthog","analytics","api","http"],"backgroundTag":"http-api-request-failed","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}