Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
autoSessionTracking: false,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Fetch a data URL to verify that the span name and attributes are sanitized
// Data URLs are used for inline resources, e.g., Web Workers with inline scripts
const dataUrl = 'data:text/plain;base64,SGVsbG8gV29ybGQh';
fetch(dataUrl).catch(() => {
// Data URL fetch might fail in some browsers, but the span should still be created
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequestOnUrl } from '../../../../utils/helpers';

sentryTest('sanitizes data URLs in fetch span name and attributes', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

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

const req = await waitForTransactionRequestOnUrl(page, url);
const transactionEvent = envelopeRequestParser(req);

const requestSpans = transactionEvent.spans?.filter(({ op }) => op === 'http.client');

expect(requestSpans).toHaveLength(1);

const span = requestSpans?.[0];

expect(span?.description).toBe('GET <data:text/plain,base64>');

expect(span?.data).toMatchObject({
'http.method': 'GET',
url: '<data:text/plain,base64>',
type: 'fetch',
});

expect(span?.data?.['http.url']).toBe('<data:text/plain,base64>');
});
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({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
autoSessionTracking: false,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// XHR request to a data URL to verify that the span name and attributes are sanitized
const dataUrl = 'data:text/plain;base64,SGVsbG8gV29ybGQh';
const xhr = new XMLHttpRequest();
xhr.open('GET', dataUrl);
xhr.send();
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest('sanitizes data URLs in XHR span name and attributes', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

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

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client');

expect(requestSpans).toHaveLength(1);

const span = requestSpans?.[0];

expect(span?.description).toBe('GET <data:text/plain,base64>');

expect(span?.data).toMatchObject({
'http.method': 'GET',
url: '<data:text/plain,base64>',
type: 'xhr',
});

expect(span?.data?.['http.url']).toBe('<data:text/plain,base64>');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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',
transport: loggingTransport,
tracesSampleRate: 1.0,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as Sentry from '@sentry/node';

async function run() {
// Wrap in a transaction so we have a parent span for the fetch span
await Sentry.startSpan({ name: 'test-span' }, async () => {
// Fetch a data URL - this should create a span with sanitized URL
const dataUrl = 'data:text/plain;base64,SGVsbG8gV29ybGQh';
try {
await fetch(dataUrl);
} catch {
// Data URL fetch might not be supported or might fail
// The span should still be created and sanitized
}
});
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect } from 'vitest';
import { createEsmAndCjsTests } from '../../../../utils/runner';

describe('outgoing fetch to data URL', () => {
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('data URL in fetch request should be sanitized in span', async () => {
await createRunner()
.expect({
transaction: {
transaction: 'test-span',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'GET <data:text/plain,base64>',
op: 'http.client',
data: expect.objectContaining({
url: '<data:text/plain,base64>',
'http.url': '<data:text/plain,base64>',
}),
}),
]),
},
})
.start()
.completed();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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',
transport: loggingTransport,
tracesSampleRate: 1.0,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as Sentry from '@sentry/node';

async function run() {
// Wrap in a transaction so we have a parent span for the fetch span
await Sentry.startSpan({ name: 'test-span' }, async () => {
// Fetch a data URL - this should create a span with sanitized URL
const dataUrl = 'data:text/plain;base64,SGVsbG8gV29ybGQh';
try {
await fetch(dataUrl);
} catch {
// Data URL fetch might not be supported or might fail
// The span should still be created and sanitized
}
});
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect } from 'vitest';
import { createEsmAndCjsTests } from '../../../../utils/runner';

describe('outgoing fetch to data URL', () => {
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('data URL in fetch request should be sanitized in span', async () => {
await createRunner()
.expect({
transaction: {
transaction: 'test-span',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'GET <data:text/plain,base64>',
op: 'http.client',
data: expect.objectContaining({
url: '<data:text/plain,base64>',
'http.url': '<data:text/plain,base64>',
}),
}),
]),
},
})
.start()
.completed();
});
});
});
14 changes: 4 additions & 10 deletions packages/browser/src/integrations/globalhandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getLocationHref,
isPrimitive,
isString,
stripDataUrlContent,
UNKNOWN_FUNCTION,
} from '@sentry/core';
import type { BrowserClient } from '../client';
Expand Down Expand Up @@ -208,14 +209,7 @@ function getFilenameFromUrl(url: string | undefined): string | undefined {
return undefined;
}

// stack frame urls can be data urls, for example when initializing a Worker with a base64 encoded script
// in this case we just show the data prefix and mime type to avoid too long raw data urls
if (url.startsWith('data:')) {
const match = url.match(/^data:([^;]+)/);
const mimeType = match ? match[1] : 'text/javascript';
const isBase64 = url.includes('base64,');
return `<data:${mimeType}${isBase64 ? ',base64' : ''}>`;
}

