events.tsx 16 KB

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