Migrate from 10.x to 11.x for Angular
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/angular/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.
Safari 14 is no longer supported. It lacks performance.timeOrigin and full visibilitychange support, which the web vitals instrumentation had to work around. The SDK still requires ES2020, so the rest of the matrix is unchanged:
- Chrome 80
- Edge 80
- Safari 15, iOS Safari 15
- Firefox 74
- Opera 67
- Samsung Internet 13.0
The same supported Node.js range applies if you use Node.js to install dependencies or build a browser project. Loading the SDK directly in a browser from a CDN does not require Node.js.
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.
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.
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.
Two defaults changed for browser sessions:
- Sessions affected by an uncaught error are recorded as
unhandledinstead ofcrashed. If you track crash-free session rates or have alerts built on them, expect the rate to shift. - The
lifecyclemode ofbrowserSessionIntegrationdefaults to'page', so a session is created on page load and is not renewed on navigation.
// Restore a new session on load and on every navigation
Sentry.init({
integrations: [Sentry.browserSessionIntegration({ lifecycle: "route" })],
});
// Restore a new session on load and on every navigation
Sentry.init({
integrations: [Sentry.browserSessionIntegration({ lifecycle: "route" })],
});
See BrowserSession for lifecycle configuration and session health details.
Events created from a DOMException no longer carry a DOMException.code tag, because the code property is deprecated in favor of DOMException.name, which is already the exception type. Switch searches and alert rules that use the tag to error.type.
Sentry.sendFeedback() now rejects with an Error in all cases. Previously it rejected with a plain string when the request timed out, was rejected with a 403, or otherwise failed to send. The message text is unchanged and is still customizable through the errorMessages hint, so read it from error.message.
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.
Several browserTracingIntegration options moved to dedicated integrations, or were removed:
| Removed option | Replacement |
|---|---|
_experiments.enableInteractions | interactionsIntegration() |
ignorePerformanceApiSpans | userTimingIntegration({ ignore: [...] }) |
trackFetchStreamPerformance | fetchStreamPerformanceIntegration() |
_experiments.enableStandalone*Spans | Removed. The trace lifecycle determines how CLS and LCP are sent. |
// Before
Sentry.init({
integrations: [
Sentry.browserTracingIntegration({
_experiments: { enableInteractions: true },
ignorePerformanceApiSpans: ["third-party-mark"],
trackFetchStreamPerformance: true,
}),
],
});
// After
Sentry.init({
integrations: [
Sentry.browserTracingIntegration(),
Sentry.interactionsIntegration(),
Sentry.userTimingIntegration({ ignore: ["third-party-mark"] }),
Sentry.fetchStreamPerformanceIntegration(),
],
});
// Before
Sentry.init({
integrations: [
Sentry.browserTracingIntegration({
_experiments: { enableInteractions: true },
ignorePerformanceApiSpans: ["third-party-mark"],
trackFetchStreamPerformance: true,
}),
],
});
// After
Sentry.init({
integrations: [
Sentry.browserTracingIntegration(),
Sentry.interactionsIntegration(),
Sentry.userTimingIntegration({ ignore: ["third-party-mark"] }),
Sentry.fetchStreamPerformanceIntegration(),
],
});
browserTracingIntegration no longer captures performance.mark() and performance.measure() spans by default, and no longer accepts an _experiments object. Configure idleTimeout, finalTimeout, and childSpanTimeout for interaction spans on interactionsIntegration, with the same defaults as before. BrowserTracing's timeout options still control pageload and navigation spans.
BrowserTracing now automatically adds the WebVitals integration. Web Vitals changed as follows:
- CLS and LCP are recorded as measurements on the pageload span, or as dedicated spans in stream mode.
- LCP and CLS spans no longer set
browser.web_vital.lcp.report_eventorbrowser.web_vital.cls.report_event, including when soft-navigation reporting is disabled. Finalization timing is unchanged. Remove filters on these attributes from searches and dashboards. - When collected, INP is always sent as a Web Vital span, carrying its value in the
browser.web_vital.inp.valueattribute instead of as a span measurement. Update custom dashboards and alerts that read it as a measurement. Built-in dashboards do not need adjustments. - The
enableInpoption is deprecated. To disable INP, usebrowserTracingIntegration({ webVitals: { ignore: ["inp"] } }). Theignorearray can also contain"cls"or"lcp"to skip those vitals. - To configure Web Vitals, pass
webVitalsoptions to BrowserTracing, or addwebVitalsIntegration()explicitly. Explicit registration takes precedence over BrowserTracing'swebVitalsandenableInpoptions. - Soft-navigation vitals are enabled by default in stream mode on browsers that support the Soft Navigations API. Set
webVitals: { softNavigations: false }to keep one set of vitals for the page lifetime. - BrowserTracing starts a navigation span after a back/forward-cache restore by default. Set
instrumentBfcacheRestore: falseto disable it. Web Vitals for restores are collected by default in stream mode wheninstrumentBfcacheRestoreis enabled. SetwebVitals: { bfcacheNavigations: false }to disable this collection. Restore vitals carrybrowser.navigation.type: bfcache. Filter by that attribute to analyze restores separately from page loads, since near-instant restores can lower combined aggregates.
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
consoleoption ofbreadcrumbsIntegrationwas removed. Console breadcrumbs come from the defaultconsoleIntegrationnow. To disable them, filter out theConsoleintegration, and if you setdefaultIntegrations: false, addconsoleIntegration()to keep them. - AI integrations are no longer available in the browser SDK. They remain available in the server-side SDKs.
- 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.
The legacy per-transaction profiling options were removed. Configure session-based profiling with profileSessionSampleRate and a profileLifecycle of 'trace' or 'manual' instead. The prune-profiler-binaries script was removed from @sentry/profiling-node.
@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"),
});
- 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").