Skip to content

Commit be4699f

Browse files
committed
properly sanitize url, add url data
1 parent 84a1b46 commit be4699f

File tree

6 files changed

+92
-22
lines changed

6 files changed

+92
-22
lines changed

packages/node/src/integrations/http.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Hub } from '@sentry/core';
22
import { getCurrentHub } from '@sentry/core';
3-
import type { EventProcessor, Integration, Span, TracePropagationTargets } from '@sentry/types';
3+
import type { EventProcessor, Integration, RequestSpanData, Span, TracePropagationTargets } from '@sentry/types';
44
import {
55
dynamicSamplingContextToSentryBaggageHeader,
66
fill,
@@ -129,16 +129,6 @@ type OriginalRequestMethod = RequestMethod;
129129
type WrappedRequestMethod = RequestMethod;
130130
type WrappedRequestMethodFactory = (original: OriginalRequestMethod) => WrappedRequestMethod;
131131

132-
/**
133-
* See https://develop.sentry.dev/sdk/data-handling/#structuring-data
134-
*/
135-
type RequestSpanData = {
136-
url: string;
137-
method: string;
138-
'http.fragment'?: string;
139-
'http.query'?: string;
140-
};
141-
142132
/**
143133
* Function which creates a function which creates wrapped versions of internal `request` and `get` calls within `http`
144134
* and `https` modules. (NB: Not a typo - this is a creator^2!)

packages/sveltekit/src/client/load.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import type { BaseClient } from '@sentry/core';
22
import { getCurrentHub, trace } from '@sentry/core';
33
import type { Breadcrumbs, BrowserTracing } from '@sentry/svelte';
44
import { captureException } from '@sentry/svelte';
5-
import type { ClientOptions } from '@sentry/types';
5+
import type { ClientOptions, RequestSpanData } from '@sentry/types';
66
import {
77
addExceptionMechanism,
88
addTracingHeadersToFetchRequest,
99
getFetchMethod,
1010
getFetchUrl,
11+
getSanitizedUrlString,
1112
objectify,
1213
stringMatchesSomePattern,
13-
stripUrlQueryAndFragment,
1414
} from '@sentry/utils';
1515
import type { LoadEvent } from '@sveltejs/kit';
1616

@@ -133,23 +133,25 @@ function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch
133133
apply: (wrappingTarget, thisArg, args: Parameters<LoadEvent['fetch']>) => {
134134
const [input, init] = args;
135135
const rawUrl = getFetchUrl(args);
136-
const sanitizedUrl = stripUrlQueryAndFragment(rawUrl);
136+
const urlObject = new URL(rawUrl);
137+
const sanitizedUrl = getSanitizedUrlString(urlObject);
137138
const method = getFetchMethod(args);
138139

139140
// TODO: extract this to a util function (and use it in breadcrumbs integration as well)
140141
if (rawUrl.match(/sentry_key/) && method === 'POST') {
141-
// We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests)
142+
// We don't create spans or breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests)
142143
return wrappingTarget.apply(thisArg, args);
143144
}
144145

145146
const patchedInit: RequestInit = { ...init } || {};
146147
const activeSpan = getCurrentHub().getScope().getSpan();
147148
const activeTransaction = activeSpan && activeSpan.transaction;
148149

149-
const attachHeaders = shouldAttachHeaders(rawUrl);
150-
const attachSpan = shouldCreateSpan(rawUrl);
150+
const attachHeaders = activeTransaction && shouldAttachHeaders(rawUrl);
151+
const createSpan = activeTransaction && shouldCreateSpan(rawUrl);
151152

152-
if (attachHeaders && attachSpan && activeTransaction) {
153+
// only attach headers if we should create a span
154+
if (attachHeaders && createSpan) {
153155
const dsc = activeTransaction.getDynamicSamplingContext();
154156
const headers = addTracingHeadersToFetchRequest(
155157
input as string | Request,
@@ -168,13 +170,20 @@ function instrumentSvelteKitFetch(originalFetch: SvelteKitFetch): SvelteKitFetch
168170

169171
let fetchPromise: Promise<Response>;
170172

171-
if (attachSpan) {
173+
if (createSpan) {
174+
const spanData: RequestSpanData = {
175+
url: sanitizedUrl,
176+
method,
177+
'http.query': urlObject.search,
178+
'http.fragment': urlObject.hash,
179+
};
180+
172181
fetchPromise = trace(
173182
{
174183
name: `${method} ${sanitizedUrl}`, // this will become the description of the span
175184
op: 'http.client',
176185
data: {
177-
/* TODO: extract query data (we might actually only do this once we tackle sanitization on the browser-side) */
186+
...spanData,
178187
},
179188
parentSpanId: activeSpan && activeSpan.spanId,
180189
},

