Skip to content

Commit 26986c5

Browse files
committed
Merge branch 'master' into tkdodo/ref/eslint-plugin-boundaries
2 parents 7640f38 + bc4b769 commit 26986c5

24 files changed

+417
-54
lines changed

.github/CODEOWNERS

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
/src/sentry/tagstore/snuba/ @getsentry/owners-snuba
2121
/src/sentry/sentry_metrics/ @getsentry/owners-snuba
2222
/tests/sentry/sentry_metrics/ @getsentry/owners-snuba
23-
/src/sentry/snuba/metrics/ @getsentry/owners-snuba @getsentry/telemetry-experience
23+
/src/sentry/snuba/metrics/ @getsentry/owners-snuba
2424
/src/sentry/snuba/metrics/query.py @getsentry/owners-snuba @getsentry/telemetry-experience
25+
/src/sentry/snuba/metrics/extraction.py @getsentry/owners-snuba @getsentry/telemetry-experience
2526
/src/sentry/snuba/metrics_layer/ @getsentry/owners-snuba
2627
/src/sentry/search/events/datasets/metrics_layer.py @getsentry/owners-snuba
2728
/tests/snuba/test_snql_snuba.py @getsentry/owners-snuba
@@ -242,9 +243,6 @@ yarn.lock @getsentry/owners-js-de
242243
/tests/snuba/api/endpoints/test_organization_events_vitals.py @getsentry/visibility
243244
/tests/snuba/api/endpoints/test_organization_tagkey_values.py @getsentry/visibility
244245

245-
/src/sentry/spans/ @getsentry/visibility
246-
/tests/sentry/spans/ @getsentry/visibility
247-
248246
/src/sentry/api/serializers/snuba.py @getsentry/visibility
249247

250248
/src/sentry/snuba/discover.py @getsentry/visibility
@@ -510,8 +508,6 @@ tests/sentry/api/endpoints/test_organization_dashboard_widget_details.py @ge
510508
/src/sentry/utils/platform_categories.py @getsentry/telemetry-experience
511509
/src/sentry/sentry_metrics/querying/ @getsentry/telemetry-experience
512510
/tests/sentry/sentry_metrics/querying/ @getsentry/telemetry-experience
513-
/src/sentry/snuba/metrics/ @getsentry/telemetry-experience
514-
/tests/sentry/snuba/metrics/ @getsentry/telemetry-experience
515511

516512
/static/app/actionCreators/metrics.tsx @getsentry/telemetry-experience
517513
/static/app/data/platformCategories.tsx @getsentry/telemetry-experience
@@ -673,6 +669,10 @@ tests/sentry/api/endpoints/test_organization_dashboard_widget_details.py @ge
673669
/src/sentry/tempest/ @getsentry/gdx
674670
/tests/sentry/tempest/ @getsentry/gdx
675671

672+
# Span buffer + process-segments are co-owned by streaming platform and vis for now.
673+
/src/sentry/spans/ @getsentry/visibility @getsentry/streaming-platform
674+
/tests/sentry/spans/ @getsentry/visibility @getsentry/streaming-platform
675+
676676
# Workflow engine
677677
/src/sentry/workflow_engine/ @getsentry/alerts-create-issues
678678
/tests/sentry/workflow_engine/ @getsentry/alerts-create-issues

requirements-base.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ rfc3339-validator>=0.1.2
6565
rfc3986-validator>=0.1.1
6666
# [end] jsonschema format validators
6767
sentry-arroyo>=2.21.0
68-
sentry-kafka-schemas>=1.3.2
68+
sentry-kafka-schemas>=1.3.6
6969
sentry-ophio>=1.1.3
7070
sentry-protos==0.2.0
7171
sentry-redis-tools>=0.5.0

requirements-dev-frozen.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ sentry-devenv==1.21.0
185185
sentry-forked-django-stubs==5.2.0.post3
186186
sentry-forked-djangorestframework-stubs==3.16.0.post1
187187
sentry-forked-email-reply-parser==0.5.12.post1
188-
sentry-kafka-schemas==1.3.2
188+
sentry-kafka-schemas==1.3.6
189189
sentry-ophio==1.1.3
190190
sentry-protos==0.2.0
191191
sentry-redis-tools==0.5.0

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pytest-django>=4.9.0
1616
pytest-fail-slow>=0.3.0
1717
pytest-json-report>=1.5.0
1818
pytest-rerunfailures>=15
19-
pytest-sentry>=0.3.0
19+
pytest-sentry>=0.3.0,<0.4.0
2020
pytest-workaround-12888
2121
pytest-xdist>=3
2222
responses>=0.23.1

