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
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ test.describe('nested SSR routes (client, server, server request)', () => {
'sentry.op': 'http.client',
'sentry.origin': 'auto.http.fetch',
url: expect.stringContaining('/api/user/myUsername123.json'),
'http.url': 'http://localhost:3030/api/user/myUsername123.json',
'url.full': 'http://localhost:3030/api/user/myUsername123.json',
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ test('Should trace outgoing fetch requests inside middleware and create breadcru
type: 'fetch',
url: 'http://localhost:3030/',
'http.url': 'http://localhost:3030/',
'url.full': 'http://localhost:3030/',
'server.address': 'localhost:3030',
'sentry.op': 'http.client',
'sentry.origin': 'auto.http.wintercg_fetch',
Expand Down
4 changes: 2 additions & 2 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
isPrimitive,
parseUrl,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_URL_FULL,
setMeasurement,
spanToJSON,
stringMatchesSomePattern,
Expand All @@ -30,6 +29,7 @@ import { getActivationStart } from './web-vitals/lib/getActivationStart';
import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry';
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
import { DEBUG_BUILD } from '../debug-build';
import { URL_FULL } from '@sentry/conventions/attributes';
interface NavigatorNetworkInformation {
readonly connection?: NetworkInformation;
}
Expand Down Expand Up @@ -775,7 +775,7 @@ export function _addResourceSpans(

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);

attributes[SEMANTIC_ATTRIBUTE_URL_FULL] = resourceUrl;
attributes[URL_FULL] = resourceUrl;

_setResourceRequestAttributes(entry, attributes, [
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStatus
Expand Down
5 changes: 2 additions & 3 deletions packages/browser/src/integrations/graphqlClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import {
isString,
SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_URL_FULL,
spanToJSON,
stringMatchesSomePattern,
} from '@sentry/core/browser';
import type { FetchHint, XhrHint } from '@sentry/browser-utils';
import { getBodyString, getFetchRequestArgBody, SENTRY_XHR_DATA_KEY } from '@sentry/browser-utils';
import { GRAPHQL_DOCUMENT } from '@sentry/conventions/attributes';
import { GRAPHQL_DOCUMENT, URL_FULL } from '@sentry/conventions/attributes';

interface GraphQLClientOptions {
endpoints: Array<string | RegExp>;
Expand Down Expand Up @@ -71,7 +70,7 @@ function _updateSpanWithGraphQLData(client: Client, options: GraphQLClientOption

// Fall back to `url` because fetch instrumentation only sets `http.url` for absolute URLs;
// relative URLs end up only in `url` (see `getFetchSpanAttributes` in packages/core/src/fetch.ts).
const httpUrl = spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL] || spanAttributes['http.url'] || spanAttributes['url'];
const httpUrl = spanAttributes[URL_FULL] || spanAttributes['http.url'] || spanAttributes['url'];
const httpMethod = spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || spanAttributes['http.method'];

if (!isString(httpUrl) || !isString(httpMethod)) {
Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/integrations/httpcontext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineIntegration, safeSetSpanJSONAttributes, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core/browser';
import { getHttpRequestData, WINDOW } from '../helpers';
import { URL_FULL } from '@sentry/conventions/attributes';

/**
* Collects information about HTTP request headers and
Expand Down Expand Up @@ -39,7 +40,7 @@ export const httpContextIntegration = defineIntegration(() => {
safeSetSpanJSONAttributes(span, {
// Coerce empty string to undefined so the helper's nullish check drops it,
// rather than writing an empty `url.full` attribute onto the span.
'url.full': spanOp !== 'http.client' ? reqData.url : undefined,
[URL_FULL]: spanOp !== 'http.client' ? reqData.url : undefined,
'http.request.header.user_agent': reqData.headers['User-Agent'],
'http.request.header.referer': reqData.headers['Referer'],
});
Expand Down
8 changes: 5 additions & 3 deletions packages/browser/src/tracing/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
} from '@sentry/browser-utils';
import type { BrowserClient } from '../client';
import { baggageHeaderHasSentryValues, createHeadersSafely, getFullURL, isPerformanceResourceTiming } from './utils';
import { HTTP_URL, URL_FULL } from '@sentry/conventions/attributes';

/** Options for Request Instrumentation */
export interface RequestInstrumentationOptions {
Expand Down Expand Up @@ -172,10 +173,11 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
const host = fullUrl ? parseUrl(fullUrl).host : undefined;
const sanitizedFullUrl = fullUrl ? stripDataUrlContent(fullUrl) : undefined;
createdSpan.setAttributes({
'http.url': sanitizedFullUrl,
// oxlint-disable-next-line typescript/no-deprecated
[HTTP_URL]: sanitizedFullUrl,
// `url.full` must match `http.url`. Setting it here ensures parentless `http.client`
// segment spans don't get `url.full` backfilled with the host page URL (see httpContextIntegration).
'url.full': sanitizedFullUrl,
[URL_FULL]: sanitizedFullUrl,
'server.address': host,
});

Expand Down Expand Up @@ -393,7 +395,7 @@ function xhrCallback(
'http.url': sanitizedFullUrl,
// `url.full` must match `http.url`. Setting it here ensures parentless `http.client`
// segment spans don't get `url.full` backfilled with the host page URL (see httpContextIntegration).
'url.full': sanitizedFullUrl,
[URL_FULL]: sanitizedFullUrl,
'server.address': parsedUrl?.host,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
Expand Down
25 changes: 23 additions & 2 deletions packages/browser/test/integrations/graphqlClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Client } from '@sentry/core/browser';
import { SentrySpan, spanToJSON } from '@sentry/core/browser';
import type { FetchHint, XhrHint } from '@sentry/browser-utils';
import { SENTRY_XHR_DATA_KEY } from '@sentry/browser-utils';
import { URL_FULL } from '@sentry/conventions/attributes';
import { describe, expect, test } from 'vitest';
import {
_getGraphQLOperation,
Expand Down Expand Up @@ -352,14 +353,15 @@ describe('GraphqlClient', () => {
extensions: {},
};

test('enriches http.client span for absolute URLs (http.url attribute)', () => {
test('enriches http.client span for absolute URLs', () => {
const handler = setupHandler([/\/graphql$/]);
const span = new SentrySpan({
name: 'POST http://localhost:4000/graphql',
op: 'http.client',
attributes: {
'http.method': 'POST',
'http.url': 'http://localhost:4000/graphql',
[URL_FULL]: 'http://localhost:4000/graphql',
url: 'http://localhost:4000/graphql',
},
});
Expand All @@ -371,9 +373,27 @@ describe('GraphqlClient', () => {
expect(json.data['graphql.document']).toBe(requestBody.query);
});

test('enriches http.client span when only url.full is present', () => {
const handler = setupHandler([/\/graphql$/]);
const span = new SentrySpan({
name: 'POST http://localhost:4000/graphql',
op: 'http.client',
attributes: {
'http.method': 'POST',
[URL_FULL]: 'http://localhost:4000/graphql',
},
});

handler(span, makeFetchHint('http://localhost:4000/graphql', requestBody));

const json = spanToJSON(span);
expect(json.description).toBe('POST http://localhost:4000/graphql (query GetHello)');
expect(json.data['graphql.document']).toBe(requestBody.query);
});

test('enriches http.client span for relative URLs (only url attribute)', () => {
const handler = setupHandler([/\/graphql$/]);
// Fetch instrumentation does NOT set http.url for relative URLs — only `url`.
// Fetch instrumentation does not set `http.url` or `url.full` for relative URLs.
const span = new SentrySpan({
name: 'POST /graphql',
op: 'http.client',
Expand Down Expand Up @@ -433,6 +453,7 @@ describe('GraphqlClient', () => {
attributes: {
'http.method': 'POST',
'http.url': 'http://localhost:4000/graphql',
[URL_FULL]: 'http://localhost:4000/graphql',
url: 'http://localhost:4000/graphql',
},
});
Expand Down
1 change: 1 addition & 0 deletions packages/bun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"dependencies": {
"@apm-js-collab/code-transformer-bundler-plugins": "^0.7.1",
"@sentry/core": "10.67.0",
"@sentry/conventions": "^0.16.0",
"@sentry/node": "10.67.0",
"@sentry/server-utils": "10.67.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/bun/src/integrations/bunserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
withIsolationScope,
} from '@sentry/core';
import type { ServeOptions } from 'bun';
import { URL_FULL } from '@sentry/conventions/attributes';

const INTEGRATION_NAME = 'BunServer' as const;

Expand Down Expand Up @@ -282,7 +283,7 @@ function getSpanAttributesFromParsedUrl(
attributes['url.path'] = parsedUrl.pathname;
}
if (!isURLObjectRelative(parsedUrl)) {
attributes['url.full'] = parsedUrl.href;
attributes[URL_FULL] = parsedUrl.href;
if (parsedUrl.port) {
attributes['url.port'] = parsedUrl.port;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HTTP_URL, URL_FULL } from '@sentry/conventions/attributes';
import { getClient } from './currentScopes';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes';
import { setHttpStatus, SPAN_STATUS_ERROR, spanIsIgnored, startInactiveSpan } from './tracing';
Expand Down Expand Up @@ -388,7 +389,9 @@ function getFetchSpanAttributes(
};
if (parsedUrl) {
if (!isURLObjectRelative(parsedUrl)) {
attributes['http.url'] = stripDataUrlContent(parsedUrl.href);
// oxlint-disable-next-line typescript/no-deprecated
attributes[HTTP_URL] = stripDataUrlContent(parsedUrl.href);
attributes[URL_FULL] = stripDataUrlContent(parsedUrl.href);
Comment thread
cursor[bot] marked this conversation as resolved.
attributes['server.address'] = parsedUrl.host;
}
if (parsedUrl.search) {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/integrations/requestdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { parseCookie } from '../utils/cookie';
import { httpHeadersToSpanAttributes } from '../utils/request';
import { getClientIPAddress, ipHeaderNames } from '../vendor/getIpAddress';
import { safeSetSpanJSONAttributes } from '../tracing/spans/captureSpan';
import { URL_FULL, URL_QUERY } from '@sentry/conventions/attributes';

interface RequestDataIncludeOptions {
cookies?: boolean;
Expand Down Expand Up @@ -137,15 +138,15 @@ function addNormalizedRequestDataToSpan(
const attributes: Record<string, unknown> = {};

if (requestData.url) {
attributes['url.full'] = requestData.url;
attributes[URL_FULL] = requestData.url;
}

if (requestData.method) {
attributes['http.request.method'] = requestData.method;
}

if (requestData.query_string) {
attributes['url.query'] = normalizeQueryString(requestData.query_string);
attributes[URL_QUERY] = normalizeQueryString(requestData.query_string);
}

safeSetSpanJSONAttributes(span, attributes);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/semanticAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ export const SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE = 'cache.item_size';

/** TODO: Remove these once we update to latest semantic conventions */
export const SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD = 'http.request.method';
/**
* @deprecated Use `URL_FULL` `@sentry/conventions/attributes` instead.
*/
export const SEMANTIC_ATTRIBUTE_URL_FULL = 'url.full';

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/utils/url.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { URL_FULL } from '@sentry/conventions/attributes';
import {
SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
SEMANTIC_ATTRIBUTE_URL_FULL,
} from '../semanticAttributes';
import type { SpanAttributes } from '../types/span';

Expand Down Expand Up @@ -188,7 +188,7 @@ export function getHttpSpanDetailsFromUrlObject(
}

if (!isURLObjectRelative(urlObject)) {
attributes[SEMANTIC_ATTRIBUTE_URL_FULL] = urlObject.href;
attributes[URL_FULL] = urlObject.href;
if (urlObject.port) {
attributes['url.port'] = urlObject.port;
}
Expand Down
44 changes: 44 additions & 0 deletions packages/core/test/lib/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { URL_FULL } from '@sentry/conventions/attributes';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { HandlerDataFetch } from '../../src';
import { _INTERNAL_getTracingHeadersForFetchRequest, instrumentFetchRequest } from '../../src/fetch';
Expand Down Expand Up @@ -444,6 +445,49 @@ describe('_INTERNAL_getTracingHeadersForFetchRequest', () => {
});

describe('instrumentFetchRequest', () => {
describe('span attributes', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('sets url.full for absolute URLs', () => {
const url = 'https://api.example.com/users/42?include=profile#bio';
const activeSpan = new SentryNonRecordingSpan();
const fetchSpan = new SentryNonRecordingSpan();
hasSpansEnabled.mockReturnValue(true);
vi.spyOn(spanUtils, 'getActiveSpan').mockReturnValue(activeSpan);
const startInactiveSpanSpy = vi.spyOn(tracing, 'startInactiveSpan').mockReturnValue(fetchSpan);

instrumentFetchRequest(
{
fetchData: { url, method: 'GET' },
args: [url],
startTimestamp: Date.now(),
},
() => true,
() => false,
{},
{ spanOrigin: 'auto.http.fetch' },
);

expect(startInactiveSpanSpy).toHaveBeenCalledWith({
name: 'GET https://api.example.com/users/42',
attributes: {
url,
type: 'fetch',
'http.method': 'GET',
'sentry.origin': 'auto.http.fetch',
'sentry.op': 'http.client',
'http.url': url,
[URL_FULL]: url,
'server.address': 'api.example.com',
'http.query': '?include=profile',
'http.fragment': '#bio',
},
});
});
});

describe('trace header span', () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import {
import type { Client, Span } from '@sentry/core';
import type { EmberRouterMain } from '../types';
import { getBackburner } from './performance';

const URL_FULL = 'url.full';
const URL_PATH = 'url.path';
const URL_TEMPLATE = 'url.template';
import { URL_FULL, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes';

type TransitionWithIntent = Transition & { intent?: { url?: string } };

Expand Down
1 change: 1 addition & 0 deletions packages/ember/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@embroider/macros": "^1.16.0",
"@sentry/browser": "10.67.0",
"@sentry/core": "10.67.0",
"@sentry/conventions": "^0.16.0",
"ember-auto-import": "^2.7.2",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.1.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/nextjs/src/server/vercelQueuesMonitoring.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { URL_FULL } from '@sentry/conventions/attributes';
import type { Span } from '@sentry/core';
import { getIsolationScope, spanToJSON } from '@sentry/core';

Expand Down Expand Up @@ -76,7 +77,7 @@ export function maybeEnrichQueueProducerSpan(span: Span): void {
const spanData = spanToJSON(span).data;

// http.client spans have url.full attribute
const urlFull = spanData?.['url.full'] as string | undefined;
const urlFull = spanData?.[URL_FULL] as string | undefined;
if (!urlFull) {
return;
}
Expand Down
11 changes: 3 additions & 8 deletions packages/node/src/integrations/http.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import type { RequestOptions } from 'node:http';
import type { HttpClientRequest, HttpIncomingMessage, HttpServerResponse, Span } from '@sentry/core';
import {
defineIntegration,
hasSpansEnabled,
SEMANTIC_ATTRIBUTE_URL_FULL,
stripDataUrlContent,
getRequestUrlFromClientRequest,
} from '@sentry/core';
import { defineIntegration, hasSpansEnabled, stripDataUrlContent, getRequestUrlFromClientRequest } from '@sentry/core';
import type {
NodeClient,
SentryHttpInstrumentationOptions,
HttpServerIntegrationOptions,
HttpServerSpansIntegrationOptions,
} from '@sentry/node-core';
import { httpServerIntegration, httpServerSpansIntegration, instrumentHttpOutgoingRequests } from '@sentry/node-core';
import { URL_FULL } from '@sentry/conventions/attributes';

const INTEGRATION_NAME = 'Http' as const;

Expand Down Expand Up @@ -210,7 +205,7 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
// TODO(v11): Update these to the Sentry semantic attributes.
// https://getsentry.github.io/sentry-conventions/attributes/
span.setAttribute('http.url', sanitizedUrl);
span.setAttribute(SEMANTIC_ATTRIBUTE_URL_FULL, sanitizedUrl);
span.setAttribute(URL_FULL, sanitizedUrl);
span.updateName(`${request.method || 'GET'} ${sanitizedUrl}`);
}
options.instrumentation?.requestHook?.(span, request);
Expand Down
Loading
Loading