packages/types/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export type { ClientOptions, Options } from './options';
4848
export type { Package } from './package';
4949
export type { PolymorphicEvent, PolymorphicRequest } from './polymorphics';
5050
export type { ReplayEvent, ReplayRecordingData, ReplayRecordingMode } from './replay';
51-
export type { QueryParams, Request } from './request';
51+
export type { QueryParams, Request, RequestSpanData } from './request';
5252
export type { Runtime } from './runtime';
5353
export type { CaptureContext, Scope, ScopeContext } from './scope';
5454
export type { SdkInfo } from './sdkinfo';

packages/types/src/request.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,14 @@ export interface Request {
1010
}
1111

1212
export type QueryParams = string | { [key: string]: string } | Array<[string, string]>;
13+
14+
/**
15+
* span.data type for http.client spans
16+
* See https://develop.sentry.dev/sdk/data-handling/#structuring-data
17+
*/
18+
export type RequestSpanData = {
19+
url: string;
20+
method: string;
21+
'http.fragment'?: string;
22+
'http.query'?: string;
23+
};

packages/utils/src/url.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,18 @@ export function getNumberOfUrlSegments(url: string): number {
5050
// split at '/' or at '\/' to split regex urls correctly
5151
return url.split(/\\?\//).filter(s => s.length > 0 && s !== ',').length;
5252
}
53+
54+
/**
55+
* Takes a URL object and returns a sanitized string which is safe to use as span description
56+
* see: https://develop.sentry.dev/sdk/data-handling/#structuring-data
57+
*/
58+
export function getSanitizedUrlString(url: URL): string {
59+
const { protocol, hostname, port: originalPort, pathname, username, password } = url;
60+
61+
// Don't show standard :80 (http) and :443 (https) ports to reduce the noise
62+
const port = !originalPort || originalPort === '80' || originalPort === '443' ? '' : `:${originalPort}`;
63+
// Always filter out authority
64+
const authority = !username && !password ? '' : '[filtered]:[filtered]@';
65+
66+
return `${protocol}//${authority}${hostname}${port}${pathname}`;
67+
}

packages/utils/test/url.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getNumberOfUrlSegments, stripUrlQueryAndFragment } from '../src/url';
1+
import { getNumberOfUrlSegments, getSanitizedUrlString, stripUrlQueryAndFragment } from '../src/url';
22

33
describe('stripQueryStringAndFragment', () => {
44
const urlString = 'http://dogs.are.great:1231/yay/';
@@ -31,3 +31,48 @@ describe('getNumberOfUrlSegments', () => {
3131
expect(getNumberOfUrlSegments(input)).toEqual(output);
3232
});
3333
});
34+
35+
describe('getSanitizedUrlString', () => {
36+
it.each([
37+
['regular url', 'https://somedomain.com', 'https://somedomain.com'],
38+
['regular url with a path', 'https://somedomain.com/path/to/happiness', 'https://somedomain.com/path/to/happiness'],
39+
[
40+
'url with standard http port 80',
41+
'http://somedomain.com:80/path/to/happiness',
42+
'http://somedomain.com/path/to/happiness',
43+
],
44+
[
45+
'url with standard https port 443',
46+
'https://somedomain.com:443/path/to/happiness',
47+
'https://somedomain.com/path/to/happiness',
48+
],
49+
[
50+
'url with non-standard port',
51+
'https://somedomain.com:4200/path/to/happiness',
52+
'https://somedomain.com:4200/path/to/happiness',
53+
],
54+
[
55+
'url with query params',
56+
'https://somedomain.com:4200/path/to/happiness?auhtToken=abc123&param2=bar',
57+
'https://somedomain.com:4200/path/to/happiness',
58+
],
59+
[
60+
'url with a fragment',
61+
'https://somedomain.com/path/to/happiness#somewildfragment123',
62+
'https://somedomain.com/path/to/happiness',
63+
],
64+
[
65+
'url with a fragment and query params',
66+
'https://somedomain.com/path/to/happiness#somewildfragment123?auhtToken=abc123&param2=bar',
67+
'https://somedomain.com/path/to/happiness',
68+
],
69+
[
70+
'url with authorization',
71+
'https://username:password@somedomain.com',
72+
'https://[filtered]:[filtered]@somedomain.com',
73+
],
74+
])('returns a sanitized URL for a %s', (_, rawUrl: string, sanitizedURL: string) => {
75+
const urlObject = new URL(rawUrl);
76+
expect(getSanitizedUrlString(urlObject)).toEqual(sanitizedURL);
77+
});
78+
});

0 commit comments

Comments
 (0)