eventEntries.tsx 6.8 KB

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