Browse Source

fix(replay): Improve messaging/render when a replay is deleted (#57035)

Before when a replay is archived the Replay Details page would totally
fail to load, and show a red banner. Also the Issue details page would
show "There was an error loading a component." which isn't really
helpful to people.

Now we're handling the case better (types could be improved), and more
importantly giving people some kind of message so they know that sentry
is working well, it's the data that's missing.

| Issue Details (replay preview) | Replay Details |
| --- | --- |
|
![SCR-20230927-jaah](https://github.com/getsentry/sentry/assets/187460/8b16af70-2d08-4610-b890-972b1c37b8b0)
|
![SCR-20230927-jacd](https://github.com/getsentry/sentry/assets/187460/7caca833-b194-4d2a-95df-2a5f1dc53b7f)
|

For reference the table view still looks like this: 

![SCR-20230927-iulf](https://github.com/getsentry/sentry/assets/187460/2b9e877a-fd9c-479b-8800-75a3e23ec7ba)


Fixes https://github.com/getsentry/sentry/issues/57029
Ryan Albrecht 1 year ago
parent
commit
f30f0170bb

+ 14 - 2
static/app/components/events/eventReplay/replayPreview.tsx

@@ -7,9 +7,10 @@ import ExternalLink from 'sentry/components/links/externalLink';
 import List from 'sentry/components/list';
 import ListItem from 'sentry/components/list/listItem';
 import Placeholder from 'sentry/components/placeholder';
+import {Flex} from 'sentry/components/profiling/flex';
 import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
 import ReplayPlayer from 'sentry/components/replays/replayPlayer';
-import {IconPlay} from 'sentry/icons';
+import {IconDelete, IconPlay} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
@@ -32,7 +33,7 @@ function ReplayPreview({orgSlug, replaySlug, eventTimestampMs, buttonProps}: Pro
     replaySlug,
   });
 
-  const startTimestampMs = replayRecord?.started_at.getTime() ?? 0;
+  const startTimestampMs = replayRecord?.started_at?.getTime() ?? 0;
   const initialTimeOffsetMs = useMemo(() => {
     if (eventTimestampMs && startTimestampMs) {
       return Math.abs(eventTimestampMs - startTimestampMs);
@@ -41,6 +42,17 @@ function ReplayPreview({orgSlug, replaySlug, eventTimestampMs, buttonProps}: Pro
     return 0;
   }, [eventTimestampMs, startTimestampMs]);
 
+  if (replayRecord?.is_archived) {
+    return (
+      <Alert type="warning" data-test-id="replay-error">
+        <Flex gap={space(0.5)}>
+          <IconDelete color="gray500" size="sm" />
+          {t('The replay for this event has been deleted.')}
+        </Flex>
+      </Alert>
+    );
+  }
+
   if (fetchError) {
     const reasons = [
       t('The replay was rate-limited and could not be accepted.'),

+ 1 - 1
static/app/utils/replays/hooks/useLogReplayDataLoaded.tsx

@@ -19,7 +19,7 @@ function useLogReplayDataLoaded({fetchError, fetching, projectSlug, replay}: Pro
   });
 
   useEffect(() => {
-    if (fetching || fetchError || !replay || !project) {
+    if (fetching || fetchError || !replay || !project || replay.getReplay().is_archived) {
       return;
     }
     const replayRecord = replay.getReplay();

+ 17 - 4
static/app/utils/replays/replayReader.tsx

@@ -122,6 +122,19 @@ export default class ReplayReader {
   }: RequiredNotNull<ReplayReaderParams>) {
     this._cacheKey = domId('replayReader-');
 
+    if (replayRecord.is_archived) {
+      this._replayRecord = replayRecord;
+      const archivedReader = new Proxy(this, {
+        get(_target, prop, _receiver) {
+          if (prop === '_replayRecord') {
+            return replayRecord;
+          }
+          return () => {};
+        },
+      });
+      return archivedReader;
+    }
+
     const {breadcrumbFrames, optionFrame, rrwebFrames, spanFrames} =
       hydrateFrames(attachments);
 
@@ -175,12 +188,12 @@ export default class ReplayReader {
   public timestampDeltas = {startedAtDelta: 0, finishedAtDelta: 0};
 
   private _cacheKey: string;
-  private _errors: ErrorFrame[];
+  private _errors: ErrorFrame[] = [];
   private _optionFrame: undefined | OptionFrame;
   private _replayRecord: ReplayRecord;
-  private _sortedBreadcrumbFrames: BreadcrumbFrame[];
-  private _sortedRRWebEvents: RecordingFrame[];
-  private _sortedSpanFrames: SpanFrame[];
+  private _sortedBreadcrumbFrames: BreadcrumbFrame[] = [];
+  private _sortedRRWebEvents: RecordingFrame[] = [];
+  private _sortedSpanFrames: SpanFrame[] = [];
 
   toJSON = () => this._cacheKey;
 

+ 5 - 1
static/app/views/replays/detail/page.tsx

@@ -38,7 +38,11 @@ function Page({
     ? `${replayRecord.id} — Session Replay — ${orgSlug}`
     : `Session Replay — ${orgSlug}`;
 
-  const header = (
+  const header = replayRecord?.is_archived ? (
+    <Header>
+      <DetailsPageBreadcrumbs orgSlug={orgSlug} replayRecord={replayRecord} />
+    </Header>
+  ) : (
     <Header>
       <DetailsPageBreadcrumbs orgSlug={orgSlug} replayRecord={replayRecord} />
 

+ 24 - 1
static/app/views/replays/details.tsx

@@ -1,18 +1,22 @@
 import {Fragment} from 'react';
 import type {RouteComponentProps} from 'react-router';
 
+import Alert from 'sentry/components/alert';
 import DetailedError from 'sentry/components/errors/detailedError';
 import NotFound from 'sentry/components/errors/notFound';
 import * as Layout from 'sentry/components/layouts/thirds';
 import List from 'sentry/components/list';
 import ListItem from 'sentry/components/list/listItem';
+import {Flex} from 'sentry/components/profiling/flex';
 import {
   Provider as ReplayContextProvider,
   useReplayContext,
 } from 'sentry/components/replays/replayContext';
+import {IconDelete} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import ConfigStore from 'sentry/stores/configStore';
 import {useLegacyStore} from 'sentry/stores/useLegacyStore';
+import {space} from 'sentry/styles/space';
 import {decodeScalar} from 'sentry/utils/queryString';
 import useInitialTimeOffsetMs, {
   TimeOffsetLocationQueryParams,
@@ -75,9 +79,28 @@ function ReplayDetails({params: {replaySlug}}: Props) {
     orgSlug,
     projectSlug,
     replayId,
-    replayStartTimestampMs: replayRecord?.started_at.getTime(),
+    replayStartTimestampMs: replayRecord?.started_at?.getTime(),
   });
 
+  if (replayRecord?.is_archived) {
+    return (
+      <Page
+        orgSlug={orgSlug}
+        replayRecord={replayRecord}
+        projectSlug={projectSlug}
+        replayErrors={replayErrors}
+      >
+        <Layout.Page>
+          <Alert system type="warning" data-test-id="replay-deleted">
+            <Flex gap={space(0.5)}>
+              <IconDelete color="gray500" size="sm" />
+              {t('This replay has been deleted.')}
+            </Flex>
+          </Alert>
+        </Layout.Page>
+      </Page>
+    );
+  }
   if (fetchError) {
     if (fetchError.statusText === 'Not Found') {
       return (