Skip to content

console: label each SQL request with its query key in Sentry and the statement log - #40075

Merged
leedqin merged 2 commits into
MaterializeInc:mainfrom
leedqin:console-sql-request-spans
Nov 4, 2026
Merged

leedqin merged 2 commits into
MaterializeInc:mainfrom
leedqin:console-sql-request-spans

Conversation

@leedqin

@leedqin leedqin commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Sentry's browser tracing records one http.client span per fetch, and every Console SQL request is a POST to /api/sql. The query's label only travels in the query_key URL parameter, so a sampled page load cannot tell which query made it slow. Only fetchClusters, the replicas query and the environment health checks have named spans today.

Description

Two commits. Both name a request by its query key's scopes, for example clusters.list.

1. A Sentry span per request. executeSqlV2 records a db.sql.query span named after the scopes of the request's query key, for example clusters.list, with db.system: materialize. Sentry groups spans with a db op by name in its Queries view.

  • Name. Only the scope of each key part goes into the name, because other parts can hold IDs and object names. A key with no scoped part is named unscoped query.
  • Parent. The span starts only under an existing span (onlyIfParent), such as a page load or a navigation. A query that polls in the background never starts a trace of its own, so sampling and the number of traces are unchanged.
  • Interval. It covers the request and parsing the response, after the environment's address resolves.
  • Status. A request the caller aborted, such as a React Query cancellation, is cancelled. A timeout is deadline_exceeded. Any other failure is internal_error.
  • startInactiveSpan, not startSpan. In the browser, startSpan with an async callback keeps its scope pushed until the promise settles, and the stack then pops whichever scope is on top. Parallel requests would nest under each other.

2. The same label in the SQL text. Each query gets a trailing comment such as /*console_query='clusters.list'*/. The statement log already records how long every Console query took on the server, in every environment. With the label in the text, a slow query can be traced to the Console feature that sent it. It can also be grouped by label in mz_recent_activity_log, for example with (regexp_match(sql, 'console_query=''([^'']+)'''))[1].

  • Placement. The comment goes last because, on a recent main build, the statement log dropped leading comments and cut the text at the statement's ;. Trailing ;s are removed first.
  • Characters. Anything outside [A-Za-z0-9_.-] in the label is replaced with _, so no label can end the comment.
  • Who sees it. Query History hides the Console's own queries unless "show console introspection" is on, so customers see the label only when they ask for those queries. They can also see it in their own mz_recent_activity_log.
  • Only the sql column. redacted_sql, and with it mz_recent_activity_log_redacted, is rebuilt from the parsed statement and has no comments. Anyone who can only read the redacted view won't see the label.
  • sql_hash changes once. Every Console statement's text, and so its sql_hash, changes when this deploys. Anything that compares Console statements by hash across that release will see them as new statements.
  • Not covered. V1 executeSql, the SQL Shell and subscribes are unchanged. useSqlTyped already puts a leading --queryKey comment, which the statement log drops. The V1 mock handler matches on that comment, so moving it is left for a follow-up.

User-visible effect: Console queries in the statement log, and in Query History with console introspection shown, end with a console_query comment naming the query.

Tests

executeSqlV2.test.ts gains:

  • A test that the queries sent to the server carry the label.
  • labelSql cases for trailing semicolons, a trailing line comment, and characters that could end the comment.
  • queryKeyLabel and spanErrorStatus cases.
  • Three tests against a real Sentry client that read the transactions it would send:
    • the span is named after the query key under an active span
    • no trace is started without one
    • an aborted request is cancelled

Each was checked to fail with the behavior removed.

Open questions

  • Span volume. Every SQL request inside a sampled trace now carries two spans: this one and the http.client fetch span. If our Sentry plan bills by span, that adds roughly one span per SQL request in sampled page loads and navigations.
  • Existing wrapper spans. fetchClusters and fetchClusterReplicas wrap executeSqlV2 in startSpan spans that can start root traces for background polls. With this change they duplicate its name. Remove them in a follow-up?
  • Shared names. Requests made inside one queryFn with keys that differ only in unscoped parts share a name. For example, the deployment lineage lookup and the utilization query both read as replicaUtilization.

🤖 Generated with Claude Code

Sentry's browser tracing records one http.client span per fetch, and every
console SQL request is a POST to /api/sql, so a trace cannot tell which
query made a page slow. executeSqlV2 now records a db.sql.query span named
after the scopes of the request's query key, for example clusters.list.
Other key parts are left out because they can hold IDs and object names.

The span starts only under an existing span, such as a page load or a
navigation, so a query that polls in the background never starts a trace
of its own and sampling is unchanged. It covers the request and parsing
the response, after the environment's address resolves. A request the
caller aborted is marked cancelled and a timeout deadline_exceeded, so
React Query cancellations do not read as SQL failures.

The span is started with startInactiveSpan. In the browser, startSpan with
an async callback keeps its scope pushed until the promise settles, so
parallel requests would nest under each other.
The statement log records how long each console query took on the
server, in every environment, but nothing in the text says which console
query it was. executeSqlV2 now appends the request's label as a trailing
comment, for example /*console_query='clusters.list'*/, the same label its
Sentry span carries. A slow console query in the statement log can then
be traced to the console feature that sent it, and grouped by label.

The comment goes last because the statement log was observed to drop
leading comments and to cut the text at the statement's semicolon, so
trailing semicolons are removed first. Only the sql column carries it:
redacted_sql is rebuilt from the parsed statement, which has no comments.
Query History hides the console's own queries unless console
introspection is shown, so customers see the label only when they ask
for those queries.
@leedqin leedqin changed the title console: name a Sentry span after each SQL request's query key console: label each SQL request with its query key in Sentry and the statement log Oct 22, 2026
@leedqin
leedqin marked this pull request as ready for review October 28, 2026 17:39
@leedqin
leedqin requested a review from a team as a code owner October 28, 2026 17:39
@leedqin
leedqin requested review from def- and jdonelson and removed request for jdonelson October 28, 2026 17:39
const httpAddress = await getHttpAddress(request);
// The span starts only under an existing one, such as a page load, so a
// query that polls in the background never starts a trace of its own.
// NOTE: `Sentry.startSpan` with an async callback keeps its scope pushed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think the startSpan nesting can happen here. The browser SDK defaults parentSpanIsAlwaysRootSpan to true, so every span parents to the root span. I tried two parallel requests wrapped in startSpan and they all came out as flat siblings under the page load. Maybe we drop the NOTE? startInactiveSpan is still a fine choice since nothing needs this span to be active.

@leedqin
leedqin merged commit 17be880 into MaterializeInc:main Nov 4, 2026
84 checks passed
@bosconi bosconi mentioned this pull request Nov 6, 2026
val-materialize added a commit that referenced this pull request Nov 13, 2026
Dates in RC snapshots are provisional until the release ships. One
commit is added per snapshot; the final snapshot sets the real dates.
**The `final` snapshot has been applied: the dates below are the real
ones** (Cloud 2026-11-12, Self-Managed 2026-11-13).

The `final` snapshot adds no entries — `v26.50.0-rc.1...v26.50.0` is one
commit, the version bump, and no agent-skills PR merged in the window.
Its substantive work was the dates, a rebase onto `main`, and the three
removals below.

**Rebased onto `main`.** The branch was 107 commits behind and
conflicted: `main`'s `## v26.49.1` section replaced the stale `##
v26.49.0` stub the branch was cut from. Resolved by keeping `main`'s
section verbatim and re-inserting `## v26.50.0` above it; same for the
compatibility YAML (`v26.50`, then `v26.49.1`, then `v26.48.2`).

**Three entries removed as already published on `main`.** The `v26.49.1`
patch train cherry-picked four Console PRs that also merged to `main`
inside this train's `rc.1` window, so both trains classified them
independently and `main` already carries them under `## v26.49.1`:

| PR(s) | Published on `main` in `## v26.49.1` as | Removed from `##
v26.50.0` |
|---|---|---|
| [#40044](#40044),
[#40074](#40074) | the
feature section *"Right size your clusters, using the new Hydration tab
on the console"*, with a screenshot | *"**Diagnose hydration from the
cluster details page**"* |
| [#40511](#40511) |
*"**Freshness and cluster details pages on Self-Managed**"*, covering
both pages | *"**The Freshness page on Self-Managed**"* |
| [#40492](#40492) |
the Bug Fix about the Console's clusters list showing a dash in the Disk
column | *"**Swap usage in the Console's Disk column**"* |

`main`'s wording is the later and fuller treatment in all three cases.
That leaves `## v26.50.0` with **5 Improvements and 7 Bug Fixes**.

Decisions required:

- **MCP protocol default flip**
([#39762](#39762)):
published as an Improvement because `enable_mcp_protocol_2026_07_28` is
`true` at the tag and the flip *is* the change. The PR was held in draft
"until the 2026-07-28 code ships in a release and passes a test on a
deployed environment" — is it confirmed on?
- **Columnar accumulators**
([#40364](#40364)):
omitted as Borderline — the default is `true` but the PR states no
measured user-facing effect. Is there a memory or CPU figure worth
publishing?
- **MySQL snapshot/replication concurrency**
([#40206](#40206),
[#40207](#40207)):
omitted as Borderline — gated on
`storage_persist_sink_description_lookahead`, which is `Duration::ZERO`
at the tag, same as the PostgreSQL counterparts held back in v26.47.0.
If the flag is served on, both halves want announcing together.
- **MCP servers GA**
([#40448](#40448)):
omitted as Borderline — the preview banners are gone, but the GA call
belongs to the release owner. Publish it?
- **Balancer connection metrics**
([#40192](#40192)):
omitted as Borderline — `mz_balancer_connection_status{status="error"}`
changes meaning, so Self-Managed dashboards and alerts on that series
will see a step change. Worth a note?
- **Console query comments**
([#40075](#40075)):
omitted as Borderline — every Console statement's `sql_hash` changes
once when this deploys. Worth calling out?

Every flag above was re-read in source at the `v26.50.0` ship tag; not
one default moved between `rc.1` and ship, so all six holds still read
correctly.

Snapshot drafts (with PR links) in
[mz-skills](https://github.com/MaterializeInc/mz-skills):

- rc.1:
[release_notes.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.50.0/rc.1/release_notes.md),
[omitted.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.50.0/rc.1/omitted.md)
- final:
[release_notes.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.50.0/final/release_notes.md),
[omitted.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.50.0/final/omitted.md)

**Borderline PRs omitted:** rc.1: 7 —
[omitted.md#borderline](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.50.0/rc.1/omitted.md#borderline),
final: 0 —
[omitted.md#borderline](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.50.0/final/omitted.md#borderline)
(no PRs in scope; `rc.1`'s six holds re-examined at the ship tag and
unchanged)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Pranshu Maheshwari <[email protected]>
Co-authored-by: Valerie Chiang <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants