Skip to content

Commit b419764

Browse files
committed
fix(utils): Fail silently if the provided Dsn is invalid
1 parent 5440807 commit b419764

File tree

4 files changed

+121
-91
lines changed

4 files changed

+121
-91
lines changed

packages/core/src/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export function getReportDialogEndpoint(
5858
},
5959
): string {
6060
const dsn = makeDsn(dsnLike);
61+
if (!dsn) {
62+
return '';
63+
}
64+
6165
const endpoint = `${getBaseApiEndpoint(dsn)}embed/error-page/`;
6266

6367
let encodedOptions = `dsn=${dsnToString(dsn)}`;

packages/core/src/baseclient.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,14 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
114114
this._options = options;
115115
if (options.dsn) {
116116
this._dsn = makeDsn(options.dsn);
117-
const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
118-
this._transport = options.transport({
119-
recordDroppedEvent: this.recordDroppedEvent.bind(this),
120-
...options.transportOptions,
121-
url,
122-
});
117+
if (this._dsn) {
118+
const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
119+
this._transport = options.transport({
120+
recordDroppedEvent: this.recordDroppedEvent.bind(this),
121+
...options.transportOptions,
122+
url,
123+
});
124+
}
123125
} else {
124126
__DEBUG_BUILD__ && logger.warn('No DSN provided, client will not do anything.');
125127
}

packages/utils/src/dsn.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { DsnComponents, DsnLike, DsnProtocol } from '@sentry/types';
22

33
import { SentryError } from './error';
4+
import { logger } from './logger';
45

56
/** Regular expression used to parse a Dsn. */
67
const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+)?)?@)([\w.-]+)(?::(\d+))?\/(.+)/;
@@ -100,9 +101,19 @@ function validateDsn(dsn: DsnComponents): boolean | void {
100101
return true;
101102
}
102103

103-
/** The Sentry Dsn, identifying a Sentry instance and project. */
104-
export function makeDsn(from: DsnLike): DsnComponents {
105-
const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from);
106-
validateDsn(components);
107-
return components;
104+
/**
105+
* Creates a valid Sentry Dsn object, identifying a Sentry instance and project.
106+
* @returns a valid DsnComponents object or `undefined` if @param from is an invalid DSN source
107+
*/
108+
export function makeDsn(from: DsnLike): DsnComponents | undefined {
109+
try {
110+
const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from);
111+
validateDsn(components);
112+
return components;
113+
} catch (e) {
114+
if (e instanceof SentryError) {
115+
logger.error(e.message);
116+
}
117+
return undefined;
118+
}
108119
}

packages/utils/test/dsn.test.ts

Lines changed: 93 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { dsnToString, makeDsn } from '../src/dsn';
2-
import { SentryError } from '../src/error';
2+
import { logger } from '../src/logger';
33

44
function testIf(condition: boolean): jest.It {
55
return condition ? test : test.skip;
66
}
77

