Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0d3087a
feat(node): Deprecate `ignoreStatusCodes` in http integrations (#23973)
Lms24 Sep 3, 2026
e168622
feat(deps): Bump fast-uri from 3.1.5 to 3.1.7 (#23980)
dependabot[bot] Sep 3, 2026
13f2632
test(e2e): Migrate nuxt-4-cloudflare to span streaming (#23950)
s1gr1d Sep 3, 2026
29b67d3
feat(server-utils)!: Start Redis spans as `cache` spans (#23933)
s1gr1d Sep 3, 2026
6011362
feat(core): Add `safeCallback` helper for isolating user-provided cal…
msonnb Sep 3, 2026
449b642
feat(core): Isolate throwing user callbacks instead of capturing them…
msonnb Sep 3, 2026
1a1b643
fix(nextjs): Add orchestrion bundling regression tests and import.met…
s1gr1d Sep 3, 2026
2700997
feat(remix)!: Move the Vite plugin to `@sentry/remix/vite` (#23989)
chargome Sep 3, 2026
80a776c
feat(remix): Auto-wire orchestrion build-time instrumentation (#23988)
chargome Sep 3, 2026
d97a57b
fix(nuxt): Windows file:// for import-in-the-middle hook and isAbsolu…
halillusion Sep 3, 2026
47c61be
docs(migration): Document the `@sentry/core` entrypoint split (#23990)
andreiborza Sep 3, 2026
b3e52bf
test(e2e): Migrate nuxt-5 to span streaming (#23951)
s1gr1d Sep 3, 2026
999a79a
test(e2e): Migrate nuxt-3-dynamic-import to span streaming (#23947)
s1gr1d Sep 3, 2026
d8f7595
test(e2e): Add nuxt-4-static app to keep static trace lifecycle cover…
s1gr1d Sep 3, 2026
374b46f
test(e2e): Migrate nuxt-4 to span streaming (#23949)
s1gr1d Sep 3, 2026
24c4038
Merge branch 'develop' into 11.0.0-beta
chargome Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,24 @@ Sentry.init({

`ignoreSpans` itself is unchanged in shape, but it now takes effect when a span **starts** rather than when the transaction is sent. Matched spans are never recorded at all, which means a matched non-segment span's children are re-parented to its parent instead of being dropped.

#### `ignoreStatusCodes` is deprecated

The `ignoreStatusCodes` option is deprecated on `httpIntegration` and `httpServerSpansIntegration` (Node and the SDKs built on it) as well as on `denoHttpIntegration` and `denoServeIntegration`. It will be removed in v12, without a direct replacement.

The filter runs on the finished transaction event, which is no longer supported span streaming. Child spans are sent as they end, before the response status code is known, so a request's spans can no longer be dropped once the status turns out to be uninteresting. The option therefore only has an effect with `traceLifecycle: 'static'`.

To keep specific requests out of Sentry, decide before they are instrumented: Use `tracesSampler`, or ignore the request via `ignoreIncomingRequests`, which matches on the incoming request instead of on the response:

```js
Sentry.init({
integrations: [
Sentry.httpIntegration({
ignoreIncomingRequests: urlPath => urlPath.startsWith('/admin'),
}),
],
});
```

#### Opting out of span streaming

To keep the previous transaction-based model, set `traceLifecycle: 'static'`:
Expand Down Expand Up @@ -656,7 +674,8 @@ Sentry.init({

This filter runs on transaction events (`processEvent`), so it only takes effect when `traceLifecycle` is `'static'`.
The default `'stream'` lifecycle does not produce transaction events, and typical Deno apps are unaffected. Node's
`httpIntegration` has the same limitation.
`httpIntegration` has the same limitation. For that reason, [`ignoreStatusCodes` is deprecated](#ignorestatuscodes-is-deprecated)
and will be removed in v12.

Transactions that are kept now also carry the HTTP status in the top-level `response` context, as in the other server
SDKs.
Expand Down Expand Up @@ -980,6 +999,8 @@ Messaging span names now read `<operation type> <destination>` in every integrat

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A Redis command whose key matches `cachePrefixes` now starts as a `cache.*` span instead of being converted from a `db.query` span at response time. `ignoreSpans` is evaluated at span start, so filters can match these spans by their cache op and name. A failed cache command reports as a cache span too, where it previously stayed a `db.query` span.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

Redis has no SQL statement to summarize and no collection to pair a command with, so redis and ioredis `db.query` spans are named after the operation and the connection instead of the command that was sent. The command and its arguments remain available on `db.query.text`, redacted as before. `MULTI`/`PIPELINE` batch spans are unchanged — they were already named after their operation, which they now also report on `db.operation.name`. `db.namespace` is deliberately not used in the name: for redis it is the numeric database index, which says nothing about what the command did.
Expand Down Expand Up @@ -1239,10 +1260,36 @@ Affected SDKs: `@sentry/react-router`.
+ import { sentryOnBuildEnd } from '@sentry/react-router/vite';
```

### Remix: Vite plugin moved to `@sentry/remix/vite`

Affected SDKs: `@sentry/remix`.

`sentryRemixVitePlugin` is no longer available from the main `@sentry/remix` entry point. Import it from the dedicated subpath instead:

```diff
// vite.config.ts
- import { sentryRemixVitePlugin } from '@sentry/remix';
+ import { sentryRemixVitePlugin } from '@sentry/remix/vite';
```

The plugin now also applies the build-time instrumentation transform. If you added `sentryOrchestrionPlugin()` from `@sentry/server-utils/orchestrion/vite` to your Vite config manually, remove it. Opt out with `sentryRemixVitePlugin({ buildTimeInstrumentation: false })`.

## 3. Removed APIs

### `@sentry/core` / All SDKs

- `@sentry/core` now exports only isomorphic code. Browser-only exports live on `@sentry/core/browser` and server-only exports on `@sentry/core/server`, and neither subpath re-exports the shared surface any more. This keeps server-only code (HTTP instrumentation, ANR, postgres and sql helpers) out of browser bundles. Most of these APIs are also re-exported by the platform SDKs (`@sentry/node`, `@sentry/browser`, ...), which is unchanged, so this only affects code importing straight from `@sentry/core`. TypeScript reports it as `has no exported member`.

```js
// 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';
```

- The internal, deprecated `addAutoIpAddressToUser` export was removed.
- `Scope.clear()` was removed. To reset scope state, re-initialize the SDK or run your code in a fresh scope via `withScope`/`withIsolationScope`.
- The deprecated positional `spanOrigin` argument of `instrumentFetchRequest` was removed. Pass an options object (e.g. `{ spanOrigin }`) as the last argument instead.
Expand Down Expand Up @@ -1424,6 +1471,8 @@ Sentry.httpIntegration({
});
```

Note that `ignoreStatusCodes` is itself [deprecated](#ignorestatuscodes-is-deprecated) and will be removed in v12.

### `@sentry/cloudflare`

- The `@sentry/cloudflare/nodejs_compat` subpath export was removed. Since `nodejs_compat` is now required for all users, the main `@sentry/cloudflare` entry point includes everything that was previously only available via the subpath.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { installGlobals } from '@remix-run/node';
import { vitePlugin as remix } from '@remix-run/dev';
import { sentryRemixVitePlugin } from '@sentry/remix';
import { sentryRemixVitePlugin } from '@sentry/remix/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { installGlobals } from '@remix-run/node';
import { vitePlugin as remix } from '@remix-run/dev';
import { sentryRemixVitePlugin } from '@sentry/remix';
import { sentryRemixVitePlugin } from '@sentry/remix/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
},
"dependencies": {
"@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
"@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz",
"@remix-run/css-bundle": "2.17.4",
"@remix-run/node": "2.17.4",
"@remix-run/react": "2.17.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { vitePlugin as remix } from '@remix-run/dev';
import { sentryRemixVitePlugin } from '@sentry/remix';
import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite';
import { sentryRemixVitePlugin } from '@sentry/remix/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

Expand All @@ -10,10 +9,6 @@ export default defineConfig({
ignoredRouteFiles: ['**/.*'],
}),
sentryRemixVitePlugin(),
// Run the orchestrion code transform over the SSR server bundle and force-bundle the
// instrumented deps (mysql, ioredis, @remix-run/server-runtime, …) so their
// diagnostics-channel calls are injected at build time.
sentryOrchestrionPlugin(),
tsconfigPaths(),
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { expect, test } from '@playwright/test';
import * as fs from 'fs';
import { createRequire } from 'module';
import * as path from 'path';
import { isDevMode } from './isDevMode';

/**
* The orchestrion bundler plugins are build-time-only, and their module-scope side effects break
* on Workers (an unawaited `WebAssembly.compile()` crashed every cold start, issue #22794). The
* worker bundle OpenNext produces must therefore never contain them: importing `@sentry/nextjs`
* on the server has to keep the plugin graph out of the deployed artifact.
*/
test('worker bundle does not contain the orchestrion bundler plugins', () => {
test.skip(isDevMode, 'requires the production worker build');

const openNextDir = path.resolve(__dirname, '..', '.open-next');
expect(fs.existsSync(path.join(openNextDir, 'worker.js'))).toBe(true);

// `assets` holds the static client files; everything else is code the worker can run.
const serverFiles = collectJsFiles(openNextDir).filter(
filePath => !filePath.startsWith(path.join(openNextDir, 'assets')),
);
expect(serverFiles.length).toBeGreaterThan(0);

const markers = ['code-transformer-bundler-plugins', '__codeTransformerWebpackDiagnostics'];

// The markers must still exist in the installed plugin build.
// If upstream renames them, this fails instead of letting the leak check below pass.
const pluginGraphSources = readOrchestrionPluginGraphSources();
for (const marker of markers) {
expect(
pluginGraphSources.some(source => source.includes(marker)),
`marker "${marker}" is gone from the @sentry/server-utils plugin build — update the markers`,
).toBe(true);
}

const leaks = serverFiles.filter(filePath => {
const content = fs.readFileSync(filePath, 'utf8');
return markers.some(marker => content.includes(marker));
});

expect(leaks.map(filePath => path.relative(openNextDir, filePath))).toEqual([]);
});

/**
* Reads the source of the installed `@sentry/server-utils` webpack plugin entry plus the files it
* requires relatively — the graph a leak would drag into the worker bundle. `createRequire` takes
* the `require` export condition, so this resolves the CJS build, whose `require('./…')` calls the
* regex below picks up.
*/
function readOrchestrionPluginGraphSources(): string[] {
const pluginEntry = createRequire(__filename).resolve('@sentry/server-utils/orchestrion/webpack');
const entrySource = fs.readFileSync(pluginEntry, 'utf8');
return [
entrySource,
...[...entrySource.matchAll(/require\('(\.\.?\/[^']+)'\)/g)].map(([, specifier]) =>
fs.readFileSync(path.resolve(path.dirname(pluginEntry), specifier), 'utf8'),
),
];
}

function collectJsFiles(dir: string): string[] {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
return collectJsFiles(fullPath);
}
return /\.(js|mjs|cjs)$/.test(entry.name) ? [fullPath] : [];
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/nuxt';
import { useRuntimeConfig } from '#imports';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: useRuntimeConfig().public.sentry.dsn,
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/nuxt';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0, // Capture 100% of the transactions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,49 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import type { Span } from '@sentry/nuxt';
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('sends a pageload root span with a parameterized URL', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-dynamic-import', async transactionEvent => {
return transactionEvent.transaction === '/test-param/:param()';
const pageloadSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', span => {
return getSpanOp(span) === 'pageload' && span.is_segment;
});

await page.goto(`/test-param/1234`);

const rootSpan = await transactionPromise;

expect(rootSpan).toMatchObject({
contexts: {
trace: {
data: {
'sentry.segment.name.source': 'route',
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
},
},
transaction: '/test-param/:param()',
transaction_info: {
source: 'route',
},
const pageloadSpan = await pageloadSpanPromise;

expect(pageloadSpan.name).toBe('/test-param/:param()');
expect(pageloadSpan.status).toBe('ok');
expect(pageloadSpan.attributes).toMatchObject({
'sentry.segment.name.source': { type: 'string', value: 'route' },
'sentry.origin': { type: 'string', value: 'auto.pageload.vue' },
'sentry.op': { type: 'string', value: 'pageload' },
'params.param': { type: 'string', value: '1234' },
'url.template': { type: 'string', value: '/test-param/:param()' },
'url.path': { type: 'string', value: '/test-param/1234' },
'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/) },
});
});

test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-dynamic-import', async transactionEvent => {
return transactionEvent.transaction === '/client-error';
});
const spansPromise = collectStreamedSpans('nuxt-3-dynamic-import', spans =>
spans.some(span => span.name === '/client-error' && span.is_segment && getSpanOp(span) === 'pageload'),
);

await page.goto(`/client-error`);

const rootSpan = await transactionPromise;
const errorButtonSpan = rootSpan.spans.find((span: Span) => span.description === 'Vue <ErrorButton>');
const spans = await spansPromise;
const errorButtonSpan = spans.find(span => span.name === 'Vue <ErrorButton>');

const expected = {
data: { 'sentry.origin': 'auto.ui.vue', 'sentry.op': 'ui.mount' },
description: 'Vue <ErrorButton>',
op: 'ui.mount',
expect(errorButtonSpan).toMatchObject({
name: 'Vue <ErrorButton>',
is_segment: false,
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
origin: 'auto.ui.vue',
};

expect(errorButtonSpan).toMatchObject(expected);
start_timestamp: expect.any(Number),
end_timestamp: expect.any(Number),
attributes: expect.objectContaining({
'sentry.op': { type: 'string', value: 'ui.mount' },
'sentry.origin': { type: 'string', value: 'auto.ui.vue' },
}),
});
});
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt';
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-dynamic-import', transactionEvent => {
return transactionEvent.transaction.includes('GET /test-param/');
test('sends a server root span on pageload', async ({ page }) => {
const serverSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', span => {
return span.is_segment && span.name.includes('GET /test-param/');
});

await page.goto('/test-param/1234');

const transaction = await transactionPromise;
const serverSpan = await serverSpanPromise;

expect(transaction.contexts.trace).toEqual(
expect.objectContaining({
data: expect.objectContaining({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.http_server',
}),
}),
);
expect(getSpanOp(serverSpan)).toBe('http.server');
expect(serverSpan.attributes['sentry.origin']?.value).toBe('auto.http.http_server');
});

test('does not send transactions for build asset folder "_nuxt"', async ({ page }) => {
test('does not send spans for build asset folder "_nuxt"', async ({ page }) => {
let buildAssetFolderOccurred = false;

waitForTransaction('nuxt-3-dynamic-import', transactionEvent => {
if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) {
waitForStreamedSpan('nuxt-3-dynamic-import', span => {
if (span.is_segment && /^GET \/_nuxt\//.test(span.name)) {
buildAssetFolderOccurred = true;
}
return false; // expects to return a boolean (but not relevant here)
});

const transactionEventPromise = waitForTransaction('nuxt-3-dynamic-import', transactionEvent => {
return transactionEvent.transaction.includes('GET /test-param/');
const serverSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', span => {
return span.is_segment && span.name.includes('GET /test-param/');
});

await page.goto('/test-param/1234');

const transactionEvent = await transactionEventPromise;
const serverSpan = await serverSpanPromise;

expect(buildAssetFolderOccurred).toBe(false);

expect(transactionEvent.transaction).toBe('GET /test-param/:param()');
expect(serverSpan.name).toBe('GET /test-param/:param()');
expect(serverSpan.attributes['sentry.segment.name.source']?.value).toBe('route');
});
Loading
Loading