Browse Source

feat(replays): Add a faux replay event with merged spans (#33364)

Move merged breadcrumbs into `useReplayEvent` and create a "merged spans" replay event as well.
Billy Vong 2 years ago
parent
commit
ab5a1b1f8f

+ 42 - 43
static/app/views/replays/details.tsx

@@ -16,12 +16,11 @@ import {t} from 'sentry/locale';
 import {PageContent} from 'sentry/styles/organization';
 import space from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
-import {Entry, EntryType, Event} from 'sentry/types/event';
+import {Event} from 'sentry/types/event';
 import {getMessage} from 'sentry/utils/events';
 import withOrganization from 'sentry/utils/withOrganization';
 import AsyncView from 'sentry/views/asyncView';
 
-import mergeBreadcrumbsEntries from './utils/mergeBreadcrumbsEntries';
 import useReplayEvent from './utils/useReplayEvent';
 
 type Props = AsyncView['props'] &
@@ -104,23 +103,28 @@ function getProjectSlug(event: Event) {
   return event.projectSlug || event['project.name']; // seems janky
 }
 
-function isReplayEventEntity(entry: Entry) {
-  // Starting with an allowlist, might be better to block only a few types (like Tags)
-  switch (entry.type) {
-    case EntryType.SPANS:
-      return true;
-    default:
-      return false;
-  }
-}
-
 function ReplayLoader(props: ReplayLoaderProps) {
   const orgSlug = props.orgId;
 
-  const {fetchError, fetching, event, replayEvents, rrwebEvents} = useReplayEvent(props);
+  const {
+    fetchError,
+    fetching,
+    breadcrumbEntry,
+    event,
+    replayEvents,
+    rrwebEvents,
+    mergedReplayEvent,
+  } = useReplayEvent(props);
 
   /* eslint-disable-next-line no-console */
-  console.log({fetchError, fetching, event, replayEvents, rrwebEvents});
+  console.log({
+    fetchError,
+    fetching,
+    event,
+    replayEvents,
+    rrwebEvents,
+    mergedReplayEvent,
+  });
 
   const renderMain = () => {
     if (fetching) {
@@ -130,39 +134,34 @@ function ReplayLoader(props: ReplayLoaderProps) {
       return <NotFound />;
     }
 
-    const breadcrumbs = mergeBreadcrumbsEntries(replayEvents || []);
-
     return (
       <React.Fragment>
         <BaseRRWebReplayer events={rrwebEvents} />
 
-        <EventEntry
-          projectSlug={getProjectSlug(event)}
-          // group={group}
-          organization={props.organization}
-          event={event}
-          entry={breadcrumbs}
-          route={props.route}
-          router={props.router}
-        />
-
-        {replayEvents?.map(replayEvent => (
-          <React.Fragment key={replayEvent.id}>
-            <TitleWrapper>ReplayEvent: {replayEvent.id}</TitleWrapper>
-            {replayEvent.entries.filter(isReplayEventEntity).map(entry => (
-              <EventEntry
-                key={`${replayEvent.id}+${entry.type}`}
-                projectSlug={getProjectSlug(replayEvent)}
-                // group={group}
-                organization={props.organization}
-                event={replayEvent}
-                entry={entry}
-                route={props.route}
-                router={props.router}
-              />
-            ))}
-          </React.Fragment>
-        ))}
+        {breadcrumbEntry && (
+          <EventEntry
+            projectSlug={getProjectSlug(event)}
+            // group={group}
+            organization={props.organization}
+            event={event}
+            entry={breadcrumbEntry}
+            route={props.route}
+            router={props.router}
+          />
+        )}
+
+        {mergedReplayEvent && (
+          <EventEntry
+            key={`${mergedReplayEvent.id}`}
+            projectSlug={getProjectSlug(mergedReplayEvent)}
+            // group={group}
+            organization={props.organization}
+            event={mergedReplayEvent}
+            entry={mergedReplayEvent.entries[0]}
+            route={props.route}
+            router={props.router}
+          />
+        )}
       </React.Fragment>
     );
   };

+ 46 - 1
static/app/views/replays/utils/useReplayEvent.tsx

@@ -2,13 +2,29 @@ import {useEffect, useState} from 'react';
 import type {eventWithTime} from 'rrweb/typings/types';
 
 import {IssueAttachment} from 'sentry/types';
-import {Event} from 'sentry/types/event';
+import {Entry, EntryType, Event} from 'sentry/types/event';
 import EventView from 'sentry/utils/discover/eventView';
 import {generateEventSlug} from 'sentry/utils/discover/urls';
 import RequestError from 'sentry/utils/requestError/requestError';
 import useApi from 'sentry/utils/useApi';
 
+import mergeBreadcrumbsEntries from './mergeBreadcrumbsEntries';
+
+function isReplayEventEntity(entry: Entry) {
+  // Starting with an allowlist, might be better to block only a few types (like Tags)
+  switch (entry.type) {
+    case EntryType.SPANS:
+      return true;
+    default:
+      return false;
+  }
+}
 type State = {
+  /**
+   * List of breadcrumbs
+   */
+  breadcrumbEntry: undefined | Entry;
+
   /**
    * The root replay event
    */
@@ -25,6 +41,8 @@ type State = {
    */
   fetching: boolean;
 
+  mergedReplayEvent: undefined | Event;
+
   /**
    * The list of related `sentry-replay-event` objects that were captured during this `sentry-replay`
    */
@@ -67,9 +85,11 @@ function useReplayEvent({eventSlug, location, orgId}: Options): Result {
   const [state, setState] = useState<State>({
     fetchError: undefined,
     fetching: true,
+    breadcrumbEntry: undefined,
     event: undefined,
     replayEvents: undefined,
     rrwebEvents: undefined,
+    mergedReplayEvent: undefined,
   });
 
   function fetchEvent() {
@@ -130,9 +150,11 @@ function useReplayEvent({eventSlug, location, orgId}: Options): Result {
       fetchError: undefined,
       fetching: true,
 
+      breadcrumbEntry: undefined,
       event: undefined,
       replayEvents: undefined,
       rrwebEvents: undefined,
+      mergedReplayEvent: undefined,
     });
     try {
       const [event, rrwebEvents, replayEvents] = await Promise.all([
@@ -141,22 +163,43 @@ function useReplayEvent({eventSlug, location, orgId}: Options): Result {
         fetchReplayEvents(),
       ]);
 
+      const breadcrumbEntry = mergeBreadcrumbsEntries(replayEvents || []);
+
+      // Get a merged list of all spans from all replay events
+      const spans = replayEvents.flatMap(
+        replayEvent => replayEvent.entries.find(isReplayEventEntity).data
+      );
+
+      // Create a merged spans entry on the first replay event and fake the
+      // endTimestamp by using the timestamp of the final span
+      const mergedReplayEvent = {
+        ...replayEvents[0],
+        breakdowns: null,
+        entries: [{type: EntryType.SPANS, data: spans}],
+        // This is probably better than taking the end timestamp of the last `replayEvent`
+        endTimestamp: spans[spans.length - 1]?.timestamp,
+      };
+
       setState({
         ...state,
         fetchError: undefined,
         fetching: false,
         event,
+        mergedReplayEvent,
         replayEvents,
         rrwebEvents,
+        breadcrumbEntry,
       });
     } catch (error) {
       setState({
         fetchError: error,
         fetching: false,
 
+        breadcrumbEntry: undefined,
         event: undefined,
         replayEvents: undefined,
         rrwebEvents: undefined,
+        mergedReplayEvent: undefined,
       });
     }
   }
@@ -167,9 +210,11 @@ function useReplayEvent({eventSlug, location, orgId}: Options): Result {
     fetchError: state.fetchError,
     fetching: state.fetching,
 
+    breadcrumbEntry: state.breadcrumbEntry,
     event: state.event,
     replayEvents: state.replayEvents,
     rrwebEvents: state.rrwebEvents,
+    mergedReplayEvent: state.mergedReplayEvent,
   };
 }