eventEntries.tsx 6.2 KB

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