Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/sentry/loader/dynamic_sdk_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions src/sentry/web/frontend/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 31 additions & 3 deletions src/sentry/web/frontend/js_sdk_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class LoaderInternalConfig(TypedDict):
hasReplay: bool
hasDebug: bool
hasFeedback: bool
hasLogsAndMetrics: bool


class LoaderContext(TypedDict):
Expand All @@ -64,17 +65,22 @@ 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 = ""
has_replay = get_dynamic_sdk_loader_option(key, DynamicSdkLoaderOption.HAS_REPLAY)
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.
Expand All @@ -95,6 +101,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:
Comment thread
chargome marked this conversation as resolved.
Outdated
has_performance = True
has_replay = True
has_feedback = True
Comment thread
chargome marked this conversation as resolved.
Outdated

# Logs and metrics always require tracing (performance)
if is_greater_or_equal_v10_sdk and has_logs_and_metrics:
has_performance = True
Comment thread
chargome marked this conversation as resolved.
Outdated

# 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"
Expand All @@ -109,12 +131,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
Comment thread
chargome marked this conversation as resolved.
# 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:
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
bundle_kind_modifier += ".es5"

if has_debug:
Expand All @@ -127,6 +153,7 @@ def _get_loader_config(
"hasReplay": has_replay,
"hasDebug": has_debug,
"hasFeedback": has_feedback,
"hasLogsAndMetrics": has_logs_and_metrics,
}

def _get_context(
Expand Down Expand Up @@ -227,6 +254,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,
)
Expand Down
183 changes: 183 additions & 0 deletions tests/sentry/web/frontend/test_js_sdk_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,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
Loading