prepareEvent.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. import { uuid4, dateTimestampInSeconds, addExceptionMechanism, truncate, GLOBAL_OBJ, normalize } from '@sentry/utils';
  2. import { DEFAULT_ENVIRONMENT } from '../constants.js';
  3. import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors.js';
  4. import { getGlobalScope, Scope } from '../scope.js';
  5. import { mergeScopeData, applyScopeDataToEvent } from './applyScopeDataToEvent.js';
  6. import { spanToJSON } from './spanUtils.js';
  7. /**
  8. * This type makes sure that we get either a CaptureContext, OR an EventHint.
  9. * It does not allow mixing them, which could lead to unexpected outcomes, e.g. this is disallowed:
  10. * { user: { id: '123' }, mechanism: { handled: false } }
  11. */
  12. /**
  13. * Adds common information to events.
  14. *
  15. * The information includes release and environment from `options`,
  16. * breadcrumbs and context (extra, tags and user) from the scope.
  17. *
  18. * Information that is already present in the event is never overwritten. For
  19. * nested objects, such as the context, keys are merged.
  20. *
  21. * Note: This also triggers callbacks for `addGlobalEventProcessor`, but not `beforeSend`.
  22. *
  23. * @param event The original event.
  24. * @param hint May contain additional information about the original exception.
  25. * @param scope A scope containing event metadata.
  26. * @returns A new event with more information.
  27. * @hidden
  28. */
  29. function prepareEvent(
  30. options,
  31. event,
  32. hint,
  33. scope,
  34. client,
  35. isolationScope,
  36. ) {
  37. const { normalizeDepth = 3, normalizeMaxBreadth = 1000 } = options;
  38. const prepared = {
  39. ...event,
  40. event_id: event.event_id || hint.event_id || uuid4(),
  41. timestamp: event.timestamp || dateTimestampInSeconds(),
  42. };
  43. const integrations = hint.integrations || options.integrations.map(i => i.name);
  44. applyClientOptions(prepared, options);
  45. applyIntegrationsMetadata(prepared, integrations);
  46. // Only put debug IDs onto frames for error events.
  47. if (event.type === undefined) {
  48. applyDebugIds(prepared, options.stackParser);
  49. }
  50. // If we have scope given to us, use it as the base for further modifications.
  51. // This allows us to prevent unnecessary copying of data if `captureContext` is not provided.
  52. const finalScope = getFinalScope(scope, hint.captureContext);
  53. if (hint.mechanism) {
  54. addExceptionMechanism(prepared, hint.mechanism);
  55. }
  56. const clientEventProcessors = client && client.getEventProcessors ? client.getEventProcessors() : [];
  57. // This should be the last thing called, since we want that
  58. // {@link Hub.addEventProcessor} gets the finished prepared event.
  59. // Merge scope data together
  60. const data = getGlobalScope().getScopeData();
  61. if (isolationScope) {
  62. const isolationData = isolationScope.getScopeData();
  63. mergeScopeData(data, isolationData);
  64. }
  65. if (finalScope) {
  66. const finalScopeData = finalScope.getScopeData();
  67. mergeScopeData(data, finalScopeData);
  68. }
  69. const attachments = [...(hint.attachments || []), ...data.attachments];
  70. if (attachments.length) {
  71. hint.attachments = attachments;
  72. }
  73. applyScopeDataToEvent(prepared, data);
  74. // TODO (v8): Update this order to be: Global > Client > Scope
  75. const eventProcessors = [
  76. ...clientEventProcessors,
  77. // eslint-disable-next-line deprecation/deprecation
  78. ...getGlobalEventProcessors(),
  79. // Run scope event processors _after_ all other processors
  80. ...data.eventProcessors,
  81. ];
  82. const result = notifyEventProcessors(eventProcessors, prepared, hint);
  83. return result.then(evt => {
  84. if (evt) {
  85. // We apply the debug_meta field only after all event processors have ran, so that if any event processors modified
  86. // file names (e.g.the RewriteFrames integration) the filename -> debug ID relationship isn't destroyed.
  87. // This should not cause any PII issues, since we're only moving data that is already on the event and not adding
  88. // any new data
  89. applyDebugMeta(evt);
  90. }
  91. if (typeof normalizeDepth === 'number' && normalizeDepth > 0) {
  92. return normalizeEvent(evt, normalizeDepth, normalizeMaxBreadth);
  93. }
  94. return evt;
  95. });
  96. }
  97. /**
  98. * Enhances event using the client configuration.
  99. * It takes care of all "static" values like environment, release and `dist`,
  100. * as well as truncating overly long values.
  101. * @param event event instance to be enhanced
  102. */
  103. function applyClientOptions(event, options) {
  104. const { environment, release, dist, maxValueLength = 250 } = options;
  105. if (!('environment' in event)) {
  106. event.environment = 'environment' in options ? environment : DEFAULT_ENVIRONMENT;
  107. }
  108. if (event.release === undefined && release !== undefined) {
  109. event.release = release;
  110. }
  111. if (event.dist === undefined && dist !== undefined) {
  112. event.dist = dist;
  113. }
  114. if (event.message) {
  115. event.message = truncate(event.message, maxValueLength);
  116. }
  117. const exception = event.exception && event.exception.values && event.exception.values[0];
  118. if (exception && exception.value) {
  119. exception.value = truncate(exception.value, maxValueLength);
  120. }
  121. const request = event.request;
  122. if (request && request.url) {
  123. request.url = truncate(request.url, maxValueLength);
  124. }
  125. }
  126. const debugIdStackParserCache = new WeakMap();
  127. /**
  128. * Puts debug IDs into the stack frames of an error event.
  129. */
  130. function applyDebugIds(event, stackParser) {
  131. const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
  132. if (!debugIdMap) {
  133. return;
  134. }
  135. let debugIdStackFramesCache;
  136. const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(stackParser);
  137. if (cachedDebugIdStackFrameCache) {
  138. debugIdStackFramesCache = cachedDebugIdStackFrameCache;
  139. } else {
  140. debugIdStackFramesCache = new Map();
  141. debugIdStackParserCache.set(stackParser, debugIdStackFramesCache);
  142. }
  143. // Build a map of filename -> debug_id
  144. const filenameDebugIdMap = Object.keys(debugIdMap).reduce((acc, debugIdStackTrace) => {
  145. let parsedStack;
  146. const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace);
  147. if (cachedParsedStack) {
  148. parsedStack = cachedParsedStack;
  149. } else {
  150. parsedStack = stackParser(debugIdStackTrace);
  151. debugIdStackFramesCache.set(debugIdStackTrace, parsedStack);
  152. }
  153. for (let i = parsedStack.length - 1; i >= 0; i--) {
  154. const stackFrame = parsedStack[i];
  155. if (stackFrame.filename) {
  156. acc[stackFrame.filename] = debugIdMap[debugIdStackTrace];
  157. break;
  158. }
  159. }
  160. return acc;
  161. }, {});
  162. try {
  163. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  164. event.exception.values.forEach(exception => {
  165. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  166. exception.stacktrace.frames.forEach(frame => {
  167. if (frame.filename) {
  168. frame.debug_id = filenameDebugIdMap[frame.filename];
  169. }
  170. });
  171. });
  172. } catch (e) {
  173. // To save bundle size we're just try catching here instead of checking for the existence of all the different objects.
  174. }
  175. }
  176. /**
  177. * Moves debug IDs from the stack frames of an error event into the debug_meta field.
  178. */
  179. function applyDebugMeta(event) {
  180. // Extract debug IDs and filenames from the stack frames on the event.
  181. const filenameDebugIdMap = {};
  182. try {
  183. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  184. event.exception.values.forEach(exception => {
  185. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  186. exception.stacktrace.frames.forEach(frame => {
  187. if (frame.debug_id) {
  188. if (frame.abs_path) {
  189. filenameDebugIdMap[frame.abs_path] = frame.debug_id;
  190. } else if (frame.filename) {
  191. filenameDebugIdMap[frame.filename] = frame.debug_id;
  192. }
  193. delete frame.debug_id;
  194. }
  195. });
  196. });
  197. } catch (e) {
  198. // To save bundle size we're just try catching here instead of checking for the existence of all the different objects.
  199. }
  200. if (Object.keys(filenameDebugIdMap).length === 0) {
  201. return;
  202. }
  203. // Fill debug_meta information
  204. event.debug_meta = event.debug_meta || {};
  205. event.debug_meta.images = event.debug_meta.images || [];
  206. const images = event.debug_meta.images;
  207. Object.keys(filenameDebugIdMap).forEach(filename => {
  208. images.push({
  209. type: 'sourcemap',
  210. code_file: filename,
  211. debug_id: filenameDebugIdMap[filename],
  212. });
  213. });
  214. }
  215. /**
  216. * This function adds all used integrations to the SDK info in the event.
  217. * @param event The event that will be filled with all integrations.
  218. */
  219. function applyIntegrationsMetadata(event, integrationNames) {
  220. if (integrationNames.length > 0) {
  221. event.sdk = event.sdk || {};
  222. event.sdk.integrations = [...(event.sdk.integrations || []), ...integrationNames];
  223. }
  224. }
  225. /**
  226. * Applies `normalize` function on necessary `Event` attributes to make them safe for serialization.
  227. * Normalized keys:
  228. * - `breadcrumbs.data`
  229. * - `user`
  230. * - `contexts`
  231. * - `extra`
  232. * @param event Event
  233. * @returns Normalized event
  234. */
  235. function normalizeEvent(event, depth, maxBreadth) {
  236. if (!event) {
  237. return null;
  238. }
  239. const normalized = {
  240. ...event,
  241. ...(event.breadcrumbs && {
  242. breadcrumbs: event.breadcrumbs.map(b => ({
  243. ...b,
  244. ...(b.data && {
  245. data: normalize(b.data, depth, maxBreadth),
  246. }),
  247. })),
  248. }),
  249. ...(event.user && {
  250. user: normalize(event.user, depth, maxBreadth),
  251. }),
  252. ...(event.contexts && {
  253. contexts: normalize(event.contexts, depth, maxBreadth),
  254. }),
  255. ...(event.extra && {
  256. extra: normalize(event.extra, depth, maxBreadth),
  257. }),
  258. };
  259. // event.contexts.trace stores information about a Transaction. Similarly,
  260. // event.spans[] stores information about child Spans. Given that a
  261. // Transaction is conceptually a Span, normalization should apply to both
  262. // Transactions and Spans consistently.
  263. // For now the decision is to skip normalization of Transactions and Spans,
  264. // so this block overwrites the normalized event to add back the original
  265. // Transaction information prior to normalization.
  266. if (event.contexts && event.contexts.trace && normalized.contexts) {
  267. normalized.contexts.trace = event.contexts.trace;
  268. // event.contexts.trace.data may contain circular/dangerous data so we need to normalize it
  269. if (event.contexts.trace.data) {
  270. normalized.contexts.trace.data = normalize(event.contexts.trace.data, depth, maxBreadth);
  271. }
  272. }
  273. // event.spans[].data may contain circular/dangerous data so we need to normalize it
  274. if (event.spans) {
  275. normalized.spans = event.spans.map(span => {
  276. const data = spanToJSON(span).data;
  277. if (data) {
  278. // This is a bit weird, as we generally have `Span` instances here, but to be safe we do not assume so
  279. // eslint-disable-next-line deprecation/deprecation
  280. span.data = normalize(data, depth, maxBreadth);
  281. }
  282. return span;
  283. });
  284. }
  285. return normalized;
  286. }
  287. function getFinalScope(scope, captureContext) {
  288. if (!captureContext) {
  289. return scope;
  290. }
  291. const finalScope = scope ? scope.clone() : new Scope();
  292. finalScope.update(captureContext);
  293. return finalScope;
  294. }
  295. /**
  296. * Parse either an `EventHint` directly, or convert a `CaptureContext` to an `EventHint`.
  297. * This is used to allow to update method signatures that used to accept a `CaptureContext` but should now accept an `EventHint`.
  298. */
  299. function parseEventHintOrCaptureContext(
  300. hint,
  301. ) {
  302. if (!hint) {
  303. return undefined;
  304. }
  305. // If you pass a Scope or `() => Scope` as CaptureContext, we just return this as captureContext
  306. if (hintIsScopeOrFunction(hint)) {
  307. return { captureContext: hint };
  308. }
  309. if (hintIsScopeContext(hint)) {
  310. return {
  311. captureContext: hint,
  312. };
  313. }
  314. return hint;
  315. }
  316. function hintIsScopeOrFunction(
  317. hint,
  318. ) {
  319. return hint instanceof Scope || typeof hint === 'function';
  320. }
  321. const captureContextKeys = [
  322. 'user',
  323. 'level',
  324. 'extra',
  325. 'contexts',
  326. 'tags',
  327. 'fingerprint',
  328. 'requestSession',
  329. 'propagationContext',
  330. ] ;
  331. function hintIsScopeContext(hint) {
  332. return Object.keys(hint).some(key => captureContextKeys.includes(key ));
  333. }
  334. export { applyDebugIds, applyDebugMeta, parseEventHintOrCaptureContext, prepareEvent };
  335. //# sourceMappingURL=prepareEvent.js.map