Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions sentry_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,18 @@ class SDKInfo(TypedDict):
Hint = Dict[str, Any]

AttributeValue = (
str | bool | float | int
# TODO: relay support coming soon for
# | list[str] | list[bool] | list[float] | list[int]
str
| bool
| float
| int
| list[str]
| list[bool]
| list[float]
| list[int]
| tuple[str, ...]
| tuple[bool, ...]
| tuple[float, ...]
| tuple[int, ...]
)
Attributes = dict[str, AttributeValue]

Expand All @@ -232,11 +241,10 @@ class SDKInfo(TypedDict):
"boolean",
"double",
"integer",
# TODO: relay support coming soon for:
# "string[]",
# "boolean[]",
# "double[]",
# "integer[]",
"string[]",
"boolean[]",
"double[]",
"integer[]",
],
"value": AttributeValue,
},
Expand Down
36 changes: 36 additions & 0 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import math
import os
import copy
import random
import re
import subprocess
Expand Down Expand Up @@ -2061,6 +2062,25 @@ def format_attribute(val: "Any") -> "AttributeValue":
if isinstance(val, (bool, int, float, str)):
return val

# Only lists of elements of a single type are supported
list_types: 'dict[type, Literal["string[]", "integer[]", "double[]", "boolean[]"]]' = {
str: "string[]",
int: "integer[]",
float: "double[]",
bool: "boolean[]",
}

if isinstance(val, (list, tuple)) and not val:
return []
elif isinstance(val, list):
ty = type(val[0])
if ty in list_types and all(type(v) is ty for v in val):
return copy.deepcopy(val)
elif isinstance(val, tuple):
ty = type(val[0])
if ty in list_types and all(type(v) is ty for v in val):
return list(val)

return safe_repr(val)


Expand All @@ -2075,6 +2095,22 @@ def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
if isinstance(val, str):
return {"value": val, "type": "string"}

if isinstance(val, list):
if not val:
return {"value": [], "type": "string[]"}

# Only lists of elements of a single type are supported
list_types: 'dict[type, Literal["string[]", "integer[]", "double[]", "boolean[]"]]' = {
str: "string[]",
int: "integer[]",
float: "double[]",
bool: "boolean[]",
}

ty = type(val[0])
if ty in list_types and all(type(v) is ty for v in val):
return {"value": val, "type": list_types[ty]}

# Coerce to string if we don't know what to do with the value. This should
# never happen as we pre-format early in format_attribute, but let's be safe.
return {"value": safe_repr(val), "type": "string"}
90 changes: 89 additions & 1 deletion tests/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,13 +661,71 @@ def test_log_attributes_override_scope_attributes(sentry_init, capture_envelopes
assert log["attributes"]["temp.attribute"] == "value2"


@minimum_python_37
def test_log_array_attributes(sentry_init, capture_envelopes):
"""Test homogeneous list and tuple attributes, and fallback for inhomogeneous collections."""

sentry_init(enable_logs=True)

envelopes = capture_envelopes()

with sentry_sdk.new_scope() as scope:
scope.set_attribute("string_list", ["value1", "value2"])
scope.set_attribute("int_tuple", (3, 2, 1, 4))
scope.set_attribute("inhomogeneous_tuple", (3, 2.0, 1, 4)) # type: ignore[arg-type]

sentry_sdk.logger.warning(
"Hello, world!",
attributes={
"float_list": [3.0, 3.5, 4.2],
"bool_tuple": (False, False, True),
"inhomogeneous_list": [3.2, True, None],
},
)

get_client().flush()

assert len(envelopes) == 1
assert len(envelopes[0].items) == 1
item = envelopes[0].items[0]
serialized_attributes = item.payload.json["items"][0]["attributes"]

assert serialized_attributes["string_list"] == {
"value": ["value1", "value2"],
"type": "string[]",
}
assert serialized_attributes["int_tuple"] == {
"value": [3, 2, 1, 4],
"type": "integer[]",
}
assert serialized_attributes["inhomogeneous_tuple"] == {
"value": "(3, 2.0, 1, 4)",
"type": "string",
}

assert serialized_attributes["float_list"] == {
"value": [3.0, 3.5, 4.2],
"type": "double[]",
}
assert serialized_attributes["bool_tuple"] == {
"value": [False, False, True],
"type": "boolean[]",
}
assert serialized_attributes["inhomogeneous_list"] == {
"value": "[3.2, True, None]",
"type": "string",
}


@minimum_python_37
def test_attributes_preserialized_in_before_send(sentry_init, capture_envelopes):
"""We don't surface references to objects in attributes."""
"""We don't surface user-held references to objects in attributes."""

def before_send_log(log, _):
assert isinstance(log["attributes"]["instance"], str)
assert isinstance(log["attributes"]["dictionary"], str)
assert isinstance(log["attributes"]["inhomogeneous_list"], str)
assert isinstance(log["attributes"]["inhomogeneous_tuple"], str)

return log

Expand All @@ -686,6 +744,8 @@ class Cat:
attributes={
"instance": instance,
"dictionary": dictionary,
"inhomogeneous_list": [3.2, True, None],
"inhomogeneous_tuple": (3, 2.0, 1, 4),
},
)

