eventEntries.tsx 7.2 KB

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