8+
const loggerSpy = jest.spyOn(logger, 'error').mockImplementation(() => {});
9+
810
describe('Dsn', () => {
11+
beforeEach(() => {
12+
loggerSpy.mockClear();
13+
});
14+
915
describe('fromComponents', () => {
1016
test('applies all components', () => {
1117
const dsn = makeDsn({
@@ -16,13 +22,13 @@ describe('Dsn', () => {
1622
protocol: 'https',
1723
publicKey: 'abc',
1824
});
19-
expect(dsn.protocol).toBe('https');
20-
expect(dsn.publicKey).toBe('abc');
21-
expect(dsn.pass).toBe('xyz');
22-
expect(dsn.host).toBe('sentry.io');
23-
expect(dsn.port).toBe('1234');
24-
expect(dsn.path).toBe('');
25-
expect(dsn.projectId).toBe('123');
25+
expect(dsn?.protocol).toBe('https');
26+
expect(dsn?.publicKey).toBe('abc');
27+
expect(dsn?.pass).toBe('xyz');
28+
expect(dsn?.host).toBe('sentry.io');
29+
expect(dsn?.port).toBe('1234');
30+
expect(dsn?.path).toBe('');
31+
expect(dsn?.projectId).toBe('123');
2632
});
2733

2834
test('applies partial components', () => {
@@ -32,169 +38,176 @@ describe('Dsn', () => {
3238
protocol: 'https',
3339
publicKey: 'abc',
3440
});
35-
expect(dsn.protocol).toBe('https');
36-
expect(dsn.publicKey).toBe('abc');
37-
expect(dsn.pass).toBe('');
38-
expect(dsn.host).toBe('sentry.io');
39-
expect(dsn.port).toBe('');
40-
expect(dsn.path).toBe('');
41-
expect(dsn.projectId).toBe('123');
41+
expect(dsn?.protocol).toBe('https');
42+
expect(dsn?.publicKey).toBe('abc');
43+
expect(dsn?.pass).toBe('');
44+
expect(dsn?.host).toBe('sentry.io');
45+
expect(dsn?.port).toBe('');
46+
expect(dsn?.path).toBe('');
47+
expect(dsn?.projectId).toBe('123');
4248
});
4349

44-
testIf(__DEBUG_BUILD__)('throws for missing components', () => {
45-
expect(() =>
50+
testIf(__DEBUG_BUILD__)('returns `undefined` for missing components', () => {
51+
expect(
4652
makeDsn({
4753
host: '',
4854
projectId: '123',
4955
protocol: 'https',
5056
publicKey: 'abc',
5157
}),
52-
).toThrow(SentryError);
53-
expect(() =>
58+
).toBeUndefined();
59+
expect(
5460
makeDsn({
5561
host: 'sentry.io',
5662
projectId: '',
5763
protocol: 'https',
5864
publicKey: 'abc',
5965
}),
60-
).toThrow(SentryError);
61-
expect(() =>
66+
).toBeUndefined();
67+
expect(
6268
makeDsn({
6369
host: 'sentry.io',
6470
projectId: '123',
6571
protocol: '' as 'http', // Trick the type checker here
6672
publicKey: 'abc',
6773
}),
68-
).toThrow(SentryError);
69-
expect(() =>
74+
).toBeUndefined();
75+
expect(
7076
makeDsn({
7177
host: 'sentry.io',
7278
projectId: '123',
7379
protocol: 'https',
7480
publicKey: '',
7581
}),
76-
).toThrow(SentryError);
82+
).toBeUndefined();
83+
84+
expect(logger.error).toHaveBeenCalledTimes(4);
7785
});
7886

79-
testIf(__DEBUG_BUILD__)('throws for invalid components', () => {
80-
expect(() =>
87+
testIf(__DEBUG_BUILD__)('returns `undefined` if components are invalid', () => {
88+
expect(
8189
makeDsn({
8290
host: 'sentry.io',
8391
projectId: '123',
8492
protocol: 'httpx' as 'http', // Trick the type checker here
8593
publicKey: 'abc',
8694
}),
87-
).toThrow(SentryError);
88-
expect(() =>
95+
).toBeUndefined();
96+
expect(
8997
makeDsn({
9098
host: 'sentry.io',
9199
port: 'xxx',
92100
projectId: '123',
93101
protocol: 'https',
94102
publicKey: 'abc',
95103
}),
96-
).toThrow(SentryError);
104+
).toBeUndefined();
105+
106+
expect(logger.error).toHaveBeenCalledTimes(2);
97107
});
98108
});
99109

100110
describe('fromString', () => {
101111
test('parses a valid full Dsn', () => {
102112
const dsn = makeDsn('https://abc:[email protected]:1234/123');
103-
expect(dsn.protocol).toBe('https');
104-
expect(dsn.publicKey).toBe('abc');
105-
expect(dsn.pass).toBe('xyz');
106-
expect(dsn.host).toBe('sentry.io');
107-
expect(dsn.port).toBe('1234');
108-
expect(dsn.path).toBe('');
109-
expect(dsn.projectId).toBe('123');
113+
expect(dsn?.protocol).toBe('https');
114+
expect(dsn?.publicKey).toBe('abc');
115+
expect(dsn?.pass).toBe('xyz');
116+
expect(dsn?.host).toBe('sentry.io');
117+
expect(dsn?.port).toBe('1234');
118+
expect(dsn?.path).toBe('');
119+
expect(dsn?.projectId).toBe('123');
110120
});
111121

112122
test('parses a valid partial Dsn', () => {
113123
const dsn = makeDsn('https://[email protected]/123/321');
114-
expect(dsn.protocol).toBe('https');
115-
expect(dsn.publicKey).toBe('abc');
116-
expect(dsn.pass).toBe('');
117-
expect(dsn.host).toBe('sentry.io');
118-
expect(dsn.port).toBe('');
119-
expect(dsn.path).toBe('123');
120-
expect(dsn.projectId).toBe('321');
124+
expect(dsn?.protocol).toBe('https');
125+
expect(dsn?.publicKey).toBe('abc');
126+
expect(dsn?.pass).toBe('');
127+
expect(dsn?.host).toBe('sentry.io');
128+
expect(dsn?.port).toBe('');
129+
expect(dsn?.path).toBe('123');
130+
expect(dsn?.projectId).toBe('321');
121131
});
122132

123133
test('parses a Dsn with empty password', () => {
124134
const dsn = makeDsn('https://abc:@sentry.io/123/321');
125-
expect(dsn.protocol).toBe('https');
126-
expect(dsn.publicKey).toBe('abc');
127-
expect(dsn.pass).toBe('');
128-
expect(dsn.host).toBe('sentry.io');
129-
expect(dsn.port).toBe('');
130-
expect(dsn.path).toBe('123');
131-
expect(dsn.projectId).toBe('321');
135+
expect(dsn?.protocol).toBe('https');
136+
expect(dsn?.publicKey).toBe('abc');
137+
expect(dsn?.pass).toBe('');
138+
expect(dsn?.host).toBe('sentry.io');
139+
expect(dsn?.port).toBe('');
140+
expect(dsn?.path).toBe('123');
141+
expect(dsn?.projectId).toBe('321');
132142
});
133143

134144
test('with a long path', () => {
135145
const dsn = makeDsn('https://[email protected]/sentry/custom/installation/321');
136-
expect(dsn.protocol).toBe('https');
137-
expect(dsn.publicKey).toBe('abc');
138-
expect(dsn.pass).toBe('');
139-
expect(dsn.host).toBe('sentry.io');
140-
expect(dsn.port).toBe('');
141-
expect(dsn.path).toBe('sentry/custom/installation');
142-
expect(dsn.projectId).toBe('321');
146+
expect(dsn?.protocol).toBe('https');
147+
expect(dsn?.publicKey).toBe('abc');
148+
expect(dsn?.pass).toBe('');
149+
expect(dsn?.host).toBe('sentry.io');
150+
expect(dsn?.port).toBe('');
151+
expect(dsn?.path).toBe('sentry/custom/installation');
152+
expect(dsn?.projectId).toBe('321');
143153
});
144154

145155
test('with a query string', () => {
146156
const dsn = makeDsn('https://[email protected]/321?sample.rate=0.1&other=value');
147-
expect(dsn.protocol).toBe('https');
148-
expect(dsn.publicKey).toBe('abc');
149-
expect(dsn.pass).toBe('');
150-
expect(dsn.host).toBe('sentry.io');
151-
expect(dsn.port).toBe('');
152-
expect(dsn.path).toBe('');
153-
expect(dsn.projectId).toBe('321');
157+
expect(dsn?.protocol).toBe('https');
158+
expect(dsn?.publicKey).toBe('abc');
159+
expect(dsn?.pass).toBe('');
160+
expect(dsn?.host).toBe('sentry.io');
161+
expect(dsn?.port).toBe('');
162+
expect(dsn?.path).toBe('');
163+
expect(dsn?.projectId).toBe('321');
154164
});
155165

156-
testIf(__DEBUG_BUILD__)('throws when provided invalid Dsn', () => {
157-
expect(() => makeDsn('[email protected]')).toThrow(SentryError);
166+
testIf(__DEBUG_BUILD__)('returns undefined when provided invalid Dsn', () => {
167+
expect(makeDsn('[email protected]')).toBeUndefined();
168+
expect(logger.error).toHaveBeenCalledTimes(1);
158169
});
159170

160-
testIf(__DEBUG_BUILD__)('throws without mandatory fields', () => {
161-
expect(() => makeDsn('://[email protected]/123')).toThrow(SentryError);
162-
expect(() => makeDsn('https://@sentry.io/123')).toThrow(SentryError);
163-
expect(() => makeDsn('https://abc@123')).toThrow(SentryError);
164-
expect(() => makeDsn('https://[email protected]/')).toThrow(SentryError);
171+
testIf(__DEBUG_BUILD__)('returns undefined if mandatory fields are missing', () => {
172+
expect(makeDsn('://[email protected]/123')).toBeUndefined();
173+
expect(makeDsn('https://@sentry.io/123')).toBeUndefined();
174+
expect(makeDsn('https://abc@123')).toBeUndefined();
175+
expect(makeDsn('https://[email protected]/')).toBeUndefined();
176+
expect(logger.error).toHaveBeenCalledTimes(4);
165177
});
166178

167-
testIf(__DEBUG_BUILD__)('throws for invalid fields', () => {
168-
expect(() => makeDsn('httpx://[email protected]/123')).toThrow(SentryError);
169-
expect(() => makeDsn('httpx://[email protected]:xxx/123')).toThrow(SentryError);
170-
expect(() => makeDsn('http://[email protected]/abc')).toThrow(SentryError);
179+
testIf(__DEBUG_BUILD__)('returns undefined if fields are invalid', () => {
180+
expect(makeDsn('httpx://[email protected]/123')).toBeUndefined();
181+
expect(makeDsn('httpx://[email protected]:xxx/123')).toBeUndefined();
182+
expect(makeDsn('http://[email protected]/abc')).toBeUndefined();
183+
expect(logger.error).toHaveBeenCalledTimes(3);
171184
});
172185
});
173186

174187
describe('toString', () => {
175188
test('excludes the password by default', () => {
176189
const dsn = makeDsn('https://abc:[email protected]:1234/123');
177-
expect(dsnToString(dsn)).toBe('https://[email protected]:1234/123');
190+
expect(dsnToString(dsn!)).toBe('https://[email protected]:1234/123');
178191
});
179192

180193
test('optionally includes the password', () => {
181194
const dsn = makeDsn('https://abc:[email protected]:1234/123');
182-
expect(dsnToString(dsn, true)).toBe('https://abc:[email protected]:1234/123');
195+
expect(dsnToString(dsn!, true)).toBe('https://abc:[email protected]:1234/123');
183196
});
184197

185198
test('renders no password if missing', () => {
186199
const dsn = makeDsn('https://[email protected]:1234/123');
187-
expect(dsnToString(dsn, true)).toBe('https://[email protected]:1234/123');
200+
expect(dsnToString(dsn!, true)).toBe('https://[email protected]:1234/123');
188201
});
189202

190203
test('renders no port if missing', () => {
191204
const dsn = makeDsn('https://[email protected]/123');
192-
expect(dsnToString(dsn)).toBe('https://[email protected]/123');
205+
expect(dsnToString(dsn!)).toBe('https://[email protected]/123');
193206
});
194207

195208
test('renders the full path correctly', () => {
196209
const dsn = makeDsn('https://[email protected]/sentry/custom/installation/321');
197-
expect(dsnToString(dsn)).toBe('https://[email protected]/sentry/custom/installation/321');
210+
expect(dsnToString(dsn!)).toBe('https://[email protected]/sentry/custom/installation/321');
198211
});
199212
});
200213
});

0 commit comments

Comments
 (0)