diff --git a/packages/browser/src/transports/base.ts b/packages/browser/src/transports/base.ts index 042311d53694..4b6c3218f1e8 100644 --- a/packages/browser/src/transports/base.ts +++ b/packages/browser/src/transports/base.ts @@ -14,6 +14,7 @@ import { } from '@sentry/types'; import { dateTimestampInSeconds, + dsnToString, eventStatusFromHttpCode, getGlobalObject, logger, @@ -116,7 +117,7 @@ export abstract class BaseTransport implements Transport { const url = getEnvelopeEndpointWithUrlEncodedAuth(this._api.dsn, this._api.tunnel); // Envelope header is required to be at least an empty object - const envelopeHeader = JSON.stringify({ ...(this._api.tunnel && { dsn: this._api.dsn.toString() }) }); + const envelopeHeader = JSON.stringify({ ...(this._api.tunnel && { dsn: dsnToString(this._api.dsn) }) }); const itemHeaders = JSON.stringify({ type: 'client_report', }); diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 78369d80fcec..bace0ad07591 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -1,5 +1,5 @@ -import { DsnLike, SdkMetadata } from '@sentry/types'; -import { Dsn, urlEncode } from '@sentry/utils'; +import { DsnComponents, DsnLike, SdkMetadata } from '@sentry/types'; +import { dsnToString, makeDsn, urlEncode } from '@sentry/utils'; const SENTRY_API_VERSION = '7'; @@ -12,7 +12,7 @@ export interface APIDetails { /** Metadata about the SDK (name, version, etc) for inclusion in envelope headers */ metadata: SdkMetadata; /** The internally used Dsn object. */ - readonly dsn: Dsn; + readonly dsn: DsnComponents; /** The envelope tunnel to use. */ readonly tunnel?: string; } @@ -32,7 +32,7 @@ export class API { public metadata: SdkMetadata; /** The internally used Dsn object. */ - private readonly _dsnObject: Dsn; + private readonly _dsnObject: DsnComponents; /** The envelope tunnel to use. */ private readonly _tunnel?: string; @@ -40,13 +40,13 @@ export class API { /** Create a new instance of API */ public constructor(dsn: DsnLike, metadata: SdkMetadata = {}, tunnel?: string) { this.dsn = dsn; - this._dsnObject = new Dsn(dsn); + this._dsnObject = makeDsn(dsn); this.metadata = metadata; this._tunnel = tunnel; } /** Returns the Dsn object. */ - public getDsn(): Dsn { + public getDsn(): DsnComponents { return this._dsnObject; } @@ -89,25 +89,25 @@ export function initAPIDetails(dsn: DsnLike, metadata?: SdkMetadata, tunnel?: st return { initDsn: dsn, metadata: metadata || {}, - dsn: new Dsn(dsn), + dsn: makeDsn(dsn), tunnel, } as APIDetails; } /** Returns the prefix to construct Sentry ingestion API endpoints. */ -function getBaseApiEndpoint(dsn: Dsn): string { +function getBaseApiEndpoint(dsn: DsnComponents): string { const protocol = dsn.protocol ? `${dsn.protocol}:` : ''; const port = dsn.port ? `:${dsn.port}` : ''; return `${protocol}//${dsn.host}${port}${dsn.path ? `/${dsn.path}` : ''}/api/`; } /** Returns the ingest API endpoint for target. */ -function _getIngestEndpoint(dsn: Dsn, target: 'store' | 'envelope'): string { +function _getIngestEndpoint(dsn: DsnComponents, target: 'store' | 'envelope'): string { return `${getBaseApiEndpoint(dsn)}${dsn.projectId}/${target}/`; } /** Returns a URL-encoded string with auth config suitable for a query string. */ -function _encodedAuth(dsn: Dsn): string { +function _encodedAuth(dsn: DsnComponents): string { return urlEncode({ // We send only the minimum set of required information. See // https://github.com/getsentry/sentry-javascript/issues/2572. @@ -117,7 +117,7 @@ function _encodedAuth(dsn: Dsn): string { } /** Returns the store endpoint URL. */ -function getStoreEndpoint(dsn: Dsn): string { +function getStoreEndpoint(dsn: DsnComponents): string { return _getIngestEndpoint(dsn, 'store'); } @@ -126,12 +126,12 @@ function getStoreEndpoint(dsn: Dsn): string { * * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. */ -export function getStoreEndpointWithUrlEncodedAuth(dsn: Dsn): string { +export function getStoreEndpointWithUrlEncodedAuth(dsn: DsnComponents): string { return `${getStoreEndpoint(dsn)}?${_encodedAuth(dsn)}`; } /** Returns the envelope endpoint URL. */ -function _getEnvelopeEndpoint(dsn: Dsn): string { +function _getEnvelopeEndpoint(dsn: DsnComponents): string { return _getIngestEndpoint(dsn, 'envelope'); } @@ -140,7 +140,7 @@ function _getEnvelopeEndpoint(dsn: Dsn): string { * * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. */ -export function getEnvelopeEndpointWithUrlEncodedAuth(dsn: Dsn, tunnel?: string): string { +export function getEnvelopeEndpointWithUrlEncodedAuth(dsn: DsnComponents, tunnel?: string): string { return tunnel ? tunnel : `${_getEnvelopeEndpoint(dsn)}?${_encodedAuth(dsn)}`; } @@ -148,7 +148,11 @@ export function getEnvelopeEndpointWithUrlEncodedAuth(dsn: Dsn, tunnel?: string) * Returns an object that can be used in request headers. * This is needed for node and the old /store endpoint in sentry */ -export function getRequestHeaders(dsn: Dsn, clientName: string, clientVersion: string): { [key: string]: string } { +export function getRequestHeaders( + dsn: DsnComponents, + clientName: string, + clientVersion: string, +): { [key: string]: string } { // CHANGE THIS to use metadata but keep clientName and clientVersion compatible const header = [`Sentry sentry_version=${SENTRY_API_VERSION}`]; header.push(`sentry_client=${clientName}/${clientVersion}`); @@ -171,10 +175,10 @@ export function getReportDialogEndpoint( user?: { name?: string; email?: string }; }, ): string { - const dsn = new Dsn(dsnLike); + const dsn = makeDsn(dsnLike); const endpoint = `${getBaseApiEndpoint(dsn)}embed/error-page/`; - let encodedOptions = `dsn=${dsn.toString()}`; + let encodedOptions = `dsn=${dsnToString(dsn)}`; for (const key in dialogOptions) { if (key === 'dsn') { continue; diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 8ff1bb60e739..ffd107b02851 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -2,6 +2,7 @@ import { Scope, Session } from '@sentry/hub'; import { Client, + DsnComponents, Event, EventHint, Integration, @@ -13,11 +14,11 @@ import { import { checkOrSetAlreadyCaught, dateTimestampInSeconds, - Dsn, isPlainObject, isPrimitive, isThenable, logger, + makeDsn, normalize, rejectedSyncPromise, resolvedSyncPromise, @@ -76,7 +77,7 @@ export abstract class BaseClient implement protected readonly _options: O; /** The client Dsn, if specified in options. Without this Dsn, the SDK will be disabled. */ - protected readonly _dsn?: Dsn; + protected readonly _dsn?: DsnComponents; /** Array of used integrations. */ protected _integrations: IntegrationIndex = {}; @@ -95,7 +96,7 @@ export abstract class BaseClient implement this._options = options; if (options.dsn) { - this._dsn = new Dsn(options.dsn); + this._dsn = makeDsn(options.dsn); } } @@ -187,7 +188,7 @@ export abstract class BaseClient implement /** * @inheritDoc */ - public getDsn(): Dsn | undefined { + public getDsn(): DsnComponents | undefined { return this._dsn; } diff --git a/packages/core/src/request.ts b/packages/core/src/request.ts index c1242e0dce12..db1810cdbbbd 100644 --- a/packages/core/src/request.ts +++ b/packages/core/src/request.ts @@ -1,4 +1,5 @@ import { Event, SdkInfo, SentryRequest, SentryRequestType, Session, SessionAggregates } from '@sentry/types'; +import { dsnToString } from '@sentry/utils'; import { APIDetails, getEnvelopeEndpointWithUrlEncodedAuth, getStoreEndpointWithUrlEncodedAuth } from './api'; @@ -33,7 +34,7 @@ export function sessionToSentryRequest(session: Session | SessionAggregates, api const envelopeHeaders = JSON.stringify({ sent_at: new Date().toISOString(), ...(sdkInfo && { sdk: sdkInfo }), - ...(!!api.tunnel && { dsn: api.dsn.toString() }), + ...(!!api.tunnel && { dsn: dsnToString(api.dsn) }), }); // I know this is hacky but we don't want to add `session` to request type since it's never rate limited const type: SentryRequestType = 'aggregates' in session ? ('sessions' as SentryRequestType) : 'session'; @@ -81,7 +82,7 @@ export function eventToSentryRequest(event: Event, api: APIDetails): SentryReque event_id: event.event_id, sent_at: new Date().toISOString(), ...(sdkInfo && { sdk: sdkInfo }), - ...(!!api.tunnel && { dsn: api.dsn.toString() }), + ...(!!api.tunnel && { dsn: dsnToString(api.dsn) }), }); const itemHeaders = JSON.stringify({ type: eventType, diff --git a/packages/core/test/lib/api.test.ts b/packages/core/test/lib/api.test.ts index fd2e97b6d77d..14b44aed9602 100644 --- a/packages/core/test/lib/api.test.ts +++ b/packages/core/test/lib/api.test.ts @@ -1,5 +1,5 @@ /* eslint-disable deprecation/deprecation */ -import { Dsn } from '@sentry/utils'; +import { makeDsn } from '@sentry/utils'; import { API, getReportDialogEndpoint, getRequestHeaders } from '../../src/api'; @@ -25,12 +25,12 @@ describe('API', () => { }); test('getRequestHeaders', () => { - expect(getRequestHeaders(new Dsn(dsnPublic), 'a', '1.0')).toMatchObject({ + expect(getRequestHeaders(makeDsn(dsnPublic), 'a', '1.0')).toMatchObject({ 'Content-Type': 'application/json', 'X-Sentry-Auth': expect.stringMatching(/^Sentry sentry_version=\d, sentry_client=a\/1\.0, sentry_key=abc$/), }); - expect(getRequestHeaders(new Dsn(legacyDsn), 'a', '1.0')).toMatchObject({ + expect(getRequestHeaders(makeDsn(legacyDsn), 'a', '1.0')).toMatchObject({ 'Content-Type': 'application/json', 'X-Sentry-Auth': expect.stringMatching( /^Sentry sentry_version=\d, sentry_client=a\/1\.0, sentry_key=abc, sentry_secret=123$/, @@ -119,6 +119,12 @@ describe('API', () => { }); test('getDsn', () => { - expect(new API(dsnPublic).getDsn()).toEqual(new Dsn(dsnPublic)); + expect(new API(dsnPublic).getDsn().host).toEqual(makeDsn(dsnPublic).host); + expect(new API(dsnPublic).getDsn().path).toEqual(makeDsn(dsnPublic).path); + expect(new API(dsnPublic).getDsn().pass).toEqual(makeDsn(dsnPublic).pass); + expect(new API(dsnPublic).getDsn().port).toEqual(makeDsn(dsnPublic).port); + expect(new API(dsnPublic).getDsn().protocol).toEqual(makeDsn(dsnPublic).protocol); + expect(new API(dsnPublic).getDsn().projectId).toEqual(makeDsn(dsnPublic).projectId); + expect(new API(dsnPublic).getDsn().publicKey).toEqual(makeDsn(dsnPublic).publicKey); }); }); diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index cd8d7f992fb4..a5156e40c26e 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,6 +1,6 @@ import { Hub, Scope, Session } from '@sentry/hub'; import { Event, Span, Transport } from '@sentry/types'; -import { logger, SentryError, SyncPromise } from '@sentry/utils'; +import { dsnToString, logger, SentryError, SyncPromise } from '@sentry/utils'; import * as integrationModule from '../../src/integration'; import { TestBackend } from '../mocks/backend'; @@ -67,7 +67,7 @@ describe('BaseClient', () => { test('returns the Dsn', () => { expect.assertions(1); const client = new TestClient({ dsn: PUBLIC_DSN }); - expect(client.getDsn()!.toString()).toBe(PUBLIC_DSN); + expect(dsnToString(client.getDsn())).toBe(PUBLIC_DSN); }); test('allows missing Dsn', () => { diff --git a/packages/nextjs/test/integration/next-env.d.ts b/packages/nextjs/test/integration/next-env.d.ts index 9bc3dd46b9d9..4f11a03dc6cc 100644 --- a/packages/nextjs/test/integration/next-env.d.ts +++ b/packages/nextjs/test/integration/next-env.d.ts @@ -1,5 +1,4 @@ /// -/// /// // NOTE: This file should not be edited diff --git a/packages/node/src/backend.ts b/packages/node/src/backend.ts index c18dd3aaafd9..83070462ba5e 100644 --- a/packages/node/src/backend.ts +++ b/packages/node/src/backend.ts @@ -1,6 +1,6 @@ import { BaseBackend } from '@sentry/core'; import { Event, EventHint, SeverityLevel, Transport, TransportOptions } from '@sentry/types'; -import { Dsn } from '@sentry/utils'; +import { makeDsn } from '@sentry/utils'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { HTTPSTransport, HTTPTransport } from './transports'; @@ -35,7 +35,7 @@ export class NodeBackend extends BaseBackend { return super._setupTransport(); } - const dsn = new Dsn(this._options.dsn); + const dsn = makeDsn(this._options.dsn); const transportOptions: TransportOptions = { ...this._options.transportOptions, diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index e71a3d2f39c6..9c4510b91806 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,4 +1,4 @@ -import { Dsn } from './dsn'; +import { DsnComponents } from './dsn'; import { Event, EventHint } from './event'; import { Integration, IntegrationClass } from './integration'; import { Options } from './options'; @@ -55,7 +55,7 @@ export interface Client { captureSession?(session: Session): void; /** Returns the current Dsn. */ - getDsn(): Dsn | undefined; + getDsn(): DsnComponents | undefined; /** Returns the current options. */ getOptions(): O; diff --git a/packages/types/src/dsn.ts b/packages/types/src/dsn.ts index b9f757392533..b21130802903 100644 --- a/packages/types/src/dsn.ts +++ b/packages/types/src/dsn.ts @@ -23,17 +23,3 @@ export interface DsnComponents { /** Anything that can be parsed into a Dsn. */ export type DsnLike = string | DsnComponents; - -/** The Sentry Dsn, identifying a Sentry instance and project. */ -export interface Dsn extends DsnComponents { - /** - * Renders the string representation of this Dsn. - * - * By default, this will render the public representation without the password - * component. To get the deprecated private representation, set `withPassword` - * to true. - * - * @param withPassword When set to true, the password will be included. - */ - toString(withPassword: boolean): string; -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index fdc33cac2d1f..a6268e586a8d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,7 +1,7 @@ export { Breadcrumb, BreadcrumbHint } from './breadcrumb'; export { Client } from './client'; export { Context, Contexts } from './context'; -export { Dsn, DsnComponents, DsnLike, DsnProtocol } from './dsn'; +export { DsnComponents, DsnLike, DsnProtocol } from './dsn'; export { DebugImage, DebugImageType, DebugMeta } from './debugMeta'; export { ExtendedError } from './error'; export { Event, EventHint } from './event'; diff --git a/packages/utils/src/dsn.ts b/packages/utils/src/dsn.ts index 89752e6ccfdf..76a7a950a64f 100644 --- a/packages/utils/src/dsn.ts +++ b/packages/utils/src/dsn.ts @@ -6,122 +6,106 @@ import { SentryError } from './error'; /** Regular expression used to parse a Dsn. */ const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+))?@)([\w.-]+)(?::(\d+))?\/(.+)/; -/** Error message */ -const ERROR_MESSAGE = 'Invalid Dsn'; +function isValidProtocol(protocol?: string): protocol is DsnProtocol { + return protocol === 'http' || protocol === 'https'; +} -/** The Sentry Dsn, identifying a Sentry instance and project. */ -export class Dsn implements DsnComponents { - /** Protocol used to connect to Sentry. */ - public protocol!: DsnProtocol; - /** Public authorization key (deprecated, renamed to publicKey). */ - public user!: string; - /** Public authorization key. */ - public publicKey!: string; - /** Private authorization key (deprecated, optional). */ - public pass!: string; - /** Hostname of the Sentry instance. */ - public host!: string; - /** Port of the Sentry instance. */ - public port!: string; - /** Path */ - public path!: string; - /** Project ID */ - public projectId!: string; - - /** Creates a new Dsn component */ - public constructor(from: DsnLike) { - if (typeof from === 'string') { - this._fromString(from); - } else { - this._fromComponents(from); - } +/** + * Renders the string representation of this Dsn. + * + * By default, this will render the public representation without the password + * component. To get the deprecated private representation, set `withPassword` + * to true. + * + * @param withPassword When set to true, the password will be included. + */ +export function dsnToString(dsn: DsnComponents, withPassword: boolean = false): string { + const { host, path, pass, port, projectId, protocol, publicKey } = dsn; + return ( + `${protocol}://${publicKey}${withPassword && pass ? `:${pass}` : ''}` + + `@${host}${port ? `:${port}` : ''}/${path ? `${path}/` : path}${projectId}` + ); +} - this._validate(); - } +function dsnFromString(str: string): DsnComponents { + const match = DSN_REGEX.exec(str); - /** - * Renders the string representation of this Dsn. - * - * By default, this will render the public representation without the password - * component. To get the deprecated private representation, set `withPassword` - * to true. - * - * @param withPassword When set to true, the password will be included. - */ - public toString(withPassword: boolean = false): string { - const { host, path, pass, port, projectId, protocol, publicKey } = this; - return ( - `${protocol}://${publicKey}${withPassword && pass ? `:${pass}` : ''}` + - `@${host}${port ? `:${port}` : ''}/${path ? `${path}/` : path}${projectId}` - ); + if (!match) { + throw new SentryError('Invalid Dsn'); } - /** Parses a string into this Dsn. */ - private _fromString(str: string): void { - const match = DSN_REGEX.exec(str); + const [protocol, publicKey, pass = '', host, port = '', lastPath] = match.slice(1); + let path = ''; + let projectId = lastPath; + + const split = projectId.split('/'); + if (split.length > 1) { + path = split.slice(0, -1).join('/'); + projectId = split.pop() as string; + } - if (!match) { - throw new SentryError(ERROR_MESSAGE); + if (projectId) { + const projectMatch = projectId.match(/^\d+/); + if (projectMatch) { + projectId = projectMatch[0]; } + } - const [protocol, publicKey, pass = '', host, port = '', lastPath] = match.slice(1); - let path = ''; - let projectId = lastPath; + return dsnFromComponents({ host, pass, path, projectId, port, protocol: protocol as DsnProtocol, publicKey }); +} - const split = projectId.split('/'); - if (split.length > 1) { - path = split.slice(0, -1).join('/'); - projectId = split.pop() as string; - } +function dsnFromComponents(components: DsnComponents): DsnComponents { + // TODO this is for backwards compatibility, and can be removed in a future version + if ('user' in components && !('publicKey' in components)) { + components.publicKey = components.user; + } - if (projectId) { - const projectMatch = projectId.match(/^\d+/); - if (projectMatch) { - projectId = projectMatch[0]; - } - } + return { + user: components.publicKey || '', + protocol: components.protocol, + publicKey: components.publicKey || '', + pass: components.pass || '', + host: components.host, + port: components.port || '', + path: components.path || '', + projectId: components.projectId, + }; +} - this._fromComponents({ host, pass, path, projectId, port, protocol: protocol as DsnProtocol, publicKey }); +function validateDsn(dsn: DsnComponents): boolean | void { + if (!isDebugBuild()) { + return; } - /** Maps Dsn components into this instance. */ - private _fromComponents(components: DsnComponents): void { - // TODO this is for backwards compatibility, and can be removed in a future version - if ('user' in components && !('publicKey' in components)) { - components.publicKey = components.user; + const { port, projectId, protocol } = dsn; + + const requiredComponents: ReadonlyArray = ['protocol', 'publicKey', 'host', 'projectId']; + requiredComponents.forEach(component => { + if (!dsn[component]) { + throw new SentryError(`Invalid Dsn: ${component} missing`); } - this.user = components.publicKey || ''; - - this.protocol = components.protocol; - this.publicKey = components.publicKey || ''; - this.pass = components.pass || ''; - this.host = components.host; - this.port = components.port || ''; - this.path = components.path || ''; - this.projectId = components.projectId; + }); + + if (!projectId.match(/^\d+$/)) { + throw new SentryError(`Invalid Dsn: Invalid projectId ${projectId}`); } - /** Validates this Dsn and throws on error. */ - private _validate(): void { - // we only validate in debug mode. This will fail later anyways. - if (isDebugBuild()) { - ['protocol', 'publicKey', 'host', 'projectId'].forEach(component => { - if (!this[component as keyof DsnComponents]) { - throw new SentryError(`${ERROR_MESSAGE}: ${component} missing`); - } - }); - - if (!this.projectId.match(/^\d+$/)) { - throw new SentryError(`${ERROR_MESSAGE}: Invalid projectId ${this.projectId}`); - } - - if (this.protocol !== 'http' && this.protocol !== 'https') { - throw new SentryError(`${ERROR_MESSAGE}: Invalid protocol ${this.protocol}`); - } - - if (this.port && isNaN(parseInt(this.port, 10))) { - throw new SentryError(`${ERROR_MESSAGE}: Invalid port ${this.port}`); - } - } + if (!isValidProtocol(protocol)) { + throw new SentryError(`Invalid Dsn: Invalid protocol ${protocol}`); + } + + if (port && isNaN(parseInt(port, 10))) { + throw new SentryError(`Invalid Dsn: Invalid port ${port}`); } + + return true; +} + +/** The Sentry Dsn, identifying a Sentry instance and project. */ +export function makeDsn(from: DsnLike): DsnComponents { + const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from); + + validateDsn(components); + + return components; } diff --git a/packages/utils/test/dsn.test.ts b/packages/utils/test/dsn.test.ts index 804fbe41536c..44d6e0678210 100644 --- a/packages/utils/test/dsn.test.ts +++ b/packages/utils/test/dsn.test.ts @@ -1,6 +1,6 @@ import { isDebugBuild } from '@sentry/utils'; -import { Dsn } from '../src/dsn'; +import { dsnToString, makeDsn } from '../src/dsn'; import { SentryError } from '../src/error'; function testIf(condition: boolean): jest.It { @@ -10,7 +10,7 @@ function testIf(condition: boolean): jest.It { describe('Dsn', () => { describe('fromComponents', () => { test('applies all components', () => { - const dsn = new Dsn({ + const dsn = makeDsn({ host: 'sentry.io', pass: 'xyz', port: '1234', @@ -28,7 +28,7 @@ describe('Dsn', () => { }); test('applies partial components', () => { - const dsn = new Dsn({ + const dsn = makeDsn({ host: 'sentry.io', projectId: '123', protocol: 'https', @@ -44,70 +44,64 @@ describe('Dsn', () => { }); testIf(isDebugBuild())('throws for missing components', () => { - expect( - () => - new Dsn({ - host: '', - projectId: '123', - protocol: 'https', - publicKey: 'abc', - }), + expect(() => + makeDsn({ + host: '', + projectId: '123', + protocol: 'https', + publicKey: 'abc', + }), ).toThrow(SentryError); - expect( - () => - new Dsn({ - host: 'sentry.io', - projectId: '', - protocol: 'https', - publicKey: 'abc', - }), + expect(() => + makeDsn({ + host: 'sentry.io', + projectId: '', + protocol: 'https', + publicKey: 'abc', + }), ).toThrow(SentryError); - expect( - () => - new Dsn({ - host: 'sentry.io', - projectId: '123', - protocol: '' as 'http', // Trick the type checker here - publicKey: 'abc', - }), + expect(() => + makeDsn({ + host: 'sentry.io', + projectId: '123', + protocol: '' as 'http', // Trick the type checker here + publicKey: 'abc', + }), ).toThrow(SentryError); - expect( - () => - new Dsn({ - host: 'sentry.io', - projectId: '123', - protocol: 'https', - publicKey: '', - }), + expect(() => + makeDsn({ + host: 'sentry.io', + projectId: '123', + protocol: 'https', + publicKey: '', + }), ).toThrow(SentryError); }); testIf(isDebugBuild())('throws for invalid components', () => { - expect( - () => - new Dsn({ - host: 'sentry.io', - projectId: '123', - protocol: 'httpx' as 'http', // Trick the type checker here - publicKey: 'abc', - }), + expect(() => + makeDsn({ + host: 'sentry.io', + projectId: '123', + protocol: 'httpx' as 'http', // Trick the type checker here + publicKey: 'abc', + }), ).toThrow(SentryError); - expect( - () => - new Dsn({ - host: 'sentry.io', - port: 'xxx', - projectId: '123', - protocol: 'https', - publicKey: 'abc', - }), + expect(() => + makeDsn({ + host: 'sentry.io', + port: 'xxx', + projectId: '123', + protocol: 'https', + publicKey: 'abc', + }), ).toThrow(SentryError); }); }); describe('fromString', () => { test('parses a valid full Dsn', () => { - const dsn = new Dsn('https://abc:xyz@sentry.io:1234/123'); + const dsn = makeDsn('https://abc:xyz@sentry.io:1234/123'); expect(dsn.protocol).toBe('https'); expect(dsn.publicKey).toBe('abc'); expect(dsn.pass).toBe('xyz'); @@ -118,7 +112,7 @@ describe('Dsn', () => { }); test('parses a valid partial Dsn', () => { - const dsn = new Dsn('https://abc@sentry.io/123/321'); + const dsn = makeDsn('https://abc@sentry.io/123/321'); expect(dsn.protocol).toBe('https'); expect(dsn.publicKey).toBe('abc'); expect(dsn.pass).toBe(''); @@ -129,7 +123,7 @@ describe('Dsn', () => { }); test('with a long path', () => { - const dsn = new Dsn('https://abc@sentry.io/sentry/custom/installation/321'); + const dsn = makeDsn('https://abc@sentry.io/sentry/custom/installation/321'); expect(dsn.protocol).toBe('https'); expect(dsn.publicKey).toBe('abc'); expect(dsn.pass).toBe(''); @@ -140,7 +134,7 @@ describe('Dsn', () => { }); test('with a query string', () => { - const dsn = new Dsn('https://abc@sentry.io/321?sample.rate=0.1&other=value'); + const dsn = makeDsn('https://abc@sentry.io/321?sample.rate=0.1&other=value'); expect(dsn.protocol).toBe('https'); expect(dsn.publicKey).toBe('abc'); expect(dsn.pass).toBe(''); @@ -151,47 +145,47 @@ describe('Dsn', () => { }); testIf(isDebugBuild())('throws when provided invalid Dsn', () => { - expect(() => new Dsn('some@random.dsn')).toThrow(SentryError); + expect(() => makeDsn('some@random.dsn')).toThrow(SentryError); }); testIf(isDebugBuild())('throws without mandatory fields', () => { - expect(() => new Dsn('://abc@sentry.io/123')).toThrow(SentryError); - expect(() => new Dsn('https://@sentry.io/123')).toThrow(SentryError); - expect(() => new Dsn('https://abc@123')).toThrow(SentryError); - expect(() => new Dsn('https://abc@sentry.io/')).toThrow(SentryError); + expect(() => makeDsn('://abc@sentry.io/123')).toThrow(SentryError); + expect(() => makeDsn('https://@sentry.io/123')).toThrow(SentryError); + expect(() => makeDsn('https://abc@123')).toThrow(SentryError); + expect(() => makeDsn('https://abc@sentry.io/')).toThrow(SentryError); }); testIf(isDebugBuild())('throws for invalid fields', () => { - expect(() => new Dsn('httpx://abc@sentry.io/123')).toThrow(SentryError); - expect(() => new Dsn('httpx://abc@sentry.io:xxx/123')).toThrow(SentryError); - expect(() => new Dsn('http://abc@sentry.io/abc')).toThrow(SentryError); + expect(() => makeDsn('httpx://abc@sentry.io/123')).toThrow(SentryError); + expect(() => makeDsn('httpx://abc@sentry.io:xxx/123')).toThrow(SentryError); + expect(() => makeDsn('http://abc@sentry.io/abc')).toThrow(SentryError); }); }); describe('toString', () => { test('excludes the password by default', () => { - const dsn = new Dsn('https://abc:xyz@sentry.io:1234/123'); - expect(dsn.toString()).toBe('https://abc@sentry.io:1234/123'); + const dsn = makeDsn('https://abc:xyz@sentry.io:1234/123'); + expect(dsnToString(dsn)).toBe('https://abc@sentry.io:1234/123'); }); test('optionally includes the password', () => { - const dsn = new Dsn('https://abc:xyz@sentry.io:1234/123'); - expect(dsn.toString(true)).toBe('https://abc:xyz@sentry.io:1234/123'); + const dsn = makeDsn('https://abc:xyz@sentry.io:1234/123'); + expect(dsnToString(dsn, true)).toBe('https://abc:xyz@sentry.io:1234/123'); }); test('renders no password if missing', () => { - const dsn = new Dsn('https://abc@sentry.io:1234/123'); - expect(dsn.toString(true)).toBe('https://abc@sentry.io:1234/123'); + const dsn = makeDsn('https://abc@sentry.io:1234/123'); + expect(dsnToString(dsn, true)).toBe('https://abc@sentry.io:1234/123'); }); test('renders no port if missing', () => { - const dsn = new Dsn('https://abc@sentry.io/123'); - expect(dsn.toString()).toBe('https://abc@sentry.io/123'); + const dsn = makeDsn('https://abc@sentry.io/123'); + expect(dsnToString(dsn)).toBe('https://abc@sentry.io/123'); }); test('renders the full path correctly', () => { - const dsn = new Dsn('https://abc@sentry.io/sentry/custom/installation/321'); - expect(dsn.toString()).toBe('https://abc@sentry.io/sentry/custom/installation/321'); + const dsn = makeDsn('https://abc@sentry.io/sentry/custom/installation/321'); + expect(dsnToString(dsn)).toBe('https://abc@sentry.io/sentry/custom/installation/321'); }); }); });