eventEntries.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import {CommitRow} from 'sentry/components/commitRow';
  5. import {EventEvidence} from 'sentry/components/events/eventEvidence';
  6. import EventReplay from 'sentry/components/events/eventReplay';
  7. import {ActionableItems} from 'sentry/components/events/interfaces/crashContent/exception/actionableItems';
  8. import {actionableItemsEnabled} from 'sentry/components/events/interfaces/crashContent/exception/useActionableItems';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {
  12. Entry,
  13. Event,
  14. Group,
  15. Organization,
  16. Project,
  17. SharedViewOrganization,
  18. } from 'sentry/types';
  19. import {EntryType, EventOrGroupType} from 'sentry/types';
  20. import {isNotSharedOrganization} from 'sentry/types/utils';
  21. import {objectIsEmpty} from 'sentry/utils';
  22. import {CustomMetricsEventData} from 'sentry/views/ddm/customMetricsEventData';
  23. import {EventContexts} from './contexts';
  24. import {EventDevice} from './device';
  25. import {EventAttachments} from './eventAttachments';
  26. import {EventDataSection} from './eventDataSection';
  27. import {EventEntry} from './eventEntry';
  28. import {EventExtraData} from './eventExtraData';
  29. import {EventSdk} from './eventSdk';
  30. import {EventTagsAndScreenshot} from './eventTagsAndScreenshot';
  31. import {EventViewHierarchy} from './eventViewHierarchy';
  32. import {EventGroupingInfo} from './groupingInfo';
  33. import {EventPackageData} from './packageData';
  34. import {EventRRWebIntegration} from './rrwebIntegration';
  35. import {DataSection} from './styles';
  36. import {SuspectCommits} from './suspectCommits';
  37. import {EventUserFeedback} from './userFeedback';
  38. type Props = {
  39. location: Location;
  40. /**
  41. * The organization can be the shared view on a public issue view.
  42. */
  43. organization: Organization | SharedViewOrganization;
  44. project: Project;
  45. className?: string;
  46. event?: Event;
  47. group?: Group;
  48. isShare?: boolean;
  49. showTagSummary?: boolean;
  50. };
  51. function EventEntries({
  52. organization,
  53. project,
  54. location,
  55. event,
  56. group,
  57. className,
  58. isShare = false,
  59. showTagSummary = true,
  60. }: Props) {
  61. const orgSlug = organization.slug;
  62. const projectSlug = project.slug;
  63. const orgFeatures = organization?.features ?? [];
  64. if (!event) {
  65. return (
  66. <LatestEventNotAvailable>
  67. <h3>{t('Latest Event Not Available')}</h3>
  68. </LatestEventNotAvailable>
  69. );
  70. }
  71. const hasContext = !objectIsEmpty(event.user ?? {}) || !objectIsEmpty(event.contexts);
  72. const hasActionableItems = actionableItemsEnabled({
  73. eventId: event.id,
  74. organization,
  75. projectSlug,
  76. });
  77. return (
  78. <div className={className}>
  79. {hasActionableItems && (
  80. <ActionableItems event={event} project={project} isShare={isShare} />
  81. )}
  82. {!isShare && isNotSharedOrganization(organization) && (
  83. <SuspectCommits
  84. project={project}
  85. eventId={event.id}
  86. group={group}
  87. commitRow={CommitRow}
  88. />
  89. )}
  90. {event.userReport && group && (
  91. <EventDataSection title="User Feedback" type="user-feedback">
  92. <EventUserFeedback
  93. report={event.userReport}
  94. orgSlug={orgSlug}
  95. issueId={group.id}
  96. />
  97. </EventDataSection>
  98. )}
  99. {showTagSummary && (
  100. <EventTagsAndScreenshot
  101. event={event}
  102. organization={organization as Organization}
  103. projectSlug={projectSlug}
  104. location={location}
  105. isShare={isShare}
  106. />
  107. )}
  108. <EventEvidence event={event} project={project} />
  109. <Entries
  110. definedEvent={event}
  111. projectSlug={projectSlug}
  112. group={group}
  113. organization={organization}
  114. isShare={isShare}
  115. />
  116. {hasContext && <EventContexts group={group} event={event} />}
  117. <EventExtraData event={event} />
  118. <EventPackageData event={event} />
  119. <EventDevice event={event} />
  120. {!isShare && <EventViewHierarchy event={event} project={project} />}
  121. {!isShare && <EventAttachments event={event} projectSlug={projectSlug} />}
  122. <EventSdk sdk={event.sdk} meta={event._meta?.sdk} />
  123. {event.type === EventOrGroupType.TRANSACTION && event._metrics_summary && (
  124. <CustomMetricsEventData
  125. metricsSummary={event._metrics_summary}
  126. startTimestamp={event.startTimestamp}
  127. />
  128. )}
  129. {!isShare && event.groupID && (
  130. <EventGroupingInfo
  131. projectSlug={projectSlug}
  132. event={event}
  133. showGroupingConfig={
  134. orgFeatures.includes('set-grouping-config') && 'groupingConfig' in event
  135. }
  136. group={group}
  137. />
  138. )}
  139. {!isShare && (
  140. <EventRRWebIntegration event={event} orgId={orgSlug} projectSlug={projectSlug} />
  141. )}
  142. </div>
  143. );
  144. }
  145. // The ordering for event entries is owned by the interface serializers on the backend.
  146. // Because replays are not an interface, we need to manually insert the replay section
  147. // into the array of entries. The long-term solution here is to move the ordering
  148. // logic to this component, similar to how GroupEventDetailsContent works.
  149. function partitionEntriesForReplay(entries: Entry[]) {
  150. let replayIndex = 0;
  151. for (const [i, entry] of entries.entries()) {
  152. if (
  153. [
  154. // The following entry types should be placed before the replay
  155. // This is similar to the ordering in GroupEventDetailsContent
  156. EntryType.MESSAGE,
  157. EntryType.STACKTRACE,
  158. EntryType.EXCEPTION,
  159. EntryType.THREADS,
  160. EntryType.SPANS,
  161. ].includes(entry.type)
  162. ) {
  163. replayIndex = i + 1;
  164. }
  165. }
  166. return [entries.slice(0, replayIndex), entries.slice(replayIndex)];
  167. }
  168. export function Entries({
  169. definedEvent,
  170. projectSlug,
  171. isShare,
  172. group,
  173. organization,
  174. hideBeforeReplayEntries = false,
  175. hideBreadCrumbs = false,
  176. }: {
  177. definedEvent: Event;
  178. projectSlug: string;
  179. hideBeforeReplayEntries?: boolean;
  180. hideBreadCrumbs?: boolean;
  181. isShare?: boolean;
  182. } & Pick<Props, 'group' | 'organization'>) {
  183. if (!Array.isArray(definedEvent.entries)) {
  184. return null;
  185. }
  186. const [beforeReplayEntries, afterReplayEntries] = partitionEntriesForReplay(
  187. definedEvent.entries
  188. );
  189. const eventEntryProps = {
  190. projectSlug,
  191. group,
  192. organization,
  193. event: definedEvent,
  194. isShare,
  195. };
  196. return (
  197. <Fragment>
  198. {!hideBeforeReplayEntries &&
  199. beforeReplayEntries.map((entry, entryIdx) => (
  200. <EventEntry key={entryIdx} entry={entry} {...eventEntryProps} />
  201. ))}
  202. {!isShare && <EventReplay {...eventEntryProps} />}
  203. {afterReplayEntries.map((entry, entryIdx) => {
  204. if (hideBreadCrumbs && entry.type === EntryType.BREADCRUMBS) {
  205. return null;
  206. }
  207. return <EventEntry key={entryIdx} entry={entry} {...eventEntryProps} />;
  208. })}
  209. </Fragment>
  210. );
  211. }
  212. const LatestEventNotAvailable = styled('div')`
  213. padding: ${space(2)} ${space(4)};
  214. `;
  215. const BorderlessEventEntries = styled(EventEntries)`
  216. & ${DataSection} {
  217. margin-left: 0 !important;
  218. margin-right: 0 !important;
  219. padding: ${space(3)} 0 0 0;
  220. }
  221. & ${DataSection}:first-child {
  222. padding-top: 0;
  223. border-top: 0;
  224. }
  225. `;
  226. export {EventEntries, BorderlessEventEntries};