diff --git a/src/sentry/api/endpoints/project_details.py b/src/sentry/api/endpoints/project_details.py index eeaf7baf282e6..362f3ebbd8d74 100644 --- a/src/sentry/api/endpoints/project_details.py +++ b/src/sentry/api/endpoints/project_details.py @@ -695,6 +695,11 @@ def put(self, request: Request, project) -> Response: "sentry:reprocessing_active", bool(options["sentry:reprocessing_active"]), ) + if "filters:react-hydration-errors" in options: + project.update_option( + "filters:react-hydration-errors", + bool(options["filters:react-hydration-errors"]), + ) if "filters:blacklisted_ips" in options: project.update_option( "sentry:blacklisted_ips", diff --git a/src/sentry/api/serializers/models/project.py b/src/sentry/api/serializers/models/project.py index be2f4b2e18857..fbaf131cef335 100644 --- a/src/sentry/api/serializers/models/project.py +++ b/src/sentry/api/serializers/models/project.py @@ -176,6 +176,7 @@ def format_options(attrs: defaultdict(dict)): "sentry:performance_issue_creation_rate" ), "filters:blacklisted_ips": "\n".join(options.get("sentry:blacklisted_ips", [])), + "filters:react-hydration-errors": bool(options.get("filters:react-hydration-errors", True)), f"filters:{FilterTypes.RELEASES}": "\n".join( options.get(f"sentry:{FilterTypes.RELEASES}", []) ), diff --git a/src/sentry/models/options/project_option.py b/src/sentry/models/options/project_option.py index 17717a987c9ab..53296880a80b7 100644 --- a/src/sentry/models/options/project_option.py +++ b/src/sentry/models/options/project_option.py @@ -59,6 +59,7 @@ "digests:mail:maximum_delay", "mail:subject_prefix", "mail:subject_template", + "filters:react-hydration-errors", ] ) diff --git a/src/sentry/projectoptions/defaults.py b/src/sentry/projectoptions/defaults.py index 570ccf1a1a7e1..83c09d3729647 100644 --- a/src/sentry/projectoptions/defaults.py +++ b/src/sentry/projectoptions/defaults.py @@ -63,15 +63,18 @@ # Default legacy-browsers filter register(key="filters:legacy-browsers", epoch_defaults={1: "0"}) -# Default legacy-browsers filter +# Default web crawlers filter register(key="filters:web-crawlers", epoch_defaults={1: "1", 6: "0"}) -# Default legacy-browsers filter +# Default browser extensions filter register(key="filters:browser-extensions", epoch_defaults={1: "0"}) -# Default legacy-browsers filter +# Default localhost filter register(key="filters:localhost", epoch_defaults={1: "0"}) +# Default react hydration errors filter +register(key="filters:react-hydration-errors", epoch_defaults={1: "1"}) + # Default breakdowns config register( key="sentry:breakdowns", diff --git a/src/sentry/relay/config/__init__.py b/src/sentry/relay/config/__init__.py index e31ac182eacea..f127e427c4efe 100644 --- a/src/sentry/relay/config/__init__.py +++ b/src/sentry/relay/config/__init__.py @@ -109,14 +109,27 @@ def get_filter_settings(project: Project) -> Mapping[str, Any]: settings = _load_filter_settings(flt, project) filter_settings[filter_id] = settings + error_messages: List[str] = [] if features.has("projects:custom-inbound-filters", project): invalid_releases = project.get_option(f"sentry:{FilterTypes.RELEASES}") if invalid_releases: filter_settings["releases"] = {"releases": invalid_releases} - error_messages = project.get_option(f"sentry:{FilterTypes.ERROR_MESSAGES}") - if error_messages: - filter_settings["errorMessages"] = {"patterns": error_messages} + error_messages += project.get_option(f"sentry:{FilterTypes.ERROR_MESSAGES}") or [] + + enable_react = project.get_option("filters:react-hydration-errors") + if enable_react: + # 418 - Hydration failed because the initial UI does not match what was rendered on the server. + # 419 - The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering. + # 422 - There was an error while hydrating this Suspense boundary. Switched to client rendering. + # 423 - There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering. + # 425 - Text content does not match server-rendered HTML. + error_messages += [ + "https://reactjs.org/docs/error-decoder.html?invariant={418,419,422,423,425}" + ] + + if error_messages: + filter_settings["errorMessages"] = {"patterns": error_messages} blacklisted_ips = project.get_option("sentry:blacklisted_ips") if blacklisted_ips: diff --git a/tests/sentry/api/endpoints/test_project_details.py b/tests/sentry/api/endpoints/test_project_details.py index b3f7ff4b6124a..cd370e1b678f4 100644 --- a/tests/sentry/api/endpoints/test_project_details.py +++ b/tests/sentry/api/endpoints/test_project_details.py @@ -469,6 +469,7 @@ def test_options(self): "sentry:token_header": "*", "sentry:verify_ssl": False, "feedback:branding": False, + "filters:react-hydration-errors": True, } with self.feature("projects:custom-inbound-filters"): self.get_success_response(self.org_slug, self.proj_slug, options=options) @@ -558,6 +559,7 @@ def test_options(self): assert AuditLogEntry.objects.filter( organization=project.organization, event=audit_log.get_event_id("PROJECT_EDIT") ).exists() + assert project.get_option("filters:react-hydration-errors", "1") def test_bookmarks(self): self.get_success_response(self.org_slug, self.proj_slug, isBookmarked="false") @@ -688,6 +690,13 @@ def test_store_crash_reports_exceeded(self): assert self.project.get_option("sentry:store_crash_reports") is None assert b"storeCrashReports" in resp.content + def test_react_hydration_errors(self): + value = False + options = {"filters:react-hydration-errors": value} + resp = self.get_success_response(self.org_slug, self.proj_slug, options=options) + assert self.project.get_option("filters:react-hydration-errors") == value + assert resp.data["options"]["filters:react-hydration-errors"] == value + def test_relay_pii_config(self): value = '{"applications": {"freeform": []}}' resp = self.get_success_response(self.org_slug, self.proj_slug, relayPiiConfig=value) diff --git a/tests/sentry/relay/snapshots/test_config/test_get_project_config/full_config/MONOLITH.pysnap b/tests/sentry/relay/snapshots/test_config/test_get_project_config/full_config/MONOLITH.pysnap index b2d6e502d7bbe..3154581663592 100644 --- a/tests/sentry/relay/snapshots/test_config/test_get_project_config/full_config/MONOLITH.pysnap +++ b/tests/sentry/relay/snapshots/test_config/test_get_project_config/full_config/MONOLITH.pysnap @@ -1,5 +1,5 @@ --- -created: '2023-01-17T00:14:23.253438Z' +created: '2023-03-02T14:24:26.512612Z' creator: sentry source: tests/sentry/relay/test_config.py --- @@ -82,6 +82,9 @@ config: - hoholikik.club - smartlink.cool - promfflinkdev.com + errorMessages: + patterns: + - https://reactjs.org/docs/error-decoder.html?invariant={418,419,422,423,425} legacyBrowsers: isEnabled: false localhost: diff --git a/tests/sentry/relay/snapshots/test_config/test_get_project_config/full_config/REGION.pysnap b/tests/sentry/relay/snapshots/test_config/test_get_project_config/full_config/REGION.pysnap index 3c9c11a4db661..b9db8cc9617bc 100644 --- a/tests/sentry/relay/snapshots/test_config/test_get_project_config/full_config/REGION.pysnap +++ b/tests/sentry/relay/snapshots/test_config/test_get_project_config/full_config/REGION.pysnap @@ -1,5 +1,5 @@ --- -created: '2023-01-17T00:14:23.169177Z' +created: '2023-03-02T14:24:26.635295Z' creator: sentry source: tests/sentry/relay/test_config.py --- @@ -82,6 +82,9 @@ config: - hoholikik.club - smartlink.cool - promfflinkdev.com + errorMessages: + patterns: + - https://reactjs.org/docs/error-decoder.html?invariant={418,419,422,423,425} legacyBrowsers: isEnabled: false localhost: diff --git a/tests/sentry/relay/test_config.py b/tests/sentry/relay/test_config.py index de2b478e29286..fb6dcaec4cd96 100644 --- a/tests/sentry/relay/test_config.py +++ b/tests/sentry/relay/test_config.py @@ -169,6 +169,7 @@ def test_project_config_uses_filter_features( blacklisted_ips = ["112.69.248.54"] default_project.update_option("sentry:error_messages", error_messages) default_project.update_option("sentry:releases", releases) + default_project.update_option("filters:react-hydration-errors", False) if has_blacklisted_ips: default_project.update_option("sentry:blacklisted_ips", blacklisted_ips)