feat(cloudflare): Split alarms into multiple traces and link them#19373
feat(cloudflare): Split alarms into multiple traces and link them#19373
Conversation
Codecov Results 📊Generated by Codecov Action |
size-limit report 📦
|
b949de1 to
ce3a761
Compare
| 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 |
There was a problem hiding this comment.
note: this is a 1:1 copy from here:
sentry-javascript/packages/browser/src/tracing/linkedTraces.ts
Lines 193 to 202 in 4ee0fea
8d8c709 to
05ad36f
Compare
b866662 to
e1f993f
Compare
e1f993f to
4973f60
Compare
|
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 |
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨Cloudflare
Core
Deps
Other
Bug Fixes 🐛
Internal Changes 🔧Core
Deps
Other
🤖 This preview updates automatically when you update the PR. |
packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Redundant local
SpanLinktype duplicates@sentry/coreexport- 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.
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; | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 43b8f06. Configure here.
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.
|



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,getAlarmanddeleteAlarminstrumentation, which is needed to make this work.The logic works as following. When
setAlarmis getting called it will store the alarm inside the durable object. Once thealarmis being executed the previous trace link will be retrieved viactx.storage.getand 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/