events.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import {
  2. BaseGroup,
  3. EntryException,
  4. EntryThreads,
  5. EventMetadata,
  6. EventOrGroupType,
  7. Group,
  8. GroupActivityAssigned,
  9. GroupActivityType,
  10. GroupTombstone,
  11. IssueCategory,
  12. IssueType,
  13. TreeLabelPart,
  14. } from 'sentry/types';
  15. import {EntryType, Event} from 'sentry/types/event';
  16. import getDaysSinceDate from 'sentry/utils/getDaysSinceDate';
  17. import {isMobilePlatform, isNativePlatform} from 'sentry/utils/platform';
  18. function isTombstone(maybe: BaseGroup | Event | GroupTombstone): maybe is GroupTombstone {
  19. return !maybe.hasOwnProperty('type');
  20. }
  21. /**
  22. * Extract the display message from an event.
  23. */
  24. export function getMessage(
  25. event: Event | BaseGroup | GroupTombstone
  26. ): string | undefined {
  27. if (isTombstone(event)) {
  28. return event.culprit || '';
  29. }
  30. const {metadata, type, culprit} = event;
  31. switch (type) {
  32. case EventOrGroupType.ERROR:
  33. case EventOrGroupType.TRANSACTION:
  34. return metadata.value;
  35. case EventOrGroupType.CSP:
  36. return metadata.message;
  37. case EventOrGroupType.EXPECTCT:
  38. case EventOrGroupType.EXPECTSTAPLE:
  39. case EventOrGroupType.HPKP:
  40. return '';
  41. default:
  42. return culprit || '';
  43. }
  44. }
  45. /**
  46. * Get the location from an event.
  47. */
  48. export function getLocation(event: Event | BaseGroup | GroupTombstone) {
  49. if (isTombstone(event)) {
  50. return undefined;
  51. }
  52. if (event.type === EventOrGroupType.ERROR && isNativePlatform(event.platform)) {
  53. return event.metadata.filename || undefined;
  54. }
  55. return undefined;
  56. }
  57. export function getTreeLabelPartDetails(part: TreeLabelPart) {
  58. // Note: This function also exists in Python in eventtypes/base.py, to make
  59. // porting efforts simpler it's recommended to keep both variants
  60. // structurally similar.
  61. if (typeof part === 'string') {
  62. return part;
  63. }
  64. const label = part?.function || part?.package || part?.filebase || part?.type;
  65. const classbase = part?.classbase;
  66. if (classbase) {
  67. return label ? `${classbase}.${label}` : classbase;
  68. }
  69. return label || '<unknown>';
  70. }
  71. function computeTitleWithTreeLabel(metadata: EventMetadata) {
  72. const {type, current_tree_label, finest_tree_label} = metadata;
  73. const treeLabel = current_tree_label || finest_tree_label;
  74. const formattedTreeLabel = treeLabel
  75. ? treeLabel.map(labelPart => getTreeLabelPartDetails(labelPart)).join(' | ')
  76. : undefined;
  77. if (!type) {
  78. return {
  79. title: formattedTreeLabel || metadata.function || '<unknown>',
  80. treeLabel,
  81. };
  82. }
  83. if (!formattedTreeLabel) {
  84. return {title: type, treeLabel: undefined};
  85. }
  86. return {
  87. title: `${type} | ${formattedTreeLabel}`,
  88. treeLabel: [{type}, ...(treeLabel ?? [])],
  89. };
  90. }
  91. export function getTitle(
  92. event: Event | BaseGroup,
  93. features: string[] = [],
  94. grouping = false
  95. ) {
  96. const {metadata, type, culprit, title} = event;
  97. const customTitle =
  98. features.includes('custom-event-title') && metadata?.title
  99. ? metadata.title
  100. : undefined;
  101. switch (type) {
  102. case EventOrGroupType.ERROR: {
  103. if (customTitle) {
  104. return {
  105. title: customTitle,
  106. subtitle: culprit,
  107. treeLabel: undefined,
  108. };
  109. }
  110. const displayTitleWithTreeLabel =
  111. features.includes('grouping-title-ui') &&
  112. (grouping ||
  113. isNativePlatform(event.platform) ||
  114. isMobilePlatform(event.platform));
  115. if (displayTitleWithTreeLabel) {
  116. return {
  117. subtitle: culprit,
  118. ...computeTitleWithTreeLabel(metadata),
  119. };
  120. }
  121. return {
  122. subtitle: culprit,
  123. title: metadata.type || metadata.function || '<unknown>',
  124. treeLabel: undefined,
  125. };
  126. }
  127. case EventOrGroupType.CSP:
  128. return {
  129. title: customTitle ?? metadata.directive ?? '',
  130. subtitle: metadata.uri ?? '',
  131. treeLabel: undefined,
  132. };
  133. case EventOrGroupType.EXPECTCT:
  134. case EventOrGroupType.EXPECTSTAPLE:
  135. case EventOrGroupType.HPKP:
  136. // Due to a regression some reports did not have message persisted
  137. // (https://github.com/getsentry/sentry/pull/19794) so we need to fall
  138. // back to the computed title for these.
  139. return {
  140. title: customTitle ?? (metadata.message || title),
  141. subtitle: metadata.origin ?? '',
  142. treeLabel: undefined,
  143. };
  144. case EventOrGroupType.DEFAULT:
  145. return {
  146. title: customTitle ?? metadata.title ?? '',
  147. subtitle: '',
  148. treeLabel: undefined,
  149. };
  150. case EventOrGroupType.TRANSACTION:
  151. const isPerfIssue = event.issueCategory === IssueCategory.PERFORMANCE;
  152. return {
  153. title: isPerfIssue ? metadata.title : customTitle ?? title,
  154. subtitle: isPerfIssue ? culprit : '',
  155. treeLabel: undefined,
  156. };
  157. default:
  158. return {
  159. title: customTitle ?? title,
  160. subtitle: '',
  161. treeLabel: undefined,
  162. };
  163. }
  164. }
  165. /**
  166. * Returns a short eventId with only 8 characters
  167. */
  168. export function getShortEventId(eventId: string) {
  169. return eventId.substring(0, 8);
  170. }
  171. /**
  172. * Returns a comma delineated list of errors
  173. */
  174. function getEventErrorString(event: Event) {
  175. return event.errors?.map(error => error.type).join(',') || '';
  176. }
  177. function hasTrace(event: Event) {
  178. if (event.type !== 'error') {
  179. return false;
  180. }
  181. return !!event.contexts?.trace;
  182. }
  183. /**
  184. * Function to determine if an event has source maps
  185. */
  186. export function eventHasSourceMaps(event: Event) {
  187. return event.entries?.some(entry => {
  188. return (
  189. entry.type === EntryType.EXCEPTION &&
  190. entry.data.values?.some(value => !!value.rawStacktrace && !!value.stacktrace)
  191. );
  192. });
  193. }
  194. function getExceptionEntries(event: Event) {
  195. return event.entries?.filter(entry => entry.type === 'exception') as EntryException[];
  196. }
  197. function getNumberOfStackFrames(event: Event) {
  198. const entries = getExceptionEntries(event);
  199. // for each entry, go through each frame and get the max
  200. const frameLengths =
  201. entries?.map(entry =>
  202. (entry.data.values || []).reduce((best, exception) => {
  203. // find the max number of frames in this entry
  204. const frameCount = exception.stacktrace?.frames?.length || 0;
  205. return Math.max(best, frameCount);
  206. }, 0)
  207. ) || [];
  208. if (!frameLengths.length) {
  209. return 0;
  210. }
  211. return Math.max(...frameLengths);
  212. }
  213. function getNumberOfInAppStackFrames(event: Event) {
  214. const entries = getExceptionEntries(event);
  215. // for each entry, go through each frame
  216. const frameLengths =
  217. entries?.map(entry =>
  218. (entry.data.values || []).reduce((best, exception) => {
  219. // find the max number of frames in this entry
  220. const frames = exception.stacktrace?.frames?.filter(f => f.inApp) || [];
  221. return Math.max(best, frames.length);
  222. }, 0)
  223. ) || [];
  224. if (!frameLengths.length) {
  225. return 0;
  226. }
  227. return Math.max(...frameLengths);
  228. }
  229. function getNumberOfThreadsWithNames(event: Event) {
  230. const threadLengths =
  231. (
  232. (event.entries?.filter(entry => entry.type === 'threads') || []) as EntryThreads[]
  233. ).map(entry => entry.data?.values?.filter(thread => !!thread.name).length || 0) || [];
  234. if (!threadLengths.length) {
  235. return 0;
  236. }
  237. return Math.max(...threadLengths);
  238. }
  239. /**
  240. * Return the integration type for the first assignment via integration
  241. */
  242. function getAssignmentIntegration(group: Group) {
  243. if (!group.activity) {
  244. return '';
  245. }
  246. const assignmentAcitivies = group.activity.filter(
  247. activity => activity.type === GroupActivityType.ASSIGNED
  248. ) as GroupActivityAssigned[];
  249. const integrationAssignments = assignmentAcitivies.find(
  250. activity => !!activity.data.integration
  251. );
  252. return integrationAssignments?.data.integration || '';
  253. }
  254. export function getAnalyticsDataForEvent(event?: Event) {
  255. return {
  256. event_id: event?.eventID || '-1',
  257. num_commits: event?.release?.commitCount || 0,
  258. num_stack_frames: event ? getNumberOfStackFrames(event) : 0,
  259. num_in_app_stack_frames: event ? getNumberOfInAppStackFrames(event) : 0,
  260. num_threads_with_names: event ? getNumberOfThreadsWithNames(event) : 0,
  261. event_platform: event?.platform,
  262. event_type: event?.type,
  263. has_release: !!event?.release,
  264. has_source_maps: event ? eventHasSourceMaps(event) : false,
  265. has_trace: event ? hasTrace(event) : false,
  266. has_commit: !!event?.release?.lastCommit,
  267. event_errors: event ? getEventErrorString(event) : '',
  268. sdk_name: event?.sdk?.name,
  269. sdk_version: event?.sdk?.version,
  270. release_user_agent: event?.release?.userAgent,
  271. error_has_replay: Boolean(event?.tags?.find(({key}) => key === 'replayId')),
  272. has_otel: event?.contexts?.otel !== undefined,
  273. };
  274. }
  275. export type CommonGroupAnalyticsData = {
  276. error_count: number;
  277. group_has_replay: boolean;
  278. group_id: number;
  279. has_external_issue: boolean;
  280. has_owner: boolean;
  281. integration_assignment_source: string;
  282. issue_age: number;
  283. issue_category: IssueCategory;
  284. issue_id: number;
  285. issue_type: IssueType;
  286. num_comments: number;
  287. is_assigned?: boolean;
  288. issue_level?: string;
  289. issue_status?: string;
  290. };
  291. export function getAnalyticsDataForGroup(group?: Group | null): CommonGroupAnalyticsData {
  292. const groupId = group ? parseInt(group.id, 10) : -1;
  293. return {
  294. group_id: groupId,
  295. // overload group_id with the issue_id
  296. issue_id: groupId,
  297. issue_category: group?.issueCategory ?? IssueCategory.ERROR,
  298. issue_type: group?.issueType ?? IssueType.ERROR,
  299. issue_status: group?.status,
  300. issue_age: group?.firstSeen ? getDaysSinceDate(group.firstSeen) : -1,
  301. issue_level: group?.level,
  302. is_assigned: !!group?.assignedTo,
  303. error_count: Number(group?.count || -1),
  304. group_has_replay: Boolean(group?.tags?.find(({key}) => key === 'replayId')),
  305. num_comments: group ? group.numComments : -1,
  306. has_external_issue: group?.annotations ? group?.annotations.length > 0 : false,
  307. has_owner: group?.owners ? group?.owners.length > 0 : false,
  308. integration_assignment_source: group ? getAssignmentIntegration(group) : '',
  309. };
  310. }