Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,11 @@ Affected SDKs: `@sentry/browser` and `@sentry/deno` (and their dependents).

The `console` option of `breadcrumbsIntegration` was removed. Use the `consoleIntegration` from `@sentry/core` to capture console breadcrumbs instead.

### Next.js: tracing removed from generated templates
### `@sentry/nextjs`

Affected SDKs: `@sentry/nextjs`.
**Tracing removed from generated templates:** Tracing was removed from the generated Pages Router API handler, Edge API handler, and Middleware wrapper templates. Route handlers and middleware are still instrumented automatically, so no action is required for most users.

Tracing was removed from the generated Pages Router API handler, Edge API handler, and Middleware wrapper templates. Route handlers and middleware are still instrumented automatically, so no action is required for most users.
**Middleware span op changed to `middleware`:** Next.js middleware spans now use the `middleware` span op instead of `http.server.middleware`. If you filter or alert on the previous op (e.g. in dashboards or dynamic sampling rules), update it to `middleware`.

### Cloudflare: `nodejs_compat` compatibility flag is now required

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test('tracesSampler receives normalizedRequest for edge middleware', async ({ re
const middlewareTransaction = await middlewareTransactionPromise;

expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware');
expect(middlewareTransaction.request?.url).toContain('/api/endpoint-behind-middleware');
expect(middlewareTransaction.request?.method).toBe('GET');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransaction = await middlewareTransactionPromise;

expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('node');
expect(middlewareTransaction.transaction_info?.source).toBe('route');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ test.skip('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransaction = await middlewareTransactionPromise;

expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
expect(middlewareTransaction.transaction_info?.source).toBe('route');

Expand Down Expand Up @@ -57,7 +57,7 @@ test.skip('Faulty middlewares', async ({ request }) => {
await test.step('should record transactions', async () => {
const middlewareTransaction = await middlewareTransactionPromise;
expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error');
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
expect(middlewareTransaction.transaction_info?.source).toBe('route');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ test('Tunnel requests should not create middleware or fetch spans', async ({ pag
expect(pageloadTransaction).toBeDefined();
expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload');

const middlewareTransactions = allTransactions.filter(tx => tx.contexts?.trace?.op === 'http.server.middleware');
const middlewareTransactions = allTransactions.filter(tx => tx.contexts?.trace?.op === 'middleware');

// We WILL have a middleware transaction for GET / (the pageload)
// But we should NOT have middleware transactions for POST requests (tunnel route)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ test('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransaction = await middlewareTransactionPromise;

expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('node');
expect(middlewareTransaction.transaction_info?.source).toBe('route');

expect(middlewareTransaction.request?.method).toBe('GET');
expect(middlewareTransaction.request?.url).toContain('/api/endpoint-behind-middleware');

// The `Middleware.execute` OTEL root span is the only `middleware` span. The build-time
// `wrapMiddlewareWithSentry` wrapper used to start a second, redundant one nested inside it.
const nestedMiddlewareSpans = middlewareTransaction.spans?.filter(span => span.op === 'middleware');
expect(nestedMiddlewareSpans).toHaveLength(0);

// Assert that isolation scope works properly
expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
Expand Down Expand Up @@ -52,7 +57,7 @@ test('Faulty middlewares', async ({ request }) => {
await test.step('should record transactions', async () => {
const middlewareTransaction = await middlewareTransactionPromise;
expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error');
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('node');
expect(middlewareTransaction.transaction_info?.source).toBe('route');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransaction = await middlewareTransactionPromise;

expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
expect(middlewareTransaction.transaction_info?.source).toBe('url');

Expand All @@ -37,7 +37,7 @@ test('Faulty middlewares', async ({ request }) => {
await test.step('should record transactions', async () => {
const middlewareTransaction = await middlewareTransactionPromise;
expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error');
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
expect(middlewareTransaction.transaction_info?.source).toBe('url');
});
Expand Down
3 changes: 2 additions & 1 deletion packages/nextjs/src/common/enhanceMiddlewareRootSpan.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { WEB_SERVER_MIDDLEWARE_SPAN_OP } from '@sentry/conventions/op';
import { stripUrlQueryAndFragment } from '@sentry/core';
import { ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from './nextSpanAttributes';

Expand Down Expand Up @@ -26,7 +27,7 @@ export function enhanceMiddlewareRootSpan(span: MutableMiddlewareRootSpan): void
return;
}

span.setOp('http.server.middleware');
span.setOp(WEB_SERVER_MIDDLEWARE_SPAN_OP);

const spanName = attributes[ATTR_NEXT_SPAN_NAME];
if (typeof spanName !== 'string' || !spanName || !span.getName()) {
Expand Down
3 changes: 2 additions & 1 deletion packages/nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetada
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
import { SENTRY_KIND } from '@sentry/conventions/attributes';
import { WEB_SERVER_MIDDLEWARE_SPAN_OP } from '@sentry/conventions/op';

export * from '@sentry/vercel-edge';
export * from '../common';
Expand Down Expand Up @@ -134,7 +135,7 @@ export function init(options: VercelEdgeOptions = {}): void {

// Make sure middleware spans get the right op
if (spanAttributes?.['next.span_type'] === 'Middleware.execute') {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, WEB_SERVER_MIDDLEWARE_SPAN_OP);
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url');
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE, HTTP_TARGET } from '@sentry/conventions/attributes';
import { WEB_SERVER_MIDDLEWARE_SPAN_OP } from '@sentry/conventions/op';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, stripUrlQueryAndFragment } from '@sentry/core';
import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes';
import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../common/span-attributes-with-logic-attached';
Expand Down Expand Up @@ -64,7 +65,7 @@ export function enhanceHandleRequestRootSpan(span: MutableRootSpan): void {

if (middlewareMatch) {
span.setName(`middleware ${middlewareMatch[1]}`);
span.setOp('http.server.middleware');
span.setOp(WEB_SERVER_MIDDLEWARE_SPAN_OP);
}

// Next.js overrides transaction names for page loads that throw an error
Expand Down
12 changes: 6 additions & 6 deletions packages/nextjs/test/common/enhanceMiddlewareRootSpan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('enhanceMiddlewareRootSpan', () => {
enhanceMiddlewareRootSpan(span);

expect(getName()).toBe('middleware');
expect(getOp()).toBe('http.server.middleware');
expect(getOp()).toBe('middleware');
});

it('sets the op but keeps the name when next.span_name is an empty string', () => {
Expand All @@ -52,7 +52,7 @@ describe('enhanceMiddlewareRootSpan', () => {
enhanceMiddlewareRootSpan(span);

expect(getName()).toBe('middleware');
expect(getOp()).toBe('http.server.middleware');
expect(getOp()).toBe('middleware');
});

it('sets the op but keeps the name when next.span_name is not a string', () => {
Expand All @@ -64,7 +64,7 @@ describe('enhanceMiddlewareRootSpan', () => {
enhanceMiddlewareRootSpan(span);

expect(getName()).toBe('middleware');
expect(getOp()).toBe('http.server.middleware');
expect(getOp()).toBe('middleware');
});

it('sets the op but keeps the name when the current name is empty', () => {
Expand All @@ -76,7 +76,7 @@ describe('enhanceMiddlewareRootSpan', () => {
enhanceMiddlewareRootSpan(span);

expect(getName()).toBeUndefined();
expect(getOp()).toBe('http.server.middleware');
expect(getOp()).toBe('middleware');
});

it.each([
Expand All @@ -93,7 +93,7 @@ describe('enhanceMiddlewareRootSpan', () => {
enhanceMiddlewareRootSpan(span);

expect(getName()).toBe(expected);
expect(getOp()).toBe('http.server.middleware');
expect(getOp()).toBe('middleware');
});

it('normalizes the plain "middleware {METHOD}" name emitted for Node.js middleware', () => {
Expand All @@ -107,7 +107,7 @@ describe('enhanceMiddlewareRootSpan', () => {
enhanceMiddlewareRootSpan(span);

expect(getName()).toBe('middleware GET');
expect(getOp()).toBe('http.server.middleware');
expect(getOp()).toBe('middleware');
});

it('strips query and fragment from non-method-prefixed middleware names', () => {
Expand Down
82 changes: 82 additions & 0 deletions packages/nextjs/test/common/wrapMiddlewareWithSentry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as SentryCore from '@sentry/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { wrapMiddlewareWithSentry } from '../../src/common/wrapMiddlewareWithSentry';

describe('wrapMiddlewareWithSentry', () => {
beforeEach(() => {
vi.spyOn(SentryCore, 'captureException').mockReturnValue('');
});

afterEach(() => {
vi.restoreAllMocks();
});

it('does not start its own span when the Next.js OTEL root span is already active', async () => {
vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({} as SentryCore.Span);
vi.spyOn(SentryCore, 'getRootSpan').mockReturnValue({} as SentryCore.Span);
const setCapturedScopesSpy = vi.spyOn(SentryCore, 'setCapturedScopesOnSpan').mockReturnValue(undefined);
const startSpanSpy = vi.spyOn(SentryCore, 'startSpan');

const handler = vi.fn(async (_req: Request) => new Response('ok'));
const wrapped = wrapMiddlewareWithSentry(handler);

await wrapped(new Request('https://example.com/foo', { method: 'GET' }));

// The middleware runs and our forked scopes are bound to the existing OTEL root span...
expect(handler).toHaveBeenCalledTimes(1);
expect(setCapturedScopesSpy).toHaveBeenCalledTimes(1);
// ...but the wrapper never starts a span itself - the `Middleware.execute` span is the transaction.
expect(startSpanSpy).not.toHaveBeenCalled();
});

it('does not start its own span when no span is active', async () => {
vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue(undefined);
const startSpanSpy = vi.spyOn(SentryCore, 'startSpan');

const handler = vi.fn(async (_req: Request) => new Response('ok'));
const wrapped = wrapMiddlewareWithSentry(handler);

await wrapped(new Request('https://example.com/foo', { method: 'GET' }));

expect(handler).toHaveBeenCalledTimes(1);
expect(startSpanSpy).not.toHaveBeenCalled();
});

it('captures errors thrown by the middleware when a root span is already active', async () => {
vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({} as SentryCore.Span);
vi.spyOn(SentryCore, 'getRootSpan').mockReturnValue({} as SentryCore.Span);
vi.spyOn(SentryCore, 'setCapturedScopesOnSpan').mockReturnValue(undefined);
const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException').mockReturnValue('');

const error = new Error('boom');
const handler = vi.fn(async (_req: Request) => {
throw error;
});
const wrapped = wrapMiddlewareWithSentry(handler);

await expect(wrapped(new Request('https://example.com/foo', { method: 'GET' }))).rejects.toThrow('boom');

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
expect(captureExceptionSpy).toHaveBeenCalledWith(
error,
expect.objectContaining({
mechanism: { type: 'auto.function.nextjs.wrap_middleware', handled: false },
}),
);
});

it('captures errors thrown by the middleware when no span is active', async () => {
vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue(undefined);
const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException').mockReturnValue('');

const error = new Error('boom');
const handler = vi.fn(async (_req: Request) => {
throw error;
});
const wrapped = wrapMiddlewareWithSentry(handler);

await expect(wrapped(new Request('https://example.com/foo', { method: 'GET' }))).rejects.toThrow('boom');

expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('enhanceHandleRequestRootSpan', () => {
expect(getName()).toBe('GET /_app');
});

it('normalizes middleware span names and sets http.server.middleware op', () => {
it('normalizes middleware span names and sets the `middleware` op', () => {
const { span, getName, getOp } = makeSpan(
{
[ATTR_NEXT_SPAN_TYPE]: 'BaseServer.handleRequest',
Expand All @@ -138,7 +138,7 @@ describe('enhanceHandleRequestRootSpan', () => {
enhanceHandleRequestRootSpan(span);

expect(getName()).toBe('middleware POST');
expect(getOp()).toBe('http.server.middleware');
expect(getOp()).toBe('middleware');
});

it('writes the middleware op into attributes when the adapter mirrors op writes (streamed shape)', () => {
Expand All @@ -163,7 +163,7 @@ describe('enhanceHandleRequestRootSpan', () => {
enhanceHandleRequestRootSpan(span);

expect(name).toBe('middleware GET');
expect(attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server.middleware');
expect(attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('middleware');
});

it('rewrites GET /_error using the http.target attribute', () => {
Expand Down
Loading