Browse Source

feat: JS SDK loader - CDN support (#8640)

* feat: Add correct header for sdk loader

* feat: Remove sdk url field from settings

* ref: Remove unused data attr fetch

* feat: Add .min.js for request, Remove middleware

* feat: Add tests for vary header + minified version

* ref: Move .min to seperate file, Move urls into settings

* feat: Make changes to cdn url

* feat: Remove placeholder in config, Add noop tmpl

* ref: Remove regex check
Daniel Griesser 6 years ago
parent
commit
6b04cfb6bc

+ 0 - 6
src/sentry/api/endpoints/project_key_details.py

@@ -40,7 +40,6 @@ class RateLimitSerializer(serializers.Serializer):
 
 class KeySerializer(serializers.Serializer):
     name = serializers.CharField(max_length=200, required=False)
-    jsSdkUrl = serializers.URLField(max_length=255, required=False)
     isActive = serializers.BooleanField(required=False)
     rateLimit = RateLimitSerializer(required=False)
 
@@ -89,11 +88,6 @@ class ProjectKeyDetailsEndpoint(ProjectEndpoint):
         if serializer.is_valid():
             result = serializer.object
 
-            if result.get('jsSdkUrl') == '':
-                key.data = {}
-            else:
-                key.data = {'js_sdk_url': result.get('jsSdkUrl', None)}
-
             if result.get('name'):
                 key.label = result['name']
 

+ 1 - 6
src/sentry/api/serializers/models/project_key.py

@@ -2,14 +2,12 @@ from __future__ import absolute_import
 
 from sentry.api.serializers import Serializer, register
 from sentry.models import ProjectKey
-from sentry.relay import Config
 
 
 @register(ProjectKey)
 class ProjectKeySerializer(Serializer):
     def serialize(self, obj, attrs, user):
         name = obj.label or obj.public_key[:14]
-        config = Config(obj.project)
         d = {
             'id': obj.public_key,
             'name': name,
@@ -29,11 +27,8 @@ class ProjectKeySerializer(Serializer):
                 'csp': obj.csp_endpoint,
                 'security': obj.security_endpoint,
                 'minidump': obj.minidump_endpoint,
+                'cdn': obj.js_sdk_loader_cdn_url,
             },
-            'relay': {
-                'url': config.get_cdn_url(obj),
-            },
-            'jsSdkUrl': obj.data.get('js_sdk_url', None),
             'dateCreated': obj.date_added,
         }
         return d

+ 11 - 0
src/sentry/conf/server.py

@@ -268,6 +268,7 @@ STATIC_URL = '/_static/{version}/'
 # cookies
 ANONYMOUS_STATIC_PREFIXES = (
     '/_static/', '/avatar/', '/organization-avatar/', '/team-avatar/', '/project-avatar/',
+    '/js-sdk-loader/'
 )
 
 STATICFILES_FINDERS = (
@@ -1343,3 +1344,13 @@ SENTRY_MINIDUMP_PATH = '/tmp/minidump'
 # Relay
 # List of PKs whitelisted by Sentry
 SENTRY_RELAY_WHITELIST_PK = []
+
+# CDN
+# If this is an absolute url like e.g.: https://js.sentry-cdn.com/
+# the full url will look like this: https://js.sentry-cdn.com/<public_key>.min.js
+# otherwise django reverse url lookup will be used.
+JS_SDK_LOADER_CDN_URL = ''
+# Version of the SDK - Used in header Surrogate-Key sdk/JS_SDK_LOADER_SDK_VERSION
+JS_SDK_LOADER_SDK_VERSION = ''
+# This should be the url pointing to the JS SDK
+JS_SDK_LOADER_DEFAULT_SDK_URL = ''

+ 1 - 0
src/sentry/middleware/user.py

@@ -12,6 +12,7 @@ class UserActiveMiddleware(object):
         'sentry.web.frontend.project_avatar',
         'sentry.web.frontend.team_avatar',
         'sentry.web.frontend.user_avatar',
+        'sentry.web.frontend.js_sdk_loader',
     )
 
     def process_view(self, request, view_func, view_args, view_kwargs):

+ 22 - 10
src/sentry/models/projectkey.py

