From 6cdedc7fc7a0ec3fdcba3ad1fbe4fff4d04547f4 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 26 Jan 2026 13:49:53 +0100 Subject: [PATCH 1/7] support logs+metric bundle --- src/sentry/loader/dynamic_sdk_options.py | 1 + src/sentry/web/frontend/analytics.py | 1 + src/sentry/web/frontend/js_sdk_loader.py | 34 +++- .../sentry/web/frontend/test_js_sdk_loader.py | 183 ++++++++++++++++++ 4 files changed, 216 insertions(+), 3 deletions(-) diff --git a/src/sentry/loader/dynamic_sdk_options.py b/src/sentry/loader/dynamic_sdk_options.py index 5cff75dea67cd8..b964d8286714eb 100644 --- a/src/sentry/loader/dynamic_sdk_options.py +++ b/src/sentry/loader/dynamic_sdk_options.py @@ -6,6 +6,7 @@ class DynamicSdkLoaderOption(str, Enum): HAS_PERFORMANCE = "hasPerformance" HAS_DEBUG = "hasDebug" HAS_FEEDBACK = "hasFeedback" + HAS_LOGS_AND_METRICS = "hasLogsAndMetrics" def get_dynamic_sdk_loader_option(project_key, option: DynamicSdkLoaderOption, default=False): diff --git a/src/sentry/web/frontend/analytics.py b/src/sentry/web/frontend/analytics.py index 19ba0e503984d5..eb270d9b677e28 100644 --- a/src/sentry/web/frontend/analytics.py +++ b/src/sentry/web/frontend/analytics.py @@ -10,6 +10,7 @@ class JsSdkLoaderRendered(analytics.Event): has_replay: bool has_debug: bool has_feedback: bool + has_logs_and_metrics: bool sdk_version: str | None tmpl: str diff --git a/src/sentry/web/frontend/js_sdk_loader.py b/src/sentry/web/frontend/js_sdk_loader.py index e4947ef9bee63f..038e12e9a2c63d 100644 --- a/src/sentry/web/frontend/js_sdk_loader.py +++ b/src/sentry/web/frontend/js_sdk_loader.py @@ -39,6 +39,7 @@ class LoaderInternalConfig(TypedDict): hasReplay: bool hasDebug: bool hasFeedback: bool + hasLogsAndMetrics: bool class LoaderContext(TypedDict): @@ -63,10 +64,12 @@ def _get_loader_config( "hasReplay": False, "hasDebug": False, "hasFeedback": False, + "hasLogsAndMetrics": False, } is_v7_sdk = sdk_version >= Version("7.0.0") and sdk_version < Version("8.0.0") is_greater_or_equal_v7_sdk = sdk_version >= Version("7.0.0") + is_greater_or_equal_v10_sdk = sdk_version >= Version("10.0.0") is_lazy = True bundle_kind_modifier = "" @@ -74,6 +77,9 @@ def _get_loader_config( has_performance = get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_PERFORMANCE) has_debug = get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_DEBUG) has_feedback = get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_FEEDBACK) + has_logs_and_metrics = get_dynamic_sdk_loader_option( + key, DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS + ) # The order in which these modifiers are added is important, as the # bundle name is built up from left to right. @@ -94,6 +100,22 @@ def _get_loader_config( has_performance = True has_replay = True + # Logs and metrics bundles require SDK >= 10.0.0 and tracing + # Available bundles: bundle.tracing.logs.metrics, bundle.tracing.replay.feedback.logs.metrics + # If logs+metrics is combined with replay or feedback, we must use the full bundle. + logs_metrics_with_other_features = has_logs_and_metrics and (has_replay or has_feedback) + + # When logs+metrics is combined with replay or feedback, we must serve the full bundle + # which includes tracing, replay, feedback, logs, and metrics. Update the flags accordingly. + if is_greater_or_equal_v10_sdk and logs_metrics_with_other_features: + has_performance = True + has_replay = True + has_feedback = True + + # Logs and metrics always require tracing (performance) + if is_greater_or_equal_v10_sdk and has_logs_and_metrics: + has_performance = True + # We depend on fixes in the tracing bundle that are only available in v7 if is_greater_or_equal_v7_sdk and has_performance: bundle_kind_modifier += ".tracing" @@ -108,12 +130,16 @@ def _get_loader_config( bundle_kind_modifier += ".feedback" is_lazy = False + if is_greater_or_equal_v10_sdk and has_logs_and_metrics: + bundle_kind_modifier += ".logs.metrics" + is_lazy = False + # In JavaScript SDK version 7, 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 or feedback, do not add the es5 modifier, as those bundles are - # ES6 only. - if is_v7_sdk and not has_replay and not has_feedback: + # If we are loading replay, feedback, or logs+metrics, do not add the es5 modifier, as those + # bundles are ES6 only. + if is_v7_sdk and not has_replay and not has_feedback and not has_logs_and_metrics: bundle_kind_modifier += ".es5" if has_debug: @@ -126,6 +152,7 @@ def _get_loader_config( "hasReplay": has_replay, "hasDebug": has_debug, "hasFeedback": has_feedback, + "hasLogsAndMetrics": has_logs_and_metrics, } def _get_context( @@ -222,6 +249,7 @@ def get( has_replay=loader_config["hasReplay"], has_debug=loader_config["hasDebug"], has_feedback=loader_config["hasFeedback"], + has_logs_and_metrics=loader_config["hasLogsAndMetrics"], sdk_version=str(sdk_version) if sdk_version else None, tmpl=tmpl, ) diff --git a/tests/sentry/web/frontend/test_js_sdk_loader.py b/tests/sentry/web/frontend/test_js_sdk_loader.py index 458567a5e0efef..516edb93cc53a4 100644 --- a/tests/sentry/web/frontend/test_js_sdk_loader.py +++ b/tests/sentry/web/frontend/test_js_sdk_loader.py @@ -438,3 +438,186 @@ def test_absolute_url(self) -> None: assert ( "https://js.sentry-cdn.com/%s.min.js" % self.projectkey.public_key ) == self.projectkey.js_sdk_loader_cdn_url + + @mock.patch("sentry.loader.browsersdkversion.load_version_from_file", return_value=["10.0.0"]) + @mock.patch( + "sentry.loader.browsersdkversion.get_selected_browser_sdk_version", return_value="10.x" + ) + def test_logs_and_metrics_bundle_modifiers( + self, load_version_from_file: MagicMock, get_selected_browser_sdk_version: MagicMock + ) -> None: + """Test logs and metrics bundles which require SDK >= 10.0.0""" + settings.JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js" + settings.JS_SDK_LOADER_SDK_VERSION = "10.0.0" + + dsn = self.projectkey.get_dsn(public=True) + + for data, expected_bundle, expected_options in [ + # Logs and metrics alone (should include tracing) + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + } + }, + b"/10.0.0/bundle.tracing.logs.metrics.min.js", + {"dsn": dsn, "tracesSampleRate": 1}, + ), + # Logs and metrics with performance (same as above, tracing already included) + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + DynamicSdkLoaderOption.HAS_PERFORMANCE.value: True, + } + }, + b"/10.0.0/bundle.tracing.logs.metrics.min.js", + {"dsn": dsn, "tracesSampleRate": 1}, + ), + # Logs and metrics with debug + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + DynamicSdkLoaderOption.HAS_DEBUG.value: True, + } + }, + b"/10.0.0/bundle.tracing.logs.metrics.debug.min.js", + {"dsn": dsn, "tracesSampleRate": 1, "debug": True}, + ), + # Logs and metrics with replay (should use full bundle) + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + DynamicSdkLoaderOption.HAS_REPLAY.value: True, + } + }, + b"/10.0.0/bundle.tracing.replay.feedback.logs.metrics.min.js", + { + "dsn": dsn, + "tracesSampleRate": 1, + "replaysSessionSampleRate": 0.1, + "replaysOnErrorSampleRate": 1, + }, + ), + # Logs and metrics with feedback (should use full bundle) + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + DynamicSdkLoaderOption.HAS_FEEDBACK.value: True, + } + }, + b"/10.0.0/bundle.tracing.replay.feedback.logs.metrics.min.js", + { + "dsn": dsn, + "tracesSampleRate": 1, + "replaysSessionSampleRate": 0.1, + "replaysOnErrorSampleRate": 1, + }, + ), + # Logs and metrics with replay and feedback (full bundle) + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + DynamicSdkLoaderOption.HAS_REPLAY.value: True, + DynamicSdkLoaderOption.HAS_FEEDBACK.value: True, + } + }, + b"/10.0.0/bundle.tracing.replay.feedback.logs.metrics.min.js", + { + "dsn": dsn, + "tracesSampleRate": 1, + "replaysSessionSampleRate": 0.1, + "replaysOnErrorSampleRate": 1, + }, + ), + # Logs and metrics with all features + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + DynamicSdkLoaderOption.HAS_PERFORMANCE.value: True, + DynamicSdkLoaderOption.HAS_REPLAY.value: True, + DynamicSdkLoaderOption.HAS_FEEDBACK.value: True, + DynamicSdkLoaderOption.HAS_DEBUG.value: True, + } + }, + b"/10.0.0/bundle.tracing.replay.feedback.logs.metrics.debug.min.js", + { + "dsn": dsn, + "tracesSampleRate": 1, + "replaysSessionSampleRate": 0.1, + "replaysOnErrorSampleRate": 1, + "debug": True, + }, + ), + ]: + 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_bundle in resp.content + + for key in expected_options: + # Convert to e.g. "option_name": 0.1 + single_option = {key: expected_options[key]} + assert json.dumps(single_option)[1:-1].encode() in resp.content + + self.projectkey.data = {} + self.projectkey.save() + + @mock.patch("sentry.loader.browsersdkversion.load_version_from_file", return_value=["9.99.0"]) + @mock.patch( + "sentry.loader.browsersdkversion.get_selected_browser_sdk_version", return_value="9.x" + ) + def test_logs_and_metrics_not_available_before_v10( + self, load_version_from_file: MagicMock, get_selected_browser_sdk_version: MagicMock + ) -> None: + """Test that logs and metrics are not loaded for SDK < 10.0.0""" + settings.JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js" + settings.JS_SDK_LOADER_SDK_VERSION = "9.99.0" + + self.projectkey.data = { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + } + } + self.projectkey.save() + + resp = self.client.get(self.path) + assert resp.status_code == 200 + self.assertTemplateUsed(resp, "sentry/js-sdk-loader.js.tmpl") + # Should not include logs.metrics in the bundle name + assert b"logs.metrics" not in resp.content + # Should fall back to base bundle without logs and metrics + assert b"/9.99.0/bundle.min.js" in resp.content + + @mock.patch("sentry.loader.browsersdkversion.load_version_from_file", return_value=["8.10.0"]) + @mock.patch( + "sentry.loader.browsersdkversion.get_selected_browser_sdk_version", return_value="8.x" + ) + def test_logs_and_metrics_not_available_on_v8( + self, load_version_from_file: MagicMock, get_selected_browser_sdk_version: MagicMock + ) -> None: + """Test that logs and metrics are not loaded for v8 SDK""" + settings.JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js" + settings.JS_SDK_LOADER_SDK_VERSION = "8.10.0" + + self.projectkey.data = { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + } + } + self.projectkey.save() + + resp = self.client.get(self.path) + assert resp.status_code == 200 + self.assertTemplateUsed(resp, "sentry/js-sdk-loader.js.tmpl") + # Should not include logs.metrics in the bundle name for v8 + assert b"logs.metrics" not in resp.content + # Should fall back to base bundle without logs and metrics + assert b"/8.10.0/bundle.min.js" in resp.content From 5e16f2bfface3a00e1d79ee89dc31803930780ee Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 26 Jan 2026 14:28:26 +0100 Subject: [PATCH 2/7] add config flag --- src/sentry/web/frontend/js_sdk_loader.py | 4 ++++ tests/sentry/web/frontend/test_js_sdk_loader.py | 13 ++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/sentry/web/frontend/js_sdk_loader.py b/src/sentry/web/frontend/js_sdk_loader.py index a04cc447d93028..8c77d9a4bd3474 100644 --- a/src/sentry/web/frontend/js_sdk_loader.py +++ b/src/sentry/web/frontend/js_sdk_loader.py @@ -31,6 +31,7 @@ class SdkConfig(TypedDict): replaysOnErrorSampleRate: NotRequired[float] debug: NotRequired[bool] autoInjectFeedback: NotRequired[bool] + enableLogs: NotRequired[bool] class LoaderInternalConfig(TypedDict): @@ -204,6 +205,9 @@ def _get_context( if loader_config["hasFeedback"]: config["autoInjectFeedback"] = True + if loader_config["hasLogsAndMetrics"]: + config["enableLogs"] = True + return ( { "config": config, diff --git a/tests/sentry/web/frontend/test_js_sdk_loader.py b/tests/sentry/web/frontend/test_js_sdk_loader.py index cfe281a8967897..77dee1e3b07f9d 100644 --- a/tests/sentry/web/frontend/test_js_sdk_loader.py +++ b/tests/sentry/web/frontend/test_js_sdk_loader.py @@ -465,7 +465,7 @@ def test_logs_and_metrics_bundle_modifiers( } }, b"/10.0.0/bundle.tracing.logs.metrics.min.js", - {"dsn": dsn, "tracesSampleRate": 1}, + {"dsn": dsn, "tracesSampleRate": 1, "enableLogs": True}, ), # Logs and metrics with performance (same as above, tracing already included) ( @@ -476,7 +476,7 @@ def test_logs_and_metrics_bundle_modifiers( } }, b"/10.0.0/bundle.tracing.logs.metrics.min.js", - {"dsn": dsn, "tracesSampleRate": 1}, + {"dsn": dsn, "tracesSampleRate": 1, "enableLogs": True}, ), # Logs and metrics with debug ( @@ -487,7 +487,7 @@ def test_logs_and_metrics_bundle_modifiers( } }, b"/10.0.0/bundle.tracing.logs.metrics.debug.min.js", - {"dsn": dsn, "tracesSampleRate": 1, "debug": True}, + {"dsn": dsn, "tracesSampleRate": 1, "debug": True, "enableLogs": True}, ), # Logs and metrics with replay (should use full bundle) ( @@ -503,6 +503,7 @@ def test_logs_and_metrics_bundle_modifiers( "tracesSampleRate": 1, "replaysSessionSampleRate": 0.1, "replaysOnErrorSampleRate": 1, + "enableLogs": True, }, ), # Logs and metrics with feedback (should use full bundle) @@ -519,6 +520,8 @@ def test_logs_and_metrics_bundle_modifiers( "tracesSampleRate": 1, "replaysSessionSampleRate": 0.1, "replaysOnErrorSampleRate": 1, + "autoInjectFeedback": True, + "enableLogs": True, }, ), # Logs and metrics with replay and feedback (full bundle) @@ -536,6 +539,8 @@ def test_logs_and_metrics_bundle_modifiers( "tracesSampleRate": 1, "replaysSessionSampleRate": 0.1, "replaysOnErrorSampleRate": 1, + "autoInjectFeedback": True, + "enableLogs": True, }, ), # Logs and metrics with all features @@ -556,6 +561,8 @@ def test_logs_and_metrics_bundle_modifiers( "replaysSessionSampleRate": 0.1, "replaysOnErrorSampleRate": 1, "debug": True, + "autoInjectFeedback": True, + "enableLogs": True, }, ), ]: From 6afca4fa0dde8df00e67df9458d634cdbe6516b6 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 26 Jan 2026 14:57:47 +0100 Subject: [PATCH 3/7] update serializer --- src/sentry/api/serializers/rest_framework/project_key.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sentry/api/serializers/rest_framework/project_key.py b/src/sentry/api/serializers/rest_framework/project_key.py index 77e2d390d2c351..c454b4b67d9fb1 100644 --- a/src/sentry/api/serializers/rest_framework/project_key.py +++ b/src/sentry/api/serializers/rest_framework/project_key.py @@ -32,13 +32,15 @@ class DynamicSdkLoaderOptionSerializer(serializers.Serializer): - `Debug Bundles & Logging` - `Session Replay` - Note that the loader will load the ES6 bundle instead of the ES5 bundle. - `User Feedback` - Note that the loader will load the ES6 bundle instead of the ES5 bundle. + - `Logs and Metrics` - Note that the loader will load the ES6 bundle instead of the ES5 bundle. Requires SDK >= 10.0.0. ```json { "dynamicSdkLoaderOptions": { "hasReplay": true, "hasPerformance": true, "hasDebug": true, - "hasFeedback": true + "hasFeedback": true, + "hasLogsAndMetrics": true } } ``` @@ -48,6 +50,7 @@ class DynamicSdkLoaderOptionSerializer(serializers.Serializer): hasPerformance = serializers.BooleanField(required=False) hasDebug = serializers.BooleanField(required=False) hasFeedback = serializers.BooleanField(required=False) + hasLogsAndMetrics = serializers.BooleanField(required=False) def to_internal_value(self, data): # Drop any fields that are not specified as a `DynamicSdkLoaderOption`. From c0907dc05f75d3e31031412667fa484b9a11e80d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 26 Jan 2026 15:53:34 +0100 Subject: [PATCH 4/7] fix feedback enabling --- src/sentry/web/frontend/js_sdk_loader.py | 25 +++++++++++++++---- .../sentry/web/frontend/test_js_sdk_loader.py | 8 +++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/sentry/web/frontend/js_sdk_loader.py b/src/sentry/web/frontend/js_sdk_loader.py index 8c77d9a4bd3474..f9c569d8ec21ac 100644 --- a/src/sentry/web/frontend/js_sdk_loader.py +++ b/src/sentry/web/frontend/js_sdk_loader.py @@ -42,6 +42,8 @@ class LoaderInternalConfig(TypedDict): hasDebug: bool hasFeedback: bool hasLogsAndMetrics: bool + userEnabledReplay: bool + userEnabledFeedback: bool class LoaderContext(TypedDict): @@ -67,6 +69,8 @@ def _get_loader_config( "hasDebug": False, "hasFeedback": False, "hasLogsAndMetrics": False, + "userEnabledReplay": False, + "userEnabledFeedback": False, } is_v7_sdk = sdk_version >= Version("7.0.0") and sdk_version < Version("8.0.0") @@ -83,6 +87,11 @@ def _get_loader_config( key, DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS ) + # Store the user's original preferences before we modify them for bundle selection. + # We only want to enable features that the user explicitly requested. + user_enabled_replay = has_replay + user_enabled_feedback = has_feedback + # 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/ @@ -97,7 +106,7 @@ def _get_loader_config( feedback_with_other_features = has_feedback and (has_performance or has_replay) # When feedback is combined with tracing or replay, we must serve the full bundle - # which includes all three features. Update the flags accordingly. + # which includes all three features. Update the flags accordingly for bundle selection. if is_greater_or_equal_v7_sdk and feedback_with_other_features: has_performance = True has_replay = True @@ -108,7 +117,8 @@ def _get_loader_config( logs_metrics_with_other_features = has_logs_and_metrics and (has_replay or has_feedback) # When logs+metrics is combined with replay or feedback, we must serve the full bundle - # which includes tracing, replay, feedback, logs, and metrics. Update the flags accordingly. + # which includes tracing, replay, feedback, logs, and metrics. Update the flags accordingly + # for bundle selection, but we won't enable features the user didn't explicitly request. if is_greater_or_equal_v10_sdk and logs_metrics_with_other_features: has_performance = True has_replay = True @@ -155,6 +165,8 @@ def _get_loader_config( "hasDebug": has_debug, "hasFeedback": has_feedback, "hasLogsAndMetrics": has_logs_and_metrics, + "userEnabledReplay": user_enabled_replay, + "userEnabledFeedback": user_enabled_feedback, } def _get_context( @@ -197,12 +209,15 @@ def _get_context( if loader_config["hasPerformance"]: config["tracesSampleRate"] = 1 - if loader_config["hasReplay"]: + # Only enable replay config if the user explicitly enabled it, not just because + # we're loading a bundle that includes replay for other features. + if loader_config["userEnabledReplay"]: config["replaysSessionSampleRate"] = 0.1 config["replaysOnErrorSampleRate"] = 1 - # Although this is not a top-level SDK option we pass this flag so we can auto-add the integration in the loader template later - if loader_config["hasFeedback"]: + # Only auto-inject feedback if the user explicitly enabled it, not just because + # we're loading a bundle that includes feedback for other features. + if loader_config["userEnabledFeedback"]: config["autoInjectFeedback"] = True if loader_config["hasLogsAndMetrics"]: diff --git a/tests/sentry/web/frontend/test_js_sdk_loader.py b/tests/sentry/web/frontend/test_js_sdk_loader.py index 77dee1e3b07f9d..53434c407b2e55 100644 --- a/tests/sentry/web/frontend/test_js_sdk_loader.py +++ b/tests/sentry/web/frontend/test_js_sdk_loader.py @@ -328,6 +328,8 @@ def test_bundle_kind_modifiers( ), # Note: There is no bundle.tracing.feedback or bundle.replay.feedback. # When feedback is combined with tracing or replay, we serve the full bundle. + # Even though the full bundle includes replay, we should NOT enable replay config + # because the user didn't explicitly enable replay. ( { "dynamicSdkLoaderOptions": { @@ -339,8 +341,6 @@ def test_bundle_kind_modifiers( { "dsn": dsn, "tracesSampleRate": 1, - "replaysSessionSampleRate": 0.1, - "replaysOnErrorSampleRate": 1, "autoInjectFeedback": True, }, ), @@ -507,6 +507,8 @@ def test_logs_and_metrics_bundle_modifiers( }, ), # Logs and metrics with feedback (should use full bundle) + # Note: Even though the full bundle includes replay, we should NOT enable replay config + # because the user didn't explicitly enable replay. ( { "dynamicSdkLoaderOptions": { @@ -518,8 +520,6 @@ def test_logs_and_metrics_bundle_modifiers( { "dsn": dsn, "tracesSampleRate": 1, - "replaysSessionSampleRate": 0.1, - "replaysOnErrorSampleRate": 1, "autoInjectFeedback": True, "enableLogs": True, }, From 0a17f9e07ad5c3e6e9321a7066b3e4f31562166b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 27 Jan 2026 10:07:07 +0100 Subject: [PATCH 5/7] version gate logs flag --- src/sentry/web/frontend/js_sdk_loader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sentry/web/frontend/js_sdk_loader.py b/src/sentry/web/frontend/js_sdk_loader.py index f9c569d8ec21ac..eea20b71b1bbd1 100644 --- a/src/sentry/web/frontend/js_sdk_loader.py +++ b/src/sentry/web/frontend/js_sdk_loader.py @@ -145,6 +145,9 @@ def _get_loader_config( if is_greater_or_equal_v10_sdk and has_logs_and_metrics: bundle_kind_modifier += ".logs.metrics" is_lazy = False + else: + # If SDK < 10.0.0, disable logs+metrics feature even if user requested it + has_logs_and_metrics = False # In JavaScript SDK version 7, 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. From ae84380b1863f1877d45d3dc39972ed967b1364d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 27 Jan 2026 10:20:44 +0100 Subject: [PATCH 6/7] fix es5 check --- src/sentry/web/frontend/js_sdk_loader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentry/web/frontend/js_sdk_loader.py b/src/sentry/web/frontend/js_sdk_loader.py index eea20b71b1bbd1..4426cda57b7b93 100644 --- a/src/sentry/web/frontend/js_sdk_loader.py +++ b/src/sentry/web/frontend/js_sdk_loader.py @@ -152,9 +152,9 @@ def _get_loader_config( # In JavaScript SDK version 7, 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, feedback, or logs+metrics, do not add the es5 modifier, as those - # bundles are ES6 only. - if is_v7_sdk and not has_replay and not has_feedback and not has_logs_and_metrics: + # If we are loading replay or feedback, do not add the es5 modifier, as those bundles are ES6 only. + # Note: logs+metrics bundles don't exist for v7 (they require v10+) + if is_v7_sdk and not has_replay and not has_feedback: bundle_kind_modifier += ".es5" if has_debug: From e5aa2c3af32323705a9cf5d25699051b31c0f2a5 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 30 Jan 2026 14:39:14 +0100 Subject: [PATCH 7/7] update for usage with new bundles --- src/sentry/web/frontend/js_sdk_loader.py | 71 +++++++++++------- .../sentry/web/frontend/test_js_sdk_loader.py | 75 ++++++++++++++----- 2 files changed, 98 insertions(+), 48 deletions(-) diff --git a/src/sentry/web/frontend/js_sdk_loader.py b/src/sentry/web/frontend/js_sdk_loader.py index 4426cda57b7b93..7db86a5654cd57 100644 --- a/src/sentry/web/frontend/js_sdk_loader.py +++ b/src/sentry/web/frontend/js_sdk_loader.py @@ -42,8 +42,10 @@ class LoaderInternalConfig(TypedDict): hasDebug: bool hasFeedback: bool hasLogsAndMetrics: bool + userEnabledPerformance: bool userEnabledReplay: bool userEnabledFeedback: bool + userEnabledLogsAndMetrics: bool class LoaderContext(TypedDict): @@ -69,8 +71,10 @@ def _get_loader_config( "hasDebug": False, "hasFeedback": False, "hasLogsAndMetrics": False, + "userEnabledPerformance": False, "userEnabledReplay": False, "userEnabledFeedback": False, + "userEnabledLogsAndMetrics": False, } is_v7_sdk = sdk_version >= Version("7.0.0") and sdk_version < Version("8.0.0") @@ -89,44 +93,52 @@ def _get_loader_config( # Store the user's original preferences before we modify them for bundle selection. # We only want to enable features that the user explicitly requested. + user_enabled_performance = has_performance user_enabled_replay = has_replay user_enabled_feedback = has_feedback + user_enabled_logs_and_metrics = has_logs_and_metrics # 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/ - # Available bundles: bundle, bundle.tracing, bundle.replay, bundle.feedback, - # bundle.tracing.replay, bundle.tracing.replay.feedback - # Note: There is NO bundle.tracing.feedback or bundle.replay.feedback. - # If feedback is combined with tracing or replay, we must use the full bundle. + # Available bundles: + # - bundle (base) + # - bundle.feedback + # - bundle.logs.metrics + # - bundle.replay + # - bundle.replay.feedback + # - bundle.replay.logs.metrics + # - bundle.tracing + # - bundle.tracing.logs.metrics + # - bundle.tracing.replay + # - bundle.tracing.replay.feedback + # - bundle.tracing.replay.feedback.logs.metrics + # - bundle.tracing.replay.logs.metrics + # + # Note: There is NO bundle.tracing.feedback (tracing + feedback without replay). + # If feedback is combined with tracing (without replay), we must use the full bundle. + # + # Note: There is NO bundle.feedback.logs.metrics, bundle.tracing.feedback.logs.metrics, + # or bundle.replay.feedback.logs.metrics. If feedback is combined with logs+metrics, + # we must use the full bundle (tracing.replay.feedback.logs.metrics). # Feedback bundles require SDK >= 7.85.0, but the frontend only allows selecting # major versions (7.x, 8.x), which resolve to versions that support feedback. - feedback_with_other_features = has_feedback and (has_performance or has_replay) - # When feedback is combined with tracing or replay, we must serve the full bundle - # which includes all three features. Update the flags accordingly for bundle selection. - if is_greater_or_equal_v7_sdk and feedback_with_other_features: - has_performance = True + # When feedback is combined with tracing (but not replay), we must serve the full bundle + # which includes tracing, replay, and feedback. Update the flags accordingly. + feedback_with_tracing_no_replay = has_feedback and has_performance and not has_replay + if is_greater_or_equal_v7_sdk and feedback_with_tracing_no_replay: has_replay = True - # Logs and metrics bundles require SDK >= 10.0.0 and tracing - # Available bundles: bundle.tracing.logs.metrics, bundle.tracing.replay.feedback.logs.metrics - # If logs+metrics is combined with replay or feedback, we must use the full bundle. - logs_metrics_with_other_features = has_logs_and_metrics and (has_replay or has_feedback) - - # When logs+metrics is combined with replay or feedback, we must serve the full bundle - # which includes tracing, replay, feedback, logs, and metrics. Update the flags accordingly - # for bundle selection, but we won't enable features the user didn't explicitly request. - if is_greater_or_equal_v10_sdk and logs_metrics_with_other_features: + # Logs and metrics bundles require SDK >= 10.0.0. + # When logs+metrics is combined with feedback, we must serve the full bundle + # (tracing.replay.feedback.logs.metrics) because there's no feedback.logs.metrics bundle. + logs_metrics_with_feedback = has_logs_and_metrics and has_feedback + if is_greater_or_equal_v10_sdk and logs_metrics_with_feedback: has_performance = True has_replay = True - has_feedback = True - - # Logs and metrics always require tracing (performance) - if is_greater_or_equal_v10_sdk and has_logs_and_metrics: - has_performance = True # We depend on fixes in the tracing bundle that are only available in v7 if is_greater_or_equal_v7_sdk and has_performance: @@ -148,6 +160,7 @@ def _get_loader_config( else: # If SDK < 10.0.0, disable logs+metrics feature even if user requested it has_logs_and_metrics = False + user_enabled_logs_and_metrics = False # In JavaScript SDK version 7, 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. @@ -168,8 +181,10 @@ def _get_loader_config( "hasDebug": has_debug, "hasFeedback": has_feedback, "hasLogsAndMetrics": has_logs_and_metrics, + "userEnabledPerformance": user_enabled_performance, "userEnabledReplay": user_enabled_replay, "userEnabledFeedback": user_enabled_feedback, + "userEnabledLogsAndMetrics": user_enabled_logs_and_metrics, } def _get_context( @@ -209,21 +224,19 @@ def _get_context( if loader_config["hasDebug"]: config["debug"] = True - if loader_config["hasPerformance"]: + # Only enable feature configs if the user explicitly enabled them, not just because + # we're loading a bundle that includes those features for compatibility reasons. + if loader_config["userEnabledPerformance"]: config["tracesSampleRate"] = 1 - # Only enable replay config if the user explicitly enabled it, not just because - # we're loading a bundle that includes replay for other features. if loader_config["userEnabledReplay"]: config["replaysSessionSampleRate"] = 0.1 config["replaysOnErrorSampleRate"] = 1 - # Only auto-inject feedback if the user explicitly enabled it, not just because - # we're loading a bundle that includes feedback for other features. if loader_config["userEnabledFeedback"]: config["autoInjectFeedback"] = True - if loader_config["hasLogsAndMetrics"]: + if loader_config["userEnabledLogsAndMetrics"]: config["enableLogs"] = True return ( diff --git a/tests/sentry/web/frontend/test_js_sdk_loader.py b/tests/sentry/web/frontend/test_js_sdk_loader.py index 53434c407b2e55..ccf88b8e35d620 100644 --- a/tests/sentry/web/frontend/test_js_sdk_loader.py +++ b/tests/sentry/web/frontend/test_js_sdk_loader.py @@ -326,8 +326,8 @@ def test_bundle_kind_modifiers( b"/7.37.0/bundle.feedback.debug.min.js", {"dsn": dsn, "debug": True, "autoInjectFeedback": True}, ), - # Note: There is no bundle.tracing.feedback or bundle.replay.feedback. - # When feedback is combined with tracing or replay, we serve the full bundle. + # Note: There is no bundle.tracing.feedback. + # When feedback is combined with tracing (but not replay), we serve the full bundle. # Even though the full bundle includes replay, we should NOT enable replay config # because the user didn't explicitly enable replay. ( @@ -340,10 +340,11 @@ def test_bundle_kind_modifiers( b"/7.37.0/bundle.tracing.replay.feedback.min.js", { "dsn": dsn, - "tracesSampleRate": 1, + "tracesSampleRate": 1, # User explicitly enabled tracing "autoInjectFeedback": True, }, ), + # Note: bundle.replay.feedback DOES exist, so we don't need to force tracing ( { "dynamicSdkLoaderOptions": { @@ -351,10 +352,9 @@ def test_bundle_kind_modifiers( DynamicSdkLoaderOption.HAS_FEEDBACK.value: True, } }, - b"/7.37.0/bundle.tracing.replay.feedback.min.js", + b"/7.37.0/bundle.replay.feedback.min.js", { "dsn": dsn, - "tracesSampleRate": 1, "replaysSessionSampleRate": 0.1, "replaysOnErrorSampleRate": 1, "autoInjectFeedback": True, @@ -457,17 +457,17 @@ def test_logs_and_metrics_bundle_modifiers( dsn = self.projectkey.get_dsn(public=True) for data, expected_bundle, expected_options in [ - # Logs and metrics alone (should include tracing) + # Logs and metrics alone (no tracing required, bundle.logs.metrics exists) ( { "dynamicSdkLoaderOptions": { DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, } }, - b"/10.0.0/bundle.tracing.logs.metrics.min.js", - {"dsn": dsn, "tracesSampleRate": 1, "enableLogs": True}, + b"/10.0.0/bundle.logs.metrics.min.js", + {"dsn": dsn, "enableLogs": True}, ), - # Logs and metrics with performance (same as above, tracing already included) + # Logs and metrics with performance (user explicitly enables tracing) ( { "dynamicSdkLoaderOptions": { @@ -478,7 +478,7 @@ def test_logs_and_metrics_bundle_modifiers( b"/10.0.0/bundle.tracing.logs.metrics.min.js", {"dsn": dsn, "tracesSampleRate": 1, "enableLogs": True}, ), - # Logs and metrics with debug + # Logs and metrics with debug (no tracing required) ( { "dynamicSdkLoaderOptions": { @@ -486,10 +486,10 @@ def test_logs_and_metrics_bundle_modifiers( DynamicSdkLoaderOption.HAS_DEBUG.value: True, } }, - b"/10.0.0/bundle.tracing.logs.metrics.debug.min.js", - {"dsn": dsn, "tracesSampleRate": 1, "debug": True, "enableLogs": True}, + b"/10.0.0/bundle.logs.metrics.debug.min.js", + {"dsn": dsn, "debug": True, "enableLogs": True}, ), - # Logs and metrics with replay (should use full bundle) + # Logs and metrics with replay (bundle.replay.logs.metrics exists, no tracing required) ( { "dynamicSdkLoaderOptions": { @@ -497,7 +497,24 @@ def test_logs_and_metrics_bundle_modifiers( DynamicSdkLoaderOption.HAS_REPLAY.value: True, } }, - b"/10.0.0/bundle.tracing.replay.feedback.logs.metrics.min.js", + b"/10.0.0/bundle.replay.logs.metrics.min.js", + { + "dsn": dsn, + "replaysSessionSampleRate": 0.1, + "replaysOnErrorSampleRate": 1, + "enableLogs": True, + }, + ), + # Logs and metrics with tracing and replay (bundle.tracing.replay.logs.metrics exists) + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + DynamicSdkLoaderOption.HAS_PERFORMANCE.value: True, + DynamicSdkLoaderOption.HAS_REPLAY.value: True, + } + }, + b"/10.0.0/bundle.tracing.replay.logs.metrics.min.js", { "dsn": dsn, "tracesSampleRate": 1, @@ -507,8 +524,9 @@ def test_logs_and_metrics_bundle_modifiers( }, ), # Logs and metrics with feedback (should use full bundle) - # Note: Even though the full bundle includes replay, we should NOT enable replay config - # because the user didn't explicitly enable replay. + # Note: There's no bundle.feedback.logs.metrics, so we must use full bundle. + # Even though the full bundle includes tracing and replay, we should NOT enable + # their configs because the user didn't explicitly enable them. ( { "dynamicSdkLoaderOptions": { @@ -519,12 +537,32 @@ def test_logs_and_metrics_bundle_modifiers( b"/10.0.0/bundle.tracing.replay.feedback.logs.metrics.min.js", { "dsn": dsn, - "tracesSampleRate": 1, "autoInjectFeedback": True, "enableLogs": True, }, ), - # Logs and metrics with replay and feedback (full bundle) + # Logs and metrics with tracing and feedback (full bundle, replay not explicitly enabled) + # Note: There's no bundle.tracing.feedback.logs.metrics, so we must use full bundle. + ( + { + "dynamicSdkLoaderOptions": { + DynamicSdkLoaderOption.HAS_LOGS_AND_METRICS.value: True, + DynamicSdkLoaderOption.HAS_PERFORMANCE.value: True, + DynamicSdkLoaderOption.HAS_FEEDBACK.value: True, + } + }, + b"/10.0.0/bundle.tracing.replay.feedback.logs.metrics.min.js", + { + "dsn": dsn, + "tracesSampleRate": 1, # User explicitly enabled tracing + "autoInjectFeedback": True, + "enableLogs": True, + }, + ), + # Logs and metrics with replay and feedback (full bundle required) + # Note: There's no bundle.replay.feedback.logs.metrics + # Even though the full bundle includes tracing, we should NOT enable + # tracesSampleRate because the user didn't explicitly enable it. ( { "dynamicSdkLoaderOptions": { @@ -536,7 +574,6 @@ def test_logs_and_metrics_bundle_modifiers( b"/10.0.0/bundle.tracing.replay.feedback.logs.metrics.min.js", { "dsn": dsn, - "tracesSampleRate": 1, "replaysSessionSampleRate": 0.1, "replaysOnErrorSampleRate": 1, "autoInjectFeedback": True,