requirements-frozen.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ rsa==4.8
123123
s3transfer==0.10.0
124124
sentry-arroyo==2.21.0
125125
sentry-forked-email-reply-parser==0.5.12.post1
126-
sentry-kafka-schemas==1.3.2
126+
sentry-kafka-schemas==1.3.6
127127
sentry-ophio==1.1.3
128128
sentry-protos==0.2.0
129129
sentry-redis-tools==0.5.0

src/sentry/spans/consumers/process_segments/enrichment.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from collections import defaultdict
12
from typing import Any, cast
23

3-
from sentry.spans.consumers.process_segments.types import Span
4+
from sentry.models.project import Project
5+
from sentry.spans.consumers.process_segments.types import MeasurementValue, Span
46

57
# Keys in `sentry_tags` that are shared across all spans in a segment. This list
68
# is taken from `extract_shared_tags` in Relay.
@@ -127,7 +129,7 @@ def set_exclusive_time(spans: list[Span]) -> None:
127129
span_map: dict[str, list[tuple[int, int]]] = {}
128130
for span in spans:
129131
if parent_span_id := span.get("parent_span_id"):
130-
interval = (_us(span["start_timestamp_precise"]), _us(span["end_timestamp_precise"]))
132+
interval = _span_interval(span)
131133
span_map.setdefault(parent_span_id, []).append(interval)
132134

133135
for span in spans:
@@ -136,7 +138,7 @@ def set_exclusive_time(spans: list[Span]) -> None:
136138
intervals.sort(key=lambda x: (x[0], -x[1]))
137139

138140
exclusive_time_us: int = 0 # microseconds to prevent rounding issues
139-
start, end = _us(span["start_timestamp_precise"]), _us(span["end_timestamp_precise"])
141+
start, end = _span_interval(span)
140142

141143
# Progressively add time gaps before the next span and then skip to its end.
142144
for child_start, child_end in intervals:
@@ -155,7 +157,75 @@ def set_exclusive_time(spans: list[Span]) -> None:
155157
span["exclusive_time_ms"] = exclusive_time_us / 1_000
156158

157159

160+
def _span_interval(span: Span) -> tuple[int, int]:
161+
"""Get the start and end timestamps of a span in microseconds."""
162+
return _us(span["start_timestamp_precise"]), _us(span["end_timestamp_precise"])
163+
164+
158165
def _us(timestamp: float) -> int:
159166
"""Convert the floating point duration or timestamp to integer microsecond
160167
precision."""
161168
return int(timestamp * 1_000_000)
169+
170+
171+
def compute_breakdowns(segment: Span, spans: list[Span], project: Project) -> None:
172+
"""
173+
Computes breakdowns from all spans and writes them to the segment span.
174+
175+
Breakdowns are measurements that are derived from the spans in the segment.
176+
By convention, their unit is in milliseconds. In the end, these measurements
177+
are converted into attributes on the span trace item.
178+
"""
179+
180+
config = project.get_option("sentry:breakdowns")
181+
182+
for breakdown_name, breakdown_config in config.items():
183+
ty = breakdown_config.get("type")
184+
185+
if ty == "spanOperations":
186+
breakdowns = _compute_span_ops(spans, breakdown_config)
187+
else:
188+
continue
189+
190+
measurements = segment.setdefault("measurements", {})
191+
for key, value in breakdowns.items():
192+
measurements[f"{breakdown_name}.{key}"] = value
193+
194+
195+
def _compute_span_ops(spans: list[Span], config: Any) -> dict[str, MeasurementValue]:
196+
matches = config.get("matches")
197+
if not matches:
198+
return {}
199+
200+
intervals_by_op = defaultdict(list)
201+
for span in spans:
202+
op = span.get("sentry_tags", {}).get("op", "")
203+
if operation_name := next(filter(lambda m: op.startswith(m), matches), None):
204+
intervals_by_op[operation_name].append(_span_interval(span))
205+
206+
measurements: dict[str, MeasurementValue] = {}
207+
for operation_name, intervals in intervals_by_op.items():
208+
duration = _get_duration_us(intervals)
209+
measurements[f"ops.{operation_name}"] = {"value": duration / 1000, "unit": "millisecond"}
210+
return measurements
211+
212+
213+
def _get_duration_us(intervals: list[tuple[int, int]]) -> int:
214+
"""
215+
Get the wall clock time duration covered by the intervals in microseconds.
216+
217+
Overlapping intervals are merged so that they are not counted twice. For
218+
example, the intervals [(1, 3), (2, 4)] would yield a duration of 3, not 4.
219+
"""
220+
221+
duration = 0
222+
last_end = 0
223+
224+
intervals.sort(key=lambda x: (x[0], -x[1]))
225+
for start, end in intervals:
226+
# Ensure the current interval doesn't overlap with the last one
227+
start = max(start, last_end)
228+
duration += max(end - start, 0)
229+
last_end = end
230+
231+
return duration

