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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
traceLifecycle: 'stream',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { sendPortToRunner } from '@sentry-internal/node-integration-tests';
import http from 'http';

// A bare `node:http` server: no framework ever resolves a route, so the server span
// keeps whatever name it was given at span start.
const server = http.createServer((_request, response) => {
response.end('Hello Node.js Server!');
});

server.listen(0, () => {
sendPortToRunner(server.address().port);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

describe('httpServerSpans-streamed (no route)', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => {
test('names the server span after the request method, not the URL path', async () => {
const runner = createRunner()
.expect({
span: container => {
const serverSpan = container.items.find(
item =>
item.attributes['sentry.op']?.type === 'string' && item.attributes['sentry.op'].value === 'http.server',
);

expect(serverSpan).toBeDefined();
expect(serverSpan?.is_segment).toBe(true);
// Without a route the name must not carry the URL path.
expect(serverSpan?.name).toBe('GET');
expect(serverSpan?.attributes['sentry.source']).toEqual({ type: 'string', value: 'url' });

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (26) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [cjs]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (26) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [esm]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [cjs]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [esm]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (20.19) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [cjs]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (20.19) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [esm]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [cjs]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [esm]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) (TS 5.0) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [cjs]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15

Check failure on line 23 in dev-packages/node-integration-tests/suites/tracing/httpServerSpans-streamed-unrouted/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) (TS 5.0) Integration Tests

suites/tracing/httpServerSpans-streamed-unrouted/test.ts > httpServerSpans-streamed (no route) > names the server span after the request method, not the URL path [esm]

AssertionError: expected undefined to deeply equal { type: 'string', value: 'url' } - Expected: { "type": "string", "value": "url", } + Received: undefined ❯ span suites/tracing/httpServerSpans-streamed-unrouted/test.ts:23:61 ❯ expectSpanContainer utils/runner/createRunner.ts:803:5 ❯ assertExpectedEnvelope utils/runner/createRunner.ts:737:5 ❯ newEnvelope utils/runner/createRunner.ts:386:15 ❯ tryParseEnvelopeFromStdoutLine utils/runner/createRunner.ts:514:15 ❯ Socket.<anonymous> utils/runner/createRunner.ts:532:15
// The path is still available as an attribute, which is what `ignoreSpans`/`tracesSampler` match on.
expect(serverSpan?.attributes['url.path']).toEqual({ type: 'string', value: '/users/42' });
},
})
.start();

await runner.makeRequest('get', '/users/42');

await runner.completed();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ Sentry.init({
tracesSampleRate: 1.0,
transport: loggingTransport,
traceLifecycle: 'stream',
ignoreSpans: [/\/health/],
ignoreSpans: [{ attributes: { 'url.path': '/health' } }],
clientReportFlushInterval: 1_000,
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests';
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampler: ({ inheritOrSampleWith, name }) => {
if (name === 'GET /health') {
tracesSampler: ({ inheritOrSampleWith, attributes }) => {
// The span name is low cardinality with span streaming, so match on `url.path` instead.
if (attributes?.['url.path'] === '/health') {
return inheritOrSampleWith(0);
}
return inheritOrSampleWith(1);
Expand Down
29 changes: 27 additions & 2 deletions packages/core/src/integrations/express/patch-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* limitations under the License.
*/

import { SENTRY_OP } from '@sentry/conventions/attributes';
import { HTTP_ROUTE, SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes';
import { MIDDLEWARE } from '@sentry/conventions/op';
import { DEBUG_BUILD } from '../../debug-build';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
Expand All @@ -37,7 +37,7 @@ import { ROUTER_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames';
import { startSpanManual } from '../../tracing/trace';
import { debug } from '../../utils/debug-logger';
import type { SpanAttributes } from '../../types/span';
import { getActiveSpan } from '../../utils/spanUtils';
import { getActiveSpan, getRootSpan, spanToJSON } from '../../utils/spanUtils';
import { getStoredLayers, storeLayer } from './request-layer-store';
import {
type ExpressRequest,
Expand Down Expand Up @@ -162,6 +162,7 @@ export function patchLayer(
// type cast b/c Otel unfortunately types info.request as any :(
const method = req.method ? req.method.toUpperCase() : 'GET';
currentScope.setTransactionName(`${method} ${constructedRoute}`);
applyRouteToRootSpan(`${method} ${constructedRoute}`, constructedRoute);
}
} else {
DEBUG_BUILD && debug.warn('Isolation scope is still default isolation scope - skipping setting transactionName');
Expand Down Expand Up @@ -289,3 +290,27 @@ export function patchLayer(
value: layerHandlePatched,
});
}

/**
* Write the resolved route onto the root `http.server` span.
*
* With span streaming the root span starts out named after the request method only, because no route
* is known at that point. Unlike the Node SDK — which goes through `setHttpServerSpanRouteAttribute` —
* nothing else on this path renames it, so a routed request would otherwise keep the method-only name.
*/
function applyRouteToRootSpan(name: string, route: string): void {
const client = getClient();
if (!client || !hasSpanStreamingEnabled(client)) {
return;
}

const activeSpan = getActiveSpan();
const rootSpan = activeSpan && getRootSpan(activeSpan);
if (!rootSpan || spanToJSON(rootSpan).attributes[SENTRY_OP] !== 'http.server') {
return;
}

rootSpan.updateName(name);
rootSpan.setAttribute(HTTP_ROUTE, route);
rootSpan.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, 'route');
}
8 changes: 7 additions & 1 deletion packages/core/src/integrations/http/server-subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { recordRequestSession } from './record-request-session';
import { generateSpanId, generateTraceId } from '../../utils/propagationContext';
import { continueTrace, startSpanManual } from '../../tracing/trace';
import { getSpanStatusFromHttpCode, SPAN_STATUS_ERROR } from '../../tracing';
import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled';
import { HTTP_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
import { safeMathRandom } from '../../utils/randomSafeContext';
import type { SpanAttributes } from '../../types/span';
Expand Down Expand Up @@ -284,7 +286,11 @@ function buildServerSpanWrap(
const urlObj = parseStringToURLObject(fullUrl);
const httpTargetWithoutQueryFragment = urlObj ? urlObj.pathname : stripUrlQueryAndFragment(fullUrl);
const method = (request.method || 'GET').toUpperCase();
const name = `${method} ${httpTargetWithoutQueryFragment}`;
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL path.
// Route instrumentations rename the span to `${method} ${route}` once a route is known.
const name = hasSpanStreamingEnabled(client)
? request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK
: `${method} ${httpTargetWithoutQueryFragment}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routed spans keep method-only names

High Severity

With streaming enabled, http.server spans start named only after the request method, assuming route instrumentations later rename them to METHOD /route. Native Express and similar integrations only call setTransactionName on the isolation scope and never update the span, so routed requests keep a method-only name.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 75d96fd. Configure here.

const headers = request.headers;
const userAgent = headers['user-agent'];
const ips = headers['x-forwarded-for'];
Expand Down
68 changes: 67 additions & 1 deletion packages/core/test/lib/integrations/express/patch-layer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ vi.mock('../../../../src/defaultScopes', () => ({

const mockSpans: MockSpan[] = [];
beforeEach(() => (mockSpans.length = 0));
beforeEach(() => (transactionNames.length = 0));
class MockSpan {
ended = false;
status: { code: number; message: string } = { code: 0, message: 'OK' };
Expand Down Expand Up @@ -131,12 +132,34 @@ const checkSpans = (expectations: Partial<MockSpanJSON>[]) => {
};

let hasActiveSpan = true;
const parentSpan = {};
// Stands in for the root `http.server` span so the route-to-root-span write can be asserted.
const parentSpan = {
name: 'GET',
attributes: { 'sentry.op': 'http.server' } as Record<string, unknown>,
updateName(name: string) {
this.name = name;
return this;
},
setAttribute(key: string, value: unknown) {
this.attributes[key] = value;
return this;
},
};
beforeEach(() => {
parentSpan.name = 'GET';
parentSpan.attributes = { 'sentry.op': 'http.server' };
});
vi.mock('../../../../src/utils/spanUtils', async () => ({
...(await import('../../../../src/utils/spanUtils')),
getActiveSpan() {
return hasActiveSpan ? parentSpan : undefined;
},
getRootSpan(span: unknown) {
return span;
},
spanToJSON(span: { attributes?: Record<string, unknown> }) {
return { attributes: span.attributes ?? {} };
},
}));

vi.mock('../../../../src/tracing', () => ({
Expand Down Expand Up @@ -367,6 +390,49 @@ describe('patchLayer', () => {
checkSpans([]);
});

it('writes the resolved route onto the root http.server span when span streaming is enabled', () => {
// Regression guard: with streaming the root span starts named `GET`, and nothing else on this
// path renames it — a routed request would otherwise keep the method-only name.
spanStreamingEnabled = true;

const req = Object.assign(new EventEmitter(), {
originalUrl: '/a/b/c/layerPath',
method: 'get',
}) as unknown as ExpressRequest;
const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse;
const layer = { name: 'handle', handle: vi.fn() } as unknown as ExpressLayer;

storeLayer(req, 'a');
storeLayer(req, '/:boo');

patchLayer(() => ({}), layer);
layer.handle(req, res);

expect(parentSpan.name).toBe('GET a/:boo');
expect(parentSpan.attributes['http.route']).toBe('a/:boo');
expect(parentSpan.attributes['sentry.segment.name.source']).toBe('route');
});

it('leaves the root span name alone without span streaming', () => {
spanStreamingEnabled = false;

const req = Object.assign(new EventEmitter(), {
originalUrl: '/a/b/c/layerPath',
method: 'get',
}) as unknown as ExpressRequest;
const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse;
const layer = { name: 'handle', handle: vi.fn() } as unknown as ExpressLayer;

storeLayer(req, 'a');
storeLayer(req, '/:boo');

patchLayer(() => ({}), layer);
layer.handle(req, res);

expect(parentSpan.name).toBe('GET');
expect(parentSpan.attributes['http.route']).toBeUndefined();
});

it('sets tx name in isolation scope', async () => {
DEBUG_BUILD = true;
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type { AddressInfo } from 'node:net';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { getIsolationScope } from '../../../../src/currentScopes';
import { Scope } from '../../../../src/scope';
import { spanToJSON } from '../../../../src/utils/spanUtils';
import { setCurrentClient } from '../../../../src/sdk';
import { HTTP_ON_SERVER_REQUEST } from '../../../../src/integrations/http/constants';
import { getHttpServerSubscriptions } from '../../../../src/integrations/http/server-subscription';
Expand Down Expand Up @@ -57,7 +59,7 @@ describe('getHttpServerSubscriptions', () => {

async function makeRequest(
path: string,
method: 'GET' | 'HEAD' | 'OPTIONS' = 'GET',
method: 'GET' | 'HEAD' | 'OPTIONS' | 'POST' = 'GET',
extraHeaders: Record<string, string> = {},
): Promise<void> {
const { port } = server.address() as AddressInfo;
Expand Down Expand Up @@ -321,4 +323,46 @@ describe('getHttpServerSubscriptions', () => {
const transaction = await waitForTransaction();
expect(transaction.transaction).toBe('GET /now-traced');
});

describe('with span streaming enabled', () => {
let streamingClient: TestClient;

beforeEach(() => {
streamingClient = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1, traceLifecycle: 'stream' }));
setCurrentClient(streamingClient);
streamingClient.init();
getIsolationScope().setClient(streamingClient);
});

async function startedSpanName(path: string, method: 'GET' | 'POST' = 'GET'): Promise<string> {
let spanName: string | undefined;
streamingClient.on('spanStart', span => {
spanName ??= spanToJSON(span).name;
});

server = http.createServer((_req, res) => res.end('ok'));
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', () => resolve()));
instrument(true);

await makeRequest(path, method);
await vi.waitUntil(() => spanName !== undefined, { timeout: 1000, interval: 10 });
return spanName!;
}

it('names the span after the request method instead of the URL path', async () => {
expect(await startedSpanName('/users/42?foo=bar')).toBe('GET');
});

it('keeps the method distinct per request', async () => {
expect(await startedSpanName('/users/42', 'POST')).toBe('POST');
});

it('keeps the raw URL path as the scope transaction name', async () => {
const setTransactionName = vi.spyOn(Scope.prototype, 'setTransactionName');

expect(await startedSpanName('/users/42?foo=bar')).toBe('GET');

expect(setTransactionName).toHaveBeenCalledWith('GET /users/42');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ import {
getUrlQuery,
filterCollectedUrl,
filterCollectedUrlQuery,
hasSpanStreamingEnabled,
HTTP_SPAN_NAME_FALLBACK,
} from '@sentry/core';
import { DEBUG_BUILD } from '../../debug-build';
import type { NodeClient } from '../../sdk/client';
Expand Down Expand Up @@ -152,15 +154,20 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions

const scheme = fullUrl.startsWith('https') ? 'https' : 'http';

const method = normalizedRequest.method || request.method?.toUpperCase() || 'GET';
const requestMethod = normalizedRequest.method || request.method?.toUpperCase();
const method = requestMethod || 'GET';
const httpTargetWithoutQueryFragment = urlObj ? urlObj.pathname : stripUrlQueryAndFragment(fullUrl);
const bestEffortTransactionName = `${method} ${httpTargetWithoutQueryFragment}`;

const query = getUrlQuery(urlObj?.search);
const fragment = getUrlFragment(urlObj?.hash);

const span = startInactiveSpan({
name: bestEffortTransactionName,
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL path.
// Route instrumentations rename the span to `${method} ${route}` once a route is known.
name: hasSpanStreamingEnabled(client)
? requestMethod || HTTP_SPAN_NAME_FALLBACK
: bestEffortTransactionName,
attributes: {
// Sentry specific attributes
[SENTRY_KIND]: 'server',
Expand Down
Loading