Просмотр исходного кода

feat(ui): Collect metrics for globals usage (#26098)

We export certain components and/or functionality to the global object
(`window`) for use in frontend systems outside of the main SPA (e.g.
plugins, getsentry). We will need to deprecate this functionality (see
https://github.com/getsentry/sentry/pull/25821 for a migration path), but
we would also like to know if this is used outside of sentry.

This is attempt #2 for https://github.com/getsentry/sentry/pull/25976 which
was reverted due to Firefox behaving differently than Chrome. For some
reason, `jQuery` is being called many times on Firefox, even though the
getter did not seem to be called from anywhere. The stacktrace always has a
single frame that originates in the getter. This also seems to only happen
from the `jQuery` property. This PR indirectly addresses this by not
recording a metric if the stacktrace only has a single frame.

Additional changes include:
* Capture up to 5 frames when recording a metric
* Limit batch_data to 20 elements
Billy Vong 3 лет назад
Родитель
Сommit
87c58ff1f5
1 измененных файлов с 75 добавлено и 3 удалено
  1. 75 3
      static/app/bootstrap/exportGlobals.tsx

+ 75 - 3
static/app/bootstrap/exportGlobals.tsx

@@ -4,10 +4,12 @@ import * as Router from 'react-router';
 import * as Sentry from '@sentry/react';
 import createReactClass from 'create-react-class';
 import jQuery from 'jquery';
+import throttle from 'lodash/throttle';
 import moment from 'moment';
 import PropTypes from 'prop-types';
 import Reflux from 'reflux';
 
+import {Client} from 'app/api';
 import plugins from 'app/plugins';
 
 const globals = {
@@ -36,7 +38,7 @@ const globals = {
 
 // The SentryApp global contains exported app modules for use in javascript
 // modules that are not compiled with the sentry bundle.
-globals.SentryApp = {
+const SentryApp = {
   // The following components are used in sentry-plugins.
   Form: require('app/components/forms/form').default,
   FormState: require('app/components/forms/index').FormState,
@@ -54,7 +56,77 @@ globals.SentryApp = {
   Modal: require('app/actionCreators/modal'),
 };
 
-// Make globals available on the window object
-Object.keys(globals).forEach(name => (window[name] = globals[name]));
+/**
+ * Wrap export so that we can track usage of these globals to determine how we want to handle deprecatation.
+ * These are sent to Sentry install, which then checks to see if SENTRY_BEACON is enabled
+ * in order to make a request to the SaaS beacon.
+ */
+let _beaconComponents: {component: string; stack: string}[] = [];
+const makeBeaconRequest = throttle(
+  async () => {
+    const api = new Client();
+
+    const components = _beaconComponents;
+    _beaconComponents = [];
+    try {
+      await api.requestPromise('/api/0/internal/beacon/', {
+        method: 'POST',
+        data: {
+          // Limit to first 20 components... if there are more than 20, then something
+          // is probably wrong.
+          batch_data: components.slice(0, 20).map(component => ({
+            description: 'SentryApp',
+            ...component,
+          })),
+        },
+      });
+    } catch (e) {
+      // Delicious failure.
+    }
+  },
+  5000,
+  {trailing: true, leading: false}
+);
+
+[
+  [SentryApp, globals.SentryApp],
+  [globals, window],
+].forEach(([obj, parent]) => {
+  const properties = Object.fromEntries(
+    Object.entries(obj).map(([key, value]) => {
+      return [
+        key,
+        {
+          configurable: false,
+          enumerable: false,
+          get() {
+            try {
+              const stack = new Error().stack;
+              // Split stack by lines and filter out empty strings
+              const stackArr = stack?.split('\n').filter(s => !!s) || [];
+              // There's an issue with Firefox where this getter for jQuery gets called many times (> 100)
+              // The stacktrace doesn't show it being called outside of this block either.
+              // And this works fine in Chrome...
+              if (key !== 'SentryApp' && stackArr.length > 1) {
+                // Limit the number of frames to include
+                _beaconComponents.push({
+                  component: key,
+                  stack: stackArr.slice(0, 5).join('\n'),
+                });
+                makeBeaconRequest();
+              }
+            } catch {
+              // Ignore errors
+            }
+
+            return value;
+          },
+        },
+      ];
+    })
+  );
+
+  Object.defineProperties(parent, properties);
+});
 
 export default globals;