diff --git a/src/sentry/api/endpoints/project_docs_platform.py b/src/sentry/api/endpoints/project_docs_platform.py index 1636e4daf28d09..07d906be77ffb5 100644 --- a/src/sentry/api/endpoints/project_docs_platform.py +++ b/src/sentry/api/endpoints/project_docs_platform.py @@ -24,10 +24,6 @@ def replace_keys(html, project_key): "___RELAY_CDN_URL___", absolute_uri(reverse("sentry-js-sdk-loader", args=[project_key.public_key])), ) - html = html.replace( - "___RELAY_DYNAMIC_CDN_URL___", - absolute_uri(reverse("sentry-js-sdk-dynamic-loader", args=[project_key.public_key])), - ) # If we actually render this in the main UI we can also provide # extra information about the project (org slug and project slug) diff --git a/src/sentry/api/serializers/models/project_key.py b/src/sentry/api/serializers/models/project_key.py index d33abc8f960422..ebc3a7adf3ab1f 100644 --- a/src/sentry/api/serializers/models/project_key.py +++ b/src/sentry/api/serializers/models/project_key.py @@ -31,7 +31,6 @@ def serialize(self, obj, attrs, user): "minidump": obj.minidump_endpoint, "unreal": obj.unreal_endpoint, "cdn": obj.js_sdk_loader_cdn_url, - "dynamicCdn": obj.js_sdk_dynamic_loader_cdn_url, }, "browserSdkVersion": get_selected_browser_sdk_version(obj), "browserSdk": {"choices": get_browser_sdk_version_choices()}, diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index d185ccc9924c64..a1009e159f8ce7 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -2552,8 +2552,6 @@ def build_cdc_postgres_init_db_volume(settings): 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 = "" -# Version of the Dynamic Loader - Used in header Surrogate-Key sdk/JS_SDK_DYNAMIC_LOADER_SDK_VERSION -JS_SDK_DYNAMIC_LOADER_SDK_VERSION = "" # This should be the url pointing to the JS SDK. It may contain up to two "%s". # The first "%s" will be replaced with the SDK version, the second one is used # to inject a bundle modifier in the JS SDK CDN loader. e.g: diff --git a/src/sentry/models/projectkey.py b/src/sentry/models/projectkey.py index b76c733ba07e20..2411144a2e2abc 100644 --- a/src/sentry/models/projectkey.py +++ b/src/sentry/models/projectkey.py @@ -237,17 +237,6 @@ def js_sdk_loader_cdn_url(self) -> str: reverse("sentry-js-sdk-loader", args=[self.public_key, ".min"]), ) - @property - def js_sdk_dynamic_loader_cdn_url(self) -> str: - if settings.JS_SDK_LOADER_CDN_URL: - return f"{settings.JS_SDK_LOADER_CDN_URL}dynamic/{self.public_key}.min.js" - else: - endpoint = self.get_endpoint() - return "{}{}".format( - endpoint, - reverse("sentry-js-sdk-dynamic-loader", args=[self.public_key, ".min"]), - ) - def get_endpoint(self, public=True): if public: endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT diff --git a/src/sentry/templates/sentry/js-sdk-loader.js.tmpl b/src/sentry/templates/sentry/js-sdk-loader.js.tmpl index 4eb3adbfc0847e..aee842cba6b899 100644 --- a/src/sentry/templates/sentry/js-sdk-loader.js.tmpl +++ b/src/sentry/templates/sentry/js-sdk-loader.js.tmpl @@ -7,14 +7,19 @@ _namespace, _publicKey, _sdkBundleUrl, - _config + _config, + _lazy ) { - var lazy = true; + var lazy = _lazy; var forceLoad = false; for (var i = 0; i < document.scripts.length; i++) { if (document.scripts[i].src.indexOf(_publicKey) > -1) { - lazy = !(document.scripts[i].getAttribute('data-lazy') === 'no'); + // If lazy was set to true above, we need to check if the user has set data-lazy="no" + // to confirm that we should lazy load the CDN bundle + if (lazy && document.scripts[i].getAttribute('data-lazy') === 'no') { + lazy = false; + } break; } } @@ -213,4 +218,4 @@ injectSdk(onLoadCallbacks); }); } -})(window, document, 'script', 'onerror', 'onunhandledrejection', 'Sentry', '{{ publicKey|safe }}', '{{ jsSdkUrl|safe }}', {{ config|to_json|safe }}); +})(window, document, 'script', 'onerror', 'onunhandledrejection', 'Sentry', '{{ publicKey|safe }}', '{{ jsSdkUrl|safe }}', {{ config|to_json|safe }}, {{ isLazy|safe|lower }}); diff --git a/src/sentry/templates/sentry/js-sdk-loader.min.js.tmpl b/src/sentry/templates/sentry/js-sdk-loader.min.js.tmpl index 7b43f9f8b7c6ba..39b0f855f22bea 100644 --- a/src/sentry/templates/sentry/js-sdk-loader.min.js.tmpl +++ b/src/sentry/templates/sentry/js-sdk-loader.min.js.tmpl @@ -1,4 +1,4 @@ -{% load sentry_helpers %}(function(c,w,C,p,q,k,D,E,x){function r(a){if(!y){y=!0;var f=w.scripts[0],d=w.createElement(C);d.src=E;d.crossOrigin="anonymous";d.addEventListener("load",function(){try{c[p]=u;c[q]=v;c.SENTRY_SDK_SOURCE="loader";var b=c[k],g=b.init;b.init=function(e){for(var h in e)Object.prototype.hasOwnProperty.call(e,h)&&(x[h]=e[h]);g(x)};F(a,b)}catch(e){console.error(e)}});f.parentNode.insertBefore(d,f)}}function F(a,f){try{for(var d=l.data,b=0;b None: - pass - - def _get_context(self, key: ProjectKey) -> Tuple[LoaderContext, Optional[str], Optional[str]]: - """Sets context information needed to render the loader""" - if not key: - return ({}, None, None) - - sdk_version = get_browser_sdk_version(key) - - bundle_kind_modifier = self._get_bundle_kind_modifier(key) - - sdk_url = "" - try: - sdk_url = settings.JS_SDK_LOADER_DEFAULT_SDK_URL % (sdk_version, bundle_kind_modifier) - except TypeError: - sdk_url = "" # It fails if it cannot inject the version in the string - - return ( - { - "config": { - "dsn": key.dsn_public, - "jsSdkUrl": sdk_url, - "publicKey": key.public_key, - } - }, - sdk_version, - sdk_url, - ) - - def _get_bundle_kind_modifier(self, key: ProjectKey) -> str: - """Returns a string that is used to modify the bundle name""" - bundle_kind_modifier = "" - - if get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_PERFORMANCE): - bundle_kind_modifier += ".tracing" - - if get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_REPLAY): - bundle_kind_modifier += ".replay" - - # TODO(abhi): Right now this loader only supports returning es6 JS bundles. - # We may want to re-evaluate this. - # if es5 - # bundle_kind_modifier += ".es5" - - if get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_DEBUG): - bundle_kind_modifier += ".debug" - - return bundle_kind_modifier - - def get(self, request: Request, public_key: str, minified: str) -> Response: - """Returns a JS file that dynamically loads the SDK based on project settings""" - key = None - try: - key = ProjectKey.objects.get_from_cache(public_key=public_key) - except ProjectKey.DoesNotExist: - pass - else: - key.project = Project.objects.get_from_cache(id=key.project_id) - - # TODO(abhi): Return more than no-op template - tmpl = "sentry/js-sdk-loader-noop.js.tmpl" - - context, sdk_version, sdk_url = self._get_context(key) - - response = render_to_response(tmpl, context, content_type="text/javascript") - - response["Access-Control-Allow-Origin"] = "*" - response["Cross-Origin-Resource-Policy"] = "cross-origin" - response["Cache-Control"] = CACHE_CONTROL - - return response diff --git a/src/sentry/web/frontend/js_sdk_loader.py b/src/sentry/web/frontend/js_sdk_loader.py index 7b81b3f895b66b..c5eb74c4e4dec0 100644 --- a/src/sentry/web/frontend/js_sdk_loader.py +++ b/src/sentry/web/frontend/js_sdk_loader.py @@ -1,4 +1,5 @@ import time +from typing import Optional, Tuple, TypedDict from django.conf import settings from packaging.version import Version @@ -6,6 +7,7 @@ from rest_framework.response import Response from sentry.loader.browsersdkversion import get_browser_sdk_version +from sentry.loader.dynamic_sdk_options import DynamicSdkLoaderOption, get_dynamic_sdk_loader_option from sentry.models import Project, ProjectKey from sentry.utils import metrics from sentry.web.frontend.base import BaseView @@ -16,6 +18,17 @@ ) +class SdkConfig(TypedDict): + dsn: str + + +class LoaderContext(TypedDict): + config: SdkConfig + jsSdkUrl: Optional[str] + publicKey: Optional[str] + isLazy: bool + + class JavaScriptSdkLoader(BaseView): auth_required = False @@ -25,18 +38,58 @@ class JavaScriptSdkLoader(BaseView): def determine_active_organization(self, request: Request, organization_slug=None) -> None: pass - def _get_context(self, key): + def _get_bundle_kind_modifier(self, key: ProjectKey, sdk_version: str) -> Tuple[str, bool]: + """Returns a string that is used to modify the bundle name""" + + is_v7_sdk = sdk_version >= Version("7.0.0") + + is_lazy = True + bundle_kind_modifier = "" + + # The order in which these modifiers are added is important, as the + # bundle name is built up from left to right. + # https://docs.sentry.io/platforms/javascript/install/cdn/ + + if get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_PERFORMANCE): + bundle_kind_modifier += ".tracing" + is_lazy = False + + has_replay = get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_REPLAY) + + # If the project does not have a v7 sdk set, we cannot load the replay bundle. + if is_v7_sdk and has_replay: + bundle_kind_modifier += ".replay" + is_lazy = False + + # From JavaScript SDK version 7 onwards, the default bundle code is ES6, however, in the loader we + # want to provide the ES5 version. This is why we need to modify the requested bundle name here. + # + # If we are loading replay, do not add the es5 modifier, as those bundles are + # ES6 only. + if is_v7_sdk and not has_replay: + bundle_kind_modifier += ".es5" + + if get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_DEBUG): + bundle_kind_modifier += ".debug" + + return bundle_kind_modifier, is_lazy + + def _get_context( + self, key: Optional[ProjectKey] + ) -> Tuple[LoaderContext, Optional[str], Optional[str]]: """Sets context information needed to render the loader""" if not key: - return ({}, None, None) + return ( + { + "isLazy": True, + }, + None, + None, + ) sdk_version = get_browser_sdk_version(key) - # From JavaScript SDK version 7 onwards, the default bundle code is ES6, however, in the loader we - # want to provide the ES5 version. This is why we need to modify the requested bundle name here. - bundle_kind_modifier = "" - if sdk_version >= Version("7.0.0"): - bundle_kind_modifier = ".es5" + bundle_kind_modifier, is_lazy = self._get_bundle_kind_modifier(key, sdk_version) js_sdk_loader_default_sdk_url_template_slot_count = ( settings.JS_SDK_LOADER_DEFAULT_SDK_URL.count("%s") @@ -60,12 +113,13 @@ def _get_context(self, key): "config": {"dsn": key.dsn_public}, "jsSdkUrl": sdk_url, "publicKey": key.public_key, + "isLazy": is_lazy, }, sdk_version, sdk_url, ) - def get(self, request: Request, public_key, minified) -> Response: + def get(self, request: Request, public_key: Optional[str], minified: Optional[str]) -> Response: """Returns a js file that can be integrated into a website""" start_time = time.time() key = None diff --git a/src/sentry/web/urls.py b/src/sentry/web/urls.py index 0354b4882a3b18..6421367f3ed54f 100644 --- a/src/sentry/web/urls.py +++ b/src/sentry/web/urls.py @@ -23,7 +23,6 @@ from sentry.web.frontend.group_tag_export import GroupTagExportView from sentry.web.frontend.home import HomeView from sentry.web.frontend.idp_email_verification import AccountConfirmationView -from sentry.web.frontend.js_sdk_dynamic_loader import JavaScriptSdkDynamicLoader from sentry.web.frontend.js_sdk_loader import JavaScriptSdkLoader from sentry.web.frontend.mailgun_inbound_webhook import MailgunInboundWebhookView from sentry.web.frontend.newest_performance_issue import NewestPerformanceIssueView @@ -117,12 +116,6 @@ JavaScriptSdkLoader.as_view(), name="sentry-js-sdk-loader", ), - # JavaScript SDK Dynamic Loader - url( - r"^js-sdk-loader/dynamic/(?P[^/\.]+)(?:(?P\.min))?\.js$", - JavaScriptSdkDynamicLoader.as_view(), - name="sentry-js-sdk-dynamic-loader", - ), # Versioned API url(r"^api/0/", include("sentry.api.urls")), # Legacy unversioned endpoints diff --git a/tests/sentry/models/test_projectkey.py b/tests/sentry/models/test_projectkey.py index 9c041cf1476244..d303cf49b0960d 100644 --- a/tests/sentry/models/test_projectkey.py +++ b/tests/sentry/models/test_projectkey.py @@ -76,19 +76,6 @@ def test_get_dsn(self): assert key.minidump_endpoint == "http://testserver/api/1/minidump/?sentry_key=abc" assert key.unreal_endpoint == "http://testserver/api/1/unreal/abc/" assert key.js_sdk_loader_cdn_url == "http://testserver/js-sdk-loader/abc.min.js" - assert ( - key.js_sdk_dynamic_loader_cdn_url - == "http://testserver/js-sdk-loader/dynamic/abc.min.js" - ) - - def test_get_js_sdk_dynamic_loader_cdn_url_from_settings(self): - key = self.model(project_id=1, public_key="abc", secret_key="xyz") - - with self.settings(JS_SDK_LOADER_CDN_URL="https://sentry-best-cdn.com/"): - assert ( - key.js_sdk_dynamic_loader_cdn_url - == "https://sentry-best-cdn.com/dynamic/abc.min.js" - ) def test_get_dsn_org_subdomain(self): with self.feature("organizations:org-subdomains"): diff --git a/tests/sentry/web/frontend/test_js_sdk_dynamic_loader.py b/tests/sentry/web/frontend/test_js_sdk_dynamic_loader.py deleted file mode 100644 index 7fcb238f03cb85..00000000000000 --- a/tests/sentry/web/frontend/test_js_sdk_dynamic_loader.py +++ /dev/null @@ -1,41 +0,0 @@ -from functools import cached_property - -import pytest -from django.conf import settings -from django.urls import reverse - -from sentry.testutils import TestCase - - -class JavaScriptSdkLoaderTest(TestCase): - @pytest.fixture(autouse=True) - def set_settings(self): - settings.JS_SDK_LOADER_SDK_VERSION = "0.5.2" - settings.JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js" - - @cached_property - def path(self): - return reverse("sentry-js-sdk-dynamic-loader", args=[self.projectkey.public_key]) - - def test_noop_no_pub_key(self): - resp = self.client.get(reverse("sentry-js-sdk-dynamic-loader", args=["abc"])) - assert resp.status_code == 200 - self.assertTemplateUsed(resp, "sentry/js-sdk-loader-noop.js.tmpl") - - def test_noop(self): - settings.JS_SDK_LOADER_DEFAULT_SDK_URL = "" - resp = self.client.get( - reverse("sentry-js-sdk-dynamic-loader", args=[self.projectkey.public_key]) - ) - assert resp.status_code == 200 - self.assertTemplateUsed(resp, "sentry/js-sdk-loader-noop.js.tmpl") - - def test_absolute_url(self): - assert ( - reverse("sentry-js-sdk-dynamic-loader", args=[self.projectkey.public_key, ".min"]) - in self.projectkey.js_sdk_dynamic_loader_cdn_url - ) - settings.JS_SDK_LOADER_CDN_URL = "https://js.sentry-cdn.com/" - assert ( - "https://js.sentry-cdn.com/dynamic/%s.min.js" % self.projectkey.public_key - ) == self.projectkey.js_sdk_dynamic_loader_cdn_url diff --git a/tests/sentry/web/frontend/test_js_sdk_loader.py b/tests/sentry/web/frontend/test_js_sdk_loader.py index 29468d630987ee..39b3b66fafd161 100644 --- a/tests/sentry/web/frontend/test_js_sdk_loader.py +++ b/tests/sentry/web/frontend/test_js_sdk_loader.py @@ -6,6 +6,7 @@ from django.conf import settings from django.urls import reverse +from sentry.loader.dynamic_sdk_options import DynamicSdkLoaderOption from sentry.testutils import TestCase @@ -104,6 +105,86 @@ def test_greater_than_v7_returns_es5( self.assertTemplateUsed(resp, "sentry/js-sdk-loader.js.tmpl") assert b"/8.3.15/bundle.es5.min.js" in resp.content + @mock.patch("sentry.loader.browsersdkversion.load_version_from_file", return_value=["7.37.0"]) + @mock.patch( + "sentry.loader.browsersdkversion.get_selected_browser_sdk_version", return_value="latest" + ) + def test_bundle_kind_modifiers(self, load_version_from_file, get_selected_browser_sdk_version): + settings.JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js" + settings.JS_SDK_LOADER_SDK_VERSION = "7.32.0" + + for data, expected in [ + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_PERFORMANCE.value: True, + } + }, + b"/7.37.0/bundle.tracing.es5.min.js", + ), + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_DEBUG.value: True, + } + }, + b"/7.37.0/bundle.es5.debug.min.js", + ), + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_REPLAY.value: True, + } + }, + b"/7.37.0/bundle.replay.min.js", + ), + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_PERFORMANCE.value: True, + DynamicSdkLoaderOption.HAS_REPLAY.value: True, + } + }, + b"/7.37.0/bundle.tracing.replay.min.js", + ), + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_REPLAY.value: True, + DynamicSdkLoaderOption.HAS_DEBUG.value: True, + } + }, + b"/7.37.0/bundle.replay.debug.min.js", + ), + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_PERFORMANCE.value: True, + DynamicSdkLoaderOption.HAS_DEBUG.value: True, + } + }, + b"/7.37.0/bundle.tracing.es5.debug.min.js", + ), + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_PERFORMANCE.value: True, + DynamicSdkLoaderOption.HAS_DEBUG.value: True, + DynamicSdkLoaderOption.HAS_REPLAY.value: True, + } + }, + b"/7.37.0/bundle.tracing.replay.debug.min.js", + ), + ]: + self.projectkey.data = data + self.projectkey.save() + resp = self.client.get(self.path) + assert resp.status_code == 200 + self.assertTemplateUsed(resp, "sentry/js-sdk-loader.js.tmpl") + assert expected in resp.content + self.projectkey.data = {} + self.projectkey.save() + @patch("sentry.loader.browsersdkversion.load_version_from_file") def test_headers(self, mock_load_version_from_file): # We want to always load the major version here since otherwise we fall back to