envelope.js
1 import { dsnToString } from './dsn.js'; 2 import { normalize } from './normalize.js'; 3 import { dropUndefinedKeys } from './object.js'; 4 5 /** 6 * Creates an envelope. 7 * Make sure to always explicitly provide the generic to this function 8 * so that the envelope types resolve correctly. 9 */ 10 function createEnvelope(headers, items = []) { 11 return [headers, items] ; 12 } 13 14 /** 15 * Add an item to an envelope. 16 * Make sure to always explicitly provide the generic to this function 17 * so that the envelope types resolve correctly. 18 */ 19 function addItemToEnvelope(envelope, newItem) { 20 const [headers, items] = envelope; 21 return [headers, [...items, newItem]] ; 22 } 23 24 /** 25 * Convenience function to loop through the items and item types of an envelope. 26 * (This function was mostly created because working with envelope types is painful at the moment) 27 * 28 * If the callback returns true, the rest of the items will be skipped. 29 */ 30 function forEachEnvelopeItem( 31 envelope, 32 callback, 33 ) { 34 const envelopeItems = envelope[1]; 35 36 for (const envelopeItem of envelopeItems) { 37 const envelopeItemType = envelopeItem[0].type; 38 const result = callback(envelopeItem, envelopeItemType); 39 40 if (result) { 41 return true; 42 } 43 } 44 45 return false; 46 } 47 48 /** 49 * Returns true if the envelope contains any of the given envelope item types 50 */ 51 function envelopeContainsItemType(envelope, types) { 52 return forEachEnvelopeItem(envelope, (_, type) => types.includes(type)); 53 } 54 55 /** 56 * Encode a string to UTF8. 57 */ 58 function encodeUTF8(input, textEncoder) { 59 const utf8 = textEncoder || new TextEncoder(); 60 return utf8.encode(input); 61 } 62 63 /** 64 * Serializes an envelope. 65 */ 66 function serializeEnvelope(envelope, textEncoder) { 67 const [envHeaders, items] = envelope; 68 69 // Initially we construct our envelope as a string and only convert to binary chunks if we encounter binary data 70 let parts = JSON.stringify(envHeaders); 71 72 function append(next) { 73 if (typeof parts === 'string') { 74 parts = typeof next === 'string' ? parts + next : [encodeUTF8(parts, textEncoder), next]; 75 } else { 76 parts.push(typeof next === 'string' ? encodeUTF8(next, textEncoder) : next); 77 } 78 } 79 80 for (const item of items) { 81 const [itemHeaders, payload] = item; 82 83 append(`\n${JSON.stringify(itemHeaders)}\n`); 84 85 if (typeof payload === 'string' || payload instanceof Uint8Array) { 86 append(payload); 87 } else { 88 let stringifiedPayload; 89 try { 90 stringifiedPayload = JSON.stringify(payload); 91 } catch (e) { 92 // In case, despite all our efforts to keep `payload` circular-dependency-free, `JSON.strinify()` still 93 // fails, we try again after normalizing it again with infinite normalization depth. This of course has a 94 // performance impact but in this case a performance hit is better than throwing. 95 stringifiedPayload = JSON.stringify(normalize(payload)); 96 } 97 append(stringifiedPayload); 98 } 99 } 100 101 return typeof parts === 'string' ? parts : concatBuffers(parts); 102 } 103 104 function concatBuffers(buffers) { 105 const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0); 106 107 const merged = new Uint8Array(totalLength); 108 let offset = 0; 109 for (const buffer of buffers) { 110 merged.set(buffer, offset); 111 offset += buffer.length; 112 } 113 114 return merged; 115 } 116 117 /** 118 * Parses an envelope 119 */ 120 function parseEnvelope( 121 env, 122 textEncoder, 123 textDecoder, 124 ) { 125 let buffer = typeof env === 'string' ? textEncoder.encode(env) : env; 126 127 function readBinary(length) { 128 const bin = buffer.subarray(0, length); 129 // Replace the buffer with the remaining data excluding trailing newline 130 buffer = buffer.subarray(length + 1); 131 return bin; 132 } 133 134 function readJson() { 135 let i = buffer.indexOf(0xa); 136 // If we couldn't find a newline, we must have found the end of the buffer 137 if (i < 0) { 138 i = buffer.length; 139 } 140 141 return JSON.parse(textDecoder.decode(readBinary(i))) ; 142 } 143 144 const envelopeHeader = readJson(); 145 // eslint-disable-next-line @typescript-eslint/no-explicit-any 146 const items = []; 147 148 while (buffer.length) { 149 const itemHeader = readJson(); 150 const binaryLength = typeof itemHeader.length === 'number' ? itemHeader.length : undefined; 151 152 items.push([itemHeader, binaryLength ? readBinary(binaryLength) : readJson()]); 153 } 154 155 return [envelopeHeader, items]; 156 } 157 158 /** 159 * Creates attachment envelope items 160 */ 161 function createAttachmentEnvelopeItem( 162 attachment, 163 textEncoder, 164 ) { 165 const buffer = typeof attachment.data === 'string' ? encodeUTF8(attachment.data, textEncoder) : attachment.data; 166 167 return [ 168 dropUndefinedKeys({ 169 type: 'attachment', 170 length: buffer.length, 171 filename: attachment.filename, 172 content_type: attachment.contentType, 173 attachment_type: attachment.attachmentType, 174 }), 175 buffer, 176 ]; 177 } 178 179 const ITEM_TYPE_TO_DATA_CATEGORY_MAP = { 180 session: 'session', 181 sessions: 'session', 182 attachment: 'attachment', 183 transaction: 'transaction', 184 event: 'error', 185 client_report: 'internal', 186 user_report: 'default', 187 profile: 'profile', 188 replay_event: 'replay', 189 replay_recording: 'replay', 190 check_in: 'monitor', 191 }; 192 193 /** 194 * Maps the type of an envelope item to a data category. 195 */ 196 function envelopeItemTypeToDataCategory(type) { 197 return ITEM_TYPE_TO_DATA_CATEGORY_MAP[type]; 198 } 199 200 /** Extracts the minimal SDK info from from the metadata or an events */ 201 function getSdkMetadataForEnvelopeHeader(metadataOrEvent) { 202 if (!metadataOrEvent || !metadataOrEvent.sdk) { 203 return; 204 } 205 const { name, version } = metadataOrEvent.sdk; 206 return { name, version }; 207 } 208 209 /** 210 * Creates event envelope headers, based on event, sdk info and tunnel 211 * Note: This function was extracted from the core package to make it available in Replay 212 */ 213 function createEventEnvelopeHeaders( 214 event, 215 sdkInfo, 216 tunnel, 217 dsn, 218 ) { 219 const dynamicSamplingContext = event.sdkProcessingMetadata && event.sdkProcessingMetadata.dynamicSamplingContext; 220 return { 221 event_id: event.event_id , 222 sent_at: new Date().toISOString(), 223 ...(sdkInfo && { sdk: sdkInfo }), 224 ...(!!tunnel && { dsn: dsnToString(dsn) }), 225 ...(dynamicSamplingContext && { 226 trace: dropUndefinedKeys({ ...dynamicSamplingContext }), 227 }), 228 }; 229 } 230 231 export { addItemToEnvelope, createAttachmentEnvelopeItem, createEnvelope, createEventEnvelopeHeaders, envelopeContainsItemType, envelopeItemTypeToDataCategory, forEachEnvelopeItem, getSdkMetadataForEnvelopeHeader, parseEnvelope, serializeEnvelope }; 232 //# sourceMappingURL=envelope.js.map