Skip to content

Commit 47d0d31

Browse files
shruthilayajandrewshie-sentry
authored andcommitted
feat: Expose sentry conventions in UI (#92370)
Adds support to expose sentry conventions for deprecated attributes behind a feature flag.
1 parent 8d10370 commit 47d0d31

File tree

12 files changed

+1275
-14
lines changed

12 files changed

+1275
-14
lines changed

src/sentry/api/endpoints/organization_trace_item_attributes.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
from sentry.search.eap.resolver import SearchResolver
3232
from sentry.search.eap.spans.definitions import SPAN_DEFINITIONS
3333
from sentry.search.eap.types import SearchResolverConfig, SupportedTraceItemType
34-
from sentry.search.eap.utils import can_expose_attribute, translate_internal_to_public_alias
34+
from sentry.search.eap.utils import (
35+
can_expose_attribute,
36+
is_sentry_convention_replacement_attribute,
37+
translate_internal_to_public_alias,
38+
translate_to_sentry_conventions,
39+
)
3540
from sentry.search.events.types import SnubaParams
3641
from sentry.snuba.referrer import Referrer
3742
from sentry.tagstore.types import TagValue
@@ -169,6 +174,12 @@ def get(self, request: Request, organization: Organization) -> Response:
169174
paginator=ChainPaginator([]),
170175
)
171176

177+
use_sentry_conventions = features.has(
178+
"organizations:performance-sentry-conventions-fields", organization, actor=request.user
179+
)
180+
181+
sentry_sdk.set_tag("feature.use_sentry_conventions", use_sentry_conventions)
182+
172183
serialized = serializer.validated_data
173184
substring_match = serialized.get("substring_match", "")
174185
query_string = serialized.get("query")
@@ -183,7 +194,7 @@ def get(self, request: Request, organization: Organization) -> Response:
183194
resolver = SearchResolver(
184195
params=snuba_params, config=SearchResolverConfig(), definitions=column_definitions
185196
)
186-
filter, _, _ = resolver.resolve_query(query_string)
197+
query_filter, _, _ = resolver.resolve_query(query_string)
187198
meta = resolver.resolve_meta(referrer=referrer.value)
188199
meta.trace_item_type = constants.SUPPORTED_TRACE_ITEM_TYPE_MAP.get(
189200
trace_item_type, ProtoTraceItemType.TRACE_ITEM_TYPE_SPAN
@@ -208,17 +219,48 @@ def data_fn(offset: int, limit: int):
208219
page_token=PageToken(offset=offset),
209220
type=attr_type,
210221
value_substring_match=value_substring_match,
211-
intersecting_attributes_filter=filter,
222+
intersecting_attributes_filter=query_filter,
212223
)
213224

214225
with handle_query_errors():
215226
rpc_response = snuba_rpc.attribute_names_rpc(rpc_request)
216227

217-
return [
218-
as_attribute_key(attribute.name, serialized["attribute_type"], trace_item_type)
219-
for attribute in rpc_response.attributes
220-
if attribute.name and can_expose_attribute(attribute.name, trace_item_type)
221-
]
228+
if use_sentry_conventions:
229+
attribute_keys = {}
230+
for attribute in rpc_response.attributes:
231+
if attribute.name and can_expose_attribute(attribute.name, trace_item_type):
232+
attr_key = as_attribute_key(
233+
attribute.name, serialized["attribute_type"], trace_item_type
234+
)
235+
public_alias = attr_key["name"]
236+
replacement = translate_to_sentry_conventions(public_alias, trace_item_type)
237+
if public_alias != replacement:
238+
attr_key = as_attribute_key(
239+
replacement, serialized["attribute_type"], trace_item_type
240+
)
241+
242+
attribute_keys[attr_key["name"]] = attr_key
243+
244+
attributes = list(attribute_keys.values())
245+
sentry_sdk.set_context("api_response", {"attributes": attributes})
246+
return attributes
247+
248+
attributes = list(
249+
filter(
250+
lambda x: not is_sentry_convention_replacement_attribute(
251+
x["name"], trace_item_type
252+
),
253+
[
254+
as_attribute_key(
255+
attribute.name, serialized["attribute_type"], trace_item_type
256+
)
257+
for attribute in rpc_response.attributes
258+
if attribute.name and can_expose_attribute(attribute.name, trace_item_type)
259+
],
260+
)
261+
)
262+
sentry_sdk.set_context("api_response", {"attributes": attributes})
263+
return attributes
222264

