Browse Source

feat(replays): add feature flagged dead and rage click columns to replays list (#53212)

Updated reverted PR (https://github.com/getsentry/sentry/pull/53116) to
add feature flagging to protect the data fetching of `count_dead_clicks`
and `count_rage_clicks`
Michelle Zhang 1 year ago
parent
commit
41d5175c00

+ 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>
 

+ 3 - 3
static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx

@@ -9,7 +9,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
 import {DEFAULT_SORT} from 'sentry/utils/replays/fetchReplayList';
 import useApi from 'sentry/utils/useApi';
 import useCleanQueryParamsOnRouteLeave from 'sentry/utils/useCleanQueryParamsOnRouteLeave';
-import {REPLAY_LIST_FIELDS} from 'sentry/views/replays/types';
+import {getReplayListFields} from 'sentry/views/replays/types';
 
 function useReplayFromIssue({
   group,
@@ -54,13 +54,13 @@ function useReplayFromIssue({
       id: '',
       name: '',
       version: 2,
-      fields: REPLAY_LIST_FIELDS,
+      fields: getReplayListFields(organization),
       query: `id:[${String(replayIds)}]`,
       range: '14d',
       projects: [],
       orderby: decodeScalar(location.query.sort, DEFAULT_SORT),
     });
-  }, [location.query.sort, replayIds]);
+  }, [location.query.sort, replayIds, organization]);
 
   useCleanQueryParamsOnRouteLeave({fieldsToClean: ['cursor']});
   useEffect(() => {

+ 4 - 3
static/app/views/performance/transactionSummary/transactionReplays/useReplaysFromTransaction.tsx

@@ -9,7 +9,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
 import {DEFAULT_SORT} from 'sentry/utils/replays/fetchReplayList';
 import useApi from 'sentry/utils/useApi';
 import type {ReplayListLocationQuery} from 'sentry/views/replays/types';
-import {REPLAY_LIST_FIELDS} from 'sentry/views/replays/types';
+import {getReplayListFields} from 'sentry/views/replays/types';
 
 type Options = {
   location: Location;
@@ -82,16 +82,17 @@ function useReplaysFromTransaction({
     if (!response.replayIds) {
       return null;
     }
+
     return EventView.fromSavedQuery({
       id: '',
       name: '',
       version: 2,
-      fields: REPLAY_LIST_FIELDS,
+      fields: getReplayListFields(organization),
       projects: [],
       query: `id:[${String(response.replayIds)}]`,
       orderby: decodeScalar(location.query.sort, DEFAULT_SORT),
     });
-  }, [location.query.sort, response.replayIds]);
+  }, [location.query.sort, response.replayIds, organization]);
 
   useEffect(() => {
     fetchReplayIds();

+ 27 - 11
static/app/views/replays/list/replaysList.tsx

@@ -21,7 +21,7 @@ import ReplayOnboardingPanel from 'sentry/views/replays/list/replayOnboardingPan
 import ReplayTable from 'sentry/views/replays/replayTable';
 import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
 import type {ReplayListLocationQuery} from 'sentry/views/replays/types';
-import {REPLAY_LIST_FIELDS} from 'sentry/views/replays/types';
+import {getReplayListFields} from 'sentry/views/replays/types';
 
 function ReplaysList() {
   const location = useLocation<ReplayListLocationQuery>();
@@ -36,14 +36,14 @@ function ReplaysList() {
         id: '',
         name: '',
         version: 2,
-        fields: REPLAY_LIST_FIELDS,
+        fields: getReplayListFields(organization),
         projects: [],
         query: conditions.formatString(),
         orderby: decodeScalar(location.query.sort, DEFAULT_SORT),
       },
       location
     );
-  }, [location]);
+  }, [location, organization]);
 
   const hasSessionReplay = organization.features.includes('session-replay');
   const {hasSentOneReplay, fetching} = useHaveSelectedProjectsSentAnyReplayEvents();
@@ -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',

Some files were not shown because too many files changed in this diff