From de6856f5b06d5d516fac5655b052f252e0b62cb3 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 9 May 2025 08:35:44 -0400 Subject: [PATCH 01/16] feat(logs): Forward extra from logger as attributes (#4374) resolves https://linear.app/getsentry/issue/LOGS-101 --- sentry_sdk/integrations/logging.py | 10 ++-- tests/test_logs.py | 74 +++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 46628bb04b..74baf3d33a 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -348,17 +348,15 @@ def emit(self, record): if not client.options["_experiments"].get("enable_logs", False): return - SentryLogsHandler._capture_log_from_record(client, record) + self._capture_log_from_record(client, record) - @staticmethod - def _capture_log_from_record(client, record): + def _capture_log_from_record(self, client, record): # type: (BaseClient, LogRecord) -> None scope = sentry_sdk.get_current_scope() otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno) project_root = client.options["project_root"] - attrs = { - "sentry.origin": "auto.logger.log", - } # type: dict[str, str | bool | float | int] + attrs = self._extra_from_record(record) # type: Any + attrs["sentry.origin"] = "auto.logger.log" if isinstance(record.msg, str): attrs["sentry.message.template"] = record.msg if record.args is not None: diff --git a/tests/test_logs.py b/tests/test_logs.py index 49ffd31ec7..1f6b07e762 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -30,7 +30,7 @@ def _convert_attr(attr): return attr["value"] if attr["value"].startswith("{"): try: - return json.loads(attr["stringValue"]) + return json.loads(attr["value"]) except ValueError: pass return str(attr["value"]) @@ -393,6 +393,78 @@ def test_log_strips_project_root(sentry_init, capture_envelopes): assert attrs["code.file.path"] == "blah/path.py" +def test_logger_with_all_attributes(sentry_init, capture_envelopes): + """ + The python logger should be able to log all attributes, including extra data. + """ + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.warning( + "log #%d", + 1, + extra={"foo": "bar", "numeric": 42, "more_complex": {"nested": "data"}}, + ) + get_client().flush() + + logs = envelopes_to_logs(envelopes) + + attributes = logs[0]["attributes"] + + assert "process.pid" in attributes + assert isinstance(attributes["process.pid"], int) + del attributes["process.pid"] + + assert "sentry.release" in attributes + assert isinstance(attributes["sentry.release"], str) + del attributes["sentry.release"] + + assert "server.address" in attributes + assert isinstance(attributes["server.address"], str) + del attributes["server.address"] + + assert "thread.id" in attributes + assert isinstance(attributes["thread.id"], int) + del attributes["thread.id"] + + assert "code.file.path" in attributes + assert isinstance(attributes["code.file.path"], str) + del attributes["code.file.path"] + + assert "code.function.name" in attributes + assert isinstance(attributes["code.function.name"], str) + del attributes["code.function.name"] + + assert "code.line.number" in attributes + assert isinstance(attributes["code.line.number"], int) + del attributes["code.line.number"] + + assert "process.executable.name" in attributes + assert isinstance(attributes["process.executable.name"], str) + del attributes["process.executable.name"] + + assert "thread.name" in attributes + assert isinstance(attributes["thread.name"], str) + del attributes["thread.name"] + + # Assert on the remaining non-dynamic attributes. + assert attributes == { + "foo": "bar", + "numeric": 42, + "more_complex": "{'nested': 'data'}", + "logger.name": "test-logger", + "sentry.origin": "auto.logger.log", + "sentry.message.template": "log #%d", + "sentry.message.parameters.0": 1, + "sentry.environment": "production", + "sentry.sdk.name": "sentry.python", + "sentry.sdk.version": VERSION, + "sentry.severity_number": 13, + "sentry.severity_text": "warn", + } + + def test_auto_flush_logs_after_100(sentry_init, capture_envelopes): """ If you log >100 logs, it should automatically trigger a flush. From 53f413e4cb0ec700a4dfa693557afa63aed47c10 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 12 May 2025 07:39:55 +0000 Subject: [PATCH 02/16] release: 2.28.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 786a9a34e5..17e1086b80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 2.28.0 + +### Various fixes & improvements + +- feat(logs): Forward extra from logger as attributes (#4374) by @AbhiPrasad +- Make use of `SPANDATA` consistent (#4373) by @antonpirker +- Fix Discord link (#4371) by @sentrivana +- Pin snowballstemmer for now (#4372) by @sentrivana +- tests: Regular tox update (#4367) by @sentrivana +- build(deps): bump actions/create-github-app-token from 2.0.2 to 2.0.6 (#4358) by @dependabot +- Put feature flags on isolation scope (#4363) by @antonpirker +- tests: bump test timeout for recursion stacktrace extract to 2s (#4351) by @booxter +- feat(ourlogs): canonicalize paths from the logger integration (#4336) by @colin-sentry +- chore(ourlogs): Use new transport (#4317) by @colin-sentry +- tests: fix test_stacktrace_big_recursion failure due to argv (#4346) by @booxter +- tests: Move anthropic under toxgen (#4348) by @sentrivana +- Deprecate `set_measurement()` API. (#3934) by @antonpirker +- tests: Update tox.ini (#4347) by @sentrivana +- Update GH issue templates for Linear compatibility (#4328) by @stephanie-anderson + ## 2.27.0 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 709f557d16..34c88ae6cd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.27.0" +release = "2.28.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index e3c29fc2d4..2ceec2738b 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1025,4 +1025,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.27.0" +VERSION = "2.28.0" diff --git a/setup.py b/setup.py index 877585472b..8fd1ae6293 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.27.0", + version="2.28.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From c7a17a06836587a89766bb934ebf11d4b87d6353 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 12 May 2025 09:44:22 +0200 Subject: [PATCH 03/16] Update changelog.md --- CHANGELOG.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e1086b80..81f749d83a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,21 +4,21 @@ ### Various fixes & improvements -- feat(logs): Forward extra from logger as attributes (#4374) by @AbhiPrasad -- Make use of `SPANDATA` consistent (#4373) by @antonpirker -- Fix Discord link (#4371) by @sentrivana -- Pin snowballstemmer for now (#4372) by @sentrivana +- fix(logs): Forward `extra` from logger as attributes (#4374) by @AbhiPrasad +- fix(logs): Canonicalize paths from the logger integration (#4336) by @colin-sentry +- fix(logs): Use new transport (#4317) by @colin-sentry +- fix: Deprecate `set_measurement()` API. (#3934) by @antonpirker +- fix: Put feature flags on isolation scope (#4363) by @antonpirker +- fix: Make use of `SPANDATA` consistent (#4373) by @antonpirker +- fix: Discord link (#4371) by @sentrivana +- tests: Pin snowballstemmer for now (#4372) by @sentrivana - tests: Regular tox update (#4367) by @sentrivana -- build(deps): bump actions/create-github-app-token from 2.0.2 to 2.0.6 (#4358) by @dependabot -- Put feature flags on isolation scope (#4363) by @antonpirker -- tests: bump test timeout for recursion stacktrace extract to 2s (#4351) by @booxter -- feat(ourlogs): canonicalize paths from the logger integration (#4336) by @colin-sentry -- chore(ourlogs): Use new transport (#4317) by @colin-sentry -- tests: fix test_stacktrace_big_recursion failure due to argv (#4346) by @booxter +- tests: Bump test timeout for recursion stacktrace extract to 2s (#4351) by @booxter +- tests: Fix test_stacktrace_big_recursion failure due to argv (#4346) by @booxter - tests: Move anthropic under toxgen (#4348) by @sentrivana -- Deprecate `set_measurement()` API. (#3934) by @antonpirker - tests: Update tox.ini (#4347) by @sentrivana -- Update GH issue templates for Linear compatibility (#4328) by @stephanie-anderson +- chore: Update GH issue templates for Linear compatibility (#4328) by @stephanie-anderson +- chore: Bump actions/create-github-app-token from 2.0.2 to 2.0.6 (#4358) by @dependabot ## 2.27.0 From 1090eeed8cd18f41d358b0dc0508d55fe614a05b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 12 May 2025 11:31:07 +0200 Subject: [PATCH 04/16] apidocs: Remove snowballstemmer pin (#4379) Fixed upstream --- requirements-docs.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index a662a0d83f..81e04ba3ef 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -3,4 +3,3 @@ shibuya sphinx<8.2 sphinx-autodoc-typehints[type_comments]>=1.8.0 typing-extensions -snowballstemmer<3.0 From eee4cac8f8ae44e5bee9364a482597680b81b52f Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 13 May 2025 10:23:19 -0400 Subject: [PATCH 05/16] fix(logs): Make `sentry.message.parameters` singular as per spec (#4387) As per https://develop.sentry.dev/sdk/telemetry/logs/#default-attributes we should be using `sentry.message.parameter` (singular) instead of `sentry.message.parameters`. This matches how we approach the other attributes in our conventions. --- sentry_sdk/integrations/logging.py | 2 +- sentry_sdk/logger.py | 2 +- tests/test_logs.py | 23 +++++++++++------------ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 74baf3d33a..449a05fbf7 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -362,7 +362,7 @@ def _capture_log_from_record(self, client, record): if record.args is not None: if isinstance(record.args, tuple): for i, arg in enumerate(record.args): - attrs[f"sentry.message.parameters.{i}"] = ( + attrs[f"sentry.message.parameter.{i}"] = ( arg if isinstance(arg, str) or isinstance(arg, float) diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index 1fa31b786b..704d7d4f9d 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -18,7 +18,7 @@ def _capture_log(severity_text, severity_number, template, **kwargs): if "attributes" in kwargs: attrs.update(kwargs.pop("attributes")) for k, v in kwargs.items(): - attrs[f"sentry.message.parameters.{k}"] = v + attrs[f"sentry.message.parameter.{k}"] = v attrs = { k: ( diff --git a/tests/test_logs.py b/tests/test_logs.py index 1f6b07e762..ec8ce917c2 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -186,7 +186,7 @@ def test_logs_attributes(sentry_init, capture_envelopes): assert logs[0]["attributes"][k] == v assert logs[0]["attributes"]["sentry.environment"] == "production" assert "sentry.release" in logs[0]["attributes"] - assert logs[0]["attributes"]["sentry.message.parameters.my_var"] == "some value" + assert logs[0]["attributes"]["sentry.message.parameter.my_var"] == "some value" assert logs[0]["attributes"][SPANDATA.SERVER_ADDRESS] == "test-server" assert logs[0]["attributes"]["sentry.sdk.name"].startswith("sentry.python") assert logs[0]["attributes"]["sentry.sdk.version"] == VERSION @@ -214,23 +214,23 @@ def test_logs_message_params(sentry_init, capture_envelopes): logs = envelopes_to_logs(envelopes) assert logs[0]["body"] == "The recorded value was '1'" - assert logs[0]["attributes"]["sentry.message.parameters.int_var"] == 1 + assert logs[0]["attributes"]["sentry.message.parameter.int_var"] == 1 assert logs[1]["body"] == "The recorded value was '2.0'" - assert logs[1]["attributes"]["sentry.message.parameters.float_var"] == 2.0 + assert logs[1]["attributes"]["sentry.message.parameter.float_var"] == 2.0 assert logs[2]["body"] == "The recorded value was 'False'" - assert logs[2]["attributes"]["sentry.message.parameters.bool_var"] is False + assert logs[2]["attributes"]["sentry.message.parameter.bool_var"] is False assert logs[3]["body"] == "The recorded value was 'some string value'" assert ( - logs[3]["attributes"]["sentry.message.parameters.string_var"] + logs[3]["attributes"]["sentry.message.parameter.string_var"] == "some string value" ) assert logs[4]["body"] == "The recorded error was 'some error'" assert ( - logs[4]["attributes"]["sentry.message.parameters.error"] + logs[4]["attributes"]["sentry.message.parameter.error"] == "Exception('some error')" ) @@ -287,8 +287,8 @@ def test_logger_integration_warning(sentry_init, capture_envelopes): assert "code.line.number" in attrs assert attrs["logger.name"] == "test-logger" assert attrs["sentry.environment"] == "production" - assert attrs["sentry.message.parameters.0"] == "1" - assert attrs["sentry.message.parameters.1"] == "2" + assert attrs["sentry.message.parameter.0"] == "1" + assert attrs["sentry.message.parameter.1"] == "2" assert attrs["sentry.origin"] == "auto.logger.log" assert logs[0]["severity_number"] == 13 assert logs[0]["severity_text"] == "warn" @@ -349,14 +349,13 @@ def test_logging_errors(sentry_init, capture_envelopes): logs = envelopes_to_logs(envelopes) assert logs[0]["severity_text"] == "error" assert "sentry.message.template" not in logs[0]["attributes"] - assert "sentry.message.parameters.0" not in logs[0]["attributes"] + assert "sentry.message.parameter.0" not in logs[0]["attributes"] assert "code.line.number" in logs[0]["attributes"] assert logs[1]["severity_text"] == "error" assert logs[1]["attributes"]["sentry.message.template"] == "error is %s" assert ( - logs[1]["attributes"]["sentry.message.parameters.0"] - == "Exception('test exc 2')" + logs[1]["attributes"]["sentry.message.parameter.0"] == "Exception('test exc 2')" ) assert "code.line.number" in logs[1]["attributes"] @@ -456,7 +455,7 @@ def test_logger_with_all_attributes(sentry_init, capture_envelopes): "logger.name": "test-logger", "sentry.origin": "auto.logger.log", "sentry.message.template": "log #%d", - "sentry.message.parameters.0": 1, + "sentry.message.parameter.0": 1, "sentry.environment": "production", "sentry.sdk.name": "sentry.python", "sentry.sdk.version": VERSION, From c639f1c083eefb6b4cc67164d9d8dfa9ac568b98 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 15 May 2025 11:40:28 +0200 Subject: [PATCH 06/16] ci: Fix pyspark test suite (#4382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pyspark was failing to bind to an address - aws_lambda fixed itself 🪄 --- tests/integrations/spark/test_spark.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integrations/spark/test_spark.py b/tests/integrations/spark/test_spark.py index 7eeab15dc4..91882a0b8f 100644 --- a/tests/integrations/spark/test_spark.py +++ b/tests/integrations/spark/test_spark.py @@ -10,7 +10,7 @@ ) from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration -from pyspark import SparkContext +from pyspark import SparkConf, SparkContext from py4j.protocol import Py4JJavaError @@ -25,12 +25,13 @@ def sentry_init_with_reset(sentry_init): from sentry_sdk.integrations import _processed_integrations yield lambda: sentry_init(integrations=[SparkIntegration()]) - _processed_integrations.remove("spark") + _processed_integrations.discard("spark") @pytest.fixture(scope="function") def create_spark_context(): - yield lambda: SparkContext(appName="Testing123") + conf = SparkConf().set("spark.driver.bindAddress", "127.0.0.1") + yield lambda: SparkContext(conf=conf, appName="Testing123") SparkContext._active_spark_context.stop() From aa0eaab19a9867ea5b80f8aab3ddc6c2e9293835 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 15 May 2025 11:40:48 +0200 Subject: [PATCH 07/16] fix(typing): Add before_send_log to Experiments (#4383) --- sentry_sdk/consts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 2ceec2738b..30461b4524 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1,5 +1,4 @@ import itertools - from enum import Enum from typing import TYPE_CHECKING @@ -47,6 +46,7 @@ class CompressionAlgo(Enum): Event, EventProcessor, Hint, + Log, MeasurementUnit, ProfilerMode, TracesSampler, @@ -79,6 +79,7 @@ class CompressionAlgo(Enum): ], "metric_code_locations": Optional[bool], "enable_logs": Optional[bool], + "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], }, total=False, ) From 2f97cdaf53eee2737b6dc4aab1d99e91d33b0b76 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 19 May 2025 10:53:31 +0200 Subject: [PATCH 08/16] fix(celery): Do not send extra check-in (#4395) When using the RedBeatScheduler, we're sending an extra in-progress check-in at scheduler start. Since this is never followed by an ok or error check-in, the check-in is marked as timed out in Sentry. We're patching the scheduler's `maybe_due`, which (as the name implies) [might not end up executing the task](https://github.com/sibson/redbeat/blob/bdefad23b47f75e2e85d45f46f9f16dd00a93d40/redbeat/schedulers.py#L506-L508). This is indeed what seems to be happening -- `maybe_due` is run when the scheduler starts, but without scheduling the task. We don't check whether `maybe_due` actually ended up scheduling anything and always fire an in-progress check-in. Patching the [scheduler's `apply_async`](https://github.com/sibson/redbeat/blob/bdefad23b47f75e2e85d45f46f9f16dd00a93d40/redbeat/schedulers.py#L511) instead. Closes https://github.com/getsentry/sentry-python/issues/4392 --- sentry_sdk/integrations/celery/__init__.py | 4 ++-- sentry_sdk/integrations/celery/beat.py | 4 ++-- .../celery/test_celery_beat_crons.py | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/celery/__init__.py b/sentry_sdk/integrations/celery/__init__.py index e8811d767e..d8d89217ca 100644 --- a/sentry_sdk/integrations/celery/__init__.py +++ b/sentry_sdk/integrations/celery/__init__.py @@ -9,7 +9,7 @@ from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.integrations.celery.beat import ( _patch_beat_apply_entry, - _patch_redbeat_maybe_due, + _patch_redbeat_apply_async, _setup_celery_beat_signals, ) from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch @@ -73,7 +73,7 @@ def __init__( self.exclude_beat_tasks = exclude_beat_tasks _patch_beat_apply_entry() - _patch_redbeat_maybe_due() + _patch_redbeat_apply_async() _setup_celery_beat_signals(monitor_beat_tasks) @staticmethod diff --git a/sentry_sdk/integrations/celery/beat.py b/sentry_sdk/integrations/celery/beat.py index ddbc8561a4..4b7e45e6f0 100644 --- a/sentry_sdk/integrations/celery/beat.py +++ b/sentry_sdk/integrations/celery/beat.py @@ -202,12 +202,12 @@ def _patch_beat_apply_entry(): Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry) -def _patch_redbeat_maybe_due(): +def _patch_redbeat_apply_async(): # type: () -> None if RedBeatScheduler is None: return - RedBeatScheduler.maybe_due = _wrap_beat_scheduler(RedBeatScheduler.maybe_due) + RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async) def _setup_celery_beat_signals(monitor_beat_tasks): diff --git a/tests/integrations/celery/test_celery_beat_crons.py b/tests/integrations/celery/test_celery_beat_crons.py index 58c4c6208d..17b4a5e73d 100644 --- a/tests/integrations/celery/test_celery_beat_crons.py +++ b/tests/integrations/celery/test_celery_beat_crons.py @@ -10,7 +10,7 @@ _get_headers, _get_monitor_config, _patch_beat_apply_entry, - _patch_redbeat_maybe_due, + _patch_redbeat_apply_async, crons_task_failure, crons_task_retry, crons_task_success, @@ -454,10 +454,10 @@ def test_exclude_redbeat_tasks_option( """ Test excluding Celery RedBeat tasks from automatic instrumentation. """ - fake_maybe_due = MagicMock() + fake_apply_async = MagicMock() fake_redbeat_scheduler = MagicMock() - fake_redbeat_scheduler.maybe_due = fake_maybe_due + fake_redbeat_scheduler.apply_async = fake_apply_async fake_integration = MagicMock() fake_integration.exclude_beat_tasks = exclude_beat_tasks @@ -481,17 +481,19 @@ def test_exclude_redbeat_tasks_option( "sentry_sdk.integrations.celery.beat._get_monitor_config", fake_get_monitor_config, ) as _get_monitor_config: - # Mimic CeleryIntegration patching of RedBeatScheduler.maybe_due() - _patch_redbeat_maybe_due() + # Mimic CeleryIntegration patching of RedBeatScheduler.apply_async() + _patch_redbeat_apply_async() # Mimic Celery RedBeat calling a task from the RedBeat schedule - RedBeatScheduler.maybe_due(fake_redbeat_scheduler, fake_schedule_entry) + RedBeatScheduler.apply_async( + fake_redbeat_scheduler, fake_schedule_entry + ) if task_in_excluded_beat_tasks: # Only the original RedBeatScheduler.maybe_due() is called, _get_monitor_config is NOT called. - assert fake_maybe_due.call_count == 1 + assert fake_apply_async.call_count == 1 _get_monitor_config.assert_not_called() else: # The original RedBeatScheduler.maybe_due() is called, AND _get_monitor_config is called. - assert fake_maybe_due.call_count == 1 + assert fake_apply_async.call_count == 1 assert _get_monitor_config.call_count == 1 From f5727572281578a73b21cd757288b820932eded6 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Mon, 19 May 2025 08:42:22 -0400 Subject: [PATCH 09/16] feat: Allow configuring `keep_alive` via environment variable (#4366) This commit enables the `keep_alive` option to be set via the `SENTRY_KEEP_ALIVE` environment variable. When both the environment variable and the argument are provided, the argument takes precedence. Closes #4354 --- sentry_sdk/client.py | 5 ++++ sentry_sdk/consts.py | 2 +- tests/test_client.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index f06166bcc8..9747c792d9 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -142,6 +142,11 @@ def _get_options(*args, **kwargs): ) rv["socket_options"] = None + if rv["keep_alive"] is None: + rv["keep_alive"] = ( + env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False + ) + if rv["enable_tracing"] is not None: warnings.warn( "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.", diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 30461b4524..ed81a512ba 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -578,7 +578,7 @@ def __init__( ignore_errors=[], # type: Sequence[Union[type, str]] # noqa: B006 max_request_body_size="medium", # type: str socket_options=None, # type: Optional[List[Tuple[int, int, int | bytes]]] - keep_alive=False, # type: bool + keep_alive=None, # type: Optional[bool] before_send=None, # type: Optional[EventProcessor] before_breadcrumb=None, # type: Optional[BreadcrumbProcessor] debug=None, # type: Optional[bool] diff --git a/tests/test_client.py b/tests/test_client.py index 67f53d989a..2986920452 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,3 +1,4 @@ +import contextlib import os import json import subprocess @@ -1496,3 +1497,66 @@ def test_dropped_transaction(sentry_init, capture_record_lost_event_calls, test_ def test_enable_tracing_deprecated(sentry_init, enable_tracing): with pytest.warns(DeprecationWarning): sentry_init(enable_tracing=enable_tracing) + + +def make_options_transport_cls(): + """Make an options transport class that captures the options passed to it.""" + # We need a unique class for each test so that the options are not + # shared between tests. + + class OptionsTransport(Transport): + """Transport that captures the options passed to it.""" + + def __init__(self, options): + super().__init__(options) + type(self).options = options + + def capture_envelope(self, _): + pass + + return OptionsTransport + + +@contextlib.contextmanager +def clear_env_var(name): + """Helper to clear the a given environment variable, + and restore it to its original value on exit.""" + old_value = os.environ.pop(name, None) + + try: + yield + finally: + if old_value is not None: + os.environ[name] = old_value + elif name in os.environ: + del os.environ[name] + + +@pytest.mark.parametrize( + ("env_value", "arg_value", "expected_value"), + [ + (None, None, False), # default + ("0", None, False), # env var false + ("1", None, True), # env var true + (None, False, False), # arg false + (None, True, True), # arg true + # Argument overrides environment variable + ("0", True, True), # env false, arg true + ("1", False, False), # env true, arg false + ], +) +def test_keep_alive(env_value, arg_value, expected_value): + transport_cls = make_options_transport_cls() + keep_alive_kwarg = {} if arg_value is None else {"keep_alive": arg_value} + + with clear_env_var("SENTRY_KEEP_ALIVE"): + if env_value is not None: + os.environ["SENTRY_KEEP_ALIVE"] = env_value + + sentry_sdk.init( + dsn="http://foo@sentry.io/123", + transport=transport_cls, + **keep_alive_kwarg, + ) + + assert transport_cls.options["keep_alive"] is expected_value From 830f270791270bd31462acd879ee7a0e8a2926c3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 19 May 2025 15:05:16 +0200 Subject: [PATCH 10/16] fix(loguru): Move integration setup from `__init__` to `setup_once` (#4399) We shouldn't have setup code in an integration's `__init__`. This can be called an arbitrary amount of times. `setup_once` exists so that the code is only guaranteed to run once. Having integration setup in `__init__` also means that the integration currently can't be disabled via `disabled_integrations`. Fixes https://github.com/getsentry/sentry-python/issues/4398 --- sentry_sdk/integrations/loguru.py | 55 ++++++++++-------------- tests/integrations/loguru/test_loguru.py | 17 ++++++-- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index 5b76ea812a..a71c4ac87f 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from logging import LogRecord - from typing import Optional, Tuple, Any + from typing import Optional, Any try: import loguru @@ -43,16 +43,16 @@ class LoggingLevels(enum.IntEnum): DEFAULT_LEVEL = LoggingLevels.INFO.value DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value -# We need to save the handlers to be able to remove them later -# in tests (they call `LoguruIntegration.__init__` multiple times, -# and we can't use `setup_once` because it's called before -# than we get configuration). -_ADDED_HANDLERS = (None, None) # type: Tuple[Optional[int], Optional[int]] class LoguruIntegration(Integration): identifier = "loguru" + level = DEFAULT_LEVEL # type: Optional[int] + event_level = DEFAULT_EVENT_LEVEL # type: Optional[int] + breadcrumb_format = DEFAULT_FORMAT + event_format = DEFAULT_FORMAT + def __init__( self, level=DEFAULT_LEVEL, @@ -61,36 +61,27 @@ def __init__( event_format=DEFAULT_FORMAT, ): # type: (Optional[int], Optional[int], str | loguru.FormatFunction, str | loguru.FormatFunction) -> None - global _ADDED_HANDLERS - breadcrumb_handler, event_handler = _ADDED_HANDLERS - - if breadcrumb_handler is not None: - logger.remove(breadcrumb_handler) - breadcrumb_handler = None - if event_handler is not None: - logger.remove(event_handler) - event_handler = None - - if level is not None: - breadcrumb_handler = logger.add( - LoguruBreadcrumbHandler(level=level), - level=level, - format=breadcrumb_format, - ) - - if event_level is not None: - event_handler = logger.add( - LoguruEventHandler(level=event_level), - level=event_level, - format=event_format, - ) - - _ADDED_HANDLERS = (breadcrumb_handler, event_handler) + LoguruIntegration.level = level + LoguruIntegration.event_level = event_level + LoguruIntegration.breadcrumb_format = breadcrumb_format + LoguruIntegration.event_format = event_format @staticmethod def setup_once(): # type: () -> None - pass # we do everything in __init__ + if LoguruIntegration.level is not None: + logger.add( + LoguruBreadcrumbHandler(level=LoguruIntegration.level), + level=LoguruIntegration.level, + format=LoguruIntegration.breadcrumb_format, + ) + + if LoguruIntegration.event_level is not None: + logger.add( + LoguruEventHandler(level=LoguruIntegration.event_level), + level=LoguruIntegration.event_level, + format=LoguruIntegration.event_format, + ) class _LoguruBaseHandler(_BaseHandler): diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index 64e9f22ba5..6be09b86dc 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -32,7 +32,12 @@ def test_just_log( expected_sentry_level, disable_breadcrumbs, disable_events, + uninstall_integration, + request, ): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + sentry_init( integrations=[ LoguruIntegration( @@ -49,7 +54,7 @@ def test_just_log( formatted_message = ( " | " + "{:9}".format(level.name.upper()) - + "| tests.integrations.loguru.test_loguru:test_just_log:47 - test" + + "| tests.integrations.loguru.test_loguru:test_just_log:52 - test" ) if not created_event: @@ -78,7 +83,10 @@ def test_just_log( assert event["logentry"]["message"][23:] == formatted_message -def test_breadcrumb_format(sentry_init, capture_events): +def test_breadcrumb_format(sentry_init, capture_events, uninstall_integration, request): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + sentry_init( integrations=[ LoguruIntegration( @@ -98,7 +106,10 @@ def test_breadcrumb_format(sentry_init, capture_events): assert breadcrumb["message"] == formatted_message -def test_event_format(sentry_init, capture_events): +def test_event_format(sentry_init, capture_events, uninstall_integration, request): + uninstall_integration("loguru") + request.addfinalizer(logger.remove) + sentry_init( integrations=[ LoguruIntegration( From 0c9333ed8bc2c4da15106fd37b4fa9fd3e9aed93 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 19 May 2025 13:30:09 +0000 Subject: [PATCH 11/16] release: 2.29.0 --- CHANGELOG.md | 12 ++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f749d83a..35ee9056dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 2.29.0 + +### Various fixes & improvements + +- fix(loguru): Move integration setup from `__init__` to `setup_once` (#4399) by @sentrivana +- feat: Allow configuring `keep_alive` via environment variable (#4366) by @szokeasaurusrex +- fix(celery): Do not send extra check-in (#4395) by @sentrivana +- fix(typing): Add before_send_log to Experiments (#4383) by @sentrivana +- ci: Fix pyspark test suite (#4382) by @sentrivana +- fix(logs): Make `sentry.message.parameters` singular as per spec (#4387) by @AbhiPrasad +- apidocs: Remove snowballstemmer pin (#4379) by @sentrivana + ## 2.28.0 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 34c88ae6cd..7735eab04f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.28.0" +release = "2.29.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index ed81a512ba..a5a6343b0b 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1026,4 +1026,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.28.0" +VERSION = "2.29.0" diff --git a/setup.py b/setup.py index 8fd1ae6293..543882dcd6 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.28.0", + version="2.29.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From 3ebd122604835bdf5cf440df1d2794490ce8b5f0 Mon Sep 17 00:00:00 2001 From: Lorenzo Cian Date: Mon, 19 May 2025 16:10:31 +0200 Subject: [PATCH 12/16] fix(logs): send `severity_text`: `warn` instead of `warning` (#4396) We need to send `warn` as the `severity_text` for warning level logs, not `warning`. If we send `warning` it will not be recognized by the backend and the severity will show up as `UNKNOWN` in the frontend. See screenshot for comparison of after and before. ![Screenshot 2025-05-16 at 18 24 50](https://github.com/user-attachments/assets/240e3c6d-0f68-49e8-b79b-a67ac0e8a5d4) --- sentry_sdk/logger.py | 2 +- tests/test_logs.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/logger.py b/sentry_sdk/logger.py index 704d7d4f9d..c675c4d95d 100644 --- a/sentry_sdk/logger.py +++ b/sentry_sdk/logger.py @@ -51,6 +51,6 @@ def _capture_log(severity_text, severity_number, template, **kwargs): trace = functools.partial(_capture_log, "trace", 1) debug = functools.partial(_capture_log, "debug", 5) info = functools.partial(_capture_log, "info", 9) -warning = functools.partial(_capture_log, "warning", 13) +warning = functools.partial(_capture_log, "warn", 13) error = functools.partial(_capture_log, "error", 17) fatal = functools.partial(_capture_log, "fatal", 21) diff --git a/tests/test_logs.py b/tests/test_logs.py index ec8ce917c2..7b886b7a36 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -102,7 +102,7 @@ def test_logs_basics(sentry_init, capture_envelopes): assert logs[2].get("severity_text") == "info" assert logs[2].get("severity_number") == 9 - assert logs[3].get("severity_text") == "warning" + assert logs[3].get("severity_text") == "warn" assert logs[3].get("severity_number") == 13 assert logs[4].get("severity_text") == "error" @@ -155,7 +155,7 @@ def _before_log(record, hint): assert logs[0]["severity_text"] == "trace" assert logs[1]["severity_text"] == "debug" assert logs[2]["severity_text"] == "info" - assert logs[3]["severity_text"] == "warning" + assert logs[3]["severity_text"] == "warn" assert before_log_called[0] From 7973ac095c0ecedba4514f11042e1536bb1234ee Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 19 May 2025 14:20:49 +0000 Subject: [PATCH 13/16] release: 2.29.1 --- CHANGELOG.md | 6 ++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ee9056dd..8d73e1bdd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.29.1 + +### Various fixes & improvements + +- fix(logs): send `severity_text`: `warn` instead of `warning` (#4396) by @lcian + ## 2.29.0 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 7735eab04f..e639185528 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.29.0" +release = "2.29.1" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index a5a6343b0b..241d197699 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1026,4 +1026,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.29.0" +VERSION = "2.29.1" diff --git a/setup.py b/setup.py index 543882dcd6..92634bd2ef 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.29.0", + version="2.29.1", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From 83e0386b4dfc3a40d0301fa90f5750dade4bb024 Mon Sep 17 00:00:00 2001 From: Roman Inflianskas Date: Tue, 20 May 2025 10:20:06 +0300 Subject: [PATCH 14/16] tests(logs): avoid failures when running with integrations enabled (#4388) When (at least) one of integrations is enabled (because some dependencies are installed in the environment), `sentry.sdk.name` is changed from `sentry.python` to `sentry.python.[FIRST_ENABLED_INTEGRATION]` which makes `test_logs_attributes` fail. Prevent failure by relaxing the check. This change is beneficial not only for packaging (this patch was required for packaging for Fedora), but also for running tests with `pytest` directly. See also: https://github.com/getsentry/sentry-python/pull/4316 --- Thank you for contributing to `sentry-python`! Please add tests to validate your changes, and lint your code using `tox -e linters`. Running the test suite on your PR might require maintainer approval. --- tests/test_logs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_logs.py b/tests/test_logs.py index 7b886b7a36..7e8a72d30a 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -447,6 +447,8 @@ def test_logger_with_all_attributes(sentry_init, capture_envelopes): assert isinstance(attributes["thread.name"], str) del attributes["thread.name"] + assert attributes.pop("sentry.sdk.name").startswith("sentry.python") + # Assert on the remaining non-dynamic attributes. assert attributes == { "foo": "bar", @@ -457,7 +459,6 @@ def test_logger_with_all_attributes(sentry_init, capture_envelopes): "sentry.message.template": "log #%d", "sentry.message.parameter.0": 1, "sentry.environment": "production", - "sentry.sdk.name": "sentry.python", "sentry.sdk.version": VERSION, "sentry.severity_number": 13, "sentry.severity_text": "warn", From 94414cdcdce2a311355967ee7b43e7035cf954ce Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Tue, 20 May 2025 04:21:49 -0300 Subject: [PATCH 15/16] Increase test coverage (#4393) Fixes GH-3515 Increase test coverage for `attachments.py` and `utils.py`. --------- Co-authored-by: Anton Pirker --- tests/test_basics.py | 60 ++++++++++ tests/test_utils.py | 188 +++++++++++++++++++++++++------- tests/utils/test_contextvars.py | 15 ++- tests/utils/test_general.py | 39 +++++++ 4 files changed, 258 insertions(+), 44 deletions(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index 0fdf9f811f..2eeba78216 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -515,6 +515,66 @@ def test_attachments_graceful_failure( assert envelope.items[1].payload.get_bytes() == b"" +def test_attachments_exceptions(sentry_init): + sentry_init() + + scope = sentry_sdk.get_isolation_scope() + + # bytes and path are None + with pytest.raises(TypeError) as e: + scope.add_attachment() + + assert str(e.value) == "path or raw bytes required for attachment" + + # filename is None + with pytest.raises(TypeError) as e: + scope.add_attachment(bytes=b"Hello World!") + + assert str(e.value) == "filename is required for attachment" + + +def test_attachments_content_type_is_none(sentry_init, capture_envelopes): + sentry_init() + envelopes = capture_envelopes() + + scope = sentry_sdk.get_isolation_scope() + + scope.add_attachment( + bytes=b"Hello World!", filename="message.txt", content_type="foo/bar" + ) + capture_exception(ValueError()) + + (envelope,) = envelopes + attachments = [x for x in envelope.items if x.type == "attachment"] + (message,) = attachments + + assert message.headers["filename"] == "message.txt" + assert message.headers["content_type"] == "foo/bar" + + +def test_attachments_repr(sentry_init): + sentry_init() + + scope = sentry_sdk.get_isolation_scope() + + scope.add_attachment(bytes=b"Hello World!", filename="message.txt") + + assert repr(scope._attachments[0]) == "" + + +def test_attachments_bytes_callable_payload(sentry_init): + sentry_init() + + scope = sentry_sdk.get_isolation_scope() + + scope.add_attachment(bytes=bytes, filename="message.txt") + + attachment = scope._attachments[0] + item = attachment.to_envelope_item() + + assert item.payload.bytes == b"" + + def test_integration_scoping(sentry_init, capture_events): logger = logging.getLogger("test_basics") diff --git a/tests/test_utils.py b/tests/test_utils.py index b731c3e3ab..efa2e7c068 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -32,6 +32,10 @@ _get_installed_modules, _generate_installed_modules, ensure_integration_enabled, + to_string, + exc_info_from_error, + get_lines_from_file, + package_version, ) @@ -61,55 +65,72 @@ def _normalize_distribution_name(name): return re.sub(r"[-_.]+", "-", name).lower() -@pytest.mark.parametrize( - ("input_str", "expected_output"), +isoformat_inputs_and_datetime_outputs = ( ( - ( - "2021-01-01T00:00:00.000000Z", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), # UTC time - ( - "2021-01-01T00:00:00.000000", - datetime(2021, 1, 1).astimezone(timezone.utc), - ), # No TZ -- assume local but convert to UTC - ( - "2021-01-01T00:00:00Z", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), # UTC - No milliseconds - ( - "2021-01-01T00:00:00.000000+00:00", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), - ( - "2021-01-01T00:00:00.000000-00:00", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), - ( - "2021-01-01T00:00:00.000000+0000", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), - ( - "2021-01-01T00:00:00.000000-0000", - datetime(2021, 1, 1, tzinfo=timezone.utc), - ), - ( - "2020-12-31T00:00:00.000000+02:00", - datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))), - ), # UTC+2 time - ( - "2020-12-31T00:00:00.000000-0200", - datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))), - ), # UTC-2 time - ( - "2020-12-31T00:00:00-0200", - datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))), - ), # UTC-2 time - no milliseconds + "2021-01-01T00:00:00.000000Z", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), # UTC time + ( + "2021-01-01T00:00:00.000000", + datetime(2021, 1, 1).astimezone(timezone.utc), + ), # No TZ -- assume local but convert to UTC + ( + "2021-01-01T00:00:00Z", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), # UTC - No milliseconds + ( + "2021-01-01T00:00:00.000000+00:00", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), + ( + "2021-01-01T00:00:00.000000-00:00", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), + ( + "2021-01-01T00:00:00.000000+0000", + datetime(2021, 1, 1, tzinfo=timezone.utc), + ), + ( + "2021-01-01T00:00:00.000000-0000", + datetime(2021, 1, 1, tzinfo=timezone.utc), ), + ( + "2020-12-31T00:00:00.000000+02:00", + datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))), + ), # UTC+2 time + ( + "2020-12-31T00:00:00.000000-0200", + datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))), + ), # UTC-2 time + ( + "2020-12-31T00:00:00-0200", + datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))), + ), # UTC-2 time - no milliseconds +) + + +@pytest.mark.parametrize( + ("input_str", "expected_output"), + isoformat_inputs_and_datetime_outputs, ) def test_datetime_from_isoformat(input_str, expected_output): assert datetime_from_isoformat(input_str) == expected_output, input_str +@pytest.mark.parametrize( + ("input_str", "expected_output"), + isoformat_inputs_and_datetime_outputs, +) +def test_datetime_from_isoformat_with_py_36_or_lower(input_str, expected_output): + """ + `fromisoformat` was added in Python version 3.7 + """ + with mock.patch("sentry_sdk.utils.datetime") as datetime_mocked: + datetime_mocked.fromisoformat.side_effect = AttributeError() + datetime_mocked.strptime = datetime.strptime + assert datetime_from_isoformat(input_str) == expected_output, input_str + + @pytest.mark.parametrize( "env_var_value,strict,expected", [ @@ -346,6 +367,12 @@ def test_sanitize_url_and_split(url, expected_result): assert sanitized_url.fragment == expected_result.fragment +def test_sanitize_url_remove_authority_is_false(): + url = "https://usr:pwd@example.com" + sanitized_url = sanitize_url(url, remove_authority=False) + assert sanitized_url == url + + @pytest.mark.parametrize( ("url", "sanitize", "expected_url", "expected_query", "expected_fragment"), [ @@ -629,6 +656,17 @@ def test_get_error_message(error, expected_result): assert get_error_message(exc_value) == expected_result(exc_value) +def test_safe_str_fails(): + class ExplodingStr: + def __str__(self): + raise Exception + + obj = ExplodingStr() + result = safe_str(obj) + + assert result == repr(obj) + + def test_installed_modules(): try: from importlib.metadata import distributions, version @@ -712,6 +750,20 @@ def test_default_release_empty_string(): assert release is None +def test_get_default_release_sentry_release_env(monkeypatch): + monkeypatch.setenv("SENTRY_RELEASE", "sentry-env-release") + assert get_default_release() == "sentry-env-release" + + +def test_get_default_release_other_release_env(monkeypatch): + monkeypatch.setenv("SOURCE_VERSION", "other-env-release") + + with mock.patch("sentry_sdk.utils.get_git_revision", return_value=""): + release = get_default_release() + + assert release == "other-env-release" + + def test_ensure_integration_enabled_integration_enabled(sentry_init): def original_function(): return "original" @@ -973,3 +1025,55 @@ def test_function(): ... sentry_sdk.utils.qualname_from_function(test_function) == "test_qualname_from_function_none_name..test_function" ) + + +def test_to_string_unicode_decode_error(): + class BadStr: + def __str__(self): + raise UnicodeDecodeError("utf-8", b"", 0, 1, "reason") + + obj = BadStr() + result = to_string(obj) + assert result == repr(obj)[1:-1] + + +def test_exc_info_from_error_dont_get_an_exc(): + class NotAnException: + pass + + with pytest.raises(ValueError) as exc: + exc_info_from_error(NotAnException()) + + assert "Expected Exception object to report, got Date: Tue, 20 May 2025 09:41:16 +0000 Subject: [PATCH 16/16] build(deps): bump codecov/codecov-action from 5.4.2 to 5.4.3 (#4397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.2 to 5.4.3.
Release notes

Sourced from codecov/codecov-action's releases.

v5.4.3

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.2...v5.4.3

Changelog

Sourced from codecov/codecov-action's changelog.

v5.4.3

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.2..v5.4.3

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=5.4.2&new-version=5.4.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anton Pirker Co-authored-by: Ivana Kellyer --- .github/workflows/test-integrations-ai.yml | 4 ++-- .github/workflows/test-integrations-cloud.yml | 4 ++-- .github/workflows/test-integrations-common.yml | 2 +- .github/workflows/test-integrations-dbs.yml | 4 ++-- .github/workflows/test-integrations-flags.yml | 2 +- .github/workflows/test-integrations-gevent.yml | 2 +- .github/workflows/test-integrations-graphql.yml | 2 +- .github/workflows/test-integrations-misc.yml | 2 +- .github/workflows/test-integrations-network.yml | 4 ++-- .github/workflows/test-integrations-tasks.yml | 4 ++-- .github/workflows/test-integrations-web-1.yml | 2 +- .github/workflows/test-integrations-web-2.yml | 4 ++-- scripts/split_tox_gh_actions/templates/test_group.jinja | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index bc89cb9afe..4aa0f36b77 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -83,7 +83,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -158,7 +158,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index 7763aa509d..114e904d4b 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -87,7 +87,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -166,7 +166,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index 864583532d..2ac8d827fa 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -67,7 +67,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index 815b550027..460ffe1ad5 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -107,7 +107,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -206,7 +206,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-flags.yml b/.github/workflows/test-integrations-flags.yml index e28067841b..0e2c9ef166 100644 --- a/.github/workflows/test-integrations-flags.yml +++ b/.github/workflows/test-integrations-flags.yml @@ -79,7 +79,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-gevent.yml b/.github/workflows/test-integrations-gevent.yml index 41a77ffe34..3e0903e2c5 100644 --- a/.github/workflows/test-integrations-gevent.yml +++ b/.github/workflows/test-integrations-gevent.yml @@ -67,7 +67,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index b741302de6..51ae8a8a81 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -79,7 +79,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index 7da9929435..05a8aaeda1 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -87,7 +87,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index 43b5e4a6a5..769a95c08a 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -75,7 +75,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -142,7 +142,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index a6850256b2..43b5d8d56d 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -97,7 +97,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -186,7 +186,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index b40027ddc7..67669c729b 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -97,7 +97,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index 1fbff47b65..c0438dc924 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -103,7 +103,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -198,7 +198,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index 901e4808e4..705ae69a7f 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -91,7 +91,7 @@ - name: Upload coverage to Codecov if: {% raw %}${{ !cancelled() }}{% endraw %} - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} files: coverage.xml