eventEntries.tsx 7.4 KB

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