Skip to content

feat(observability): replace sentry with posthog#1844

Closed
riderx wants to merge 4 commits intomainfrom
codex/replace-sentry-with-posthog
Closed

feat(observability): replace sentry with posthog#1844
riderx wants to merge 4 commits intomainfrom
codex/replace-sentry-with-posthog

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Mar 23, 2026

Summary (AI generated)

  • replace the shared Hono Sentry middleware path with PostHog exception capture for Cloudflare Workers and Supabase edge functions
  • capture frontend runtime, promise rejection, and Vue app errors through the existing PostHog loader
  • remove Sentry-specific dependencies, env examples, and CI secret wiring so the repo no longer references Sentry

Motivation (AI generated)

Capgo already uses PostHog for product analytics, but operational errors were still split across Sentry-specific middleware and deploy wiring. Consolidating on PostHog removes the extra vendor-specific maintenance surface and keeps error events, product events, and user identity on the same platform.

Business Impact (AI generated)

This reduces observability integration overhead, removes Sentry-specific secret management from CI/CD, and gives support and product teams a single place to inspect both usage signals and application failures. It also lowers the risk of drift between analytics and error-reporting identity data.

Test Plan (AI generated)

  • bun run lint:fix
  • bun run lint:backend
  • bun run typecheck
  • bun run test:backend

Generated with AI

Summary by CodeRabbit

  • Chores

    • Removed Sentry from deployment and runtime setups; deleted related build/upload steps and dependencies.
    • Adopted PostHog for telemetry and updated environment/config templates.
  • Bug Fixes

    • Broadened client- and server-side error reporting to capture richer exception details and improve diagnostics.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

This PR removes Sentry instrumentation and dependencies from CI, server, and worker entrypoints, replaces Sentry error capture with PostHog-based exception reporting across backend functions, and adds client-side PostHog exception capture and earlier PostHog initialization.

Changes