src/sentry/spans/consumers/process_segments/message.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
record_release_received,
2525
)
2626
from sentry.spans.consumers.process_segments.enrichment import (
27+
compute_breakdowns,
2728
match_schemas,
2829
set_exclusive_time,
2930
set_shared_tags,
@@ -50,6 +51,7 @@ def process_segment(unprocessed_spans: list[UnprocessedSpan]) -> list[Span]:
5051
# If the project does not exist then it might have been deleted during ingestion.
5152
return []
5253

54+
compute_breakdowns(segment_span, spans, project)
5355
_create_models(segment_span, project)
5456
_detect_performance_problems(segment_span, spans, project)
5557
_record_signals(segment_span, spans, project)

src/sentry/spans/consumers/process_segments/types.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
from typing import Any, NotRequired
1+
from typing import NotRequired
22

3+
from sentry_kafka_schemas.schema_types.buffered_segments_v1 import MeasurementValue
34
from sentry_kafka_schemas.schema_types.buffered_segments_v1 import SegmentSpan as UnprocessedSpan
45

56
__all__ = (
7+
"MeasurementValue",
68
"Span",
79
"UnprocessedSpan",
810
)
@@ -14,11 +16,6 @@ class Span(UnprocessedSpan, total=True):
1416
extracted.
1517
"""
1618

17-
# Missing in schema
18-
start_timestamp_precise: float
19-
end_timestamp_precise: float
20-
data: NotRequired[dict[str, Any]] # currently unused
21-
2219
# Added in enrichment
2320
exclusive_time: float
2421
exclusive_time_ms: float

static/app/components/charts/chartWidgetLoader-unmocked-imports.spec.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,29 @@ jest.mock(
152152
})),
153153
})
154154
);
155+
jest.mock('sentry/views/insights/common/queries/useDiscover', () => ({
156+
useEAPSpans: jest.fn(() => ({
157+
data: [
158+
{
159+
'avg(span.duration)': 123,
160+
'sum(span.duration)': 456,
161+
'span.group': 'abc123',
162+
'span.description': 'span1',
163+
'sentry.normalized_description': 'span1',
164+
transaction: 'transaction_a',
165+
},
166+
],
167+
isPending: false,
168+
error: null,
169+
})),
170+
}));
171+
jest.mock('sentry/views/insights/common/queries/useTopNDiscoverSeries', () => ({
172+
useTopNSpanEAPSeries: jest.fn(() => ({
173+
data: [mockDiscoverSeries('transaction_a,abc123')],
174+
isPending: false,
175+
error: null,
176+
})),
177+
}));
155178
jest.mock('sentry/views/insights/common/queries/useDiscoverSeries', () => ({
156179
useEAPSeries: jest.fn(() => ({
157180
data: {

static/app/components/charts/chartWidgetLoader.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,34 @@ const CHART_MAP = {
155155
import(
156156
'sentry/views/insights/common/components/widgets/httpDomainSummaryDurationChartWidget'
157157
),
158+
overviewAgentsRunsChartWidget: () =>
159+
import(
160+
'sentry/views/insights/common/components/widgets/overviewAgentsRunsChartWidget'
161+
),
162+
overviewAgentsDurationChartWidget: () =>
163+
import(
164+
'sentry/views/insights/common/components/widgets/overviewAgentsDurationChartWidget'
165+
),
158166
overviewApiLatencyChartWidget: () =>
159167
import(
160168
'sentry/views/insights/common/components/widgets/overviewApiLatencyChartWidget'
161169
),
170+
overviewCacheMissChartWidget: () =>
171+
import(
172+
'sentry/views/insights/common/components/widgets/overviewCacheMissChartWidget'
173+
),
174+
overviewJobsChartWidget: () =>
175+
import('sentry/views/insights/common/components/widgets/overviewJobsChartWidget'),
162176
overviewPageloadsChartWidget: () =>
163177
import(
164178
'sentry/views/insights/common/components/widgets/overviewPageloadsChartWidget'
165179
),
166180
overviewRequestsChartWidget: () =>
167181
import('sentry/views/insights/common/components/widgets/overviewRequestsChartWidget'),
182+
overviewSlowQueriesChartWidget: () =>
183+
import(
184+
'sentry/views/insights/common/components/widgets/overviewSlowQueriesChartWidget'
185+
),
168186
} satisfies Record<string, () => Promise<{default: React.FC<LoadableChartWidgetProps>}>>;
169187

170188
/**
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// These are the span op we are currently ingesting.
2+
// They will probably change.
3+
const AI_PIPELINE_OPS = ['ai.pipeline.generateText', 'ai.pipeline.generateObject'];
4+
5+
export const getAgentRunsFilter = () => {
6+
return `span.op:[${AI_PIPELINE_OPS.join(',')}]`;
7+
};

static/app/views/insights/agentMonitoring/views/agentsOverviewPage.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLay
2626
import {ModulePageProviders} from 'sentry/views/insights/common/components/modulePageProviders';
2727
import {ModuleBodyUpsellHook} from 'sentry/views/insights/common/components/moduleUpsellHookWrapper';
2828
import {ToolRibbon} from 'sentry/views/insights/common/components/ribbon';
29+
import OverviewAgentsDurationChartWidget from 'sentry/views/insights/common/components/widgets/overviewAgentsDurationChartWidget';
30+
import OverviewAgentsRunsChartWidget from 'sentry/views/insights/common/components/widgets/overviewAgentsRunsChartWidget';
2931
import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnboardingProject';
3032
import {AgentsPageHeader} from 'sentry/views/insights/pages/agents/agentsPageHeader';
33+
import {IssuesWidget} from 'sentry/views/insights/pages/platform/shared/issuesWidget';
3134
import {WidgetGrid} from 'sentry/views/insights/pages/platform/shared/styles';
3235
import {useTransactionNameQuery} from 'sentry/views/insights/pages/platform/shared/useTransactionNameQuery';
3336
import {ModuleName} from 'sentry/views/insights/types';
@@ -76,13 +79,13 @@ function AgentsMonitoringPage() {
7679
<ModuleLayout.Full>
7780
<WidgetGrid>
7881
<WidgetGrid.Position1>
79-
<PlaceholderWidget title="Traffic" />
82+
<OverviewAgentsRunsChartWidget />
8083
</WidgetGrid.Position1>
8184
<WidgetGrid.Position2>
82-
<PlaceholderWidget title="Duration" />
85+
<OverviewAgentsDurationChartWidget />
8386
</WidgetGrid.Position2>
8487
<WidgetGrid.Position3>
85-
<PlaceholderWidget title="Issues" />
88+
<IssuesWidget />
8689
</WidgetGrid.Position3>
8790
<WidgetGrid.Position4>
8891
<PlaceholderWidget title="LLM Generations" />
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {t} from 'sentry/locale';
2+
import {getAgentRunsFilter} from 'sentry/views/insights/agentMonitoring/utils/query';
3+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
4+
import BaseLatencyWidget from 'sentry/views/insights/pages/platform/shared/baseLatencyWidget';
5+
6+
export default function OverviewAgentsDurationChartWidget(
7+
props: LoadableChartWidgetProps
8+
) {
9+
return (
10+
<BaseLatencyWidget
11+
id="overviewAgentsDurationChartWidget"
12+
title={t('Duration')}
13+
baseQuery={getAgentRunsFilter()}
14+
{...props}
15+
/>
16+
);
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {t} from 'sentry/locale';
2+
import {getAgentRunsFilter} from 'sentry/views/insights/agentMonitoring/utils/query';
3+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
4+
import {BaseTrafficWidget} from 'sentry/views/insights/pages/platform/shared/baseTrafficWidget';
5+
6+
export default function OverviewAgentsRunsChartWidget(props: LoadableChartWidgetProps) {
7+
return (
8+
<BaseTrafficWidget
9+
id="overviewAgentsRunsChartWidget"
10+
title={t('Traffic')}
11+
trafficSeriesName={t('Runs')}
12+
baseQuery={getAgentRunsFilter()}
13+
{...props}
14+
/>
15+
);
16+
}

0 commit comments

Comments
 (0)