Просмотр исходного кода

feat(replay): Read the has_viewed field on replay records and reflect that in the list (#67951)

Depends on backend support for the new `has_viewed` field.

Follows the blueprint in
https://github.com/getsentry/sentry/blob/master/src/sentry/replays/blueprints/api.md
Relates to https://github.com/getsentry/team-replay/issues/19
Relates to https://github.com/getsentry/sentry/issues/64924


Depends on https://github.com/getsentry/sentry/pull/68628
Ryan Albrecht 11 месяцев назад
Родитель
Сommit
7e2cea737f

+ 1 - 0
fixtures/js-stubs/replayList.ts

@@ -20,6 +20,7 @@ export function ReplayListFixture(
       count_errors: 0,
       duration: duration(30000),
       finished_at: new Date('2022-09-15T06:54:00+00:00'),
+      has_viewed: false,
       id: '346789a703f6454384f1de473b8b9fcc',
       is_archived: false,
       os: {

+ 1 - 0
fixtures/js-stubs/replayRecord.ts

@@ -27,6 +27,7 @@ export function ReplayRecordFixture(
     environment: 'demo',
     error_ids: ['5c83aaccfffb4a708ae893bad9be3a1c'],
     finished_at: new Date('Sep 22, 2022 5:00:03 PM UTC'),
+    has_viewed: false,
     id: '761104e184c64d439ee1014b72b4d83b',
     is_archived: false,
     os: {

+ 5 - 0
static/app/utils/replays/replayDataUtils.tsx

@@ -5,6 +5,10 @@ import isValidDate from 'sentry/utils/date/isValidDate';
 import getMinMax from 'sentry/utils/getMinMax';
 import type {ReplayRecord} from 'sentry/views/replays/types';
 
+const defaultValues = {
+  has_viewed: false,
+};
+
 export function mapResponseToReplayRecord(apiResponse: any): ReplayRecord {
   // Marshal special fields into tags
   const user = Object.fromEntries(
@@ -40,6 +44,7 @@ export function mapResponseToReplayRecord(apiResponse: any): ReplayRecord {
   const finishedAt = new Date(apiResponse.finished_at);
   invariant(isValidDate(finishedAt), 'replay.finished_at is invalid');
   return {
+    ...defaultValues,
     ...apiResponse,
     ...(apiResponse.started_at ? {started_at: startedAt} : {}),
     ...(apiResponse.finished_at ? {finished_at: finishedAt} : {}),

+ 1 - 0
static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx

@@ -201,6 +201,7 @@ describe('GroupReplays', () => {
                 'count_rage_clicks',
                 'duration',
                 'finished_at',
+                'has_viewed',
                 'id',
                 'is_archived',
                 'os',

+ 37 - 16
static/app/views/replays/replayTable/tableCell.tsx

@@ -4,9 +4,9 @@ import styled from '@emotion/styled';
 import type {Location} from 'history';
 
 import Avatar from 'sentry/components/avatar';
+import UserAvatar from 'sentry/components/avatar/userAvatar';
 import {Button} from 'sentry/components/button';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import UserBadge from 'sentry/components/idBadge/userBadge';
 import Link from 'sentry/components/links/link';
 import ContextIcon from 'sentry/components/replays/contextIcon';
 import ReplayPlayPauseButton from 'sentry/components/replays/replayPlayPauseButton';
@@ -374,21 +374,25 @@ export function ReplayCell({
 
   return (
     <Item isWidget={isWidget} isReplayCell className={className}>
-      <UserBadge
-        avatarSize={24}
-        displayName={
-          replay.is_archived ? (
-            replay.user.display_name || t('Anonymous User')
-          ) : (
-            <MainLink to={detailsTab} onClick={trackNavigationEvent}>
-              {replay.user.display_name || t('Anonymous User')}
-            </MainLink>
-          )
-        }
-        user={getUserBadgeUser(replay)}
-        // this is the subheading for the avatar, so displayEmail in this case is a misnomer
-        displayEmail={subText}
-      />
+      <Row gap={1}>
+        <UserAvatar user={getUserBadgeUser(replay)} size={24} />
+        <SubText>
+          <Row gap={0.5}>
+            {replay.is_archived ? (
+              replay.user.display_name || t('Anonymous User')
+            ) : (
+              <MainLink
+                to={detailsTab}
+                onClick={trackNavigationEvent}
+                data-has-viewed={replay.has_viewed}
+              >
+                {replay.user.display_name || t('Anonymous User')}
+              </MainLink>
+            )}
+          </Row>
+          <Row gap={0.5}>{subText}</Row>
+        </SubText>
+      </Row>
     </Item>
   );
 }
@@ -417,6 +421,23 @@ const Row = styled('div')<{gap: ValidSize; minWidth?: number}>`
 
 const MainLink = styled(Link)`
   font-size: ${p => p.theme.fontSizeLarge};
+  line-height: normal;
+  ${p => p.theme.overflowEllipsis};
+
+  font-weight: bold;
+  &[data-has-viewed='true'] {
+    font-weight: normal;
+  }
+`;
+
+const SubText = styled('div')`
+  font-size: 0.875em;
+  line-height: normal;
+  color: ${p => p.theme.gray300};
+  ${p => p.theme.overflowEllipsis};
+  display: flex;
+  flex-direction: column;
+  gap: ${space(0.25)};
 `;
 
 export function TransactionCell({

+ 6 - 0
static/app/views/replays/types.tsx

@@ -40,6 +40,10 @@ export type ReplayRecord = {
    * The **latest** timestamp received as determined by the SDK.
    */
   finished_at: Date;
+  /**
+   * Whether the currently authenticated user has seen this replay or not.
+   */
+  has_viewed: boolean;
   /**
    * The ID of the Replay instance
    */
@@ -120,6 +124,7 @@ export const REPLAY_LIST_FIELDS = [
   'count_rage_clicks',
   'duration',
   'finished_at',
+  'has_viewed',
   'id',
   'is_archived',
   'os.name',
@@ -139,6 +144,7 @@ export type ReplayListRecord = Pick<
   | 'count_rage_clicks'
   | 'duration'
   | 'finished_at'
+  | 'has_viewed'
   | 'id'
   | 'is_archived'
   | 'os'