Skip to content

feat(cloudflare): Split alarms into multiple traces and link them#19373

Merged
JPeer264 merged 4 commits intodevelopfrom
jp/split-alarm
Apr 13, 2026
Merged

feat(cloudflare): Split alarms into multiple traces and link them#19373
JPeer264 merged 4 commits intodevelopfrom
jp/split-alarm

Conversation

@JPeer264
Copy link
Copy Markdown
Member

@JPeer264 JPeer264 commented Feb 18, 2026

closes #19105
closes JS-1604

closes #19453
closes JS-1774

This actually splits up alarms into its own traces and binding them with span links. It also adds the setAlarm, getAlarm and deleteAlarm instrumentation, which is needed to make this work.

The logic works as following. When setAlarm is getting called it will store the alarm inside the durable object. Once the alarm is being executed the previous trace link will be retrieved via ctx.storage.get and then set as span link. Using the durable object itself as storage between alarms is even used on Cloudflare's alarm page.

Also it is worth to mention that only 1 alarm at a time can happen, so it is safe to use a fixed key for the previous trace. I implemented the trace links, so they could be reused in the future for other methods as well, so they are not exclusively for alarms.

Example alarm that triggers 3 new alarms to show the span links: https://sentry-sdks.sentry.io/explore/traces/trace/1ef3f388601b425d96d1ed9de0d5b7b4/

@JPeer264 JPeer264 self-assigned this Feb 18, 2026
@JPeer264 JPeer264 changed the title ref(cloudflare): Move internal files and functions around feat(cloudflare): Split alarms into multiple traces and link them Feb 18, 2026
@linear
Copy link
Copy Markdown

linear bot commented Feb 18, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 18, 2026

Codecov Results 📊


Generated by Codecov Action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 18, 2026

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 25.72 kB - -
@sentry/browser - with treeshaking flags 24.21 kB - -
@sentry/browser (incl. Tracing) 42.73 kB - -
@sentry/browser (incl. Tracing, Profiling) 47.35 kB - -
@sentry/browser (incl. Tracing, Replay) 81.54 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 71.11 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 86.25 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 98.45 kB - -
@sentry/browser (incl. Feedback) 42.51 kB - -
@sentry/browser (incl. sendFeedback) 30.39 kB - -
@sentry/browser (incl. FeedbackAsync) 35.38 kB - -
@sentry/browser (incl. Metrics) 27.04 kB - -
@sentry/browser (incl. Logs) 27.18 kB - -
@sentry/browser (incl. Metrics & Logs) 27.86 kB - -
@sentry/react 27.48 kB - -
@sentry/react (incl. Tracing) 45.05 kB - -
@sentry/vue 30.56 kB - -
@sentry/vue (incl. Tracing) 44.59 kB - -
@sentry/svelte 25.74 kB - -
CDN Bundle 28.41 kB - -
CDN Bundle (incl. Tracing) 43.75 kB - -
CDN Bundle (incl. Logs, Metrics) 29.78 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 44.83 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 68.59 kB - -
CDN Bundle (incl. Tracing, Replay) 80.64 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 81.66 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 86.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 87.2 kB - -
CDN Bundle - uncompressed 82.99 kB - -
CDN Bundle (incl. Tracing) - uncompressed 129.77 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 87.14 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 133.19 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 210.12 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 246.65 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 250.05 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 259.56 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 262.95 kB - -
@sentry/nextjs (client) 47.47 kB - -
@sentry/sveltekit (client) 43.2 kB - -
@sentry/node-core 57.85 kB +0.01% +4 B 🔺
@sentry/node 174.93 kB +0.03% +44 B 🔺
@sentry/node - without tracing 97.97 kB +0.03% +20 B 🔺
@sentry/aws-serverless 115.22 kB +0.02% +18 B 🔺

View base workflow run