223265
return self.paginate(
224266
request=request,

src/sentry/api/endpoints/project_trace_item_details.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sentry_protos.snuba.v1.endpoint_trace_item_details_pb2 import TraceItemDetailsRequest
1111
from sentry_protos.snuba.v1.request_common_pb2 import RequestMeta
1212

13+
from sentry import features
1314
from sentry.api.api_owners import ApiOwner
1415
from sentry.api.api_publish_status import ApiPublishStatus
1516
from sentry.api.base import region_silo_endpoint
@@ -18,16 +19,23 @@
1819
from sentry.models.project import Project
1920
from sentry.search.eap import constants
2021
from sentry.search.eap.types import SupportedTraceItemType, TraceItemAttribute
21-
from sentry.search.eap.utils import PRIVATE_ATTRIBUTES, translate_internal_to_public_alias
22+
from sentry.search.eap.utils import (
23+
PRIVATE_ATTRIBUTES,
24+
is_sentry_convention_replacement_attribute,
25+
translate_internal_to_public_alias,
26+
translate_to_sentry_conventions,
27+
)
2228
from sentry.snuba.referrer import Referrer
2329
from sentry.utils import snuba_rpc
2430

2531

2632
def convert_rpc_attribute_to_json(
2733
attributes: list[dict],
2834
trace_item_type: SupportedTraceItemType,
35+
use_sentry_conventions: bool = False,
2936
) -> list[TraceItemAttribute]:
3037
result: list[TraceItemAttribute] = []
38+
seen_sentry_conventions: set[str] = set()
3139
for attribute in attributes:
3240
internal_name = attribute["name"]
3341
if internal_name in PRIVATE_ATTRIBUTES.get(trace_item_type, []):
@@ -53,6 +61,17 @@ def convert_rpc_attribute_to_json(
5361
internal_name, column_type, trace_item_type
5462
)
5563

64+
if use_sentry_conventions and external_name:
65+
external_name = translate_to_sentry_conventions(external_name, trace_item_type)
66+
if external_name in seen_sentry_conventions:
67+
continue
68+
seen_sentry_conventions.add(external_name)
69+
else:
70+
if external_name and is_sentry_convention_replacement_attribute(
71+
external_name, trace_item_type
72+
):
73+
continue
74+
5675
if trace_item_type == SupportedTraceItemType.SPANS and internal_name.startswith(
5776
"sentry."
5877
):
@@ -149,10 +168,18 @@ def get(request: Request, project: Project, item_id: str) -> Response:
149168

150169
resp = MessageToDict(snuba_rpc.trace_item_details_rpc(req))
151170

171+
use_sentry_conventions = features.has(
172+
"organizations:performance-sentry-conventions-fields",
173+
project.organization,
174+
actor=request.user,
175+
)
176+
152177
resp_dict = {
153178
"itemId": serialize_item_id(resp["itemId"], item_type),
154179
"timestamp": resp["timestamp"],
155-
"attributes": convert_rpc_attribute_to_json(resp["attributes"], item_type),
180+
"attributes": convert_rpc_attribute_to_json(
181+
resp["attributes"], item_type, use_sentry_conventions
182+
),
156183
}
157184

158185
return Response(resp_dict)

src/sentry/features/temporary.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ def register_temporary_features(manager: FeatureManager):
245245
manager.add("organizations:performance-trace-details", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
246246
# Enable trace explorer features
247247
manager.add("organizations:performance-trace-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
248+
# Enable sentry convention fields
249+
manager.add("organizations:performance-sentry-conventions-fields", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
248250
# Enable querying spans fields stats from comparative workflows project
249251
manager.add("organizations:performance-spans-fields-stats", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
250252
# Enable FE/BE for tracing without performance

src/sentry/search/eap/columns.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class ResolvedAttribute(ResolvedColumn):
8686
is_aggregate: bool = field(default=False, init=False)
8787
# There are columns in RPC that are available but we don't want rendered to the user
8888
private: bool = False
89+
replacement: str | None = field(default=None)
90+
deprecation_status: str | None = field(default=None)
8991

9092
@property
9193
def proto_definition(self) -> AttributeKey:

src/sentry/search/eap/ourlogs/attributes.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,15 @@
112112
for definition in OURLOG_ATTRIBUTE_DEFINITIONS.values()
113113
if definition.private
114114
}
115+
116+
LOGS_REPLACEMENT_ATTRIBUTES: set[str] = {
117+
definition.replacement
118+
for definition in OURLOG_ATTRIBUTE_DEFINITIONS.values()
119+
if definition.replacement
120+
}
121+
122+
LOGS_REPLACEMENT_MAP: dict[str, str] = {
123+
definition.public_alias: definition.replacement
124+
for definition in OURLOG_ATTRIBUTE_DEFINITIONS.values()
125+
if definition.replacement
126+
}

src/sentry/search/eap/spans/attributes.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
from typing import Literal
1+
import logging
2+
import os
3+
from dataclasses import replace
4+
from typing import Any, Literal
25

36
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import VirtualColumnContext
47

@@ -14,15 +17,20 @@
1417
simple_sentry_field,
1518
)
1619
from sentry.search.eap.common_columns import COMMON_COLUMNS
20+
from sentry.search.eap.spans.sentry_conventions import SENTRY_CONVENTIONS_DIRECTORY
1721
from sentry.search.events.constants import (
1822
PRECISE_FINISH_TS,
1923
PRECISE_START_TS,
2024
SPAN_MODULE_CATEGORY_VALUES,
2125
)
2226
from sentry.search.events.types import SnubaParams
2327
from sentry.search.utils import DEVICE_CLASS
28+
from sentry.utils import json
2429
from sentry.utils.validators import is_empty_string, is_event_id_or_list, is_span_id
2530

31+
logger = logging.getLogger(__name__)
32+
33+
2634
SPAN_ATTRIBUTE_DEFINITIONS = {
2735
column.public_alias: column
2836
for column in COMMON_COLUMNS
@@ -426,6 +434,53 @@
426434
]
427435
}
428436

437+
DEPRECATED_ATTRIBUTES: list[dict[str, Any]] = []
438+
try:
439+
with open(os.path.join(SENTRY_CONVENTIONS_DIRECTORY, "deprecated_attributes.json"), "rb") as f:
440+
DEPRECATED_ATTRIBUTES = json.loads(f.read())["attributes"]
441+
except Exception:
442+
logger.exception("Failed to load deprecated attributes from 'deprecated_attributes.json'")
443+
444+
445+
try:
446+
for attribute in DEPRECATED_ATTRIBUTES:
447+
deprecation = attribute.get("deprecation", {})
448+
attr_type = attribute.get("type", "string")
449+
key = attribute["key"]
450+
if (
451+
"replacement" in deprecation
452+
and "_status" in deprecation
453+
and deprecation["_status"] == "backfill"
454+
):
455+
status = deprecation["_status"]
456+
replacement = deprecation["replacement"]
457+
if key in SPAN_ATTRIBUTE_DEFINITIONS:
458+
deprecated_attr = SPAN_ATTRIBUTE_DEFINITIONS[key]
459+
SPAN_ATTRIBUTE_DEFINITIONS[key] = replace(
460+
deprecated_attr, replacement=replacement, deprecation_status=status
461+
)
462+
# TODO: Introduce units to attribute schema.
463+
SPAN_ATTRIBUTE_DEFINITIONS[replacement] = replace(
464+
deprecated_attr, public_alias=replacement, internal_name=replacement
465+
)
466+
else:
467+
SPAN_ATTRIBUTE_DEFINITIONS[key] = ResolvedAttribute(
468+
public_alias=key,
469+
internal_name=key,
470+
search_type=attr_type,
471+
replacement=replacement,
472+
deprecation_status=status,
473+
)
474+
475+
SPAN_ATTRIBUTE_DEFINITIONS[replacement] = ResolvedAttribute(
476+
public_alias=replacement,
477+
internal_name=replacement,
478+
search_type=attr_type,
479+
)
480+
481+
except Exception as e:
482+
logger.exception("Failed to update attribute definitions: %s", e)
483+
429484

430485
def device_class_context_constructor(params: SnubaParams) -> VirtualColumnContext:
431486
# EAP defaults to lower case `unknown`, but in querybuilder we used `Unknown`
@@ -503,6 +558,18 @@ def is_starred_segment_context_constructor(params: SnubaParams) -> VirtualColumn
503558
if definition.private
504559
}
505560

561+
SPANS_REPLACEMENT_ATTRIBUTES: set[str] = {
562+
definition.replacement
563+
for definition in SPAN_ATTRIBUTE_DEFINITIONS.values()
564+
if definition.replacement
565+
}
566+
567+
SPANS_REPLACEMENT_MAP: dict[str, str] = {
568+
definition.public_alias: definition.replacement
569+
for definition in SPAN_ATTRIBUTE_DEFINITIONS.values()
570+
if definition.replacement
571+
}
572+
506573

507574
SPAN_VIRTUAL_CONTEXTS = {
508575
"device.class": VirtualColumnDefinition(
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import os
2+
3+
SENTRY_CONVENTIONS_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)))

0 commit comments

Comments
 (0)