Browse Source

feat(workflow): Handle data to performance section on release details (#29426)

* feat(workflow): Handle data to performance section on release details

Handle data on the Performance Section on the Release Details page for Frontend, Backend, Mobile, and Unknown.

FIXES WOR-1391
FIXES WOR-967

* fix empty apdex cell, update trend calculation for strings/numbers

* refactor checking platform type, fix bug when no results are returned for all releases/this release query

* refactor user_misery and apdex to use project config

* refactor discover link, add translation
Kelly Carino 3 years ago
parent
commit
dfc77cb75f

+ 639 - 217
static/app/components/discover/performanceCardTable.tsx

@@ -1,100 +1,209 @@
 import {Fragment} from 'react';
 import * as React from 'react';
 import {Link} from 'react-router';
+import {css} from '@emotion/react';
 import styled from '@emotion/styled';
+import {Location} from 'history';
 
 import Alert from 'app/components/alert';
+import AsyncComponent from 'app/components/asyncComponent';
 import LoadingIndicator from 'app/components/loadingIndicator';
+import NotAvailable from 'app/components/notAvailable';
 import {PanelItem} from 'app/components/panels';
 import PanelTable from 'app/components/panels/panelTable';
-import UserMisery from 'app/components/userMisery';
-import {backend, frontend, mobile, serverless} from 'app/data/platformCategories';
-import {IconWarning} from 'app/icons';
-import {t} from 'app/locale';
+import {IconArrow, IconWarning} from 'app/icons';
+import {t, tct} from 'app/locale';
 import overflowEllipsis from 'app/styles/overflowEllipsis';
 import space from 'app/styles/space';
 import {Organization, ReleaseProject} from 'app/types';
+import DiscoverQuery, {TableData} from 'app/utils/discover/discoverQuery';
+import EventView from 'app/utils/discover/eventView';
+import {getFieldRenderer} from 'app/utils/discover/fieldRenderers';
 import {MobileVital, WebVital} from 'app/utils/discover/fields';
 import {
   MOBILE_VITAL_DETAILS,
   WEB_VITAL_DETAILS,
 } from 'app/utils/performance/vitals/constants';
+import {PROJECT_PERFORMANCE_TYPE} from 'app/views/performance/utils';
 
-const FRONTEND_PLATFORMS: string[] = [...frontend];
-const BACKEND_PLATFORMS: string[] = [...backend, ...serverless];
-const MOBILE_PLATFORMS: string[] = [...mobile];
-
-type Props = {
+type PerformanceCardTableProps = {
   organization: Organization;
+  location: Location;
   project: ReleaseProject;
+  allReleasesEventView: EventView;
+  releaseEventView: EventView;
+  allReleasesTableData: TableData | null;
+  thisReleaseTableData: TableData | null;
+  performanceType: string;
   isLoading: boolean;
-  isEmpty: boolean;
 };
 
-class PerformanceCardTable extends React.PureComponent<Props> {
-  userMiseryField() {
+function PerformanceCardTable({
+  organization,
+  location,
+  project,
+  releaseEventView,
+  allReleasesTableData,
+  thisReleaseTableData,
+  performanceType,
+  isLoading,
+}: PerformanceCardTableProps) {
+  const miseryRenderer =
+    allReleasesTableData?.meta &&
+    getFieldRenderer('user_misery', allReleasesTableData.meta);
+
+  function renderChange(
+    allReleasesScore: number,
+    thisReleaseScore: number,
+    meta: string
+  ) {
+    if (allReleasesScore === undefined || thisReleaseScore === undefined) {
+      return <StyledNotAvailable />;
+    }
+
+    const trend = allReleasesScore - thisReleaseScore;
+    const trendSeconds = trend >= 1000 ? trend / 1000 : trend;
+    const trendPercentage = (allReleasesScore - thisReleaseScore) * 100;
+    const valPercentage = Math.round(Math.abs(trendPercentage));
+    const val = Math.abs(trendSeconds).toFixed(2);
+
+    if (trend === 0) {
+      return <SubText>{`0${meta === 'duration' ? 'ms' : '%'}`}</SubText>;
+    }
+
     return (
-      <UserMiseryPanelItem>
-        <StyledUserMisery
-          bars={10}
-          barHeight={20}
-          miseryLimit={1000}
-          totalUsers={500}
-          userMisery={300}
-          miserableUsers={200}
+      <TrendText color={trend >= 0 ? 'success' : 'error'}>
+        {`${meta === 'duration' ? val : valPercentage}${
+          meta === 'duration' ? (trend >= 1000 ? 's' : 'ms') : '%'
+        }`}
+        <StyledIconArrow
+          color={trend >= 0 ? 'success' : 'error'}
+          direction={trend >= 0 ? 'down' : 'up'}
+          size="xs"
         />
-      </UserMiseryPanelItem>
+      </TrendText>
     );
   }
 
-  sectionField(field: JSX.Element[]) {
+  function userMiseryTrend() {
+    const allReleasesUserMisery = allReleasesTableData?.data?.[0]?.user_misery;
+    const thisReleaseUserMisery = thisReleaseTableData?.data?.[0]?.user_misery;
     return (
       <StyledPanelItem>
-        <TitleSpace />
-        {field}
+        {renderChange(
+          allReleasesUserMisery as number,
+          thisReleaseUserMisery as number,
+          'number' as string
+        )}
       </StyledPanelItem>
     );
   }
 
-  renderFrontendPerformance() {
-    const vitals = [WebVital.FCP, WebVital.FID, WebVital.LCP, WebVital.CLS];
-    const webVitalTitles = vitals.map(vital => {
+  function renderFrontendPerformance() {
+    const webVitals = [
+      {title: WebVital.FCP, field: 'p75_measurements_fcp'},
+      {title: WebVital.FID, field: 'p75_measurements_fid'},
+      {title: WebVital.LCP, field: 'p75_measurements_lcp'},
+      {title: WebVital.CLS, field: 'p75_measurements_cls'},
+    ];
+
+    const spans = [
+      {title: 'HTTP', column: 'p75(spans.http)', field: 'p75_spans_http'},
+      {title: 'Browser', column: 'p75(spans.browser)', field: 'p75_spans_browser'},
+      {title: 'Resource', column: 'p75(spans.resource)', field: 'p75_spans_resource'},
+    ];
+
+    const webVitalTitles = webVitals.map((vital, idx) => {
+      const newView = releaseEventView.withColumns([
+        {kind: 'field', field: `p75(${vital.title})`},
+      ]);
       return (
-        <SubTitle key={vital}>
-          {WEB_VITAL_DETAILS[vital].name} ({WEB_VITAL_DETAILS[vital].acronym})
+        <SubTitle key={idx}>
+          <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
+            {WEB_VITAL_DETAILS[vital.title].name} (
+            {WEB_VITAL_DETAILS[vital.title].acronym})
+          </Link>
         </SubTitle>
       );
     });
 
-    const spans = ['HTTP', 'DB', 'Browser', 'Resource'];
-    const spanTitles = spans.map(span => {
-      return <SubTitle key={span}>{t(span)}</SubTitle>;
-    });
-
-    // TODO(kelly): placeholder data. will need to add discover data for webvitals and span operations in follow-up pr
-    const fieldData = ['0ms', '0ms', '0ms', '0ms'];
-    const field = fieldData.map(data => {
+    const spanTitles = spans.map((span, idx) => {
+      const newView = releaseEventView.withColumns([
+        {kind: 'field', field: `${span.column}`},
+      ]);
       return (
-        <Field key={data} align="right">
-          {data}
-        </Field>
+        <SubTitle key={idx}>
+          <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
+            {t(span.title)}
+          </Link>
+        </SubTitle>
       );
     });
 
-    const columnData = () => {
-      return (
-        <div>
-          {this.userMiseryField()}
-          {this.sectionField(field)}
-          {this.sectionField(field)}
-        </div>
-      );
-    };
+    const webVitalsRenderer = webVitals.map(
+      vital =>
+        allReleasesTableData?.meta &&
+        getFieldRenderer(vital.field, allReleasesTableData?.meta)
+    );
+
+    const spansRenderer = spans.map(
+      span =>
+        allReleasesTableData?.meta &&
+        getFieldRenderer(span.field, allReleasesTableData?.meta)
+    );
+
+    const webReleaseTrend = webVitals.map(vital => {
+      return {
+        allReleasesRow: {
+          data: allReleasesTableData?.data?.[0]?.[vital.field],
+          meta: allReleasesTableData?.meta?.[vital.field],
+        },
+        thisReleaseRow: {
+          data: thisReleaseTableData?.data?.[0]?.[vital.field],
+          meta: thisReleaseTableData?.meta?.[vital.field],
+        },
+      };
+    });
+    const spansReleaseTrend = spans.map(span => {
+      return {
+        allReleasesRow: {
+          data: allReleasesTableData?.data?.[0]?.[span.field],
+          meta: allReleasesTableData?.meta?.[span.field],
+        },
+        thisReleaseRow: {
+          data: thisReleaseTableData?.data?.[0]?.[span.field],
+          meta: thisReleaseTableData?.meta?.[span.field],
+        },
+      };
+    });
+
+    const emptyColumn = (
+      <div>
+        <SingleEmptySubText>
+          <StyledNotAvailable tooltip={t('No results found')} />
+        </SingleEmptySubText>
+        <StyledPanelItem>
+          <TitleSpace />
+          {webVitals.map((vital, index) => (
+            <MultipleEmptySubText key={vital[index]}>
+              {<StyledNotAvailable tooltip={t('No results found')} />}
+            </MultipleEmptySubText>
+          ))}
+        </StyledPanelItem>
+        <StyledPanelItem>
+          <TitleSpace />
+          {spans.map((span, index) => (
+            <MultipleEmptySubText key={span[index]}>
+              {<StyledNotAvailable tooltip={t('No results found')} />}
+            </MultipleEmptySubText>
+          ))}
+        </StyledPanelItem>
+      </div>
+    );
 
     return (
       <Fragment>
         <div>
-          {/* Table description column */}
           <PanelItem>{t('User Misery')}</PanelItem>
           <StyledPanelItem>
             <div>{t('Web Vitals')}</div>
@@ -105,62 +214,167 @@ class PerformanceCardTable extends React.PureComponent<Props> {
             {spanTitles}
           </StyledPanelItem>
         </div>
+        {allReleasesTableData?.data.length === 0
+          ? emptyColumn
+          : allReleasesTableData?.data.map((dataRow, idx) => {
+              const allReleasesMisery = miseryRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+              const allReleasesWebVitals = webVitalsRenderer?.map(renderer =>
+                renderer?.(dataRow, {organization, location})
+              );
+              const allReleasesSpans = spansRenderer?.map(renderer =>
+                renderer?.(dataRow, {organization, location})
+              );
+
+              return (
+                <div key={idx}>
+                  <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
+                  <StyledPanelItem>
+                    <TitleSpace />
+                    {allReleasesWebVitals.map(webVital => webVital)}
+                  </StyledPanelItem>
+                  <StyledPanelItem>
+                    <TitleSpace />
+                    {allReleasesSpans.map(span => span)}
+                  </StyledPanelItem>
+                </div>
+              );
+            })}
+        {thisReleaseTableData?.data.length === 0
+          ? emptyColumn
+          : thisReleaseTableData?.data.map((dataRow, idx) => {
+              const thisReleasesMisery = miseryRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+              const thisReleasesWebVitals = webVitalsRenderer?.map(renderer =>
+                renderer?.(dataRow, {organization, location})
+              );
+              const thisReleasesSpans = spansRenderer?.map(renderer =>
+                renderer?.(dataRow, {organization, location})
+              );
+
+              return (
+                <div key={idx}>
+                  <div>
+                    <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
+                    <StyledPanelItem>
+                      <TitleSpace />
+                      {thisReleasesWebVitals.map(webVital => webVital)}
+                    </StyledPanelItem>
+                    <StyledPanelItem>
+                      <TitleSpace />
+                      {thisReleasesSpans.map(span => span)}
+                    </StyledPanelItem>
+                  </div>
+                </div>
+              );
+            })}
         <div>
-          {/* Table All Releases column */}
-          {/* TODO(kelly): placeholder data. will need to add user misery data in follow-up pr */}
-          {columnData()}
-        </div>
-        <div>
-          {/* Table This Release column */}
-          {columnData()}
-        </div>
-        <div>
-          {/* Table Change column */}
-          {columnData()}
+          {userMiseryTrend()}
+          <StyledPanelItem>
+            <TitleSpace />
+            {webReleaseTrend?.map(row =>
+              renderChange(
+                row.allReleasesRow?.data as number,
+                row.thisReleaseRow?.data as number,
+                row.allReleasesRow?.meta as string
+              )
+            )}
+          </StyledPanelItem>
+          <StyledPanelItem>
+            <TitleSpace />
+            {spansReleaseTrend?.map(row =>
+              renderChange(
+                row.allReleasesRow?.data as number,
+                row.thisReleaseRow?.data as number,
+                row.allReleasesRow?.meta as string
+              )
+            )}
+          </StyledPanelItem>
         </div>
       </Fragment>
     );
   }
 
-  renderBackendPerformance() {
-    const spans = ['HTTP', 'DB'];
-    const spanTitles = spans.map(span => {
-      return <SubTitle key={span}>{t(span)}</SubTitle>;
-    });
+  function renderBackendPerformance() {
+    const spans = [
+      {title: 'HTTP', column: 'p75(spans.http)', field: 'p75_spans_http'},
+      {title: 'DB', column: 'p75(spans.db)', field: 'p75_spans_db'},
+    ];
 
-    // TODO(kelly): placeholder data. will need to add discover data for webvitals and span operations in follow-up pr
-    const apdexData = ['0ms'];
-    const apdexField = apdexData.map(data => {
+    const spanTitles = spans.map((span, idx) => {
+      const newView = releaseEventView.withColumns([
+        {kind: 'field', field: `${span.column}`},
+      ]);
       return (
-        <Field key={data} align="right">
-          {data}
-        </Field>
+        <SubTitle key={idx}>
+          <Link to={newView.getResultsViewUrlTarget(organization.slug)}>
+            {t(span.title)}
+          </Link>
+        </SubTitle>
       );
     });
 
-    const fieldData = ['0ms', '0ms'];
-    const field = fieldData.map(data => {
-      return (
-        <Field key={data} align="right">
-          {data}
-        </Field>
-      );
+    const apdexRenderer =
+      allReleasesTableData?.meta && getFieldRenderer('apdex', allReleasesTableData.meta);
+
+    const spansRenderer = spans.map(
+      span =>
+        allReleasesTableData?.meta &&
+        getFieldRenderer(span.field, allReleasesTableData?.meta)
+    );
+
+    const spansReleaseTrend = spans.map(span => {
+      return {
+        allReleasesRow: {
+          data: allReleasesTableData?.data?.[0]?.[span.field],
+          meta: allReleasesTableData?.meta?.[span.field],
+        },
+        thisReleaseRow: {
+          data: thisReleaseTableData?.data?.[0]?.[span.field],
+          meta: thisReleaseTableData?.meta?.[span.field],
+        },
+      };
     });
 
-    const columnData = () => {
+    function apdexTrend() {
+      const allReleasesApdex = allReleasesTableData?.data?.[0]?.apdex;
+      const thisReleaseApdex = thisReleaseTableData?.data?.[0]?.apdex;
       return (
-        <div>
-          {this.userMiseryField()}
-          <StyledPanelItem>{apdexField}</StyledPanelItem>
-          {this.sectionField(field)}
-        </div>
+        <StyledPanelItem>
+          {renderChange(
+            allReleasesApdex as number,
+            thisReleaseApdex as number,
+            'string' as string
+          )}
+        </StyledPanelItem>
       );
-    };
-
+    }
+
+    const emptyColumn = (
+      <div>
+        <SingleEmptySubText>
+          <StyledNotAvailable tooltip={t('No results found')} />
+        </SingleEmptySubText>
+        <SingleEmptySubText>
+          <StyledNotAvailable tooltip={t('No results found')} />
+        </SingleEmptySubText>
+        <StyledPanelItem>
+          <TitleSpace />
+          {spans.map((span, index) => (
+            <MultipleEmptySubText key={span[index]}>
+              {<StyledNotAvailable tooltip={t('No results found')} />}
+            </MultipleEmptySubText>
+          ))}
+        </StyledPanelItem>
+      </div>
+    );
     return (
       <Fragment>
         <div>
-          {/* Table description column */}
           <PanelItem>{t('User Misery')}</PanelItem>
           <StyledPanelItem>
             <div>{t('Apdex')}</div>
@@ -170,29 +384,79 @@ class PerformanceCardTable extends React.PureComponent<Props> {
             {spanTitles}
           </StyledPanelItem>
         </div>
+        {allReleasesTableData?.data.length === 0
+          ? emptyColumn
+          : allReleasesTableData?.data.map((dataRow, idx) => {
+              const allReleasesMisery = miseryRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+              const allReleasesApdex = apdexRenderer?.(dataRow, {organization, location});
+              const allReleasesSpans = spansRenderer?.map(renderer =>
+                renderer?.(dataRow, {organization, location})
+              );
+
+              return (
+                <div key={idx}>
+                  <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
+                  <ApdexPanelItem>{allReleasesApdex}</ApdexPanelItem>
+                  <StyledPanelItem>
+                    <TitleSpace />
+                    {allReleasesSpans.map(span => span)}
+                  </StyledPanelItem>
+                </div>
+              );
+            })}
+        {thisReleaseTableData?.data.length === 0
+          ? emptyColumn
+          : thisReleaseTableData?.data.map((dataRow, idx) => {
+              const thisReleasesMisery = miseryRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+              const thisReleasesApdex = apdexRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+              const thisReleasesSpans = spansRenderer?.map(renderer =>
+                renderer?.(dataRow, {organization, location})
+              );
+
+              return (
+                <div key={idx}>
+                  <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
+                  <ApdexPanelItem>{thisReleasesApdex}</ApdexPanelItem>
+                  <StyledPanelItem>
+                    <TitleSpace />
+                    {thisReleasesSpans.map(span => span)}
+                  </StyledPanelItem>
+                </div>
+              );
+            })}
         <div>
-          {/* Table All Releases column */}
-          {/* TODO(kelly): placeholder data. will need to add user misery data in follow-up pr */}
-          {columnData()}
-        </div>
-        <div>
-          {/* Table This Release column */}
-          {columnData()}
-        </div>
-        <div>
-          {/* Table Change column */}
-          {columnData()}
+          {userMiseryTrend()}
+          {apdexTrend()}
+          <StyledPanelItem>
+            <TitleSpace />
+            {spansReleaseTrend?.map(row =>
+              renderChange(
+                row.allReleasesRow?.data as number,
+                row.thisReleaseRow?.data as number,
+                row.allReleasesRow?.meta as string
+              )
+            )}
+          </StyledPanelItem>
         </div>
       </Fragment>
     );
   }
 
-  renderMobilePerformance() {
+  function renderMobilePerformance() {
     const mobileVitals = [
       MobileVital.AppStartCold,
       MobileVital.AppStartWarm,
       MobileVital.FramesSlow,
-      MobileVital.FramesFrozenRate,
+      MobileVital.FramesFrozen,
     ];
     const mobileVitalTitles = mobileVitals.map(mobileVital => {
       return (
@@ -200,140 +464,279 @@ class PerformanceCardTable extends React.PureComponent<Props> {
       );
     });
 
-    // TODO(kelly): placeholder data. will need to add mobile data for mobilevitals in follow-up pr
-    const mobileData = ['0ms'];
-    const mobileField = mobileData.map(data => {
-      return (
-        <Field key={data} align="right">
-          {data}
-        </Field>
-      );
-    });
-    const field = mobileVitals.map(vital => {
-      return <StyledPanelItem key={vital}>{mobileField}</StyledPanelItem>;
+    const mobileVitalFields = [
+      'p75_measurements_app_start_cold',
+      'p75_measurements_app_start_warm',
+      'p75_measurements_frames_slow',
+      'p75_measurements_frames_frozen',
+    ];
+    const mobileVitalsRenderer = mobileVitalFields.map(
+      field =>
+        allReleasesTableData?.meta && getFieldRenderer(field, allReleasesTableData?.meta)
+    );
+
+    const mobileReleaseTrend = mobileVitalFields.map(field => {
+      return {
+        allReleasesRow: {
+          data: allReleasesTableData?.data?.[0]?.[field],
+          meta: allReleasesTableData?.meta?.[field],
+        },
+        thisReleaseRow: {
+          data: thisReleaseTableData?.data?.[0]?.[field],
+          meta: thisReleaseTableData?.meta?.[field],
+        },
+      };
     });
 
-    const columnData = () => {
-      return (
-        <div>
-          {this.userMiseryField()}
-          {field}
-        </div>
-      );
-    };
+    const emptyColumn = (
+      <div>
+        <SingleEmptySubText>
+          <StyledNotAvailable tooltip={t('No results found')} />
+        </SingleEmptySubText>
+        {mobileVitalFields.map((vital, index) => (
+          <SingleEmptySubText key={vital[index]}>
+            <StyledNotAvailable tooltip={t('No results found')} />
+          </SingleEmptySubText>
+        ))}
+      </div>
+    );
 
     return (
       <Fragment>
         <div>
-          {/* Table description column */}
           <PanelItem>{t('User Misery')}</PanelItem>
           {mobileVitalTitles}
         </div>
+        {allReleasesTableData?.data.length === 0
+          ? emptyColumn
+          : allReleasesTableData?.data.map((dataRow, idx) => {
+              const allReleasesMisery = miseryRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+              const allReleasesMobile = mobileVitalsRenderer?.map(renderer =>
+                renderer?.(dataRow, {organization, location})
+              );
+
+              return (
+                <div key={idx}>
+                  <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
+                  {allReleasesMobile.map((mobileVital, i) => (
+                    <StyledPanelItem key={i}>{mobileVital}</StyledPanelItem>
+                  ))}
+                </div>
+              );
+            })}
+        {thisReleaseTableData?.data.length === 0
+          ? emptyColumn
+          : thisReleaseTableData?.data.map((dataRow, idx) => {
+              const thisReleasesMisery = miseryRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+              const thisReleasesMobile = mobileVitalsRenderer?.map(renderer =>
+                renderer?.(dataRow, {organization, location})
+              );
+
+              return (
+                <div key={idx}>
+                  <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
+                  {thisReleasesMobile.map((mobileVital, i) => (
+                    <StyledPanelItem key={i}>{mobileVital}</StyledPanelItem>
+                  ))}
+                </div>
+              );
+            })}
         <div>
-          {/* Table All Releases column */}
-          {/* TODO(kelly): placeholder data. will need to add user misery data in follow-up pr */}
-          {columnData()}
-        </div>
-        <div>
-          {/* Table This Release column */}
-          {columnData()}
-        </div>
-        <div>
-          {/* Table Change column */}
-          {columnData()}
+          {userMiseryTrend()}
+          {mobileReleaseTrend?.map((row, idx) => (
+            <StyledPanelItem key={idx}>
+              {renderChange(
+                row.allReleasesRow?.data as number,
+                row.thisReleaseRow?.data as number,
+                row.allReleasesRow?.meta as string
+              )}
+            </StyledPanelItem>
+          ))}
         </div>
       </Fragment>
     );
   }
 
-  renderUnknownPerformance() {
+  function renderUnknownPerformance() {
+    const emptyColumn = (
+      <div>
+        <SingleEmptySubText>
+          <StyledNotAvailable tooltip={t('No results found')} />
+        </SingleEmptySubText>
+      </div>
+    );
+
     return (
       <Fragment>
         <div>
-          {/* Table description column */}
           <PanelItem>{t('User Misery')}</PanelItem>
         </div>
-        <div>
-          {/* TODO(kelly): placeholder data. will need to add user misery data in follow-up pr */}
-          {this.userMiseryField()}
-        </div>
-        <div>
-          {/* Table All Releases column */}
-          {this.userMiseryField()}
-        </div>
-        <div>
-          {/* Table This Release column */}
-          {this.userMiseryField()}
-        </div>
+        {allReleasesTableData?.data.length === 0
+          ? emptyColumn
+          : allReleasesTableData?.data.map((dataRow, idx) => {
+              const allReleasesMisery = miseryRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+
+              return (
+                <div key={idx}>
+                  <UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
+                </div>
+              );
+            })}
+        {thisReleaseTableData?.data.length === 0
+          ? emptyColumn
+          : thisReleaseTableData?.data.map((dataRow, idx) => {
+              const thisReleasesMisery = miseryRenderer?.(dataRow, {
+                organization,
+                location,
+              });
+
+              return (
+                <div key={idx}>
+                  <UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
+                </div>
+              );
+            })}
+        <div>{userMiseryTrend()}</div>
       </Fragment>
     );
   }
 
-  render() {
-    const {project, organization, isLoading, isEmpty} = this.props;
-    // Custom set the height so we don't have layout shift when results are loaded.
-    const loader = <LoadingIndicator style={{margin: '70px auto'}} />;
-
-    const title = FRONTEND_PLATFORMS.includes(project.platform as string)
-      ? t('Frontend Performance')
-      : BACKEND_PLATFORMS.includes(project.platform as string)
-      ? t('Backend Performance')
-      : MOBILE_PLATFORMS.includes(project.platform as string)
-      ? t('Mobile Performance')
-      : t('[Unknown] Performance');
-    const platformPerformance = FRONTEND_PLATFORMS.includes(project.platform as string)
-      ? this.renderFrontendPerformance()
-      : BACKEND_PLATFORMS.includes(project.platform as string)
-      ? this.renderBackendPerformance()
-      : MOBILE_PLATFORMS.includes(project.platform as string)
-      ? this.renderMobilePerformance()
-      : this.renderUnknownPerformance();
-    const isUnknownPlatform = ![
-      ...FRONTEND_PLATFORMS,
-      ...BACKEND_PLATFORMS,
-      ...MOBILE_PLATFORMS,
-    ].includes(project.platform);
+  const loader = <StyledLoadingIndicator />;
+
+  const platformPerformanceRender = {
+    [PROJECT_PERFORMANCE_TYPE.FRONTEND]: {
+      title: t('Frontend Performance'),
+      section: renderFrontendPerformance(),
+    },
+    [PROJECT_PERFORMANCE_TYPE.BACKEND]: {
+      title: t('Backend Performance'),
+      section: renderBackendPerformance(),
+    },
+    [PROJECT_PERFORMANCE_TYPE.MOBILE]: {
+      title: t('Mobile Performance'),
+      section: renderMobilePerformance(),
+    },
+    [PROJECT_PERFORMANCE_TYPE.ANY]: {
+      title: t('[Unknown] Performance'),
+      section: renderUnknownPerformance(),
+    },
+  };
+
+  const isUnknownPlatform = performanceType === PROJECT_PERFORMANCE_TYPE.ANY;
+
+  return (
+    <Fragment>
+      <HeadCellContainer>
+        {platformPerformanceRender[performanceType].title}
+      </HeadCellContainer>
+      {isUnknownPlatform && (
+        <StyledAlert type="warning" icon={<IconWarning size="md" />} system>
+          {tct(
+            'For more performance metrics, specify which platform this project is using in [link]',
+            {
+              link: (
+                <Link to={`/settings/${organization.slug}/projects/${project.slug}/`}>
+                  {t('project settings.')}
+                </Link>
+              ),
+            }
+          )}
+        </StyledAlert>
+      )}
+      <StyledPanelTable
+        isLoading={isLoading}
+        headers={[
+          <Cell key="description" align="left">
+            {t('Description')}
+          </Cell>,
+          <Cell key="releases" align="right">
+            {t('All Releases')}
+          </Cell>,
+          <Cell key="release" align="right">
+            {t('This Release')}
+          </Cell>,
+          <Cell key="change" align="right">
+            {t('Change')}
+          </Cell>,
+        ]}
+        disablePadding
+        loader={loader}
+        disableTopBorder={isUnknownPlatform}
+      >
+        {platformPerformanceRender[performanceType].section}
+      </StyledPanelTable>
+    </Fragment>
+  );
+}
 
-    return (
-      <Fragment>
-        <HeadCellContainer>{title}</HeadCellContainer>
-        {isUnknownPlatform ? (
-          <StyledAlert type="warning" icon={<IconWarning size="md" />} system>
-            For more performance metrics, specify which platform this project is using in{' '}
-            <Link to={`/settings/${organization.slug}/projects/${project.slug}/`}>
-              project settings.
-            </Link>
-          </StyledAlert>
-        ) : null}
-        <StyledPanelTable
-          isLoading={isLoading}
-          isEmpty={isEmpty}
-          emptyMessage={t('No transactions found')}
-          headers={[
-            <Cell key="description" align="left">
-              {t('Description')}
-            </Cell>,
-            <Cell key="releases" align="right">
-              {t('All Releases')}
-            </Cell>,
-            <Cell key="release" align="right">
-              {t('This Release')}
-            </Cell>,
-            <Cell key="change" align="right">
-              {t('Change')}
-            </Cell>,
-          ]}
-          disablePadding
-          loader={loader}
-          disableTopBorder={isUnknownPlatform}
+type Props = AsyncComponent['props'] & {
+  organization: Organization;
+  allReleasesEventView: EventView;
+  releaseEventView: EventView;
+  performanceType: string;
+  project: ReleaseProject;
+  location: Location;
+};
+
+function PerformanceCardTableWrapper({
+  organization,
+  project,
+  allReleasesEventView,
+  releaseEventView,
+  performanceType,
+  location,
+}: Props) {
+  return (
+    <DiscoverQuery
+      eventView={allReleasesEventView}
+      orgSlug={organization.slug}
+      location={location}
+    >
+      {({isLoading, tableData: allReleasesTableData}) => (
+        <DiscoverQuery
+          eventView={releaseEventView}
+          orgSlug={organization.slug}
+          location={location}
         >
-          {platformPerformance}
-        </StyledPanelTable>
-      </Fragment>
-    );
-  }
+          {({isLoading: isReleaseLoading, tableData: thisReleaseTableData}) => (
+            <PerformanceCardTable
+              isLoading={isLoading || isReleaseLoading}
+              organization={organization}
+              location={location}
+              project={project}
+              allReleasesEventView={allReleasesEventView}
+              releaseEventView={releaseEventView}
+              allReleasesTableData={allReleasesTableData}
+              thisReleaseTableData={thisReleaseTableData}
+              performanceType={performanceType}
+            />
+          )}
+        </DiscoverQuery>
+      )}
+    </DiscoverQuery>
+  );
 }
 
+export default PerformanceCardTableWrapper;
+
+const emptyFieldCss = p => css`
+  color: ${p.theme.chartOther};
+  text-align: right;
+`;
+
+const StyledLoadingIndicator = styled(LoadingIndicator)`
+  margin: 70px auto;
+`;
+
 const HeadCellContainer = styled('div')`
   font-size: ${p => p.theme.fontSizeExtraLarge};
   padding: ${space(2)};
@@ -367,19 +770,21 @@ const TitleSpace = styled('div')`
   height: 24px;
 `;
 
-const StyledUserMisery = styled(UserMisery)`
-  ${PanelItem} {
-    justify-content: flex-end;
-  }
-`;
-
 const UserMiseryPanelItem = styled(PanelItem)`
   justify-content: flex-end;
 `;
 
-const Field = styled('div')<{align: 'left' | 'right'}>`
-  text-align: ${p => p.align};
-  margin-left: ${p => p.align === 'left' && space(2)};
+const ApdexPanelItem = styled(PanelItem)`
+  text-align: right;
+`;
+
+const SingleEmptySubText = styled(PanelItem)`
+  display: block;
+  ${emptyFieldCss}
+`;
+
+const MultipleEmptySubText = styled('div')`
+  ${emptyFieldCss}
 `;
 
 const Cell = styled('div')<{align: 'left' | 'right'}>`
@@ -396,4 +801,21 @@ const StyledAlert = styled(Alert)`
   margin-bottom: 0;
 `;
 
-export default PerformanceCardTable;
+const StyledNotAvailable = styled(NotAvailable)`
+  text-align: right;
+`;
+
+const SubText = styled('div')`
+  color: ${p => p.theme.subText};
+  text-align: right;
+`;
+
+const TrendText = styled('div')<{color: string}>`
+  color: ${p => p.theme[p.color]};
+  text-align: right;
+`;
+
+const StyledIconArrow = styled(IconArrow)<{color: string}>`
+  color: ${p => p.theme[p.color]};
+  margin-left: ${space(0.5)};
+`;

+ 11 - 3
static/app/views/performance/utils.tsx

@@ -4,7 +4,13 @@ import {Location, LocationDescriptor, Query} from 'history';
 import Duration from 'app/components/duration';
 import {ALL_ACCESS_PROJECTS} from 'app/constants/globalSelectionHeader';
 import {backend, frontend, mobile} from 'app/data/platformCategories';
-import {GlobalSelection, Organization, OrganizationSummary, Project} from 'app/types';
+import {
+  GlobalSelection,
+  Organization,
+  OrganizationSummary,
+  Project,
+  ReleaseProject,
+} from 'app/types';
 import {defined} from 'app/utils';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
 import {statsPeriodToDays} from 'app/utils/dates';
@@ -34,13 +40,15 @@ const BACKEND_PLATFORMS: string[] = [...backend];
 const MOBILE_PLATFORMS: string[] = [...mobile];
 
 export function platformToPerformanceType(
-  projects: Project[],
+  projects: (Project | ReleaseProject)[],
   projectIds: readonly number[]
 ) {
   if (projectIds.length === 0 || projectIds[0] === ALL_ACCESS_PROJECTS) {
     return PROJECT_PERFORMANCE_TYPE.ANY;
   }
-  const selectedProjects = projects.filter(p => projectIds.includes(parseInt(p.id, 10)));
+  const selectedProjects = projects.filter(p =>
+    projectIds.includes(parseInt(`${p.id}`, 10))
+  );
   if (selectedProjects.length === 0 || selectedProjects.some(p => !p.platform)) {
     return PROJECT_PERFORMANCE_TYPE.ANY;
   }

+ 122 - 4
static/app/views/releases/detail/overview/index.tsx

@@ -29,6 +29,7 @@ import {
 import {getUtcDateString} from 'app/utils/dates';
 import {TableDataRow} from 'app/utils/discover/discoverQuery';
 import EventView from 'app/utils/discover/eventView';
+import {MobileVital, WebVital} from 'app/utils/discover/fields';
 import {formatVersion} from 'app/utils/formatters';
 import {decodeScalar} from 'app/utils/queryString';
 import routeTitleGen from 'app/utils/routeTitle';
@@ -39,6 +40,10 @@ import AsyncView from 'app/views/asyncView';
 import {DisplayModes} from 'app/views/performance/transactionSummary/transactionOverview/charts';
 import {transactionSummaryRouteWithQuery} from 'app/views/performance/transactionSummary/utils';
 import {TrendChangeType, TrendView} from 'app/views/performance/trends/types';
+import {
+  platformToPerformanceType,
+  PROJECT_PERFORMANCE_TYPE,
+} from 'app/views/performance/utils';
 
 import {getReleaseParams, isReleaseArchived, ReleaseBounds} from '../../utils';
 import {ReleaseContext} from '..';
@@ -182,6 +187,107 @@ class ReleaseOverview extends AsyncView<Props> {
     return trendView;
   }
 
+  getReleasePerformanceEventView(
+    performanceType: string,
+    baseQuery: NewQuery
+  ): EventView {
+    const eventView =
+      performanceType === PROJECT_PERFORMANCE_TYPE.FRONTEND
+        ? (EventView.fromSavedQuery({
+            ...baseQuery,
+            fields: [
+              ...baseQuery.fields,
+              `p75(${WebVital.FCP})`,
+              `p75(${WebVital.FID})`,
+              `p75(${WebVital.LCP})`,
+              `p75(${WebVital.CLS})`,
+              'p75(spans.http)',
+              'p75(spans.browser)',
+              'p75(spans.resource)',
+            ],
+          }) as EventView)
+        : performanceType === PROJECT_PERFORMANCE_TYPE.BACKEND
+        ? (EventView.fromSavedQuery({
+            ...baseQuery,
+            fields: [...baseQuery.fields, 'apdex()', 'p75(spans.http)', 'p75(spans.db)'],
+          }) as EventView)
+        : performanceType === PROJECT_PERFORMANCE_TYPE.MOBILE
+        ? (EventView.fromSavedQuery({
+            ...baseQuery,
+            fields: [
+              ...baseQuery.fields,
+              `p75(${MobileVital.AppStartCold})`,
+              `p75(${MobileVital.AppStartWarm})`,
+              `p75(${MobileVital.FramesSlow})`,
+              `p75(${MobileVital.FramesFrozen})`,
+            ],
+          }) as EventView)
+        : (EventView.fromSavedQuery({
+            ...baseQuery,
+          }) as EventView);
+
+    return eventView;
+  }
+
+  getAllReleasesPerformanceView(
+    projectId: number,
+    performanceType: string,
+    releaseBounds: ReleaseBounds
+  ) {
+    const {selection, location} = this.props;
+    const {environments} = selection;
+
+    const {start, end, statsPeriod} = getReleaseParams({
+      location,
+      releaseBounds,
+    });
+
+    const baseQuery: NewQuery = {
+      id: undefined,
+      version: 2,
+      name: 'All Releases',
+      query: 'event.type:transaction',
+      fields: ['user_misery()'],
+      range: statsPeriod || undefined,
+      environment: environments,
+      projects: [projectId],
+      start: start ? getUtcDateString(start) : undefined,
+      end: end ? getUtcDateString(end) : undefined,
+    };
+
+    return this.getReleasePerformanceEventView(performanceType, baseQuery);
+  }
+
+  getReleasePerformanceView(
+    version: string,
+    projectId: number,
+    performanceType: string,
+    releaseBounds: ReleaseBounds
+  ) {
+    const {selection, location} = this.props;
+    const {environments} = selection;
+
+    const {start, end, statsPeriod} = getReleaseParams({
+      location,
+      releaseBounds,
+    });
+
+    const baseQuery: NewQuery = {
+      id: undefined,
+      version: 2,
+      name: `Release:${version}`,
+      query: `event.type:transaction release:${version}`,
+      fields: ['user_misery()'],
+      range: statsPeriod || undefined,
+      environment: environments,
+      projects: [projectId],
+      start: start ? getUtcDateString(start) : undefined,
+      end: end ? getUtcDateString(end) : undefined,
+    };
+
+    return this.getReleasePerformanceEventView(performanceType, baseQuery);
+  }
+
   get pageDateTime(): DateTimeObject {
     const query = this.props.location.query;
 
@@ -268,7 +374,7 @@ class ReleaseOverview extends AsyncView<Props> {
             'release-comparison-performance'
           );
           const {environments} = selection;
-
+          const performanceType = platformToPerformanceType([project], [project.id]);
           const {selectedSort, sortOptions} = getTransactionsListSort(location);
           const releaseEventView = this.getReleaseEventView(
             version,
@@ -286,6 +392,17 @@ class ReleaseOverview extends AsyncView<Props> {
             releaseMeta.released,
             releaseBounds
           );
+          const allReleasesPerformanceView = this.getAllReleasesPerformanceView(
+            project.id,
+            performanceType,
+            releaseBounds
+          );
+          const releasePerformanceView = this.getReleasePerformanceView(
+            version,
+            project.id,
+            performanceType,
+            releaseBounds
+          );
 
           const generateLink = {
             transaction: generateTransactionLink(
@@ -397,9 +514,10 @@ class ReleaseOverview extends AsyncView<Props> {
                               <PerformanceCardTable
                                 organization={organization}
                                 project={project}
-                                isLoading={loading}
-                                // TODO(kelly): hardcoding this until I have data
-                                isEmpty={false}
+                                location={location}
+                                allReleasesEventView={allReleasesPerformanceView}
+                                releaseEventView={releasePerformanceView}
+                                performanceType={performanceType}
                               />
                             ) : (
                               <TransactionsList