Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 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,16 @@ function ReplayPreviewPlayer({
},
};

const {mutate: markAsViewed} = useMarkReplayViewed();
useEffect(() => {
if (!organization.features.includes('session-replay-viewed-by-ui')) {
return;
}
if (replayRecord && !replayRecord.has_viewed && !isFetching && isPlaying) {
markAsViewed({projectSlug: replayRecord.project_id, replayId: replayRecord.id});
}
}, [isFetching, isPlaying, markAsViewed, organization, 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,
});
}
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
27 changes: 26 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,30 @@ function ReplayDetails({params: {replaySlug}}: Props) {

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

const {mutate: markAsViewed} = useMarkReplayViewed();
useEffect(() => {
if (!organization.features.includes('session-replay-viewed-by-ui')) {
return;
}
if (
!fetchError &&
replayRecord &&
!replayRecord.has_viewed &&
projectSlug &&
!fetching
) {
markAsViewed({projectSlug, replayId});
}
}, [
fetchError,
fetching,
markAsViewed,
organization,
projectSlug,
replayId,
replayRecord,
]);

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