Skip to content

feat(studio): correlate logs with traces and expand span detail - #369

Merged
prisis merged 11 commits into
alphafrom
feat/studio-trace-log-correlation
Aug 9, 2026
Merged

feat(studio): correlate logs with traces and expand span detail#369
prisis merged 11 commits into
alphafrom
feat/studio-trace-log-correlation

Conversation

@prisis

@prisis prisis commented Aug 8, 2026

Copy link
Copy Markdown
Member

Why

The Studio could render a request's waterfall in Traces and the same
request's log lines in Logs, and nothing connected them. LogEvent has
always carried a traceId to every sink — the in-memory log buffer dropped it
on the way in, and the durable request log never had a column for it — so the
join was impossible on the client no matter how you squinted at timestamps.

Two smaller versions of the same problem: SpanEvent.kind and .events were
recorded by ctx.trace and then discarded by foldTraces, so a
span.recordException reached the buffer and vanished before anything could
render it; and a span's attributes were truncated into a fixed-width cell, which
is enough to know an attribute exists and not enough to read it — the
{ orderId } you opened the panel for is exactly what got cut.

What changed

Buffers / fold

  • LogEntry gains traceId, stamped at the ctx.log and dispatch-error push
    sites.
  • foldTraces carries kind and events onto TraceSpan.

Alarm-path log sites are correlated too. An earlier revision of this
description claimed the three remaining logs.push sites "genuinely have no
ambient trace". That holds only for the lifecycle-hook failure, reached from
webSocketMessage/webSocketClose — deliberately not wrapped in
withTriggerTrace. The TTL sweep and the shape fan-out's alarm callers do run
under an anchor that withTriggerTrace publishes, so they are now stamped:

  • withTriggerTrace publishes its anchor on a dedicated currentTriggerTrace
    field — needed only because runner.handleAlarm() sits between it and the
    handler, so the anchor cannot simply be an argument.
  • handleAlarmCloudflare captures that field into a local synchronously at
    entry, before its first await
    , then passes it by value to pollTier,
    pollTtlSweepsdeleteExpiredTtlRow, and pollGlobalShapes
    pollSocketGlobalShapes. recordShapeError takes it as an optional
    parameter, so the socket-frame callers stay untraced.

That discipline is the point: recordShapeError has callers on both paths, so
reading a field there would let a socket frame interleaving at an await point
file its own failure under the alarm's trace — the same class of bug as the one
above. Its regression test drives two alarm ticks and asserts each failure
carries a distinct 32-hex id; it fails without the threading.

The external-source path is threaded too, which is a cross-package change: the
generated pollExternalSources override now takes the alarm's trace and
forwards it to all three sites where it records a contained ingest failure. The
anchor crosses the boundary as TraceRefLike, a structural projection exported
from @lunora/do — that package deliberately does not re-export
@lunora/observability, and a generated app should not take on that dependency
just to name a parameter.

Bug found while threading it. The emitted poll loop wrote
this.logs.push(...), and logs is private on ShardDO — so that
statement cannot compile in the generated subclass. Any project declaring a
.source() table has been emitting a shard that fails tsc. It survived
because no fixture or example declares one, and the emitted string is only
ever asserted against as text, never compiled. It now goes through a
protected recordExternalSourceWarning seam, which keeps the ring
encapsulated and gives the line the same correlation as its sibling.

The gap that let it happen is closed too. Nothing in CI compiled emitted
output, and golden fixtures cannot do it — packages/codegen/tsconfig.json
excludes them, because generated code only type-checks inside a whole app. So
the guard is two halves, each useless alone:

  • packages/codegen/__tests__/emitted-shard-contract.ts — a real ShardDO
    subclass exercising every base member the emitted sourced shard touches. Never
    executed; it only has to compile, so lint:types fails in the package that
    owns the emitter the moment one of those members changes visibility or
    signature.
  • A test asserting the emitter still restricts itself to that set, computed as
    the delta between a sourced and a non-sourced shard's this.* usage — so
    there is no hand-maintained allowlist to rot.

Verified in both directions: putting this.logs.push(...) back into the
contract reproduces the original TS2341, and pointing the emitter at an
uncovered private member fails the delta test.

Durable request log

  • A trace_id column added via a guarded ALTER, mirroring error_fingerprint.
    Each ALTER gets its own try: one shared block would let the first
    column's duplicate-column error skip the second add, leaving a shard that has
    error_fingerprint but never gains trace_id.
  • Stamped from the shared per-request anchor, so the row lands in the same trace
    as that dispatch's spans and log lines.
  • Emitted on the Logpush console event — the join key on the far side, letting a
    SIEM correlate a request to the collector's spans.
  • Never redacted: a trace id is an opaque 32-hex identifier, and masking it
    destroys the only thing it is for. Covered by a test.

