|
@@ -1,10 +1,23 @@
|
|
|
import {Fragment, Profiler, ReactNode, useEffect, useRef} from 'react';
|
|
|
-import {captureException} from '@sentry/react';
|
|
|
+import {captureException, captureMessage} from '@sentry/react';
|
|
|
+import * as Sentry from '@sentry/react';
|
|
|
+import {IdleTransaction} from '@sentry/tracing';
|
|
|
+import {Transaction} from '@sentry/types';
|
|
|
import {browserPerformanceTimeOrigin, timestampWithMs} from '@sentry/utils';
|
|
|
|
|
|
import getCurrentSentryReactTransaction from './getCurrentSentryReactTransaction';
|
|
|
|
|
|
-const MIN_UPDATE_SPAN_TIME = 5; // Frame boundary @ 60fps
|
|
|
+const MIN_UPDATE_SPAN_TIME = 16; // Frame boundary @ 60fps
|
|
|
+const WAIT_POST_INTERACTION = 50; // Leave a small amount of time for observers and onRenderCallback to log since they come in after they occur and not during.
|
|
|
+const INTERACTION_TIMEOUT = 2 * 60_000; // 2min. Wrap interactions up after this time since we don't want transactions sticking around forever.
|
|
|
+
|
|
|
+/**
|
|
|
+ * It depends on where it is called but the way we fetch transactions can be empty despite an ongoing transaction existing.
|
|
|
+ * This will return an interaction-type transaction held onto by a class static if one exists.
|
|
|
+ */
|
|
|
+export function getPerformanceTransaction(): IdleTransaction | Transaction | undefined {
|
|
|
+ return PerformanceInteraction.getTransaction() ?? getCurrentSentryReactTransaction();
|
|
|
+}
|
|
|
|
|
|
/**
|
|
|
* Callback for React Profiler https://reactjs.org/docs/profiler.html
|
|
@@ -15,7 +28,7 @@ export function onRenderCallback(
|
|
|
actualDuration: number
|
|
|
) {
|
|
|
try {
|
|
|
- const transaction = getCurrentSentryReactTransaction();
|
|
|
+ const transaction: Transaction | undefined = getPerformanceTransaction();
|
|
|
if (transaction && actualDuration > MIN_UPDATE_SPAN_TIME) {
|
|
|
const now = timestampWithMs();
|
|
|
transaction.startChild({
|
|
@@ -30,59 +43,155 @@ export function onRenderCallback(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+export class PerformanceInteraction {
|
|
|
+ private static interactionTransaction: Transaction | null = null;
|
|
|
+ private static interactionTimeoutId: number | undefined = undefined;
|
|
|
+
|
|
|
+ static getTransaction() {
|
|
|
+ return PerformanceInteraction.interactionTransaction;
|
|
|
+ }
|
|
|
+
|
|
|
+ static async startInteraction(
|
|
|
+ name: string,
|
|
|
+ timeout = INTERACTION_TIMEOUT,
|
|
|
+ immediate = true
|
|
|
+ ) {
|
|
|
+ try {
|
|
|
+ const currentIdleTransaction = getCurrentSentryReactTransaction();
|
|
|
+ if (currentIdleTransaction) {
|
|
|
+ // If interaction is started while idle still exists.
|
|
|
+ LongTaskObserver.setLongTaskTags(currentIdleTransaction);
|
|
|
+ currentIdleTransaction.setTag('finishReason', 'sentry.interactionStarted'); // Override finish reason so we can capture if this has effects on idle timeout.
|
|
|
+ currentIdleTransaction.finish();
|
|
|
+ }
|
|
|
+ PerformanceInteraction.finishInteraction(immediate);
|
|
|
+
|
|
|
+ const txn = Sentry?.startTransaction({
|
|
|
+ name: `ui.${name}`,
|
|
|
+ op: 'interaction',
|
|
|
+ });
|
|
|
+
|
|
|
+ PerformanceInteraction.interactionTransaction = txn;
|
|
|
+
|
|
|
+ // Auto interaction timeout
|
|
|
+ PerformanceInteraction.interactionTimeoutId = window.setTimeout(() => {
|
|
|
+ if (!PerformanceInteraction.interactionTransaction) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ PerformanceInteraction.interactionTransaction.setTag(
|
|
|
+ 'ui.interaction.finish',
|
|
|
+ 'timeout'
|
|
|
+ );
|
|
|
+ PerformanceInteraction.finishInteraction(true);
|
|
|
+ }, timeout);
|
|
|
+ } catch (e) {
|
|
|
+ captureMessage(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static async finishInteraction(immediate = false) {
|
|
|
+ try {
|
|
|
+ if (!PerformanceInteraction.interactionTransaction) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ clearTimeout(PerformanceInteraction.interactionTimeoutId);
|
|
|
+
|
|
|
+ LongTaskObserver.setLongTaskTags(PerformanceInteraction.interactionTransaction);
|
|
|
+
|
|
|
+ if (immediate) {
|
|
|
+ PerformanceInteraction.interactionTransaction?.finish();
|
|
|
+ PerformanceInteraction.interactionTransaction = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add a slight wait if this isn't called as the result of another transaction starting.
|
|
|
+ await new Promise(resolve => setTimeout(resolve, WAIT_POST_INTERACTION));
|
|
|
+ PerformanceInteraction.interactionTransaction?.finish();
|
|
|
+ PerformanceInteraction.interactionTransaction = null;
|
|
|
+
|
|
|
+ return;
|
|
|
+ } catch (e) {
|
|
|
+ captureMessage(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
class LongTaskObserver {
|
|
|
private static observer: PerformanceObserver;
|
|
|
private static longTaskCount = 0;
|
|
|
+ private static lastTransaction: IdleTransaction | Transaction | undefined;
|
|
|
private static currentId: string;
|
|
|
+
|
|
|
+ static setLongTaskTags(t: IdleTransaction | Transaction) {
|
|
|
+ t.setTag('ui.longTaskCount', LongTaskObserver.longTaskCount);
|
|
|
+ const group =
|
|
|
+ [
|
|
|
+ 1, 2, 5, 10, 25, 50, 100, 150, 200, 250, 300, 400, 500, 600, 700, 800, 900, 1001,
|
|
|
+ ].find(n => LongTaskObserver.longTaskCount <= n) || -1;
|
|
|
+ t.setTag('ui.longTaskCount.grouped', group < 1001 ? `<=${group}` : `>1000`);
|
|
|
+ }
|
|
|
+
|
|
|
static getPerformanceObserver(id: string): PerformanceObserver | null {
|
|
|
try {
|
|
|
LongTaskObserver.currentId = id;
|
|
|
if (LongTaskObserver.observer) {
|
|
|
LongTaskObserver.observer.disconnect();
|
|
|
- LongTaskObserver.observer.observe({entryTypes: ['longtask']});
|
|
|
+ try {
|
|
|
+ LongTaskObserver.observer.observe({entryTypes: ['longtask']});
|
|
|
+ } catch (_) {
|
|
|
+ // Safari doesn't support longtask, ignore this error.
|
|
|
+ }
|
|
|
return LongTaskObserver.observer;
|
|
|
}
|
|
|
if (!window.PerformanceObserver || !browserPerformanceTimeOrigin) {
|
|
|
return null;
|
|
|
}
|
|
|
- const transaction: any = getCurrentSentryReactTransaction();
|
|
|
|
|
|
const timeOrigin = browserPerformanceTimeOrigin / 1000;
|
|
|
|
|
|
const observer = new PerformanceObserver(function (list) {
|
|
|
- const perfEntries = list.getEntries();
|
|
|
+ try {
|
|
|
+ const transaction = getPerformanceTransaction();
|
|
|
+ const perfEntries = list.getEntries();
|
|
|
|
|
|
- if (!transaction) {
|
|
|
- return;
|
|
|
- }
|
|
|
- perfEntries.forEach(entry => {
|
|
|
- const startSeconds = timeOrigin + entry.startTime / 1000;
|
|
|
- LongTaskObserver.longTaskCount++;
|
|
|
- transaction.startChild({
|
|
|
- description: `Long Task - ${LongTaskObserver.currentId}`,
|
|
|
- op: `ui.sentry.long-task`,
|
|
|
- startTimestamp: startSeconds,
|
|
|
- endTimestamp: startSeconds + entry.duration / 1000,
|
|
|
- });
|
|
|
- });
|
|
|
- });
|
|
|
+ if (!transaction) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (!transaction) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- transaction?.registerBeforeFinishCallback?.(t => {
|
|
|
- if (!browserPerformanceTimeOrigin) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (transaction !== LongTaskObserver.lastTransaction) {
|
|
|
+ // If long tasks observer is active and is called while the transaction has changed.
|
|
|
+ if (LongTaskObserver.lastTransaction) {
|
|
|
+ LongTaskObserver.setLongTaskTags(LongTaskObserver.lastTransaction);
|
|
|
+ }
|
|
|
+ LongTaskObserver.longTaskCount = 0;
|
|
|
+ LongTaskObserver.lastTransaction = transaction;
|
|
|
+ }
|
|
|
|
|
|
- t.setTag('longTaskCount', LongTaskObserver.longTaskCount);
|
|
|
+ perfEntries.forEach(entry => {
|
|
|
+ const startSeconds = timeOrigin + entry.startTime / 1000;
|
|
|
+ LongTaskObserver.longTaskCount++;
|
|
|
+ transaction.startChild({
|
|
|
+ description: `Long Task - ${LongTaskObserver.currentId}`,
|
|
|
+ op: `ui.sentry.long-task`,
|
|
|
+ startTimestamp: startSeconds,
|
|
|
+ endTimestamp: startSeconds + entry.duration / 1000,
|
|
|
+ });
|
|
|
+ });
|
|
|
+ LongTaskObserver.setLongTaskTags(transaction);
|
|
|
+ } catch (_) {
|
|
|
+ // Defensive catch.
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
if (!observer || !observer.observe) {
|
|
|
return null;
|
|
|
}
|
|
|
LongTaskObserver.observer = observer;
|
|
|
- LongTaskObserver.observer.observe({entryTypes: ['longtask']});
|
|
|
+ try {
|
|
|
+ LongTaskObserver.observer.observe({entryTypes: ['longtask']});
|
|
|
+ } catch (_) {
|
|
|
+ // Safari doesn't support longtask, ignore this error.
|
|
|
+ }
|
|
|
|
|
|
return LongTaskObserver.observer;
|
|
|
} catch (e) {
|