Migrate from 10.x to 11.x for Cloudflare
Learn about migrating from Sentry JavaScript SDK 10.x to 11.x.
Use curl to download and read https://docs.sentry.io/platforms/javascript/guides/cloudflare/migration/v10-to-v11.md before making changes. Upgrade this project from Sentry JavaScript SDK v10 to v11. Inspect the project's Sentry packages, configuration, runtime, and framework versions, then apply every migration step that applies. Preserve the intended data collection behavior when migrating sendDefaultPii to dataCollection. Use the project's package manager to update dependencies and its existing checks to verify the migration. Summarize the changes and any remaining manual steps.Version 11 of the Sentry JavaScript SDK focuses on better OpenTelemetry interoperability, more flexible instrumentation, and better out-of-the-box defaults. The biggest changes are:
- OpenTelemetry interoperability: The SDK no longer takes over your OpenTelemetry setup.
- Instrumentation: The instrumentations of the SDK now runs through diagnostics channels, which unlocks tracing on platforms like Vercel, Netlify and Cloudflare, but also runtimes like Bun and Deno.
- Span streaming: Stream mode is the new default, so spans no longer hit the size and volume limits of transactions.
- Data collection:
sendDefaultPiiis replaced bydataCollection, which controls each category of data separately and collects more by default (Read more aboutdataCollection) - Version support: Node.js 20.19.0 is the new minimum. We also raised the minimum TypeScript version and the minimum versions of several frameworks.
We recommend that you upgrade to the most recent 10.x release first, because most of what v11 removes is already deprecated there.
Version 11 of the SDK requires Sentry self-hosted 26.4.2 or higher. Lower versions may continue to work, but are not supported. We recommend that you update self-hosted Sentry to the latest version.
Prefer a checklist?
The Interactive v11 Migration Guide shows the steps for your selected platform and lets you check off completed steps.
Version 11 has new compatibility ranges for runtimes, frameworks, and libraries.
The minimum required TypeScript version is 5.0.4. The SDK no longer ships down-leveled types. Older TypeScript versions may continue to work, but no guarantees apply.
Fastify 3.0 through 3.20 are no longer supported. The minimum version is 3.21.
sendDefaultPii is replaced by dataCollection, which controls each category of collected data separately. Our blog post The Data Collection Control Panel covers the thinking behind it.
This is a behavior change, not a rename. In v10, an unset sendDefaultPii was restrictive. In v11, an unset dataCollection collects everything by default.
| Category | v10 default (sendDefaultPii off) | v11 default |
|---|---|---|
userInfo | false | true |
cookies | not collected | true |
httpHeaders | request + response, PII scrubbed | request + response |
httpBodies | not collected (size only) | all request/response |
urlQueryParams | true | true |
genAI | inputs + outputs not collected | inputs + outputs |
databaseQueryData | false | true |
stackFrameVariables | true | true |
frameContextLines | 7 | 5 |
If you set sendDefaultPii: true, remove it. The v11 default matches that behavior.
If you relied on the v10 default, set the baseline explicitly.
Sentry scrubs values whose key looks sensitive (e.g. auth, token, secret, password). The match runs on the key name, so treat it as best effort: a credential in a field that isn't named like one still reaches Sentry. Review your data scrubbing config for the categories v11 collects now, especially request and response bodies.
See the dataCollection option for the full list of categories.
Tracing moves to span streaming in v11. Instead of holding a trace in memory and sending one transaction when the root span ends, the SDK sends spans in small batches as they finish. Transactions no longer exist: what used to be one transaction event is a service span with its child spans.
That changes how spans are sent, named, and filtered. Most of it isn't caught by TypeScript, so check your filters, dashboards, and alerts.
Spans are no longer capped at 1000 per transaction, and individual payload limits are higher. See stream mode for how it works.
Because no transaction events are produced, beforeSendTransaction and ignoreTransactions no longer do anything, and beforeSendSpan receives a different payload.
Streamed spans only carry attributes, so scope tags and extra no longer reach any span, including the service span that replaced the transaction. They still apply to errors, so you don't have to remove them. In transaction mode (traceLifecycle: 'static') they keep landing on the transaction as before.
Set attributes for everything that should be searchable on spans, which also applies them to logs and metrics:
// Before: applied to the transaction
Sentry.setTag("order_id", order.id);
Sentry.setTags({ user_tier: user.tier });
// After: applied to spans, logs, and metrics
Sentry.setAttribute("order_id", order.id);
Sentry.setAttributes({ user_tier: user.tier });
// Before: applied to the transaction
Sentry.setTag("order_id", order.id);
Sentry.setTags({ user_tier: user.tier });
// After: applied to spans, logs, and metrics
Sentry.setAttribute("order_id", order.id);
Sentry.setAttributes({ user_tier: user.tier });
Attributes accept strings, numbers, booleans, and arrays of those, so numbers and booleans no longer have to be stringified. Like tags, they can be set on a specific scope:
// Applied to all spans, logs, and metrics of the application
Sentry.getGlobalScope().setAttributes({ "app.version": "2.1.0" });
// Applied to a single operation
Sentry.withScope((scope) => {
scope.setAttribute("checkout.step", "payment");
});
// Applied to all spans, logs, and metrics of the application
Sentry.getGlobalScope().setAttributes({ "app.version": "2.1.0" });
// Applied to a single operation
Sentry.withScope((scope) => {
scope.setAttribute("checkout.step", "payment");
});
The callback now runs as each span finishes and receives a StreamedSpanJSON object. Its fields were renamed:
Before (SpanJSON) | After (StreamedSpanJSON) |
|---|---|
description | name |
data | attributes |
op | attributes['sentry.op'] |
timestamp | end_timestamp |
status (string) | status ('ok' or 'error') |
// Before
Sentry.init({
beforeSendSpan: (span) => {
if (span.op === "db.query") {
span.description = scrub(span.description);
span.data["db.statement"] = scrub(span.data["db.statement"]);
}
return span;
},
});
// After
Sentry.init({
beforeSendSpan: (span) => {
if (span.attributes["sentry.op"] === "db.query") {
span.name = scrub(span.name);
span.attributes["db.query.text"] = scrub(
span.attributes["db.query.text"]
);
}
return span;
},
});
// Before
Sentry.init({
beforeSendSpan: (span) => {
if (span.op === "db.query") {
span.description = scrub(span.description);
span.data["db.statement"] = scrub(span.data["db.statement"]);
}
return span;
},
});
// After
Sentry.init({
beforeSendSpan: (span) => {
if (span.attributes["sentry.op"] === "db.query") {
span.name = scrub(span.name);
span.attributes["db.query.text"] = scrub(
span.attributes["db.query.text"]
);
}
return span;
},
});
The status field of that payload is always set, and holds only 'ok' or 'error'. The finer-grained statuses of v10 (internal_error, not_found, and the rest) all map to 'error', with the original value kept on the span's sentry.status.message attribute.
If you wrapped the callback with withStreamedSpan() to opt into this payload in v10, drop the wrapper. It's a no-op now that streamed payloads are the default, and it's deprecated.
A callback that doesn't match your traceLifecycle is never invoked: an unwrapped one is ignored in transaction mode, a wrapped one in stream mode. In transaction mode, wrap it with Sentry.withStaticSpan() and it keeps receiving the old SpanJSON. Turn on debug logging to see a warning about the mismatch.
Move data changes to beforeSendSpan and guard on is_segment to target the service span, which is what used to be the transaction. Use ignoreSpans to drop spans: beforeSendSpan can only change spans, not drop them.
// Before
Sentry.init({
ignoreTransactions: ["GET /health"],
beforeSendTransaction: (event) => {
event.transaction = scrubIds(event.transaction);
return event;
},
});
// After
Sentry.init({
ignoreSpans: ["GET /health"],
beforeSendSpan: (span) => {
if (span.is_segment) {
span.name = scrubIds(span.name);
}
return span;
},
});
// Before
Sentry.init({
ignoreTransactions: ["GET /health"],
beforeSendTransaction: (event) => {
event.transaction = scrubIds(event.transaction);
return event;
},
});
// After
Sentry.init({
ignoreSpans: ["GET /health"],
beforeSendSpan: (span) => {
if (span.is_segment) {
span.name = scrubIds(span.name);
}
return span;
},
});
Ignoring a service span drops its child spans with it, which is the same result as dropping a transaction. Because ignoreSpans applies to every span, narrow the filter with the object form so that child spans with the same name are kept:
Sentry.init({
ignoreSpans: [
{ name: "GET /health", attributes: { "sentry.op": "http.server" } },
],
});
Sentry.init({
ignoreSpans: [
{ name: "GET /health", attributes: { "sentry.op": "http.server" } },
],
});
Scope tags and extra aren't carried over to streamed spans either.
Sentry.spanToJSON() now returns a StreamedSpanJSON object, the same shape beforeSendSpan receives. The spanToStreamedSpanJSON helper was removed, so replace calls to it with spanToJSON.
In stream mode, span names follow the Sentry span name conventions. URLs, route parameters, and other client-supplied values are no longer part of the name:
| Span op | Before | After |
|---|---|---|
http.client | GET https://api.example.com/users/123 | GET api.example.com |
http.server | GET /users/123 | GET /users/:id, or GET |
pageload, navigation | /users/123 | /users/:id, or Pageload |
resource.* | /assets/app.js | cdn.example.com, or Resource |
graphql | query GetUser | GraphQL query |
queue.* | my-queue process, poll my-topic | process my-queue, receive my-topic |
db.query | SELECT * FROM "User" WHERE "id" = $1 | SELECT "User", or Database operation |
http.server names are unchanged when the SDK resolves a route, and fall back to the method alone when it can't. pageload and navigation fall back to Pageload and Navigation.
Database spans work the same way: a statement that touches no table (SELECT NOW()) summarizes to the bare operation (SELECT), and without a statement to summarize the name falls back to the database namespace, then to Database operation.
Whatever the name no longer carries stays on an attribute, in both trace lifecycles: url.full and url.domain, graphql.operation.name and the new graphql.processing.type, db.query.text, and so on.
ignoreSpans and tracesSampler both run when a span starts, so a span may not have its final name yet. Match on attributes instead:
Sentry.init({
// Before
ignoreSpans: ["/health", "SELECT * FROM health_check"],
tracesSampler: ({ name, inheritOrSampleWith }) =>
inheritOrSampleWith(name === "GET /health" ? 0 : 1),
// After
ignoreSpans: [
{ attributes: { "sentry.op": "pageload", "url.path": "/health" } },
{ attributes: { "db.query.text": "SELECT * FROM health_check" } },
],
tracesSampler: ({ attributes, inheritOrSampleWith }) =>
inheritOrSampleWith(attributes["url.path"] === "/health" ? 0 : 1),
});
Sentry.init({
// Before
ignoreSpans: ["/health", "SELECT * FROM health_check"],
tracesSampler: ({ name, inheritOrSampleWith }) =>
inheritOrSampleWith(name === "GET /health" ? 0 : 1),
// After
ignoreSpans: [
{ attributes: { "sentry.op": "pageload", "url.path": "/health" } },
{ attributes: { "db.query.text": "SELECT * FROM health_check" } },
],
tracesSampler: ({ attributes, inheritOrSampleWith }) =>
inheritOrSampleWith(attributes["url.path"] === "/health" ? 0 : 1),
});
Span ops are now a smaller, framework-neutral set. The detail that used to live in the op, such as the framework, library, method name, or lifecycle phase, is preserved in attributes like code.function.name, sentry.origin, db.system.name, db.operation.name, and faas.trigger.
Update anything that filters, groups, or alerts on span ops: dashboards, dynamic sampling rules, ignoreSpans, and beforeSendSpan. TypeScript doesn't catch these.
Legacy span attributes were replaced by their semantic-convention equivalents. Update any custom instrumentation, beforeSendSpan logic, dashboards, or alerts that reference them.
Sentry and OpenTelemetry are separate pipelines now, and you choose how they connect.
By default, the SDK no longer registers an OpenTelemetry tracer provider. It owns the full span lifecycle, produces native Sentry spans, and ignores spans created through @opentelemetry/api. You can now run your own OpenTelemetry setup next to Sentry without Sentry spans leaking into your pipeline, and without using Sentry components for export, context management, and trace propagation.
If you only use the Sentry SDK, tracing works as before and there's nothing to do.
skipOpenTelemetrySetup is replaced by enableOpenTelemetrySetup. Turn it on and the SDK registers a custom OpenTelemetry tracer provider, context manager, and propagator, which turns spans from @opentelemetry/api into Sentry spans. It's off by default, except on @sentry/nextjs and @sentry/sveltekit, which need it to capture the spans those frameworks emit.
How the SDK hooks into your app changed, along with the way some frameworks are set up.
AI integrations now trace model invocations, tool calls, and agent invocations only. They no longer emit spans for Anthropic messages.countTokens, models.retrieve, or models.get, or LangGraph graph compilation with gen_ai.create_agent.
Update dashboards and alerts that reference these spans. LangGraph's gen_ai.invoke_agent and gen_ai.execute_tool spans are unchanged.
Instrumentation now runs through diagnostics channels (orchestrion) instead of import-in-the-middle. It allows instrumentation at run time and build time, which enables tracing on deployment targets like Vercel and Netlify, and on non-Node.js runtimes like Cloudflare, Bun, and Deno. Most setups need no changes.
The vercelAIIntegration is the exception. It no longer works on Vercel Edge, which doesn't support diagnostics channels, and it no longer enhances the native OpenTelemetry spans the ai package emits. Those spans are captured as they are, so some agent monitoring capabilities are lost.
The enableLogs and enableMetrics options were removed, including their _experiments variants. Logs and metrics are captured whenever you use their APIs (Sentry.logger.*, Sentry.metrics.*) or add a logging integration such as consoleLoggingIntegration(). The _experiments.beforeSendMetric callback moved to the top-level beforeSendMetric option.
// Before
Sentry.init({
enableLogs: true,
_experiments: {
enableMetrics: true,
beforeSendMetric: (metric) => metric,
},
});
// After
Sentry.init({
beforeSendMetric: (metric) => metric,
});
// Before
Sentry.init({
enableLogs: true,
_experiments: {
enableMetrics: true,
beforeSendMetric: (metric) => metric,
},
});
// After
Sentry.init({
beforeSendMetric: (metric) => metric,
});
New defaults change which events carry a stack trace and how sessions are counted.
Sentry.captureMessage() events, and non-Error values passed to Sentry.captureException(), now attach a synthetic stack trace that points to the call site. Set attachStacktrace: false to restore the previous behavior.
Two consequences are worth checking after you upgrade:
- Issue grouping: Sentry groups events with and without stack traces differently, so you may see new issue groups.
- Release health: Events with a stack trace count as errors, so a
captureMessage()call marks the current session as errored. Crash-free session rate is not affected. For purely informational output, consider Sentry Logs instead.
String and regular expression matching for tracePropagationTargets no longer depends on casing. In browsers this was especially surprising, because the URL is normalized with new URL() before matching, which lower-cases the origin. A target such as 'myApi.com' could therefore never match a request to https://myApi.com.
Sentry.init({
// In a browser, neither of these matched `https://myApi.com` in v10. In v11 both do.
tracePropagationTargets: ["myApi.com", /^https:\/\/myApi\.com/],
});
Sentry.init({
// In a browser, neither of these matched `https://myApi.com` in v10. In v11 both do.
tracePropagationTargets: ["myApi.com", /^https:\/\/myApi\.com/],
});
If you relied on case-sensitive matching to tell two targets apart, narrow the target with a more specific path. The g and y flags are ignored now, because they made matching stateful and a target like /myApi\.com/g matched only every other request.
Workers need a new compatibility flag, and a few APIs moved or became automatic.
The SDK requires the nodejs_compat compatibility flag instead of nodejs_als. Update your wrangler.toml or wrangler.jsonc:
wrangler.jsonc compatibility_flags = ["nodejs_als"]
compatibility_flags = ["nodejs_compat"]
- compatibility_flags = ["nodejs_als"]
+ compatibility_flags = ["nodejs_compat"]
Workers enable nodejs_compat by default from a compatibility_date of 2026-08-04, so you only need the flag on earlier dates.
Because Node.js compatibility is required for everyone, the @sentry/cloudflare/nodejs_compat subpath export was removed. Import from @sentry/cloudflare instead.
sentryCloudflareVitePlugin() wraps your Worker entry at build time, along with any Durable Object, Workflow, or WorkerEntrypoint class listed in your wrangler config. Entries you wrapped yourself are left untouched, so most setups need no changes. The experimental _experimental.autoInstrumentation opt-in was removed, and you can opt out with the top-level autoInstrumentation option:
vite.config.mjssentryCloudflareVitePlugin({ autoInstrumentation: false });
sentryCloudflareVitePlugin({ autoInstrumentation: false });
The enableRpcTracePropagation option was removed, and trace context is no longer appended to every RPC call on env. List the bindings you call in rpcTracePropagationBindings instead:
export default Sentry.withSentry(
(env) => ({
dsn: env.SENTRY_DSN,
- enableRpcTracePropagation: true,
+ rpcTracePropagationBindings: ['ORDERS', /^SVC_/],
}),
handler,
);
export default Sentry.withSentry(
(env) => ({
dsn: env.SENTRY_DSN,
- enableRpcTracePropagation: true,
+ rpcTracePropagationBindings: ['ORDERS', /^SVC_/],
}),
handler,
);
If the Vite plugin instruments your Worker, it adds the bindings it wrapped to rpcTracePropagationBindings for you, so you only have to list the ones it didn't, such as services in other Workers. See RPC Trace Propagation for the option itself.
Strings match a binding name in full, regular expressions match by pattern, and both ignore casing. The option covers RPC method calls only. stub.fetch() and service binding fetch() carry the trace context in HTTP headers, so they propagate either way. Receivers no longer take the option, because an instrumented Durable Object or WorkerEntrypoint reads the trace context whenever a caller sends it.
In v10, the SDK created a new client for every request, so the Dedupe integration never compared errors from different requests. The SDK now reuses one client for all requests and Durable Object calls in an isolate, so Dedupe works like in the other SDKs: it drops an error that has the same type, message, stack trace, and fingerprint as the error captured directly before it in that isolate, even when a different request threw it. An error that many requests throw in a row can have a lower event count in Sentry than before.
To send every occurrence, remove the integration:
export default Sentry.withSentry(
(env) => ({
dsn: env.SENTRY_DSN,
+ integrations: (integrations) =>
+ integrations.filter((integration) => integration.name !== "Dedupe"),
}),
handler,
);
export default Sentry.withSentry(
(env) => ({
dsn: env.SENTRY_DSN,
+ integrations: (integrations) =>
+ integrations.filter((integration) => integration.name !== "Dedupe"),
}),
handler,
);
wrapRequestHandlermoved to the@sentry/cloudflare/requestsubpath.instrumentD1WithSentrywas removed.withSentry()instruments all D1 bindings onenvautomatically.- The
instrumentPrototypeMethodsoption ofinstrumentDurableObjectWithSentrywas removed. Prototype methods are always wrapped now, so remove the option. honoIntegrationwas removed. Use the@sentry/honomiddleware instead.- Rate limiter bindings (
env.MY_RATE_LIMITER.limit()) no longer create a span. Remove any dashboard, alert, orignoreSpansentry that references therpcspan withrpc.service: cloudflare.rate_limit. - The
envtypes and the generics onwithSentryandinstrumentDurableObjectWithSentrywere reworked. If you don't pass explicit generic type parameters, nothing changes.
- export default withSentry<Env>(
+ export default withSentry(
(env) => ({ dsn: env.SENTRY_DSN }),
{
async fetch(request, env, ctx) {},
} satisfies ExportedHandler<Env>,
);
- export default withSentry<Env>(
+ export default withSentry(
(env) => ({ dsn: env.SENTRY_DSN }),
{
async fetch(request, env, ctx) {},
} satisfies ExportedHandler<Env>,
);
The changes in this section detail deprecated APIs that are now removed.
@sentry/coreexports only isomorphic code now. Browser-only exports live on@sentry/core/browserand server-only ones on@sentry/core/server, which keeps server code out of browser bundles. This only affects imports straight from@sentry/core, since the platform SDKs re-export as before. TypeScript reports it ashas no exported member.
// Before
import { loadModule, trpcMiddleware } from "@sentry/core";
import type { BrowserClientReplayOptions } from "@sentry/core";
// After
import { loadModule, trpcMiddleware } from "@sentry/core/server";
import type { BrowserClientReplayOptions } from "@sentry/core/browser";
// Before
import { loadModule, trpcMiddleware } from "@sentry/core";
import type { BrowserClientReplayOptions } from "@sentry/core";
// After
import { loadModule, trpcMiddleware } from "@sentry/core/server";
import type { BrowserClientReplayOptions } from "@sentry/core/browser";
sendDefaultPiiwas removed. UsedataCollectioninstead.Scope.clear()was removed. To reset scope state, re-initialize the SDK or run your code in a fresh scope withwithScopeorwithIsolationScope.- The
disableInstrumentationWarningsoption and theMissingInstrumentationContexttype were removed. With channel-based instrumentation, the SDK can no longer detect that a framework was imported beforeSentry.init(). createSpanEnvelopeand theSpanEnvelopeandSpanItemtypes were removed. Standalone spans are gone: spans are sent on their transaction, or as streamed spans.- The positional
spanOriginargument ofinstrumentFetchRequestwas removed. Pass an options object as the last argument instead. - The internal
addAutoIpAddressToUserexport was removed.
- The
enableTruncationandstreamGenAiSpansflags were removed. Gen AI spans are always streamed and never truncated now. - The
addVercelAiProcessorshelper was removed. AddvercelAIIntegration()instead. - The AI instrumentation moved from
@sentry/coreto@sentry/server-utils. If you imported a helper such asinstrumentOpenAiClientorcreateLangChainCallbackHandlerdirectly from@sentry/core, import it from your platform SDK (for example@sentry/node) or from@sentry/server-utils. - Low-level provider instrumentation internals, integration-name constants, and their types are no longer exported from
@sentry/core.
@sentry/typesis no longer published. Import all types from@sentry/core. The package has only re-exported from@sentry/coresince v8.@sentry/node-corewas merged back into@sentry/node. With the reduced OpenTelemetry footprint, it no longer serves a purpose. Import everything from@sentry/node.@sentry/tanstackstartwas removed. Use@sentry/tanstackstart-react.- Metrics moved out of the base CDN bundle. They ship only in the
*.logs.metricsbundles now. On the other bundles,Sentry.metrics.*is a no-op that warns in debug builds.
These integrations and options kept their behavior, but changed their name.
The inboundFiltersIntegration export was removed, and the integration reports itself as EventFilters. Update references by name, for example in client.getIntegrationByName():
// Before
Sentry.init({
integrations: (integrations) =>
integrations.filter((integration) => integration.name !== "InboundFilters"),
});
// After
Sentry.init({
integrations: (integrations) =>
integrations.filter((integration) => integration.name !== "EventFilters"),
});
// Before
Sentry.init({
integrations: (integrations) =>
integrations.filter((integration) => integration.name !== "InboundFilters"),
});
// After
Sentry.init({
integrations: (integrations) =>
integrations.filter((integration) => integration.name !== "EventFilters"),
});
otlpIntegration()was renamed toopenTelemetryIntegration(), because it sends nothing over OTLP. It reports itself asOpenTelemetryinstead ofOtlpIntegration.getOtlpTracesEndpoint()keeps its name.instrumentLangGraphwas renamed toinstrumentStateGraph, because it only instruments theStateGraphclass.- The low-level
propagateTraceoption ofgetHttpClientSubscriptionsandpatchHttpModuleClientis calledtracePropagationnow, matching thehttpIntegrationoption of the same name.
- Several public types that used
anynow useunknown, includingStackFrame,SamplingContext,SentryError, andUser. You may need to narrow types explicitly. - Attribute typing and serialization were unified across the SDK.
- The
attributesfield of theSamplingContextpassed totracesSampleris required now. The SDK always provides it, so this only affects code that narrows or buildsSamplingContextobjects by hand. - The
attributesfield ofScopeDatais required now. This only affects code that constructsScopeDataobjects manually: addattributes: {}there. - The
endTimestampproperty was removed fromSentrySpanArguments. Callspan.end(timestamp)instead. BrowserOptionssupports theTransportOptionsgeneric now.
Version support timelines are stressful for everybody using the SDK, so we won't be defining one. Instead, we will be applying bug fixes and features to older versions as long as there is demand.
Additionally, we hold ourselves accountable to any security issues, meaning that if any vulnerabilities are found, we will in almost all cases backport them.
It's decided on a case-by-case basis what gets backported. If you need a fix or feature in a previous version of the SDK, please reach out via a GitHub issue.
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").