Browse Source

feat(replay): Refetch the viewed-by data after an update to it (#69357)

This follows https://github.com/getsentry/sentry/pull/69232 which
removed some optimistic updated that would cause crazy re-rendering.
Instead we'll just invalidate (and re-fetch if needed) the `/viewed-by/`
endpoint data.

This is related to the viewed-by replay project:
https://github.com/getsentry/team-replay/issues/19
https://github.com/getsentry/sentry/issues/64924
Ryan Albrecht 10 months ago
parent
commit
1d04b42f8a

+ 1 - 1
static/app/components/events/eventReplay/replayPreviewPlayer.tsx

@@ -79,7 +79,7 @@ function ReplayPreviewPlayer({
 
   const {mutate: markAsViewed} = useMarkReplayViewed();
   useEffect(() => {
-    if (replayRecord && !replayRecord.has_viewed && !isFetching && isPlaying) {
+    if (replayRecord?.id && !replayRecord.has_viewed && !isFetching && isPlaying) {
       markAsViewed({projectSlug: replayRecord.project_id, replayId: replayRecord.id});
     }
   }, [isFetching, isPlaying, markAsViewed, organization, replayRecord]);

+ 5 - 17
static/app/components/replays/header/replayMetaData.tsx

@@ -1,18 +1,16 @@
-import {Fragment, useEffect} from 'react';
+import {Fragment} from 'react';
 import styled from '@emotion/styled';
-import * as Sentry from '@sentry/react';
 
-import AvatarList from 'sentry/components/avatar/avatarList';
 import Link from 'sentry/components/links/link';
 import ErrorCounts from 'sentry/components/replays/header/errorCounts';
 import HeaderPlaceholder from 'sentry/components/replays/header/headerPlaceholder';
+import ReplayViewers from 'sentry/components/replays/header/replayViewers';
 import {IconCursorArrow} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import EventView from 'sentry/utils/discover/eventView';
 import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
 import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
-import useReplayViewedByData from 'sentry/utils/replays/hooks/useReplayViewedByData';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useRoutes} from 'sentry/utils/useRoutes';
 import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
@@ -28,16 +26,6 @@ function ReplayMetaData({replayErrors, replayRecord, showDeadRageClicks = true}:
   const routes = useRoutes();
   const referrer = getRouteStringFromRoutes(routes);
   const eventView = EventView.fromLocation(location);
-  const viewersResult = useReplayViewedByData({
-    projectSlug: replayRecord?.project_id,
-    replayId: replayRecord?.id,
-  });
-
-  useEffect(() => {
-    if (viewersResult.isError) {
-      Sentry.captureException(viewersResult.error);
-    }
-  });
 
   const breadcrumbTab = {
     ...location,
@@ -97,10 +85,10 @@ function ReplayMetaData({replayErrors, replayRecord, showDeadRageClicks = true}:
       </KeyMetricData>
       <KeyMetricLabel>{t('Seen By')}</KeyMetricLabel>
       <KeyMetricData>
-        {viewersResult.isLoading ? (
-          <HeaderPlaceholder width="55px" height="27px" />
+        {replayRecord ? (
+          <ReplayViewers projectId={replayRecord.project_id} replayId={replayRecord.id} />
         ) : (
-          <AvatarList avatarSize={25} users={viewersResult.data?.data.viewed_by} />
+          <HeaderPlaceholder width="55px" height="27px" />
         )}
       </KeyMetricData>
     </KeyMetrics>

+ 36 - 0
static/app/components/replays/header/replayViewers.tsx

@@ -0,0 +1,36 @@
+import AvatarList from 'sentry/components/avatar/avatarList';
+import HeaderPlaceholder from 'sentry/components/replays/header/headerPlaceholder';
+import type {User} from 'sentry/types/user';
+import {useApiQuery} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+import useProjects from 'sentry/utils/useProjects';
+
+type TResponseData = {
+  data: {
+    viewed_by: User[];
+  };
+};
+
+interface Props {
+  projectId: string;
+  replayId: string;
+}
+
+export default function ReplayViewers({projectId, replayId}: Props) {
+  const organization = useOrganization();
+
+  const {projects} = useProjects();
+  const project = projects.find(p => p.id === projectId);
+  const projectSlug = project?.slug;
+  const url = `/projects/${organization.slug}/${projectSlug}/replays/${replayId}/viewed-by/`;
+
+  const {data, isError, isLoading} = useApiQuery<TResponseData>([url], {
+    staleTime: 0,
+  });
+
+  return isLoading || isError ? (
+    <HeaderPlaceholder width="55px" height="27px" />
+  ) : (
+    <AvatarList avatarSize={25} users={data?.data.viewed_by} />
+  );
+}

+ 1 - 1
static/app/utils/replays/hooks/useFetchReplayList.ts

@@ -76,7 +76,7 @@ export default function useFetchReplayList({
   }, [options, organization.slug, queryReferrer]);
 
   const {data, ...result} = useApiQuery<{data: any[]}>(fixedQueryKey, {
-    staleTime: Infinity,
+    staleTime: 0,
     enabled: true,
   });
 

+ 6 - 2
static/app/utils/replays/hooks/useMarkReplayViewed.tsx

@@ -1,4 +1,4 @@
-import {fetchMutation, useMutation} from 'sentry/utils/queryClient';
+import {fetchMutation, useMutation, useQueryClient} from 'sentry/utils/queryClient';
 import useApi from 'sentry/utils/useApi';
 
 type TData = unknown;
@@ -13,13 +13,17 @@ export default function useMarkReplayViewed() {
   const api = useApi({
     persistInFlight: false,
   });
+  const queryClient = useQueryClient();
 
   return useMutation<TData, TError, TVariables, TContext>({
     mutationFn: ({projectSlug, replayId}) => {
       const url = `/projects/${organization.slug}/${projectSlug}/replays/${replayId}/viewed-by/`;
       return fetchMutation(api)(['POST', url]);
     },
-    cacheTime: 0,
+    onSuccess(_data, {projectSlug, replayId}) {
+      const url = `/projects/${organization.slug}/${projectSlug}/replays/${replayId}/viewed-by/`;
+      queryClient.refetchQueries({queryKey: [url]});
+    },
     retry: false,
   });
 }

+ 0 - 30
static/app/utils/replays/hooks/useReplayViewedByData.tsx

@@ -1,30 +0,0 @@
-import type {User} from 'sentry/types/user';
-import {useApiQuery, type UseApiQueryOptions} from 'sentry/utils/queryClient';
-import useOrganization from 'sentry/utils/useOrganization';
-
-interface Props {
-  projectSlug: undefined | string;
-  replayId: undefined | string;
-}
-
-type TResponseData = {
-  data: {
-    viewed_by: User[];
-  };
-};
-
-export default function useReplayViewedByData(
-  {projectSlug, replayId}: Props,
-  options: Partial<UseApiQueryOptions<TResponseData>> = {}
-) {
-  const organization = useOrganization();
-  return useApiQuery<TResponseData>(
-    [`/projects/${organization.slug}/${projectSlug}/replays/${replayId}/viewed-by/`],
-    {
-      enabled: Boolean(projectSlug && replayId),
-      staleTime: Infinity,
-      retry: false,
-      ...options,
-    }
-  );
-}

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

@@ -79,7 +79,8 @@ function ReplayDetails({params: {replaySlug}}: Props) {
       replayRecord &&
       !replayRecord.has_viewed &&
       projectSlug &&
-      !fetching
+      !fetching &&
+      replayId
     ) {
       markAsViewed({projectSlug, replayId});
     }