From 35d2b7af0214304a322844028deaca8f033ad76f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 17 Oct 2024 15:21:38 +0200 Subject: [PATCH 1/6] Serialize w/ json.dumps instead of str --- sentry_sdk/integrations/asyncpg.py | 3 ++- sentry_sdk/integrations/clickhouse_driver.py | 8 +++++--- sentry_sdk/tracing_utils.py | 9 +++++---- .../clickhouse_driver/test_clickhouse_driver.py | 4 +++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/asyncpg.py b/sentry_sdk/integrations/asyncpg.py index 71740cb3aa..3f91bcb03f 100644 --- a/sentry_sdk/integrations/asyncpg.py +++ b/sentry_sdk/integrations/asyncpg.py @@ -1,5 +1,6 @@ from __future__ import annotations import contextlib +import json from typing import Any, TypeVar, Callable, Awaitable, Iterator import sentry_sdk @@ -147,7 +148,7 @@ def _inner(*args: Any, **kwargs: Any) -> T: # noqa: N807 ) as span: _set_db_data(span, args[0]) res = f(*args, **kwargs) - span.set_attribute("db.cursor", str(res)) + span.set_attribute("db.cursor", json.dumps(res)) return res diff --git a/sentry_sdk/integrations/clickhouse_driver.py b/sentry_sdk/integrations/clickhouse_driver.py index 4ee18d0e54..6d3f14c8de 100644 --- a/sentry_sdk/integrations/clickhouse_driver.py +++ b/sentry_sdk/integrations/clickhouse_driver.py @@ -1,3 +1,5 @@ +import json + import sentry_sdk from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import Integration, DidNotEnable @@ -99,7 +101,7 @@ def _inner(*args: P.args, **kwargs: P.kwargs) -> T: if params and should_send_default_pii(): connection._sentry_db_params = params - span.set_attribute("db.params", str(params)) + span.set_attribute("db.params", json.dumps(params)) # run the original code ret = f(*args, **kwargs) @@ -117,7 +119,7 @@ def _inner_end(*args: P.args, **kwargs: P.kwargs) -> T: if span is not None: if res is not None and should_send_default_pii(): - span.set_attribute("db.result", str(res)) + span.set_attribute("db.result", json.dumps(res)) with capture_internal_exceptions(): query = span.get_attribute("db.query.text") @@ -159,7 +161,7 @@ def _inner_send_data(*args: P.args, **kwargs: P.kwargs) -> T: getattr(instance.connection, "_sentry_db_params", None) or [] ) db_params.extend(data) - span.set_attribute("db.params", str(db_params)) + span.set_attribute("db.params", json.dumps(db_params)) try: del instance.connection._sentry_db_params except AttributeError: diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index b8f7288374..83cc482e8b 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1,13 +1,14 @@ import contextlib import inspect +import json import os import re import sys +import uuid from collections.abc import Mapping from datetime import datetime, timedelta, timezone from functools import wraps from urllib.parse import quote, unquote -import uuid import sentry_sdk from sentry_sdk.consts import OP, SPANDATA @@ -133,13 +134,13 @@ def record_sql_queries( data = {} if params_list is not None: - data["db.params"] = str(params_list) + data["db.params"] = json.dumps(params_list) if paramstyle is not None: - data["db.paramstyle"] = str(paramstyle) + data["db.paramstyle"] = json.dumps(paramstyle) if executemany: data["db.executemany"] = True if record_cursor_repr and cursor is not None: - data["db.cursor"] = str(cursor) + data["db.cursor"] = json.dumps(cursor) with capture_internal_exceptions(): sentry_sdk.add_breadcrumb(message=query, category="query", data=data) diff --git a/tests/integrations/clickhouse_driver/test_clickhouse_driver.py b/tests/integrations/clickhouse_driver/test_clickhouse_driver.py index 6378919b06..d590167441 100644 --- a/tests/integrations/clickhouse_driver/test_clickhouse_driver.py +++ b/tests/integrations/clickhouse_driver/test_clickhouse_driver.py @@ -5,6 +5,8 @@ ``` """ +import json + import clickhouse_driver from clickhouse_driver import Client, connect @@ -16,7 +18,7 @@ if clickhouse_driver.VERSION < (0, 2, 6): EXPECT_PARAMS_IN_SELECT = False -PARAMS_SERIALIZER = str +PARAMS_SERIALIZER = json.dumps def test_clickhouse_client_breadcrumbs(sentry_init, capture_events) -> None: From 6e5be2efab9cfd179183f3a6ac7fdd369d4e1a3b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 17 Oct 2024 15:41:15 +0200 Subject: [PATCH 2/6] add util --- sentry_sdk/utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 80a5df9700..9af4581ae5 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -24,6 +24,8 @@ # Python 3.10 and below BaseExceptionGroup = None # type: ignore +from opentelemetry.util.types import AttributeValue + import sentry_sdk from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH, EndpointType @@ -1831,3 +1833,25 @@ def get_current_thread_meta(thread=None): # we've tried everything, time to give up return None, None + + +def _serialize_span_attribute(value): + # type: (Any) -> AttributeValue + """Serialize an object so that it's OTel-compatible and displays nicely in Sentry.""" + # check for allowed primitives + if isinstance(value, (int, str, float, bool)): + return value + + # lists are allowed too, as long as they don't mix types + if isinstance(value, list): + for type_ in (int, str, float, bool): + if all(isinstance(item, type_) for item in value): + return value + + # if this is anything else, just try to coerce to string + # we prefer json.dumps since this makes things like dictionaries display + # nicely in the UI + try: + return json.dumps(value) + except TypeError: + return str(value) From 5142fdc227c9c84ca46021338486944828e7bb74 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 17 Oct 2024 16:06:46 +0200 Subject: [PATCH 3/6] more stuff --- sentry_sdk/integrations/asyncpg.py | 4 +-- sentry_sdk/integrations/clickhouse_driver.py | 14 ++++++----- sentry_sdk/tracing_utils.py | 8 +++--- sentry_sdk/utils.py | 4 +-- tests/test_utils.py | 26 ++++++++++++++++++++ 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/asyncpg.py b/sentry_sdk/integrations/asyncpg.py index 3f91bcb03f..c1f2557a20 100644 --- a/sentry_sdk/integrations/asyncpg.py +++ b/sentry_sdk/integrations/asyncpg.py @@ -1,6 +1,5 @@ from __future__ import annotations import contextlib -import json from typing import Any, TypeVar, Callable, Awaitable, Iterator import sentry_sdk @@ -9,6 +8,7 @@ from sentry_sdk.tracing import Span from sentry_sdk.tracing_utils import add_query_source, record_sql_queries from sentry_sdk.utils import ( + _serialize_span_attribute, ensure_integration_enabled, parse_version, capture_internal_exceptions, @@ -148,7 +148,7 @@ def _inner(*args: Any, **kwargs: Any) -> T: # noqa: N807 ) as span: _set_db_data(span, args[0]) res = f(*args, **kwargs) - span.set_attribute("db.cursor", json.dumps(res)) + span.set_attribute("db.cursor", _serialize_span_attribute(res)) return res diff --git a/sentry_sdk/integrations/clickhouse_driver.py b/sentry_sdk/integrations/clickhouse_driver.py index 6d3f14c8de..245ea0ef71 100644 --- a/sentry_sdk/integrations/clickhouse_driver.py +++ b/sentry_sdk/integrations/clickhouse_driver.py @@ -1,11 +1,13 @@ -import json - import sentry_sdk from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.tracing import Span from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled +from sentry_sdk.utils import ( + _serialize_span_attribute, + capture_internal_exceptions, + ensure_integration_enabled, +) from typing import TYPE_CHECKING, TypeVar @@ -101,7 +103,7 @@ def _inner(*args: P.args, **kwargs: P.kwargs) -> T: if params and should_send_default_pii(): connection._sentry_db_params = params - span.set_attribute("db.params", json.dumps(params)) + span.set_attribute("db.params", _serialize_span_attribute(params)) # run the original code ret = f(*args, **kwargs) @@ -119,7 +121,7 @@ def _inner_end(*args: P.args, **kwargs: P.kwargs) -> T: if span is not None: if res is not None and should_send_default_pii(): - span.set_attribute("db.result", json.dumps(res)) + span.set_attribute("db.result", _serialize_span_attribute(res)) with capture_internal_exceptions(): query = span.get_attribute("db.query.text") @@ -161,7 +163,7 @@ def _inner_send_data(*args: P.args, **kwargs: P.kwargs) -> T: getattr(instance.connection, "_sentry_db_params", None) or [] ) db_params.extend(data) - span.set_attribute("db.params", json.dumps(db_params)) + span.set_attribute("db.params", _serialize_span_attribute(db_params)) try: del instance.connection._sentry_db_params except AttributeError: diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 83cc482e8b..28b301c397 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -1,6 +1,5 @@ import contextlib import inspect -import json import os import re import sys @@ -24,6 +23,7 @@ _is_external_source, _is_in_project_root, _module_in_list, + _serialize_span_attribute, ) from typing import TYPE_CHECKING @@ -134,13 +134,13 @@ def record_sql_queries( data = {} if params_list is not None: - data["db.params"] = json.dumps(params_list) + data["db.params"] = _serialize_span_attribute(params_list) if paramstyle is not None: - data["db.paramstyle"] = json.dumps(paramstyle) + data["db.paramstyle"] = _serialize_span_attribute(paramstyle) if executemany: data["db.executemany"] = True if record_cursor_repr and cursor is not None: - data["db.cursor"] = json.dumps(cursor) + data["db.cursor"] = _serialize_span_attribute(cursor) with capture_internal_exceptions(): sentry_sdk.add_breadcrumb(message=query, category="query", data=data) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 9af4581ae5..e1102024d2 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1843,10 +1843,10 @@ def _serialize_span_attribute(value): return value # lists are allowed too, as long as they don't mix types - if isinstance(value, list): + if isinstance(value, (list, tuple)): for type_ in (int, str, float, bool): if all(isinstance(item, type_) for item in value): - return value + return list(value) # if this is anything else, just try to coerce to string # we prefer json.dumps since this makes things like dictionaries display diff --git a/tests/test_utils.py b/tests/test_utils.py index 6745e2a966..e222f6e1f4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -30,6 +30,7 @@ _get_installed_modules, _generate_installed_modules, ensure_integration_enabled, + _serialize_span_attribute, ) @@ -901,3 +902,28 @@ def test_format_timestamp_naive(): # Ensure that some timestamp is returned, without error. We currently treat these as local time, but this is an # implementation detail which we should not assert here. assert re.fullmatch(timestamp_regex, format_timestamp(datetime_object)) + + +@pytest.mark.parametrize( + ("value", "result"), + ( + ("meow", "meow"), + (1, 1), + (47.0, 47.0), + (True, True), + (["meow", "bark"], ["meow", "bark"]), + ([True, False], [True, False]), + ([1, 2, 3], [1, 2, 3]), + ([46.5, 47.0, 47.5], [46.5, 47.0, 47.5]), + (["meow", 47], '["meow", 47]'), # mixed types not allowed in a list + (None, "null"), + ( + {"cat": "meow", "dog": ["bark", "woof"]}, + '{"cat": "meow", "dog": ["bark", "woof"]}', + ), + (datetime(2024, 1, 1), "2024-01-01 00:00:00"), + (("meow", "purr"), ["meow", "purr"]), + ), +) +def test_serialize_span_attribute(value, result): + assert _serialize_span_attribute(value) == result From 71dd358317e920c6192ee164a448a1b2af13f8f9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 17 Oct 2024 16:36:38 +0200 Subject: [PATCH 4/6] just do literals --- sentry_sdk/utils.py | 3 +- .../test_clickhouse_driver.py | 50 +++++++++---------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index e1102024d2..d8ae32ac2e 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -24,8 +24,6 @@ # Python 3.10 and below BaseExceptionGroup = None # type: ignore -from opentelemetry.util.types import AttributeValue - import sentry_sdk from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH, EndpointType @@ -53,6 +51,7 @@ ) from gevent.hub import Hub as GeventHub + from opentelemetry.util.types import AttributeValue from sentry_sdk._types import Event, ExcInfo diff --git a/tests/integrations/clickhouse_driver/test_clickhouse_driver.py b/tests/integrations/clickhouse_driver/test_clickhouse_driver.py index d590167441..5da77ce13d 100644 --- a/tests/integrations/clickhouse_driver/test_clickhouse_driver.py +++ b/tests/integrations/clickhouse_driver/test_clickhouse_driver.py @@ -5,8 +5,6 @@ ``` """ -import json - import clickhouse_driver from clickhouse_driver import Client, connect @@ -18,8 +16,6 @@ if clickhouse_driver.VERSION < (0, 2, 6): EXPECT_PARAMS_IN_SELECT = False -PARAMS_SERIALIZER = json.dumps - def test_clickhouse_client_breadcrumbs(sentry_init, capture_events) -> None: sentry_init( @@ -146,7 +142,7 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) -> "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.result": str([]), + "db.result": [], }, "message": "DROP TABLE IF EXISTS test", "type": "default", @@ -159,7 +155,7 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) -> "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.result": str([]), + "db.result": [], }, "message": "CREATE TABLE test (x Int32) ENGINE = Memory", "type": "default", @@ -172,7 +168,7 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) -> "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.params": PARAMS_SERIALIZER([{"x": 100}]), + "db.params": '[{"x": 100}]', }, "message": "INSERT INTO test (x) VALUES", "type": "default", @@ -185,7 +181,7 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) -> "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.params": PARAMS_SERIALIZER([[170], [200]]), + "db.params": "[[170], [200]]", }, "message": "INSERT INTO test (x) VALUES", "type": "default", @@ -198,8 +194,8 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) -> "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.result": str([[370]]), - "db.params": PARAMS_SERIALIZER({"minv": 150}), + "db.result": "[[370]]", + "db.params": '{"minv": 150}', }, "message": "SELECT sum(x) FROM test WHERE x > 150", "type": "default", @@ -398,7 +394,7 @@ def test_clickhouse_client_spans_with_pii( "server.address": "localhost", "server.port": 9000, "db.query.text": "DROP TABLE IF EXISTS test", - "db.result": str([]), + "db.result": [], }, "trace_id": transaction_trace_id, "parent_span_id": transaction_span_id, @@ -415,7 +411,7 @@ def test_clickhouse_client_spans_with_pii( "db.name": "", "db.user": "default", "db.query.text": "CREATE TABLE test (x Int32) ENGINE = Memory", - "db.result": str([]), + "db.result": [], "server.address": "localhost", "server.port": 9000, }, @@ -434,7 +430,7 @@ def test_clickhouse_client_spans_with_pii( "db.name": "", "db.user": "default", "db.query.text": "INSERT INTO test (x) VALUES", - "db.params": PARAMS_SERIALIZER([{"x": 100}]), + "db.params": '[{"x": 100}]', "server.address": "localhost", "server.port": 9000, }, @@ -470,9 +466,9 @@ def test_clickhouse_client_spans_with_pii( "db.system": "clickhouse", "db.name": "", "db.user": "default", - "db.params": PARAMS_SERIALIZER({"minv": 150}), + "db.params": '{"minv": 150}', "db.query.text": "SELECT sum(x) FROM test WHERE x > 150", - "db.result": str([(370,)]), + "db.result": "[[370]]", "server.address": "localhost", "server.port": 9000, }, @@ -624,7 +620,7 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.result": str([[], []]), + "db.result": "[[], []]", }, "message": "DROP TABLE IF EXISTS test", "type": "default", @@ -637,7 +633,7 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.result": str([[], []]), + "db.result": "[[], []]", }, "message": "CREATE TABLE test (x Int32) ENGINE = Memory", "type": "default", @@ -650,7 +646,7 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.params": PARAMS_SERIALIZER([{"x": 100}]), + "db.params": '[{"x": 100}]', }, "message": "INSERT INTO test (x) VALUES", "type": "default", @@ -663,7 +659,7 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.params": PARAMS_SERIALIZER([[170], [200]]), + "db.params": "[[170], [200]]", }, "message": "INSERT INTO test (x) VALUES", "type": "default", @@ -676,8 +672,8 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N "db.user": "default", "server.address": "localhost", "server.port": 9000, - "db.params": PARAMS_SERIALIZER({"minv": 150}), - "db.result": str([[["370"]], [["'sum(x)'", "'Int64'"]]]), + "db.params": '{"minv": 150}', + "db.result": '[[["370"]], [["\'sum(x)\'", "\'Int64\'"]]]', }, "message": "SELECT sum(x) FROM test WHERE x > 150", "type": "default", @@ -871,7 +867,7 @@ def test_clickhouse_dbapi_spans_with_pii( "db.name": "", "db.user": "default", "db.query.text": "DROP TABLE IF EXISTS test", - "db.result": str(([], [])), + "db.result": "[[], []]", "server.address": "localhost", "server.port": 9000, }, @@ -890,7 +886,7 @@ def test_clickhouse_dbapi_spans_with_pii( "db.name": "", "db.user": "default", "db.query.text": "CREATE TABLE test (x Int32) ENGINE = Memory", - "db.result": str(([], [])), + "db.result": "[[], []]", "server.address": "localhost", "server.port": 9000, }, @@ -909,7 +905,7 @@ def test_clickhouse_dbapi_spans_with_pii( "db.name": "", "db.user": "default", "db.query.text": "INSERT INTO test (x) VALUES", - "db.params": PARAMS_SERIALIZER([{"x": 100}]), + "db.params": '[{"x": 100}]', "server.address": "localhost", "server.port": 9000, }, @@ -928,7 +924,7 @@ def test_clickhouse_dbapi_spans_with_pii( "db.name": "", "db.user": "default", "db.query.text": "INSERT INTO test (x) VALUES", - "db.params": PARAMS_SERIALIZER([[170], [200]]), + "db.params": "[[170], [200]]", "server.address": "localhost", "server.port": 9000, }, @@ -947,8 +943,8 @@ def test_clickhouse_dbapi_spans_with_pii( "db.name": "", "db.user": "default", "db.query.text": "SELECT sum(x) FROM test WHERE x > 150", - "db.params": PARAMS_SERIALIZER({"minv": 150}), - "db.result": str(([(370,)], [("sum(x)", "Int64")])), + "db.params": '{"minv": 150}', + "db.result": '[[[370]], [["sum(x)", "Int64"]]]', "server.address": "localhost", "server.port": 9000, }, From 1912635541dd9874a9f39f882a1cb69fcf987458 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 17 Oct 2024 16:55:03 +0200 Subject: [PATCH 5/6] more test fixes --- tests/integrations/django/test_basic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index ed39dda350..8d0dbb9e32 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -376,7 +376,7 @@ def test_sql_queries(sentry_init, capture_events, with_integration): crumb = event["breadcrumbs"]["values"][-1] assert crumb["message"] == "SELECT count(*) FROM people_person WHERE foo = %s" - assert crumb["data"]["db.params"] == "[123]" + assert crumb["data"]["db.params"] == [123] @pytest.mark.forked @@ -411,7 +411,7 @@ def test_sql_dict_query_params(sentry_init, capture_events): assert crumb["message"] == ( "SELECT count(*) FROM people_person WHERE foo = %(my_foo)s" ) - assert crumb["data"]["db.params"] == str({"my_foo": 10}) + assert crumb["data"]["db.params"] == '{"my_foo": 10}' @pytest.mark.forked @@ -473,7 +473,7 @@ def test_sql_psycopg2_string_composition(sentry_init, capture_events, query): (event,) = events crumb = event["breadcrumbs"]["values"][-1] assert crumb["message"] == ('SELECT %(my_param)s FROM "foobar"') - assert crumb["data"]["db.params"] == str({"my_param": 10}) + assert crumb["data"]["db.params"] == '{"my_param": 10}' @pytest.mark.forked @@ -526,7 +526,7 @@ def test_sql_psycopg2_placeholders(sentry_init, capture_events): { "category": "query", "data": { - "db.params": str({"first_var": "fizz", "second_var": "not a date"}), + "db.params": '{"first_var": "fizz", "second_var": "not a date"}', "db.paramstyle": "format", }, "message": 'insert into my_test_table ("foo", "bar") values (%(first_var)s, ' From 6ba5365565ec06506d1bb8abe88661bcf2417ff3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 17 Oct 2024 17:01:57 +0200 Subject: [PATCH 6/6] fallback --- sentry_sdk/utils.py | 7 +++++-- tests/test_utils.py | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index d8ae32ac2e..61ba34d956 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1835,7 +1835,7 @@ def get_current_thread_meta(thread=None): def _serialize_span_attribute(value): - # type: (Any) -> AttributeValue + # type: (Any) -> Optional[AttributeValue] """Serialize an object so that it's OTel-compatible and displays nicely in Sentry.""" # check for allowed primitives if isinstance(value, (int, str, float, bool)): @@ -1853,4 +1853,7 @@ def _serialize_span_attribute(value): try: return json.dumps(value) except TypeError: - return str(value) + try: + return str(value) + except Exception: + return None diff --git a/tests/test_utils.py b/tests/test_utils.py index e222f6e1f4..5011662f05 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -904,6 +904,11 @@ def test_format_timestamp_naive(): assert re.fullmatch(timestamp_regex, format_timestamp(datetime_object)) +class NoStr: + def __str__(self): + 1 / 0 + + @pytest.mark.parametrize( ("value", "result"), ( @@ -923,6 +928,7 @@ def test_format_timestamp_naive(): ), (datetime(2024, 1, 1), "2024-01-01 00:00:00"), (("meow", "purr"), ["meow", "purr"]), + (NoStr(), None), ), ) def test_serialize_span_attribute(value, result):