Seeing what your app is doing
You have three observability tools and they answer different questions. Picking the wrong one is why debugging turns into six round-trips.
| You want to know | Call |
|---|---|
| What changed, and what broke, and did one cause the other | get_timeline({ applicationId }) |
| Is anything wrong across all my apps right now | get_timeline() — no applicationId |
| The actual lines behind a specific error | get_logs |
| Everything about one app in one payload | debug_app |
get_timeline is aggregates only — it will never hand you raw log lines, no matter what you pass. That's deliberate: the broad question is answered by counts and correlations, and returning lines would blow your context on the call where you don't yet know what you're looking for.
The loop
Start broad, drill once, act.
1. get_timeline({ applicationId })
→ a deploy landed at 14:06:12 and passed its health gate
→ requests p95 jumps 60ms → 2840ms in the 14:05 bucket, 5xx climbs
→ top error signature: "pool: connection acquire timed out after <NUM>ms",
first seen 14:07:03 — 51 seconds after the deploy went live
2. get_logs({ applicationId, signature: "sig_7d1e", since: "2026-07-31T14:05:00Z" })
→ the real lines, with stack context, already redacted
3. list_deployments / rollback (or push a fix)
Step 2 takes the signature id verbatim from step 1. You never write a regex, and the filter can't be silently downgraded the way an unsafe grep pattern can.
What's in a per-app timeline
Four families, each independently requestable via include:
platform — the exact, listed events
Every state change the platform made, merged from five separate records into one time-ordered list: deploys and their per-phase timings, health-gate verdicts, rollbacks (manual and automatic), app sleeps (idle hibernation and pool exhaustion), preview creation and reaping, domain and cert failures, failed scheduled jobs, and every attributable action from the audit trail.
These are listed verbatim rather than bucketed — there are tens of them in a window, not thousands, and you need them exactly to correlate against everything else. Each carries at, kind, code, applicationId, deploymentId, actor and result, so you can branch on kind without knowing every code.
Successful scheduled runs are deliberately excluded. A once-a-minute schedule fires 1,440 times a day; listing those would bury everything else. Failed ticks show up as schedule_run_failed.
logs — counts and signatures, never lines
buckets— per-bucket counts at every level. Always every level, even when you setminLevel: a log storm that isn't technically an error is still a signal.signatures— the top message templates, most frequent first. Each has atemplate(the message with ids, numbers, paths and quoted strings normalized out), acount,firstSeen/lastSeen, onesampleline, and thesignatureid you pass toget_logs.minLevel(defaultwarn+) filters this list only.totalSignatures— how many distinct templates existed, so a capped list never reads as complete.
Read the coverage caveat below before concluding anything from this family.
requests — what the ingress actually saw
Per-bucket request count, the 2xx/3xx/4xx/5xx split, and estimated p50/p95 latency, observed at the edge for every attached domain. No instrumentation in your app, no SDK, nothing to configure.
Latency figures are estimates from a fixed histogram — good enough to tell you p95 went from 60ms to 2.8s, not precise enough for SLA accounting. When several minutes fold into one bucket the worst quantile wins, because a timeline is looking for the spike.
If your deployment has no ingress metrics source, this family reports unavailable, which means unknown — not zero traffic.
resources — opt-in, current sample only
Live CPU and memory against the app's configured limits. Add "resources" to include to get it. historyAvailable is false and will stay that way until a metrics store exists: this is a point-in-time reading, so it can tell you the app is at 98% memory now, but it cannot show you a trend or tell you what it was during the incident ten minutes ago.
Coverage: how to not fool yourself
Every timeline response carries a coverage block, and it is the most important field in it:
{
"complete": false,
"families": { "platform": "complete", "logs": "partial",
"requests": "complete", "resources": "not_requested" },
"degraded": [], "missing": [], "clampedSince": null,
"note": "Log aggregates are computed over a fetched TAIL of each source…"
}
coverage.complete is the only verdict that supports concluding "nothing is wrong." Each family is graded:
complete— every event in the window is represented.partial— real data, but not the whole window.degraded— the source exists but failed this call. Retry; don't conclude.unavailable— no source for this family exists here. Unknown, never zero.not_requested— you didn't ask for it.
Today logs is always partial. The aggregates are computed over a fetched tail of each source (logTail, default 400 lines), not over the whole window you asked for. On a busy app 400 lines might cover thirty seconds of an hour-long window. An absent error in that sample is not evidence there wasn't one — raise logTail, narrow the window, or say out loud that you couldn't rule it out.
requests also drops to partial in one specific case worth understanding: when no app in your whole organization recorded a request in the window. An empty series and a collector that isn't running look identical from the outside, so rather than report 0 requests, complete — which reads as "no traffic, no errors, all fine" — the response says it couldn't tell. Once any app in the org has recorded traffic, the collector is demonstrably working and a specific app's zero becomes a real zero, reported complete.
Windows longer than 31 days are clamped rather than refused, and coverage.clampedSince reports the instant you actually asked for.
Watching the whole fleet
Call get_timeline() with no applicationId:
{
"scope": "org",
"apps": [
{ "applicationId": "app_4f2", "name": "checkout",
"requests": { "total": 18402, "status5xx": 47,
"errorRate": 0.0026, "previousErrorRate": 0.0001,
"errorRateDelta": 0.0025,
"worstP95Ms": 2840, "previousWorstP95Ms": 71 },
"platform": { "total": 3, "errors": 1, "codes": ["deploy", "health_gate_failed"] },
"attention": ["error_rate_rising", "latency_rising", "platform_errors"] }
],
"quietApps": 11
}
Each row compares the window against the immediately preceding window of equal length, which is what makes it useful: a steady 2% error rate is a fact about an app, while 0% → 2% is an incident. Sort by errorRateDelta, or just look at attention — an empty array means nothing stood out.
attention values: error_rate_rising, errors_present (errors with no prior window to compare), latency_rising (p95 at least doubled), platform_errors.
Apps with no traffic and no events are omitted and counted in quietApps — a fleet view listing 100 idle apps buries the one that broke.
This shape is built to be polled. It's one bounded query regardless of how many apps you have: no per-app fan-out, no log reads. Honor minPollAfterMs (returned on every response) and you'll never ask for a series that can't have changed yet.
Worked example: "the app feels slow"
You have an applicationId and a complaint, nothing else.
Call 1 — get_timeline({ applicationId }). Defaults give you the last hour, auto-bucketed, with platform + logs + requests. Read the correlation directly: the deploy at 14:06:12, the p95 jump at 14:05, the connection-pool timeout signature starting 51 seconds after cutover.
Call 2 — get_logs({ applicationId, signature, since }) for forty real lines of the one error that matters.
Call 3 — get_logs({ applicationId, source: "build", deploymentId }) or list_deployments to confirm what that deploy changed.
Three calls. The causal link arrived in the first one.
Retention and privacy
Request metrics are stored as counts and latency quantiles per app-minute. No URLs, no query strings, no client IPs, no headers, no per-request records — the access-log line is parsed for its host, status and duration and then discarded. They're kept for a bounded window (a week by default) and are never the system of record for anything; billing and the audit trail have their own durable stores.
Platform events come from the audit log and the notification record and follow those retention windows, which are longer.
Log signature templates and samples go through the same secret redaction as get_logs before they're returned. As there, redaction is best-effort: it catches injected credentials and common secret shapes, not arbitrary PII your app chose to log.
Related
- Reading logs — the raw-line drill-down, filters, and secret redaction.
- Health checks — the deploy-time health gate whose verdicts show up as
health_gate_passed/health_gate_failed. - Rollback — what to do once the timeline shows you which deploy broke it.