@@ -29,8 +29,9 @@ from sentry.db.models import (
 
 _uuid4_re = re.compile(r'^[a-f0-9]{32}$')
 
-
 # TODO(dcramer): pull in enum library
+
+
 class ProjectKeyStatus(object):
     ACTIVE = 0
     INACTIVE = 1
@@ -167,9 +168,7 @@ class ProjectKey(Model):
 
     @property
     def csp_endpoint(self):
-        endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT
-        if not endpoint:
-            endpoint = options.get('system.url-prefix')
+        endpoint = self.get_endpoint()
 
         return '%s%s?sentry_key=%s' % (
             endpoint,
@@ -179,9 +178,7 @@ class ProjectKey(Model):
 
     @property
     def security_endpoint(self):
-        endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT
-        if not endpoint:
-            endpoint = options.get('system.url-prefix')
+        endpoint = self.get_endpoint()
 
         return '%s%s?sentry_key=%s' % (
             endpoint,
@@ -191,9 +188,7 @@ class ProjectKey(Model):
 
     @property
     def minidump_endpoint(self):
-        endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT
-        if not endpoint:
-            endpoint = options.get('system.url-prefix')
+        endpoint = self.get_endpoint()
 
         return '%s%s/?sentry_key=%s' % (
             endpoint,
@@ -201,6 +196,23 @@ class ProjectKey(Model):
             self.public_key,
         )
 
+    @property
+    def js_sdk_loader_cdn_url(self):
+        if settings.JS_SDK_LOADER_CDN_URL:
+            return '%s%s.min.js' % (settings.JS_SDK_LOADER_CDN_URL, self.public_key)
+        else:
+            endpoint = self.get_endpoint()
+            return '%s%s' % (
+                endpoint,
+                reverse('sentry-js-sdk-loader', args=[self.public_key, '.min'])
+            )
+
+    def get_endpoint(self):
+        endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT
+        if not endpoint:
+            endpoint = options.get('system.url-prefix')
+        return endpoint
+
     def get_allowed_origins(self):
         from sentry.utils.http import get_origins
         return get_origins(self.project)

+ 0 - 7
src/sentry/relay/config.py

@@ -5,10 +5,7 @@ import uuid
 from datetime import datetime
 from pytz import utc
 
-from django.core.urlresolvers import reverse
-
 from sentry.models import ProjectStatus, ProjectKey
-from sentry.utils.http import absolute_uri
 
 
 class Config(object):
@@ -51,7 +48,3 @@ class Config(object):
 
     def is_project_enabled(self):
         return self.project.status != ProjectStatus.VISIBLE
-
-    def get_cdn_url(self, project_key):
-        """Return the url to the js cdn file for a specific project key"""
-        return absolute_uri(reverse('sentry-relay-cdn-loader', args=[project_key.public_key]))

+ 7 - 17
src/sentry/static/sentry/app/views/settings/project/projectKeys/projectKeyDetails.jsx

@@ -292,7 +292,7 @@ const KeySettings = createReactClass({
     data: SentryTypes.ProjectKey.isRequired,
     onRemove: PropTypes.func.isRequired,
     rateLimitsEnabled: PropTypes.bool,
-    relayEnabled: PropTypes.bool,
+    jsSdkLoaderEnabled: PropTypes.bool,
   },
 
   mixins: [ApiMixin],
@@ -332,7 +332,7 @@ const KeySettings = createReactClass({
       access,
       data,
       rateLimitsEnabled,
-      relayEnabled,
+      jsSdkLoaderEnabled,
       organization,
       project,
     } = this.props;
@@ -379,7 +379,7 @@ const KeySettings = createReactClass({
           hooksDisabled={this.state.hooksDisabled}
         />
 
-        {relayEnabled && (
+        {jsSdkLoaderEnabled && (
           <Form
             saveOnBlur
             allowUndo
@@ -390,23 +390,13 @@ const KeySettings = createReactClass({
             <Panel>
               <PanelHeader>{t('CDN')}</PanelHeader>
               <PanelBody>
-                <TextField
-                  name="jsSdkUrl"
-                  help={t(
-                    'Change this to the URL of the SDK of your choice. By default this is the latest SDK version. If you set an URL here you need to update it manually.'
-                  )}
-                  label={t('Url of SDK to be loaded')}
-                  placeholder={t('Leave empty to use default')}
-                  required={false}
-                />
-
                 <Field
                   help={t('Copy this into your website and you are good to go')}
                   inline={false}
                   flexibleControlStateSize
                 >
-                  <TextCopyInput>{`<script src='${data.relay
-                    .url}'></script>`}</TextCopyInput>
+                  <TextCopyInput>{`<script src='${data.dsn
+                    .cdn}'></script>`}</TextCopyInput>
                 </Field>
               </PanelBody>
             </Panel>
@@ -491,7 +481,7 @@ export default class ProjectKeyDetails extends AsyncView {
     let features = new Set(project.features);
     let hasRateLimitsEnabled = features.has('rate-limits');
     let orgFeatures = new Set(organization.features);
-    let hasRelayEnabled = orgFeatures.has('relay');
+    let hasjsSdkLoaderEnabled = orgFeatures.has('relay');
 
     return (
       <div className="ref-key-details">
@@ -505,7 +495,7 @@ export default class ProjectKeyDetails extends AsyncView {
           access={access}
           params={params}
           rateLimitsEnabled={hasRateLimitsEnabled}
-          relayEnabled={hasRelayEnabled}
+          jsSdkLoaderEnabled={hasjsSdkLoaderEnabled}
           data={data}
           onRemove={this.handleRemove}
         />

+ 1 - 0
src/sentry/templates/sentry/js-sdk-loader-noop.js.tmpl

@@ -0,0 +1 @@
+// Please set JS_SDK_LOADER_DEFAULT_SDK_URL in your config to point to a valid js file

+ 74 - 0
src/sentry/templates/sentry/js-sdk-loader.js.tmpl

@@ -0,0 +1,74 @@
+{% load sentry_helpers %}
+// Sentry Loader
+(function(_window, _document, _script, _onerror, _onunhandledrejection) {
+  // Create a namespace and attach function that will store captured exception
+  // Because functions are also objects, we can attach the queue itself straight to it and save some bytes
+  var queue = function(exception) {
+    queue.data.push(exception);
+  };
+  queue.data = [];
+
+  // Store reference to the old `onerror` handler and override it with our own function
+  // that will just push exceptions to the queue and call through old handler if we found one
+  var _oldOnerror = _window[_onerror];
+  _window[_onerror] = function(message, source, lineno, colno, exception) {
+    // Use keys as "data type" to save some characters"
+    queue({
+      e: [].slice.call(arguments)
+    });
+
+    if (_oldOnerror) _oldOnerror.apply(_window, arguments);
+  };
+
+  // Do the same store/queue/call operations for `onunhandledrejection` event
+  var _oldOnunhandledrejection = _window[_onunhandledrejection];
+  _window[_onunhandledrejection] = function(exception) {
+    queue({
+      p: exception.reason
+    });
+    if (_oldOnunhandledrejection) _oldOnunhandledrejection.apply(_window, arguments);
+  };
+
+  // Create a `script` tag with provided SDK `url` and attach it just before the first, already existing `script` tag
+  // Scripts that are dynamically created and added to the document are async by default,
+  // they don't block rendering and execute as soon as they download, meaning they could
+  // come out in the wrong order. Because of that we don't need async=1 as GA does.
+  // it was probably(?) a legacy behavior that they left to not modify few years old snippet
+  // https://www.html5rocks.com/en/tutorials/speed/script-loading/
+  var _currentScriptTag = _document.getElementsByTagName(_script)[0];
+  var _newScriptTag = _document.createElement(_script);
+  _newScriptTag.src = '{{ jsSdkUrl|safe }}';
+  _newScriptTag.crossorigin = 'anonymous';
+
+  // Once our SDK is loaded
+  _newScriptTag.addEventListener('load', function() {
+    try {
+      // Restore onerror/onunhandledrejection handlers
+      _window[_onerror] = _oldOnerror;
+      _window[_onunhandledrejection] = _oldOnunhandledrejection;
+
+      var data = queue.data;
+      var SDK = _window.Sentry;
+      // Configure it using provided DSN and config object
+      SDK.init({{ config|to_json|safe }});
+      // Because we installed the SDK, at this point we have an access to TraceKit's handler,
+      // which can take care of browser differences (eg. missing exception argument in onerror)
+      var tracekitErrorHandler = _window[_onerror];
+
+      // And capture all previously caught exceptions
+      if (data.length) {
+        for (var i = 0; i < data.length; i++) {
+          if (data[i].e) {
+            tracekitErrorHandler(data[i].e);
+          } else if (data[i].p) {
+            SDK.captureException(data[i].p);
+          }
+        }
+      }
+    } catch (o_O) {
+      console.log(o_O);
+    }
+  });
+
+  _currentScriptTag.parentNode.insertBefore(_newScriptTag, _currentScriptTag);
+})(window, document, 'script', 'onerror', 'onunhandledrejection');

+ 2 - 0
src/sentry/templates/sentry/js-sdk-loader.min.js.tmpl

@@ -0,0 +1,2 @@
+{% load sentry_helpers %}(function(c,a,g,e,h){var f=function(b){f.data.push(b)};f.data=[];var k=c[e];c[e]=function(b,a,e,d,h){f({e:[].slice.call(arguments)});k&&k.apply(c,arguments)};var l=c[h];c[h]=function(b){f({p:b.reason});l&&l.apply(c,arguments)};var m=a.getElementsByTagName(g)[0];a=a.createElement(g);a.src="{{ jsSdkUrl|safe }}";a.crossorigin="anonymous";a.addEventListener("load",function(){try{c[e]=k;c[h]=l;var b=f.data,a=c.Sentry;a.init({{ config|to_json|safe }});
+var g=c[e];if(b.length)for(var d=0;d<b.length;d++)b[d].e?g(b[d].e):b[d].p&&a.captureException(b[d].p)}catch(n){console.log(n)}});m.parentNode.insertBefore(a,m)})(window,document,"script","onerror","onunhandledrejection");

Some files were not shown because too many files changed in this diff