A support timeline should answer:
What happened to this account, in what order, and where should an authorized
operator investigate next?
It is not a general user-surveillance feed. Include facts that help diagnose a customer-reported workflow, exclude unrelated behavior, and enforce account access on the server.
Design the operator workflow#
Start with common questions:
- Was onboarding completed?
- Did an invitation, export, report, or webhook reach a terminal outcome?
- Did a release or integration error coincide with the complaint?
- Was access, plan, or entitlement state changed?
- Is the problem isolated to one account or broadly occurring?
- Which request, delivery, or job ID connects to detailed logs?
For each question, identify the smallest event and property set that answers it. Do not start by unioning every collection and exposing raw JSON.
Define a normalized timeline contract#
Return a stable browser shape:
type TimelineItem = {
id: string;
occurredAt: string;
category:
| "lifecycle"
| "product"
| "billing"
| "integration"
| "reliability";
title: string;
outcome?: "success" | "failure" | "pending";
actorLabel?: string;
reference?: {
type: "request" | "job" | "delivery" | "invoice";
id: string;
};
release?: string;
};
Titles come from a server-owned event registry:
const timelineRegistry = {
account_activated: {
category: "lifecycle",
title: "Account activated"
},
report_generation_completed: {
category: "product",
title: "Report generation completed"
},
webhook_delivery_failed: {
category: "integration",
title: "Webhook delivery failed"
}
} as const;
Never render an event-controlled HTML title or arbitrary error message.
Emit support-ready terminal facts#
Useful events are compact and correlated:
{
"event": "report_generation_completed",
"event_id": "job_01J...",
"account_id": "acct_7",
"job_id": "job_01J...",
"report_id": "rpt_42",
"outcome": "failure",
"failure_class": "upstream_timeout",
"request_id": "req_91",
"release": "2026.07.26.1",
"schema_version": 1
}
Use allowlisted failure classes. Free-form stack traces, SQL, request bodies, email content, and provider payloads belong in restricted operational systems, if anywhere.
Prefer one terminal event per job or delivery. Attempt-level details can be high-volume and should appear only when an operator drills into an authorized diagnostic system.
Query an allowlisted union#
Normalize only approved event types from each collection:
WITH timeline AS (
SELECT
timestamp AS occurred_at,
JSONExtractString(json, 'event_id') AS item_id,
'product' AS category,
JSONExtractString(json, 'event') AS event,
JSONExtractString(json, 'outcome') AS outcome,
JSONExtractString(json, 'request_id') AS reference_id,
JSONExtractString(json, 'release') AS release
FROM product_events
WHERE
JSONExtractString(json, 'account_id') = 'acct_7'
AND JSONExtractString(json, 'event') IN (
'account_activated',
'report_generation_completed',
'dashboard_shared'
)
UNION ALL
SELECT
timestamp AS occurred_at,
JSONExtractString(json, 'event_id') AS item_id,
'integration' AS category,
JSONExtractString(json, 'event') AS event,
JSONExtractString(json, 'outcome') AS outcome,
JSONExtractString(json, 'delivery_id') AS reference_id,
JSONExtractString(json, 'release') AS release
FROM integration_events
WHERE
JSONExtractString(json, 'account_id') = 'acct_7'
AND JSONExtractString(json, 'event') IN (
'integration_connected',
'webhook_delivery_succeeded',
'webhook_delivery_failed'
)
)
SELECT
occurred_at,
item_id,
category,
event,
outcome,
reference_id,
release
FROM timeline
ORDER BY occurred_at DESC, item_id DESC
LIMIT 200
The literal account ID is illustrative. In production, the server validates an operator’s support scope and binds a normalized account ID into a fixed query template. The browser never submits arbitrary SQL, collection names, or unverified account filters.
Use stable item IDs to break timestamp ties. If multiple versions of the same
terminal event can appear, deduplicate by event_id before ordering.
Authorize the account on every request#
Recommended request path:
support browser
→ authenticated internal endpoint
→ support role and case authorization
→ account-scope lookup
→ fixed GraphJSON report
→ field normalization and redaction
→ browser
Validate:
- operator identity and active session
- support role
- organization or region assignment
- case or just-in-time access when policy requires it
- account existence and canonical ID
- requested date range and result limit
GraphJSON workspace access is not a substitute for the application’s case-level authorization. Keep the API key on the server and return only the normalized timeline.
Correlate without overexposing#
Reference IDs help an operator move from account context to detailed evidence:
request_idfor a server requestjob_idfor background workdelivery_idfor webhook attemptsinvoice_idfor a billing workflowreleasefor regression analysis
Show the reference only to roles that can use the destination system. An ID is not an authorization token.
Do not put a trace or log viewer in the customer-facing application by embedding an internal dashboard. Build a server-owned support workflow with explicit scope.
Model state changes explicitly#
For plan, entitlement, access, integration, or subscription changes, show:
previous → new
effective at
reason code
source
Do not label an older product event with today’s plan. Either store
plan_at_event on the fact or use a tested point-in-time lookup.
Correction and replay events should display their business effective time and, when useful, a small “received later” annotation. Operators need to distinguish late analytics delivery from a late customer action.
Handle time and pagination#
Store and query Unix seconds in UTC. Display:
- the operator’s selected time zone
- an unambiguous offset on hover or detail
- the exact covered date range
- whether recent data is still settling
The Data API SQL result is capped at 2,000 rows. A timeline should use a much smaller page such as 100 or 200 items. For deeper history, use a bounded older date cursor and re-run the fixed query; do not fetch every event and paginate only in the browser.
Use (occurred_at, item_id) as a stable continuation boundary. Never rely on
row offset alone while new events are arriving.
Render evidence, not noise#
Group adjacent attempts into one terminal outcome:
Webhook delivery failed
3 attempts over 7 minutes · upstream timeout
Reference: delivery_91
Useful filters:
- category
- success or failure
- date range
- exact request/job/delivery reference
Avoid default filters for arbitrary property names. A support UI should not become a generic workspace explorer.
Every empty state should explain whether:
- no allowlisted events occurred
- the account is too new
- the selected interval is empty
- data is delayed
- the report failed
- the operator lacks scope
Do not render failure as “no issues found.”
Protect sensitive data#
Apply a field allowlist before the browser response. Review:
- personal identifiers
- IP and location
- billing references
- feature names under confidentiality
- support and message content
- security events
- employee-only diagnostics
Use pseudonymous account and user IDs where possible. Resolve a display name from the authoritative application only after authorization; do not copy an email into every analytical event for convenience.
Define retention based on support and reliability need, not curiosity.
Audit sensitive investigations#
For high-trust workflows, record:
{
"event": "support_timeline_viewed",
"access_event_id": "access_01J...",
"operator_id": "op_42",
"account_id": "acct_7",
"case_id": "case_91",
"reason": "customer_reported_export_failure",
"occurred_at": 1785088800
}
Store access audit data in the appropriate security system or restricted collection. Do not show this audit stream back through the same general support timeline.
Test the complete boundary#
Fixtures should prove:
- an allowed operator can see the correct account
- another account never appears
- a revoked or out-of-scope operator is denied
- only allowlisted event names and fields survive normalization
- duplicates collapse predictably
- equal timestamps have deterministic order
- late and corrected facts remain understandable
- time zone and older-page boundaries do not skip or repeat rows
- upstream error and empty result render differently
- raw payload and error text never reach the browser
Also test the drill-down system independently. A visible request ID is only useful when the runbook and permissions behind it work.
Production checklist#
- Timeline questions and event allowlist are tied to support workflows.
- A stable normalized browser contract excludes raw JSON.
- Server authorization validates operator, case, account, range, and limit.
- API key and arbitrary SQL never reach the browser.
- Terminal outcomes are correlated with bounded reference IDs.
- Event-time state and late delivery are represented accurately.
- Pagination uses a stable time-and-ID boundary.
- Empty, delayed, unauthorized, and failed states are distinct.
- Sensitive fields and low-value activity are excluded.
- Access audit and retention follow security and privacy policy.
Continue with structured application logs, users, accounts, and identity, and native Data API frontend.