Skip to content
Draft
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
13 changes: 11 additions & 2 deletions packages/astro/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
getRootSpan,
getUrlFragment,
getUrlQuery,
hasSpanStreamingEnabled,
HTTP_SPAN_NAME_FALLBACK,
objectify,
SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD,
spanToJSON,
Expand Down Expand Up @@ -228,9 +230,16 @@ async function instrumentRequestStartHttpServerSpan(
attributes[URL_QUERY] = filterCollectedUrlQuery(getUrlQuery(ctx.url.search));
attributes[URL_FRAGMENT] = getUrlFragment(ctx.url.hash);

const name = `${method} ${parametrizedRoute || ctx.url.pathname}`;
const transactionName = `${method} ${parametrizedRoute || ctx.url.pathname}`;

isolationScope.setTransactionName(name);
// The scope's transaction name is what error events are grouped by, so it keeps the URL path.
isolationScope.setTransactionName(transactionName);

// With span streaming, span names have to be low cardinality, so we can't fall back to the URL path.
const name =
parametrizedRoute || !hasSpanStreamingEnabled(client)
? transactionName
: method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK;

const res = await startSpan(
{
Expand Down
62 changes: 62 additions & 0 deletions packages/astro/test/server/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,68 @@
expect(resultFromNext).toStrictEqual(nextResult);
});

describe('with span streaming enabled', () => {
beforeEach(() => {
vi.spyOn(SentryNode, 'getClient').mockImplementation(
() =>
({
getOptions: () => ({ traceLifecycle: 'stream' }),
getDataCollectionOptions: () => ({
userInfo: false,
cookies: true,
httpHeaders: { request: true, response: true },
httpBodies: [],
urlQueryParams: true,
graphQL: { document: true, variables: true },
genAI: { inputs: true, outputs: true },
databaseQueryData: true,
stackFrameVariables: true,
frameContextLines: 5,
}),
}) as unknown as Client,
);
});

it('names an unparameterized span after the request method', async () => {
const middleware = handleRequest();
const ctx = {
...DYNAMIC_REQUEST_CONTEXT,
request: { method: 'GET', url: '/a%xx', headers: new Headers() },
url: { pathname: 'a%xx', href: 'http://localhost:1234/a%xx' },
params: {},
};

// @ts-expect-error, a partial ctx object is fine here

Check failure on line 167 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Unit Tests

Unhandled error

TypeCheckError: Unused '@ts-expect-error' directive. ❯ test/server/middleware.test.ts:167:7

Check failure on line 167 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (26) Unit Tests

Unhandled error

TypeCheckError: Unused '@ts-expect-error' directive. ❯ test/server/middleware.test.ts:167:7

Check failure on line 167 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (20.19) Unit Tests

Unhandled error

TypeCheckError: Unused '@ts-expect-error' directive. ❯ test/server/middleware.test.ts:167:7

Check failure on line 167 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Unit Tests

Unhandled error

TypeCheckError: Unused '@ts-expect-error' directive. ❯ test/server/middleware.test.ts:167:7
await middleware(
ctx,

Check failure on line 169 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Unit Tests

Unhandled error

TypeCheckError: Type '{ clientAddress string; request { method string; url string; headers Headers; }; url { pathname string; href string; }; params {}; }' is missing the following properties from type 'APIContext<Record<string, any>, Record<string, string | undefined>>' site, generator, props, redirect, and 8 more. ❯ test/server/middleware.test.ts:169:9

Check failure on line 169 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (26) Unit Tests

Unhandled error

TypeCheckError: Type '{ clientAddress string; request { method string; url string; headers Headers; }; url { pathname string; href string; }; params {}; }' is missing the following properties from type 'APIContext<Record<string, any>, Record<string, string | undefined>>' site, generator, props, redirect, and 8 more. ❯ test/server/middleware.test.ts:169:9

Check failure on line 169 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (20.19) Unit Tests

Unhandled error

TypeCheckError: Type '{ clientAddress string; request { method string; url string; headers Headers; }; url { pathname string; href string; }; params {}; }' is missing the following properties from type 'APIContext<Record<string, any>, Record<string, string | undefined>>' site, generator, props, redirect, and 8 more. ❯ test/server/middleware.test.ts:169:9

Check failure on line 169 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Unit Tests

Unhandled error

TypeCheckError: Type '{ clientAddress string; request { method string; url string; headers Headers; }; url { pathname string; href string; }; params {}; }' is missing the following properties from type 'APIContext<Record<string, any>, Record<string, string | undefined>>' site, generator, props, redirect, and 8 more. ❯ test/server/middleware.test.ts:169:9
vi.fn(() => nextResult),
);

expect(startSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'GET' }), expect.any(Function));
});

it('keeps the parameterized route as the span name', async () => {
const middleware = handleRequest();
const ctx = {
...DYNAMIC_REQUEST_CONTEXT,
request: { method: 'GET', url: '/users/123/details', headers: new Headers() },
params: { id: '123' },
url: new URL('https://myDomain.io/users/123/details'),
};

// @ts-expect-error, a partial ctx object is fine here

Check failure on line 185 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Unit Tests

Unhandled error

TypeCheckError: Unused '@ts-expect-error' directive. ❯ test/server/middleware.test.ts:185:7

Check failure on line 185 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (26) Unit Tests

Unhandled error

TypeCheckError: Unused '@ts-expect-error' directive. ❯ test/server/middleware.test.ts:185:7

Check failure on line 185 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (20.19) Unit Tests

Unhandled error

TypeCheckError: Unused '@ts-expect-error' directive. ❯ test/server/middleware.test.ts:185:7

Check failure on line 185 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Unit Tests

Unhandled error

TypeCheckError: Unused '@ts-expect-error' directive. ❯ test/server/middleware.test.ts:185:7
await middleware(
ctx,

Check failure on line 187 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Unit Tests

Unhandled error

TypeCheckError: Type '{ clientAddress string; request { method string; url string; headers Headers; }; params { id string; }; url URL; }' is missing the following properties from type 'APIContext<Record<string, any>, Record<string, string | undefined>>' site, generator, props, redirect, and 8 more. ❯ test/server/middleware.test.ts:187:9

Check failure on line 187 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (26) Unit Tests

Unhandled error

TypeCheckError: Type '{ clientAddress string; request { method string; url string; headers Headers; }; params { id string; }; url URL; }' is missing the following properties from type 'APIContext<Record<string, any>, Record<string, string | undefined>>' site, generator, props, redirect, and 8 more. ❯ test/server/middleware.test.ts:187:9

Check failure on line 187 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (20.19) Unit Tests

Unhandled error

TypeCheckError: Type '{ clientAddress string; request { method string; url string; headers Headers; }; params { id string; }; url URL; }' is missing the following properties from type 'APIContext<Record<string, any>, Record<string, string | undefined>>' site, generator, props, redirect, and 8 more. ❯ test/server/middleware.test.ts:187:9

Check failure on line 187 in packages/astro/test/server/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Unit Tests

Unhandled error

TypeCheckError: Type '{ clientAddress string; request { method string; url string; headers Headers; }; params { id string; }; url URL; }' is missing the following properties from type 'APIContext<Record<string, any>, Record<string, string | undefined>>' site, generator, props, redirect, and 8 more. ❯ test/server/middleware.test.ts:187:9
vi.fn(() => nextResult),
);

expect(startSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({ name: 'GET /users/[id]/details' }),
expect.any(Function),
);
});
});