Base automatically changed from jp/prepare-context-instrument to develop February 18, 2026 10:53
@JPeer264 JPeer264 force-pushed the jp/split-alarm branch 2 times, most recently from b949de1 to ce3a761 Compare February 20, 2026 10:40
@JPeer264 JPeer264 marked this pull request as ready for review February 20, 2026 10:59
const result = Reflect.apply(target, thisArg, args);
const executeSpan = (): unknown => {
return startSpan({ name: spanName, attributes, links }, async span => {
// TODO: Remove this once EAP can store span links. We currently only set this attribute so that we
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

note: this is a 1:1 copy from here:

// TODO: Remove this once EAP can store span links. We currently only set this attribute so that we
// can obtain the previous trace information from the EAP store. Long-term, EAP will handle
// span links and then we should remove this again. Also throwing in a TODO(v11), to remind us
// to check this at v11 time :)
span.setAttribute(
PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE,
`${previousTraceSpanCtx.traceId}-${previousTraceSpanCtx.spanId}-${
spanContextSampled(previousTraceSpanCtx) ? 1 : 0
}`,
);

@JPeer264 JPeer264 marked this pull request as draft February 23, 2026 11:18
@JPeer264 JPeer264 force-pushed the jp/split-alarm branch 2 times, most recently from b866662 to e1f993f Compare February 23, 2026 14:22
@JPeer264 JPeer264 marked this pull request as ready for review February 23, 2026 14:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

This pull request has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you apply the label PR: no-auto-close I will leave it alone ... forever!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Cloudflare

  • Split alarms into multiple traces and link them by JPeer264 in #19373
  • Propagate traceparent to RPC calls - via fetch by JPeer264 in #19991

Core

  • Automatically disable truncation when span streaming is enabled in LangGraph integration by andreiborza in #20231
  • Automatically disable truncation when span streaming is enabled in LangChain integration by andreiborza in #20230
  • Automatically disable truncation when span streaming is enabled in Google GenAI integration by andreiborza in #20229
  • Automatically disable truncation when span streaming is enabled in Anthropic AI integration by andreiborza in #20228
  • Automatically disable truncation when span streaming is enabled in Vercel AI integration by andreiborza in #20232
  • Automatically disable truncation when span streaming is enabled in OpenAI integration by andreiborza in #20227
  • Add enableTruncation option to Vercel AI integration by nicohrubec in #20195
  • Add enableTruncation option to Google GenAI integration by andreiborza in #20184
  • Add enableTruncation option to Anthropic AI integration by andreiborza in #20181
  • Add enableTruncation option to LangGraph integration by andreiborza in #20183
  • Add enableTruncation option to LangChain integration by andreiborza in #20182
  • Add enableTruncation option to OpenAI integration by andreiborza in #20167
  • Export a reusable function to add tracing headers by JPeer264 in #20076

Deps

  • Bump axios from 1.13.5 to 1.15.0 by dependabot in #20180
  • Bump hono from 4.12.7 to 4.12.12 by dependabot in #20118
  • Bump defu from 6.1.4 to 6.1.6 by dependabot in #20104

Bug Fixes 🐛

Deno

  • Handle reader.closed rejection from releaseLock() in streaming by andreiborza in #20187
  • Avoid inferring invalid span op from Deno tracer by Lms24 in #20128

Other

  • (ci) Prevent command injection in ci-metadata workflow by fix-it-felix-sentry in #19899
  • (e2e) Add op check to waitForTransaction in React Router e2e tests by copilot-swe-agent in #20193
  • (node-integration-tests) Fix flaky kafkajs test race condition by copilot-swe-agent in #20189

Internal Changes 🔧

Deps

  • Bump hono from 4.12.7 to 4.12.12 in /dev-packages/e2e-tests/test-applications/cloudflare-hono by dependabot in #20119
  • Bump axios from 1.13.5 to 1.15.0 in /dev-packages/e2e-tests/test-applications/nestjs-basic by dependabot in #20179

Other

  • (bugbot) Add rules to flag test-flake-provoking patterns by Lms24 in #20192
  • (deps-dev) Bump vite from 7.2.0 to 7.3.2 in /dev-packages/e2e-tests/test-applications/tanstackstart-react by dependabot in #20107
  • (react) Remove duplicated test mock by s1gr1d in #20200
  • (size-limit) Bump failing size limit scenario by Lms24 in #20186
  • Fix flaky ANR test by increasing blocking duration by JPeer264 in #20239
  • Add automatic flaky test detector by nicohrubec in #18684

🤖 This preview updates automatically when you update the PR.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Redundant local SpanLink type duplicates @sentry/core export
    • Exported SpanLink from @sentry/core and replaced the local duplicate type definition with an import from core.
  • ✅ Fixed: Unnecessary teardown overhead for non-setAlarm storage methods
    • Moved teardown logic inside a conditional check so only setAlarm applies the .then() wrapper and waitUntil call, eliminating overhead for other storage methods.

Create PR

Or push these changes by commenting:

@cursor push 52d276ce19
Preview (52d276ce19)
diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts
--- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts
+++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts
@@ -57,33 +57,35 @@
             },
           },
           () => {
-            const teardown = async (): Promise<void> => {
-              // When setAlarm is called, store the current span context so that when the alarm
-              // fires later, it can link back to the trace that called setAlarm.
-              // We use the original (uninstrumented) storage (target) to avoid creating a span
-              // for this internal operation. The storage is deferred via waitUntil to not block.
-              if (methodName === 'setAlarm') {
+            const result = (original as (...args: unknown[]) => unknown).apply(target, args);
+
+            // Only setAlarm needs teardown to store span context for trace linking
+            if (methodName === 'setAlarm') {
+              const teardown = async (): Promise<void> => {
+                // Store the current span context so that when the alarm fires later,
+                // it can link back to the trace that called setAlarm.
+                // We use the original (uninstrumented) storage (target) to avoid creating a span
+                // for this internal operation. The storage is deferred via waitUntil to not block.
                 await storeSpanContext(target, 'alarm');
+              };
+
+              if (!isThenable(result)) {
+                waitUntil?.(teardown());
+                return result;
               }
-            };
 
-            const result = (original as (...args: unknown[]) => unknown).apply(target, args);
-
-            if (!isThenable(result)) {
-              waitUntil?.(teardown());
-
-              return result;
+              return result.then(
+                res => {
+                  waitUntil?.(teardown());
+                  return res;
+                },
+                e => {
+                  throw e;
+                },
+              );
             }
 
-            return result.then(
-              res => {
-                waitUntil?.(teardown());
-                return res;
-              },
-              e => {
-                throw e;
-              },
-            );
+            return result;
           },
         );
       };

diff --git a/packages/cloudflare/src/utils/traceLinks.ts b/packages/cloudflare/src/utils/traceLinks.ts
--- a/packages/cloudflare/src/utils/traceLinks.ts
+++ b/packages/cloudflare/src/utils/traceLinks.ts
@@ -1,6 +1,6 @@
 import type { DurableObjectStorage } from '@cloudflare/workers-types';
 import { TraceFlags } from '@opentelemetry/api';
-import { getActiveSpan } from '@sentry/core';
+import { getActiveSpan, type SpanLink } from '@sentry/core';
 
 /** Storage key prefix for the span context that links consecutive method invocations */
 const SENTRY_TRACE_LINK_KEY_PREFIX = '__SENTRY_TRACE_LINK__';
@@ -12,16 +12,6 @@
   sampled: boolean;
 }
 
