Migrate from 10.x to 11.x for Cloudflare

Learn about migrating from Sentry JavaScript SDK 10.x to 11.x.

Agent-Assisted Migration
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.
Copy this prompt into your AI agent to migrate to v11. Works with Cursor, Claude Code, Codex, and more.

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: sendDefaultPii is replaced by dataCollection, which controls each category of data separately and collects more by default (Read more about dataCollection)
  • 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.

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.

Categoryv10 default (sendDefaultPii off)v11 default
userInfofalsetrue
cookiesnot collectedtrue
httpHeadersrequest + response, PII scrubbedrequest + response
httpBodiesnot collected (size only)all request/response
urlQueryParamstruetrue
genAIinputs + outputs not collectedinputs + outputs
databaseQueryDatafalsetrue
stackFrameVariablestruetrue
frameContextLines75

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.

Keeping the v10 collection defaults
Copied
Sentry.init({
  dataCollection: {
    userInfo: false,
    cookies: false,
    httpHeaders: {
      request: { deny: ["forwarded", "-ip", "remote-", "via", "-user"] },
      response: { deny: ["forwarded", "-ip", "remote-", "via", "-user"] },
    },
    httpBodies: [],
    urlQueryParams: { deny: ["forwarded", "-ip", "remote-", "via", "-user"] },
    genAI: { inputs: false, outputs: false },
    databaseQueryData: false,
    graphQL: { document: false, variables: false },
  },
});

The cookies, urlQueryParams, and httpHeaders fields also accept true, false, { allow: string[] }, or { deny: string[] }.

Overriding a category for a single integration

The include options of requestDataIntegration stay an integration-level override. An explicit false keeps a category off the event, and an explicit true attaches it even when dataCollection has that category disabled. Any allow or deny filtering still applies, and a category that only include enables is filtered with the default denylist for sensitive values.

User IP address inference is now controlled by dataCollection.userInfo. To keep it for the data this integration collects, set requestDataIntegration({ include: { ip: true } }).

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.

Staying on transaction mode

Set traceLifecycle: 'static' to keep the previous model. In Node.js, Bun, Vercel Edge, and Cloudflare you can set the SENTRY_TRACE_LIFECYCLE=static environment variable instead.

Copied
Sentry.init({
  traceLifecycle: "static",

  // `beforeSendSpan` must be wrapped with `Sentry.withStaticSpan`
  beforeSendSpan: Sentry.withStaticSpan((span) => {
    span.description = scrub(span.description);
    return span;
  }),
});

In transaction mode, spanToStaticSpanJSON returns the static SpanJSON format.

Transaction mode only exists for backwards compatibility and will be removed in a future major version. Treat it as a temporary measure.

Spans are no longer capped at 1000 per transaction, and individual payload limits are higher. See stream mode for how it works.

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:

Copied
// 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:

Copied
// 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)
descriptionname
dataattributes
opattributes['sentry.op']
timestampend_timestamp
status (string)status ('ok' or 'error')
Copied
// 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.

Copied
// 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:

Copied
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 opBeforeAfter
http.clientGET https://api.example.com/users/123GET api.example.com
http.serverGET /users/123GET /users/:id, or GET
pageload, navigation/users/123/users/:id, or Pageload
resource.*/assets/app.jscdn.example.com, or Resource
graphqlquery GetUserGraphQL query
queue.*my-queue process, poll my-topicprocess my-queue, receive my-topic
db.querySELECT * FROM "User" WHERE "id" = $1SELECT "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.

