diff --git a/packages/browser/src/transports/base.ts b/packages/browser/src/transports/base.ts index 072b8f88b463..8fd960593ac5 100644 --- a/packages/browser/src/transports/base.ts +++ b/packages/browser/src/transports/base.ts @@ -1,4 +1,9 @@ -import { API } from '@sentry/core'; +import { + APIDetails, + getEnvelopeEndpointWithUrlEncodedAuth, + getStoreEndpointWithUrlEncodedAuth, + initAPIDetails, +} from '@sentry/core'; import { Event, Outcome, @@ -38,7 +43,7 @@ export abstract class BaseTransport implements Transport { public url: string; /** Helper to get Sentry API endpoints. */ - protected readonly _api: API; + protected readonly _api: APIDetails; /** A simple buffer holding all requests. */ protected readonly _buffer: PromiseBuffer = new PromiseBuffer(30); @@ -49,9 +54,9 @@ export abstract class BaseTransport implements Transport { protected _outcomes: { [key: string]: number } = {}; public constructor(public options: TransportOptions) { - this._api = new API(options.dsn, options._metadata, options.tunnel); + this._api = initAPIDetails(options.dsn, options._metadata, options.tunnel); // eslint-disable-next-line deprecation/deprecation - this.url = this._api.getStoreEndpointWithUrlEncodedAuth(); + this.url = getStoreEndpointWithUrlEncodedAuth(this._api.dsn); if (this.options.sendClientReports && global.document) { global.document.addEventListener('visibilitychange', () => { @@ -112,9 +117,9 @@ export abstract class BaseTransport implements Transport { logger.log(`Flushing outcomes:\n${JSON.stringify(outcomes, null, 2)}`); - const url = this._api.getEnvelopeEndpointWithUrlEncodedAuth(); + 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.options.tunnel && { dsn: this._api.getDsn().toString() }) }); + const envelopeHeader = JSON.stringify({ ...(this._api.tunnel && { dsn: this._api.dsn.toString() }) }); const itemHeaders = JSON.stringify({ type: 'client_report', }); diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index c5dff08ba653..78369d80fcec 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -3,10 +3,26 @@ import { Dsn, urlEncode } from '@sentry/utils'; const SENTRY_API_VERSION = '7'; +/** + * Stores details about a Sentry SDK + */ +export interface APIDetails { + /** The DSN as passed to Sentry.init() */ + initDsn: DsnLike; + /** Metadata about the SDK (name, version, etc) for inclusion in envelope headers */ + metadata: SdkMetadata; + /** The internally used Dsn object. */ + readonly dsn: Dsn; + /** The envelope tunnel to use. */ + readonly tunnel?: string; +} + /** * Helper class to provide urls, headers and metadata that can be used to form * different types of requests to Sentry endpoints. * Supports both envelopes and regular event requests. + * + * @deprecated Please use APIDetails **/ export class API { /** The DSN as passed to Sentry.init() */ @@ -41,13 +57,12 @@ export class API { /** Returns the prefix to construct Sentry ingestion API endpoints. */ public getBaseApiEndpoint(): string { - const dsn = this.getDsn(); - return getBaseApiEndpoint(dsn); + return getBaseApiEndpoint(this._dsnObject); } /** Returns the store endpoint URL. */ public getStoreEndpoint(): string { - return this._getIngestEndpoint('store'); + return getStoreEndpoint(this._dsnObject); } /** @@ -56,7 +71,7 @@ export class API { * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. */ public getStoreEndpointWithUrlEncodedAuth(): string { - return `${this.getStoreEndpoint()}?${this._encodedAuth()}`; + return getStoreEndpointWithUrlEncodedAuth(this._dsnObject); } /** @@ -65,64 +80,18 @@ export class API { * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. */ public getEnvelopeEndpointWithUrlEncodedAuth(): string { - if (this.forceEnvelope()) { - return this._tunnel as string; - } - - return `${this._getEnvelopeEndpoint()}?${this._encodedAuth()}`; - } - - /** Returns only the path component for the store endpoint. */ - public getStoreEndpointPath(): string { - const dsn = this.getDsn(); - return `${dsn.path ? `/${dsn.path}` : ''}/api/${dsn.projectId}/store/`; - } - - /** - * Returns an object that can be used in request headers. - * This is needed for node and the old /store endpoint in sentry - */ - public getRequestHeaders(clientName: string, clientVersion: string): { [key: string]: string } { - // CHANGE THIS to use metadata but keep clientName and clientVersion compatible - const dsn = this.getDsn(); - const header = [`Sentry sentry_version=${SENTRY_API_VERSION}`]; - header.push(`sentry_client=${clientName}/${clientVersion}`); - header.push(`sentry_key=${dsn.publicKey}`); - if (dsn.pass) { - header.push(`sentry_secret=${dsn.pass}`); - } - return { - 'Content-Type': 'application/json', - 'X-Sentry-Auth': header.join(', '), - }; - } - - /** Returns the envelope endpoint URL. */ - private _getEnvelopeEndpoint(): string { - return this._getIngestEndpoint('envelope'); - } - - /** Returns the ingest API endpoint for target. */ - private _getIngestEndpoint(target: 'store' | 'envelope'): string { - if (this._tunnel) { - return this._tunnel; - } - const base = this.getBaseApiEndpoint(); - const dsn = this.getDsn(); - return `${base}${dsn.projectId}/${target}/`; + return getEnvelopeEndpointWithUrlEncodedAuth(this._dsnObject, this._tunnel); } +} - /** Returns a URL-encoded string with auth config suitable for a query string. */ - private _encodedAuth(): string { - const dsn = this.getDsn(); - const auth = { - // We send only the minimum set of required information. See - // https://github.com/getsentry/sentry-javascript/issues/2572. - sentry_key: dsn.publicKey, - sentry_version: SENTRY_API_VERSION, - }; - return urlEncode(auth); - } +/** Initializes API Details */ +export function initAPIDetails(dsn: DsnLike, metadata?: SdkMetadata, tunnel?: string): APIDetails { + return { + initDsn: dsn, + metadata: metadata || {}, + dsn: new Dsn(dsn), + tunnel, + } as APIDetails; } /** Returns the prefix to construct Sentry ingestion API endpoints. */ @@ -132,6 +101,67 @@ function getBaseApiEndpoint(dsn: Dsn): string { 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 { + return `${getBaseApiEndpoint(dsn)}${dsn.projectId}/${target}/`; +} + +/** Returns a URL-encoded string with auth config suitable for a query string. */ +function _encodedAuth(dsn: Dsn): string { + return urlEncode({ + // We send only the minimum set of required information. See + // https://github.com/getsentry/sentry-javascript/issues/2572. + sentry_key: dsn.publicKey, + sentry_version: SENTRY_API_VERSION, + }); +} + +/** Returns the store endpoint URL. */ +function getStoreEndpoint(dsn: Dsn): string { + return _getIngestEndpoint(dsn, 'store'); +} + +/** + * Returns the store endpoint URL with auth in the query 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 { + return `${getStoreEndpoint(dsn)}?${_encodedAuth(dsn)}`; +} + +/** Returns the envelope endpoint URL. */ +function _getEnvelopeEndpoint(dsn: Dsn): string { + return _getIngestEndpoint(dsn, 'envelope'); +} + +/** + * Returns the envelope endpoint URL with auth in the query 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 { + return tunnel ? tunnel : `${_getEnvelopeEndpoint(dsn)}?${_encodedAuth(dsn)}`; +} + +/** + * 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 } { + // 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}`); + header.push(`sentry_key=${dsn.publicKey}`); + if (dsn.pass) { + header.push(`sentry_secret=${dsn.pass}`); + } + return { + 'Content-Type': 'application/json', + 'X-Sentry-Auth': header.join(', '), + }; +} + /** Returns the url to the report dialog endpoint. */ export function getReportDialogEndpoint( dsnLike: DsnLike, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7e7423b11ff8..22308a0cb7e8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,7 +14,16 @@ export { withScope, } from '@sentry/minimal'; export { addGlobalEventProcessor, getCurrentHub, getHubFromCarrier, Hub, makeMain, Scope } from '@sentry/hub'; -export { API, getReportDialogEndpoint } from './api'; +export { + // eslint-disable-next-line deprecation/deprecation + API, + APIDetails, + getEnvelopeEndpointWithUrlEncodedAuth, + getStoreEndpointWithUrlEncodedAuth, + getRequestHeaders, + initAPIDetails, + getReportDialogEndpoint, +} from './api'; export { BaseClient } from './baseclient'; export { BackendClass, BaseBackend } from './basebackend'; export { eventToSentryRequest, sessionToSentryRequest } from './request'; diff --git a/packages/core/src/request.ts b/packages/core/src/request.ts index 54197023a2eb..c1242e0dce12 100644 --- a/packages/core/src/request.ts +++ b/packages/core/src/request.ts @@ -1,9 +1,9 @@ import { Event, SdkInfo, SentryRequest, SentryRequestType, Session, SessionAggregates } from '@sentry/types'; -import { API } from './api'; +import { APIDetails, getEnvelopeEndpointWithUrlEncodedAuth, getStoreEndpointWithUrlEncodedAuth } from './api'; /** Extract sdk info from from the API metadata */ -function getSdkMetadataForEnvelopeHeader(api: API): SdkInfo | undefined { +function getSdkMetadataForEnvelopeHeader(api: APIDetails): SdkInfo | undefined { if (!api.metadata || !api.metadata.sdk) { return; } @@ -28,12 +28,12 @@ function enhanceEventWithSdkInfo(event: Event, sdkInfo?: SdkInfo): Event { } /** Creates a SentryRequest from a Session. */ -export function sessionToSentryRequest(session: Session | SessionAggregates, api: API): SentryRequest { +export function sessionToSentryRequest(session: Session | SessionAggregates, api: APIDetails): SentryRequest { const sdkInfo = getSdkMetadataForEnvelopeHeader(api); const envelopeHeaders = JSON.stringify({ sent_at: new Date().toISOString(), ...(sdkInfo && { sdk: sdkInfo }), - ...(api.forceEnvelope() && { dsn: api.getDsn().toString() }), + ...(!!api.tunnel && { dsn: api.dsn.toString() }), }); // 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'; @@ -44,15 +44,15 @@ export function sessionToSentryRequest(session: Session | SessionAggregates, api return { body: `${envelopeHeaders}\n${itemHeaders}\n${JSON.stringify(session)}`, type, - url: api.getEnvelopeEndpointWithUrlEncodedAuth(), + url: getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel), }; } /** Creates a SentryRequest from an event. */ -export function eventToSentryRequest(event: Event, api: API): SentryRequest { +export function eventToSentryRequest(event: Event, api: APIDetails): SentryRequest { const sdkInfo = getSdkMetadataForEnvelopeHeader(api); const eventType = event.type || 'event'; - const useEnvelope = eventType === 'transaction' || api.forceEnvelope(); + const useEnvelope = eventType === 'transaction' || !!api.tunnel; const { transactionSampling, ...metadata } = event.debug_meta || {}; const { method: samplingMethod, rate: sampleRate } = transactionSampling || {}; @@ -65,7 +65,9 @@ export function eventToSentryRequest(event: Event, api: API): SentryRequest { const req: SentryRequest = { body: JSON.stringify(sdkInfo ? enhanceEventWithSdkInfo(event, api.metadata.sdk) : event), type: eventType, - url: useEnvelope ? api.getEnvelopeEndpointWithUrlEncodedAuth() : api.getStoreEndpointWithUrlEncodedAuth(), + url: useEnvelope + ? getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel) + : getStoreEndpointWithUrlEncodedAuth(api.dsn), }; // https://develop.sentry.dev/sdk/envelopes/ @@ -79,7 +81,7 @@ export function eventToSentryRequest(event: Event, api: API): SentryRequest { event_id: event.event_id, sent_at: new Date().toISOString(), ...(sdkInfo && { sdk: sdkInfo }), - ...(api.forceEnvelope() && { dsn: api.getDsn().toString() }), + ...(!!api.tunnel && { dsn: api.dsn.toString() }), }); 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 4ca0b381ed09..fd2e97b6d77d 100644 --- a/packages/core/test/lib/api.test.ts +++ b/packages/core/test/lib/api.test.ts @@ -1,6 +1,7 @@ +/* eslint-disable deprecation/deprecation */ import { Dsn } from '@sentry/utils'; -import { API, getReportDialogEndpoint } from '../../src/api'; +import { API, getReportDialogEndpoint, getRequestHeaders } from '../../src/api'; const ingestDsn = 'https://abc@xxxx.ingest.sentry.io:1234/subpath/123'; const dsnPublic = 'https://abc@sentry.io:1234/subpath/123'; @@ -24,12 +25,12 @@ describe('API', () => { }); test('getRequestHeaders', () => { - expect(new API(dsnPublic).getRequestHeaders('a', '1.0')).toMatchObject({ + expect(getRequestHeaders(new Dsn(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(new API(legacyDsn).getRequestHeaders('a', '1.0')).toMatchObject({ + expect(getRequestHeaders(new Dsn(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$/, diff --git a/packages/core/test/lib/request.test.ts b/packages/core/test/lib/request.test.ts index 3419874d1a97..6e27bb26a4af 100644 --- a/packages/core/test/lib/request.test.ts +++ b/packages/core/test/lib/request.test.ts @@ -1,6 +1,6 @@ import { DebugMeta, Event, SentryRequest, TransactionSamplingMethod } from '@sentry/types'; -import { API } from '../../src/api'; +import { initAPIDetails } from '../../src/api'; import { eventToSentryRequest, sessionToSentryRequest } from '../../src/request'; const ingestDsn = 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012'; @@ -8,7 +8,7 @@ const ingestUrl = 'https://squirrelchasers.ingest.sentry.io/api/12312012/envelope/?sentry_key=dogsarebadatkeepingsecrets&sentry_version=7'; const tunnel = 'https://hello.com/world'; -const api = new API(ingestDsn, { +const api = initAPIDetails(ingestDsn, { sdk: { integrations: ['AWSLambda'], name: 'sentry.javascript.browser', @@ -132,15 +132,15 @@ describe('eventToSentryRequest', () => { }); it('uses tunnel as the url if it is configured', () => { - const tunnelRequest = eventToSentryRequest(event, new API(ingestDsn, {}, tunnel)); + const tunnelRequest = eventToSentryRequest(event, initAPIDetails(ingestDsn, {}, tunnel)); expect(tunnelRequest.url).toEqual(tunnel); - const defaultRequest = eventToSentryRequest(event, new API(ingestDsn, {})); + const defaultRequest = eventToSentryRequest(event, initAPIDetails(ingestDsn, {})); expect(defaultRequest.url).toEqual(ingestUrl); }); it('adds dsn to envelope header if tunnel is configured', () => { - const result = eventToSentryRequest(event, new API(ingestDsn, {}, tunnel)); + const result = eventToSentryRequest(event, initAPIDetails(ingestDsn, {}, tunnel)); const envelope = parseEnvelopeRequest(result); expect(envelope.envelopeHeader).toEqual( @@ -153,7 +153,7 @@ describe('eventToSentryRequest', () => { it('adds default "event" item type to item header if tunnel is configured', () => { delete event.type; - const result = eventToSentryRequest(event, new API(ingestDsn, {}, tunnel)); + const result = eventToSentryRequest(event, initAPIDetails(ingestDsn, {}, tunnel)); const envelope = parseEnvelopeRequest(result); expect(envelope.itemHeader).toEqual( @@ -193,15 +193,15 @@ describe('sessionToSentryRequest', () => { }); it('uses tunnel as the url if it is configured', () => { - const tunnelRequest = sessionToSentryRequest({ aggregates: [] }, new API(ingestDsn, {}, tunnel)); + const tunnelRequest = sessionToSentryRequest({ aggregates: [] }, initAPIDetails(ingestDsn, {}, tunnel)); expect(tunnelRequest.url).toEqual(tunnel); - const defaultRequest = sessionToSentryRequest({ aggregates: [] }, new API(ingestDsn, {})); + const defaultRequest = sessionToSentryRequest({ aggregates: [] }, initAPIDetails(ingestDsn, {})); expect(defaultRequest.url).toEqual(ingestUrl); }); it('adds dsn to envelope header if tunnel is configured', () => { - const result = sessionToSentryRequest({ aggregates: [] }, new API(ingestDsn, {}, tunnel)); + const result = sessionToSentryRequest({ aggregates: [] }, initAPIDetails(ingestDsn, {}, tunnel)); const envelope = parseEnvelopeRequest(result); expect(envelope.envelopeHeader).toEqual( diff --git a/packages/node/src/transports/base/index.ts b/packages/node/src/transports/base/index.ts index 0cfa3f78943f..11260113ab62 100644 --- a/packages/node/src/transports/base/index.ts +++ b/packages/node/src/transports/base/index.ts @@ -1,4 +1,4 @@ -import { API, SDK_VERSION } from '@sentry/core'; +import { APIDetails, getRequestHeaders, initAPIDetails, SDK_VERSION } from '@sentry/core'; import { DsnProtocol, Event, @@ -41,7 +41,7 @@ export abstract class BaseTransport implements Transport { public client?: http.Agent | https.Agent; /** API object */ - protected _api: API; + protected _api: APIDetails; /** A simple buffer holding all requests. */ protected readonly _buffer: PromiseBuffer = new PromiseBuffer(30); @@ -51,7 +51,8 @@ export abstract class BaseTransport implements Transport { /** Create instance and set this.dsn */ public constructor(public options: TransportOptions) { - this._api = new API(options.dsn, options._metadata, options.tunnel); + // eslint-disable-next-line deprecation/deprecation + this._api = initAPIDetails(options.dsn, options._metadata, options.tunnel); } /** Default function used to parse URLs */ @@ -89,7 +90,7 @@ export abstract class BaseTransport implements Transport { return proxy; } - const { host, port } = this._api.getDsn(); + const { host, port } = this._api.dsn; for (const np of no_proxy.split(',')) { if (host.endsWith(np) || `${host}:${port}`.endsWith(np)) { return; @@ -102,7 +103,7 @@ export abstract class BaseTransport implements Transport { /** Returns a build request option object used by request */ protected _getRequestOptions(urlParts: URLParts): http.RequestOptions | https.RequestOptions { const headers = { - ...this._api.getRequestHeaders(SDK_NAME, SDK_VERSION), + ...getRequestHeaders(this._api.dsn, SDK_NAME, SDK_VERSION), ...this.options.headers, }; const { hostname, pathname, port, protocol } = urlParts;