Browse Source

feat(issues): monitor inp on issues stream page (#63780)

Keeping this isolated on issues stream because I want to see how well
the selector name inference works. Currently just reporting all inp
events w/o a threshold. I expect we will need to tweak this, but I'm
unsure what a good boundary will be without seeing the data.
Jonas 1 year ago
parent
commit
d0b9fda25f

+ 75 - 0
static/app/utils/performanceForSentry/index.tsx

@@ -575,3 +575,78 @@ export const addUIElementTag = (transaction: TransactionEvent) => {
 
   transaction.tags.interactionElement = interactionSpan?.description;
 };
+
+function supportsINP() {
+  return (
+    'PerformanceObserver' in window &&
+    'PerformanceEventTiming' in window &&
+    'interactionId' in PerformanceEventTiming.prototype
+  );
+}
+
+interface INPPerformanceEntry extends PerformanceEntry {
+  cancellable: boolean;
+  duration: number;
+  entryType: 'first-input';
+  name: string;
+  processingEnd: number;
+  processingStart: number;
+  startTime: number;
+  target: HTMLElement | undefined;
+}
+
+function isINPEntity(entry: PerformanceEntry): entry is INPPerformanceEntry {
+  return entry.entryType === 'first-input';
+}
+
+function getNearestElementName(node: HTMLElement | undefined): string | undefined {
+  if (!node) {
+    return 'unknown';
+  }
+
+  let current: HTMLElement | null = node;
+  while (current && current !== document.body) {
+    const elementName =
+      current.dataset?.testId ?? current.dataset?.component ?? current.dataset?.element;
+
+    if (elementName !== undefined) {
+      return elementName;
+    }
+
+    current = current.parentElement;
+  }
+
+  return 'unknown';
+}
+
+export function makeIssuesINPObserver(): PerformanceObserver | undefined {
+  if (!supportsINP()) {
+    return undefined;
+  }
+
+  const observer = new PerformanceObserver(entryList => {
+    entryList.getEntries().forEach(entry => {
+      if (!isINPEntity(entry)) {
+        return;
+      }
+
+      if (entry.duration) {
+        // < 16 ms wont cause frame drops so just ignore this for now
+        if (entry.duration < 16) {
+          return;
+        }
+        Sentry.metrics.distribution('issues-stream.inp', entry.duration, {
+          unit: 'millisecond',
+          tags: {
+            element: getNearestElementName(entry.target),
+            entryType: entry.entryType,
+            interaction: entry.name,
+          },
+        });
+      }
+    });
+  });
+
+  observer.observe({type: 'first-input', buffered: true});
+  return observer;
+}

+ 10 - 1
static/app/views/issueList/overview.tsx

@@ -46,7 +46,10 @@ import {getUtcDateString} from 'sentry/utils/dates';
 import getCurrentSentryReactTransaction from 'sentry/utils/getCurrentSentryReactTransaction';
 import parseApiError from 'sentry/utils/parseApiError';
 import parseLinkHeader from 'sentry/utils/parseLinkHeader';
-import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
+import {
+  makeIssuesINPObserver,
+  VisuallyCompleteWithData,
+} from 'sentry/utils/performanceForSentry';
 import {decodeScalar} from 'sentry/utils/queryString';
 import withRouteAnalytics, {
   WithRouteAnalyticsProps,
@@ -174,6 +177,7 @@ class IssueListOverview extends Component<Props, State> {
   }
 
   componentDidMount() {
+    this._performanceObserver = makeIssuesINPObserver();
     this._poller = new CursorPoller({
       linkPreviousHref: parseLinkHeader(this.state.pageLinks)?.previous?.href,
       success: this.onRealtimePoll,
@@ -297,6 +301,10 @@ class IssueListOverview extends Component<Props, State> {
         pageLinks: this.state.pageLinks,
       });
     }
+
+    if (this._performanceObserver) {
+      this._performanceObserver.disconnect();
+    }
     this._poller.disable();
     SelectedGroupStore.reset();
     GroupStore.reset();
@@ -304,6 +312,7 @@ class IssueListOverview extends Component<Props, State> {
     this.listener?.();
   }
 
+  private _performanceObserver: PerformanceObserver | undefined;
   private _poller: any;
   private _lastRequest: any;
   private _lastStatsRequest: any;