123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- import hashlib
- from datetime import timedelta
- from unittest import mock
- from uuid import uuid4
- from django.urls import reverse
- from snuba_sdk import Column, Condition, Function, Op
- from sentry.api.endpoints.organization_spans_aggregation import NULL_GROUP
- from sentry.testutils.cases import APITestCase, SnubaTestCase
- from sentry.testutils.helpers.datetime import before_now
- from sentry.utils.samples import load_data
- MOCK_SNUBA_RESPONSE = {
- "data": [
- {
- "transaction_id": "80fe542aea4945ffbe612646987ee449",
- "count": 71,
- "spans": [
- [
- "root_1",
- 1,
- "parent_1",
- "e238e6c2e2466b07",
- "api/0/foo",
- "other",
- "2023-09-13 17:12:19",
- 100,
- 1000,
- 1000,
- ],
- [
- "B1",
- 0,
- "root_1",
- "B",
- "connect",
- "db",
- "2023-09-13 17:12:19",
- 150,
- 50,
- 50.0,
- ],
- [
- "C1",
- 0,
- "root_1",
- "C",
- "resolve_conditions",
- "discover.endpoint",
- "2023-09-13 17:12:19",
- 155,
- 0,
- 10.0,
- ],
- [
- "D1",
- 0,
- "C1",
- "D",
- "resolve_orderby",
- "discover.snql",
- "2023-09-13 17:12:19",
- 157,
- 0,
- 20.0,
- ],
- [
- "E1",
- 0,
- "C1",
- NULL_GROUP,
- "resolve_columns",
- "discover.snql",
- "2023-09-13 17:12:19",
- 157,
- 0,
- 20.0,
- ],
- ],
- },
- {
- "transaction_id": "86b21833d1854d9b811000b91e7fccfa",
- "count": 71,
- "spans": [
- [
- "root_2",
- 1,
- "parent_2",
- "e238e6c2e2466b07",
- "bind_organization_context",
- "other",
- "2023-09-13 17:12:39",
- 100,
- 700,
- 0.0,
- ],
- [
- "B2",
- 0,
- "root_2",
- "B",
- "connect",
- "db",
- "2023-09-13 17:12:39",
- 110,
- 10,
- 30.0,
- ],
- [
- "C2",
- 0,
- "root_2",
- "C",
- "resolve_conditions",
- "discover.endpoint",
- "2023-09-13 17:12:39",
- 115,
- 0,
- 40.0,
- ],
- [
- "D2",
- 0,
- "C2",
- "D",
- "resolve_orderby",
- "discover.snql",
- "2023-09-13 17:12:39",
- 150,
- 0,
- 10.0,
- ],
- [
- "D2-duplicate",
- 0,
- "C2",
- "D",
- "resolve_orderby",
- "discover.snql",
- "2023-09-13 17:12:40",
- 155,
- 0,
- 20.0,
- ],
- [
- "E2",
- 0,
- "C2",
- NULL_GROUP,
- "resolve_columns",
- "discover.snql",
- "2023-09-13 17:12:39",
- 157,
- 0,
- 20.0,
- ],
- ],
- },
- ]
- }
- class OrganizationSpansAggregationTest(APITestCase, SnubaTestCase):
- url_name = "sentry-api-0-organization-spans-aggregation"
- FEATURES = [
- "organizations:starfish-aggregate-span-waterfall",
- "organizations:performance-view",
- ]
- def get_start_end(self, duration):
- return self.day_ago, self.day_ago + timedelta(milliseconds=duration)
- def create_event(
- self,
- trace,
- transaction,
- spans,
- parent_span_id,
- project_id,
- tags=None,
- duration=4000,
- span_id=None,
- measurements=None,
- trace_context=None,
- environment=None,
- **kwargs,
- ):
- start, end = self.get_start_end(duration)
- data = load_data(
- "transaction",
- trace=trace,
- spans=spans,
- timestamp=end,
- start_timestamp=start,
- trace_context=trace_context,
- )
- data["transaction"] = transaction
- data["contexts"]["trace"]["parent_span_id"] = parent_span_id
- if span_id:
- data["contexts"]["trace"]["span_id"] = span_id
- if measurements:
- for key, value in measurements.items():
- data["measurements"][key]["value"] = value
- if tags is not None:
- data["tags"] = tags
- if environment is not None:
- data["environment"] = environment
- with self.feature(self.FEATURES):
- return self.store_event(data, project_id=project_id, **kwargs)
- def setUp(self):
- super().setUp()
- self.login_as(user=self.user)
- self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
- self.span_ids_event_1 = dict(
- zip(["A", "B", "C", "D", "E"], [uuid4().hex[:16] for _ in range(5)])
- )
- self.trace_id_1 = uuid4().hex
- self.root_event_1 = self.create_event(
- trace=self.trace_id_1,
- trace_context={
- "trace_id": self.trace_id_1,
- "span_id": self.span_ids_event_1["A"],
- "exclusive_time": 100,
- },
- transaction="api/0/foo",
- spans=[
- {
- "same_process_as_parent": True,
- "op": "db",
- "description": "connect",
- "span_id": self.span_ids_event_1["B"],
- "trace_id": self.trace_id_1,
- "parent_span_id": self.span_ids_event_1["A"],
- "exclusive_time": 50.0,
- "data": {
- "duration": 0.050,
- "offset": 0.050,
- "span.group": "B",
- "span.description": "connect",
- },
- "sentry_tags": {
- "description": "connect",
- },
- },
- {
- "same_process_as_parent": True,
- "op": "discover.endpoint",
- "description": "resolve_conditions",
- "span_id": self.span_ids_event_1["C"],
- "trace_id": self.trace_id_1,
- "parent_span_id": self.span_ids_event_1["A"],
- "exclusive_time": 10,
- "data": {
- "duration": 0.00,
- "offset": 0.055,
- "span.group": "C",
- "span.description": "connect",
- },
- "sentry_tags": {
- "description": "connect",
- },
- },
- {
- "same_process_as_parent": True,
- "op": "discover.snql",
- "description": "resolve_orderby",
- "span_id": self.span_ids_event_1["D"],
- "trace_id": self.trace_id_1,
- "parent_span_id": self.span_ids_event_1["C"],
- "exclusive_time": 20,
- "data": {
- "duration": 0.00,
- "offset": 0.057,
- "span.group": "D",
- "span.description": "resolve_orderby",
- },
- "sentry_tags": {
- "description": "resolve_orderby",
- },
- },
- {
- "same_process_as_parent": True,
- "op": "discover.snql",
- "description": "resolve_columns",
- "span_id": self.span_ids_event_1["E"],
- "trace_id": self.trace_id_1,
- "parent_span_id": self.span_ids_event_1["C"],
- "exclusive_time": 20,
- "data": {
- "duration": 0.00,
- "offset": 0.057,
- "span.description": "resolve_columns",
- },
- },
- ],
- parent_span_id=None,
- project_id=self.project.id,
- duration=1000,
- environment="production",
- )
- self.span_ids_event_2 = dict(
- zip(["A", "B", "C", "D", "D2", "E"], [uuid4().hex[:16] for _ in range(6)])
- )
- self.trace_id_2 = uuid4().hex
- self.root_event_2 = self.create_event(
- trace=self.trace_id_2,
- trace_context={
- "trace_id": self.trace_id_2,
- "span_id": self.span_ids_event_2["A"],
- "exclusive_time": 100,
- },
- transaction="api/0/foo",
- spans=[
- {
- "same_process_as_parent": True,
- "op": "db",
- "description": "connect",
- "span_id": self.span_ids_event_2["B"],
- "trace_id": self.trace_id_2,
- "parent_span_id": self.span_ids_event_2["A"],
- "exclusive_time": 50.0,
- "data": {
- "duration": 0.010,
- "offset": 0.010,
- "span.group": "B",
- "span.description": "connect",
- },
- "sentry_tags": {
- "description": "connect",
- },
- },
- {
- "same_process_as_parent": True,
- "op": "discover.endpoint",
- "description": "resolve_conditions",
- "span_id": self.span_ids_event_2["C"],
- "trace_id": self.trace_id_2,
- "parent_span_id": self.span_ids_event_2["A"],
- "exclusive_time": 10,
- "data": {
- "duration": 0.00,
- "offset": 0.015,
- "span.group": "C",
- "span.description": "connect",
- },
- "sentry_tags": {
- "description": "connect",
- },
- },
- {
- "same_process_as_parent": True,
- "op": "discover.snql",
- "description": "resolve_orderby",
- "span_id": self.span_ids_event_2["D"],
- "trace_id": self.trace_id_2,
- "parent_span_id": self.span_ids_event_2["C"],
- "exclusive_time": 10,
- "data": {
- "duration": 0.00,
- "offset": 0.050,
- "span.group": "D",
- "span.description": "resolve_orderby",
- },
- "sentry_tags": {
- "description": "resolve_orderby",
- },
- },
- {
- "same_process_as_parent": True,
- "op": "discover.snql",
- "description": "resolve_orderby",
- "span_id": self.span_ids_event_2["D2"],
- "trace_id": self.trace_id_2,
- "parent_span_id": self.span_ids_event_2["C"],
- "exclusive_time": 20,
- "data": {
- "duration": 0.00,
- "offset": 1.055,
- "span.group": "D",
- "span.description": "resolve_orderby",
- },
- "sentry_tags": {
- "description": "resolve_orderby",
- },
- },
- {
- "same_process_as_parent": True,
- "op": "discover.snql",
- "description": "resolve_columns",
- "span_id": self.span_ids_event_2["E"],
- "trace_id": self.trace_id_2,
- "parent_span_id": self.span_ids_event_2["C"],
- "exclusive_time": 20,
- "data": {
- "duration": 0.00,
- "offset": 0.057,
- "span.description": "resolve_columns",
- },
- },
- ],
- parent_span_id=None,
- project_id=self.project.id,
- duration=700,
- environment="development",
- )
- self.url = reverse(
- self.url_name,
- kwargs={"organization_slug": self.project.organization.slug},
- )
- @mock.patch("sentry.api.endpoints.organization_spans_aggregation.raw_snql_query")
- def test_simple(self, mock_query):
- mock_query.side_effect = [MOCK_SNUBA_RESPONSE]
- for backend in ["indexedSpans", "nodestore"]:
- with self.feature(self.FEATURES):
- response = self.client.get(
- self.url,
- data={"transaction": "api/0/foo", "backend": backend},
- format="json",
- )
- assert response.data
- data = response.data
- root_fingerprint = hashlib.md5(b"e238e6c2e2466b07").hexdigest()[:16]
- assert root_fingerprint in data
- assert data[root_fingerprint]["count()"] == 2
- assert data[root_fingerprint]["description"] == "api/0/foo"
- assert round(data[root_fingerprint]["avg(duration)"]) == 850
- if backend == "indexedSpans":
- assert data[root_fingerprint]["samples"] == {
- ("80fe542aea4945ffbe612646987ee449", "root_1"),
- ("86b21833d1854d9b811000b91e7fccfa", "root_2"),
- }
- else:
- assert data[root_fingerprint]["samples"] == {
- (self.root_event_1.event_id, self.span_ids_event_1["A"]),
- (self.root_event_2.event_id, self.span_ids_event_2["A"]),
- }
- fingerprint = hashlib.md5(b"e238e6c2e2466b07-B").hexdigest()[:16]
- assert data[fingerprint]["description"] == "connect"
- assert round(data[fingerprint]["avg(duration)"]) == 30
- fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-D").hexdigest()[:16]
- assert data[fingerprint]["description"] == "resolve_orderby"
- assert data[fingerprint]["avg(exclusive_time)"] == 15.0
- assert data[fingerprint]["count()"] == 2
- fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-D2").hexdigest()[:16]
- assert data[fingerprint]["description"] == "resolve_orderby"
- assert data[fingerprint]["avg(exclusive_time)"] == 20.0
- assert data[fingerprint]["count()"] == 1
- @mock.patch("sentry.api.endpoints.organization_spans_aggregation.raw_snql_query")
- def test_offset_logic(self, mock_query):
- mock_query.side_effect = [MOCK_SNUBA_RESPONSE]
- for backend in ["indexedSpans", "nodestore"]:
- with self.feature(self.FEATURES):
- response = self.client.get(
- self.url,
- data={"transaction": "api/0/foo", "backend": backend},
- format="json",
- )
- assert response.data
- data = response.data
- root_fingerprint = hashlib.md5(b"e238e6c2e2466b07").hexdigest()[:16]
- assert root_fingerprint in data
- assert data[root_fingerprint]["avg(absolute_offset)"] == 0.0
- fingerprint = hashlib.md5(b"e238e6c2e2466b07-B").hexdigest()[:16]
- assert data[fingerprint]["avg(absolute_offset)"] == 30.0
- fingerprint = hashlib.md5(b"e238e6c2e2466b07-C").hexdigest()[:16]
- assert data[fingerprint]["avg(absolute_offset)"] == 35.0
- fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-D").hexdigest()[:16]
- assert data[fingerprint]["avg(absolute_offset)"] == 53.5
- fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-D2").hexdigest()[:16]
- assert data[fingerprint]["avg(absolute_offset)"] == 1075.0
- @mock.patch("sentry.api.endpoints.organization_spans_aggregation.raw_snql_query")
- def test_null_group_fallback(self, mock_query):
- mock_query.side_effect = [MOCK_SNUBA_RESPONSE]
- for backend in ["indexedSpans", "nodestore"]:
- with self.feature(self.FEATURES):
- response = self.client.get(
- self.url,
- data={"transaction": "api/0/foo", "backend": backend},
- format="json",
- )
- assert response.data
- data = response.data
- root_fingerprint = hashlib.md5(b"e238e6c2e2466b07-C-discover.snql").hexdigest()[:16]
- assert root_fingerprint in data
- assert data[root_fingerprint]["description"] == ""
- assert data[root_fingerprint]["count()"] == 2
- @mock.patch("sentry.api.endpoints.organization_spans_aggregation.raw_snql_query")
- def test_http_method_filter(self, mock_query):
- with self.feature(self.FEATURES):
- response = self.client.get(
- self.url,
- data={"transaction": "api/0/foo", "backend": "nodestore", "http.method": "GET"},
- format="json",
- )
- assert response.data
- data = response.data
- root_fingerprint = hashlib.md5(b"e238e6c2e2466b07").hexdigest()[:16]
- assert root_fingerprint in data
- assert data[root_fingerprint]["count()"] == 2
- with self.feature(self.FEATURES):
- response = self.client.get(
- self.url,
- data={"transaction": "api/0/foo", "backend": "nodestore", "http.method": "POST"},
- format="json",
- )
- assert response.data == {}
- with self.feature(self.FEATURES):
- self.client.get(
- self.url,
- data={"transaction": "api/0/foo", "backend": "indexedSpans", "http.method": "GET"},
- format="json",
- )
- assert (
- Condition(
- lhs=Function(
- function="ifNull",
- parameters=[
- Column(
- name="sentry_tags[transaction.method]",
- ),
- "",
- ],
- alias=None,
- ),
- op=Op.EQ,
- rhs="GET",
- )
- in mock_query.mock_calls[0].args[0].query.where
- )
- def test_environment_filter(self):
- with self.feature(self.FEATURES):
- response = self.client.get(
- self.url,
- data={
- "transaction": "api/0/foo",
- "backend": "nodestore",
- "environment": "production",
- },
- format="json",
- )
- assert response.data
- data = response.data
- root_fingerprint = hashlib.md5(b"e238e6c2e2466b07").hexdigest()[:16]
- assert root_fingerprint in data
- assert data[root_fingerprint]["count()"] == 1
- with self.feature(self.FEATURES):
- response = self.client.get(
- self.url,
- data={
- "transaction": "api/0/foo",
- "backend": "nodestore",
- "environment": ["production", "development"],
- },
- format="json",
- )
- assert response.data
- data = response.data
- root_fingerprint = hashlib.md5(b"e238e6c2e2466b07").hexdigest()[:16]
- assert root_fingerprint in data
- assert data[root_fingerprint]["count()"] == 2
|