Skip to content

Commit

Permalink
feat(replay): Persist has-viewed state to the server when replays are…
Browse files Browse the repository at this point in the history
… seen (#68743)

Depends on #67951

Relates to getsentry/team-replay#19
Relates to #64924
  • Loading branch information
ryan953 committed Apr 16, 2024
1 parent 96c8ab7 commit 6855896
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 10 deletions.
12 changes: 10 additions & 2 deletions static/app/components/events/eventReplay/replayPreviewPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ComponentProps} from 'react';
import {useRef, useState} from 'react';
import {useEffect, useRef, useState} from 'react';
import styled from '@emotion/styled';

import {Button, LinkButton} from 'sentry/components/button';
Expand All @@ -18,6 +18,7 @@ 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 useMarkReplayViewed from 'sentry/utils/replays/hooks/useMarkReplayViewed';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import {useRoutes} from 'sentry/utils/useRoutes';
Expand Down Expand Up @@ -53,7 +54,7 @@ function ReplayPreviewPlayer({
const location = useLocation();
const organization = useOrganization();
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const {replay, currentTime, isFinished, isPlaying} = useReplayContext();
const {replay, currentTime, isFetching, isFinished, isPlaying} = useReplayContext();
const eventView = EventView.fromLocation(location);

const fullscreenRef = useRef(null);
Expand All @@ -76,6 +77,13 @@ function ReplayPreviewPlayer({
},
};

const {mutate: markAsViewed} = useMarkReplayViewed();
useEffect(() => {
if (replayRecord && !replayRecord.has_viewed && !isFetching && isPlaying) {
markAsViewed({projectSlug: replayRecord.project_id, replayId: replayRecord.id});
}
}, [isFetching, isPlaying, markAsViewed, replayRecord]);

return (
<PlayerPanel>
<HeaderWrapper>
Expand Down
59 changes: 59 additions & 0 deletions static/app/utils/replays/hooks/useMarkReplayViewed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {useCallback} from 'react';

import type {ApiResult} from 'sentry/api';
import {fetchMutation, useMutation, useQueryClient} from 'sentry/utils/queryClient';
import useApi from 'sentry/utils/useApi';

type TData = unknown;
type TError = unknown;
type TVariables = {projectSlug: string; replayId: string};
type TContext = unknown;

import useOrganization from 'sentry/utils/useOrganization';

export default function useMarkReplayViewed() {
const organization = useOrganization();
const api = useApi({
persistInFlight: false,
});
const queryClient = useQueryClient();

const updateCache = useCallback(
({replayId}: TVariables, hasViewed: boolean) => {
const cache = queryClient.getQueryCache();
const cachedResponses = cache.findAll([
`/organizations/${organization.slug}/replays/${replayId}/`,
]);
cachedResponses.forEach(cached => {
const [data, ...rest] = cached.state.data as ApiResult<{
data: Record<string, unknown>;
}>;
cached.setData([
{
data: {
...data.data,
has_viewed: hasViewed,
},
},
...rest,
]);
});
},
[organization.slug, queryClient]
);

return useMutation<TData, TError, TVariables, TContext>({
onMutate: variables => {
updateCache(variables, true);
},
mutationFn: ({projectSlug, replayId}) => {
const url = `/projects/${organization.slug}/${projectSlug}/replays/${replayId}/viewed-by/`;
return fetchMutation(api)(['POST', url]);
},
onError: (_error, variables) => {
updateCache(variables, false);
},
cacheTime: 0,
retry: false,
});
}
20 changes: 13 additions & 7 deletions static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const mockEventTimestampMs = mockEventTimestamp.getTime();
// Get replay data with the mocked replay reader params
const mockReplay = ReplayReader.factory({
replayRecord: ReplayRecordFixture({
id: REPLAY_ID_1,
browser: {
name: 'Chrome',
version: '110.0.0',
Expand Down Expand Up @@ -83,7 +84,7 @@ mockUseReplayReader.mockImplementation(() => {
projectSlug: ProjectFixture().slug,
replay: mockReplay,
replayId: REPLAY_ID_1,
replayRecord: ReplayRecordFixture(),
replayRecord: ReplayRecordFixture({id: REPLAY_ID_1}),
};
});

Expand Down Expand Up @@ -370,7 +371,7 @@ describe('GroupReplays', () => {
count_errors: 1,
duration: 52346,
finished_at: new Date('2022-09-15T06:54:00+00:00'),
id: '346789a703f6454384f1de473b8b9fcc',
id: REPLAY_ID_1,
started_at: new Date('2022-09-15T06:50:00+00:00'),
urls: [
'https://dev.getsentry.net:7999/replays/',
Expand All @@ -382,7 +383,7 @@ describe('GroupReplays', () => {
count_errors: 4,
duration: 400,
finished_at: new Date('2022-09-21T21:40:38+00:00'),
id: 'b05dae9b6be54d21a4d5ad9f8f02b780',
id: REPLAY_ID_2,
started_at: new Date('2022-09-21T21:30:44+00:00'),
urls: [
'https://dev.getsentry.net:7999/organizations/org-slug/replays/?project=2&statsPeriod=24h',
Expand Down Expand Up @@ -475,7 +476,7 @@ describe('GroupReplays', () => {
count_errors: 1,
duration: 52346,
finished_at: new Date('2022-09-15T06:54:00+00:00'),
id: '346789a703f6454384f1de473b8b9fcc',
id: REPLAY_ID_1,
started_at: new Date('2022-09-15T06:50:00+00:00'),
urls: [
'https://dev.getsentry.net:7999/replays/',
Expand All @@ -487,7 +488,7 @@ describe('GroupReplays', () => {
count_errors: 4,
duration: 400,
finished_at: new Date('2022-09-21T21:40:38+00:00'),
id: 'b05dae9b6be54d21a4d5ad9f8f02b780',
id: REPLAY_ID_2,
started_at: new Date('2022-09-21T21:30:44+00:00'),
urls: [
'https://dev.getsentry.net:7999/organizations/org-slug/replays/?project=2&statsPeriod=24h',
Expand Down Expand Up @@ -531,6 +532,7 @@ describe('GroupReplays', () => {
organizationProps: {features: ['session-replay']},
}));
const mockGroup = GroupFixture();
const mockReplayRecord = mockReplay?.getReplay();

const mockReplayCountApi = MockApiClient.addMockResponse({
url: mockReplayCountUrl,
Expand All @@ -548,7 +550,7 @@ describe('GroupReplays', () => {
count_errors: 1,
duration: 52346,
finished_at: new Date('2022-09-15T06:54:00+00:00'),
id: '346789a703f6454384f1de473b8b9fcc',
id: REPLAY_ID_1,
started_at: new Date('2022-09-15T06:50:00+00:00'),
urls: [
'https://dev.getsentry.net:7999/replays/',
Expand All @@ -560,7 +562,7 @@ describe('GroupReplays', () => {
count_errors: 4,
duration: 400,
finished_at: new Date('2022-09-21T21:40:38+00:00'),
id: 'b05dae9b6be54d21a4d5ad9f8f02b780',
id: REPLAY_ID_2,
started_at: new Date('2022-09-21T21:30:44+00:00'),
urls: [
'https://dev.getsentry.net:7999/organizations/org-slug/replays/?project=2&statsPeriod=24h',
Expand All @@ -575,6 +577,10 @@ describe('GroupReplays', () => {
})),
},
});
MockApiClient.addMockResponse({
method: 'POST',
url: `/projects/${organization.slug}/${mockReplayRecord?.project_id}/replays/${mockReplayRecord?.id}/viewed-by/`,
});

render(<GroupReplays group={mockGroup} />, {
context: routerContext,
Expand Down
16 changes: 15 additions & 1 deletion static/app/views/replays/details.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Fragment} from 'react';
import {Fragment, useEffect} from 'react';
import type {RouteComponentProps} from 'react-router';

import Alert from 'sentry/components/alert';
Expand All @@ -17,6 +17,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
import type {TimeOffsetLocationQueryParams} from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
import useInitialTimeOffsetMs from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
import useLogReplayDataLoaded from 'sentry/utils/replays/hooks/useLogReplayDataLoaded';
import useMarkReplayViewed from 'sentry/utils/replays/hooks/useMarkReplayViewed';
import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview';
import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
Expand Down Expand Up @@ -71,6 +72,19 @@ function ReplayDetails({params: {replaySlug}}: Props) {

useLogReplayDataLoaded({fetchError, fetching, projectSlug, replay});

const {mutate: markAsViewed} = useMarkReplayViewed();
useEffect(() => {
if (
!fetchError &&
replayRecord &&
!replayRecord.has_viewed &&
projectSlug &&
!fetching
) {
markAsViewed({projectSlug, replayId});
}
}, [fetchError, fetching, markAsViewed, projectSlug, replayId, replayRecord]);

const initialTimeOffsetMs = useInitialTimeOffsetMs({
orgSlug,
projectSlug,
Expand Down

0 comments on commit 6855896

Please sign in to comment.