Traces panel

  • A span row expands into a detail block: span/parent ids, kind, timing, the
    full attribute bag, the error, and any addEvent / recordException events.
  • The dispatch's log lines render under its expanded waterfall, read from the
    same live getLogs ring the Logs panel uses — a client-side join of two reads
    already pushed over this socket, not a new correlated RPC.
  • An elapsed-time ruler over the bars, an Errors only filter, and search that
    matches span names and span ids as well as the trace's own identifiers.

Logs panel

  • Both views carry a Trace link, sharing one extracted TraceLinkCell.

Mock

  • apps/studio's backend-free dev client seeds matching trace ids, a handled
    retry, and a recorded exception, plus one row deliberately left trace-less to
    stand in for a pre-column row.

The retention asymmetry (read this before reviewing the Requests link)

The request log is durable and bounded by row count; the ctx.trace span
ring it points at is in-memory and resets on hibernation. So a Trace link on
a row older than the current DO instance lands on an empty Traces panel — the
trace has aged out locally, and the panel's ordinary empty state is the correct
outcome rather than an error.

Recording the id is still worth it: it is what joins the row to whatever
collector otlpSink ships to (where a deployed app's traces actually live) and
to the Logpush event. This is stated at the module docstring, the RequestLogEntry
type on both sides, the component, and the docs page, so nobody re-derives it
from a surprising empty panel.

Prior art

Modelled on Cloudflare's Local Explorer observability tab
(blog,
docs),
whose trace view is a list → waterfall → per-span attributes → correlated
console logs. Deliberately skipped from that UI: collapsible subtrees, a
resizable name column, per-kind icons, and a clear button — none of them earn
their complexity until someone hits a trace deep enough to need collapsing.

Not in scope

Logs correlate at trace level, not per span: LogEvent.spanId is the
dispatch root span and is constant across a dispatch, so carrying it would imply
a precision it doesn't have. It is deliberately not stored.

Review pass

A two-reviewer audit (bugs/security + maintainability) ran over the branch. It
found one real correctness bug, now fixed with a regression test:

recordRequestLog read the shared currentRequestTrace after the handler's
awaits.
A DO interleaves dispatches at await points, so a concurrent request
re-sets that field — or clears it in its finally — before the first one
records its row. Two concurrent /rpc calls could file request A's durable row,
and the Logpush event carrying it, under request B's trace, silently. The anchor
is now threaded by value, matching the dispatchTrace capture the same function
already used for the root span, whose comment describes this exact hazard three
lines above the code that ignored it. The new test parks a dispatch inside its
handler, runs a sibling to completion, then releases; it fails against the
previous code with undefined.

Also fixed from that pass: the correlated-log read is now gated on an expanded
row (it was opening a second live subscription on every page visit for a
collapsed section); the waterfall row's accessible name is explicit again
(as a <button> it concatenated every cell into a run-on string); the ruler and
span rows share one grid so gridlines can't drift from the bars; correlated log
rows are keyed by position (identical messages in one millisecond collided); the
guarded ALTERs are a loop; LogLine and useOpenTrace are shared rather than
duplicated; the waterfall moved to its own module; and two weak tests were
repaired — one asserted absence against a prior render's DOM, the other's stated
rationale was factually wrong.

Verification

lint:eslint, lint:types, lint:package-json, and repo-wide lint:prettier
clean for @lunora/studio, @lunora/do, @lunora/observability, and
apps/studio. Suites: studio 1027, do 525, codegen 1070, observability 230 —
all passing. api:check green; the snapshot changes are additive (optional
trace parameters, the new recordExternalSourceWarning seam, and the
TraceRefLike projection).

The migration path has its own test: it builds the pre-column schema
verbatim
, inserts a row, then asserts the guarded ALTER adds the column,
the new row round-trips its id, and the pre-existing row survives with none —
the path a fresh CREATE never exercises.

The TraceSpan key drift guards in both packages/do and packages/studio
caught the new keys on the first type-check, which is what they exist for.

Rebased onto the branch's merge of current alpha; lockfile untouched. One
commit here is unrelated housekeeping: a redundant paren group that landed on
alpha via #365 was failing the repo-wide prettier job on every branch that
merged alpha afterwards.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG

Summary by CodeRabbit

  • New Features
    • Expanded trace inspection with elapsed-time rulers, detailed span metadata, events, errors, and correlated logs.
    • Added expandable span details, error-only filtering, and search across traces and spans.
    • Added Trace links from log and request-log entries.
    • Added trace IDs to logs and request logs for durable correlation across asynchronous activity and external collectors.
  • Documentation
    • Updated observability documentation with trace inspection, filtering, linking, and trace-retention details.

prisis and others added 2 commits August 8, 2026 16:26
The Traces panel could show a request's waterfall and the Logs panel could
show the same request's log lines, but nothing connected them: `LogEvent`
carries a `traceId` to every sink, and the in-memory log buffer dropped it
on the way in. A span's attributes were likewise truncated into a
fixed-width cell, so the `{ orderId }` you opened the panel to read was
exactly what got cut, and `kind` / recorded span events never left the fold
at all.

- buffer `traceId` on a log entry and stamp it at the `ctx.log` and
  dispatch-error push sites, so `getLogs` can be joined to `getTraces`
- carry `kind` and `events` through `foldTraces` onto `TraceSpan`
- expand a span row into a detail block: ids, kind, timing, the full
  attribute bag, the error, and `addEvent`/`recordException` events
- show the dispatch's log lines under its expanded waterfall, read from the
  same live log ring rather than a new correlated RPC
- draw an elapsed-time ruler over the bars, add an "Errors only" filter, and
  match the search box against span names and span ids
- link a log row to its trace, reusing the metrics exemplar hand-off

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG
…ents

The backend-free mock is what `main.mock.tsx` renders for design work, and it
answered `getLogs` with entries carrying no `traceId` and `getTraces` with
spans carrying no `kind` or events — so the trace/log correlation, the span
detail's kind row, and the events list were all invisible in mock mode.

The log fixtures already lined up with the trace fixtures by function path,
so this only stamps the matching trace ids and adds a handled retry plus a
recorded exception. One log line is deliberately left untraced, standing in
for a container-lifecycle entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG
@netlify

netlify Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploy Preview for lunorash ready!

Name Link
🔨 Latest commit cf943c2
🔍 Latest deploy log https://app.netlify.com/projects/lunorash/deploys/6a7854adbd7c930008aba4f4
😎 Deploy Preview https://deploy-preview-369--lunorash.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • api-snapshots/lunora.api.md is excluded by none and included by none

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d0287c77-dd0c-4563-920c-53a59b92032e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The PR propagates trace IDs through logs and request logs, preserves span events and kinds, and expands Studio trace inspection, filtering, log correlation, and navigation from Logs views.

Changes

Trace observability

Layer / File(s) Summary
Trace correlation contracts and persistence
packages/observability/*, packages/studio/src/lib/admin.ts
Log entries, request-log records, and trace spans now carry optional trace metadata. Request-log storage persists and reads nullable trace IDs.
Dispatch propagation and trace fixtures
packages/do/src/shard-do.ts, packages/do/src/index.ts, packages/codegen/src/emit.ts, apps/studio/src/mock/dev-client.ts
Dispatch, alarm, TTL, RPC, shape-error, and external-source paths preserve trace references. Fixtures include span events, correlated logs, linked requests, and a legacy request without a trace ID.
Trace filtering and span inspection
packages/studio/src/features/traces/*, packages/studio/src/locales/en.ts, apps/docs/src/content/docs/concepts/observability.mdx
The Traces panel adds span search, error filtering, elapsed-time rulers, expandable details, and correlated log lines. Labels and documentation describe the new behavior.
Log-to-trace navigation
packages/studio/src/features/logs/*, packages/studio/src/hooks/use-open-trace.ts, packages/studio/src/features/reports/metrics-panel.tsx
Log and request-log rows with trace IDs now link to the Traces panel with shard and trace filters. Metrics uses the shared trace-opening hook.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LogsPanel
  participant UseOpenTrace
  participant TracesPanel
  participant LogRing
  LogsPanel->>UseOpenTrace: store shard and traceId
  UseOpenTrace->>TracesPanel: navigate to /traces
  TracesPanel->>LogRing: query correlated logs
  LogRing-->>TracesPanel: return entries indexed by traceId
Loading

Possibly related PRs

  • anolilab/lunora#143: Extends dispatch trace context propagation in shard-do.ts.
  • anolilab/lunora#147: Shares trace and log correlation changes across observability and Studio log components.
  • anolilab/lunora#149: Provides related trace infrastructure used by span metadata and trace inspection.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary Studio changes: trace-log correlation and expanded span details.
Description check ✅ Passed The description thoroughly explains the changes, scope, migration impact, testing, review findings, and documentation updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/studio-trace-log-correlation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Thank you for following the naming conventions! 🙏

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Thank you for confirming the Contributor License Agreement! 🙏

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit cf943c2.

@codecov-commenter

codecov-commenter commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.48649% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.10%. Comparing base (95d33d6) to head (cf943c2).
⚠️ Report is 77 commits behind head on alpha.

Files with missing lines Patch % Lines
packages/do/src/shard-do.ts 81.48% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            alpha     #369      +/-   ##
==========================================
+ Coverage   87.09%   87.10%   +0.01%     
==========================================
  Files        1172     1177       +5     
  Lines       63383    63582     +199     
  Branches    15447    15306     -141     
==========================================
+ Hits        55202    55384     +182     
- Misses       7654     7676      +22     
+ Partials      527      522       -5     
Files with missing lines Coverage Δ
packages/codegen/src/emit.ts 97.20% <100.00%> (ø)
packages/observability/src/log-buffer.ts 100.00% <ø> (ø)
packages/observability/src/request-log.ts 95.20% <100.00%> (+0.14%) ⬆️
packages/observability/src/span-buffer.ts 90.12% <100.00%> (+0.25%) ⬆️
packages/do/src/shard-do.ts 85.02% <81.48%> (+0.05%) ⬆️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented Aug 8, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 3.17%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 251 untouched benchmarks
⏩ 10 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
in-batch: single IN(...) query + id->doc re-projection 799.4 µs 948.9 µs -15.76%
count, no attributes 67.1 µs 60.3 µs +11.31%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/studio-trace-log-correlation (cf943c2) with alpha (1762097)2

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on alpha (2dc73bf) during the generation of this report, so 1762097 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

…est log

The in-memory log buffer now carries a trace id, but the durable request log —
the one that survives hibernation and records EVERY dispatch — did not, so the
Requests view had no way back to a waterfall and a SIEM receiving the Logpush
event could not join it to the collector's spans for the same request.

- add a `trace_id` column via a guarded ALTER, mirroring `error_fingerprint`;
  each ALTER gets its own try so a shard that already has one column still
  gains the other
- stamp it from the shared per-request anchor, so the row lands in the same
  trace as that dispatch's spans and log lines
- emit it on the Logpush console event — the join key on the far side
- never redact it: a trace id is an opaque identifier, and masking it destroys
  the only thing it is for
- link a request row to its trace, extracting the cell both log views now share

Note the deliberate retention asymmetry, documented at the module, the type,
the component, and the docs page: this log is durable while the span ring it
points at is in-memory, so a link on a row older than the current DO instance
resolves in an external collector but not against the local waterfall.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/do/src/shard-do.ts (1)

6412-6455: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fix the trace-id race in recordRequestLog.

recordRequestLog reads this.currentRequestTrace?.traceId at line 6449. Both call sites (line 4467, the "ok" path, and line 4526, the "error" path) invoke it AFTER await this.handleRpc(...) resolves or throws. A Durable Object can interleave a different dispatch's synchronous code during that await. When that happens, this.currentRequestTrace no longer holds THIS dispatch's anchor — it holds whatever dispatch last set the shared field.

The surrounding code already solves this exact problem for the same dispatch: dispatchTrace is captured as a local at line 4322 specifically because "the finally below runs after the handler's awaits, by which point an interleaved dispatch may have re-set the shared field." The error-path this.logs.push({ ..., traceId: dispatchTrace.traceId }) at line 4535 correctly uses that local. recordRequestLog does not — it re-reads the shared field instead of receiving the anchor by value.

Under concurrent dispatch on the same shard instance, a request-log row (and its mirrored Logpush event, per emitRequestLogEvent) can be persisted with another request's traceId. This directly undermines the log↔trace correlation this PR adds, and does so silently.

As per path instructions for packages/**/src/**/*.ts ("Verify error handling" and "adheres to best practices associated with nodejs"), thread the trace anchor through by value instead of reading the shared field.

🐛 Proposed fix: thread the trace anchor by value into `recordRequestLog`
     private recordRequestLog(
         functionPath: string,
         args: Record<string, unknown>,
         durationMs: number,
         outcome: "error" | "ok",
         tablesWritten: string[],
         errorMessage?: string,
+        trace?: { traceId: string },
     ): void {
         const config = this.requestLogConfig();
@@
             tablesWritten,
             // Read from the shared per-request anchor rather than re-resolved, so
             // the row lands in the SAME trace as this dispatch's `ctx.trace` spans
             // and its `ctx.log` lines. `undefined` outside a dispatch-scoped call.
-            traceId: this.currentRequestTrace?.traceId,
+            traceId: trace?.traceId,
             ts: Date.now(),
             userId: this.getCurrentUserId(),
         };

And pass dispatchTrace at both call sites:

-            this.recordRequestLog(payload.functionPath, payload.args ?? {}, durationMs, "ok", tablesWritten);
+            this.recordRequestLog(payload.functionPath, payload.args ?? {}, durationMs, "ok", tablesWritten, dispatchTrace);
-            this.recordRequestLog(payload.functionPath, payload.args ?? {}, durationMs, "error", [...(this.pendingChangedTables ?? [])], message);
+            this.recordRequestLog(payload.functionPath, payload.args ?? {}, durationMs, "error", [...(this.pendingChangedTables ?? [])], message, dispatchTrace);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/do/src/shard-do.ts` around lines 6412 - 6455, Fix the trace-id race
by adding a trace-anchor parameter to recordRequestLog and using that value when
populating entry.traceId instead of reading this.currentRequestTrace. Pass the
dispatchTrace local from both the success and error call sites after handleRpc
completes, preserving the same anchor for persisted request logs and emitted
events.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/studio/src/features/traces/traces-panel.tsx`:
- Around line 119-122: Update the tick label rendering in the ticks.map block so
the terminal 100% label is aligned to the left of its gridline while preserving
the existing 100% ruler position and alignment for other ticks. Apply the
conditional positioning through the span’s styling or classes using
tick.percent.

---

Outside diff comments:
In `@packages/do/src/shard-do.ts`:
- Around line 6412-6455: Fix the trace-id race by adding a trace-anchor
parameter to recordRequestLog and using that value when populating entry.traceId
instead of reading this.currentRequestTrace. Pass the dispatchTrace local from
both the success and error call sites after handleRpc completes, preserving the
same anchor for persisted request logs and emitted events.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fe2c5a11-8387-48e2-89c2-56094b115783

📥 Commits

Reviewing files that changed from the base of the PR and between c2f07b3 and a891bb3.

⛔ Files ignored due to path filters (9)
  • api-snapshots/observability.api.md is excluded by none and included by none
  • api-snapshots/studio.api.md is excluded by none and included by none
  • packages/do/__tests__/shard-do.admin.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/observability/__tests__/request-log.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/observability/__tests__/span-buffer.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/app/studio.test.tsx is excluded by !**/__tests__/** and included by packages/**
  • packages/studio/__tests__/features/logs/logs-panel.test.tsx is excluded by !**/__tests__/** and included by packages/**
  • packages/studio/__tests__/features/traces/trace-geometry.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/features/traces/traces-panel.test.tsx is excluded by !**/__tests__/** and included by packages/**
📒 Files selected for processing (12)
  • apps/docs/src/content/docs/concepts/observability.mdx
  • apps/studio/src/mock/dev-client.ts
  • packages/do/src/shard-do.ts
  • packages/observability/src/log-buffer.ts
  • packages/observability/src/request-log.ts
  • packages/observability/src/span-buffer.ts
  • packages/studio/src/features/logs/logs-panel.tsx
  • packages/studio/src/features/traces/span-detail.tsx
  • packages/studio/src/features/traces/trace-geometry.ts
  • packages/studio/src/features/traces/traces-panel.tsx
  • packages/studio/src/lib/admin.ts
  • packages/studio/src/locales/en.ts

Comment on lines +119 to +122
{ticks.map((tick) => (
<span className="absolute inset-y-0 border-l border-border/70 pl-1" key={tick.percent} style={{ left: `${String(tick.percent)}%` }}>
{tick.label}
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the terminal ruler label inside the timeline track.

Line 120 positions the 100% tick at left: 100%. Its label starts after the track and can overlap the duration and detail columns. Keep the gridline at 100%, but align its label to the left of that edge.

Proposed fix
                     <span className="absolute inset-y-0 border-l border-border/70 pl-1" key={tick.percent} style={{ left: `${String(tick.percent)}%` }}>
-                        {tick.label}
+                        <span className={tick.percent === 100 ? "inline-block -translate-x-full" : undefined}>{tick.label}</span>
                     </span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ticks.map((tick) => (
<span className="absolute inset-y-0 border-l border-border/70 pl-1" key={tick.percent} style={{ left: `${String(tick.percent)}%` }}>
{tick.label}
</span>
{ticks.map((tick) => (
<span className="absolute inset-y-0 border-l border-border/70 pl-1" key={tick.percent} style={{ left: `${String(tick.percent)}%` }}>
<span className={tick.percent === 100 ? "inline-block -translate-x-full" : undefined}>{tick.label}</span>
</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/studio/src/features/traces/traces-panel.tsx` around lines 119 - 122,
Update the tick label rendering in the ticks.map block so the terminal 100%
label is aligned to the left of its gridline while preserving the existing 100%
ruler position and alignment for other ticks. Apply the conditional positioning
through the span’s styling or classes using tick.percent.

prisis and others added 2 commits August 8, 2026 23:26
Formatting only, no behaviour change. The bigint-queryability test landed on
alpha with a paren group prettier removes, which fails the repo-wide
`Lint (prettier)` job on every branch that merges alpha afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG
`recordRequestLog` runs after the handler's awaits, and it read the shared
`currentRequestTrace` field. A Durable Object interleaves dispatches at await
points, so a concurrent request re-sets that field — or clears it in its
`finally` — before the first one records its row. Two concurrent `/rpc` calls
could file request A's durable row, and the Logpush event carrying it, under
request B's trace. Silently: the Requests view's Trace link would open the
wrong waterfall and nothing would report a mismatch.

The anchor is now threaded by value, matching the `dispatchTrace` capture the
same function already uses for the root span — whose comment describes this
exact hazard three lines above the code that ignored it.

Regression test drives the real race: park a dispatch inside its handler, run
a sibling with a different trace to completion, then release. It fails against
the previous code with `undefined`, the symptom of the sibling's `finally`.

Review fixes, same pass:

- gate the Traces panel's correlated-log read on an expanded row, so visiting
  the page no longer opens a second live subscription streaming the whole log
  ring for a section that starts collapsed
- restore the waterfall row's accessible name: as a button it concatenated
  every cell, including the bar's own label, into a run-on string
- put the ruler and the span rows on one shared grid, so gridlines cannot
  silently drift from the bars they annotate
- key correlated log rows by position; identical messages in the same
  millisecond are ordinary and collided on a content-derived key
- shift the terminal ruler label back over its gridline instead of past the track
- loop the guarded ALTERs, making per-column isolation structural rather than
  a comment defending two copy-pasted blocks
- share one LogLine between both log views (they had already drifted on the
  level column's width and on timestamp formatting) and one useOpenTrace hook
  between the Logs and Metrics drill-downs
- move the waterfall into its own module; let a span row own its expansion
  state instead of prop-drilling a Set from the panel
- replace the hand-rolled log bucketing with Map.groupBy, dropping a useMemo
  React Compiler flagged as redundant
- split the ruler test, which asserted absence against a prior render's DOM,
  and make the redaction test prove redaction actually ran on the row
- collapse the retention-asymmetry note from five copies to one

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/studio/src/hooks/use-open-trace.ts (1)

21-24: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider clearing the hand-off if navigation fails.

fireAndForget discards a rejected navigate promise. If navigation fails, the pending trace filter stays in session storage. The next manual visit to Traces then applies a filter the user did not request — the exact failure the doc comment at Lines 12-15 describes. Clear the stored filter on the rejection path.

♻️ Proposed change
 import { fireAndForget } from "../lib/internal";
-import { writePendingTraceFilter } from "../lib/trace-handoff";
+import { clearPendingTraceFilter, writePendingTraceFilter } from "../lib/trace-handoff";
@@
     return (traceId: string): void => {
         writePendingTraceFilter({ shardKey, traceId });
-        fireAndForget(navigate({ to: "/traces" }));
+        fireAndForget(
+            navigate({ to: "/traces" }).catch((error: unknown) => {
+                // Navigation failed, so no panel will consume the hand-off.
+                clearPendingTraceFilter();
+                throw error;
+            }),
+        );
     };

As per path instructions: "Verify error handling".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/studio/src/hooks/use-open-trace.ts` around lines 21 - 24, Update the
callback returned by the trace-opening hook to handle rejected navigation from
navigate instead of discarding it through fireAndForget. On rejection, clear the
pending trace filter written by writePendingTraceFilter, while preserving the
existing successful navigation behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/studio/src/hooks/use-open-trace.ts`:
- Around line 21-24: Update the callback returned by the trace-opening hook to
handle rejected navigation from navigate instead of discarding it through
fireAndForget. On rejection, clear the pending trace filter written by
writePendingTraceFilter, while preserving the existing successful navigation
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bc9c12a0-2283-4110-9d54-e4cd4fcbc9b3

📥 Commits

Reviewing files that changed from the base of the PR and between a891bb3 and 63646b9.

⛔ Files ignored due to path filters (5)
  • packages/do/__tests__/shard-do.sampling.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/observability/__tests__/request-log.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/shard-engine/__tests__/ctx-db.bigint-bytes.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/features/logs/logs-panel.test.tsx is excluded by !**/__tests__/** and included by packages/**
  • packages/studio/__tests__/features/traces/traces-panel.test.tsx is excluded by !**/__tests__/** and included by packages/**
📒 Files selected for processing (11)
  • packages/do/src/shard-do.ts
  • packages/observability/src/log-buffer.ts
  • packages/observability/src/request-log.ts
  • packages/studio/src/features/logs/log-line.tsx
  • packages/studio/src/features/logs/logs-panel.tsx
  • packages/studio/src/features/reports/metrics-panel.tsx
  • packages/studio/src/features/traces/trace-waterfall.tsx
  • packages/studio/src/features/traces/traces-panel.tsx
  • packages/studio/src/hooks/use-open-trace.ts
  • packages/studio/src/lib/admin.ts
  • packages/studio/src/locales/en.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/studio/src/locales/en.ts
  • packages/observability/src/log-buffer.ts
  • packages/studio/src/lib/admin.ts
  • packages/studio/src/features/logs/logs-panel.tsx
  • packages/do/src/shard-do.ts
  • packages/observability/src/request-log.ts

The TTL sweep and the shape fan-out's alarm callers were recorded with no
trace, on the claim that no trace was ambient. That was wrong:
`withTriggerTrace` publishes an anchor for the whole alarm, so those lines
were attributable all along — and the alarm is, per its own docstring, where
a silent failure hides longest because no client is waiting on a response.

Threading it by value rather than reading a field, because `recordShapeError`
has callers on both the alarm and socket-frame paths: a field read there would
let a socket frame interleaving at an await point file its own failure under
the alarm's trace, which is the bug this branch already fixed once.

- `withTriggerTrace` publishes its anchor on a dedicated `currentTriggerTrace`,
  needed only because `runner.handleAlarm()` sits between it and the handler
- `handleAlarmCloudflare` captures that into a local synchronously at entry,
  before its first await, and passes it down through `pollTier`,
  `pollTtlSweeps` → `deleteExpiredTtlRow`, and `pollGlobalShapes` →
  `pollSocketGlobalShapes`
- `recordShapeError` takes the anchor as an optional parameter, so the
  socket-frame callers stay untraced, which is correct for them

The test drives two alarm ticks and asserts each contained failure carries its
own 32-hex id; it fails without the threading. `recordExternalSourceError` is
left alone — it is called from codegen-emitted code, so threading it is a
cross-package change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/do/src/shard-do.ts (2)

4638-4665: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Carry the alarm anchor into deferred subscription refreshes.

handleAlarmCloudflare calls flushChangedTables() after the traced poll work. flushChangedTables can submit drainSubscriptionRefreshes() to runner.background(...) and return before that work completes.

The withTriggerTrace cleanup then restores currentTriggerTrace and clears currentRequestTrace. A later recordSubscriptionRefreshError call uses recordUserLog without an explicit anchor. The error is then trace-less or can inherit a concurrent RPC trace.

Thread the captured TraceAnchor through the deferred refresh path and pass it to recordSubscriptionRefreshError and recordUserLog. Do not read ambient request fields from the background task.

This follows the PR objective that alarm-path log errors retain the trigger trace.

Also applies to: 4763-4780

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/do/src/shard-do.ts` around lines 4638 - 4665, Thread the captured
TraceAnchor from handleAlarmCloudflare through flushChangedTables and any
deferred drainSubscriptionRefreshes background task. Pass it explicitly to
recordSubscriptionRefreshError and recordUserLog, ensuring the background
callback never reads ambient currentTriggerTrace or request fields and
alarm-path errors retain the originating trigger trace.

8567-8588: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Propagate trace to contained alarm error records.

pollGlobalShapes(trace) reaches the outer captures, but the cap path still logs without trace: refreshGlobalShape(...) calls withinGlobalShapeBound(...) without an anchor, so recordShapeError(...) drops traceId. Add a trace?: TraceAnchor parameter here and pass it from pollSocketGlobalShapes.

recordExternalSourceError(table, error) always calls this.recordShapeError(...) without an anchor. The codegen-generated pollExternalSources() uses this in the alarm tier, so per-table source failures can lose their alarm trace identity. Add trace?: TraceAnchor here and update the generated callers to pass the alarm trace down.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/do/src/shard-do.ts` around lines 8567 - 8588, Propagate the optional
TraceAnchor through the global-shape alarm error paths: update
pollSocketGlobalShapes and refreshGlobalShape to accept and forward trace into
withinGlobalShapeBound, ensuring recordShapeError receives it. Also update
recordExternalSourceError to accept trace and pass it to recordShapeError, then
update generated pollExternalSources callers to forward the alarm trace for each
table failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/do/src/shard-do.ts`:
- Around line 4638-4665: Thread the captured TraceAnchor from
handleAlarmCloudflare through flushChangedTables and any deferred
drainSubscriptionRefreshes background task. Pass it explicitly to
recordSubscriptionRefreshError and recordUserLog, ensuring the background
callback never reads ambient currentTriggerTrace or request fields and
alarm-path errors retain the originating trigger trace.
- Around line 8567-8588: Propagate the optional TraceAnchor through the
global-shape alarm error paths: update pollSocketGlobalShapes and
refreshGlobalShape to accept and forward trace into withinGlobalShapeBound,
ensuring recordShapeError receives it. Also update recordExternalSourceError to
accept trace and pass it to recordShapeError, then update generated
pollExternalSources callers to forward the alarm trace for each table failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3147f59f-5ea0-43b9-a821-33e647afa2e8

📥 Commits

Reviewing files that changed from the base of the PR and between 63646b9 and cbfa4d7.

⛔ Files ignored due to path filters (2)
  • api-snapshots/do.api.md is excluded by none and included by none
  • packages/do/__tests__/shard-do.admin.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
📒 Files selected for processing (1)
  • packages/do/src/shard-do.ts

…ching into a private field

Closes the last uncorrelated alarm-path log site. The generated
`pollExternalSources` override now takes the alarm's trace and forwards it to
all three places it records a contained failure, so a `.source()` table's
ingest error is attributable to the tick that produced it.

While threading it, found that the emitted poll loop writes
`this.logs.push(...)` — and `logs` is PRIVATE on `ShardDO`, so that statement
cannot compile in the generated subclass. Any project declaring a `.source()`
table has been emitting a shard that fails `tsc`. It went unnoticed because no
fixture or example declares one and the emitted string is only ever asserted
against as text, never compiled. Routed through a new protected
`recordExternalSourceWarning` seam instead, which keeps the ring encapsulated
and gives the line the same correlation as its sibling.

The trace crosses the package boundary as `TraceRefLike`, a structural
projection exported from `@lunora/do` — `@lunora/do` deliberately does not
re-export `@lunora/observability`, and a generated app should not take on that
dependency to name a parameter.

Verified the emitted shape compiles from OUTSIDE the base class (a throwaway
subclass mirroring exactly what the emitter writes) rather than trusting the
substring assertions that missed the private access. Two new emit tests pin it:
one asserts all three sites forward the trace, one asserts the output never
contains `this.logs`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG
…ds on

Closes the gap that let `this.logs.push(...)` — a private access — ship inside
the generated poll loop. The emitter produces TypeScript as a string and every
test asserts on substrings of it; nothing compiles it, and golden fixtures
cannot, since `tsconfig.json` excludes them (generated output only type-checks
inside a whole app).

Two halves, each useless alone:

- `emitted-shard-contract.ts` is a real subclass of `ShardDO` exercising every
  base member the emitted sourced shard touches. It is never executed — it just
  has to compile, so `lint:types` fails in the package that owns the emitter the
  moment one of those members changes visibility or signature.
- A test asserting the emitter still restricts itself to that set, computed as
  the delta between a sourced and a non-sourced shard's `this.*` usage, so it
  needs no hand-maintained allowlist of unrelated members.

Verified both directions: adding `this.logs.push(...)` back to the contract
reproduces the original `TS2341`, and making the emitter reach for an uncovered
private member (`pendingChangedTables`) fails the delta test. Both reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG
The gate that stops the Traces page opening a second live subscription on
every visit had no test, so removing it would have broken nothing. Asserts
`getLogs` is untouched while the list is collapsed and read once a trace
opens; fails against the ungated version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBeSX2o4sTCPjVDRDWkVQG
@prisis
prisis merged commit 52fe8f8 into alpha Aug 9, 2026
14 of 15 checks passed
@prisis
prisis deleted the feat/studio-trace-log-correlation branch August 9, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants