Skip to content

fix(cloudflare): Skip binding instrumentation work when spans are not sent - #24655

Merged
JPeer264 merged 4 commits into
developfrom
jp/cloudflare-sql-skip-without-tracing
Oct 1, 2026
Merged

JPeer264 merged 4 commits into
developfrom
jp/cloudflare-sql-skip-without-tracing

Conversation

@JPeer264

@JPeer264 JPeer264 commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

The Durable Object SQL instrumentation sanitized and summarized each query, also when the span could not be sent: tracing not configured, or an unsampled parent. On SQL-heavy Durable Objects, this was a large CPU cost with tracesSampleRate: 0.

In these cases, the query is not sanitized now. startSpan still runs with the name exec and the static attributes, so an unsampled span still records its sample_rate outcome. A query that can target a cf_ table takes the full path, because an internal query must not start a span.

Under span streaming with an unsampled parent, an ignoreSpans rule that matches the span name or db.query.text does not match now. Thus the dropped span is reported as sample_rate, not ignored. The number of dropped spans does not change. Rules that match op: 'db.query' are not affected.

@JPeer264 JPeer264 self-assigned this Sep 23, 2026
@JPeer264
JPeer264 force-pushed the jp/cloudflare-sql-skip-without-tracing branch 6 times, most recently from db6f184 to 28885a9 Compare September 25, 2026 10:57
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 29.51 kB - -
@sentry/browser - with treeshaking flags 27.68 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.58 kB - -
@sentry/browser (incl. Tracing) 51.46 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 51.47 kB - -
@sentry/browser (incl. Tracing, Profiling) 54.47 kB - -
@sentry/browser (incl. Tracing, Replay) 91.05 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.03 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 95.73 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 108.72 kB - -
@sentry/browser (incl. Feedback) 47.04 kB - -
@sentry/browser (incl. sendFeedback) 34.57 kB - -
@sentry/browser (incl. FeedbackAsync) 39.69 kB - -
@sentry/browser (incl. Metrics) 30.53 kB - -
@sentry/browser (incl. Logs) 30.82 kB - -
@sentry/browser (incl. Metrics & Logs) 31.48 kB - -
@sentry/react 31.36 kB - -
@sentry/react (incl. Tracing) 53.82 kB - -
@sentry/vue 37.51 kB - -
@sentry/vue (incl. Tracing) 54.36 kB - -
@sentry/svelte 29.54 kB - -
CDN Bundle 31.22 kB - -
CDN Bundle (incl. Tracing) 51.99 kB - -
CDN Bundle (incl. Logs, Metrics) 33.46 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 53.94 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.2 kB - -
CDN Bundle (incl. Tracing, Replay) 89.56 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 91.54 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 95.73 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 97.73 kB - -
CDN Bundle - uncompressed 92.14 kB - -
CDN Bundle (incl. Tracing) - uncompressed 154.53 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 98.71 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 160.48 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 228.28 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 274.26 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 280.2 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 287.96 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 293.89 kB - -
@sentry/nextjs (client) 56.33 kB - -
@sentry/sveltekit (client) 51.88 kB - -
@sentry/core/server 39.99 kB - -
@sentry/core/browser 13.63 kB - -
@sentry/node 144.32 kB +0.01% +11 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 83.01 kB - -
@sentry/node - without tracing 92.96 kB +0.02% +10 B 🔺
@sentry/node - without channel injection 122.66 kB +0.01% +12 B 🔺
@sentry/aws-serverless 101.25 kB +0.01% +6 B 🔺
@sentry/cloudflare (withSentry) - minified 207.1 kB +0.14% +270 B 🔺
@sentry/cloudflare (withSentry) 515.31 kB +0.14% +711 B 🔺

View base workflow run

@JPeer264
JPeer264 marked this pull request as ready for review September 25, 2026 16:01
@JPeer264
JPeer264 requested a review from a team as a code owner September 25, 2026 16:01
@JPeer264
JPeer264 requested review from andreiborza, isaacs and mydea and removed request for a team September 25, 2026 16:01
@andreiborza