Expand All @@ -696,3 +756,31 @@ class Cat:

assert isinstance(log["attributes"]["instance"], str)
assert isinstance(log["attributes"]["dictionary"], str)
assert isinstance(log["attributes"]["inhomogeneous_list"], str)
assert isinstance(log["attributes"]["inhomogeneous_tuple"], str)


@minimum_python_37
def test_array_attributes_deep_copied_in_before_send(sentry_init, capture_envelopes):
"""We don't surface user-held references to objects in attributes."""

strings = ["value1", "value2"]
ints = (3, 2, 1, 4)

def before_send_log(log, _):
assert log["attributes"]["string_list"] is not strings
assert log["attributes"]["int_tuple"] is not ints

return log

sentry_init(enable_logs=True, before_send_log=before_send_log)

sentry_sdk.logger.warning(
"Hello world!",
attributes={
"string_list": strings,
"int_tuple": ints,
},
)

get_client().flush()
88 changes: 87 additions & 1 deletion tests/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,12 +394,70 @@ def test_metric_attributes_override_scope_attributes(sentry_init, capture_envelo
assert metric["attributes"]["temp.attribute"] == "value2"


def test_log_array_attributes(sentry_init, capture_envelopes):
"""Test homogeneous list and tuple attributes, and fallback for inhomogeneous collections."""

sentry_init()

envelopes = capture_envelopes()

with sentry_sdk.new_scope() as scope:
scope.set_attribute("string_list.attribute", ["value1", "value2"])
scope.set_attribute("int_tuple.attribute", (3, 2, 1, 4))
scope.set_attribute("inhomogeneous_tuple.attribute", (3, 2.0, 1, 4)) # type: ignore[arg-type]

sentry_sdk.metrics.count(
"test",
1,
attributes={
"float_list.attribute": [3.0, 3.5, 4.2],
"bool_tuple.attribute": (False, False, True),
"inhomogeneous_list.attribute": [3.2, True, None],
},
)

get_client().flush()

assert len(envelopes) == 1
assert len(envelopes[0].items) == 1
item = envelopes[0].items[0]
serialized_attributes = item.payload.json["items"][0]["attributes"]

assert serialized_attributes["string_list.attribute"] == {
"value": ["value1", "value2"],
"type": "string[]",
}
assert serialized_attributes["int_tuple.attribute"] == {
"value": [3, 2, 1, 4],
"type": "integer[]",
}
assert serialized_attributes["inhomogeneous_tuple.attribute"] == {
"value": "(3, 2.0, 1, 4)",
"type": "string",
}

assert serialized_attributes["float_list.attribute"] == {
"value": [3.0, 3.5, 4.2],
"type": "double[]",
}
assert serialized_attributes["bool_tuple.attribute"] == {
"value": [False, False, True],
"type": "boolean[]",
}
assert serialized_attributes["inhomogeneous_list.attribute"] == {
"value": "[3.2, True, None]",
"type": "string",
}


def test_attributes_preserialized_in_before_send(sentry_init, capture_envelopes):
"""We don't surface references to objects in attributes."""
"""We don't surface user-held references to objects in attributes."""

def before_send_metric(metric, _):
assert isinstance(metric["attributes"]["instance"], str)
assert isinstance(metric["attributes"]["dictionary"], str)
assert isinstance(metric["attributes"]["inhomogeneous_list"], str)
assert isinstance(metric["attributes"]["inhomogeneous_tuple"], str)

return metric

Expand All @@ -419,6 +477,8 @@ class Cat:
attributes={
"instance": instance,
"dictionary": dictionary,
"inhomogeneous_list": [3.2, True, None],
"inhomogeneous_tuple": (3, 2.0, 1, 4),
},
)

Expand All @@ -429,3 +489,29 @@ class Cat:

assert isinstance(metric["attributes"]["instance"], str)
assert isinstance(metric["attributes"]["dictionary"], str)


def test_array_attributes_deep_copied_in_before_send(sentry_init, capture_envelopes):
"""We don't surface user-held references to objects in attributes."""

strings = ["value1", "value2"]
ints = (3, 2, 1, 4)

def before_send_metric(metric, _):
assert metric["attributes"]["string_list"] is not strings
assert metric["attributes"]["int_tuple"] is not ints

return metric

sentry_init(before_send_metric=before_send_metric)

sentry_sdk.metrics.count(
"test.counter",
1,
attributes={
"string_list": strings,
"int_tuple": ints,
},
)

get_client().flush()
Loading