observability: backend-agnostic error-capture seam (Phase 1, inert until SENTRY_DSN)
This commit is contained in:
@@ -14,6 +14,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { timingSafeEqual } from 'node:crypto'
|
||||
import { createAdminClient } from '@/lib/supabase/admin'
|
||||
import { pollConnectionLog, ingestEvents, type IngestConn } from '@/lib/door/ingest'
|
||||
import { captureException, captureMessage } from '@/lib/observability/capture'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -50,7 +51,10 @@ export async function POST(req: NextRequest) {
|
||||
.eq('adapter', 'native_heatsync')
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(MAX_CONNECTIONS)
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
if (error) {
|
||||
captureException(error, { surface: 'cron/door-ingest', tags: { stage: 'load-connections' } })
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
|
||||
// Poll connections concurrently: each connection is independent, and the
|
||||
// executor's per-call timeout means a sequential loop would take the SUM of
|
||||
@@ -65,6 +69,12 @@ export async function POST(req: NextRequest) {
|
||||
const poll = await pollConnectionLog(admin, c)
|
||||
if (!poll.ok) {
|
||||
console.error(`[door-ingest] connection ${c.id}: ${poll.detail}`)
|
||||
captureMessage('door poll failed', {
|
||||
surface: 'cron/door-ingest',
|
||||
level: 'warning',
|
||||
tags: { space: c.space_id, connection: c.id },
|
||||
extra: { detail: poll.detail },
|
||||
})
|
||||
return { failed: true, inserted: 0, resolved: 0 }
|
||||
}
|
||||
const res = await ingestEvents(admin, c.space_id, c.id, poll.events)
|
||||
@@ -79,6 +89,7 @@ export async function POST(req: NextRequest) {
|
||||
if (r.status === 'rejected') {
|
||||
failed++
|
||||
console.error('[door-ingest] connection threw:', r.reason instanceof Error ? r.reason.message : r.reason)
|
||||
captureException(r.reason, { surface: 'cron/door-ingest', tags: { stage: 'poll' } })
|
||||
continue
|
||||
}
|
||||
if (r.value.failed) failed++
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type PrefMap,
|
||||
type NotificationCategory,
|
||||
} from '@/lib/notifications-prefs-logic'
|
||||
import { captureException, captureMessage } from '@/lib/observability/capture'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -63,7 +64,10 @@ export async function POST(req: NextRequest) {
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(CANDIDATES)
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
if (error) {
|
||||
captureException(error, { surface: 'cron/notifications', tags: { stage: 'candidates' } })
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
|
||||
// Fair drain: round-robin the oldest-first candidates across spaces so one
|
||||
// tenant's burst can't head-of-line-block others. Fairness binds only under
|
||||
@@ -110,7 +114,14 @@ export async function POST(req: NextRequest) {
|
||||
.in('member_id', memberIds)
|
||||
// Fail open: if the prefs lookup errors, send everything rather than risk
|
||||
// silently dropping a wanted (e.g. dues-failure) email on a transient blip.
|
||||
if (prefErr) console.error('[cron/notifications] prefs lookup failed:', prefErr.message)
|
||||
if (prefErr) {
|
||||
console.error('[cron/notifications] prefs lookup failed:', prefErr.message)
|
||||
captureMessage('prefs lookup failed (failing open, sending all)', {
|
||||
surface: 'cron/notifications',
|
||||
level: 'warning',
|
||||
extra: { error: prefErr.message },
|
||||
})
|
||||
}
|
||||
for (const p of prefRows ?? []) {
|
||||
const mid = p.member_id as string
|
||||
if (!prefsByMember.has(mid)) prefsByMember.set(mid, {})
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
getSpaceName as readSpaceName,
|
||||
buildManageUrl,
|
||||
} from '@/lib/notifications/enqueue'
|
||||
import { captureException } from '@/lib/observability/capture'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -76,6 +77,10 @@ export async function POST(
|
||||
return NextResponse.json({ received: true, duplicate: true })
|
||||
}
|
||||
console.error('[stripe webhook] dedupe insert failed:', dedupe.error.message)
|
||||
captureException(dedupe.error, {
|
||||
surface: 'stripe-webhook',
|
||||
tags: { space: spaceId, eventType: event.type, stage: 'dedupe' },
|
||||
})
|
||||
return NextResponse.json({ error: 'Webhook processing error' }, { status: 500 })
|
||||
}
|
||||
|
||||
@@ -315,6 +320,10 @@ export async function POST(
|
||||
// than silently short-circuited as a duplicate (no lost events).
|
||||
await admin.from('stripe_webhook_events').delete().eq('event_id', event.id)
|
||||
console.error('[stripe webhook] handler error:', e instanceof Error ? e.message : e)
|
||||
captureException(e, {
|
||||
surface: 'stripe-webhook',
|
||||
tags: { space: spaceId, eventId: event.id, eventType: event.type, stage: 'handler' },
|
||||
})
|
||||
return NextResponse.json({ error: 'Webhook handler error' }, { status: 500 })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user