Browse Source

feat(webvitals): Add INP samples query hook (#66420)

Adds hooks to query for INP span samples in the web vitals module.
Also prepares good meh and bad category query hooks for both transactions and INP span samples for use in the page overview.
edwardgou-sentry 1 year ago
parent
commit
29d804d7e1

+ 25 - 0
static/app/views/performance/browser/webVitals/utils/queries/useGoodMehAndBadInteractionsSamplesQuery.tsx

@@ -0,0 +1,25 @@
+import {useInpSpanSamplesWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/useInpSpanSamplesWebVitalsQuery';
+import type {InteractionSpanSampleRowWithScore} from 'sentry/views/performance/browser/webVitals/utils/types';
+
+type Props = {
+  enabled: boolean;
+  transaction: string;
+};
+
+export function useGoodMehAndBadInteractionsSamplesQuery({transaction, enabled}: Props) {
+  // TODO: Update this function to query good, meh, and bad interactions
+  const {data, isFetching} = useInpSpanSamplesWebVitalsQuery({
+    transaction,
+    enabled,
+    limit: 9,
+  });
+
+  const interactionsTableData: InteractionSpanSampleRowWithScore[] = data.sort(
+    (a, b) => a.inpScore - b.inpScore
+  );
+
+  return {
+    data: interactionsTableData,
+    isLoading: isFetching,
+  };
+}

+ 76 - 0
static/app/views/performance/browser/webVitals/utils/queries/useGoodMehAndBadTransactionsSamplesQuery.tsx

@@ -0,0 +1,76 @@
+import {
+  PERFORMANCE_SCORE_MEDIANS,
+  PERFORMANCE_SCORE_P90S,
+} from 'sentry/views/performance/browser/webVitals/utils/queries/rawWebVitalsQueries/calculatePerformanceScore';
+import {useTransactionSamplesWebVitalsQuery} from 'sentry/views/performance/browser/webVitals/utils/queries/useTransactionSamplesWebVitalsQuery';
+import type {
+  TransactionSampleRowWithScore,
+  WebVitals,
+} from 'sentry/views/performance/browser/webVitals/utils/types';
+
+type Props = {
+  enabled: boolean;
+  transaction: string;
+  webVital: WebVitals | null;
+};
+
+export function useGoodMehAndBadTransactionsSamplesQuery({
+  transaction,
+  webVital,
+  enabled,
+}: Props) {
+  const {data: goodData, isLoading: isGoodTransactionWebVitalsQueryLoading} =
+    useTransactionSamplesWebVitalsQuery({
+      limit: 3,
+      transaction: transaction ?? '',
+      query: webVital
+        ? `measurements.${webVital}:<${PERFORMANCE_SCORE_P90S[webVital]}`
+        : undefined,
+      enabled,
+      withProfiles: true,
+      sortName: 'webVitalSort',
+      webVital: webVital ?? undefined,
+    });
+
+  const {data: mehData, isLoading: isMehTransactionWebVitalsQueryLoading} =
+    useTransactionSamplesWebVitalsQuery({
+      limit: 3,
+      transaction: transaction ?? '',
+      query: webVital
+        ? `measurements.${webVital}:<${PERFORMANCE_SCORE_MEDIANS[webVital]} measurements.${webVital}:>=${PERFORMANCE_SCORE_P90S[webVital]}`
+        : undefined,
+      enabled,
+      withProfiles: true,
+      sortName: 'webVitalSort',
+      webVital: webVital ?? undefined,
+    });
+
+  const {data: poorData, isLoading: isPoorTransactionWebVitalsQueryLoading} =
+    useTransactionSamplesWebVitalsQuery({
+      limit: 3,
+      transaction: transaction ?? '',
+      query: webVital
+        ? `measurements.${webVital}:>=${PERFORMANCE_SCORE_MEDIANS[webVital]}`
+        : undefined,
+      enabled,
+      withProfiles: true,
+      sortName: 'webVitalSort',
+      webVital: webVital ?? undefined,
+    });
+
+  const data = [...goodData, ...mehData, ...poorData];
+
+  const isTransactionWebVitalsQueryLoading =
+    isGoodTransactionWebVitalsQueryLoading ||
+    isMehTransactionWebVitalsQueryLoading ||
+    isPoorTransactionWebVitalsQueryLoading;
+
+  const transactionsTableData: TransactionSampleRowWithScore[] = data.sort(
+    (a, b) => a[`${webVital}Score`] - b[`${webVital}Score`]
+  );
+
+  return {
+    data: transactionsTableData,
+    isLoading: isTransactionWebVitalsQueryLoading,
+  };
+}

+ 72 - 0
static/app/views/performance/browser/webVitals/utils/queries/useInpSpanSamplesWebVitalsQuery.tsx

@@ -0,0 +1,72 @@
+import type {InteractionSpanSampleRowWithScore} from 'sentry/views/performance/browser/webVitals/utils/types';
+import {
+  type Filters,
+  useIndexedSpans,
+} from 'sentry/views/starfish/queries/useIndexedSpans';
+import {SpanIndexedField} from 'sentry/views/starfish/types';
+
+export function useInpSpanSamplesWebVitalsQuery({
+  transaction,
+  limit,
+  enabled,
+  filters = {},
+}: {
+  limit: number;
+  enabled?: boolean;
+  filters?: Filters;
+  transaction?: string;
+}) {
+  const {data, isLoading, ...rest} = useIndexedSpans({
+    filters: {
+      'span.op': 'ui.interaction.click',
+      'measurements.score.weight.inp': '>0',
+      ...(transaction !== undefined
+        ? {[SpanIndexedField.ORIGIN_TRANSACTION]: transaction}
+        : {}),
+      ...filters,
+    },
+    sorts: [{field: SpanIndexedField.TIMESTAMP, kind: 'desc'}],
+    fields: [
+      SpanIndexedField.INP,
+      SpanIndexedField.INP_SCORE,
+      SpanIndexedField.INP_SCORE_WEIGHT,
+      SpanIndexedField.TOTAL_SCORE,
+      SpanIndexedField.ID,
+      SpanIndexedField.TIMESTAMP,
+      SpanIndexedField.PROFILE_ID,
+      SpanIndexedField.REPLAY_ID,
+      SpanIndexedField.USER,
+      SpanIndexedField.ORIGIN_TRANSACTION,
+      SpanIndexedField.PROJECT,
+      SpanIndexedField.BROWSER_NAME,
+      SpanIndexedField.SPAN_SELF_TIME,
+    ],
+    enabled,
+    limit,
+    referrer: 'api.performance.browser.web-vitals.spans',
+  });
+  const tableData: InteractionSpanSampleRowWithScore[] =
+    !isLoading && data?.length
+      ? data.map(row => {
+          return {
+            ...row,
+            'measurements.inp': row[SpanIndexedField.INP],
+            'user.display': row[SpanIndexedField.USER],
+            replayId: row[SpanIndexedField.REPLAY_ID],
+            'profile.id': row[SpanIndexedField.PROFILE_ID],
+            inpScore: Math.round(
+              ((row[`measurements.score.inp`] ?? 0) /
+                (row[`measurements.score.weight.inp`] ?? 0)) *
+                100
+            ),
+            totalScore: Math.round(row[`measurements.score.total`] ?? 0),
+            projectSlug: row[SpanIndexedField.PROJECT],
+          };
+        })
+      : [];
+  return {
+    data: tableData,
+    isLoading,
+    ...rest,
+  };
+}

+ 1 - 3
static/app/views/performance/browser/webVitals/utils/types.tsx

@@ -46,17 +46,15 @@ export type InteractionSpanSampleRow = {
   'profile.id': string;
   projectSlug: string;
   replayId: string;
-  'segment.id': string;
-  'span.description': string;
   'span.op': string;
   'span.self_time': number;
   timestamp: string;
-  totalScore: number;
   'user.display': string;
 };
 
 export type InteractionSpanSampleRowWithScore = InteractionSpanSampleRow & {
   inpScore: number;
+  totalScore: number;
 };
 
 export type Weight = {

+ 1 - 1
static/app/views/starfish/queries/useIndexedSpans.tsx

@@ -8,7 +8,7 @@ import {useLocation} from 'sentry/utils/useLocation';
 import type {SpanIndexedField, SpanIndexedFieldTypes} from 'sentry/views/starfish/types';
 import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
 
-interface Filters {
+export interface Filters {
   [key: string]: string;
 }
 

+ 16 - 0
static/app/views/starfish/types.tsx

@@ -121,6 +121,14 @@ export enum SpanIndexedField {
   PROJECT_ID = 'project_id',
   PROFILE_ID = 'profile_id',
   TRANSACTION = 'transaction',
+  ORIGIN_TRANSACTION = 'origin.transaction',
+  REPLAY_ID = 'replay.id',
+  BROWSER_NAME = 'browser.name',
+  USER = 'user',
+  INP = 'measurements.inp',
+  INP_SCORE = 'measurements.score.inp',
+  INP_SCORE_WEIGHT = 'measurements.score.weight.inp',
+  TOTAL_SCORE = 'measurements.score.total',
 }
 
 export type SpanIndexedFieldTypes = {
@@ -141,6 +149,14 @@ export type SpanIndexedFieldTypes = {
   [SpanIndexedField.PROFILE_ID]: string;
   [SpanIndexedField.RESOURCE_RENDER_BLOCKING_STATUS]: '' | 'non-blocking' | 'blocking';
   [SpanIndexedField.HTTP_RESPONSE_CONTENT_LENGTH]: string;
+  [SpanIndexedField.ORIGIN_TRANSACTION]: string;
+  [SpanIndexedField.REPLAY_ID]: string;
+  [SpanIndexedField.BROWSER_NAME]: string;
+  [SpanIndexedField.USER]: string;
+  [SpanIndexedField.INP]: number;
+  [SpanIndexedField.INP_SCORE]: number;
+  [SpanIndexedField.INP_SCORE_WEIGHT]: number;
+  [SpanIndexedField.TOTAL_SCORE]: number;
 };
 
 export type Op = SpanIndexedFieldTypes[SpanIndexedField.SPAN_OP];