-/** Span link structure for connecting traces */
-export interface SpanLink {
-  context: {
-    traceId: string;
-    spanId: string;
-    traceFlags: number;
-  };
-  attributes?: Record<string, string>;
-}
-
 /**
  * Gets the storage key for a specific method's trace link.
  */

diff --git a/packages/cloudflare/src/wrapMethodWithSentry.ts b/packages/cloudflare/src/wrapMethodWithSentry.ts
--- a/packages/cloudflare/src/wrapMethodWithSentry.ts
+++ b/packages/cloudflare/src/wrapMethodWithSentry.ts
@@ -90,7 +90,10 @@
             // but the scope still holds a reference to it (e.g., alarm handlers in Durable Objects)
             // For startNewTrace, always create a fresh client
             if (startNewTrace || !scopeClient?.getTransport()) {
-              const client = init({ ...wrapperOptions.options, ctx: context as unknown as ExecutionContext | undefined });
+              const client = init({
+                ...wrapperOptions.options,
+                ctx: context as unknown as ExecutionContext | undefined,
+              });
               scope.setClient(client);
               scopeClient = client;
             }

diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -361,6 +361,7 @@
   XhrBreadcrumbHint,
 } from './types-hoist/breadcrumb';
 export type { ClientReport, Outcome, EventDropReason } from './types-hoist/clientreport';
+export type { SpanLink, SpanLinkJSON } from './types-hoist/link';
 export type {
   Context,
   Contexts,

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 43b8f06. Configure here.

e => {
throw e;
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unnecessary teardown overhead for non-setAlarm storage methods

Low Severity

The teardown closure, .then() wrapper, and waitUntil call are applied to every instrumented storage method (get, put, delete, list, getAlarm, deleteAlarm), but only setAlarm actually has teardown work. For all other methods, this creates a no-op async function, wraps every async result in an extra .then() hop, and passes an empty promise to waitUntil — all unnecessarily. This adds a microtask per storage operation.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 43b8f06. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 9,631 - 9,311 +3%
GET With Sentry 1,808 19% 1,810 -0%
GET With Sentry (error only) 5,953 62% 6,200 -4%
POST Baseline 1,217 - 1,212 +0%
POST With Sentry 604 50% 624 -3%
POST With Sentry (error only) 1,078 89% 1,066 +1%
MYSQL Baseline 3,326 - 3,149 +6%
MYSQL With Sentry 511 15% 484 +6%
MYSQL With Sentry (error only) 2,711 82% 2,637 +3%

View base workflow run

Copy link
Copy Markdown
Member

@nicohrubec nicohrubec left a comment

Choose a reason for hiding this comment

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

nice, lgtm!

Copy link
Copy Markdown
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

Good to see some usage of span links :)

@JPeer264 JPeer264 merged commit 34869c7 into develop Apr 13, 2026
701 of 709 checks passed
@JPeer264 JPeer264 deleted the jp/split-alarm branch April 13, 2026 15:44
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.

Cloudflare Instrument Alarm Api Cloudflare alarm split in different traces

4 participants