11import { Event , SdkInfo , SentryRequest , SentryRequestType , Session , SessionAggregates } from '@sentry/types' ;
2+ import { base64ToUnicode , logger } from '@sentry/utils' ;
23
34import { API } from './api' ;
45
@@ -12,19 +13,20 @@ function getSdkMetadataForEnvelopeHeader(api: API): SdkInfo | undefined {
1213}
1314
1415/**
15- * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key.
16- * Merge with existing data if any.
16+ * Add SDK metadata (name, version, packages, integrations) to the event.
17+ *
18+ * Mutates the object in place. If prior metadata exists, it will be merged with the given metadata.
1719 **/
18- function enhanceEventWithSdkInfo ( event : Event , sdkInfo ?: SdkInfo ) : Event {
20+ function enhanceEventWithSdkInfo ( event : Event , sdkInfo ?: SdkInfo ) : void {
1921 if ( ! sdkInfo ) {
20- return event ;
22+ return ;
2123 }
2224 event . sdk = event . sdk || { } ;
2325 event . sdk . name = event . sdk . name || sdkInfo . name ;
2426 event . sdk . version = event . sdk . version || sdkInfo . version ;
2527 event . sdk . integrations = [ ...( event . sdk . integrations || [ ] ) , ...( sdkInfo . integrations || [ ] ) ] ;
2628 event . sdk . packages = [ ...( event . sdk . packages || [ ] ) , ...( sdkInfo . packages || [ ] ) ] ;
27- return event ;
29+ return ;
2830}
2931
3032/** Creates a SentryRequest from a Session. */
@@ -54,61 +56,81 @@ export function eventToSentryRequest(event: Event, api: API): SentryRequest {
5456 const eventType = event . type || 'event' ;
5557 const useEnvelope = eventType === 'transaction' || api . forceEnvelope ( ) ;
5658
57- const { transactionSampling, ...metadata } = event . debug_meta || { } ;
58- const { method : samplingMethod , rate : sampleRate } = transactionSampling || { } ;
59- if ( Object . keys ( metadata ) . length === 0 ) {
60- delete event . debug_meta ;
61- } else {
62- event . debug_meta = metadata ;
63- }
59+ enhanceEventWithSdkInfo ( event , api . metadata . sdk ) ;
6460
65- const req : SentryRequest = {
66- body : JSON . stringify ( sdkInfo ? enhanceEventWithSdkInfo ( event , api . metadata . sdk ) : event ) ,
67- type : eventType ,
68- url : useEnvelope ? api . getEnvelopeEndpointWithUrlEncodedAuth ( ) : api . getStoreEndpointWithUrlEncodedAuth ( ) ,
69- } ;
61+ // Since we don't need to manipulate envelopes nor store them, there is no exported concept of an Envelope with
62+ // operations including serialization and deserialization. Instead, we only implement a minimal subset of the spec to
63+ // serialize events inline here. See https://develop.sentry.dev/sdk/envelopes/.
64+ if ( useEnvelope ) {
65+ // Extract header information from event
66+ const { transactionSampling, tracestate, ...metadata } = event . debug_meta || { } ;
67+ if ( Object . keys ( metadata ) . length === 0 ) {
68+ delete event . debug_meta ;
69+ } else {
70+ event . debug_meta = metadata ;
71+ }
7072
71- // https://develop.sentry.dev/sdk/envelopes/
73+ // the tracestate is stored in bas64-encoded JSON, but envelope header values are expected to be full JS values,
74+ // so we have to decode and reinflate it
75+ let reinflatedTracestate ;
76+ try {
77+ // Because transaction metadata passes through a number of locations (transactionContext, transaction, event during
78+ // processing, event as sent), each with different requirements, all of the parts are typed as optional. That said,
79+ // if we get to this point and either `tracestate` or `tracestate.sentry` are undefined, something's gone very wrong.
80+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
81+ const encodedSentryValue = tracestate ! . sentry ! . replace ( 'sentry=' , '' ) ;
82+ reinflatedTracestate = JSON . parse ( base64ToUnicode ( encodedSentryValue ) ) ;
83+ } catch ( err ) {
84+ logger . warn ( err ) ;
85+ }
7286
73- // Since we don't need to manipulate envelopes nor store them, there is no
74- // exported concept of an Envelope with operations including serialization and
75- // deserialization. Instead, we only implement a minimal subset of the spec to
76- // serialize events inline here.
77- if ( useEnvelope ) {
7887 const envelopeHeaders = JSON . stringify ( {
7988 event_id : event . event_id ,
8089 sent_at : new Date ( ) . toISOString ( ) ,
8190 ...( sdkInfo && { sdk : sdkInfo } ) ,
8291 ...( api . forceEnvelope ( ) && { dsn : api . getDsn ( ) . toString ( ) } ) ,
92+ ...( reinflatedTracestate && { trace : reinflatedTracestate } ) , // trace context for dynamic sampling on relay
8393 } ) ;
84- const itemHeaders = JSON . stringify ( {
85- type : eventType ,
8694
87- // TODO: Right now, sampleRate may or may not be defined (it won't be in the cases of inheritance and
88- // explicitly-set sampling decisions). Are we good with that?
89- sample_rates : [ { id : samplingMethod , rate : sampleRate } ] ,
95+ const itemHeaderEntries : { [ key : string ] : unknown } = {
96+ type : eventType ,
9097
91- // The content-type is assumed to be 'application/json' and not part of
92- // the current spec for transaction items, so we don't bloat the request
93- // body with it.
98+ // Note: as mentioned above, `content_type` and `length` were left out on purpose.
9499 //
95- // content_type: 'application/json',
100+ // `content_type`:
101+ // Assumed to be 'application/json' and not part of the current spec for transaction items. No point in bloating the
102+ // request body with it. (Would be `content_type: 'application/json'`.)
96103 //
97- // The length is optional. It must be the number of bytes in req.Body
98- // encoded as UTF-8. Since the server can figure this out and would
99- // otherwise refuse events that report the length incorrectly, we decided
100- // not to send the length to avoid problems related to reporting the wrong
101- // size and to reduce request body size.
102- //
103- // length: new TextEncoder().encode(req.body).length,
104- } ) ;
105- // The trailing newline is optional. We intentionally don't send it to avoid
106- // sending unnecessary bytes.
107- //
108- // const envelope = `${envelopeHeaders}\n${itemHeaders}\n${req.body}\n`;
109- const envelope = `${ envelopeHeaders } \n${ itemHeaders } \n${ req . body } ` ;
110- req . body = envelope ;
104+ // `length`:
105+ // Optional and equal to the number of bytes in `req.Body` encoded as UTF-8. Since the server can figure this out
106+ // and will refuse events that report the length incorrectly, we decided not to send the length to reduce request
107+ // body size and to avoid problems related to reporting the wrong size.(Would be
108+ // `length: new TextEncoder().encode(req.body).length`.)
109+ } ;
110+
111+ if ( eventType === 'transaction' ) {
112+ // TODO: Right now, `sampleRate` will be undefined in the cases of inheritance and explicitly-set sampling decisions.
113+ itemHeaderEntries . sample_rates = [ { id : transactionSampling ?. method , rate : transactionSampling ?. rate } ] ;
114+ }
115+
116+ const itemHeaders = JSON . stringify ( itemHeaderEntries ) ;
117+
118+ const eventJSON = JSON . stringify ( event ) ;
119+
120+ // The trailing newline is optional; leave it off to avoid sending unnecessary bytes. (Would be
121+ // `const envelope = `${envelopeHeaders}\n${itemHeaders}\n${req.body}\n`;`.)
122+ const envelope = `${ envelopeHeaders } \n${ itemHeaders } \n${ eventJSON } ` ;
123+
124+ return {
125+ body : envelope ,
126+ type : eventType ,
127+ url : api . getEnvelopeEndpointWithUrlEncodedAuth ( ) ,
128+ } ;
111129 }
112130
113- return req ;
131+ return {
132+ body : JSON . stringify ( event ) ,
133+ type : eventType ,
134+ url : api . getStoreEndpointWithUrlEncodedAuth ( ) ,
135+ } ;
114136}
0 commit comments