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
@@ -0,0 +1,5 @@
Sentry.startSpan({ name: 'parent-span', op: 'test' }, () => {
Sentry.startSpan({ name: 'child-span', op: 'test-child' }, () => {
// noop
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../utils/helpers';
import { URL_FULL, USER_AGENT_ORIGINAL } from '@sentry/conventions/attributes';
import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils';

sentryTest('httpContextIntegration captures url, user-agent, and referer', async ({ getLocalTestUrl, page }) => {
Expand All @@ -15,8 +16,8 @@ sentryTest('httpContextIntegration captures url, user-agent, and referer', async

const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload');

expect(pageloadSpan!.attributes['url.full']).toEqual({ type: 'string', value: expect.any(String) });
expect(pageloadSpan!.attributes['http.request.header.user_agent']).toEqual({
expect(pageloadSpan!.attributes[URL_FULL]).toEqual({ type: 'string', value: expect.any(String) });
expect(pageloadSpan!.attributes[USER_AGENT_ORIGINAL]).toEqual({
type: 'string',
value: expect.any(String),
});
Expand All @@ -25,3 +26,25 @@ sentryTest('httpContextIntegration captures url, user-agent, and referer', async
value: 'https://sentry.io/',
});
});

sentryTest(
'httpContextIntegration only attaches the user agent to non-segment spans',
async ({ getLocalTestUrl, page }) => {
sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });

const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => s.name === 'child-span'));

await page.goto(url, { referer: 'https://sentry.io/' });

const spans = await spansPromise;

const childSpan = spans.find(s => s.name === 'child-span');

expect(childSpan!.is_segment).toBe(false);
expect(childSpan!.attributes[USER_AGENT_ORIGINAL]).toEqual({ type: 'string', value: expect.any(String) });
// The document URL and referer only belong on the segment span.
expect(childSpan!.attributes[URL_FULL]).toBeUndefined();
expect(childSpan!.attributes['http.request.header.referer']).toBeUndefined();
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SENTRY_SDK_NAME,
SENTRY_SDK_VERSION,
SENTRY_TRACE_LIFECYCLE,
USER_AGENT_ORIGINAL,
} from '@sentry/conventions/attributes';

sentryTest(
Expand Down Expand Up @@ -107,6 +108,10 @@ sentryTest(
type: 'string',
value: 'stream',
},
[USER_AGENT_ORIGINAL]: {
type: 'string',
value: expect.any(String),
},
},
end_timestamp: expect.any(Number),
is_segment: false,
Expand Down Expand Up @@ -147,6 +152,10 @@ sentryTest(
type: 'string',
value: 'stream',
},
[USER_AGENT_ORIGINAL]: {
type: 'string',
value: expect.any(String),
},
},
end_timestamp: expect.any(Number),
is_segment: false,
Expand Down Expand Up @@ -191,6 +200,10 @@ sentryTest(
type: 'string',
value: 'stream',
},
[USER_AGENT_ORIGINAL]: {
type: 'string',
value: expect.any(String),
},
},
end_timestamp: expect.any(Number),
is_segment: false,
Expand All @@ -215,7 +228,7 @@ sentryTest(
type: 'string',
value: expect.any(String),
},
'http.request.header.user_agent': {
[USER_AGENT_ORIGINAL]: {
type: 'string',
value: expect.any(String),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SENTRY_SDK_NAME,
SENTRY_SDK_VERSION,
SENTRY_TRACE_LIFECYCLE,
USER_AGENT_ORIGINAL,
} from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
Expand Down Expand Up @@ -61,7 +62,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
type: 'string',
value: expect.any(String),
},
'http.request.header.user_agent': {
[USER_AGENT_ORIGINAL]: {
type: 'string',
value: expect.any(String),
},
Expand Down Expand Up @@ -137,6 +138,10 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
type: 'string',
value: 'ui.interaction.click',
},
[USER_AGENT_ORIGINAL]: {
type: 'string',
value: expect.any(String),
},
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
type: 'string',
value: 'auto.ui.browser.metrics',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
import { SENTRY_TRACE_LIFECYCLE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes';
import { SENTRY_TRACE_LIFECYCLE, URL_FULL, URL_PATH, USER_AGENT_ORIGINAL } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
import {
Expand Down Expand Up @@ -88,7 +88,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow
type: 'string',
value: expect.any(String),
},
'http.request.header.user_agent': {
[USER_AGENT_ORIGINAL]: {
type: 'string',
value: expect.any(String),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SENTRY_TRACE_LIFECYCLE,
URL_FULL,
URL_PATH,
USER_AGENT_ORIGINAL,
} from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
Expand Down Expand Up @@ -81,7 +82,7 @@ sentryTest(
type: 'string',
value: expect.any(String),
},
'http.request.header.user_agent': {
[USER_AGENT_ORIGINAL]: {
type: 'string',
value: expect.any(String),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

test('Sends an error event to Sentry', async ({ request }) => {
// The thrown error is reported twice: once via the h3 tracing channel and once via Nitro's `error`
// hook (which sees it wrapped in an `HTTPError`). Match on the mechanism so we deterministically
// await the event under test instead of whichever arrives first.
const errorEventPromise = waitForError('nitro-3', event => {
return !event.type && !!event.exception?.values?.some(v => v.value === 'This is a test error');
return (
!event.type &&
!!event.exception?.values?.some(
v => v.value === 'This is a test error' && v.mechanism?.type === 'auto.http.nitro.onTraceError',
)
);
});

await request.get('/api/test-error').catch(() => {
Expand Down
23 changes: 14 additions & 9 deletions packages/browser/src/integrations/httpcontext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineIntegration, safeSetSpanJSONAttributes, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core/browser';
import { defineIntegration, safeSetSpanJSONAttributes } from '@sentry/core/browser';
import { getHttpRequestData, WINDOW } from '../helpers';
import { URL_FULL } from '@sentry/conventions/attributes';
import { HTTP_REQUEST_HEADER_KEY_BASE, SENTRY_OP, URL_FULL, USER_AGENT_ORIGINAL } from '@sentry/conventions/attributes';

/**
* Collects information about HTTP request headers and
Expand All @@ -27,9 +27,8 @@ export const httpContextIntegration = defineIntegration(() => {
headers,
};
},
processSegmentSpan(span) {
const spanOp = span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP];

processSpan(span) {
// if none of the information we want exists, don't bother
if (!WINDOW.navigator && !WINDOW.location && !WINDOW.document) {
return;
Expand All @@ -38,11 +37,17 @@ export const httpContextIntegration = defineIntegration(() => {
const reqData = getHttpRequestData();

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,
'http.request.header.user_agent': reqData.headers['User-Agent'],
'http.request.header.referer': reqData.headers['Referer'],
// This attribute is used by the "Filter out events from legacy browsers and crawlers" features on the Sentry backend.
// Therefore, it's set on every span.
[USER_AGENT_ORIGINAL]: reqData.headers['User-Agent'],

// These attributes, we only need on the segment span (analogous to the `request` context for events)
...(span.is_segment && {
// 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]: span.attributes?.[SENTRY_OP] !== 'http.client' ? reqData.url : undefined,
[`${HTTP_REQUEST_HEADER_KEY_BASE}.referer`]: reqData.headers['Referer'],
}),
});
},
};
Expand Down
32 changes: 26 additions & 6 deletions packages/browser/test/integrations/httpcontext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type { StreamedSpanJSON } from '@sentry/core';
import { getDefaultBrowserClientOptions } from '../helper/browser-client-options';
import { BrowserClient } from '../../src/client';

const USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';

describe('httpContextIntegration', () => {
globalThis.navigator = {
userAgent:
Expand All @@ -24,43 +27,60 @@ describe('httpContextIntegration', () => {
const integration = httpContextIntegration();

const span: Partial<StreamedSpanJSON> = {
is_segment: true,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
},
};

const browserClient = new BrowserClient(getDefaultBrowserClientOptions());

integration.processSegmentSpan!(span as StreamedSpanJSON, browserClient);
integration.processSpan!(span as StreamedSpanJSON, browserClient);

expect(span.attributes).not.toHaveProperty('url.full');
expect(span.attributes).toEqual({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
'http.request.header.referer': 'https://example.com',
'http.request.header.user_agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'user_agent.original': USER_AGENT,
});
});

it('attaches url.full to non-http.client segment spans', () => {
const integration = httpContextIntegration();

const span: Partial<StreamedSpanJSON> = {
is_segment: true,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
},
};

const browserClient = new BrowserClient(getDefaultBrowserClientOptions());

integration.processSegmentSpan!(span as StreamedSpanJSON, browserClient);
integration.processSpan!(span as StreamedSpanJSON, browserClient);

expect(span.attributes).toEqual({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
'http.request.header.referer': 'https://example.com',
'http.request.header.user_agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'user_agent.original': USER_AGENT,
'url.full': 'https://example.com',
});
});

it('only attaches the user agent to non-segment spans', () => {
const integration = httpContextIntegration();

const span: Partial<StreamedSpanJSON> = {
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.click',
},
};

integration.processSpan!(span as StreamedSpanJSON, new BrowserClient(getDefaultBrowserClientOptions()));

expect(span.attributes).toEqual({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.click',
'user_agent.original': USER_AGENT,
});
});
});
Loading