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..4afb13c94953b 100644 --- a/src/sentry/relay/config/__init__.py +++ b/src/sentry/relay/config/__init__.py @@ -109,14 +109,31 @@ def get_filter_settings(project: Project) -> Mapping[str, Any]: settings = _load_filter_settings(flt, project) filter_settings[filter_id] = settings + error_messages = [] 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: + error_messages += [ + # Hydration failed because the initial UI does not match what was rendered on the server. + "https://reactjs.org/docs/error-decoder.html?invariant=418", + # The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering. + "https://reactjs.org/docs/error-decoder.html?invariant=419", + # There was an error while hydrating this Suspense boundary. Switched to client rendering. + "https://reactjs.org/docs/error-decoder.html?invariant=422", + # There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering. + "https://reactjs.org/docs/error-decoder.html?invariant=423", + # Text content does not match server-rendered HTML. + "https://reactjs.org/docs/error-decoder.html?invariant=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..4d465e3fcaec4 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,15 @@ 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 + resp = self.get_success_response( + self.org_slug, self.proj_slug, options={"filters:react-hydration-errors": value} + ) + project = Project.objects.get(id=self.project.id) + assert 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)