Coverage for src / sentry_tool / commands / traces.py: 97.87%
94 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-15 10:53 -0500
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-15 10:53 -0500
1"""Trace and transaction commands."""
3import re
4from typing import Annotated, Any
6import structlog
7import typer
8from rich.console import Console
9from rich.table import Table
11from sentry_tool.output import Column, OutputFormat, render
12from sentry_tool.utils import api, get_config
14log = structlog.get_logger()
16TRACE_ID_PATTERN = re.compile(r"^[0-9a-fA-F]{32}$")
19def list_transactions(
20 max_rows: Annotated[int, typer.Option("--max", "-n", help="Maximum transactions to show")] = 10,
21 format: Annotated[
22 OutputFormat,
23 typer.Option("--format", "-f", help="Output format"),
24 ] = OutputFormat.table,
25) -> None:
26 """List recent transactions for the active project.
28 Examples:
29 sentry-tool transactions
30 sentry-tool transactions -n 5
31 sentry-tool transactions --format json
32 """
33 config = get_config()
35 response = api(
36 f"/organizations/{config['org']}/events/"
37 f"?query=event.type:transaction project:{config['project']}"
38 "&field=title&field=id&field=trace&field=transaction.duration"
39 "&field=transaction.status&field=project&field=timestamp"
40 "&sort=-timestamp",
41 token=config["auth_token"],
42 base_url=config["url"],
43 )
45 events = response.get("data", [])
47 if not events:
48 log.info("No transactions found", project=config["project"])
49 return
51 events = events[:max_rows]
53 if format == OutputFormat.json:
54 render(events, format)
55 return
57 rows = [
58 {
59 "title": evt.get("title", ""),
60 "id": evt.get("id", ""),
61 "trace": evt.get("trace", ""),
62 "duration": str(evt.get("transaction.duration", "")),
63 "status": evt.get("transaction.status", ""),
64 "timestamp": evt.get("timestamp", "")[:19],
65 }
66 for evt in events
67 ]
69 columns = [
70 Column("Transaction", "title", max_width=30),
71 Column("Event ID", "id", style="dim", max_width=36),
72 Column("Trace ID", "trace", style="dim", max_width=36),
73 Column("Duration (ms)", "duration", justify="right"),
74 Column("Status", "status"),
75 Column("Timestamp", "timestamp"),
76 ]
78 render(rows, format, columns=columns, footer=f"Showing {len(events)} transactions")
81def lookup_trace(
82 trace_id: Annotated[str, typer.Argument(help="Trace ID (32-character hex string)")],
83 max_rows: Annotated[int, typer.Option("--max", "-n", help="Maximum events to show")] = 25,
84 format: Annotated[
85 OutputFormat,
86 typer.Option("--format", "-f", help="Output format"),
87 ] = OutputFormat.table,
88) -> None:
89 """Examples:
90 sentry-tool trace abc123def456789012345678901234ab
91 sentry-tool trace abc123def456789012345678901234ab -n 10
92 sentry-tool trace abc123def456789012345678901234ab --format json
93 """
94 if not TRACE_ID_PATTERN.match(trace_id):
95 log.error("Invalid trace ID format", trace_id=trace_id, expected="32-character hex string")
96 raise typer.Exit(1)
98 config = get_config()
100 response = api(
101 f"/organizations/{config['org']}/events/?query=trace:{trace_id}"
102 "&field=title&field=id&field=span_id&field=transaction.duration"
103 "&field=transaction.status&field=project&field=timestamp",
104 token=config["auth_token"],
105 base_url=config["url"],
106 )
108 events = response.get("data", [])
110 if not events:
111 log.info("No events found for trace", trace_id=trace_id)
112 return
114 events.sort(key=lambda e: e.get("timestamp", ""))
115 events = events[:max_rows]
117 if format == OutputFormat.json:
118 render(events, format)
119 return
121 rows = [
122 {
123 "title": evt.get("title", ""),
124 "span_id": evt.get("span_id", ""),
125 "duration": str(evt.get("transaction.duration", "")),
126 "status": evt.get("transaction.status", ""),
127 "project": evt.get("project", ""),
128 "timestamp": evt.get("timestamp", "")[:19],
129 }
130 for evt in events
131 ]
133 columns = [
134 Column("Transaction", "title", max_width=40),
135 Column("Span ID", "span_id", style="dim", max_width=16),
136 Column("Duration", "duration", justify="right"),
137 Column("Status", "status"),
138 Column("Project", "project"),
139 Column("Timestamp", "timestamp"),
140 ]
142 render(rows, format, columns=columns, footer=f"Showing {len(events)} events")
145def _render_timeline(spans: list[dict[str, Any]], console: Console) -> None:
146 min_start = min(s.get("start_timestamp", 0) for s in spans)
147 max_end = max(s.get("timestamp", 0) for s in spans)
148 total_duration = max_end - min_start
150 if total_duration <= 0:
151 console.print("\n[dim]Cannot render timeline (zero duration)[/dim]")
152 return
154 bar_width = 40
156 table = Table(title="Span Timeline")
157 table.add_column("Op", style="cyan", max_width=20)
158 table.add_column("Description", max_width=35)
159 table.add_column("Start", justify="right")
160 table.add_column("Dur (s)", justify="right")
161 table.add_column("Timeline", no_wrap=True)
163 for span in spans:
164 start = span.get("start_timestamp", 0)
165 end = span.get("timestamp", 0)
166 offset = start - min_start
167 duration = end - start
169 pos = int(offset / total_duration * bar_width)
170 length = max(1, int(duration / total_duration * bar_width))
171 bar = " " * pos + "█" * length
173 table.add_row(
174 span.get("op", ""),
175 span.get("description", "")[:35],
176 f"{offset:.3f}s",
177 f"{duration:.3f}s",
178 bar,
179 )
181 console.print()
182 console.print(table)
183 console.print(f"\n Total duration: {total_duration:.3f}s")
186def show_transaction(
187 event_id: Annotated[str, typer.Argument(help="Event ID")],
188 format: Annotated[
189 OutputFormat,
190 typer.Option("--format", "-f", help="Output format"),
191 ] = OutputFormat.table,
192 timeline: Annotated[
193 bool,
194 typer.Option("--timeline", "-t", help="Show Gantt-chart timeline of spans"),
195 ] = False,
196) -> None:
197 """Fetch and display detailed transaction information including spans.
199 Examples:
200 sentry-tool transaction d3f1d81247ad4516b61da92f1db050dd
201 sentry-tool transaction d3f1d81247ad4516b61da92f1db050dd --format json
202 """
203 config = get_config()
205 event = api(
206 f"/organizations/{config['org']}/events/{config['project']}:{event_id}/",
207 token=config["auth_token"],
208 base_url=config["url"],
209 )
211 if format == OutputFormat.json:
212 render([event], format)
213 return
215 console = Console()
216 console.print(f"\n[bold cyan]=== Transaction {event_id[:8]}... ===[/bold cyan]")
218 rows = [
219 {"field": "Transaction", "value": event.get("title", "N/A")},
220 {"field": "Event ID", "value": event.get("eventID", "N/A")},
221 {
222 "field": "Trace ID",
223 "value": event.get("contexts", {}).get("trace", {}).get("trace_id", "N/A"),
224 },
225 {
226 "field": "Span ID",
227 "value": event.get("contexts", {}).get("trace", {}).get("span_id", "N/A"),
228 },
229 {
230 "field": "Parent Span",
231 "value": event.get("contexts", {}).get("trace", {}).get("parent_span_id", "N/A"),
232 },
233 {
234 "field": "Duration",
235 "value": f"{event.get('contexts', {}).get('trace', {}).get('duration', 'N/A')} ms",
236 },
237 {
238 "field": "Status",
239 "value": event.get("contexts", {}).get("trace", {}).get("status", "N/A"),
240 },
241 {"field": "Timestamp", "value": event.get("dateCreated", "N/A")},
242 ]
244 detail_columns = [
245 Column("Field", "field", style="bold"),
246 Column("Value", "value"),
247 ]
248 render(rows, OutputFormat.table, columns=detail_columns)
250 entries = event.get("entries", [])
251 spans = []
252 for entry in entries:
253 if entry.get("type") == "spans":
254 spans = entry.get("data", [])
255 break
257 if spans:
258 if timeline:
259 _render_timeline(spans, console)
260 else:
261 span_rows = [
262 {
263 "operation": span.get("op", ""),
264 "description": span.get("description", "")[:50],
265 "duration": f"{span.get('timestamp', 0) - span.get('start_timestamp', 0):.3f}",
266 }
267 for span in spans
268 ]
270 span_columns = [
271 Column("Operation", "operation", max_width=20),
272 Column("Description", "description", max_width=50),
273 Column("Duration (s)", "duration", justify="right"),
274 ]
275 console.print()
276 render(
277 span_rows, OutputFormat.table, columns=span_columns, footer=f"{len(spans)} spans"
278 )
279 else:
280 console.print("\n[dim]No span data found[/dim]")
282 console.print()