Browse Source

add trend breakpoint on transaction summary page (#49765)

<!-- Describe your PR here. -->

The breakpoint that's visible on the Most Regressed Transactions graph
on the Trends page is now visible on the Trends graph on the Transaction
Summary page.

<table>
<tr>
<th>Before</th>
<th>After</th>
</tr>
<tr>
<td><img
src="https://github.com/getsentry/sentry/assets/72356613/b9e7e6c9-4eea-4b1b-adc4-79888c7bfc54"></td>
<td><img
src="https://github.com/getsentry/sentry/assets/72356613/93825a1f-2568-4f5f-a868-32d2c480e25f"></td>
</tr>
</table>

The `events-trends-statsv2` endpoint is called to obtain the breakpoint
values. These breakpoints are then translated into series data that have
been put on the existing graph.
nikkikapadia 1 year ago
parent
commit
f3e79aa189

+ 6 - 1
static/app/views/performance/transactionSummary/transactionOverview/charts.tsx

@@ -10,7 +10,7 @@ import {
 } from 'sentry/components/charts/styles';
 import {Panel} from 'sentry/components/panels';
 import {t} from 'sentry/locale';
-import {Organization, SelectValue} from 'sentry/types';
+import {Organization, Project, SelectValue} from 'sentry/types';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import EventView from 'sentry/utils/discover/eventView';
 import {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
@@ -72,6 +72,7 @@ type Props = {
   organization: Organization;
   totalValue: number | null;
   withoutZerofill: boolean;
+  project?: Project;
 };
 
 function TransactionSummaryCharts({
@@ -81,6 +82,7 @@ function TransactionSummaryCharts({
   location,
   currentFilter,
   withoutZerofill,
+  project,
 }: Props) {
   function handleDisplayChange(value: string) {
     const display = decodeScalar(location.query.display, DisplayModes.DURATION);
@@ -201,6 +203,7 @@ function TransactionSummaryCharts({
         )}
         {display === DisplayModes.TREND && (
           <TrendChart
+            eventView={eventView}
             trendFunction={trendFunction}
             trendParameter={trendColumn}
             organization={organization}
@@ -212,6 +215,8 @@ function TransactionSummaryCharts({
             end={eventView.end}
             statsPeriod={eventView.statsPeriod}
             withoutZerofill={withoutZerofill}
+            projects={project ? [project] : []}
+            withBreakpoint={organization.features.includes('performance-new-trends')}
           />
         )}
         {display === DisplayModes.VITALS && (

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

@@ -370,6 +370,7 @@ function SummaryContent({
             totalValue={totalCount}
             currentFilter={spanOperationBreakdownFilter}
             withoutZerofill={hasPerformanceChartInterpolation}
+            project={project}
           />
           <TransactionsList
             location={location}

+ 17 - 2
static/app/views/performance/transactionSummary/transactionOverview/trendChart/content.tsx

@@ -18,6 +18,8 @@ import {
   tooltipFormatter,
 } from 'sentry/utils/discover/charts';
 import getDynamicText from 'sentry/utils/getDynamicText';
+import {NormalizedTrendsTransaction} from 'sentry/views/performance/trends/types';
+import {getIntervalLine} from 'sentry/views/performance/utils';
 
 import {transformEventStatsSmoothed} from '../../../trends/utils';
 
@@ -33,6 +35,8 @@ type Props = {
     end: number;
     start: number;
   };
+  transaction?: NormalizedTrendsTransaction;
+  withBreakpoint?: boolean;
 } & Omit<React.ComponentProps<typeof ReleaseSeries>, 'children' | 'queryExtra'> &
   Pick<LineChartProps, 'onLegendSelectChanged' | 'legend'>;
 
@@ -52,6 +56,8 @@ function Content({
   utc,
   queryExtra,
   router,
+  withBreakpoint,
+  transaction,
   onLegendSelectChanged,
 }: Props) {
   if (errored) {
@@ -77,6 +83,11 @@ function Content({
         .reverse()
     : [];
 
+  const needsLabel = false;
+  const breakpointSeries = withBreakpoint
+    ? getIntervalLine(theme, data || [], 0.5, needsLabel, transaction)
+    : [];
+
   const durationUnit = getDurationUnit(series, legend);
 
   const chartOptions: Omit<LineChartProps, 'series'> = {
@@ -90,7 +101,6 @@ function Content({
       showSymbol: false,
     },
     tooltip: {
-      trigger: 'axis',
       valueFormatter: (value: number | null) => tooltipFormatter(value, 'duration'),
     },
     xAxis: timeFrame
@@ -146,7 +156,12 @@ function Content({
                     {...chartOptions}
                     legend={legend}
                     onLegendSelectChanged={onLegendSelectChanged}
-                    series={[...series, ...smoothedSeries, ...releaseSeries]}
+                    series={[
+                      ...series,
+                      ...smoothedSeries,
+                      ...releaseSeries,
+                      ...breakpointSeries,
+                    ]}
                   />
                 ),
                 fixed: <Placeholder height="200px" testId="skeleton-ui" />,

+ 171 - 27
static/app/views/performance/transactionSummary/transactionOverview/trendChart/index.tsx

@@ -9,24 +9,40 @@ import {getInterval, getSeriesSelection} from 'sentry/components/charts/utils';
 import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
 import QuestionTooltip from 'sentry/components/questionTooltip';
 import {t} from 'sentry/locale';
-import {OrganizationSummary} from 'sentry/types';
+import {EventsStats, EventsStatsData, OrganizationSummary, Project} from 'sentry/types';
+import {Series} from 'sentry/types/echarts';
 import {getUtcToLocalDateObject} from 'sentry/utils/dates';
+import EventView from 'sentry/utils/discover/eventView';
+import {DURATION_UNITS, SIZE_UNITS} from 'sentry/utils/discover/fieldRenderers';
+import {getAggregateAlias} from 'sentry/utils/discover/fields';
+import TrendsDiscoverQuery from 'sentry/utils/performance/trends/trendsDiscoverQuery';
 import useApi from 'sentry/utils/useApi';
 import {useLocation} from 'sentry/utils/useLocation';
 import useRouter from 'sentry/utils/useRouter';
-
-import {TrendFunctionField} from '../../../trends/types';
-import {generateTrendFunctionAsString} from '../../../trends/utils';
-import {ViewProps} from '../../../types';
+import {
+  TrendChangeType,
+  TrendFunctionField,
+  TrendView,
+} from 'sentry/views/performance/trends/types';
+import {
+  generateTrendFunctionAsString,
+  modifyTrendView,
+  normalizeTrends,
+} from 'sentry/views/performance/trends/utils';
+import {ViewProps} from 'sentry/views/performance/types';
+import {getSelectedTransaction} from 'sentry/views/performance/utils';
 
 import Content from './content';
 
 type Props = ViewProps & {
+  eventView: EventView;
   organization: OrganizationSummary;
+  projects: Project[];
   queryExtra: Query;
   trendFunction: TrendFunctionField;
   trendParameter: string;
   withoutZerofill: boolean;
+  withBreakpoint?: boolean;
 };
 
 function TrendChart({
@@ -39,8 +55,11 @@ function TrendChart({
   trendParameter,
   queryExtra,
   withoutZerofill,
+  withBreakpoint,
+  eventView,
   start: propsStart,
   end: propsEnd,
+  projects,
 }: Props) {
   const router = useRouter();
   const location = useLocation();
@@ -116,31 +135,156 @@ function TrendChart({
 
   const trendDisplay = generateTrendFunctionAsString(trendFunction, trendParameter);
 
+  const trendView = eventView.clone() as TrendView;
+  modifyTrendView(
+    trendView,
+    location,
+    TrendChangeType.REGRESSION,
+    projects,
+    organization
+  );
+
+  function transformTimeseriesData(
+    data: EventsStatsData,
+    meta: EventsStats['meta'],
+    seriesName: string
+  ): Series[] {
+    let scale = 1;
+    if (seriesName) {
+      const unit = meta?.units?.[getAggregateAlias(seriesName)];
+      // Scale series values to milliseconds or bytes depending on units from meta
+      scale = (unit && (DURATION_UNITS[unit] ?? SIZE_UNITS[unit])) ?? 1;
+    }
+
+    return [
+      {
+        seriesName,
+        data: data.map(([timestamp, countsForTimestamp]) => ({
+          name: timestamp * 1000,
+          value: countsForTimestamp.reduce((acc, {count}) => acc + count, 0) * scale,
+        })),
+      },
+    ];
+  }
+
   return (
     <Fragment>
       {header}
-      <EventsRequest
-        {...requestCommonProps}
-        organization={organization}
-        showLoading={false}
-        includePrevious={false}
-        yAxis={trendDisplay}
-        currentSeriesNames={[trendDisplay]}
-        partial
-        withoutZerofill={withoutZerofill}
-        referrer="api.performance.transaction-summary.trends-chart"
-      >
-        {({errored, loading, reloading, timeseriesData, timeframe: timeFrame}) => (
-          <Content
-            series={timeseriesData}
-            errored={errored}
-            loading={loading}
-            reloading={reloading}
-            timeFrame={timeFrame}
-            {...contentCommonProps}
-          />
-        )}
-      </EventsRequest>
+      {withBreakpoint ? (
+        // queries events-trends-statsv2 for breakpoint data (feature flag only)
+        <TrendsDiscoverQuery
+          eventView={trendView}
+          orgSlug={organization.slug}
+          location={location}
+          limit={1}
+          withBreakpoint={withBreakpoint}
+        >
+          {({isLoading, trendsData}) => {
+            const events = normalizeTrends(
+              (trendsData && trendsData.events && trendsData.events.data) || []
+            );
+
+            // keep trend change type as regression until the backend can support passing the type
+            const selectedTransaction = getSelectedTransaction(
+              location,
+              TrendChangeType.REGRESSION,
+              events
+            );
+
+            const statsData = trendsData?.stats || {};
+
+            const transactionEvent = (
+              statsData &&
+              selectedTransaction?.project &&
+              selectedTransaction?.transaction
+                ? statsData[
+                    [selectedTransaction?.project, selectedTransaction?.transaction].join(
+                      ','
+                    )
+                  ]
+                : undefined
+            ) as EventsStats;
+            const data = transactionEvent?.data ?? [];
+            const meta = transactionEvent?.meta ?? ({} as EventsStats['meta']);
+            const timeSeriesMetricsData = transformTimeseriesData(
+              data,
+              meta,
+              trendDisplay
+            );
+
+            const metricsTimeFrame =
+              transactionEvent && transactionEvent.start && transactionEvent.end
+                ? {start: transactionEvent.start * 1000, end: transactionEvent.end * 1000}
+                : undefined;
+
+            return data.length !== 0 ? (
+              <Content
+                series={timeSeriesMetricsData}
+                errored={!trendsData && !isLoading}
+                loading={isLoading}
+                reloading={isLoading}
+                timeFrame={metricsTimeFrame}
+                withBreakpoint
+                transaction={selectedTransaction}
+                {...contentCommonProps}
+              />
+            ) : (
+              // queries events-stats for trend data if metrics trend data not found
+              <EventsRequest
+                {...requestCommonProps}
+                organization={organization}
+                showLoading={false}
+                includePrevious={false}
+                yAxis={trendDisplay}
+                currentSeriesNames={[trendDisplay]}
+                partial
+                withoutZerofill={withoutZerofill}
+                referrer="api.performance.transaction-summary.trends-chart"
+              >
+                {({errored, loading, reloading, timeseriesData, timeframe}) => {
+                  return (
+                    <Content
+                      series={timeseriesData}
+                      errored={errored}
+                      loading={loading || isLoading}
+                      reloading={reloading}
+                      timeFrame={timeframe}
+                      withBreakpoint
+                      transaction={selectedTransaction}
+                      {...contentCommonProps}
+                    />
+                  );
+                }}
+              </EventsRequest>
+            );
+          }}
+        </TrendsDiscoverQuery>
+      ) : (
+        <EventsRequest
+          {...requestCommonProps}
+          organization={organization}
+          showLoading={false}
+          includePrevious={false}
+          yAxis={trendDisplay}
+          currentSeriesNames={[trendDisplay]}
+          partial
+          withoutZerofill={withoutZerofill}
+          referrer="api.performance.transaction-summary.trends-chart"
+        >
+          {({errored, loading, reloading, timeseriesData, timeframe: timeFrame}) => {
+            return (
+              <Content
+                series={timeseriesData}
+                errored={errored}
+                loading={loading}
+                reloading={reloading}
+                timeFrame={timeFrame}
+                {...contentCommonProps}
+              />
+            );
+          }}
+        </EventsRequest>
+      )}
     </Fragment>
   );
 }

+ 1 - 24
static/app/views/performance/trends/changedTransactions.tsx

@@ -37,6 +37,7 @@ import {
   DisplayModes,
   transactionSummaryRouteWithQuery,
 } from 'sentry/views/performance/transactionSummary/utils';
+import {getSelectedTransaction} from 'sentry/views/performance/utils';
 
 import Chart from './chart';
 import {
@@ -107,30 +108,6 @@ function getChartTitle(trendChangeType: TrendChangeType): string {
   }
 }
 
-function getSelectedTransaction(
-  location: Location,
-  trendChangeType: TrendChangeType,
-  transactions?: NormalizedTrendsTransaction[]
-): NormalizedTrendsTransaction | undefined {
-  const queryKey = getSelectedQueryKey(trendChangeType);
-  const selectedTransactionName = decodeScalar(location.query[queryKey]);
-
-  if (!transactions) {
-    return undefined;
-  }
-
-  const selectedTransaction = transactions.find(
-    transaction =>
-      `${transaction.transaction}-${transaction.project}` === selectedTransactionName
-  );
-
-  if (selectedTransaction) {
-    return selectedTransaction;
-  }
-
-  return transactions.length > 0 ? transactions[0] : undefined;
-}
-
 function handleChangeSelected(
   location: Location,
   organization: Organization,

+ 11 - 161
static/app/views/performance/trends/chart.tsx

@@ -1,17 +1,12 @@
 import {browserHistory} from 'react-router';
-import {Theme, useTheme} from '@emotion/react';
+import {useTheme} from '@emotion/react';
 import type {LegendComponentOption} from 'echarts';
 
 import ChartZoom from 'sentry/components/charts/chartZoom';
-import {
-  LineChart,
-  LineChartProps,
-  LineChartSeries,
-} from 'sentry/components/charts/lineChart';
+import {LineChart, LineChartProps} from 'sentry/components/charts/lineChart';
 import TransitionChart from 'sentry/components/charts/transitionChart';
 import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
 import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
-import {t} from 'sentry/locale';
 import {EventsStatsData, OrganizationSummary, Project} from 'sentry/types';
 import {Series} from 'sentry/types/echarts';
 import {getUtcToLocalDateObject} from 'sentry/utils/dates';
@@ -25,6 +20,7 @@ import getDynamicText from 'sentry/utils/getDynamicText';
 import {decodeList} from 'sentry/utils/queryString';
 import {useLocation} from 'sentry/utils/useLocation';
 import useRouter from 'sentry/utils/useRouter';
+import {getIntervalLine} from 'sentry/views/performance/utils';
 
 import {ViewProps} from '../types';
 
@@ -57,18 +53,6 @@ type Props = ViewProps & {
   trendFunctionField?: TrendFunctionField;
 };
 
-function transformTransaction(
-  transaction: NormalizedTrendsTransaction
-): NormalizedTrendsTransaction {
-  if (transaction && transaction.breakpoint) {
-    return {
-      ...transaction,
-      breakpoint: transaction.breakpoint * 1000,
-    };
-  }
-  return transaction;
-}
-
 function transformEventStats(data: EventsStatsData, seriesName?: string): Series[] {
   return [
     {
@@ -102,147 +86,6 @@ function getLegend(trendFunction: string): LegendComponentOption {
   };
 }
 
-function getIntervalLine(
-  theme: Theme,
-  series: Series[],
-  intervalRatio: number,
-  transaction?: NormalizedTrendsTransaction
-): LineChartSeries[] {
-  if (!transaction || !series.length || !series[0].data || !series[0].data.length) {
-    return [];
-  }
-
-  const transformedTransaction = transformTransaction(transaction);
-
-  const seriesStart = parseInt(series[0].data[0].name as string, 10);
-  const seriesEnd = parseInt(series[0].data.slice(-1)[0].name as string, 10);
-
-  if (seriesEnd < seriesStart) {
-    return [];
-  }
-
-  const periodLine: LineChartSeries = {
-    data: [],
-    color: theme.textColor,
-    markLine: {
-      data: [],
-      label: {},
-      lineStyle: {
-        color: theme.textColor,
-        type: 'dashed',
-        width: 1,
-      },
-      symbol: ['none', 'none'],
-      tooltip: {
-        show: false,
-      },
-    },
-    seriesName: 'Baseline',
-  };
-
-  const periodLineLabel = {
-    fontSize: 11,
-    show: true,
-    color: theme.textColor,
-    silent: true,
-  };
-
-  const previousPeriod = {
-    ...periodLine,
-    markLine: {...periodLine.markLine},
-    seriesName: 'Baseline',
-  };
-  const currentPeriod = {
-    ...periodLine,
-    markLine: {...periodLine.markLine},
-    seriesName: 'Baseline',
-  };
-  const periodDividingLine = {
-    ...periodLine,
-    markLine: {...periodLine.markLine},
-    seriesName: 'Period split',
-  };
-
-  const seriesDiff = seriesEnd - seriesStart;
-  const seriesLine = seriesDiff * intervalRatio + seriesStart;
-  const {breakpoint} = transformedTransaction;
-
-  const divider = breakpoint || seriesLine;
-
-  previousPeriod.markLine.data = [
-    [
-      {value: 'Past', coord: [seriesStart, transformedTransaction.aggregate_range_1]},
-      {coord: [divider, transformedTransaction.aggregate_range_1]},
-    ],
-  ];
-  previousPeriod.markLine.tooltip = {
-    formatter: () => {
-      return [
-        '<div class="tooltip-series tooltip-series-solo">',
-        '<div>',
-        `<span class="tooltip-label"><strong>${t('Past Baseline')}</strong></span>`,
-        // p50() coerces the axis to be time based
-        tooltipFormatter(transformedTransaction.aggregate_range_1, 'duration'),
-        '</div>',
-        '</div>',
-        '<div class="tooltip-arrow"></div>',
-      ].join('');
-    },
-  };
-  currentPeriod.markLine.data = [
-    [
-      {value: 'Present', coord: [divider, transformedTransaction.aggregate_range_2]},
-      {coord: [seriesEnd, transformedTransaction.aggregate_range_2]},
-    ],
-  ];
-  currentPeriod.markLine.tooltip = {
-    formatter: () => {
-      return [
-        '<div class="tooltip-series tooltip-series-solo">',
-        '<div>',
-        `<span class="tooltip-label"><strong>${t('Present Baseline')}</strong></span>`,
-        // p50() coerces the axis to be time based
-        tooltipFormatter(transformedTransaction.aggregate_range_2, 'duration'),
-        '</div>',
-        '</div>',
-        '<div class="tooltip-arrow"></div>',
-      ].join('');
-    },
-  };
-  periodDividingLine.markLine = {
-    data: [
-      {
-        xAxis: divider,
-      },
-    ],
-    label: {show: false},
-    lineStyle: {
-      color: theme.textColor,
-      type: 'solid',
-      width: 2,
-    },
-    symbol: ['none', 'none'],
-    tooltip: {
-      show: false,
-    },
-    silent: true,
-  };
-
-  previousPeriod.markLine.label = {
-    ...periodLineLabel,
-    formatter: 'Past',
-    position: 'insideStartBottom',
-  };
-  currentPeriod.markLine.label = {
-    ...periodLineLabel,
-    formatter: 'Present',
-    position: 'insideEndBottom',
-  };
-
-  const additionalLineSeries = [previousPeriod, currentPeriod, periodDividingLine];
-  return additionalLineSeries;
-}
-
 export function Chart({
   trendChangeType,
   statsPeriod,
@@ -344,7 +187,14 @@ export function Chart({
       })
     : [];
 
-  const intervalSeries = getIntervalLine(theme, smoothedResults || [], 0.5, transaction);
+  const needsLabel = true;
+  const intervalSeries = getIntervalLine(
+    theme,
+    smoothedResults || [],
+    0.5,
+    needsLabel,
+    transaction
+  );
 
   const yDiff = yMax - yMin;
   const yMargin = yDiff * 0.1;

+ 2 - 2
static/app/views/performance/trends/utils.tsx

@@ -4,7 +4,7 @@ import moment from 'moment';
 
 import {getInterval} from 'sentry/components/charts/utils';
 import {t} from 'sentry/locale';
-import {Organization, Project} from 'sentry/types';
+import {OrganizationSummary, Project} from 'sentry/types';
 import {Series, SeriesDataUnit} from 'sentry/types/echarts';
 import EventView from 'sentry/utils/discover/eventView';
 import {
@@ -235,7 +235,7 @@ export function modifyTrendView(
   location: Location,
   trendsType: TrendChangeType,
   projects: Project[],
-  organization: Organization,
+  organization: OrganizationSummary,
   isProjectOnly?: boolean
 ) {
   const trendFunction = getCurrentTrendFunction(location);

+ 187 - 1
static/app/views/performance/utils.tsx

@@ -1,6 +1,8 @@
 import {browserHistory} from 'react-router';
+import {Theme} from '@emotion/react';
 import {Location} from 'history';
 
+import {LineChartSeries} from 'sentry/components/charts/lineChart';
 import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
 import {backend, frontend, mobile} from 'sentry/data/platformCategories';
 import {t} from 'sentry/locale';
@@ -12,8 +14,10 @@ import {
   Project,
   ReleaseProject,
 } from 'sentry/types';
+import {Series} from 'sentry/types/echarts';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {statsPeriodToDays} from 'sentry/utils/dates';
+import {tooltipFormatter} from 'sentry/utils/discover/charts';
 import EventView, {EventData} from 'sentry/utils/discover/eventView';
 import {TRACING_FIELDS} from 'sentry/utils/discover/fields';
 import {getDuration} from 'sentry/utils/formatters';
@@ -22,8 +26,12 @@ import {decodeScalar} from 'sentry/utils/queryString';
 import toArray from 'sentry/utils/toArray';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
+import {
+  NormalizedTrendsTransaction,
+  TrendChangeType,
+} from 'sentry/views/performance/trends/types';
 
-import {DEFAULT_MAX_DURATION} from './trends/utils';
+import {DEFAULT_MAX_DURATION, getSelectedQueryKey} from './trends/utils';
 
 export const QUERY_KEYS = [
   'environment',
@@ -360,3 +368,181 @@ export function getProjectID(
 
   return projects.find(currentProject => currentProject.slug === projectSlug)?.id;
 }
+
+export function transformTransaction(
+  transaction: NormalizedTrendsTransaction
+): NormalizedTrendsTransaction {
+  if (transaction && transaction.breakpoint) {
+    return {
+      ...transaction,
+      breakpoint: transaction.breakpoint * 1000,
+    };
+  }
+  return transaction;
+}
+
+export function getIntervalLine(
+  theme: Theme,
+  series: Series[],
+  intervalRatio: number,
+  label: boolean,
+  transaction?: NormalizedTrendsTransaction
+): LineChartSeries[] {
+  if (!transaction || !series.length || !series[0].data || !series[0].data.length) {
+    return [];
+  }
+
+  const transformedTransaction = transformTransaction(transaction);
+
+  const seriesStart = parseInt(series[0].data[0].name as string, 10);
+  const seriesEnd = parseInt(series[0].data.slice(-1)[0].name as string, 10);
+
+  if (seriesEnd < seriesStart) {
+    return [];
+  }
+
+  const periodLine: LineChartSeries = {
+    data: [],
+    color: theme.textColor,
+    markLine: {
+      data: [],
+      label: {},
+      lineStyle: {
+        color: theme.textColor,
+        type: 'dashed',
+        width: label ? 1 : 2,
+      },
+      symbol: ['none', 'none'],
+      tooltip: {
+        show: false,
+      },
+    },
+    seriesName: 'Baseline',
+  };
+
+  const periodLineLabel = {
+    fontSize: 11,
+    show: label,
+    color: theme.textColor,
+    silent: label,
+  };
+
+  const previousPeriod = {
+    ...periodLine,
+    markLine: {...periodLine.markLine},
+    seriesName: 'Baseline',
+  };
+  const currentPeriod = {
+    ...periodLine,
+    markLine: {...periodLine.markLine},
+    seriesName: 'Baseline',
+  };
+  const periodDividingLine = {
+    ...periodLine,
+    markLine: {...periodLine.markLine},
+    seriesName: 'Baseline',
+  };
+
+  const seriesDiff = seriesEnd - seriesStart;
+  const seriesLine = seriesDiff * intervalRatio + seriesStart;
+  const {breakpoint} = transformedTransaction;
+
+  const divider = breakpoint || seriesLine;
+
+  previousPeriod.markLine.data = [
+    [
+      {value: 'Past', coord: [seriesStart, transformedTransaction.aggregate_range_1]},
+      {coord: [divider, transformedTransaction.aggregate_range_1]},
+    ],
+  ];
+  previousPeriod.markLine.tooltip = {
+    formatter: () => {
+      return [
+        '<div class="tooltip-series tooltip-series-solo">',
+        '<div>',
+        `<span class="tooltip-label"><strong>${t('Past Baseline')}</strong></span>`,
+        // p50() coerces the axis to be time based
+        tooltipFormatter(transformedTransaction.aggregate_range_1, 'duration'),
+        '</div>',
+        '</div>',
+        '<div class="tooltip-arrow"></div>',
+      ].join('');
+    },
+  };
+  currentPeriod.markLine.data = [
+    [
+      {value: 'Present', coord: [divider, transformedTransaction.aggregate_range_2]},
+      {coord: [seriesEnd, transformedTransaction.aggregate_range_2]},
+    ],
+  ];
+  currentPeriod.markLine.tooltip = {
+    formatter: () => {
+      return [
+        '<div class="tooltip-series tooltip-series-solo">',
+        '<div>',
+        `<span class="tooltip-label"><strong>${t('Present Baseline')}</strong></span>`,
+        // p50() coerces the axis to be time based
+        tooltipFormatter(transformedTransaction.aggregate_range_2, 'duration'),
+        '</div>',
+        '</div>',
+        '<div class="tooltip-arrow"></div>',
+      ].join('');
+    },
+  };
+  periodDividingLine.markLine = {
+    data: [
+      {
+        xAxis: divider,
+      },
+    ],
+    label: {show: false},
+    lineStyle: {
+      color: theme.textColor,
+      type: 'solid',
+      width: 2,
+    },
+    symbol: ['none', 'none'],
+    tooltip: {
+      show: false,
+    },
+    silent: true,
+  };
+
+  previousPeriod.markLine.label = {
+    ...periodLineLabel,
+    formatter: 'Past',
+    position: 'insideStartBottom',
+  };
+  currentPeriod.markLine.label = {
+    ...periodLineLabel,
+    formatter: 'Present',
+    position: 'insideEndBottom',
+  };
+
+  const additionalLineSeries = [previousPeriod, currentPeriod, periodDividingLine];
+  return additionalLineSeries;
+}
+
+export function getSelectedTransaction(
+  location: Location,
+  trendChangeType: TrendChangeType,
+  transactions?: NormalizedTrendsTransaction[]
+): NormalizedTrendsTransaction | undefined {
+  const queryKey = getSelectedQueryKey(trendChangeType);
+  const selectedTransactionName = decodeScalar(location.query[queryKey]);
+
+  if (!transactions) {
+    return undefined;
+  }
+
+  const selectedTransaction = transactions.find(
+    transaction =>
+      `${transaction.transaction}-${transaction.project}` === selectedTransactionName
+  );
+
+  if (selectedTransaction) {
+    return selectedTransaction;
+  }
+
+  return transactions.length > 0 ? transactions[0] : undefined;
+}