events.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. import {SymbolicatorStatus} from 'sentry/components/events/interfaces/types';
  2. import ConfigStore from 'sentry/stores/configStore';
  3. import type {
  4. BaseGroup,
  5. EntryException,
  6. EntryRequest,
  7. EntryThreads,
  8. EventMetadata,
  9. Group,
  10. GroupActivityAssigned,
  11. GroupTombstoneHelper,
  12. TreeLabelPart,
  13. } from 'sentry/types';
  14. import {
  15. EventOrGroupType,
  16. GroupActivityType,
  17. IssueCategory,
  18. IssueType,
  19. } from 'sentry/types';
  20. import type {Event, ExceptionValue, Frame, Thread} from 'sentry/types/event';
  21. import {EntryType} from 'sentry/types/event';
  22. import {defined} from 'sentry/utils';
  23. import type {BaseEventAnalyticsParams} from 'sentry/utils/analytics/workflowAnalyticsEvents';
  24. import {uniq} from 'sentry/utils/array/uniq';
  25. import {getDaysSinceDatePrecise} from 'sentry/utils/getDaysSinceDate';
  26. import {isMobilePlatform, isNativePlatform} from 'sentry/utils/platform';
  27. import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent';
  28. export function isTombstone(
  29. maybe: BaseGroup | Event | GroupTombstoneHelper
  30. ): maybe is GroupTombstoneHelper {
  31. return 'isTombstone' in maybe && maybe.isTombstone;
  32. }
  33. /**
  34. * Extract the display message from an event.
  35. */
  36. export function getMessage(
  37. event: Event | BaseGroup | GroupTombstoneHelper
  38. ): string | undefined {
  39. if (isTombstone(event)) {
  40. return event.culprit || '';
  41. }
  42. const {metadata, type, culprit} = event;
  43. switch (type) {
  44. case EventOrGroupType.ERROR:
  45. case EventOrGroupType.TRANSACTION:
  46. return metadata.value;
  47. case EventOrGroupType.CSP:
  48. return metadata.message;
  49. case EventOrGroupType.EXPECTCT:
  50. case EventOrGroupType.EXPECTSTAPLE:
  51. case EventOrGroupType.HPKP:
  52. return '';
  53. case EventOrGroupType.GENERIC:
  54. return metadata.value;
  55. default:
  56. return culprit || '';
  57. }
  58. }
  59. /**
  60. * Get the location from an event.
  61. */
  62. export function getLocation(event: Event | BaseGroup | GroupTombstoneHelper) {
  63. if (isTombstone(event)) {
  64. return undefined;
  65. }
  66. if (event.type === EventOrGroupType.ERROR && isNativePlatform(event.platform)) {
  67. return event.metadata.filename || undefined;
  68. }
  69. return undefined;
  70. }
  71. export function getTreeLabelPartDetails(part: TreeLabelPart) {
  72. // Note: This function also exists in Python in eventtypes/base.py, to make
  73. // porting efforts simpler it's recommended to keep both variants
  74. // structurally similar.
  75. if (typeof part === 'string') {
  76. return part;
  77. }
  78. const label = part?.function || part?.package || part?.filebase || part?.type;
  79. const classbase = part?.classbase;
  80. if (classbase) {
  81. return label ? `${classbase}.${label}` : classbase;
  82. }
  83. return label || '<unknown>';
  84. }
  85. function computeTitleWithTreeLabel(metadata: EventMetadata) {
  86. const {type, current_tree_label, finest_tree_label} = metadata;
  87. const treeLabel = current_tree_label || finest_tree_label;
  88. const formattedTreeLabel = treeLabel
  89. ? treeLabel.map(labelPart => getTreeLabelPartDetails(labelPart)).join(' | ')
  90. : undefined;
  91. if (!type) {
  92. return {
  93. title: formattedTreeLabel || metadata.function || '<unknown>',
  94. treeLabel,
  95. };
  96. }
  97. if (!formattedTreeLabel) {
  98. return {title: type, treeLabel: undefined};
  99. }
  100. return {
  101. title: `${type} | ${formattedTreeLabel}`,
  102. treeLabel: [{type}, ...(treeLabel ?? [])],
  103. };
  104. }
  105. export function getTitle(
  106. event: Event | BaseGroup | GroupTombstoneHelper,
  107. features: string[] = [],
  108. grouping = false
  109. ) {
  110. const {metadata, type, culprit, title} = event;
  111. const customTitle = metadata?.title;
  112. switch (type) {
  113. case EventOrGroupType.ERROR: {
  114. if (customTitle) {
  115. return {
  116. title: customTitle,
  117. subtitle: culprit,
  118. treeLabel: undefined,
  119. };
  120. }
  121. const displayTitleWithTreeLabel =
  122. !isTombstone(event) &&
  123. features.includes('grouping-title-ui') &&
  124. (grouping ||
  125. isNativePlatform(event.platform) ||
  126. isMobilePlatform(event.platform));
  127. if (displayTitleWithTreeLabel) {
  128. return {
  129. subtitle: culprit,
  130. ...computeTitleWithTreeLabel(metadata),
  131. };
  132. }
  133. return {
  134. subtitle: culprit,
  135. title: metadata.type || metadata.function || '<unknown>',
  136. treeLabel: undefined,
  137. };
  138. }
  139. case EventOrGroupType.CSP:
  140. case EventOrGroupType.NEL:
  141. return {
  142. title: customTitle ?? metadata.directive ?? '',
  143. subtitle: metadata.uri ?? '',
  144. treeLabel: undefined,
  145. };
  146. case EventOrGroupType.EXPECTCT:
  147. case EventOrGroupType.EXPECTSTAPLE:
  148. case EventOrGroupType.HPKP:
  149. // Due to a regression some reports did not have message persisted
  150. // (https://github.com/getsentry/sentry/pull/19794) so we need to fall
  151. // back to the computed title for these.
  152. return {
  153. title: customTitle ?? (metadata.message || title),
  154. subtitle: metadata.origin ?? '',
  155. treeLabel: undefined,
  156. };
  157. case EventOrGroupType.DEFAULT:
  158. return {
  159. title: customTitle ?? metadata.title ?? '',
  160. subtitle: '',
  161. treeLabel: undefined,
  162. };
  163. case EventOrGroupType.TRANSACTION:
  164. case EventOrGroupType.GENERIC:
  165. const isIssue = !isTombstone(event) && defined(event.issueCategory);
  166. return {
  167. title: customTitle ?? title,
  168. subtitle: isIssue ? culprit : '',
  169. treeLabel: undefined,
  170. };
  171. default:
  172. return {
  173. title: customTitle ?? title,
  174. subtitle: '',
  175. treeLabel: undefined,
  176. };
  177. }
  178. }
  179. /**
  180. * Returns a short eventId with only 8 characters
  181. */
  182. export function getShortEventId(eventId: string) {
  183. return eventId.substring(0, 8);
  184. }
  185. /**
  186. * Returns a comma delineated list of errors
  187. */
  188. function getEventErrorString(event: Event) {
  189. return uniq(event.errors?.map(error => error.type)).join(',');
  190. }
  191. function hasTrace(event: Event) {
  192. if (event.type !== 'error') {
  193. return false;
  194. }
  195. return !!event.contexts?.trace;
  196. }
  197. function hasProfile(event: Event) {
  198. return defined(event.contexts?.profile);
  199. }
  200. /**
  201. * Function to determine if an event has source maps
  202. * by ensuring that every inApp frame has a valid sourcemap
  203. */
  204. export function eventHasSourceMaps(event: Event) {
  205. const inAppFrames = getExceptionFrames(event, true);
  206. // the map field tells us if it's sourcemapped
  207. return inAppFrames.every(frame => !!frame.map);
  208. }
  209. /**
  210. * Function to determine if an event has been symbolicated. If the event
  211. * goes through symbolicator and has in-app frames, it looks for at least one in-app frame
  212. * to be successfully symbolicated. Otherwise falls back to checking for `rawStacktrace` field presence.
  213. */
  214. export function eventIsSymbolicated(event: Event) {
  215. const frames = getAllFrames(event, false);
  216. const fromSymbolicator = frames.some(frame => defined(frame.symbolicatorStatus));
  217. if (fromSymbolicator) {
  218. // if the event goes through symbolicator and have in-app frames, we say it's symbolicated if
  219. // at least one in-app frame is successfully symbolicated
  220. const inAppFrames = frames.filter(frame => frame.inApp);
  221. if (inAppFrames.length > 0) {
  222. return inAppFrames.some(
  223. frame => frame.symbolicatorStatus === SymbolicatorStatus.SYMBOLICATED
  224. );
  225. }
  226. // if there's no in-app frames, we say it's symbolicated if at least
  227. // one system frame is successfully symbolicated
  228. return frames.some(
  229. frame => frame.symbolicatorStatus === SymbolicatorStatus.SYMBOLICATED
  230. );
  231. }
  232. // if none of the frames have symbolicatorStatus defined, most likely the event does not
  233. // go through symbolicator and it's Java/Android/Javascript or something alike, so we fallback
  234. // to the rawStacktrace presence
  235. return event.entries?.some(entry => {
  236. return (
  237. (entry.type === EntryType.EXCEPTION || entry.type === EntryType.THREADS) &&
  238. entry.data.values?.some(
  239. (value: Thread | ExceptionValue) => !!value.rawStacktrace && !!value.stacktrace
  240. )
  241. );
  242. });
  243. }
  244. /**
  245. * Function to determine if an event has source context
  246. */
  247. export function eventHasSourceContext(event: Event) {
  248. const frames = getAllFrames(event, false);
  249. return frames.some(frame => defined(frame.context) && !!frame.context.length);
  250. }
  251. /**
  252. * Function to determine if an event has local variables
  253. */
  254. export function eventHasLocalVariables(event: Event) {
  255. const frames = getAllFrames(event, false);
  256. return frames.some(frame => defined(frame.vars));
  257. }
  258. /**
  259. * Function to get status about how many frames have source maps
  260. */
  261. export function getFrameBreakdownOfSourcemaps(event?: Event | null) {
  262. if (!event) {
  263. // return undefined if there is no event
  264. return {};
  265. }
  266. const inAppFrames = getExceptionFrames(event, true);
  267. if (!inAppFrames.length) {
  268. return {};
  269. }
  270. return {
  271. framesWithSourcemapsPercent:
  272. (inAppFrames.filter(frame => !!frame.map).length * 100) / inAppFrames.length,
  273. framesWithoutSourceMapsPercent:
  274. (inAppFrames.filter(frame => !frame.map).length * 100) / inAppFrames.length,
  275. };
  276. }
  277. /**
  278. * Returns all stack frames of type 'exception' of this event
  279. */
  280. function getExceptionFrames(event: Event, inAppOnly: boolean) {
  281. const exceptions = getExceptionEntries(event);
  282. const frames = exceptions
  283. .flatMap(exception => exception.data.values || [])
  284. .flatMap(exceptionValue => exceptionValue?.stacktrace?.frames || []);
  285. return inAppOnly ? frames.filter(frame => frame.inApp) : frames;
  286. }
  287. /**
  288. * Returns all entries of type 'exception' of this event
  289. */
  290. function getExceptionEntries(event: Event) {
  291. return (event.entries?.filter(entry => entry.type === EntryType.EXCEPTION) ||
  292. []) as EntryException[];
  293. }
  294. /**
  295. * Returns all stack frames of type 'exception' or 'threads' of this event
  296. */
  297. function getAllFrames(event: Event, inAppOnly: boolean): Frame[] {
  298. const exceptions: EntryException[] | EntryThreads[] = getEntriesWithFrames(event);
  299. const frames: Frame[] = exceptions
  300. .flatMap(withStacktrace => withStacktrace.data.values ?? [])
  301. .flatMap(
  302. (withStacktrace: ExceptionValue | Thread) =>
  303. withStacktrace?.stacktrace?.frames ?? []
  304. );
  305. return inAppOnly ? frames.filter(frame => frame.inApp) : frames;
  306. }
  307. /**
  308. * Returns all entries that can have stack frames, currently of 'exception' and 'threads' type
  309. */
  310. function getEntriesWithFrames(event: Event) {
  311. return (event.entries?.filter(
  312. entry => entry.type === EntryType.EXCEPTION || entry.type === EntryType.THREADS
  313. ) || []) as EntryException[] | EntryThreads[];
  314. }
  315. function getNumberOfStackFrames(event: Event) {
  316. const entries = getExceptionEntries(event);
  317. // for each entry, go through each frame and get the max
  318. const frameLengths =
  319. entries?.map(entry =>
  320. (entry.data.values || []).reduce((best, exception) => {
  321. // find the max number of frames in this entry
  322. const frameCount = exception.stacktrace?.frames?.length || 0;
  323. return Math.max(best, frameCount);
  324. }, 0)
  325. ) || [];
  326. if (!frameLengths.length) {
  327. return 0;
  328. }
  329. return Math.max(...frameLengths);
  330. }
  331. function getNumberOfInAppStackFrames(event: Event) {
  332. const entries = getExceptionEntries(event);
  333. // for each entry, go through each frame
  334. const frameLengths =
  335. entries?.map(entry =>
  336. (entry.data.values || []).reduce((best, exception) => {
  337. // find the max number of frames in this entry
  338. const frames = exception.stacktrace?.frames?.filter(f => f.inApp) || [];
  339. return Math.max(best, frames.length);
  340. }, 0)
  341. ) || [];
  342. if (!frameLengths.length) {
  343. return 0;
  344. }
  345. return Math.max(...frameLengths);
  346. }
  347. function getNumberOfThreadsWithNames(event: Event) {
  348. const threadLengths =
  349. (
  350. (event.entries?.filter(entry => entry.type === 'threads') || []) as EntryThreads[]
  351. ).map(entry => entry.data?.values?.filter(thread => !!thread.name).length || 0) || [];
  352. if (!threadLengths.length) {
  353. return 0;
  354. }
  355. return Math.max(...threadLengths);
  356. }
  357. export function eventHasExceptionGroup(event: Event) {
  358. const exceptionEntries = getExceptionEntries(event);
  359. return exceptionEntries.some(entry =>
  360. entry.data.values?.some(({mechanism}) => mechanism?.is_exception_group)
  361. );
  362. }
  363. export function eventHasGraphQlRequest(event: Event) {
  364. const requestEntry = event.entries?.find(entry => entry.type === EntryType.REQUEST) as
  365. | EntryRequest
  366. | undefined;
  367. return (
  368. typeof requestEntry?.data?.apiTarget === 'string' &&
  369. requestEntry.data.apiTarget.toLowerCase() === 'graphql'
  370. );
  371. }
  372. /**
  373. * Return the integration type for the first assignment via integration
  374. */
  375. function getAssignmentIntegration(group: Group) {
  376. if (!group.activity) {
  377. return '';
  378. }
  379. const assignmentAcitivies = group.activity.filter(
  380. activity => activity.type === GroupActivityType.ASSIGNED
  381. ) as GroupActivityAssigned[];
  382. const integrationAssignments = assignmentAcitivies.find(
  383. activity => !!activity.data.integration
  384. );
  385. return integrationAssignments?.data.integration || '';
  386. }
  387. export function getAnalyticsDataForEvent(event?: Event | null): BaseEventAnalyticsParams {
  388. const {framesWithSourcemapsPercent, framesWithoutSourceMapsPercent} =
  389. getFrameBreakdownOfSourcemaps(event);
  390. return {
  391. event_id: event?.eventID || '-1',
  392. num_commits: event?.release?.commitCount || 0,
  393. num_event_tags: event?.tags?.length ?? 0,
  394. num_stack_frames: event ? getNumberOfStackFrames(event) : 0,
  395. num_in_app_stack_frames: event ? getNumberOfInAppStackFrames(event) : 0,
  396. num_threads_with_names: event ? getNumberOfThreadsWithNames(event) : 0,
  397. event_platform: event?.platform,
  398. event_runtime: event?.tags?.find(tag => tag.key === 'runtime')?.value,
  399. event_type: event?.type,
  400. has_release: !!event?.release,
  401. has_exception_group: event ? eventHasExceptionGroup(event) : false,
  402. has_graphql_request: event ? eventHasGraphQlRequest(event) : false,
  403. has_profile: event ? hasProfile(event) : false,
  404. has_source_context: event ? eventHasSourceContext(event) : false,
  405. has_local_variables: event ? eventHasLocalVariables(event) : false,
  406. has_source_maps: event ? eventHasSourceMaps(event) : false,
  407. has_trace: event ? hasTrace(event) : false,
  408. has_commit: !!event?.release?.lastCommit,
  409. has_next_event: event ? defined(event.nextEventID) : false,
  410. has_previous_event: event ? defined(event.previousEventID) : false,
  411. is_symbolicated: event ? eventIsSymbolicated(event) : false,
  412. event_errors: event ? getEventErrorString(event) : '',
  413. frames_with_sourcemaps_percent: framesWithSourcemapsPercent,
  414. frames_without_source_maps_percent: framesWithoutSourceMapsPercent,
  415. sdk_name: event?.sdk?.name,
  416. sdk_version: event?.sdk?.version,
  417. release_user_agent: event?.release?.userAgent,
  418. resolved_with: event?.resolvedWith ?? [],
  419. error_has_replay: Boolean(getReplayIdFromEvent(event)),
  420. error_has_user_feedback: defined(event?.userReport),
  421. has_otel: event?.contexts?.otel !== undefined,
  422. event_mechanism:
  423. event?.tags?.find(tag => tag.key === 'mechanism')?.value || undefined,
  424. };
  425. }
  426. export type CommonGroupAnalyticsData = {
  427. days_since_last_seen: number;
  428. error_count: number;
  429. group_id: number;
  430. group_num_user_feedback: number;
  431. has_external_issue: boolean;
  432. has_owner: boolean;
  433. integration_assignment_source: string;
  434. issue_age: number;
  435. issue_category: IssueCategory;
  436. issue_id: number;
  437. issue_type: IssueType;
  438. num_comments: number;
  439. num_participants: number;
  440. num_viewers: number;
  441. is_assigned?: boolean;
  442. issue_level?: string;
  443. issue_status?: string;
  444. issue_substatus?: string | null;
  445. priority?: string;
  446. };
  447. export function getAnalyticsDataForGroup(group?: Group | null): CommonGroupAnalyticsData {
  448. const groupId = group ? parseInt(group.id, 10) : -1;
  449. const activeUser = ConfigStore.get('user');
  450. return {
  451. group_id: groupId,
  452. // overload group_id with the issue_id
  453. issue_id: groupId,
  454. issue_category: group?.issueCategory ?? IssueCategory.ERROR,
  455. issue_type: group?.issueType ?? IssueType.ERROR,
  456. issue_status: group?.status,
  457. issue_substatus: group?.substatus,
  458. issue_age: group?.firstSeen ? getDaysSinceDatePrecise(group.firstSeen) : -1,
  459. days_since_last_seen: group?.lastSeen ? getDaysSinceDatePrecise(group.lastSeen) : -1,
  460. issue_level: group?.level,
  461. is_assigned: !!group?.assignedTo,
  462. error_count: Number(group?.count || -1),
  463. num_comments: group ? group.numComments : -1,
  464. has_external_issue: group?.annotations ? group?.annotations.length > 0 : false,
  465. has_owner: group?.owners ? group?.owners.length > 0 : false,
  466. integration_assignment_source: group ? getAssignmentIntegration(group) : '',
  467. num_participants: group?.participants?.length ?? 0,
  468. num_viewers: group?.seenBy?.filter(user => user.id !== activeUser?.id).length ?? 0,
  469. group_num_user_feedback: group?.userReportCount ?? 0,
  470. priority: group?.priority,
  471. };
  472. }
  473. export function eventIsProfilingIssue(event: BaseGroup | Event | GroupTombstoneHelper) {
  474. if (isTombstone(event) || isGroup(event)) {
  475. return false;
  476. }
  477. if (event.issueCategory === IssueCategory.PROFILE) {
  478. return true;
  479. }
  480. const evidenceData = event.occurrence?.evidenceData ?? {};
  481. return evidenceData.templateName === 'profile';
  482. }
  483. function isGroup(event: BaseGroup | Event): event is BaseGroup {
  484. return (event as BaseGroup).status !== undefined;
  485. }