A standards-first observability contract for checkout flows — stitching browser actions through GraphQL, Rails, and Sidekiq into one traceable causal chain.
Link every backend request and worker job back to the specific user click or page load that triggered it.
See the exact operation name, route, and whether it was a query or mutation across the private/public boundary.
Distinguish request execution time from Sidekiq queue wait from worker execution — pinpoint the bottleneck.
Join across checkout_request_id, receipt_id, and payment_intent_id — all correlated.
traceparent — W3C trace + span IDtracestate — vendor statebaggage — strict allowlisted keys onlywhop.origin_action_id — causal rootUser clicks "Pay" — mints origin_action_id
Attaches x-whop-* headers + baggage
Rails normalizes trace & trust policy
Mutation runs, attaches business IDs
Serializes job["whop_trace"]
Restores trace context for immediate jobs
Checkout processed — full lineage visible
flowchart LR
A["🖱 Browser action"] --> B["⚡ Frontend producer"]
B --> C["🛡 Private GraphQL ingress"]
C --> D["⚙ Rails execution"]
D --> E["📤 Sidekiq enqueue"]
E --> F["📥 Sidekiq dequeue"]
F --> G["💳 Checkout worker"]
A -. "whop.origin_action_id" .-> B
B -. "traceparent / tracestate / baggage" .-> C
E -. "job[whop_trace] v1" .-> F
C --> H["📊 New Relic traces"]
C --> I["📈 api.request_served"]
C --> J["📋 MutationTracking"]
G --> H
Traces, logs, span timing, request/worker lineage, async boundary latency
User/business reporting, funnel analysis, cohort analysis, step counts
Exceptions, stack traces, breadcrumbs, error-centric debugging
whop.origin_action_id as a shared join key. One checkout issue = one ID across New Relic, ClickHouse, and Sentry.
| Boundary | Trust | Trace Policy | Causality |
|---|---|---|---|
| Browser → Frontend Producer | First-party | Agent-first; app augments | Mint or reuse action context |
| Browser → Hosted Checkout Handler | Mixed | Proxy/producer specific | Preserve same action unless new |
| Frontend/Next → Private GraphQL | Trusted | Continue normalized trace | Preserve action context |
| Public GraphQL / REST | Broader | Restart server trace | Preserve allowed causality only |
| Rails → Sidekiq Enqueue | Trusted | Immediate-only continuation | Serialize small v1 payload |
| Sidekiq Dequeue → Worker | Trusted | Continue immediate; else restart | Always preserve action ID |
whop.* namespace. Headers stay x-whop-*.User clicks, submits, or explicitly triggers
Component mounts and fetches initial data
Cache invalidation triggers a refetch
Periodic or stale-while-revalidate refresh
// ActionContext reference type { "origin_action_id": "act_01JPD3V0W9S0M8Q8F7M5JX4A2Z", "action_kind": "explicit_user_action", "action_name": "checkout_submit", "request_origin": "browser", "surface_area": "checkout", "product_type": "payin", "platform": "web" }
checkout_initial_load — page mountcheckout_submit — user clicks Paypayment_method_update — user changes payment methodcheckout_post_submit_refresh — post-submit pollingaction_name is categorical and controlled, not free-formjob["whop_trace"] — carries versioned trace + causality metadata across the async boundary.// job["whop_trace"] — v1 schema { "v": 1, "traceparent": "00-<trace-id>-<span-id>-01", "tracestate": "vendor-state-if-present", "baggage": "whop.request_origin=browser,whop.action_kind=explicit_user_action,...", "origin_action_id": "act_01JPD3V0W9S0M8Q8F7M5JX4A2Z", "request_id": "req_123", "graphql_operation_name": "ProcessCheckout" }
job["whop_trace"]job["whop_trace"]origin_action_idorigin_action_id for causal linkage.
flowchart TB
A["Read headers: traceparent, tracestate, baggage, x-whop-*"] --> B{"Route class?"}
B -->|Private first-party| C["Continue normalized upstream trace"]
B -->|Public / REST| D["Restart server trace"]
C --> E["Parse allowlisted baggage keys"]
D --> E
E --> F["Derive server-side fields"]
F --> G["Resolve precedence:\nserver > x-whop-* > baggage"]
G --> H["Build request observability context"]
H --> I["Attach to traces, logs, request context"]
I --> J["Return for request + enqueue usage"]
When fields disagree:
x-whop-* header valuesWhere an action context should exist but doesn't — indicates a gap in producer instrumentation.
Baggage that couldn't be parsed was dropped — may indicate a misbehaving upstream.
Budget exceeded, optional keys were shed — monitor for unexpected cardinality growth.
Route policy triggered a trace restart — expected for public paths, unexpected for private ones.
Worker couldn't restore trace from whop_trace — degraded lineage marker emitted.
Canonical names, precedence rules, baggage allowlist/budget, route-class trust policy, whop_trace v1 schema, unit tests.
One checkout producer path, Rails private GraphQL normalization, request/log enrichment, rollout-health markers.
Client and server middleware, immediate-only continuity rule, one checkout worker continuation path.
Queries-only private GraphQL request parity, validation guide, rollout dashboards/queries.
Only after golden-path validation succeeds. Additional checkout actions, adjacent producers or workers.
frontend/apps/corefrontend/packages/gqlfrontend/packages/sdk/gql/serverai-agent/| Layer | Test Case | Expected |
|---|---|---|
| Browser producer | Explicit checkout submit | Action headers present; action reused on retry |
| Browser producer | Mount load | Action ID exists with mount kind |
| Next producer | Proxy continuation | Preserves trusted context without minting new action |
| Rails ingress | Private checkout GraphQL | Normalized trace/context attached |
| Rails ingress | Public GraphQL/REST | Server trace restart + preserved causality |
| Sidekiq client | Enqueue from request path | whop_trace.v = 1 with small payload |
| Sidekiq server | Immediate continuation | Trace restored/continued |
| Sidekiq server | Delayed retry | Fresh trace; same action ID |
| Worker logging | Checkout worker | origin_action_id + checkout join key visible |
whop.origin_action_idwhop.origin_action_idjob["whop_trace"] with v: 1 on enqueuewhop.origin_action_idcheckout_request_id visible once knownwhop.origin_action_idtrace_idcheckout_request_idMulti-error response modeling as a first-class request-envelope concern.
Tracing causal chains through websocket delivery and real-time updates.
Instrumentation for ai-agent/ paths and their unique action models.
D-021 Existing framework/vendor propagation owns W3C transport where it already works. App helpers own x-whop-* causality headers, baggage filtering, trust normalization, and Sidekiq payload serialization.
D-022 Continue normalized inbound trace context on private first-party checkout/private GraphQL paths. Restart server trace by default on public GraphQL and REST/proxy ingress.
whop.*
Naming Schema
D-023 Use semconv-leaning names like http.route and graphql.operation.*, with Whop-specific fields under whop.*.
D-024 Direct request-triggered enqueue may remain in the same trace tree. Delayed retries or manual requeues start a fresh trace while preserving action lineage.
x-whop-* > baggage
Field Precedence
D-025 Server-derived values win, normalized first-party headers win next, baggage is lowest priority.
D-026 Every emitted action context gets an action ID: explicit actions, mounts, invalidations, and background refreshes.
D-027 Keep GraphQL request-envelope outcome modeling minimal in v1. Deeper partial-failure work stays out of the first rollout.
D-028 Version only the custom Sidekiq carrier via job["whop_trace"].v = 1; keep HTTP/header versioning implicit.
whop.origin_action_id
Canonical Causality Attribute
D-029 Use whop.origin_action_id as the canonical emitted telemetry attribute, while keeping x-whop-origin-action-id as the header.
D-030 Prefer public tags when naturally available, but reuse existing safe IDs when that avoids churn. Do not expand risky internal IDs casually.
D-031 Require lightweight queryable signals for missing action IDs, malformed baggage, truncation, trust-policy restarts, and Sidekiq restore failures.
| ID | Status | Decision | Why it mattered |
|---|---|---|---|
D-001 |
Decided | Use CRITICAL-PATH-OBSERVABILITY as the spec slug. |
Anchored the full artifact trail in one durable directory. |
D-002 |
Decided | Ground the work in repo evidence and primary docs, not the intern handoff alone. | Kept the spec tied to actual seams instead of handoff assumptions. |
D-005 |
Decided | Standards-first transport with W3C trace context. | Locked the foundational transport stance early. |
D-006 |
Decided | Narrow v1 scope. | Prevented the project from expanding into payouts, websockets, and AI too early. |
D-008 |
Decided | Keep file placement lightly opinionated. | Left room for local conventions while still specing responsibilities clearly. |
D-009 |
Proposed | Private GraphQL likely missed request-served parity by rollout gap, not design. | Shaped the parity work as cleanup rather than a radical new pattern. |
D-014 |
Decided | First-party action lineage ID mandatory in v1. | Made causality explicit instead of hoping trace transport alone would answer product questions. |
D-015 |
Decided | Very strict allowlisted baggage. | Locked in the privacy and cardinality posture before any implementation details spread. |
D-016 |
Decided | Private api.request_served parity is queries-only in v1. |
Kept request-envelope telemetry separate from MutationTracking. |
D-018 |
Decided | Exclude AI agent from the current rollout. | Removed the biggest multi-runtime ambiguity from the first slice. |
D-019 |
Decided | origin_action_id is global causality; checkout_request_id is checkout business join. |
Preserved the core conceptual split when merging drafts. |
D-020 |
Decided | Publish merged drafts as separate artifacts instead of overwriting earlier specs. | Kept the review trail inspectable across iterations. |
| ID | Status | Short decision |
|---|---|---|
D-001 | Decided | Slug is CRITICAL-PATH-OBSERVABILITY. |
D-002 | Decided | Ground in repo evidence and primary docs. |
D-003 | Proposed | Early W3C transport proposal before user confirmation. |
D-004 | Proposed | Early broader rollout focus before v1 narrowing. |
D-005 | Decided | Standards-first W3C transport. |
D-006 | Decided | Keep v1 narrow. |
D-007 | Proposed | Tentative private GraphQL request-served parity. |
D-008 | Decided | Lightly opinionated file placement. |
D-009 | Proposed | Private GraphQL parity gap likely accidental. |
D-010 | Proposed | Early two-layer causality model before user ratified mandatory action ID. |
D-011 | Proposed | Early very-small baggage proposal before final confirmation. |
D-012 | Proposed | Request-envelope telemetry should not replace mutation analytics. |
D-013 | Proposed | Websocket causality deferred out of v1. |
D-014 | Decided | Action lineage ID mandatory in v1. |
D-015 | Decided | Strict baggage allowlist. |
D-016 | Decided | Private request-served parity is queries-only. |
D-017 | Deferred | AI approval semantics deferred. |
D-018 | Decided | AI agent excluded from current spec. |
D-019 | Decided | Global causality key vs checkout business join key preserved. |
D-020 | Decided | Merged draft published separately. |
D-021 | Decided | Hybrid, agent-first propagation ownership. |
D-022 | Decided | Route-class trust policy. |
D-023 | Decided | Semconv-leaning naming with whop.*. |
D-024 | Decided | Immediate-only Sidekiq trace continuity. |
D-025 | Decided | Precedence is server-derived then first-party headers then baggage. |
D-026 | Decided | All emitted action contexts get IDs. |
D-027 | Decided | GraphQL outcome modeling stays minimal in v1. |
D-028 | Decided | Version Sidekiq carrier only. |
D-029 | Decided | whop.origin_action_id is the canonical emitted telemetry attribute. |
D-030 | Decided | Mixed existing-safe business ID policy. |
D-031 | Decided | Require lightweight rollout-health signals in v1. |