Browse Source

feat(browser-starfish): add example images in resource summary (#61866)

Dominik Buszowiecki 1 year ago
parent
commit
50d70b0d31

+ 7 - 1
static/app/views/performance/browser/resources/resourceSummaryPage/index.tsx

@@ -15,7 +15,9 @@ import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import ResourceInfo from 'sentry/views/performance/browser/resources/resourceSummaryPage/resourceInfo';
 import ResourceSummaryCharts from 'sentry/views/performance/browser/resources/resourceSummaryPage/resourceSummaryCharts';
 import ResourceSummaryTable from 'sentry/views/performance/browser/resources/resourceSummaryPage/resourceSummaryTable';
+import SampleImages from 'sentry/views/performance/browser/resources/resourceSummaryPage/sampleImages';
 import {FilterOptionsContainer} from 'sentry/views/performance/browser/resources/resourceView';
+import {IMAGE_FILE_EXTENSIONS} from 'sentry/views/performance/browser/resources/shared/constants';
 import RenderBlockingSelector from 'sentry/views/performance/browser/resources/shared/renderBlockingSelector';
 import {useResourceModuleFilters} from 'sentry/views/performance/browser/resources/utils/useResourceFilters';
 import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
@@ -54,9 +56,12 @@ function ResourceSummary() {
       'time_spent_percentage()',
     ]
   );
-
   const spanMetrics = data[0] ?? {};
 
+  const isImage = IMAGE_FILE_EXTENSIONS.includes(
+    spanMetrics[SpanMetricsField.SPAN_DESCRIPTION]?.split('.').pop() || ''
+  );
+
   return (
     <ModulePageProviders
       title={[t('Performance'), t('Resources'), t('Resource Summary')].join(' — ')}
@@ -113,6 +118,7 @@ function ResourceSummary() {
               timeSpentPercentage={spanMetrics[`time_spent_percentage()`]}
             />
           </HeaderContainer>
+          {isImage && <SampleImages groupId={groupId} />}
           <ResourceSummaryCharts groupId={groupId} />
           <ResourceSummaryTable />
           <SampleList

+ 90 - 0
static/app/views/performance/browser/resources/resourceSummaryPage/sampleImages.tsx

@@ -0,0 +1,90 @@
+import styled from '@emotion/styled';
+
+import {t} from 'sentry/locale';
+import {formatBytesBase2} from 'sentry/utils';
+import getDynamicText from 'sentry/utils/getDynamicText';
+import {useIndexedResourcesQuery} from 'sentry/views/performance/browser/resources/utils/useIndexedResourceQuery';
+import ChartPanel from 'sentry/views/starfish/components/chartPanel';
+import {SpanIndexedField} from 'sentry/views/starfish/types';
+
+type Props = {groupId: string};
+
+const {SPAN_GROUP, SPAN_DESCRIPTION, HTTP_RESPONSE_CONTENT_LENGTH} = SpanIndexedField;
+const imageWidth = '200px';
+
+function SampleImages({groupId}: Props) {
+  const imageResources = useIndexedResourcesQuery({
+    queryConditions: [`${SPAN_GROUP}:${groupId}`],
+    sorts: [{field: HTTP_RESPONSE_CONTENT_LENGTH, kind: 'desc'}],
+    limit: 100,
+  });
+
+  const uniqueResources = new Set();
+
+  const filteredResources = imageResources.data
+    .filter(resource => {
+      const fileName = getFileNameFromDescription(resource[SPAN_DESCRIPTION]);
+      if (uniqueResources.has(fileName)) {
+        return false;
+      }
+      uniqueResources.add(fileName);
+      return true;
+    })
+    .splice(0, 5);
+
+  return (
+    <ChartPanel title={t('Example Images')}>
+      <ImageWrapper>
+        {filteredResources.map(resource => {
+          return (
+            <ImageContainer
+              src={resource[SPAN_DESCRIPTION]}
+              fileName={getFileNameFromDescription(resource[SPAN_DESCRIPTION])}
+              size={resource[HTTP_RESPONSE_CONTENT_LENGTH]}
+              key={resource[SPAN_DESCRIPTION]}
+            />
+          );
+        })}
+      </ImageWrapper>
+    </ChartPanel>
+  );
+}
+
+function ImageContainer({
+  src,
+  fileName,
+  size,
+}: {
+  fileName: string;
+  size: number;
+  src: string;
+}) {
+  const fileSize = getDynamicText({
+    value: formatBytesBase2(size),
+    fixed: 'xx KB',
+  });
+
+  return (
+    <div style={{width: '100%', wordWrap: 'break-word'}}>
+      <img src={src} style={{minWidth: imageWidth, height: '200px'}} />
+      {fileName} ({fileSize})
+    </div>
+  );
+}
+
+const ImageWrapper = styled('div')`
+  display: grid;
+  grid-template-columns: repeat(auto-fill, ${imageWidth});
+  gap: 30px;
+`;
+
+const getFileNameFromDescription = (description: string) => {
+  try {
+    const url = new URL(description);
+    return url.pathname.split('/').pop() || '';
+  } catch (e) {
+    return description;
+  }
+};
+
+export default SampleImages;

+ 75 - 0
static/app/views/performance/browser/resources/utils/useIndexedResourceQuery.ts

@@ -0,0 +1,75 @@
+import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
+import EventView from 'sentry/utils/discover/eventView';
+import {Sort} from 'sentry/utils/discover/fields';
+import {DiscoverDatasets} from 'sentry/utils/discover/types';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import {SpanIndexedField} from 'sentry/views/starfish/types';
+
+const {SPAN_DESCRIPTION, HTTP_RESPONSE_CONTENT_LENGTH} = SpanIndexedField;
+
+type Options = {
+  limit?: number;
+  queryConditions?: string[];
+  referrer?: string;
+  sorts?: Sort[];
+};
+
+export const useIndexedResourcesQuery = ({
+  queryConditions = [],
+  limit = 50,
+  sorts,
+  referrer,
+}: Options) => {
+  const pageFilters = usePageFilters();
+  const location = useLocation();
+  const {slug: orgSlug} = useOrganization();
+
+  // TODO - we should be using metrics data here
+  const eventView = EventView.fromNewQueryWithPageFilters(
+    {
+      fields: [
+        `any(id)`,
+        'project',
+        'span.group',
+        SPAN_DESCRIPTION,
+        HTTP_RESPONSE_CONTENT_LENGTH,
+      ],
+      name: 'Indexed Resource Query',
+      query: queryConditions.join(' '),
+      version: 2,
+      orderby: '-count()',
+      dataset: DiscoverDatasets.SPANS_INDEXED,
+    },
+    pageFilters.selection
+  );
+
+  if (sorts) {
+    eventView.sorts = sorts;
+  }
+
+  const result = useDiscoverQuery({
+    eventView,
+    limit,
+    location,
+    orgSlug,
+    referrer,
+    options: {
+      refetchOnWindowFocus: false,
+    },
+  });
+
+  const data =
+    result?.data?.data.map(row => ({
+      project: row.project as string,
+      'transaction.id': row['transaction.id'] as string,
+      [SPAN_DESCRIPTION]: row[SPAN_DESCRIPTION]?.toString(),
+      // TODO - parseFloat here is temporary, we should be parsing from the backend
+      [HTTP_RESPONSE_CONTENT_LENGTH]: parseFloat(
+        (row[HTTP_RESPONSE_CONTENT_LENGTH] as string) || '0'
+      ),
+    })) ?? [];
+
+  return {...result, data};
+};