Copy link
Copy Markdown
Member

h: I don't think this is a good approach. It's easy to forget to add this, moreover it goes against our recording of dropped spans. So users would see less spans than client outcomes report as dropped.

If I understand the issue correctly, it mostly started out by looking at sql sanitization. Maybe we can keep it scoped to that and only do it if the span is recording, i.e. via span.isRecording()?

/**
* Returns `true` when an active span exists and is not sampled. Returns `false` when there is no active span.
*/
export function isInUnsampledSpan(): boolean {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

m: I find this name non-ideal, as it implies there is def. a span 😅
I would adjust this to:

  1. check hasSpansEnabled() first, which is generally what we use to guard such things
  2. this will not cover the "unsampled parent" case, but I would really check this specifically only if there is a span. the current behavior would (in theory) also swallow stuff if there is simply no parent yet - realistically this will not matter today I assume because there will always be a parent, but we may as well guard this "properly"

Plus, hasSpansEnabled can generally be tree shaken via the SENTRY_TRACING flag thing (added benefit, yay)

So I'd adjust to:

function activeSpanIsUnsampled() {
  const activeSpan = getActiveSpan();
  return !!activeSpan && !spanIsSampled(activeSpan);
}

// every call site
if (!hasSpansEnabled() || activeSpanIsUnsampled() {
 // opt out
}

JPeer264 and others added 2 commits September 29, 2026 17:08
…be sent

The binding instrumentations (DO SQL, DO KV, sync KV, D1, R2, queue producer,
agent callable RPC) built span data on every call, even when the SDK was
disabled, had no DSN, had no tracing configured, or ran under an unsampled
parent. On SQL-heavy Durable Objects the per-query sanitizing alone showed up
as a large CPU regression with `tracesSampleRate: 0`.

They now call the original method directly in these cases. D1 keeps adding
breadcrumbs while the SDK is enabled, and sanitizes the query only when a
statement runs. `setAlarm` keeps its span path, because it stores the span
context for the alarm trace link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JPeer264
JPeer264 force-pushed the jp/cloudflare-sql-skip-without-tracing branch from d7bf44a to 48b1d3e Compare September 29, 2026 14:08
@JPeer264

Copy link
Copy Markdown
Member Author

@andreiborza now only the sql part is skipped. In fact it still adds a span, but without the calculations

@isaacs

isaacs commented Sep 29, 2026

Copy link
Copy Markdown
Member

@andreiborza

Maybe we can keep it scoped to that and only do it if the span is recording, i.e. via span.isRecording()?

That would avoid copying core's logic. But two problems with that:

  • Under span streaming, ignoreSpans runs once, at span start, on the start name and attributes (_shouldIgnoreStreamedSpan in packages/core/src/tracing/trace.ts). The only other shouldIgnoreSpan calls in core are in client.ts (static transactions) and idleSpan.ts. A span that starts as exec and then later gets updateName('SELECT users') would escape any ignoreSpans rule that matches the real name or db.query.text.
  • The cf_ internal-table filter has to decide before startSpan, because an internal query can't start a span or record an outcome.

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A few opportunities to polish up a bit, but I think this is good to move forward with.

One idea for follow-up: in packages/cloudflare/src/instrumentations/worker/instrumentD1.ts, D1 calls sanitizeSqlQuery on each prepare and each exec, whether or not spans are sampled. The breadcrumb also uses the sanitized text, so the same fast path does not drop in directly. I wouldn't stuff that into this PR, though. Should be its own change.

const [query, ...bindings] = args as [string, ...unknown[]];

const activeSpan = getActiveSpan();
const spanIsNeverSent = !hasSpansEnabled() || (!!activeSpan && !spanIsSampled(activeSpan));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not a bug, but possibly a missed optimization: This predicts what createChildOrRootSpan and _startChildSpan in packages/core/src/tracing/trace.ts will decide, and covers "no tracing" and "unsampled parent", but not suppressTracing(), where _startChildSpan makes a non-recording child even under a sampled parent. So in that edge case, the code still sanitizes for nothing.

That's not a huge deal, but the risk is drift. If core adds a new reason to drop a child, this check will not know about it. In the unsafe direction (a span predicted dropped but actually sent, much less likely), users would get spans named exec with no db.query.text.

There's nothing like that now, which is why this isn't a bug. parentSpanIsAlwaysRootSpan picks the root span, which has the same sampling decision. An ignored child is never set active (makeSpanActive in startSpan), so the active span stays the sampled parent. An ignored root span is set active and is unsampled. Its children take the ignored drop reason, as before.

Suggestion: keep the check, but move it to a small named helper, for example spanWillNotBeRecorded(). Give it a short comment that it must agree with _startChildSpan in core. Also read getActiveSpan() only after hasSpansEnabled() returns true. As written, getActiveSpan() runs on every call, even with tracing off:

function childSpanWillNotBeRecorded(): boolean {
  if (!hasSpansEnabled()) {
    return true;
  }
  const activeSpan = getActiveSpan();
  return !!activeSpan && !spanIsSampled(activeSpan);
}

This also matches the shape @mydea proposed.


const activeSpan = getActiveSpan();
const spanIsNeverSent = !hasSpansEnabled() || (!!activeSpan && !spanIsSampled(activeSpan));
if (spanIsNeverSent && !mayTargetCloudflareInternalTable(query)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

_shouldIgnoreStreamedSpan runs before the parent sampling check. Before this PR, under an unsampled parent, a query that matched a name-based or db.query.text-based ignoreSpans rule recorded an ignored outcome.

Now the fast path starts the span as exec with no db.query.text, so such a rule no longer matches. The outcome becomes sample_rate. The count of dropped spans stays the same, but the reason changes. Rules that match on op: 'db.query' still match, because SPAN_ATTRIBUTES has the op. (Verified this by looking at the code in core, not with a test, so it's possible I'm getting it twisted somehow.)

Suggestion: accept it, but state it in the PR body. Or, if the difference matters, take the full path when getClient()?.getOptions().ignoreSpans?.length is set and span streaming is on. That costs one more options read per query. Personally, I'd just accept the difference and document it.

Comment thread packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts Outdated
JPeer264 and others added 2 commits September 30, 2026 10:03
…cannot be sent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +84 to +91
function childSpanWillNotBeRecorded(): boolean {
if (!hasSpansEnabled()) {
return true;
}

const activeSpan = getActiveSpan();
return !!activeSpan && !spanIsSampled(activeSpan);
}

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.

Bug: The childSpanWillNotBeRecorded function fails to check for suppressTracing, causing a performance optimization for SQL instrumentation to be skipped unnecessarily.
Severity: LOW

Suggested Fix

Update childSpanWillNotBeRecorded to check if tracing is suppressed by calling isTracingSuppressed(). If it returns true, the function should also return true. This will require importing isTracingSuppressed from @sentry/core.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts#L84-L91

Potential issue: The function `childSpanWillNotBeRecorded` in the Cloudflare SQL
instrumentation incorrectly determines if a new span will be recorded. When a SQL query
is executed within a `suppressTracing` block, the core SDK correctly creates a
non-recording span. However, `childSpanWillNotBeRecorded` does not check for this
suppressed state and wrongly concludes that a span will be recorded. This failure
prevents a performance optimization from being applied, leading to the unnecessary
execution of expensive functions like `sanitizeSqlQuery` and `getSqlQuerySummary`.

Did we get this right? 👍 / 👎 to inform future reviews.

@JPeer264
JPeer264 merged commit fe68d49 into develop Oct 1, 2026
681 of 686 checks passed
@JPeer264
JPeer264 deleted the jp/cloudflare-sql-skip-without-tracing branch October 1, 2026 14:17
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.

4 participants