From 03c4e2314aa0ed3ea234c94edab4ac1b7aef329a Mon Sep 17 00:00:00 2001 From: "Victor \"David\" Medina" Date: Mon, 15 Jun 2026 19:49:53 -0400 Subject: [PATCH 1/2] feat(obs): Phase-0 oauth+approve trace wire (additive, un-deployed) Wire the Phase-0 OAUTH -> APPROVE trace into relaylaunch-console as a dependency-free, failure-isolated logger shim. NO new npm deps, NO package.json/lock change, NO migration, NO SDK/exporter registration. - lib/observability/phase0-trace.ts: dep-free shim. emit() records the Phase-0 span name + honesty attrs to the existing pino logger (@/lib/logger), internally try/catch so it can NEVER throw into the caller. Public phase0.* surface matches the spec stub so the deploy-time swap to @opentelemetry/api + Langfuse OTLP exporter happens at the SAME call-sites with no caller changes. - app/(auth)/auth/callback/route.ts: phase0.oauthConnect after the real verifyOtp/exchangeCodeForSession result (oauth.result success|error). - app/api/v1/actions/approve/route.ts: phase0.approve after the recovery_items approve update succeeds (email deep-link, mode='live'). - app/api/v1/action-map/briefs/[id]/approve/route.ts: phase0.approve after the recovery_opportunities update succeeds (in-app, mode='live'). ADDITIVE ONLY: every phase0.* call is a new standalone statement placed AFTER the real outcome is known; no existing logic changed/reordered/gated. Honesty attrs: approval.mode=live + oauth.result. Verified tsc --noEmit + eslint clean on all four touched files. DRAFT until first receipt. Contract: docs/specs/PHASE0-OTEL-TRACE-OAUTH-SEND-RECEIPT-2026-06-15.md Co-Authored-By: Claude Opus 4.6 (1M context) --- app/(auth)/auth/callback/route.ts | 4 + .../action-map/briefs/[id]/approve/route.ts | 12 +++ app/api/v1/actions/approve/route.ts | 12 +++ lib/observability/phase0-trace.ts | 81 +++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 lib/observability/phase0-trace.ts diff --git a/app/(auth)/auth/callback/route.ts b/app/(auth)/auth/callback/route.ts index 6f07c4f9a..dc67ee0e9 100644 --- a/app/(auth)/auth/callback/route.ts +++ b/app/(auth)/auth/callback/route.ts @@ -5,6 +5,7 @@ import { withErrorHandler } from "@/lib/api/error-handler"; import { TIER_RANK } from "@/lib/navigation"; import { sanitizeInternalRedirect } from "@/lib/auth/safe-redirect"; import logger from "@/lib/logger"; +import { phase0 } from "@/lib/observability/phase0-trace"; export const GET = withErrorHandler(async function GET(request: NextRequest) { const { searchParams, origin } = new URL(request.url); @@ -41,6 +42,9 @@ export const GET = withErrorHandler(async function GET(request: NextRequest) { "[auth-callback] Session exchange failed", ); } + // [phase0] relay.oauth.connect — additive, after the real auth result is known. Failure-isolated + // (emit() is internally try/catch); never gates or reorders the redirect logic below. + phase0.oauthConnect({ provider: "google", result: error ? "error" : "success" }); if (!error) { const { data: { user } } = await supabase.auth.getUser(); if (user) { diff --git a/app/api/v1/action-map/briefs/[id]/approve/route.ts b/app/api/v1/action-map/briefs/[id]/approve/route.ts index 02c7602fe..39a7a99d6 100644 --- a/app/api/v1/action-map/briefs/[id]/approve/route.ts +++ b/app/api/v1/action-map/briefs/[id]/approve/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@/lib/supabase/server"; import { withErrorHandler } from "@/lib/api/error-handler"; import { buildOutcomeLedgerSeed } from "@/lib/outcomes"; +import { phase0 } from "@/lib/observability/phase0-trace"; type ApprovalOpportunity = { id: string; @@ -124,6 +125,17 @@ export const POST = withErrorHandler(async function POST(request: NextRequest) { } opportunity = approvedOpportunity as ApprovalOpportunity; + + // [phase0] relay.action.approve — additive, AFTER the recovery_opportunities update succeeds. + // In-app approve surface. mode='live'; channel='email' (the eventual outreach channel for this + // recovery action). Failure-isolated; runs after the real write, never gates the loop. + phase0.approve({ + tenantId: profile.tenant_id, + actionId: id, + channel: "email", + approver: user.id, + mode: "live", + }); } else if (!isAlreadyApproved) { failed++; continue; diff --git a/app/api/v1/actions/approve/route.ts b/app/api/v1/actions/approve/route.ts index a94ead9db..842732765 100644 --- a/app/api/v1/actions/approve/route.ts +++ b/app/api/v1/actions/approve/route.ts @@ -2,6 +2,7 @@ import { NextRequest } from "next/server"; import { verifyActionToken } from "@/lib/actions/token"; import { getSupabaseAdmin } from "@/lib/supabase/admin"; import logger from "@/lib/logger"; +import { phase0 } from "@/lib/observability/phase0-trace"; export const dynamic = "force-dynamic"; @@ -88,6 +89,17 @@ export async function GET(request: NextRequest) { ); } + // [phase0] relay.action.approve — additive, AFTER the recovery_items approve update succeeds. + // mode='live' (real one-tap email approval, not a projected demo). Failure-isolated; runs after + // the real write, so even a throw could not undo the approval. channel='email' (deep-link path). + phase0.approve({ + tenantId: item.tenant_id, + actionId: item.id, + channel: "email", + approver: payload.userId, + mode: "live", + }); + // Queue outreach if there's a suggested action if (item.suggested_action) { await supabase.from("outreach_queue").insert({ diff --git a/lib/observability/phase0-trace.ts b/lib/observability/phase0-trace.ts new file mode 100644 index 000000000..ac5d54a5b --- /dev/null +++ b/lib/observability/phase0-trace.ts @@ -0,0 +1,81 @@ +/** + * Phase 0 trace shim — OAuth -> approve -> send -> receipt, the first wire. + * + * STATUS: NO-DEPLOY-RISK FIRST WIRE. Dependency-free. Records the Phase-0 span name + honesty + * attributes to the repo's EXISTING structured logger (pino, `@/lib/logger`), wrapped so it can + * NEVER throw into the caller. This makes the Phase-0 path observable in runtime logs today + * without adding any npm dependency, SDK, exporter, or migration. + * + * Contract: docs/specs/PHASE0-OTEL-TRACE-OAUTH-SEND-RECEIPT-2026-06-15.md (+ phase0-trace.stub.ts). + * The stable attribute keys (`oauth.result`, `approval.mode`, `send.status`, `receipt.amount_cents`, + * etc.) mirror that spec. `approval.mode=live|projected` and `send.status=held` are the honesty + * guards: a HOLD or a projected (not-live) run stays visible, so a demo can never be mistaken for a + * real recovered dollar (keeps the $500 HRC website payment vs first-recovered-dollar line honest). + * + * DEPLOY-TIME FOLLOW-UP (documented, not done here): swap the `emit()` body for the real + * `@opentelemetry/api` SDK + Langfuse OTLP exporter at these SAME call-sites — the public `phase0.*` + * surface stays identical, so wiring those call-sites once is the whole cost. Confirm the Langfuse + * OTLP path + tracer API via Context7 before that swap (CONTEXT7 ALWAYS). + */ + +import logger from "@/lib/logger"; + +type Attrs = Record; + +/** + * Internal sink. Failure-isolated by design: the entire body is wrapped in try/catch so a logger + * fault (or a swapped-in OTel exporter fault, later) can never propagate into the auth / approve / + * send / receipt path it is observing. + */ +function emit(span: string, attrs: Attrs): void { + try { + logger.info({ phase0_span: span, ...attrs }, "[phase0] " + span); + } catch { + /* never throw into the caller — observability must not break the critical path */ + } +} + +export const phase0 = { + /** Stage 1 — OAuth connect (console). `result`=success|invalid_nonce|error. */ + oauthConnect(a: { + tenantId?: string; + provider: string; + result: string; + durationMs?: number; + }): void { + emit("relay.oauth.connect", a); + }, + + /** Stage 2 — Approve (console). `mode`=live|projected is the honesty guard. */ + approve(a: { + tenantId?: string; + actionId: string; + channel?: string; + approver?: string; + mode: string; + }): void { + emit("relay.action.approve", a); + }, + + /** Stage 3 — Send (relay-pulse). `status`=sent|failed|held. Wired at the pulse send path. */ + send(a: { + actionId: string; + provider: string; + status: string; + messageId?: string; + durationMs?: number; + }): void { + emit("relay.send.dispatch", a); + }, + + /** Stage 4 — Receipt (relay-pulse). The one measured number; `attributed` gates demo vs real. */ + receipt(a: { + actionId: string; + type: string; + amountCents: number; + attributed: boolean; + latencyMs?: number; + }): void { + emit("relay.receipt.attribute", a); + }, +}; From 63b120fe18b861aeb327873302d5c80e2f5fe4e2 Mon Sep 17 00:00:00 2001 From: "Victor \"David\" Medina" Date: Mon, 15 Jun 2026 19:56:51 -0400 Subject: [PATCH 2/2] chore(obs): de-identify Phase-0 shim comment (PII gate) Genericize the honesty-guard comment in lib/observability/phase0-trace.ts: the pilot business name is a banned token in changed-file PII scan (scripts/scan-pii-leaks.ps1). Replace it with "website/console payment" - same honesty point (measured recovered dollar is distinct from the website/console payment), no PII. Code behavior unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/observability/phase0-trace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/observability/phase0-trace.ts b/lib/observability/phase0-trace.ts index ac5d54a5b..fb4dbced2 100644 --- a/lib/observability/phase0-trace.ts +++ b/lib/observability/phase0-trace.ts @@ -10,7 +10,7 @@ * The stable attribute keys (`oauth.result`, `approval.mode`, `send.status`, `receipt.amount_cents`, * etc.) mirror that spec. `approval.mode=live|projected` and `send.status=held` are the honesty * guards: a HOLD or a projected (not-live) run stays visible, so a demo can never be mistaken for a - * real recovered dollar (keeps the $500 HRC website payment vs first-recovered-dollar line honest). + * real recovered dollar (keeps the website/console payment vs first-recovered-dollar line honest). * * DEPLOY-TIME FOLLOW-UP (documented, not done here): swap the `emit()` body for the real * `@opentelemetry/api` SDK + Langfuse OTLP exporter at these SAME call-sites — the public `phase0.*`