-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: Send transactions in envelopes #2553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c5fbedf
8afcb73
beb44b5
181781d
b0339da
d8d7569
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,15 +5,20 @@ import { PromiseBuffer, SentryError } from '@sentry/utils'; | |
/** Base Transport class implementation */ | ||
export abstract class BaseTransport implements Transport { | ||
/** | ||
* @inheritDoc | ||
* @deprecated | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This depends on whether a single transport will eventually have to deal with multiple endpoints like now, or whether we'll have independent transports, each having a fixed target endpoint URL. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should stick with one transport with multiple methods for getting url. Although I'm not sure if we should deprecate this attribute just yet, as we are not using envelopes for errors. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The transport now always use the |
||
*/ | ||
public url: string; | ||
|
||
/** Helper to get Sentry API endpoints. */ | ||
protected readonly _api: API; | ||
|
||
/** A simple buffer holding all requests. */ | ||
protected readonly _buffer: PromiseBuffer<Response> = new PromiseBuffer(30); | ||
|
||
public constructor(public options: TransportOptions) { | ||
this.url = new API(this.options.dsn).getStoreEndpointWithUrlEncodedAuth(); | ||
this._api = new API(this.options.dsn); | ||
// tslint:disable-next-line:deprecation | ||
this.url = this._api.getStoreEndpointWithUrlEncodedAuth(); | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,29 +17,59 @@ export class API { | |
return this._dsnObject; | ||
} | ||
|
||
/** Returns a string with auth headers in the url to the store endpoint. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was the documentation of |
||
/** Returns the prefix to construct Sentry ingestion API endpoints. */ | ||
public getBaseApiEndpoint(): string { | ||
const dsn = this._dsnObject; | ||
const protocol = dsn.protocol ? `${dsn.protocol}:` : ''; | ||
const port = dsn.port ? `:${dsn.port}` : ''; | ||
return `${protocol}//${dsn.host}${port}${dsn.path ? `/${dsn.path}` : ''}/api/`; | ||
} | ||
|
||
/** Returns the store endpoint URL. */ | ||
public getStoreEndpoint(): string { | ||
return `${this._getBaseUrl()}${this.getStoreEndpointPath()}`; | ||
return this._getIngestEndpoint('store'); | ||
} | ||
|
||
/** 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 { | ||
const base = this.getBaseApiEndpoint(); | ||
const dsn = this._dsnObject; | ||
return `${base}${dsn.projectId}/${target}/`; | ||
} | ||
|
||
/** Returns the store endpoint with auth added in url encoded. */ | ||
/** | ||
* 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. | ||
*/ | ||
public getStoreEndpointWithUrlEncodedAuth(): string { | ||
rhcarvalho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return `${this.getStoreEndpoint()}?${this._encodedAuth()}`; | ||
} | ||
|
||
/** | ||
* 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. | ||
*/ | ||
public getEnvelopeEndpointWithUrlEncodedAuth(): string { | ||
return `${this._getEnvelopeEndpoint()}?${this._encodedAuth()}`; | ||
} | ||
|
||
/** Returns a URL-encoded string with auth config suitable for a query string. */ | ||
private _encodedAuth(): string { | ||
const dsn = this._dsnObject; | ||
const auth = { | ||
sentry_key: dsn.user, // sentry_key is currently used in tracing integration to identify internal sentry requests | ||
// We send only the minimum set of required information. See | ||
// https://github.com/getsentry/sentry-javascript/issues/2572. | ||
sentry_key: dsn.user, | ||
sentry_version: SENTRY_API_VERSION, | ||
}; | ||
// Auth is intentionally sent as part of query string (NOT as custom HTTP header) | ||
// to avoid preflight CORS requests | ||
return `${this.getStoreEndpoint()}?${urlEncode(auth)}`; | ||
} | ||
|
||
/** Returns the base path of the url including the port. */ | ||
private _getBaseUrl(): string { | ||
const dsn = this._dsnObject; | ||
const protocol = dsn.protocol ? `${dsn.protocol}:` : ''; | ||
const port = dsn.port ? `:${dsn.port}` : ''; | ||
return `${protocol}//${dsn.host}${port}`; | ||
return urlEncode(auth); | ||
} | ||
|
||
/** Returns only the path component for the store endpoint. */ | ||
|
@@ -48,7 +78,11 @@ export class API { | |
return `${dsn.path ? `/${dsn.path}` : ''}/api/${dsn.projectId}/store/`; | ||
} | ||
|
||
/** Returns an object that can be used in request headers. */ | ||
/** | ||
* Returns an object that can be used in request headers. | ||
* | ||
* @deprecated in favor of `getStoreEndpointWithUrlEncodedAuth` and `getEnvelopeEndpointWithUrlEncodedAuth`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
*/ | ||
public getRequestHeaders(clientName: string, clientVersion: string): { [key: string]: string } { | ||
const dsn = this._dsnObject; | ||
const header = [`Sentry sentry_version=${SENTRY_API_VERSION}`]; | ||
|
@@ -71,7 +105,7 @@ export class API { | |
} = {}, | ||
): string { | ||
const dsn = this._dsnObject; | ||
const endpoint = `${this._getBaseUrl()}${dsn.path ? `/${dsn.path}` : ''}/api/embed/error-page/`; | ||
const endpoint = `${this.getBaseApiEndpoint()}embed/error-page/`; | ||
|
||
const encodedOptions = []; | ||
encodedOptions.push(`dsn=${dsn.toString()}`); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { Event } from '@sentry/types'; | ||
|
||
import { API } from './api'; | ||
|
||
/** A generic client request. */ | ||
interface SentryRequest { | ||
body: string; | ||
url: string; | ||
// headers would contain auth & content-type headers for @sentry/node, but | ||
// since @sentry/browser avoids custom headers to prevent CORS preflight | ||
// requests, we can use the same approach for @sentry/browser and @sentry/node | ||
// for simplicity -- no headers involved. | ||
// headers: { [key: string]: string }; | ||
} | ||
|
||
/** Creates a SentryRequest from an event. */ | ||
export function eventToSentryRequest(event: Event, api: API): SentryRequest { | ||
const useEnvelope = event.type === 'transaction'; | ||
|
||
const req: SentryRequest = { | ||
body: JSON.stringify(event), | ||
url: useEnvelope ? api.getEnvelopeEndpointWithUrlEncodedAuth() : api.getStoreEndpointWithUrlEncodedAuth(), | ||
}; | ||
|
||
// https://develop.sentry.dev/sdk/envelopes/ | ||
|
||
// Since we don't need to manipulate envelopes nor store them, there is no | ||
// exported concept of an Envelope with operations including serialization and | ||
// deserialization. Instead, we only implement a minimal subset of the spec to | ||
// serialize events inline here. | ||
if (useEnvelope) { | ||
const envelopeHeaders = JSON.stringify({ | ||
event_id: event.event_id, | ||
sent_at: new Date().toISOString(), | ||
}); | ||
const itemHeaders = JSON.stringify({ | ||
type: event.type, | ||
// The content-type is assumed to be 'application/json' and not part of | ||
// the current spec for transaction items, so we don't bloat the request | ||
// body with it. | ||
// | ||
// content_type: 'application/json', | ||
// | ||
// The length is optional. It must be the number of bytes in req.Body | ||
// encoded as UTF-8. Since the server can figure this out and would | ||
// otherwise refuse events that report the length incorrectly, we decided | ||
// not to send the length to avoid problems related to reporting the wrong | ||
// size and to reduce request body size. | ||
// | ||
// length: new TextEncoder().encode(req.body).length, | ||
}); | ||
// The trailing newline is optional. We intentionally don't send it to avoid | ||
// sending unnecessary bytes. | ||
// | ||
// const envelope = `${envelopeHeaders}\n${itemHeaders}\n${req.body}\n`; | ||
const envelope = `${envelopeHeaders}\n${itemHeaders}\n${req.body}`; | ||
req.body = envelope; | ||
} | ||
|
||
return req; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.