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
1 change: 1 addition & 0 deletions src/sentry/search/eap/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
SupportedTraceItemType.PREPROD: TraceItemType.TRACE_ITEM_TYPE_PREPROD,
SupportedTraceItemType.ATTACHMENTS: TraceItemType.TRACE_ITEM_TYPE_ATTACHMENT,
SupportedTraceItemType.PROCESSING_ERRORS: TraceItemType.TRACE_ITEM_TYPE_PROCESSING_ERROR,
SupportedTraceItemType.OCCURRENCES: TraceItemType.TRACE_ITEM_TYPE_OCCURRENCE,
}

SUPPORTED_STATS_TYPES = {"attributeDistributions"}
Expand Down
75 changes: 75 additions & 0 deletions src/sentry/search/eap/occurrences/attributes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Literal

from sentry.search.eap import constants
from sentry.search.eap.columns import (
ResolvedAttribute,
Expand Down Expand Up @@ -294,3 +296,76 @@
OCCURRENCE_VIRTUAL_CONTEXTS = {
**project_virtual_contexts(),
}

OCCURRENCE_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS: dict[
Literal["string", "number", "boolean"], dict[str, str]
] = {
"string": {
definition.internal_name: definition.public_alias
for definition in OCCURRENCE_ATTRIBUTE_DEFINITIONS.values()
if not definition.secondary_alias and definition.search_type == "string"
}
| {
# sentry.service is the project id as a string, but map to project for convenience
"sentry.service": "project",
},
"boolean": {
definition.internal_name: definition.public_alias
for definition in OCCURRENCE_ATTRIBUTE_DEFINITIONS.values()
if not definition.secondary_alias and definition.search_type == "boolean"
},
"number": {
definition.internal_name: definition.public_alias
for definition in OCCURRENCE_ATTRIBUTE_DEFINITIONS.values()
# Include boolean attributes because they're stored as numbers (0 or 1)
if not definition.secondary_alias and definition.search_type != "string"
},
}

OCCURRENCE_PRIVATE_ATTRIBUTES: set[str] = {
definition.internal_name
for definition in OCCURRENCE_ATTRIBUTE_DEFINITIONS.values()
if definition.private
}

# For dynamic internal attributes (eg. meta information for attributes) we match by the beginning of the key.
OCCURRENCE_PRIVATE_ATTRIBUTE_PREFIXES: set[str] = {constants.META_PREFIX}

OCCURRENCE_REPLACEMENT_ATTRIBUTES: set[str] = {
definition.replacement
for definition in OCCURRENCE_ATTRIBUTE_DEFINITIONS.values()
if definition.replacement
}

OCCURRENCE_REPLACEMENT_MAP: dict[str, str] = {
definition.public_alias: definition.replacement
for definition in OCCURRENCE_ATTRIBUTE_DEFINITIONS.values()
if definition.replacement
}

OCCURRENCE_INTERNAL_TO_SECONDARY_ALIASES_MAPPING: dict[str, set[str]] = {}

for definition in OCCURRENCE_ATTRIBUTE_DEFINITIONS.values():
if not definition.secondary_alias:
continue

secondary_aliases = OCCURRENCE_INTERNAL_TO_SECONDARY_ALIASES_MAPPING.get(
definition.internal_name, set()
)
secondary_aliases.add(definition.public_alias)
OCCURRENCE_INTERNAL_TO_SECONDARY_ALIASES_MAPPING[definition.internal_name] = secondary_aliases

# Attributes excluded from stats queries (e.g., attribute distributions)
# These are typically system-level identifiers that don't provide useful distribution insights
OCCURRENCE_STATS_EXCLUDED_ATTRIBUTES_PUBLIC_ALIAS: set[str] = {
"id",
"trace",
"span_id",
"group_id",
"issue_occurrence_id",
"primary_hash",
"fingerprint",
"resource_id",
"profile_id",
"replay_id",
}
Comment thread
shashjar marked this conversation as resolved.
18 changes: 18 additions & 0 deletions src/sentry/search/eap/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

from sentry.search.eap.columns import ColumnDefinitions, ResolvedAttribute
from sentry.search.eap.constants import SENTRY_INTERNAL_PREFIXES
from sentry.search.eap.occurrences.attributes import (
OCCURRENCE_ATTRIBUTE_DEFINITIONS,
OCCURRENCE_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS,
OCCURRENCE_INTERNAL_TO_SECONDARY_ALIASES_MAPPING,
OCCURRENCE_PRIVATE_ATTRIBUTE_PREFIXES,
OCCURRENCE_PRIVATE_ATTRIBUTES,
OCCURRENCE_REPLACEMENT_ATTRIBUTES,
OCCURRENCE_REPLACEMENT_MAP,
)
from sentry.search.eap.occurrences.definitions import OCCURRENCE_DEFINITIONS
from sentry.search.eap.ourlogs.attributes import (
LOGS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS,
LOGS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING,
Expand Down Expand Up @@ -73,13 +83,15 @@ def add_start_end_conditions(
SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS,
SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS,
SupportedTraceItemType.PREPROD: PREPROD_SIZE_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS,
SupportedTraceItemType.OCCURRENCES: OCCURRENCE_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS,
}

PUBLIC_ALIAS_TO_INTERNAL_MAPPING: dict[SupportedTraceItemType, dict[str, ResolvedAttribute]] = {
SupportedTraceItemType.SPANS: SPAN_ATTRIBUTE_DEFINITIONS,
SupportedTraceItemType.LOGS: OURLOG_ATTRIBUTE_DEFINITIONS,
SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_ATTRIBUTE_DEFINITIONS,
SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_ATTRIBUTE_DEFINITIONS,
SupportedTraceItemType.OCCURRENCES: OCCURRENCE_ATTRIBUTE_DEFINITIONS,
}


Expand All @@ -88,27 +100,31 @@ def add_start_end_conditions(
SupportedTraceItemType.LOGS: LOGS_PRIVATE_ATTRIBUTES,
SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_PRIVATE_ATTRIBUTES,
SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_PRIVATE_ATTRIBUTES,
SupportedTraceItemType.OCCURRENCES: OCCURRENCE_PRIVATE_ATTRIBUTES,
}

PRIVATE_ATTRIBUTE_PREFIXES: dict[SupportedTraceItemType, set[str]] = {
SupportedTraceItemType.SPANS: SPANS_PRIVATE_ATTRIBUTE_PREFIXES,
SupportedTraceItemType.LOGS: LOGS_PRIVATE_ATTRIBUTE_PREFIXES,
SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_PRIVATE_ATTRIBUTE_PREFIXES,
SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_PRIVATE_ATTRIBUTE_PREFIXES,
SupportedTraceItemType.OCCURRENCES: OCCURRENCE_PRIVATE_ATTRIBUTE_PREFIXES,
}

SENTRY_CONVENTIONS_REPLACEMENT_ATTRIBUTES: dict[SupportedTraceItemType, set[str]] = {
SupportedTraceItemType.SPANS: SPANS_REPLACEMENT_ATTRIBUTES,
SupportedTraceItemType.LOGS: LOGS_REPLACEMENT_ATTRIBUTES,
SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_REPLACEMENT_ATTRIBUTES,
SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_REPLACEMENT_ATTRIBUTES,
SupportedTraceItemType.OCCURRENCES: OCCURRENCE_REPLACEMENT_ATTRIBUTES,
}

SENTRY_CONVENTIONS_REPLACEMENT_MAPPINGS: dict[SupportedTraceItemType, dict[str, str]] = {
SupportedTraceItemType.SPANS: SPANS_REPLACEMENT_MAP,
SupportedTraceItemType.LOGS: LOGS_REPLACEMENT_MAP,
SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_REPLACEMENT_MAP,
SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_REPLACEMENT_MAP,
SupportedTraceItemType.OCCURRENCES: OCCURRENCE_REPLACEMENT_MAP,
}


Expand All @@ -117,13 +133,15 @@ def add_start_end_conditions(
SupportedTraceItemType.LOGS: LOGS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING,
SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING,
SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_INTERNAL_TO_SECONDARY_ALIASES_MAPPING,
SupportedTraceItemType.OCCURRENCES: OCCURRENCE_INTERNAL_TO_SECONDARY_ALIASES_MAPPING,
}

TRACE_ITEM_TYPE_DEFINITIONS: dict[SupportedTraceItemType, ColumnDefinitions] = {
SupportedTraceItemType.SPANS: SPAN_DEFINITIONS,
SupportedTraceItemType.LOGS: OURLOG_DEFINITIONS,
SupportedTraceItemType.TRACEMETRICS: TRACE_METRICS_DEFINITIONS,
SupportedTraceItemType.PROFILE_FUNCTIONS: PROFILE_FUNCTIONS_DEFINITIONS,
SupportedTraceItemType.OCCURRENCES: OCCURRENCE_DEFINITIONS,
}


Expand Down
96 changes: 95 additions & 1 deletion src/sentry/snuba/occurrences_rpc.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import logging
from collections import defaultdict
from enum import Enum
from typing import Any

import sentry_sdk
from sentry_protos.snuba.v1.endpoint_trace_item_stats_pb2 import (
AttributeDistributionsRequest,
StatsType,
TraceItemStatsRequest,
)
from sentry_protos.snuba.v1.request_common_pb2 import PageToken
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey
from sentry_protos.snuba.v1.trace_item_filter_pb2 import (
Expand All @@ -13,12 +19,19 @@

from sentry.models.group import Group
from sentry.search.eap.columns import ColumnDefinitions, ResolvedAttribute, ResolvedColumn
from sentry.search.eap.constants import SUPPORTED_STATS_TYPES
from sentry.search.eap.occurrences.definitions import OCCURRENCE_DEFINITIONS
from sentry.search.eap.resolver import SearchResolver
from sentry.search.eap.rpc_utils import and_trace_item_filters
from sentry.search.eap.types import AdditionalQueries, EAPResponse, SearchResolverConfig
from sentry.search.eap.types import (
AdditionalQueries,
EAPResponse,
SearchResolverConfig,
SupportedTraceItemType,
)
from sentry.search.events.types import SAMPLING_MODES, SnubaData, SnubaParams
from sentry.snuba import rpc_dataset_common
from sentry.utils import snuba_rpc
from sentry.utils.snuba import process_value

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -253,6 +266,87 @@ def run_grouped_timeseries_query(

return results

@classmethod
@sentry_sdk.trace
def run_stats_query(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that arbitrary attributes on occurrence trace items are stored in EAP with columns constructed as f"attr[{key}]". This may require some transformation work down the line to make sure attributes get displayed nicely in the frontend.

cls,
*,
params: SnubaParams,
stats_types: set[str],
query_string: str,
referrer: str,
config: SearchResolverConfig,
search_resolver: SearchResolver | None = None,
attributes: list[AttributeKey] | None = None,
max_buckets: int = 75,
skip_translate_internal_to_public_alias: bool = False,
occurrence_category: OccurrenceCategory | None = None,
Copy link
Copy Markdown
Member Author

@shashjar shashjar Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that (like the other query functions in the Occurrences RPC class) run_stats_query accepts an optional occurrence_category argument to filter down to error occurrences or issue platform occurrences (queries both by default). We may want to take this into account for Errors on Explore, i.e. if we want to limit the queries to error occurrences only.

Question for Explore team: will Errors on Explore be truly errors-only, or do we want to include issue platform events there as well?

) -> list[dict[str, Any]]:
search_resolver = search_resolver or cls.get_resolver(params, config)
stats_filter, _, _ = search_resolver.resolve_query(query_string)

stats_filter = and_trace_item_filters(
stats_filter, cls._build_category_filter(occurrence_category)
)

meta = search_resolver.resolve_meta(
referrer=referrer,
sampling_mode=params.sampling_mode,
)
stats_request = TraceItemStatsRequest(
filter=stats_filter,
meta=meta,
stats_types=[],
)

if not set(stats_types).intersection(SUPPORTED_STATS_TYPES):
return []

if "attributeDistributions" in stats_types:
stats_request.stats_types.append(
StatsType(
attribute_distributions=AttributeDistributionsRequest(
max_buckets=max_buckets,
attributes=attributes,
)
)
)

response = snuba_rpc.trace_item_stats_rpc(stats_request)
stats = []

from sentry.search.eap.utils import can_expose_attribute, translate_internal_to_public_alias

for result in response.results:
if "attributeDistributions" in stats_types and result.HasField(
"attribute_distributions"
):
attrs: dict[str, list[dict[str, Any]]] = defaultdict(list)
for attribute in result.attribute_distributions.attributes:
if not can_expose_attribute(
attribute.attribute_name, SupportedTraceItemType.OCCURRENCES
):
continue

for bucket in attribute.buckets:
if skip_translate_internal_to_public_alias:
attrs[attribute.attribute_name].append(
{"label": bucket.label, "value": bucket.value}
)
else:
public_alias, _, _ = translate_internal_to_public_alias(
attribute.attribute_name,
"string",
SupportedTraceItemType.OCCURRENCES,
)
public_alias = public_alias or attribute.attribute_name
attrs[public_alias].append(
{"label": bucket.label, "value": bucket.value}
)
stats.append({"attribute_distributions": {"data": attrs}})

return stats
Comment thread
shashjar marked this conversation as resolved.

@classmethod
def _fetch_issue_labels(
cls,
Expand Down
1 change: 1 addition & 0 deletions src/sentry/snuba/referrer.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ class Referrer(StrEnum):
API_SPANS_FREQUENCY_STATS_RPC = "api.spans.fields-stats.rpc"
API_SPANS_TAG_VALUES_RPC = "api.spans.tags-values.rpc"
API_SPANS_TRACE_VIEW = "api.spans.trace-view"
API_OCCURRENCES_FREQUENCY_STATS_RPC = "api.occurrences.fields-stats.rpc"
API_TRACE_METRICS_TAG_KEYS_RPC = "api.tracemetrics.tags-keys.rpc"
API_TRACE_METRICS_TAG_VALUES_RPC = "api.tracemetrics.tags-values.rpc"

Expand Down
Loading
Loading