Browse Source

feat(replays): New column replayId into the mini events table on the Transaction Summary page (#39798)

Adds new column `replayid` inside the mini events table on the Transaction Summary page

### Changes
- [x] short form of the id, not the whole thing
- [x] link to the replay details page
- [x] link includes ?referrer
- [x] prefix with the replay 'Play' icon

![image](https://user-images.githubusercontent.com/39612839/195182922-7859d9a3-d7ce-47e1-88bd-e7436d0daf81.png)

### Tests notes

- Tested it with different transactions 
- Made sure the url was correct
- Made sure it didn't appear when the user doesn't have the replay
feature

Closes #39718
Jesus Padron 2 years ago
parent
commit
64f02c5245

+ 18 - 0
static/app/utils/discover/fieldRenderers.tsx

@@ -47,6 +47,7 @@ import {
   FlexContainer,
   NumberContainer,
   OverflowLink,
+  StyledIconPlay,
   UserIcon,
   VersionContainer,
 } from './styles';
@@ -285,6 +286,7 @@ type SpecialFields = {
   'issue.id': SpecialField;
   project: SpecialField;
   release: SpecialField;
+  replayId: SpecialField;
   team_key_transaction: SpecialField;
   'timestamp.to_day': SpecialField;
   'timestamp.to_hour': SpecialField;
@@ -337,6 +339,22 @@ const SPECIAL_FIELDS: SpecialFields = {
       );
     },
   },
+  replayId: {
+    sortField: 'replayId',
+    renderFunc: data => {
+      const replayId = data?.replayId;
+      if (typeof replayId !== 'string') {
+        return null;
+      }
+
+      return (
+        <FlexContainer>
+          <StyledIconPlay size="xs" />
+          <Container>{getShortEventId(replayId)}</Container>
+        </FlexContainer>
+      );
+    },
+  },
   issue: {
     sortField: null,
     renderFunc: (data, {organization}) => {

+ 7 - 0
static/app/utils/discover/styles.tsx

@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
 import DateTime from 'sentry/components/dateTime';
 import Link from 'sentry/components/links/link';
 import ShortId from 'sentry/components/shortId';
+import {IconPlay} from 'sentry/icons';
 import {IconUser} from 'sentry/icons/iconUser';
 import space from 'sentry/styles/space';
 
@@ -61,3 +62,9 @@ export const ActorContainer = styled('div')`
     cursor: default;
   }
 `;
+
+export const StyledIconPlay = styled(IconPlay)`
+  position: relative;
+  top: -1px;
+  margin-right: ${space(0.5)};
+`;

+ 6 - 2
static/app/views/performance/transactionSummary/pageLayout.tsx

@@ -39,7 +39,11 @@ export type ChildProps = {
 
 type Props = {
   childComponent: (props: ChildProps) => JSX.Element;
-  generateEventView: (props: {location: Location; transactionName: string}) => EventView;
+  generateEventView: (props: {
+    location: Location;
+    organization: Organization;
+    transactionName: string;
+  }) => EventView;
   getDocumentTitle: (name: string) => string;
   location: Location;
   organization: Organization;
@@ -76,7 +80,7 @@ function PageLayout(props: Props) {
 
   const project = projects.find(p => p.id === projectId);
 
-  const eventView = generateEventView({location, transactionName});
+  const eventView = generateEventView({location, transactionName, organization});
 
   return (
     <SentryDocumentTitle

+ 16 - 4
static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx

@@ -1,3 +1,5 @@
+import {InjectedRouter} from 'react-router';
+
 import {mountWithTheme} from 'sentry-test/enzyme';
 import {initializeOrg} from 'sentry-test/initializeOrg';
 
@@ -6,6 +8,7 @@ import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhan
 import {OrganizationContext} from 'sentry/views/organizationContext';
 import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
 import SummaryContent from 'sentry/views/performance/transactionSummary/transactionOverview/content';
+import {RouteContext} from 'sentry/views/routeContext';
 
 function initialize(project, query, additionalFeatures: string[] = []) {
   const features = ['transaction-event', 'performance-view', ...additionalFeatures];
@@ -49,13 +52,18 @@ function initialize(project, query, additionalFeatures: string[] = []) {
 
 const WrappedComponent = ({
   organization,
+  router,
   ...props
-}: React.ComponentProps<typeof SummaryContent>) => {
+}: React.ComponentProps<typeof SummaryContent> & {
+  router: InjectedRouter<Record<string, string>, any>;
+}) => {
   return (
     <OrganizationContext.Provider value={organization}>
-      <MEPSettingProvider>
-        <SummaryContent organization={organization} {...props} />
-      </MEPSettingProvider>
+      <RouteContext.Provider value={{router, ...router}}>
+        <MEPSettingProvider>
+          <SummaryContent organization={organization} {...props} />
+        </MEPSettingProvider>
+      </RouteContext.Provider>
     </OrganizationContext.Provider>
   );
 };
@@ -117,6 +125,7 @@ describe('Transaction Summary Content', function () {
       eventView,
       spanOperationBreakdownFilter,
       transactionName,
+      router,
     } = initialize(project, {});
     const routerContext = TestStubs.routerContext([{organization}]);
 
@@ -132,6 +141,7 @@ describe('Transaction Summary Content', function () {
         spanOperationBreakdownFilter={spanOperationBreakdownFilter}
         error={null}
         onChangeFilter={() => {}}
+        router={router}
       />,
       routerContext
     );
@@ -163,6 +173,7 @@ describe('Transaction Summary Content', function () {
       eventView,
       spanOperationBreakdownFilter,
       transactionName,
+      router,
     } = initialize(project, {}, ['performance-chart-interpolation']);
     const routerContext = TestStubs.routerContext([{organization}]);
 
@@ -178,6 +189,7 @@ describe('Transaction Summary Content', function () {
         spanOperationBreakdownFilter={spanOperationBreakdownFilter}
         error={null}
         onChangeFilter={() => {}}
+        router={router}
       />,
       routerContext
     );

+ 9 - 0
static/app/views/performance/transactionSummary/transactionOverview/content.tsx

@@ -31,6 +31,7 @@ import {
 import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
 import {canUseMetricsData} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import {decodeScalar} from 'sentry/utils/queryString';
+import {useRoutes} from 'sentry/utils/useRoutes';
 import withProjects from 'sentry/utils/withProjects';
 import {Actions, updateQuery} from 'sentry/views/eventsV2/table/cellAction';
 import {TableColumn} from 'sentry/views/eventsV2/table/types';
@@ -48,6 +49,7 @@ import Filter, {
   SpanOperationBreakdownFilter,
 } from '../filter';
 import {
+  generateReplayLink,
   generateTraceLink,
   generateTransactionLink,
   normalizeSearchConditions,
@@ -91,6 +93,8 @@ function SummaryContent({
   transactionName,
   onChangeFilter,
 }: Props) {
+  const routes = useRoutes();
+
   const useAggregateAlias = !organization.features.includes(
     'performance-frontend-use-events-endpoint'
   );
@@ -213,6 +217,10 @@ function SummaryContent({
     t('timestamp'),
   ];
 
+  if (organization.features.includes('session-replay-ui')) {
+    transactionsListTitles.push(t('replay id'));
+  }
+
   let transactionsListEventView = eventView.clone();
 
   if (organization.features.includes('performance-ops-breakdown')) {
@@ -325,6 +333,7 @@ function SummaryContent({
           generateLink={{
             id: generateTransactionLink(transactionName),
             trace: generateTraceLink(eventView.normalizeDateSelection(location)),
+            replayId: generateReplayLink(routes),
           }}
           handleCellAction={handleCellAction}
           {...getTransactionsListSort(location, {

+ 41 - 33
static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx

@@ -1,4 +1,4 @@
-import {browserHistory} from 'react-router';
+import {browserHistory, InjectedRouter} from 'react-router';
 import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
 
 import {initializeOrg} from 'sentry-test/initializeOrg';
@@ -12,6 +12,7 @@ import {
   MEPState,
 } from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import TransactionSummary from 'sentry/views/performance/transactionSummary/transactionOverview';
+import {RouteContext} from 'sentry/views/routeContext';
 
 const teams = [
   TestStubs.Team({id: '1', slug: 'team1', name: 'Team 1'}),
@@ -51,12 +52,19 @@ function initializeData({
   return initialData;
 }
 
-const TestComponent = ({...props}: React.ComponentProps<typeof TransactionSummary>) => {
+const TestComponent = ({
+  router,
+  ...props
+}: React.ComponentProps<typeof TransactionSummary> & {
+  router: InjectedRouter<Record<string, string>, any>;
+}) => {
   const client = new QueryClient();
 
   return (
     <QueryClientProvider client={client}>
-      <TransactionSummary {...props} />
+      <RouteContext.Provider value={{router, ...router}}>
+        <TransactionSummary {...props} />
+      </RouteContext.Provider>
     </QueryClientProvider>
   );
 };
@@ -393,7 +401,7 @@ describe('Performance > TransactionSummary', function () {
     it('renders basic UI elements', async function () {
       const {organization, router, routerContext} = initializeData();
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -436,7 +444,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['incidents'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -454,7 +462,7 @@ describe('Performance > TransactionSummary', function () {
         },
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -473,7 +481,7 @@ describe('Performance > TransactionSummary', function () {
     it('renders sidebar widgets', async function () {
       const {organization, router, routerContext} = initializeData();
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -512,7 +520,7 @@ describe('Performance > TransactionSummary', function () {
         },
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -539,7 +547,7 @@ describe('Performance > TransactionSummary', function () {
         },
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -553,7 +561,7 @@ describe('Performance > TransactionSummary', function () {
     it('triggers a navigation on search', function () {
       const {organization, router, routerContext} = initializeData();
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -578,7 +586,7 @@ describe('Performance > TransactionSummary', function () {
     it('can mark a transaction as key', async function () {
       const {organization, router, routerContext} = initializeData();
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -605,7 +613,7 @@ describe('Performance > TransactionSummary', function () {
     it('triggers a navigation on transaction filter', async function () {
       const {organization, router, routerContext} = initializeData();
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -634,7 +642,7 @@ describe('Performance > TransactionSummary', function () {
     it('renders pagination buttons', async function () {
       const {organization, router, routerContext} = initializeData();
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -667,7 +675,7 @@ describe('Performance > TransactionSummary', function () {
         query: {query: 'tag:value'},
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -693,7 +701,7 @@ describe('Performance > TransactionSummary', function () {
         query: {query: 'tag:value event.type:transaction'},
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -713,7 +721,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-suspect-spans-view'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -724,7 +732,7 @@ describe('Performance > TransactionSummary', function () {
     it('adds search condition on transaction status when clicking on status breakdown', async function () {
       const {organization, router, routerContext} = initializeData();
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -746,7 +754,7 @@ describe('Performance > TransactionSummary', function () {
     it('appends tag value to existing query when clicked', async function () {
       const {organization, router, routerContext} = initializeData();
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -800,7 +808,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -843,7 +851,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['incidents', 'performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -862,7 +870,7 @@ describe('Performance > TransactionSummary', function () {
         },
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -883,7 +891,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -924,7 +932,7 @@ describe('Performance > TransactionSummary', function () {
         },
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -953,7 +961,7 @@ describe('Performance > TransactionSummary', function () {
         },
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -969,7 +977,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -996,7 +1004,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -1025,7 +1033,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -1056,7 +1064,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -1090,7 +1098,7 @@ describe('Performance > TransactionSummary', function () {
         query: {query: 'tag:value'},
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -1117,7 +1125,7 @@ describe('Performance > TransactionSummary', function () {
         query: {query: 'tag:value event.type:transaction'},
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -1140,7 +1148,7 @@ describe('Performance > TransactionSummary', function () {
         ],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -1153,7 +1161,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });
@@ -1177,7 +1185,7 @@ describe('Performance > TransactionSummary', function () {
         features: ['performance-frontend-use-events-endpoint'],
       });
 
-      render(<TestComponent location={router.location} />, {
+      render(<TestComponent router={router} location={router.location} />, {
         context: routerContext,
         organization,
       });

+ 6 - 0
static/app/views/performance/transactionSummary/transactionOverview/index.tsx

@@ -155,9 +155,11 @@ function getDocumentTitle(transactionName: string): string {
 
 function generateEventView({
   location,
+  organization,
   transactionName,
 }: {
   location: Location;
+  organization: Organization;
   transactionName: string;
 }): EventView {
   // Use the user supplied query but overwrite any transaction or event type
@@ -176,6 +178,10 @@ function generateEventView({
 
   const fields = ['id', 'user.display', 'transaction.duration', 'trace', 'timestamp'];
 
+  if (organization.features.includes('session-replay-ui')) {
+    fields.push('replayId');
+  }
+
   return EventView.fromNewQueryWithLocation(
     {
       id: undefined,

+ 21 - 0
static/app/views/performance/transactionSummary/utils.tsx

@@ -1,3 +1,4 @@
+import {PlainRoute} from 'react-router';
 import styled from '@emotion/styled';
 import {LocationDescriptor, Query} from 'history';
 
@@ -5,6 +6,7 @@ import space from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
 import {TableDataRow} from 'sentry/utils/discover/discoverQuery';
 import {generateEventSlug} from 'sentry/utils/discover/urls';
+import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
 import {getTransactionDetailsUrl} from 'sentry/utils/performance/urls';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
@@ -133,6 +135,25 @@ export function generateTransactionLink(transactionName: string) {
   };
 }
 
+export function generateReplayLink(routes: PlainRoute<any>[]) {
+  return (
+    organization: Organization,
+    tableRow: TableDataRow,
+    _query: Query
+  ): LocationDescriptor => {
+    const replayId = tableRow.replayId;
+    const replaySlug = `${tableRow['project.name']}:${replayId}`;
+    const referrer = encodeURIComponent(getRouteStringFromRoutes(routes));
+
+    return {
+      pathname: `/organizations/${organization.slug}/replays/${replaySlug}`,
+      query: {
+        referrer,
+      },
+    };
+  };
+}
+
 export const SidebarSpacer = styled('div')`
   margin-top: ${space(3)};
 `;