Full list of span name changes
Span opBeforeAfter
pageload, navigationThe route, or the raw URL path (/users/123)The route, or Pageload and Navigation
http.serverThe method and route, or the raw URL path (GET /users/123)GET /users/:id, or the method alone (GET)
http.client*The method and sanitized URL (GET https://api.example.com/users/123)The method and domain (GET api.example.com), or the method alone
routerFramework-specific, sometimes the raw URL (SvelteKit Route Change)The span's http.route, or Router
handlerFramework-specific, often with the request method (GET /users/:id, getUser)The span's http.route, or Request handler
graphqlThe phase and operation name (query GetUser, graphql.resolve user.0.name)The operation or processing type (GraphQL query, GraphQL parse)
gen_ai inference ops{operation} {model}, or {operation} unknown (chat unknown){operation} {model}, or {operation} (chat)
gen_ai.invoke_agentThe LangChain chain name, prefixed with chain (chain format_prompt){operation} {name} from gen_ai.agent.name, gen_ai.pipeline.name, or gen_ai.function_id, or {operation}
resource.*The resource URL (/assets/app.js)The resource domain (cdn.example.com), or Resource
mcp.serverThe method and its target (resources/read file:///docs/api.md)The method alone for resource methods (resources/read). Tool and prompt names are unchanged
mcp.notification.*The notification method nameThe notification method name, or MCP notification
queue.publishIntegration-specific (publish my-exchange, send my-topic)The operation type and destination (send my-exchange), or the operation type alone
queue.processIntegration-specific (my-queue process)The operation type and destination (process my-queue), or the operation type alone
queue.receiveThe kafkajs operation name (poll my-topic)The operation type and destination (receive my-topic)
cache.*The cache keys (user:123), or for dataloader the operation and loader nameThe cache operation (cache.get, cache.put, cache.remove)
db, db.query (SQL)The statement the driver ran (SELECT * FROM "User" WHERE "id" = $1)A summary of it (SELECT "User"), or, without a statement, the operation and table, the namespace, or the database system (postgresql)
db (MongoDB)The serialized command, or mongodb.<operation> (mongodb.find)The operation and collection (find users), the database namespace, or mongodb
db (Mongoose)mongoose.<Model>.<operation> (mongoose.BlogPost.findOne)The operation and collection (findOne blogposts), the database namespace, or mongodb
db (Supabase)The query builder call and table (select(...) from(users)), or auth <method>The operation and table (select users), or the dotted auth method (auth.signInWithPassword)
db.query (Redis, ioredis)The serialized command with redacted arguments (set test-key [1 other arguments])The operation and connection (SET localhost:6379), the operation and redis function for FCALL, or redis
browser.* navigation timingThe document URLA static phase name, such as DNS lookup, Request, or Load event
functionIntegration-specific, such as serverAction/updateUser or LOADER routes/users.$idcode.function.name, such as updateUser or loader
function (Angular TraceMethod)<getUser> or <unnamed>The decorator's name, or Function execution
function (SvelteKit)The route or request pathThe wrapped function's name, such as load or GET
function (Ember route hooks)The full route nameThe hook name, such as beforeModel, model, or setupController
function (React Router route hooks)The route, raw URL path, or fetcher keyThe hook name, such as loader, action, clientLoader, or fetcher
function (NestJS @OnEvent)event user.createdThe event name, such as user.created
function (TanStack Start server functions)GET /_serverFn/abc123The function name, or serverFn until middleware resolves it
function.gcpThe HTTP method and path, or trigger typeThe function name, or Serverless function execution
function.awsThe Lambda function nameUnchanged, with Serverless function execution as the fallback
ui.mount, ui.render, ui.update, ui.unmount<UserList>, Vue <Root>, Application Render, or initThe unwrapped component name, or Component mount, Component render, Component update, or Component unmount
ui.taskrunloopUI task
ui.action.clickThe pageload or navigation span's namerouter.navigation.route.id, url.template, or http.route, in that order, or Click
ui.interaction.*The element's DOM pathThe annotated component name, or Click, Hover, Drag, or Key press
ui.webvital.lcpThe LCP element's DOM pathThe annotated component name, or Largest contentful paint
ui.webvital.clsThe first layout-shift source's DOM pathThe annotated component name, or Layout shift

In transaction mode, span names remain unchanged. A few consequences worth checking:

  • useOperationNameForRootSpan no longer renames the enclosing root span. The operations stay on that span's sentry.graphql.operation attribute.
  • graphqlClientIntegration no longer appends the operation to the outgoing request span name. It's on graphql.operation.name and graphql.operation.type instead.
  • Only the Express, Koa, and Hapi integrations resolve a route for router spans. Angular, Ember, and SvelteKit router spans are named Router.
  • Express, Fastify, Hapi, and Elysia resolve a route for handler spans. NestJS has none at span start, so its handler spans are named Request handler. The handler function name moved to nestjs.callback for NestJS and code.function.name for Elysia.
  • Cache keys are no longer part of a cache span name. They stay on cache.key, and every cache span carries the new cache.operation attribute (get, put, remove). This affects the Redis and ioredis cache spans, the Nuxt and Nitro storage spans, and the dataloader spans, which report the loader name on db.collection.name now. A Redis command matching cachePrefixes starts as a cache span now instead of turning into one at response time, and maxCacheKeyLength has no effect in stream mode.
  • The pg, postgres.js, mysql, mysql2, knex, tedious, Prisma, Nitro db0, and Cloudflare D1 instrumentations name their query spans the same way now. A statement that touches no table (SELECT NOW()) summarizes to the bare operation. Connect and pool spans (pg.connect, mysql2.connect, redis-connect, generic-pool.acquire) keep their names.
  • Supabase query spans drop the builder call from the name, and the modifiers stay on db.query. Auth spans are named after the method they call (auth.admin.createUser).
  • AWS SQS SendMessage, SendMessageBatch, and ReceiveMessage, and SNS Publish, are messaging spans now instead of rpc spans. Every other command on those clients stays rpc.
  • Child spans carry their service span's name in sentry.segment.name, so that changes with it.
Browser timing, function, and UI span details

Browser navigation timing spans use these names:

Span opName
browser.cacheCache lookup
browser.dnsDNS lookup
browser.connectConnect
browser.tls_sslTLS handshake
browser.redirectRedirect
browser.requestRequest
browser.responseResponse
browser.unload_eventUnload event
browser.dom_content_loaded_eventDOMContentLoaded event
browser.load_eventLoad event

The document URL stays on url.full in both trace lifecycles, subject to dataCollection.urlQueryParams. Update name-based filters to use that attribute.

Framework function spans keep the previous name on sentry.description. For SvelteKit, this includes wrapLoadWithSentry, wrapServerLoadWithSentry, wrapServerRouteWithSentry, and spans from SvelteKit's own tracing. Match the route on http.route, or url.template for client-side universal loads, and the request path on url.path.

GCP and Firebase function names come from FUNCTION_TARGET or K_SERVICE. The function name stays on faas.name, trigger details on gcp.function.context.*, and HTTP method and path on http.request.method and url.path. AWS Lambda spans fall back to Serverless function execution when neither the invocation context nor AWS_LAMBDA_FUNCTION_NAME provides a name. Their sentry.segment.name.source is now component in both trace lifecycles.

Component spans keep their original names on sentry.description. Vue's Application Render becomes Root, and Ember's init becomes Component mount. Update filters matching <UserList> or Vue <Root> to use the new name or the description.

Click idle spans copy router.navigation.route.id, url.template, http.route, url.path, and url.full from the route span. Event Timing and INP spans keep the DOM path on browser.web_vital.inp.target and the annotated component on ui.component_name, in both trace lifecycles. Match the target attribute if you previously filtered by DOM path.

LCP and CLS DOM paths stay on browser.web_vital.lcp.element and browser.web_vital.cls.source.1. Long task and long animation frame names remain Main UI thread blocked.

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.

Full list of span op changes

Backend HTTP, handlers, middleware, and routers

AreaBeforeAfter
Request handlers (Express, Koa, Fastify, Elysia, NestJS)request_handler.<library>, handler.nestjshandler
Hono app.request() in-process dispatchhono.requesthttp.server
Web-server middlewaremiddleware.express, middleware.koa, middleware.hono, middleware.elysia, middleware.nestjs, middleware.nuxt, middleware.nitro, middleware.tanstackstart, hook.fastify, http.server.middleware (Next.js)middleware
Backend router layersrouter.express, router.koa, router.hapirouter
Hapi server extensionsserver.ext.hapimiddleware
NestJS setup and lifecycle handlersapp_creation.nestjs, request_context.nestjs, event.nestjsfunction

Framework functions

AreaBeforeAfter
Loaders, actions, and server functions (Next.js, Remix, React Router, SvelteKit, SolidStart, TanStack Start)function.nextjs, function.sveltekit.load, function.react_router.loader, function.remix.document_request, loader.remix, action.remix, function.server_action, function.tanstackstartfunction

Frontend and UI

AreaBeforeAfter
Frontend routingui.angular.routing, ui.sveltekit.routing, ui.ember.transitionrouter
React, Vue, and Svelte component lifecyclesui.react.mount/render/update, ui.svelte.init/update, Vue render/update/mount/create/activate/unmount/destroyui.mount, ui.render, ui.update, ui.unmount
Angular tracing decoratorsui.angular.init, ui.angular.<method>ui.mount, function
Ember route hooks, runloop, and componentsui.ember.route.<hook>, ui.ember.runloop.<queue>, ui.ember.component.render/definition/initfunction, ui.task, ui.render/ui.resolve/ui.mount
Browser paint entriespaintbrowser.paint

Databases, cache, and messaging

AreaBeforeAfter
Redis commands and connectdb.redis, db.redis.connectdb.query, db
Nuxt and Nitro storage (unstorage)cache.has_item, cache.get_item, cache.get_items, cache.get_keys, cache.set_item, cache.set_items, cache.remove_item, cache.clearcache.get, cache.put, cache.remove
Kafka, AMQP, and OTel-inferred messagingmessage, message.produce, message.consumequeue.publish, queue.receive, queue.process
AWS SQS and SNS messaging commandsrpcqueue.publish, queue.receive

RPC and Gen AI

AreaBeforeAfter
tRPCrpc.serverrpc
GCP gRPC callsgrpc.<service>grpc
AWS Bedrock inferencerpcgen_ai.chat, gen_ai.generate_content
Gen AI fallbacks and model metadata (Vercel AI, LangGraph)gen_ai.unknown, ai.run, gen_ai.modelsfunction

FaaS, serverless, and HTTP clients

AreaBeforeAfter
AWS Lambda functionsfunction.aws.lambdafunction.aws
GCP functionsfunction.gcp.http, function.gcp.event, function.gcp.cloud_eventfunction.gcp
Firebase functionshttp.requestfunction.gcp
Cloudflare cron, email, and workflow stepsfaas.cron, faas.email, function.step.dofunction
OTel-inferred FaaS spans (from faas.trigger)arbitrary trigger strings used verbatimhttp.server, queue.process, function
GCP HTTP clienthttp.client.<service>http.client
Prefetch HTTP requestshttp.client.prefetch, http.server.prefetchhttp.client, http.server

Casing normalized to snake_case

BeforeAfter
ui.long-taskui.long_task
ui.long-animation-frameui.long_animation_frame
browser.unloadEventbrowser.unload_event
browser.domContentLoadedEventbrowser.dom_content_loaded_event
browser.loadEventbrowser.load_event
browser.TLS/SSLbrowser.tls_ssl
browser.DNSbrowser.dns

Legacy span attributes were replaced by their semantic-convention equivalents. Update any custom instrumentation, beforeSendSpan logic, dashboards, or alerts that reference them.

HTTP and network attributes
v10 attributev11 attribute
http.hostserver.address
http.flavornetwork.protocol.version
http.client_ipclient.address
http.methodhttp.request.method
http.status_codehttp.response.status_code
http.status_texthttp.response.status_text
http.schemeurl.scheme
http.user_agentuser_agent.original
http.request_content_lengthhttp.request.body.size
http.response_content_lengthhttp.response.body.size
http.decoded_response_content_lengthhttp.response.body.decoded_size
http.response_transfer_sizehttp.response.size
http.targeturl.path + url.query
http.queryurl.query
http.fragmenturl.fragment
url.same_originhttp.request.same_origin
net.host.name, net.peer.nameserver.address
net.host.ipnetwork.local.address
net.host.portnetwork.local.port
net.peer.ipnetwork.peer.address
net.peer.portnetwork.peer.port
net.transportnetwork.transport

SanitizedRequestData, the shape used for http breadcrumb data and http.client span data, uses http.request.method as the key for the request method now.

Transport values change from ip_tcp and ip_udp to tcp and udp. HTTP instrumentation reports the protocol as network.protocol.name and its version as network.protocol.version.

HTTP header attribute names keep their hyphens instead of replacing them with underscores. For example, http.request.header.user_agent becomes http.request.header.user-agent. Header values are now string arrays, with one entry per value: ['gzip', 'deflate'] replaces 'gzip;deflate'.

Cookies are no longer split into attributes per cookie name. http.request.header.cookie and http.request.header.set-cookie now hold arrays of name=value entries in the order sent, such as ['session=[Filtered]', 'theme=dark']. Sensitive values remain filtered, and Set-Cookie attributes such as HttpOnly are still omitted.

On server-side HTTP spans, the content-length header is always reported as http.request.body.size and http.response.body.size, instead of switching to http.request_body_size_uncompressed when no encoding was present.

Database and messaging attributes
v10 attributev11 attribute
db.systemdb.system.name
db.namedb.namespace
db.operationdb.operation.name
db.statementdb.query.text
db.mongodb.collectiondb.collection.name
net.peer.nameserver.address
net.peer.portserver.port

SQL query spans carry a new db.query.summary attribute, holding the summary of the sanitized statement (SELECT "User"). It's set in both trace lifecycles.

Mongoose spans report db.system.name: 'mongodb' instead of 'mongoose', because Mongoose is an ODM, not a database system.

The Redis and ioredis instrumentations no longer emit db.connection_string. The connection is described by server.address and server.port instead.

The AMQP instrumentation reports messaging.destination.name, messaging.rabbitmq.destination.routing_key, messaging.message.id, messaging.message.conversation_id, messaging.operation.name, network.protocol.name, network.protocol.version, and url.full. It no longer emits messaging.destination_kind.

Gen AI attributes
v10 attributev11 attribute
gen_ai.systemgen_ai.provider.name
gen_ai.request.available_toolsgen_ai.tool.definitions
gen_ai.tool.inputgen_ai.tool.call.arguments
gen_ai.tool.outputgen_ai.tool.call.result
gen_ai.usage.cache_creation_input_tokensgen_ai.usage.cache_creation.input_tokens
gen_ai.usage.cache_read_input_tokensgen_ai.usage.cache_read.input_tokens
gen_ai.usage.input_tokens.cachedgen_ai.usage.cache_read.input_tokens
gen_ai.usage.input_tokens.cache_writegen_ai.usage.cache_creation.input_tokens
gen_ai.usage.output_tokens.reasoninggen_ai.usage.reasoning.output_tokens
ai.pipeline.namegen_ai.pipeline.name
ai.streaminggen_ai.response.streaming
langchain.chain.namegen_ai.pipeline.name

gen_ai.tool.type is no longer set on tool spans. gen_ai.pipeline.name is omitted for unnamed chains, instead of holding unknown_chain.

The Anthropic integration no longer sets gen_ai.prompt. The legacy Completions API prompt is reported as a user message on gen_ai.input.messages, like every other request shape.

Other attributes and constants
  • On ui.long_animation_frame spans, code.filepath and code.function became code.file.path and code.function.name.
  • On file spans, fs_error became error.type and holds the syscall error code (ENOENT) instead of the full message.
  • The TanStack Router integration reports path parameters as url.path.parameter.<key> instead of url.path.params.<key>.
  • Vue Router's navigation.route.id became router.navigation.route.id, with the same matched route name.
  • AWS Lambda spans use faas.invocation_id instead of faas.execution for the request ID, and cloud.resource_id instead of faas.id for the invoked function ARN. Lambda Invoke spans from awsIntegration also use faas.invocation_id.
  • Koa router and middleware spans no longer set koa.name. Use http.route for the route and code.function.name for the middleware handler.
  • The Cloudflare-specific sentry.cloudflare_tracer attribute is no longer set.
  • Import attribute constants from @sentry/core directly. The semanticAttributes re-export was removed.
  • sentry.source (SEMANTIC_ATTRIBUTE_SENTRY_SOURCE) was replaced by sentry.segment.name.source, which is only set on the root span.

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.

Sending spans from @opentelemetry/api to Sentry

Turn this on when a library you depend on, or your own code, creates spans through @opentelemetry/api and you want them in Sentry:

Copied
import { trace } from "@opentelemetry/api";

// Somewhere in your app, or inside a dependency
const tracer = trace.getTracer("my-library");

tracer.startActiveSpan("work", (span) => {
  doWork();
  span.end();
});

With enableOpenTelemetrySetup: true, the span is captured, without it, the span is ignored. enableOpenTelemetrySetup isn't meant for setups that already run their own OpenTelemetry pipeline, which is the next section.

Copied
Sentry.init({
  dsn: "___PUBLIC_DSN___",
  tracesSampleRate: 1.0,
  enableOpenTelemetrySetup: true,
});

This is not a general OpenTelemetry pipeline: no exporter, no OTLP output. Sentry also refuses to register its provider if you already registered one of your own, and the @sentry/cloudflare/request entry point doesn't support the option at all.

Running your own OpenTelemetry setup

Keep enableOpenTelemetrySetup off, turn Sentry tracing off, and add openTelemetryIntegration() to connect Sentry errors, logs, metrics, and crons to your OpenTelemetry traces. getOtlpTracesEndpoint() turns your DSN into the URL and auth headers of Sentry's OTLP endpoint.

Copied
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import * as Sentry from "@sentry/node";

const provider = new NodeTracerProvider({
  spanProcessors: [
    new BatchSpanProcessor(
      new OTLPTraceExporter(Sentry.getOtlpTracesEndpoint("___PUBLIC_DSN___"))
    ),
  ],
});

provider.register();

Sentry.init({
  dsn: "___PUBLIC_DSN___",
  // No `tracesSampleRate`: OpenTelemetry owns spans, Sentry owns errors and logs
  integrations: [Sentry.openTelemetryIntegration()],
});

On @sentry/nextjs and @sentry/sveltekit, set enableOpenTelemetrySetup: false explicitly.

The two pipelines stay separate: Sentry sends no spans, and no Sentry span reaches your OpenTelemetry pipeline.

This setup only works with Sentry tracing off, so leave tracesSampleRate unset. Sentry instruments many of the libraries OpenTelemetry does, so leaving tracing on gives you two spans for every operation, in two pipelines that never join up. With tracing off, Sentry's instrumentation stays installed and keeps isolating requests, but emits no spans. Note that this changed from v10, where skipOpenTelemetrySetup: true also turned off Sentry's HTTP and fetch spans.

If you used the v10 integration from @sentry/node-core/light/otlp: it moved to the main export of every server SDK, it no longer sets up an exporter (the setupOtlpTracesExporter and collectorUrl options were removed), and it was renamed to openTelemetryIntegration().

SentryContextManager, SentrySampler, and SentrySpanProcessor were removed, so there is no longer a way to route spans from your own provider into Sentry as Sentry spans. Export them over OTLP instead.

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.

Copied
// 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.

Copied
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
Copied
 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.mjs
Copied
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:

Copied
  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:

Copied
  export default Sentry.withSentry(
    (env) => ({
      dsn: env.SENTRY_DSN,
+     integrations: (integrations) =>
+       integrations.filter((integration) => integration.name !== "Dedupe"),
    }),
    handler,
  );

  • wrapRequestHandler moved to the @sentry/cloudflare/request subpath.
  • instrumentD1WithSentry was removed. withSentry() instruments all D1 bindings on env automatically.
  • The instrumentPrototypeMethods option of instrumentDurableObjectWithSentry was removed. Prototype methods are always wrapped now, so remove the option.
  • honoIntegration was removed. Use the @sentry/hono middleware instead.
  • Rate limiter bindings (env.MY_RATE_LIMITER.limit()) no longer create a span. Remove any dashboard, alert, or ignoreSpans entry that references the rpc span with rpc.service: cloudflare.rate_limit.
  • The env types and the generics on withSentry and instrumentDurableObjectWithSentry were reworked. If you don't pass explicit generic type parameters, nothing changes.
Copied
- 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/core exports only isomorphic code now. Browser-only exports live on @sentry/core/browser and 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 as has no exported member.
Copied
// 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";
  • sendDefaultPii was removed. Use dataCollection instead.
  • Scope.clear() was removed. To reset scope state, re-initialize the SDK or run your code in a fresh scope with withScope or withIsolationScope.
  • The disableInstrumentationWarnings option and the MissingInstrumentationContext type were removed. With channel-based instrumentation, the SDK can no longer detect that a framework was imported before Sentry.init().
  • createSpanEnvelope and the SpanEnvelope and SpanItem types were removed. Standalone spans are gone: spans are sent on their transaction, or as streamed spans.
  • The positional spanOrigin argument of instrumentFetchRequest was removed. Pass an options object as the last argument instead.
  • The internal addAutoIpAddressToUser export was removed.

  • The enableTruncation and streamGenAiSpans flags were removed. Gen AI spans are always streamed and never truncated now.
  • The addVercelAiProcessors helper was removed. Add vercelAIIntegration() instead.
  • The AI instrumentation moved from @sentry/core to @sentry/server-utils. If you imported a helper such as instrumentOpenAiClient or createLangChainCallbackHandler directly 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/types is no longer published. Import all types from @sentry/core. The package has only re-exported from @sentry/core since v8.
  • @sentry/node-core was merged back into @sentry/node. With the reduced OpenTelemetry footprint, it no longer serves a purpose. Import everything from @sentry/node.
  • @sentry/tanstackstart was removed. Use @sentry/tanstackstart-react.
  • Metrics moved out of the base CDN bundle. They ship only in the *.logs.metrics bundles 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():

Copied
// 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 to openTelemetryIntegration(), because it sends nothing over OTLP. It reports itself as OpenTelemetry instead of OtlpIntegration. getOtlpTracesEndpoint() keeps its name.
  • instrumentLangGraph was renamed to instrumentStateGraph, because it only instruments the StateGraph class.
  • The low-level propagateTrace option of getHttpClientSubscriptions and patchHttpModuleClient is called tracePropagation now, matching the httpIntegration option of the same name.

  • Several public types that used any now use unknown, including StackFrame, SamplingContext, SentryError, and User. You may need to narrow types explicitly.
  • Attribute typing and serialization were unified across the SDK.
  • The attributes field of the SamplingContext passed to tracesSampler is required now. The SDK always provides it, so this only affects code that narrows or builds SamplingContext objects by hand.
  • The attributes field of ScopeData is required now. This only affects code that constructs ScopeData objects manually: add attributes: {} there.
  • The endTimestamp property was removed from SentrySpanArguments. Call span.end(timestamp) instead.
  • BrowserOptions supports the TransportOptions generic 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.

Was this helpful?
Help improve this content
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").