From 167ee740adf197713fbf4b0afa190036183608a7 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Thu, 2 Mar 2023 11:04:38 +0100 Subject: [PATCH 1/7] feat(inbound-filters): Add react hydration errors filter --- src/sentry/api/endpoints/project_details.py | 5 ++++ src/sentry/api/serializers/models/project.py | 1 + src/sentry/models/options/project_option.py | 1 + src/sentry/projectoptions/defaults.py | 9 +++++--- src/sentry/relay/config/__init__.py | 23 ++++++++++++++++--- .../api/endpoints/test_project_details.py | 11 +++++++++ 6 files changed, 44 insertions(+), 6 deletions(-) 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) From 2d25023a1e54de5ad991e5eb264514b55ad1b27f Mon Sep 17 00:00:00 2001 From: Jan Michael Auer Date: Thu, 2 Mar 2023 09:41:04 +0100 Subject: [PATCH 2/7] feat(monitors): Add basic ingestion via Kafka (#45079) Adds an ingest consumer similar to Replays and Profiles that reads from a Kafka topic `ingest-monitors` and updates them directly in postgres. The consumer runs automatically in devserver and can be started via `sentry run ingest-monitors`. This supports creating and updating check ins with a status and duration. Creating monitors is not supported yet. The consumer closely follows the logic from checkin creation, but there is a difference for disabled monitors compared to the Update (PUT) endpoint. --- src/sentry/conf/server.py | 2 + src/sentry/models/monitorcheckin.py | 3 +- src/sentry/monitors/consumers/__init__.py | 58 +++++++++++ src/sentry/monitors/consumers/check_in.py | 112 ++++++++++++++++++++++ src/sentry/runner/commands/devserver.py | 6 +- src/sentry/runner/commands/run.py | 13 +++ 6 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 src/sentry/monitors/consumers/__init__.py create mode 100644 src/sentry/monitors/consumers/check_in.py diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index cf2cc8cfa719c..f9f753b853036 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -2667,6 +2667,7 @@ def build_cdc_postgres_init_db_volume(settings): KAFKA_INGEST_REPLAY_EVENTS = "ingest-replay-events" KAFKA_INGEST_REPLAYS_RECORDINGS = "ingest-replay-recordings" KAFKA_INGEST_OCCURRENCES = "ingest-occurrences" +KAFKA_INGEST_MONITORS = "ingest-monitors" KAFKA_REGION_TO_CONTROL = "region-to-control" KAFKA_EVENTSTREAM_GENERIC = "generic-events" @@ -2715,6 +2716,7 @@ def build_cdc_postgres_init_db_volume(settings): KAFKA_INGEST_REPLAY_EVENTS: {"cluster": "default"}, KAFKA_INGEST_REPLAYS_RECORDINGS: {"cluster": "default"}, KAFKA_INGEST_OCCURRENCES: {"cluster": "default"}, + KAFKA_INGEST_MONITORS: {"cluster": "default"}, # Metrics Testing Topics KAFKA_SNUBA_GENERICS_METRICS_CS: {"cluster": "default"}, # Region to Control Silo messaging - eg UserIp and AuditLog diff --git a/src/sentry/models/monitorcheckin.py b/src/sentry/models/monitorcheckin.py index c0b75e12e15fa..ce3663ef0f5cf 100644 --- a/src/sentry/models/monitorcheckin.py +++ b/src/sentry/models/monitorcheckin.py @@ -60,9 +60,10 @@ class MonitorCheckIn(Model): duration = BoundedPositiveIntegerField(null=True) date_added = models.DateTimeField(default=timezone.now) date_updated = models.DateTimeField(default=timezone.now) - objects = BaseManager(cache_fields=("guid",)) attachment_id = BoundedBigIntegerField(null=True) + objects = BaseManager(cache_fields=("guid",)) + class Meta: app_label = "sentry" db_table = "sentry_monitorcheckin" diff --git a/src/sentry/monitors/consumers/__init__.py b/src/sentry/monitors/consumers/__init__.py new file mode 100644 index 0000000000000..ec56576011d90 --- /dev/null +++ b/src/sentry/monitors/consumers/__init__.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Any, MutableMapping + +from arroyo import Topic +from arroyo.backends.kafka.configuration import build_kafka_consumer_configuration +from arroyo.backends.kafka.consumer import KafkaConsumer, KafkaPayload +from arroyo.commit import ONCE_PER_SECOND +from arroyo.processing.processor import StreamProcessor +from django.conf import settings + +from sentry.monitors.consumers.check_in import StoreMonitorCheckInStrategyFactory +from sentry.utils import kafka_config +from sentry.utils.batching_kafka_consumer import create_topics + + +def get_monitor_check_ins_consumer( + topic: str, + group_id: str, + auto_offset_reset: str, + strict_offset_reset: bool, + force_topic: str | None, + force_cluster: str | None, +) -> StreamProcessor[KafkaPayload]: + topic = force_topic or topic + consumer_config = get_config( + topic, + group_id, + auto_offset_reset=auto_offset_reset, + strict_offset_reset=strict_offset_reset, + force_cluster=force_cluster, + ) + consumer = KafkaConsumer(consumer_config) + return StreamProcessor( + consumer=consumer, + topic=Topic(topic), + processor_factory=StoreMonitorCheckInStrategyFactory(), + commit_policy=ONCE_PER_SECOND, + ) + + +def get_config( + topic: str, + group_id: str, + auto_offset_reset: str, + strict_offset_reset: bool, + force_cluster: str | None, +) -> MutableMapping[str, Any]: + cluster_name: str = force_cluster or settings.KAFKA_TOPICS[topic]["cluster"] + create_topics(cluster_name, [topic]) + return build_kafka_consumer_configuration( + kafka_config.get_kafka_consumer_cluster_options( + cluster_name, + ), + group_id=group_id, + auto_offset_reset=auto_offset_reset, + strict_offset_reset=strict_offset_reset, + ) diff --git a/src/sentry/monitors/consumers/check_in.py b/src/sentry/monitors/consumers/check_in.py new file mode 100644 index 0000000000000..0627970002e6d --- /dev/null +++ b/src/sentry/monitors/consumers/check_in.py @@ -0,0 +1,112 @@ +import datetime +import logging +from typing import Mapping + +import msgpack +from arroyo.backends.kafka.consumer import KafkaPayload +from arroyo.processing.strategies.abstract import ProcessingStrategy, ProcessingStrategyFactory +from arroyo.processing.strategies.commit import CommitOffsets +from arroyo.processing.strategies.run_task import RunTask +from arroyo.types import Commit, Message, Partition +from django.db import transaction + +from sentry.models import CheckInStatus, Monitor, MonitorCheckIn, MonitorStatus, Project +from sentry.signals import first_cron_checkin_received, first_cron_monitor_created +from sentry.utils import json +from sentry.utils.dates import to_datetime + +logger = logging.getLogger(__name__) + + +def process_message(message: Message[KafkaPayload]) -> None: + wrapper = msgpack.unpackb(message.payload.value) + + params = json.loads(wrapper["payload"]) + start_time = to_datetime(float(wrapper["start_time"])) + project_id = int(wrapper["project_id"]) + + # TODO: Same as the check-in endpoints. Keep in sync or factor out. + try: + with transaction.atomic(): + try: + monitor = Monitor.objects.select_for_update().get( + guid=params["monitor_id"], project_id=project_id + ) + except Monitor.DoesNotExist: + logger.debug("monitor does not exist: %s", params["monitor_id"]) + return + + status = getattr(CheckInStatus, params["status"].upper()) + duration = int(params["duration"]) if params.get("duration") is not None else None + + try: + check_in = MonitorCheckIn.objects.select_for_update().get( + guid=params["check_in_id"], + project_id=project_id, + monitor=monitor, + ) + + if duration is None: + duration = int((start_time - check_in.date_added).total_seconds() * 1000) + + check_in.update(status=status, duration=duration) + + except MonitorCheckIn.DoesNotExist: + # Infer the original start time of the check-in from the duration. + # Note that the clock of this worker may be off from what Relay is reporting. + date_added = start_time + if duration is not None: + date_added -= datetime.timedelta(seconds=duration) + + check_in = MonitorCheckIn.objects.create( + project_id=project_id, + monitor=monitor, + guid=params["check_in_id"], + duration=duration, + status=status, + date_added=date_added, + date_updated=start_time, + ) + + project = Project.objects.get_from_cache(id=project_id) + if not project.flags.has_cron_checkins: + # Backfill users that already have cron monitors + if not project.flags.has_cron_monitors: + first_cron_monitor_created.send_robust( + project=project, user=None, sender=Project + ) + + first_cron_checkin_received.send_robust( + project=project, monitor_id=str(monitor.guid), sender=Project + ) + + if check_in.status == CheckInStatus.ERROR and monitor.status != MonitorStatus.DISABLED: + monitor.mark_failed(start_time) + return + + monitor_params = { + "last_checkin": start_time, + "next_checkin": monitor.get_next_scheduled_checkin(start_time), + } + + if check_in.status == CheckInStatus.OK and monitor.status != MonitorStatus.DISABLED: + monitor_params["status"] = MonitorStatus.OK + + Monitor.objects.filter(id=monitor.id).exclude(last_checkin__gt=start_time).update( + **monitor_params + ) + except Exception: + # Skip this message and continue processing in the consumer. + logger.exception("Failed to process check-in", exc_info=True) + + +class StoreMonitorCheckInStrategyFactory(ProcessingStrategyFactory[KafkaPayload]): + def create_with_partitions( + self, + commit: Commit, + partitions: Mapping[Partition, int], + ) -> ProcessingStrategy[KafkaPayload]: + return RunTask( + function=process_message, + next_step=CommitOffsets(commit), + ) diff --git a/src/sentry/runner/commands/devserver.py b/src/sentry/runner/commands/devserver.py index 91fd6d29ddbe1..cb2188feaafc1 100644 --- a/src/sentry/runner/commands/devserver.py +++ b/src/sentry/runner/commands/devserver.py @@ -27,6 +27,9 @@ "--no-strict-offset-reset", ] +# NOTE: These do NOT start automatically. Add your daemon to the `daemons` list +# in `devserver()` like so: +# daemons += [_get_daemon("my_new_daemon")] _DEFAULT_DAEMONS = { "worker": ["sentry", "run", "worker", "-c", "1", "--autoreload"], "cron": ["sentry", "run", "cron", "--autoreload"], @@ -96,6 +99,7 @@ ], "metrics-billing": ["sentry", "run", "billing-metrics-consumer", "--no-strict-offset-reset"], "profiles": ["sentry", "run", "ingest-profiles", "--no-strict-offset-reset"], + "monitors": ["sentry", "run", "ingest-monitors", "--no-strict-offset-reset"], } @@ -324,7 +328,7 @@ def devserver( ] if settings.SENTRY_USE_RELAY: - daemons += [_get_daemon("ingest")] + daemons += [_get_daemon("ingest"), _get_daemon("monitors")] if settings.SENTRY_USE_PROFILING: daemons += [_get_daemon("profiles")] diff --git a/src/sentry/runner/commands/run.py b/src/sentry/runner/commands/run.py index 58db746bfee79..09d6fb12d8bcc 100644 --- a/src/sentry/runner/commands/run.py +++ b/src/sentry/runner/commands/run.py @@ -699,6 +699,19 @@ def replays_recordings_consumer(**options): run_processor_with_signals(consumer) +@run.command("ingest-monitors") +@log_options() +@click.option("--topic", default="ingest-monitors", help="Topic to get monitor check-in data from.") +@kafka_options("ingest-monitors") +@strict_offset_reset_option() +@configuration +def monitors_consumer(**options): + from sentry.monitors.consumers import get_monitor_check_ins_consumer + + consumer = get_monitor_check_ins_consumer(**options) + run_processor_with_signals(consumer) + + @run.command("indexer-last-seen-updater") @log_options() @configuration From 1fb75f50c208e48d82ad0e1a8648149856dd076b Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Thu, 2 Mar 2023 11:43:33 +0100 Subject: [PATCH 3/7] fix typo --- tests/sentry/api/endpoints/test_project_details.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/sentry/api/endpoints/test_project_details.py b/tests/sentry/api/endpoints/test_project_details.py index 4d465e3fcaec4..cd370e1b678f4 100644 --- a/tests/sentry/api/endpoints/test_project_details.py +++ b/tests/sentry/api/endpoints/test_project_details.py @@ -692,12 +692,10 @@ def test_store_crash_reports_exceeded(self): 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 + 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": []}}' From 19248d7f4b7e44a5b37ee355ffec74a18d298964 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Thu, 2 Mar 2023 11:46:20 +0100 Subject: [PATCH 4/7] cr feedback --- src/sentry/relay/config/__init__.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/sentry/relay/config/__init__.py b/src/sentry/relay/config/__init__.py index 4afb13c94953b..85cb49412588f 100644 --- a/src/sentry/relay/config/__init__.py +++ b/src/sentry/relay/config/__init__.py @@ -119,17 +119,13 @@ def get_filter_settings(project: Project) -> Mapping[str, Any]: 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 += [ - # 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", + "https://reactjs.org/docs/error-decoder.html?invariant={418,419,422,423,425}" ] if error_messages: From 2acdce5a82a2064869d348fede3fd4bafe6a9e7d Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Thu, 2 Mar 2023 11:54:19 +0100 Subject: [PATCH 5/7] add type definition --- src/sentry/relay/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/relay/config/__init__.py b/src/sentry/relay/config/__init__.py index 85cb49412588f..f127e427c4efe 100644 --- a/src/sentry/relay/config/__init__.py +++ b/src/sentry/relay/config/__init__.py @@ -109,7 +109,7 @@ def get_filter_settings(project: Project) -> Mapping[str, Any]: settings = _load_filter_settings(flt, project) filter_settings[filter_id] = settings - error_messages = [] + error_messages: List[str] = [] if features.has("projects:custom-inbound-filters", project): invalid_releases = project.get_option(f"sentry:{FilterTypes.RELEASES}") if invalid_releases: From 813a06aa1a3e3d417fc50391fe880a69993cc3c8 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Thu, 2 Mar 2023 13:21:38 +0100 Subject: [PATCH 6/7] fix test_config.py tests --- tests/sentry/relay/test_config.py | 1 + 1 file changed, 1 insertion(+) 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) From 9940a17c46287df3553d8321af32bfe1dcff5f98 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Thu, 2 Mar 2023 15:24:48 +0100 Subject: [PATCH 7/7] update snapshot --- .../test_get_project_config/full_config/MONOLITH.pysnap | 5 ++++- .../test_get_project_config/full_config/REGION.pysnap | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) 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: