From 7078a19987738a9c7aa53ffc519c974150e968fd Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Mon, 4 Mar 2024 10:40:47 -0800 Subject: [PATCH 1/8] feat: DLQ unprocessable messages on ingest-events This PR attempts to be somewhat cautious about DLQing and not DLQ anything that is even potentially retriable. This could be tweaked later. An alternative (and imo better) approach would be to validate the schema of failed messages and only then put them into the DLQ. However no schema is currently registered for this topic so this cannot be done easily. --- src/sentry/consumers/__init__.py | 1 + src/sentry/ingest/consumer/processors.py | 4 ++ src/sentry/ingest/consumer/simple_event.py | 60 ++++++++++++++-------- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/sentry/consumers/__init__.py b/src/sentry/consumers/__init__.py index 400311869c8082..1a9c62a522e6d4 100644 --- a/src/sentry/consumers/__init__.py +++ b/src/sentry/consumers/__init__.py @@ -270,6 +270,7 @@ def ingest_events_options() -> list[click.Option]: "static_args": { "consumer_type": "events", }, + "dlq_topic": settings.KAFKA_INGEST_EVENTS_DLQ, }, "ingest-attachments": { "topic": settings.KAFKA_INGEST_ATTACHMENTS, diff --git a/src/sentry/ingest/consumer/processors.py b/src/sentry/ingest/consumer/processors.py index 5773bca1c11931..e67f503f1ee062 100644 --- a/src/sentry/ingest/consumer/processors.py +++ b/src/sentry/ingest/consumer/processors.py @@ -32,6 +32,10 @@ IngestMessage = Mapping[str, Any] +class Retriable(Exception): + pass + + def trace_func(**span_kwargs): def wrapper(f): @functools.wraps(f) diff --git a/src/sentry/ingest/consumer/simple_event.py b/src/sentry/ingest/consumer/simple_event.py index d705fc2fa305de..9c9f9042f9e524 100644 --- a/src/sentry/ingest/consumer/simple_event.py +++ b/src/sentry/ingest/consumer/simple_event.py @@ -2,12 +2,13 @@ import msgpack from arroyo.backends.kafka.consumer import KafkaPayload -from arroyo.types import Message +from arroyo.dlq import InvalidMessage +from arroyo.types import BrokerValue, Message from sentry.models.project import Project from sentry.utils import metrics -from .processors import IngestMessage, process_event +from .processors import IngestMessage, Retriable, process_event logger = logging.getLogger(__name__) @@ -29,26 +30,43 @@ def process_simple_event_message( `symbolicate_event` or `process_event`. """ - raw_payload = raw_message.payload.value - metrics.distribution( - "ingest_consumer.payload_size", - len(raw_payload), - tags={"consumer": consumer_type}, - unit="byte", - ) - message: IngestMessage = msgpack.unpackb(raw_payload, use_list=False) + try: + raw_payload = raw_message.payload.value + metrics.distribution( + "ingest_consumer.payload_size", + len(raw_payload), + tags={"consumer": consumer_type}, + unit="byte", + ) + message: IngestMessage = msgpack.unpackb(raw_payload, use_list=False) - message_type = message["type"] - project_id = message["project_id"] + message_type = message["type"] + project_id = message["project_id"] - if message_type != "event": - raise ValueError(f"Unsupported message type: {message_type}") + if message_type != "event": + raise ValueError(f"Unsupported message type: {message_type}") - try: - with metrics.timer("ingest_consumer.fetch_project"): - project = Project.objects.get_from_cache(id=project_id) - except Project.DoesNotExist: - logger.exception("Project for ingested event does not exist: %s", project_id) - return + try: + try: + with metrics.timer("ingest_consumer.fetch_project"): + project = Project.objects.get_from_cache(id=project_id) + except Project.DoesNotExist: + logger.exception("Project for ingested event does not exist: %s", project_id) + return + + return process_event(message, project, reprocess_only_stuck_events) + except Exception as exc: + raise Retriable(exc) - return process_event(message, project, reprocess_only_stuck_events) + except Exception as exc: + # Non retriable exceptions raise InvalidMessage, which Arroyo will DLQ. + # The consumer will crash on any potentially retriable exceptions. + # A better approach would be to instead validate the schema here and only + # DLQ messages that fail schema validation, but this topic has no schema + # defined currently. + if not isinstance(exc, Retriable): + raw_value = raw_message.value + assert isinstance(raw_value, BrokerValue) + raise InvalidMessage(raw_value.partition, raw_value.offset) + else: + raise From 9e31d5d589e6f5bfa63143e84b0fbb914f70de03 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Tue, 5 Mar 2024 20:41:05 -0800 Subject: [PATCH 2/8] use schema to determine what to dlq --- src/sentry/ingest/consumer/processors.py | 4 -- src/sentry/ingest/consumer/simple_event.py | 83 +++++++++++++--------- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/src/sentry/ingest/consumer/processors.py b/src/sentry/ingest/consumer/processors.py index e67f503f1ee062..5773bca1c11931 100644 --- a/src/sentry/ingest/consumer/processors.py +++ b/src/sentry/ingest/consumer/processors.py @@ -32,10 +32,6 @@ IngestMessage = Mapping[str, Any] -class Retriable(Exception): - pass - - def trace_func(**span_kwargs): def wrapper(f): @functools.wraps(f) diff --git a/src/sentry/ingest/consumer/simple_event.py b/src/sentry/ingest/consumer/simple_event.py index 9c9f9042f9e524..69105b824aeb9b 100644 --- a/src/sentry/ingest/consumer/simple_event.py +++ b/src/sentry/ingest/consumer/simple_event.py @@ -1,18 +1,27 @@ import logging import msgpack +import sentry_kafka_schemas from arroyo.backends.kafka.consumer import KafkaPayload from arroyo.dlq import InvalidMessage from arroyo.types import BrokerValue, Message +from sentry.conf.types.kafka_definition import Topic from sentry.models.project import Project from sentry.utils import metrics -from .processors import IngestMessage, Retriable, process_event +from .processors import IngestMessage, process_event logger = logging.getLogger(__name__) +consumer_type_to_default_topic = { + "events": Topic.INGEST_EVENTS, + "transactions": Topic.INGEST_TRANSACTIONS, + "attachments": Topic.INGEST_ATTACHMENTS, +} + + def process_simple_event_message( raw_message: Message[KafkaPayload], consumer_type: str, reprocess_only_stuck_events: bool ) -> None: @@ -30,43 +39,51 @@ def process_simple_event_message( `symbolicate_event` or `process_event`. """ + raw_payload = raw_message.payload.value + metrics.distribution( + "ingest_consumer.payload_size", + len(raw_payload), + tags={"consumer": consumer_type}, + unit="byte", + ) + message: IngestMessage = msgpack.unpackb(raw_payload, use_list=False) + + message_type = message["type"] + project_id = message["project_id"] + + if message_type != "event": + raise ValueError(f"Unsupported message type: {message_type}") + try: - raw_payload = raw_message.payload.value - metrics.distribution( - "ingest_consumer.payload_size", - len(raw_payload), - tags={"consumer": consumer_type}, - unit="byte", - ) - message: IngestMessage = msgpack.unpackb(raw_payload, use_list=False) + try: + with metrics.timer("ingest_consumer.fetch_project"): + project = Project.objects.get_from_cache(id=project_id) + except Project.DoesNotExist: + logger.exception("Project for ingested event does not exist: %s", project_id) + return + + return process_event(message, project, reprocess_only_stuck_events) - message_type = message["type"] - project_id = message["project_id"] + except Exception: + # Any exception that fails schema validation will raise InvalidMessage, which Arroyo will DLQ. + # Messages that pass schema validation will not be DLQed as they may be retriable. - if message_type != "event": - raise ValueError(f"Unsupported message type: {message_type}") + default_topic = consumer_type_to_default_topic[consumer_type].value + + # TODO: Currently, there is only a schema for ingest-events, so just continue to re-raise + # the exception if it's a different topic. This can be removed once attachments and transactions + # have schemas too. + if default_topic != "ingest-events": + raise + + codec = sentry_kafka_schemas.get_codec(default_topic) try: - try: - with metrics.timer("ingest_consumer.fetch_project"): - project = Project.objects.get_from_cache(id=project_id) - except Project.DoesNotExist: - logger.exception("Project for ingested event does not exist: %s", project_id) - return - - return process_event(message, project, reprocess_only_stuck_events) - except Exception as exc: - raise Retriable(exc) - - except Exception as exc: - # Non retriable exceptions raise InvalidMessage, which Arroyo will DLQ. - # The consumer will crash on any potentially retriable exceptions. - # A better approach would be to instead validate the schema here and only - # DLQ messages that fail schema validation, but this topic has no schema - # defined currently. - if not isinstance(exc, Retriable): + codec.validate(raw_payload) + except Exception: raw_value = raw_message.value assert isinstance(raw_value, BrokerValue) + raise InvalidMessage(raw_value.partition, raw_value.offset) - else: - raise + + raise From e71b3fb16382402fbb3fb5776b29a5fdd08c66e3 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Fri, 8 Mar 2024 10:34:11 -0800 Subject: [PATCH 3/8] fix topic --- src/sentry/consumers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/consumers/__init__.py b/src/sentry/consumers/__init__.py index b923890d637dc1..271ce0fd2e4472 100644 --- a/src/sentry/consumers/__init__.py +++ b/src/sentry/consumers/__init__.py @@ -259,7 +259,7 @@ def ingest_events_options() -> list[click.Option]: "static_args": { "consumer_type": "events", }, - "dlq_topic": settings.KAFKA_INGEST_EVENTS_DLQ, + "dlq_topic": Topic.INGEST_EVENTS_DLQ, }, "ingest-attachments": { "topic": Topic.INGEST_ATTACHMENTS, From c93b85cecc64d238a6fd69127df27db452ff69f4 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Tue, 12 Mar 2024 16:03:44 -0700 Subject: [PATCH 4/8] add test, also raise if encoding is wrong --- src/sentry/ingest/consumer/simple_event.py | 13 ++-- .../sentry/ingest/ingest_consumer/test_dlq.py | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 tests/sentry/ingest/ingest_consumer/test_dlq.py diff --git a/src/sentry/ingest/consumer/simple_event.py b/src/sentry/ingest/consumer/simple_event.py index 69105b824aeb9b..ba97777cc298b7 100644 --- a/src/sentry/ingest/consumer/simple_event.py +++ b/src/sentry/ingest/consumer/simple_event.py @@ -46,15 +46,16 @@ def process_simple_event_message( tags={"consumer": consumer_type}, unit="byte", ) - message: IngestMessage = msgpack.unpackb(raw_payload, use_list=False) - message_type = message["type"] - project_id = message["project_id"] + try: + message: IngestMessage = msgpack.unpackb(raw_payload, use_list=False) - if message_type != "event": - raise ValueError(f"Unsupported message type: {message_type}") + message_type = message["type"] + project_id = message["project_id"] + + if message_type != "event": + raise ValueError(f"Unsupported message type: {message_type}") - try: try: with metrics.timer("ingest_consumer.fetch_project"): project = Project.objects.get_from_cache(id=project_id) diff --git a/tests/sentry/ingest/ingest_consumer/test_dlq.py b/tests/sentry/ingest/ingest_consumer/test_dlq.py new file mode 100644 index 00000000000000..433c03bec36ad5 --- /dev/null +++ b/tests/sentry/ingest/ingest_consumer/test_dlq.py @@ -0,0 +1,60 @@ +from datetime import datetime +from unittest.mock import Mock + +import pytest +from arroyo.backends.kafka import KafkaPayload +from arroyo.dlq import InvalidMessage +from arroyo.types import BrokerValue, Message, Partition, Topic + +from sentry.ingest.consumer.factory import IngestStrategyFactory +from sentry.testutils.pytest.fixtures import django_db_all + + +@django_db_all +def test_dlq_invalid_messages() -> None: + partition = Partition(Topic("ingest-events"), 0) + offset = 5 + + invalid_message = Message( + BrokerValue( + KafkaPayload(None, b"bogus message", []), + partition, + offset, + datetime.now(), + ) + ) + + # DLQ is defined + factory = IngestStrategyFactory( + "events", + reprocess_only_stuck_events=False, + num_processes=1, + max_batch_size=1, + max_batch_time=1, + input_block_size=None, + output_block_size=None, + ) + + strategy = factory.create_with_partitions(Mock(), Mock()) + + with pytest.raises(InvalidMessage) as exc_info: + strategy.submit(invalid_message) + + assert exc_info.value.partition == partition + assert exc_info.value.offset == offset + + # Transactions has no DLQ so we still get the original value error + factory = IngestStrategyFactory( + "transactions", + reprocess_only_stuck_events=False, + num_processes=1, + max_batch_size=1, + max_batch_time=1, + input_block_size=None, + output_block_size=None, + ) + + strategy = factory.create_with_partitions(Mock(), Mock()) + + with pytest.raises(ValueError): + strategy.submit(invalid_message) From 3926556eda7734bf060d894058b0e584805fb4bd Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Tue, 12 Mar 2024 16:47:11 -0700 Subject: [PATCH 5/8] improve test and fix bug --- src/sentry/ingest/consumer/simple_event.py | 2 +- .../sentry/ingest/ingest_consumer/test_dlq.py | 62 +++++++++++-------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/sentry/ingest/consumer/simple_event.py b/src/sentry/ingest/consumer/simple_event.py index ba97777cc298b7..fab02bb5aee3da 100644 --- a/src/sentry/ingest/consumer/simple_event.py +++ b/src/sentry/ingest/consumer/simple_event.py @@ -80,7 +80,7 @@ def process_simple_event_message( codec = sentry_kafka_schemas.get_codec(default_topic) try: - codec.validate(raw_payload) + codec.decode(raw_payload, validate=True) except Exception: raw_value = raw_message.value assert isinstance(raw_value, BrokerValue) diff --git a/tests/sentry/ingest/ingest_consumer/test_dlq.py b/tests/sentry/ingest/ingest_consumer/test_dlq.py index 433c03bec36ad5..3f098cdd238c78 100644 --- a/tests/sentry/ingest/ingest_consumer/test_dlq.py +++ b/tests/sentry/ingest/ingest_consumer/test_dlq.py @@ -1,6 +1,8 @@ +import time from datetime import datetime from unittest.mock import Mock +import msgpack import pytest from arroyo.backends.kafka import KafkaPayload from arroyo.dlq import InvalidMessage @@ -10,21 +12,37 @@ from sentry.testutils.pytest.fixtures import django_db_all -@django_db_all -def test_dlq_invalid_messages() -> None: - partition = Partition(Topic("ingest-events"), 0) - offset = 5 - - invalid_message = Message( +def make_message(payload: bytes, partition: Partition, offset: int) -> Message: + return Message( BrokerValue( - KafkaPayload(None, b"bogus message", []), + KafkaPayload(None, payload, []), partition, offset, datetime.now(), ) ) - # DLQ is defined + +@django_db_all +def test_dlq_invalid_messages(factories) -> None: + organization = factories.create_organization() + project = factories.create_project(organization=organization) + + valid_payload = msgpack.packb( + { + "type": "event", + "project_id": project.id, + "payload": b"{}", + "start_time": int(time.time()), + "event_id": "aaa", + } + ) + + bogus_payload = b"bogus message" + + partition = Partition(Topic("ingest-events"), 0) + offset = 5 + factory = IngestStrategyFactory( "events", reprocess_only_stuck_events=False, @@ -34,27 +52,19 @@ def test_dlq_invalid_messages() -> None: input_block_size=None, output_block_size=None, ) - strategy = factory.create_with_partitions(Mock(), Mock()) + # Valid payload raises original error + with pytest.raises(Exception) as exc_info: + message = make_message(valid_payload, partition, offset) + strategy.submit(message) + assert not isinstance(exc_info.value, InvalidMessage) + + # Invalid payload raises InvalidMessage error + with pytest.raises(InvalidMessage) as exc_info: - strategy.submit(invalid_message) + message = make_message(bogus_payload, partition, offset) + strategy.submit(message) assert exc_info.value.partition == partition assert exc_info.value.offset == offset - - # Transactions has no DLQ so we still get the original value error - factory = IngestStrategyFactory( - "transactions", - reprocess_only_stuck_events=False, - num_processes=1, - max_batch_size=1, - max_batch_time=1, - input_block_size=None, - output_block_size=None, - ) - - strategy = factory.create_with_partitions(Mock(), Mock()) - - with pytest.raises(ValueError): - strategy.submit(invalid_message) From ab4b4a03d481bcfca2464794f89d5ece97c2bed6 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Thu, 14 Mar 2024 11:49:20 -0700 Subject: [PATCH 6/8] feedback events --- src/sentry/ingest/consumer/simple_event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/ingest/consumer/simple_event.py b/src/sentry/ingest/consumer/simple_event.py index fab02bb5aee3da..2b28f2ae9c69fa 100644 --- a/src/sentry/ingest/consumer/simple_event.py +++ b/src/sentry/ingest/consumer/simple_event.py @@ -19,6 +19,7 @@ "events": Topic.INGEST_EVENTS, "transactions": Topic.INGEST_TRANSACTIONS, "attachments": Topic.INGEST_ATTACHMENTS, + "ingest-feedback-events": Topic.INGEST_FEEDBACK_EVENTS, } From 52351dd9864961946b9b0b0f2fff782cacd7b6f1 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Thu, 14 Mar 2024 12:45:37 -0700 Subject: [PATCH 7/8] add back retriable exception --- src/sentry/ingest/consumer/processors.py | 145 +++++++++++---------- src/sentry/ingest/consumer/simple_event.py | 10 +- 2 files changed, 84 insertions(+), 71 deletions(-) diff --git a/src/sentry/ingest/consumer/processors.py b/src/sentry/ingest/consumer/processors.py index 5773bca1c11931..42e20cec2d16c3 100644 --- a/src/sentry/ingest/consumer/processors.py +++ b/src/sentry/ingest/consumer/processors.py @@ -32,6 +32,10 @@ IngestMessage = Mapping[str, Any] +class Retriable(Exception): + pass + + def trace_func(**span_kwargs): def wrapper(f): @functools.wraps(f) @@ -127,78 +131,85 @@ def process_event( ): return - # If we only want to reprocess "stuck" events, we check if this event is already in the - # `processing_store`. We only continue here if the event *is* present, as that will eventually - # process and consume the event from the `processing_store`, whereby getting it "unstuck". - if reprocess_only_stuck_events and not event_processing_store.exists(data): - return - - with metrics.timer("ingest_consumer._store_event"): - cache_key = event_processing_store.store(data) - + # Raise the retriable exception and skip DLQ if anything below this point fails as it may be caused by + # intermittent network issue try: - # Records rc-processing usage broken down by - # event type. - event_type = data.get("type") - if event_type == "error": - app_feature = "errors" - elif event_type == "transaction": - app_feature = "transactions" - else: - app_feature = None - - if app_feature is not None: - record(settings.EVENT_PROCESSING_STORE, app_feature, len(payload), UsageUnit.BYTES) - except Exception: - pass - - if attachments: - with sentry_sdk.start_span(op="ingest_consumer.set_attachment_cache"): - attachment_objects = [ - CachedAttachment(type=attachment.pop("attachment_type"), **attachment) - for attachment in attachments - ] - - attachment_cache.set(cache_key, attachments=attachment_objects, timeout=CACHE_TIMEOUT) - - if data.get("type") == "transaction": - # No need for preprocess/process for transactions thus submit - # directly transaction specific save_event task. - save_event_transaction.delay( - cache_key=cache_key, - data=None, - start_time=start_time, - event_id=event_id, - project_id=project_id, - ) - elif data.get("type") == "feedback": - if features.has("organizations:user-feedback-ingest", project.organization, actor=None): - save_event_feedback.delay( - cache_key=None, # no need to cache as volume is low - data=data, - start_time=start_time, - event_id=event_id, - project_id=project_id, - ) - else: - # Preprocess this event, which spawns either process_event or - # save_event. Pass data explicitly to avoid fetching it again from the - # cache. - with sentry_sdk.start_span(op="ingest_consumer.process_event.preprocess_event"): - preprocess_event( + # If we only want to reprocess "stuck" events, we check if this event is already in the + # `processing_store`. We only continue here if the event *is* present, as that will eventually + # process and consume the event from the `processing_store`, whereby getting it "unstuck". + if reprocess_only_stuck_events and not event_processing_store.exists(data): + return + + with metrics.timer("ingest_consumer._store_event"): + cache_key = event_processing_store.store(data) + + try: + # Records rc-processing usage broken down by + # event type. + event_type = data.get("type") + if event_type == "error": + app_feature = "errors" + elif event_type == "transaction": + app_feature = "transactions" + else: + app_feature = None + + if app_feature is not None: + record(settings.EVENT_PROCESSING_STORE, app_feature, len(payload), UsageUnit.BYTES) + except Exception: + pass + + if attachments: + with sentry_sdk.start_span(op="ingest_consumer.set_attachment_cache"): + attachment_objects = [ + CachedAttachment(type=attachment.pop("attachment_type"), **attachment) + for attachment in attachments + ] + + attachment_cache.set( + cache_key, attachments=attachment_objects, timeout=CACHE_TIMEOUT + ) + + if data.get("type") == "transaction": + # No need for preprocess/process for transactions thus submit + # directly transaction specific save_event task. + save_event_transaction.delay( cache_key=cache_key, - data=data, + data=None, start_time=start_time, event_id=event_id, - project=project, - has_attachments=bool(attachments), + project_id=project_id, ) - - # remember for an 1 hour that we saved this event (deduplication protection) - cache.set(deduplication_key, "", CACHE_TIMEOUT) - - # emit event_accepted once everything is done - event_accepted.send_robust(ip=remote_addr, data=data, project=project, sender=process_event) + elif data.get("type") == "feedback": + if features.has("organizations:user-feedback-ingest", project.organization, actor=None): + save_event_feedback.delay( + cache_key=None, # no need to cache as volume is low + data=data, + start_time=start_time, + event_id=event_id, + project_id=project_id, + ) + else: + # Preprocess this event, which spawns either process_event or + # save_event. Pass data explicitly to avoid fetching it again from the + # cache. + with sentry_sdk.start_span(op="ingest_consumer.process_event.preprocess_event"): + preprocess_event( + cache_key=cache_key, + data=data, + start_time=start_time, + event_id=event_id, + project=project, + has_attachments=bool(attachments), + ) + + # remember for an 1 hour that we saved this event (deduplication protection) + cache.set(deduplication_key, "", CACHE_TIMEOUT) + + # emit event_accepted once everything is done + event_accepted.send_robust(ip=remote_addr, data=data, project=project, sender=process_event) + except Exception as exc: + raise Retriable(exc) @trace_func(name="ingest_consumer.process_attachment_chunk") diff --git a/src/sentry/ingest/consumer/simple_event.py b/src/sentry/ingest/consumer/simple_event.py index 2b28f2ae9c69fa..e1278ab8890c06 100644 --- a/src/sentry/ingest/consumer/simple_event.py +++ b/src/sentry/ingest/consumer/simple_event.py @@ -10,7 +10,7 @@ from sentry.models.project import Project from sentry.utils import metrics -from .processors import IngestMessage, process_event +from .processors import IngestMessage, Retriable, process_event logger = logging.getLogger(__name__) @@ -66,10 +66,12 @@ def process_simple_event_message( return process_event(message, project, reprocess_only_stuck_events) - except Exception: - # Any exception that fails schema validation will raise InvalidMessage, which Arroyo will DLQ. - # Messages that pass schema validation will not be DLQed as they may be retriable. + except Exception as exc: + # If the retriable exception was raised, we should not DLQ + if isinstance(exc, Retriable): + raise + # If no retriable exception was raised, check the schema to decide whether to DLQ default_topic = consumer_type_to_default_topic[consumer_type].value # TODO: Currently, there is only a schema for ingest-events, so just continue to re-raise From 6550ee1e9a8b92cb8c45d360ec1a0c2012f01a32 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Fri, 15 Mar 2024 12:53:06 -0700 Subject: [PATCH 8/8] test --- tests/relay_integration/test_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/relay_integration/test_sdk.py b/tests/relay_integration/test_sdk.py index e09817076f5362..b2f78ff032a97e 100644 --- a/tests/relay_integration/test_sdk.py +++ b/tests/relay_integration/test_sdk.py @@ -89,7 +89,7 @@ def test_recursion_breaker(settings, post_event_with_sdk): with mock.patch( "sentry.event_manager.EventManager.save", spec=Event, side_effect=ValueError("oh no!") ) as save: - with pytest.raises(ValueError): + with pytest.raises(Exception): post_event_with_sdk({"message": "internal client test", "event_id": event_id}) assert_mock_called_once_with_partial(save, settings.SENTRY_PROJECT, cache_key=f"e:{event_id}:1")