it("sets source route if the url couldn't be decoded correctly", async () => {
const middleware = handleRequest();
const ctx = {
Expand Down
8 changes: 7 additions & 1 deletion packages/bun/src/integrations/bunserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
getClient,
getUrlFragment,
getUrlQuery,
hasSpanStreamingEnabled,
httpHeadersToSpanAttributes,
HTTP_SPAN_NAME_FALLBACK,
isURLObjectRelative,
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD,
Expand Down Expand Up @@ -246,7 +248,11 @@ function wrapRequestHandler<T extends RouteHandler = RouteHandler>(
{
attributes,
op: 'http.server',
name: `${request.method} ${routeName}`,
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL path.
name:
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'route' || !client || !hasSpanStreamingEnabled(client)
? `${request.method} ${routeName}`
: request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK,
},
async span => {
try {
Expand Down
11 changes: 10 additions & 1 deletion packages/cloudflare/src/wrapRequestHandlerWithInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import {
captureException,
continueTrace,
getHttpSpanDetailsFromUrlObject,
hasSpanStreamingEnabled,
httpHeadersToSpanAttributes,
HTTP_SPAN_NAME_FALLBACK,
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
setHttpStatus,
startSpanManual,
winterCGHeadersToDict,
Expand Down Expand Up @@ -72,14 +75,20 @@ export function wrapRequestHandlerWithInit(
isolationScope.setClient(client);

const urlObject = parseStringToURLObject(request.url);
const [name, attributes] = getHttpSpanDetailsFromUrlObject(
const [rawName, attributes] = getHttpSpanDetailsFromUrlObject(
urlObject,
'server',
'auto.http.cloudflare',
request,
undefined,
client,
);
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
// A `route` source means the name already is (e.g. the `/` path), so it is kept as-is.
const name =
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'route' || !client || !hasSpanStreamingEnabled(client)
? rawName
: request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK;

const contentLength = request.headers.get('content-length');
if (contentLength) {
Expand Down
30 changes: 30 additions & 0 deletions packages/cloudflare/test/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@ describe('withSentry', () => {
vi.clearAllMocks();
});

describe('with span streaming enabled', () => {
async function segmentSpanNameFor(url: string): Promise<string | undefined> {
let spanName: string | undefined;

await wrapRequestHandler(
{
options: { ...MOCK_OPTIONS, traceLifecycle: 'stream', tracesSampleRate: 1 },
request: new Request(url),
context: createMockExecutionContext(),
},
() => {
// Read the name while the request is in flight: the gate applies at span start.
const activeSpan = SentryCore.getActiveSpan();
spanName = activeSpan ? SentryCore.spanToJSON(SentryCore.getRootSpan(activeSpan)).name : undefined;
return new Response('test');
},
);

return spanName;
}

test('names a span without a resolvable route after the request method', async () => {
expect(await segmentSpanNameFor('https://example.com/users/42')).toBe('GET');
});

test('keeps the root path, which is already low cardinality', async () => {
expect(await segmentSpanNameFor('https://example.com/')).toBe('GET /');
});
});

test('passes through the response from the handler', async () => {
const response = new Response('test');
const result = await wrapRequestHandler(
Expand Down
11 changes: 10 additions & 1 deletion packages/deno/src/wrap-deno-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {
continueTrace,
getClient,
getHttpSpanDetailsFromUrlObject,
hasSpanStreamingEnabled,
httpHeadersToSpanAttributes,
HTTP_SPAN_NAME_FALLBACK,
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
setHttpStatus,
startSpanManual,
winterCGHeadersToDict,
Expand Down Expand Up @@ -60,14 +63,20 @@ export const wrapDenoRequestHandler = <Addr extends Deno.Addr = Deno.Addr>(
}

const urlObject = parseStringToURLObject(request.url);
const [name, attributes] = getHttpSpanDetailsFromUrlObject(
const [rawName, attributes] = getHttpSpanDetailsFromUrlObject(
urlObject,
'server',
'auto.http.deno',
request,
undefined,
client,
);
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
// A `route` source means the name already is (e.g. the `/` path), so it is kept as-is.
const name =
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'route' || !hasSpanStreamingEnabled(client)
? rawName
: request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK;

const contentLength = request.headers.get('content-length');
assignIfSet(attributes, 'http.request.body.size', contentLength && parseInt(contentLength, 10));
Expand Down
11 changes: 10 additions & 1 deletion packages/elysia/src/withElysia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
captureException,
continueTrace,
getActiveSpan,
getClient,
getIsolationScope,
getRootSpan,
getTraceData,
Expand All @@ -18,6 +19,8 @@ import {
winterCGRequestToRequestData,
withIsolationScope,
filterCollectedUrl,
hasSpanStreamingEnabled,
HTTP_SPAN_NAME_FALLBACK,
} from '@sentry/core';
import type { AnyElysia, Elysia, ErrorContext, TraceHandler, TraceListener } from 'elysia';

Expand Down Expand Up @@ -201,10 +204,16 @@ export function withElysia<T extends AnyElysia>(app: T, options: ElysiaHandlerOp
baggage: request.headers.get('baggage'),
},
() => {
const client = getClient();
return startSpanManual(
{
op: 'http.server',
name: `${request.method} ${new URL(request.url).pathname}`,
// With span streaming, span names have to be low cardinality, so we can't fall back to the
// URL path. `updateRouteTransactionName` renames the span once Elysia resolves the route.
name:
client && hasSpanStreamingEnabled(client)
? request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK
: `${request.method} ${new URL(request.url).pathname}`,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN,
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
Expand Down
19 changes: 18 additions & 1 deletion packages/nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
/* eslint-disable import/export */
import {
applySdkMetadata,
getClient,
getGlobalScope,
getIsolationScope,
getRootSpan,
GLOBAL_OBJ,
hasSpanStreamingEnabled,
HTTP_SPAN_NAME_FALLBACK,
registerSpanErrorInstrumentation,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
Expand Down Expand Up @@ -34,7 +37,7 @@ import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetada
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
import { enhanceRunHandlerRootSpan } from './enhanceRunHandlerRootSpan';
import { SENTRY_KIND } from '@sentry/conventions/attributes';
import { HTTP_METHOD, HTTP_REQUEST_METHOD, SENTRY_KIND } from '@sentry/conventions/attributes';
import { MIDDLEWARE } from '@sentry/conventions/op';

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

dropMiddlewareTunnelRequests(span, spanAttributes);

// Next.js names the incoming-request span after the raw URL. With span streaming, span names have to
// be low cardinality, so we replace it here at span start; the `next.route` hoisting below renames it
// to `${method} ${route}` once Next.js reports a route.
if (isRootSpan && spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest') {
const client = getClient();
if (client && hasSpanStreamingEnabled(client)) {
// oxlint-disable-next-line typescript/no-deprecated
const method = spanAttributes[HTTP_REQUEST_METHOD] ?? spanAttributes[HTTP_METHOD];
createLiveRootSpanAdapter(span).setName(
(typeof method === 'string' ? method.toUpperCase() : '') || HTTP_SPAN_NAME_FALLBACK,
);
}
}

// Mark all spans generated by Next.js as 'auto' & server
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
Expand Down
18 changes: 18 additions & 0 deletions packages/nextjs/src/server/handleOnSpanStart.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE } from '@sentry/conventions/attributes';
import type { Span } from '@sentry/core';
import {
getClient,
getIsolationScope,
getRootSpan,
hasSpanStreamingEnabled,
HTTP_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
spanToJSON,
} from '@sentry/core';
import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes';
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests';
import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter';
import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan';
import { maybeEnhanceServerComponentSpanName } from '../common/utils/tracingUtils';
import { maybeStartCronCheckIn } from './vercelCronsMonitoring';
Expand All @@ -29,6 +33,20 @@ export function handleOnSpanStart(span: Span): void {

dropMiddlewareTunnelRequests(span, spanAttributes);

// Next.js names the incoming-request span after the raw URL. With span streaming, span names have to
// be low cardinality, so we replace it here at span start; the `next.route` hoisting below renames it
// to `${method} ${route}` once Next.js reports a route.
if (isRootSpan && spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest') {
const client = getClient();
if (client && hasSpanStreamingEnabled(client)) {
// eslint-disable-next-line typescript/no-deprecated
const method = spanAttributes[HTTP_REQUEST_METHOD] ?? spanAttributes[HTTP_METHOD];
createLiveRootSpanAdapter(span).setName(
(typeof method === 'string' ? method.toUpperCase() : '') || HTTP_SPAN_NAME_FALLBACK,
);
}
}

// What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted
// by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute.
if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') {
Expand Down
Loading
Loading