Cohort / File(s) Summary
CI / Package
\.github/workflows/build_and_deploy.yml, package.json
Removed SENTRY_AUTH_TOKEN usage, Sentry release flag, sourcemap upload script/step, and deleted @hono/sentry / @sentry/cli entries.
Cloudflare Workers
cloudflare_workers/api/index.ts, cloudflare_workers/files/index.ts, cloudflare_workers/plugin/index.ts
Removed env imports and stopped passing env.SENTRY_DSN into createHono.
Supabase Function Entrypoints
supabase/functions/*/index.ts (multiple functions)
Removed third argument Deno.env.get('SENTRY_DSN_SUPABASE') from all createHono calls.
Supabase Utils
supabase/functions/_backend/utils/hono.ts, supabase/functions/_backend/utils/on_error.ts, supabase/functions/_backend/utils/posthog.ts, supabase/functions/deno.json
Deleted @hono/sentry import and global Sentry middleware; rewrote on_error to enqueue PostHog exception captures and added robust PostHog exception capture, stack parsing, send helper, and Deno import removal.
Client App
src/main.ts
Initialized PostHog earlier; expanded window error and unhandledrejection handlers and added app.config.errorHandler to report exceptions via PostHog with metadata.
PostHog Service
src/services/posthog.ts
Added exported captureException to safely forward exceptions to PostHog (guards for local env and missing client).
Env Example
supabase/functions/.env.example
Removed SENTRY_DSN placeholder; added POSTHOG_API_KEY and POSTHOG_API_HOST placeholders.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant EdgeFunction
  participant BackgroundTask
  participant PostHog
  participant Discord
  Client->>EdgeFunction: request triggers error
  EdgeFunction->>BackgroundTask: enqueue capturePosthogException(error, metadata)
  BackgroundTask->>PostHog: POST /capture with exception payload
  BackgroundTask->>Discord: optional alert (depending on error)
  PostHog-->>BackgroundTask: 200 / non-OK logged
  EdgeFunction-->>Client: returns HTTP response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped from DSNs to PostHog's glen,
Sending stacks and traces now and then,
I queue the catch, I log the clue,
A tiny rabbit ships it through.
🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the core changes and motivation but lacks a test plan section matching the required template structure. Add a properly formatted 'Test plan' section with specific steps, or reorganize the AI-generated content to match the template's 'Summary', 'Test plan', 'Screenshots', and 'Checklist' sections.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: replacing Sentry observability with PostHog across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/replace-sentry-with-posthog

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
supabase/functions/_backend/utils/posthog.ts (1)

68-72: Minor: Redundant condition check.

The !existInEnv(c, 'POSTHOG_API_KEY') check is redundant. If apiKey is truthy (non-empty string), then existInEnv must return true. The !apiKey check alone handles both "key doesn't exist" and "key exists but is empty" cases.

♻️ Suggested simplification
 async function sendPostHogCapture(c: Context, body: PostHogCapturePayload) {
   const apiKey = getEnv(c, 'POSTHOG_API_KEY')
-  if (!apiKey || !existInEnv(c, 'POSTHOG_API_KEY')) {
+  if (!apiKey) {
     cloudlog({ requestId: c.get('requestId'), message: 'PostHog not configured' })
     return false
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/utils/posthog.ts` around lines 68 - 72, The
condition in sendPostHogCapture is redundant: remove the existInEnv(c,
'POSTHOG_API_KEY') check and only check the apiKey value returned by getEnv(c,
'POSTHOG_API_KEY'); update the if to use if (!apiKey) so the block that logs
"PostHog not configured" and returns false runs when the key is missing or
empty, referencing the sendPostHogCapture function and the getEnv/existInEnv
calls to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@supabase/functions/_backend/utils/posthog.ts`:
- Around line 104-106: The helper parseIntOrUndefined incorrectly treats parsed
value 0 as missing because it returns Number.parseInt(input || '', 10) ||
undefined; update parseIntOrUndefined to first handle undefined input, then
parse with Number.parseInt and return undefined only when Number.isNaN(parsed)
(or otherwise detect invalid parse); specifically, keep the function name
parseIntOrUndefined and change its logic to return the parsed number (including
0) or undefined when parsing fails.

---

Nitpick comments:
In `@supabase/functions/_backend/utils/posthog.ts`:
- Around line 68-72: The condition in sendPostHogCapture is redundant: remove
the existInEnv(c, 'POSTHOG_API_KEY') check and only check the apiKey value
returned by getEnv(c, 'POSTHOG_API_KEY'); update the if to use if (!apiKey) so
the block that logs "PostHog not configured" and returns false runs when the key
is missing or empty, referencing the sendPostHogCapture function and the
getEnv/existInEnv calls to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 538bd666-eab1-4bd5-a0d5-00c412bf79f8

📥 Commits

Reviewing files that changed from the base of the PR and between 68c6957 and 4752f0d.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • supabase/functions/deno.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • .github/workflows/build_and_deploy.yml
  • cloudflare_workers/api/index.ts
  • cloudflare_workers/files/index.ts
  • cloudflare_workers/plugin/index.ts
  • package.json
  • src/main.ts
  • src/services/posthog.ts
  • supabase/functions/.env.example
  • supabase/functions/_backend/utils/hono.ts
  • supabase/functions/_backend/utils/on_error.ts
  • supabase/functions/_backend/utils/posthog.ts
  • supabase/functions/apikey/index.ts
  • supabase/functions/app/index.ts
  • supabase/functions/build/index.ts
  • supabase/functions/bundle/index.ts
  • supabase/functions/channel/index.ts
  • supabase/functions/channel_self/index.ts
  • supabase/functions/deno.json
  • supabase/functions/device/index.ts
  • supabase/functions/files/index.ts
  • supabase/functions/ok/index.ts
  • supabase/functions/organization/index.ts
  • supabase/functions/private/index.ts
  • supabase/functions/replication/index.ts
  • supabase/functions/statistics/index.ts
  • supabase/functions/stats/index.ts
  • supabase/functions/triggers/index.ts
  • supabase/functions/updates/index.ts
  • supabase/functions/updates_debug/index.ts
  • supabase/functions/webhooks/index.ts
💤 Files with no reviewable changes (2)
  • supabase/functions/deno.json
  • .github/workflows/build_and_deploy.yml

Comment thread supabase/functions/_backend/utils/posthog.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@supabase/functions/_backend/utils/posthog.ts`:
- Around line 77-87: The PostHog POST currently awaits fetch(posthogUrl, ...)
with no timeout; add an AbortController-based deadline so the request fails fast
on slow edge. Create an AbortController before calling fetch, pass
controller.signal into fetch options, start a timer (e.g., 500–2000ms) that
calls controller.abort() on expiry, and clear the timer after fetch completes;
keep the existing body payload (apiKey and ...body) and handle the aborted fetch
as a fast failure inside the same try block (the symbols to update are
posthogUrl, apiKey, body and the existing fetch call).
- Around line 61-75: getPostHogCaptureUrl and sendPostHogCapture currently
produce malformed URLs when POSTHOG_API_HOST ends with "capture" (with or
without trailing slash) and the helper is called before validation; update
getPostHogCaptureUrl to defensively normalize the host by stripping any trailing
"capture" or "capture/" segment before appending the "capture/" path, and move
the call to getPostHogCaptureUrl inside sendPostHogCapture's try-block after
checking apiKey so malformed hosts can be caught and cause sendPostHogCapture to
return false; ensure sendPostHogCapture handles URL construction errors (invalid
host) by logging and returning false.
- Around line 317-333: trackPosthogEvent currently uses a global 'anonymous'
distinct_id which merges unauthenticated traffic; change distinctId to
payload.user_id || c.get('requestId') || crypto.randomUUID(), and when user_id
is absent set properties.$process_person_profile = false and omit
properties.$set (or only include $set when user_id exists) to avoid mutating
person profiles; update the properties object and the call to sendPostHogCapture
accordingly (see trackPosthogEvent and sendPostHogCapture references).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c02824c8-f9e0-40cc-b723-700462c67df7

📥 Commits

Reviewing files that changed from the base of the PR and between d29ec51 and dca0b1e.

📒 Files selected for processing (1)
  • supabase/functions/_backend/utils/posthog.ts

Comment on lines +61 to +75
function getPostHogCaptureUrl(c: Context) {
const host = getEnv(c, 'POSTHOG_API_HOST') || POSTHOG_CAPTURE_URL
const posthogUrl = host.endsWith('/capture/')
return host.endsWith('/capture/')
? host
: new URL('capture/', host.endsWith('/') ? host : `${host}/`).toString()
}

const distinctId = payload.user_id || 'anonymous'

const properties = {
...(payload.tags || {}),
channel: payload.channel,
description: payload.description,
$set: payload.tags,
async function sendPostHogCapture(c: Context, body: PostHogCapturePayload) {
const apiKey = getEnv(c, 'POSTHOG_API_KEY')
if (!apiKey) {
cloudlog({ requestId: c.get('requestId'), message: 'PostHog not configured' })
return false
}

const body = {
api_key: apiKey,
event: payload.event,
distinct_id: distinctId,
properties,
ip: payload.ip,
timestamp: new Date().toISOString(),
}
const posthogUrl = getPostHogCaptureUrl(c)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize POSTHOG_API_HOST more defensively.

If POSTHOG_API_HOST is configured as .../capture without the trailing slash, Line 65 resolves it to .../capture/capture/. Also, getPostHogCaptureUrl(c) runs before the try, so a malformed host will escape this helper instead of degrading to false.

🔧 Proposed fix
 function getPostHogCaptureUrl(c: Context) {
   const host = getEnv(c, 'POSTHOG_API_HOST') || POSTHOG_CAPTURE_URL
+  if (host.endsWith('/capture'))
+    return `${host}/`
   return host.endsWith('/capture/')
     ? host
     : new URL('capture/', host.endsWith('/') ? host : `${host}/`).toString()
 }
 
 async function sendPostHogCapture(c: Context, body: PostHogCapturePayload) {
   const apiKey = getEnv(c, 'POSTHOG_API_KEY')
   if (!apiKey) {
     cloudlog({ requestId: c.get('requestId'), message: 'PostHog not configured' })
     return false
   }
 
-  const posthogUrl = getPostHogCaptureUrl(c)
-
   try {
+    const posthogUrl = getPostHogCaptureUrl(c)
     const res = await fetch(posthogUrl, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/utils/posthog.ts` around lines 61 - 75,
getPostHogCaptureUrl and sendPostHogCapture currently produce malformed URLs
when POSTHOG_API_HOST ends with "capture" (with or without trailing slash) and
the helper is called before validation; update getPostHogCaptureUrl to
defensively normalize the host by stripping any trailing "capture" or "capture/"
segment before appending the "capture/" path, and move the call to
getPostHogCaptureUrl inside sendPostHogCapture's try-block after checking apiKey
so malformed hosts can be caught and cause sendPostHogCapture to return false;
ensure sendPostHogCapture handles URL construction errors (invalid host) by
logging and returning false.

Comment on lines 77 to 87
try {
const res = await fetch(posthogUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
body: JSON.stringify({
api_key: apiKey,
...body,
}),
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a deadline to the PostHog fetch.

This helper is awaited from request-scoped code, so a slow PostHog edge can hold the API path open until the platform timeout. Observability failures should fail fast here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/utils/posthog.ts` around lines 77 - 87, The
PostHog POST currently awaits fetch(posthogUrl, ...) with no timeout; add an
AbortController-based deadline so the request fails fast on slow edge. Create an
AbortController before calling fetch, pass controller.signal into fetch options,
start a timer (e.g., 500–2000ms) that calls controller.abort() on expiry, and
clear the timer after fetch completes; keep the existing body payload (apiKey
and ...body) and handle the aborted fetch as a fast failure inside the same try
block (the symbols to update are posthogUrl, apiKey, body and the existing fetch
call).

Comment on lines +317 to +333
export async function trackPosthogEvent(c: Context, payload: Pick<TrackOptions, 'event'> & { user_id?: string } & Pick<TrackOptions, 'channel' | 'description'> & { ip?: string, tags?: Record<string, any> }) {
const distinctId = payload.user_id || 'anonymous'

const properties = {
...(payload.tags || {}),
channel: payload.channel,
description: payload.description,
$set: payload.tags,
}

return sendPostHogCapture(c, {
event: payload.event,
distinct_id: distinctId,
properties,
ip: payload.ip,
timestamp: new Date().toISOString(),
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the posthog.ts file to see the full context
cd supabase/functions/_backend/utils && head -400 posthog.ts | tail -100

Repository: Cap-go/capgo

Length of output: 3107


🏁 Script executed:

# Look for capturePosthogException function
rg -A 15 "function capturePosthogException|const capturePosthogException" supabase/functions/_backend/

Repository: Cap-go/capgo

Length of output: 1450


🏁 Script executed:

# Check how requestId is set in middleware
rg -B 5 -A 10 "requestId" supabase/functions/_backend/utils/ --type ts

Repository: Cap-go/capgo

Length of output: 50369


🏁 Script executed:

# Search for other usages of c.get('requestId') in backend code
rg "c\.get\('requestId'\)" supabase/functions/_backend/ --type ts

Repository: Cap-go/capgo

Length of output: 50369


Use per-request identity for unauthenticated PostHog events.

Currently, all unauthenticated events get distinct_id: 'anonymous', merging unrelated traffic into a single person profile where $set fields overwrite each other. Mirror the pattern in capturePosthogException() (line 346): use c.get('requestId') || crypto.randomUUID() as fallback and set $process_person_profile = false when user_id is absent to prevent profile mutations.

Fix
 export async function trackPosthogEvent(c: Context, payload: Pick<TrackOptions, 'event'> & { user_id?: string } & Pick<TrackOptions, 'channel' | 'description'> & { ip?: string, tags?: Record<string, any> }) {
-  const distinctId = payload.user_id || 'anonymous'
+  const distinctId = payload.user_id || c.get('requestId') || crypto.randomUUID()
 
-  const properties = {
+  const properties: Record<string, any> = {
     ...(payload.tags || {}),
     channel: payload.channel,
     description: payload.description,
     $set: payload.tags,
   }
+  if (!payload.user_id)
+    properties.$process_person_profile = false
 
   return sendPostHogCapture(c, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/utils/posthog.ts` around lines 317 - 333,
trackPosthogEvent currently uses a global 'anonymous' distinct_id which merges
unauthenticated traffic; change distinctId to payload.user_id ||
c.get('requestId') || crypto.randomUUID(), and when user_id is absent set
properties.$process_person_profile = false and omit properties.$set (or only
include $set when user_id exists) to avoid mutating person profiles; update the
properties object and the call to sendPostHogCapture accordingly (see
trackPosthogEvent and sendPostHogCapture references).

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx closed this Mar 25, 2026
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