return url; // it's fine to not truncate it as it's not put in a regex (https://codeql.github.com/codeql-query-help/javascript/js-polynomial-redos)
// Strip data URL content to avoid long base64 strings in stack frames
// (e.g. when initializing a Worker with a base64 encoded script)
return stripDataUrlContent(url);
}
9 changes: 5 additions & 4 deletions packages/browser/src/tracing/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
spanToJSON,
startInactiveSpan,
stringMatchesSomePattern,
stripDataUrlContent,
stripUrlQueryAndFragment,
} from '@sentry/core';
import type { XhrHint } from '@sentry-internal/browser-utils';
Expand Down Expand Up @@ -199,7 +200,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
const fullUrl = getFullURL(handlerData.fetchData.url);
const host = fullUrl ? parseUrl(fullUrl).host : undefined;
createdSpan.setAttributes({
'http.url': fullUrl,
'http.url': fullUrl ? stripDataUrlContent(fullUrl) : undefined,
'server.address': host,
});

Expand Down Expand Up @@ -355,7 +356,7 @@ function xhrCallback(
const fullUrl = getFullURL(url);
const parsedUrl = fullUrl ? parseUrl(fullUrl) : parseUrl(url);

const urlForSpanName = stripUrlQueryAndFragment(url);
const urlForSpanName = stripDataUrlContent(stripUrlQueryAndFragment(url));

const hasParent = !!getActiveSpan();

Expand All @@ -364,10 +365,10 @@ function xhrCallback(
? startInactiveSpan({
name: `${method} ${urlForSpanName}`,
attributes: {
url,
url: stripDataUrlContent(url),
type: 'xhr',
'http.method': method,
'http.url': fullUrl,
'http.url': fullUrl ? stripDataUrlContent(fullUrl) : undefined,
'server.address': parsedUrl?.host,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
Expand Down
26 changes: 22 additions & 4 deletions packages/core/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { hasSpansEnabled } from './utils/hasSpansEnabled';
import { isInstanceOf, isRequest } from './utils/is';
import { getActiveSpan } from './utils/spanUtils';
import { getTraceData } from './utils/traceData';
import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils/url';
import {
getSanitizedUrlStringFromUrlObject,
isURLObjectRelative,
parseStringToURLObject,
stripDataUrlContent,
} from './utils/url';

type PolymorphicRequestHeaders =
| Record<string, string | undefined>
Expand Down Expand Up @@ -317,9 +322,22 @@ function getSpanStartOptions(
method: string,
spanOrigin: SpanOrigin,
): Parameters<typeof startInactiveSpan>[0] {
// Data URLs need special handling because parseStringToURLObject treats them as "relative"
// (no "://"), causing getSanitizedUrlStringFromUrlObject to return just the pathname
// without the "data:" prefix, making later stripDataUrlContent calls ineffective.
// So for data URLs, we strip the content first and use that directly.
if (url.startsWith('data:')) {
const sanitizedUrl = stripDataUrlContent(url);
return {
name: `${method} ${sanitizedUrl}`,
attributes: getFetchSpanAttributes(url, undefined, method, spanOrigin),
};
}

const parsedUrl = parseStringToURLObject(url);
const sanitizedUrl = parsedUrl ? getSanitizedUrlStringFromUrlObject(parsedUrl) : url;
return {
name: parsedUrl ? `${method} ${getSanitizedUrlStringFromUrlObject(parsedUrl)}` : method,
name: `${method} ${sanitizedUrl}`,
attributes: getFetchSpanAttributes(url, parsedUrl, method, spanOrigin),
};
}
Expand All @@ -331,15 +349,15 @@ function getFetchSpanAttributes(
spanOrigin: SpanOrigin,
): SpanAttributes {
const attributes: SpanAttributes = {
url,
url: stripDataUrlContent(url),
type: 'fetch',
'http.method': method,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
};
if (parsedUrl) {
if (!isURLObjectRelative(parsedUrl)) {
attributes['http.url'] = parsedUrl.href;
attributes['http.url'] = stripDataUrlContent(parsedUrl.href);
attributes['server.address'] = parsedUrl.host;
}
if (parsedUrl.search) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ export {
getHttpSpanDetailsFromUrlObject,
isURLObjectRelative,
getSanitizedUrlStringFromUrlObject,
stripDataUrlContent,
} from './utils/url';
export {
eventFromMessage,
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,26 @@ export function getSanitizedUrlString(url: PartialURL): string {

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
}

/**
* Strips the content from a data URL, returning a placeholder with the MIME type.
*
* Data URLs can be very long (e.g. base64 encoded scripts for Web Workers),
* with little valuable information, often leading to envelopes getting dropped due
* to size limit violations. Therefore, we strip data URLs and replace them with a
* placeholder.
*
* @param url - The URL to process
* @returns For data URLs, returns a short format like `<data:text/javascript,base64>`.
* For non-data URLs, returns the original URL unchanged.
*/
export function stripDataUrlContent(url: string): string {
if (url.startsWith('data:')) {
// Match the MIME type (everything after 'data:' until the first ';' or ',')
const match = url.match(/^data:([^;,]+)/);
const mimeType = match ? match[1] : 'text/plain';
const isBase64 = url.includes(';base64,');
return `<data:${mimeType}${isBase64 ? ',base64' : ''}>`;
}
return url;
}
Loading
Loading