Browse Source

feat(replays): add dead click and rage click columns to replay list (#53116)

Closes #52980 by adding v0 dead click and rage click columns to the
replay list table. Also, OS and browser version numbers are always
hidden.

<img width="1224" alt="SCR-20230718-ovzo"
src="https://github.com/getsentry/sentry/assets/56095982/45cd0376-787a-4067-970e-641b25753a8c">


<img width="1219" alt="SCR-20230718-owge"
src="https://github.com/getsentry/sentry/assets/56095982/c67d3915-acb3-42b6-90cc-0adcb8e4e6b0">
Michelle Zhang 1 year ago
parent
commit
fa6092029d

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

@@ -15,6 +15,8 @@ export function ReplayList(
         name: 'Firefox',
         version: '111.0',
       },
+      count_dead_clicks: 0,
+      count_rage_clicks: 0,
       count_errors: 0,
       duration: duration(30000),
       finished_at: new Date('2022-09-15T06:54:00+00:00'),

+ 3 - 2
static/app/components/replays/contextIcon.tsx

@@ -12,13 +12,14 @@ type Props = {
   name: string;
   version: undefined | string;
   className?: string;
+  showVersion?: boolean;
 };
 
 const LazyContextIcon = lazy(
   () => import('sentry/components/events/contextSummary/contextIcon')
 );
 
-const ContextIcon = styled(({className, name, version}: Props) => {
+const ContextIcon = styled(({className, name, version, showVersion}: Props) => {
   const icon = generateIconName(name, version);
 
   const title = (
@@ -34,7 +35,7 @@ const ContextIcon = styled(({className, name, version}: Props) => {
       <Suspense fallback={<LoadingMask />}>
         <LazyContextIcon name={icon} size="sm" />
       </Suspense>
-      {version ? version : null}
+      {showVersion ? (version ? version : null) : undefined}
     </Tooltip>
   );
 })`

+ 2 - 0
static/app/components/replays/header/replayMetaData.tsx

@@ -23,6 +23,7 @@ function ReplayMetaData({replayErrors, replayRecord}: Props) {
         <ContextIcon
           name={replayRecord?.os.name ?? ''}
           version={replayRecord?.os.version ?? undefined}
+          showVersion
         />
       </KeyMetricData>
 
@@ -31,6 +32,7 @@ function ReplayMetaData({replayErrors, replayRecord}: Props) {
         <ContextIcon
           name={replayRecord?.browser.name ?? ''}
           version={replayRecord?.browser.version ?? undefined}
+          showVersion
         />
       </KeyMetricData>
 

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

@@ -120,7 +120,9 @@ describe('GroupReplays', () => {
               field: [
                 'activity',
                 'browser',
+                'count_dead_clicks',
                 'count_errors',
+                'count_rage_clicks',
                 'duration',
                 'finished_at',
                 'id',

+ 24 - 8
static/app/views/replays/list/replaysList.tsx

@@ -92,6 +92,29 @@ function ReplaysListTable({
 
   const hasReplayClick = conditions.getFilterKeys().some(k => k.startsWith('click.'));
 
+  const hasDeadRageCols = organization.features.includes(
+    'replay-rage-click-dead-click-columns'
+  );
+  const visibleCols = hasDeadRageCols
+    ? [
+        ReplayColumn.REPLAY,
+        ReplayColumn.OS,
+        ReplayColumn.BROWSER,
+        ReplayColumn.DURATION,
+        ReplayColumn.COUNT_ERRORS,
+        ReplayColumn.COUNT_DEAD_CLICKS,
+        ReplayColumn.COUNT_RAGE_CLICKS,
+        ReplayColumn.ACTIVITY,
+      ]
+    : [
+        ReplayColumn.REPLAY,
+        ReplayColumn.OS,
+        ReplayColumn.BROWSER,
+        ReplayColumn.DURATION,
+        ReplayColumn.COUNT_ERRORS,
+        ReplayColumn.ACTIVITY,
+      ];
+
   return (
     <Fragment>
       <ReplayTable
@@ -99,14 +122,7 @@ function ReplaysListTable({
         isFetching={isFetching}
         replays={replays}
         sort={eventView.sorts[0]}
-        visibleColumns={[
-          ReplayColumn.REPLAY,
-          ReplayColumn.OS,
-          ReplayColumn.BROWSER,
-          ReplayColumn.DURATION,
-          ReplayColumn.COUNT_ERRORS,
-          ReplayColumn.ACTIVITY,
-        ]}
+        visibleColumns={visibleCols}
         emptyMessage={
           allSelectedProjectsNeedUpdates && hasReplayClick ? (
             <Fragment>

+ 24 - 0
static/app/views/replays/replayTable/headerCell.tsx

@@ -25,9 +25,33 @@ function HeaderCell({column, sort}: Props) {
     case ReplayColumn.BROWSER:
       return <SortableHeader sort={sort} fieldName="browser.name" label={t('Browser')} />;
 
+    case ReplayColumn.COUNT_DEAD_CLICKS:
+      return (
+        <SortableHeader
+          sort={sort}
+          fieldName="count_dead_clicks"
+          label={t('Dead Clicks')}
+          tooltip={t(
+            'A dead click is a user click that does not result in any page activity after 7 seconds.'
+          )}
+        />
+      );
+
     case ReplayColumn.COUNT_ERRORS:
       return <SortableHeader sort={sort} fieldName="count_errors" label={t('Errors')} />;
 
+    case ReplayColumn.COUNT_RAGE_CLICKS:
+      return (
+        <SortableHeader
+          sort={sort}
+          fieldName="count_rage_clicks"
+          label={t('Rage Clicks')}
+          tooltip={t(
+            'A rage click is 5 or more clicks on a dead element, which exhibits no page activity after 7 seconds.'
+          )}
+        />
+      );
+
     case ReplayColumn.DURATION:
       return <SortableHeader sort={sort} fieldName="duration" label={t('Duration')} />;
 

+ 8 - 0
static/app/views/replays/replayTable/index.tsx

@@ -15,9 +15,11 @@ import HeaderCell from 'sentry/views/replays/replayTable/headerCell';
 import {
   ActivityCell,
   BrowserCell,
+  DeadClickCountCell,
   DurationCell,
   ErrorCountCell,
   OSCell,
+  RageClickCountCell,
   ReplayCell,
   TransactionCell,
 } from 'sentry/views/replays/replayTable/tableCell';
@@ -92,9 +94,15 @@ function ReplayTable({
                 case ReplayColumn.BROWSER:
                   return <BrowserCell key="browser" replay={replay} />;
 
+                case ReplayColumn.COUNT_DEAD_CLICKS:
+                  return <DeadClickCountCell key="countDeadClicks" replay={replay} />;
+
                 case ReplayColumn.COUNT_ERRORS:
                   return <ErrorCountCell key="countErrors" replay={replay} />;
 
+                case ReplayColumn.COUNT_RAGE_CLICKS:
+                  return <RageClickCountCell key="countRageClicks" replay={replay} />;
+
                 case ReplayColumn.DURATION:
                   return <DurationCell key="duration" replay={replay} />;
 

+ 32 - 0
static/app/views/replays/replayTable/tableCell.tsx

@@ -190,6 +190,7 @@ export function OSCell({replay}: Props) {
       <ContextIcon
         name={name ?? ''}
         version={version && hasRoomForColumns ? version : undefined}
+        showVersion={false}
       />
     </Item>
   );
@@ -208,6 +209,7 @@ export function BrowserCell({replay}: Props) {
       <ContextIcon
         name={name ?? ''}
         version={version && hasRoomForColumns ? version : undefined}
+        showVersion={false}
       />
     </Item>
   );
@@ -224,6 +226,36 @@ export function DurationCell({replay}: Props) {
   );
 }
 
+export function RageClickCountCell({replay}: Props) {
+  if (replay.is_archived) {
+    return <Item isArchived />;
+  }
+  return (
+    <Item data-test-id="replay-table-count-rage-clicks">
+      {replay.count_rage_clicks ? (
+        <Count>{replay.count_rage_clicks}</Count>
+      ) : (
+        <Count>0</Count>
+      )}
+    </Item>
+  );
+}
+
+export function DeadClickCountCell({replay}: Props) {
+  if (replay.is_archived) {
+    return <Item isArchived />;
+  }
+  return (
+    <Item data-test-id="replay-table-count-dead-clicks">
+      {replay.count_dead_clicks ? (
+        <Count>{replay.count_dead_clicks}</Count>
+      ) : (
+        <Count>0</Count>
+      )}
+    </Item>
+  );
+}
+
 export function ErrorCountCell({replay}: Props) {
   if (replay.is_archived) {
     return <Item isArchived />;

+ 2 - 0
static/app/views/replays/replayTable/types.tsx

@@ -1,7 +1,9 @@
 export enum ReplayColumn {
   ACTIVITY = 'activity',
   BROWSER = 'browser',
+  COUNT_DEAD_CLICKS = 'countDeadClicks',
   COUNT_ERRORS = 'countErrors',
+  COUNT_RAGE_CLICKS = 'countRageClicks',
   DURATION = 'duration',
   OS = 'os',
   REPLAY = 'replay',

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

@@ -116,7 +116,9 @@ export type ReplayListRecord = Pick<
   ReplayRecord,
   | 'activity'
   | 'browser'
+  | 'count_dead_clicks'
   | 'count_errors'
+  | 'count_rage_clicks'
   | 'duration'
   | 'finished_at'
   | 'id'
@@ -133,7 +135,9 @@ export const REPLAY_LIST_FIELDS: ReplayRecordNestedFieldName[] = [
   'activity',
   'browser.name',
   'browser.version',
+  'count_dead_clicks',
   'count_errors',
+  'count_rage_clicks',
   'duration',
   'finished_at',
   'id',