Skip to content
Open
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
@@ -1,7 +1,7 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';
import { sentryTest } from '../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
import { envelopeRequestParser, getFirstSentryEnvelopeRequest, waitForErrorRequest } from '../../../utils/helpers';

sentryTest('httpContextIntegration captures user-agent and referrer', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });
Expand All @@ -23,3 +23,15 @@ sentryTest('httpContextIntegration captures user-agent and referrer', async ({ g
url: expect.any(String),
});
});

sentryTest('httpContextIntegration filters sensitive query params in the URL', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

const errorEventPromise = waitForErrorRequest(page);

await page.goto(`${url}?jwt=sensitive-jwt-value&page=1`);

const errorEvent = envelopeRequestParser(await errorEventPromise);

expect(errorEvent.request?.url).toBe(`${url}?jwt=[Filtered]&page=1`);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<img src="https://sentry-test-site.example/path/to/image.svg?token=sensitive-token-value&size=2" />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';

sentryTest(
'filters sensitive query params in the pageload transaction URL and span descriptions',
async ({ getLocalTestUrl, page }) => {
sentryTest.skip(shouldSkipTracingTest());

await page.route('https://sentry-test-site.example/path/to/image.svg?*', route =>
route.fulfill({ body: '<svg xmlns="http://www.w3.org/2000/svg"/>', contentType: 'image/svg+xml' }),
);

const url = await getLocalTestUrl({ testDir: __dirname });

const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload');
await page.goto(`${url}?jwt=sensitive-jwt-value&page=1`);
const eventData = envelopeRequestParser<Event>(await pageloadRequestPromise);

expect(eventData.request?.url).toBe(`${url}?jwt=[Filtered]&page=1`);
expect(eventData.spans?.find(({ op }) => op === 'browser.request')?.description).toBe(
`${url}?jwt=[Filtered]&page=1`,
);
expect(eventData.spans?.find(({ op }) => op === 'resource.img')?.description).toBe(
'https://sentry-test-site.example/path/to/image.svg?token=[Filtered]&size=2',
);

const serializedEvent = JSON.stringify(eventData);
expect(serializedEvent).not.toContain('sensitive-jwt-value');
expect(serializedEvent).not.toContain('sensitive-token-value');
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
</head>
<body>
<div id="content"></div>
<img src="https://sentry-test-site.example/my/image.png" />
<img src="https://sentry-test-site.example/my/image.png?token=sensitive-token-value&v=1" />
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ sentryTest('captures LCP vitals with element details', async ({ browserName, get
}

page.route('**', route => route.continue());
page.route('**/my/image.png', async (route: Route) => {
page.route('**/my/image.png?*', async (route: Route) => {
return route.fulfill({
path: `${__dirname}/assets/sentry-logo-600x179.png`,
});
Expand All @@ -26,6 +26,9 @@ sentryTest('captures LCP vitals with element details', async ({ browserName, get
expect(eventData.contexts?.trace?.data?.['lcp.size']).toBeGreaterThan(0);
expect(eventData.contexts?.trace?.data?.['lcp.loadTime']).toBeGreaterThan(0);
expect(eventData.contexts?.trace?.data?.['lcp.renderTime']).toBeGreaterThan(0);
expect(eventData.contexts?.trace?.data?.['lcp.url']).toBe(
'https://sentry-test-site.example/my/image.png?token=[Filtered]&v=1',
);

// The LCP value should be the renderTime because the renderTime is set
expect(eventData.measurements?.lcp?.value).toBeCloseTo(eventData.contexts?.trace?.data?.['lcp.renderTime']);
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/browser-integration-tests/utils/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const sentryTest = base.extend<TestFixtures>({
}

await page.route(`${TEST_HOST}/*.*`, route => {
const file = route.request().url().split('/').pop();
const file = new URL(route.request().url()).pathname.split('/').pop();
const filePath = path.resolve(tmpDir, `./${file}`);

return fs.existsSync(filePath)
Expand Down
15 changes: 9 additions & 6 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ function _addPerformanceNavigationTiming(
}
startAndEndSpan(span, timeOrigin + msToSec(start), timeOrigin + msToSec(end), {
op: `browser.${name}`,
name: entry.name,
name: filterCollectedUrl(entry.name),
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
...(event === 'redirect' && entry.redirectCount != null ? { 'http.redirect_count': entry.redirectCount } : {}),
Expand Down Expand Up @@ -718,17 +718,19 @@ function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin:
// In this case, ie. when the document request hasn't finished yet, `entry.responseEnd` will be 0.
// In order not to produce faulty spans, where the end timestamp is before the start timestamp, we will only collect
// these spans when the responseEnd value is available. The backend (Relay) would drop the entire span if it contained faulty spans.
const filteredUrl = filterCollectedUrl(entry.name);

startAndEndSpan(span, requestStartTimestamp, responseEndTimestamp, {
op: 'browser.request',
name: entry.name,
name: filteredUrl,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
});

startAndEndSpan(span, responseStartTimestamp, responseEndTimestamp, {
op: 'browser.response',
name: entry.name,
name: filteredUrl,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
Expand Down Expand Up @@ -776,7 +778,8 @@ export function _addResourceSpans(

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

attributes[URL_FULL] = filterCollectedUrl(resourceUrl);
const filteredUrl = filterCollectedUrl(resourceUrl);
attributes[URL_FULL] = filteredUrl;

_setResourceRequestAttributes(entry, attributes, [
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStatus
Expand All @@ -799,7 +802,7 @@ export function _addResourceSpans(
const endTimestamp = startTimestamp + duration;

startAndEndSpan(span, startTimestamp, endTimestamp, {
name: resourceUrl.replace(WINDOW.location.origin, ''),
name: filteredUrl.replace(WINDOW.location.origin, ''),
op,
attributes: attributesWithResourceTiming,
});
Expand Down Expand Up @@ -873,7 +876,7 @@ function _setWebVitalAttributes(span: Span, options: AddWebVitalsToSpanOptions):

if (_lcpEntry.url) {
// Trim URL to the first 200 characters.
span.setAttribute('lcp.url', _lcpEntry.url.trim().slice(0, 200));
span.setAttribute('lcp.url', filterCollectedUrl(_lcpEntry.url.trim()).slice(0, 200));
}

if (_lcpEntry.loadTime != null) {
Expand Down
3 changes: 2 additions & 1 deletion packages/browser-utils/src/metrics/lcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Client, SpanAttributes } from '@sentry/core';
import {
browserPerformanceTimeOrigin,
debug,
filterCollectedUrl,
getCurrentScope,
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
Expand Down Expand Up @@ -90,7 +91,7 @@ export function _sendStandaloneLcpSpan(
entry.element && (attributes['lcp.element'] = htmlTreeAsString(entry.element));
entry.id && (attributes['lcp.id'] = entry.id);

entry.url && (attributes['lcp.url'] = entry.url);
entry.url && (attributes['lcp.url'] = filterCollectedUrl(entry.url));

// loadTime is the time of LCP that's related to receiving the LCP element response..
entry.loadTime != null && (attributes['lcp.loadTime'] = entry.loadTime);
Expand Down
3 changes: 2 additions & 1 deletion packages/browser-utils/src/metrics/webVitalSpans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Client, Span, SpanAttributes } from '@sentry/core';
import {
browserPerformanceTimeOrigin,
debug,
filterCollectedUrl,
getActiveSpan,
getCurrentScope,
getRootSpan,
Expand Down Expand Up @@ -162,7 +163,7 @@ export function _sendLcpSpan(

entry?.element && (attributes['browser.web_vital.lcp.element'] = htmlTreeAsString(entry.element));
entry?.id && (attributes['browser.web_vital.lcp.id'] = entry.id);
entry?.url && (attributes['browser.web_vital.lcp.url'] = entry.url);
entry?.url && (attributes['browser.web_vital.lcp.url'] = filterCollectedUrl(entry.url));
entry?.loadTime != null && (attributes['browser.web_vital.lcp.load_time'] = entry.loadTime);
entry?.renderTime != null && (attributes['browser.web_vital.lcp.render_time'] = entry.renderTime);
entry?.size != null && (attributes['browser.web_vital.lcp.size'] = entry.size);
Expand Down
111 changes: 73 additions & 38 deletions packages/browser-utils/test/browser/browserMetrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,26 @@ describe('_addResourceSpans', () => {
expect(json.data['url.full']).toBe('https://example.com/assets/app.js?v=42#main');
});

it('filters sensitive query params in the description and url.full', () => {
const spans: Span[] = [];

getClient()?.on('spanEnd', span => {
spans.push(span);
});

const entry = mockPerformanceResourceTiming({
initiatorType: 'script',
nextHopProtocol: 'http/1.1',
});

_addResourceSpans(span, entry, 'https://example.com/assets/app.js?token=abc&v=42', 100, 23, 345);

expect(spans).toHaveLength(1);
const json = spanToJSON(spans[0]!);
expect(json.description).toBe('/assets/app.js?token=[Filtered]&v=42');
expect(json.data['url.full']).toBe('https://example.com/assets/app.js?token=[Filtered]&v=42');
});

it('sets url.full to the full cross-origin URL', () => {
const spans: Span[] = [];

Expand Down Expand Up @@ -818,45 +838,46 @@ describe('_addNavigationSpans', () => {
client.init();
});

// entry taken from a real entry via browser dev tools
const entry: PerformanceNavigationTiming = {
name: 'https://santry.com/test',
entryType: 'navigation',
startTime: 0,
duration: 546.1000000014901,
initiatorType: 'navigation',
nextHopProtocol: 'h2',
workerStart: 0,
redirectStart: 7.5,
redirectEnd: 20.5,
redirectCount: 2,
fetchStart: 4.9000000059604645,
domainLookupStart: 4.9000000059604645,
domainLookupEnd: 4.9000000059604645,
connectStart: 4.9000000059604645,
secureConnectionStart: 4.9000000059604645,
connectEnd: 4.9000000059604645,
requestStart: 7.9000000059604645,
responseStart: 396.80000000447035,
responseEnd: 416.40000000596046,
transferSize: 14726,
encodedBodySize: 14426,
decodedBodySize: 67232,
responseStatus: 200,
serverTiming: [],
unloadEventStart: 0,
unloadEventEnd: 0,
domInteractive: 473.20000000298023,
domContentLoadedEventStart: 480.1000000014901,
domContentLoadedEventEnd: 480.30000000447035,
domComplete: 546,
loadEventStart: 546,
loadEventEnd: 546.1000000014901,
type: 'navigate',
activationStart: 0,
toJSON: () => ({}),
};

it('adds navigation spans based on the navigation performance entry', () => {
// entry taken from a real entry via browser dev tools
const entry: PerformanceNavigationTiming = {
name: 'https://santry.com/test',
entryType: 'navigation',
startTime: 0,
duration: 546.1000000014901,
initiatorType: 'navigation',
nextHopProtocol: 'h2',
workerStart: 0,
redirectStart: 7.5,
redirectEnd: 20.5,
redirectCount: 2,
fetchStart: 4.9000000059604645,
domainLookupStart: 4.9000000059604645,
domainLookupEnd: 4.9000000059604645,
connectStart: 4.9000000059604645,
secureConnectionStart: 4.9000000059604645,
connectEnd: 4.9000000059604645,
requestStart: 7.9000000059604645,
responseStart: 396.80000000447035,
responseEnd: 416.40000000596046,
transferSize: 14726,
encodedBodySize: 14426,
decodedBodySize: 67232,
responseStatus: 200,
serverTiming: [],
unloadEventStart: 0,
unloadEventEnd: 0,
domInteractive: 473.20000000298023,
domContentLoadedEventStart: 480.1000000014901,
domContentLoadedEventEnd: 480.30000000447035,
domComplete: 546,
loadEventStart: 546,
loadEventEnd: 546.1000000014901,
type: 'navigate',
activationStart: 0,
toJSON: () => ({}),
};
const spans: Span[] = [];

getClient()?.on('spanEnd', span => {
Expand Down Expand Up @@ -974,6 +995,20 @@ describe('_addNavigationSpans', () => {
]),
);
});

it('filters sensitive query params in span names', () => {
const spans: Span[] = [];

getClient()?.on('spanEnd', span => {
spans.push(span);
});

_addNavigationSpans(pageloadSpan, { ...entry, name: 'https://santry.com/test?jwt=baz&q=1' }, 999);

expect(spans.map(spanToJSON)).toEqual(
Array(9).fill(expect.objectContaining({ description: 'https://santry.com/test?jwt=[Filtered]&q=1' })),
);
});
});

describe('_setResourceRequestAttributes', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/browser-utils/test/metrics/lcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('_sendStandaloneLcpSpan', () => {
startTime: 100,
duration: 0,
id: 'image',
url: 'https://example.com/image.png',
url: 'https://example.com/image.png?token=abc&size=2',
size: 1234,
loadTime: 95,
renderTime: 100,
Expand All @@ -84,7 +84,7 @@ describe('_sendStandaloneLcpSpan', () => {
'sentry.report_event': 'navigation',
'lcp.element': '<img>',
'lcp.id': 'image',
'lcp.url': 'https://example.com/image.png',
'lcp.url': 'https://example.com/image.png?token=[Filtered]&size=2',
'lcp.loadTime': 95,
'lcp.renderTime': 100,
'lcp.size': 1234,
Expand Down
4 changes: 2 additions & 2 deletions packages/browser-utils/test/metrics/webVitalSpans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ describe('_sendLcpSpan', () => {
const mockEntry = {
element: { tagName: 'img' } as Element,
id: 'hero',
url: 'https://example.com/hero.jpg',
url: 'https://example.com/hero.jpg?token=abc&size=2',
loadTime: 100,
renderTime: 150,
size: 50000,
Expand All @@ -251,7 +251,7 @@ describe('_sendLcpSpan', () => {
'sentry.pageload.span_id': 'pageload-123',
'browser.web_vital.lcp.element': '<img>',
'browser.web_vital.lcp.id': 'hero',
'browser.web_vital.lcp.url': 'https://example.com/hero.jpg',
'browser.web_vital.lcp.url': 'https://example.com/hero.jpg?token=[Filtered]&size=2',
'browser.web_vital.lcp.load_time': 100,
'browser.web_vital.lcp.render_time': 150,
'browser.web_vital.lcp.size': 50000,
Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/integrations/httpcontext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { HTTP_REQUEST_HEADER_KEY_BASE, SENTRY_OP, URL_FULL, USER_AGENT_ORIGINAL
export const httpContextIntegration = defineIntegration(() => {
return {
name: 'HttpContext' as const,
preprocessEvent(event) {
preprocessEvent(event, _hint, client) {
// if none of the information we want exists, don't bother
if (!WINDOW.navigator && !WINDOW.location && !WINDOW.document) {
return;
Expand All @@ -23,6 +23,7 @@ export const httpContextIntegration = defineIntegration(() => {

event.request = {
...reqData,
url: filterCollectedUrl(reqData.url, client),
...event.request,
headers,
};
Expand Down
Loading
Loading