Skip to content
Merged
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
20 changes: 14 additions & 6 deletions src/sentry/dynamic_sampling/per_org/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,9 @@ def get_eap_transaction_volumes(

end_time = datetime.now(UTC)
start_time = end_time - time_interval
transaction_counts_by_project: defaultdict[int, list[tuple[str, float]]] = defaultdict(list)
transaction_counts_by_project: defaultdict[int, defaultdict[str, float]] = defaultdict(
lambda: defaultdict(float)
)

orderby = [
DynamicSamplingQueryFields.DSC_PROJECT_ID,
Expand All @@ -372,7 +374,7 @@ def get_eap_transaction_volumes(
projects=config.projects,
organization=config.organization,
),
"query_string": f"{DynamicSamplingQueryFilters.IS_SEGMENT} {DynamicSamplingQueryFields.DSC_PROJECT_ID}:[{root_project_filter}] has:{DynamicSamplingQueryFields.DSC_TRANSACTION}",
"query_string": f"{DynamicSamplingQueryFilters.IS_SEGMENT} {DynamicSamplingQueryFields.DSC_PROJECT_ID}:[{root_project_filter}]",
"selected_columns": [
DynamicSamplingQueryFields.DSC_PROJECT_ID,
DynamicSamplingQueryFields.DSC_TRANSACTION,
Expand All @@ -391,20 +393,26 @@ def get_eap_transaction_volumes(
"sampling_mode": SAMPLING_MODE_HIGHEST_ACCURACY,
}
):
transaction = row.get(DynamicSamplingQueryFields.DSC_TRANSACTION)
total = _get_aggregate_float(row, DynamicSamplingQueryFields.COUNT)
if total <= 0:
continue

# A root span with no transaction name and one named "" are the same unnamed
# transaction, but EAP returns them as separate groups. Coalescing to "" keeps
# them a single class in the rebalancing model instead of two, one of which
# would carry the misleading name "None".
transaction = row.get(DynamicSamplingQueryFields.DSC_TRANSACTION) or ""

project_id = _get_aggregate_int(row, DynamicSamplingQueryFields.DSC_PROJECT_ID)
transaction_counts = transaction_counts_by_project[project_id]
transaction_counts.append((str(transaction), total))
transaction_counts_by_project[project_id][transaction] += total

return [
ProjectTransactionCounts(
project_id=project_id,
org_id=config.organization.id,
transaction_counts=transaction_counts,
transaction_counts=sorted(
transaction_counts.items(), key=lambda item: (-item[1], item[0])
),
)
for project_id, transaction_counts in sorted(transaction_counts_by_project.items())
]
52 changes: 50 additions & 2 deletions tests/sentry/dynamic_sampling/per_org/test_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dataclasses import replace
from datetime import UTC, datetime, timedelta
from typing import Any
from unittest.mock import patch

from sentry_protos.snuba.v1.trace_item_attribute_pb2 import ExtrapolationMode
Expand Down Expand Up @@ -429,7 +430,7 @@ def test_get_eap_transaction_volumes(self) -> None:
project=project,
start_ts=timestamp + timedelta(seconds=5),
),
# missing dsc.transaction — excluded by the has:sentry.dsc.transaction filter
# missing dsc.transaction — counted as the unnamed transaction ""
self.create_span(
{
"is_segment": True,
Expand Down Expand Up @@ -465,7 +466,7 @@ def test_get_eap_transaction_volumes(self) -> None:
ProjectTransactionCounts(
org_id=organization.id,
project_id=project.id,
transaction_counts=[("checkout", 3), ("product", 1)],
transaction_counts=[("checkout", 3), ("", 1), ("product", 1)],
),
ProjectTransactionCounts(
org_id=organization.id,
Expand Down Expand Up @@ -669,6 +670,53 @@ def segment(transaction, offset):
)
]

def test_get_eap_transaction_volumes_coalesces_empty_dsc_transaction(self) -> None:
"""
A root span with an empty ``sentry.dsc.transaction`` and one with the attribute
absent are the same unnamed transaction, but EAP returns them as two groups. Both
reach the rebalancing model as a single ``""`` class holding their summed count.
"""
organization = self.create_organization()
project = self.create_project(organization=organization)
timestamp = before_now(minutes=15)

def segment(transaction: str | None, offset: int) -> dict[str, Any]:
dsc_tags = {} if transaction is None else {"dsc.transaction": transaction}
return self.create_span(
{
"is_segment": True,
"sentry_tags": {
"transaction": str(transaction),
"dsc.project_id": str(project.id),
**dsc_tags,
},
},
organization=organization,
project=project,
start_ts=timestamp + timedelta(seconds=offset),
)

self.store_spans(
[
segment("checkout", 0),
segment("checkout", 1),
# Root transaction name set to the empty string.
segment("", 2),
# Root transaction name absent entirely.
segment(None, 3),
]
)

volumes = get_eap_transaction_volumes(self.get_config(organization))

assert volumes == [
ProjectTransactionCounts(
org_id=organization.id,
project_id=project.id,
transaction_counts=[("", 2), ("checkout", 2)],
)
]

def test_get_eap_transaction_volumes_project_over_cap_does_not_starve_other_projects(
self,
) -> None:
Expand Down
Loading