|
1 | 1 | <script lang="ts" setup> |
2 | 2 | import moment from 'moment' |
3 | | -import { computed, ref } from 'vue' |
| 3 | +import { computed, ref, watch } from 'vue' |
4 | 4 | import type { NormalizedEvent } from '@/shared/types' |
5 | 5 | import { EventDetailLayout, PageTabs, PageTab } from '@/shared/ui' |
6 | | -import type { Sentry } from '../../types' |
| 6 | +import { useSentryRequests } from '../../lib/use-sentry-requests' |
| 7 | +import type { Sentry, SentryTraceSummary } from '../../types' |
7 | 8 | import { SentryException } from '../sentry-exception' |
8 | 9 | import { SentryPageApp } from '../sentry-page-app' |
9 | 10 | import { SentryPageBreadcrumbs } from '../sentry-page-breadcrumbs' |
@@ -79,6 +80,86 @@ const scrollToException = (idx: number) => { |
79 | 80 | el.scrollIntoView({ behavior: 'smooth', block: 'start' }) |
80 | 81 | } |
81 | 82 | } |
| 83 | +
|
| 84 | +// Trace summary for the "Trace context" block. |
| 85 | +// |
| 86 | +// The canonical event payload carries `contexts.trace.trace_id` but no |
| 87 | +// information about whether a transaction was captured for that trace. We |
| 88 | +// probe /api/sentry/traces/{traceId} on mount: when it returns we get real |
| 89 | +// span counts/durations and the "View full trace" link can render; when it |
| 90 | +// 404s we keep a minimal synthetic summary and the link stays hidden (the |
| 91 | +// detail page would 404 anyway). |
| 92 | +const traceId = computed(() => { |
| 93 | + const id = props.event.payload.contexts?.trace?.trace_id |
| 94 | + return typeof id === 'string' ? id : '' |
| 95 | +}) |
| 96 | +
|
| 97 | +const traceOp = computed(() => { |
| 98 | + const op = props.event.payload.contexts?.trace?.op |
| 99 | + return typeof op === 'string' ? op : '' |
| 100 | +}) |
| 101 | +
|
| 102 | +// SentrySpanPreview narrows peer_type to a fixed set of categories. Map |
| 103 | +// anything else from the API response to null so the type checker is happy. |
| 104 | +const normalizePeerType = ( |
| 105 | + v: string | null | undefined |
| 106 | +): 'db' | 'http' | 'cache' | 'queue' | null => { |
| 107 | + if (v === 'db' || v === 'http' || v === 'cache' || v === 'queue') return v |
| 108 | + return null |
| 109 | +} |
| 110 | +
|
| 111 | +const buildSyntheticSummary = (): SentryTraceSummary => ({ |
| 112 | + trace_id: traceId.value, |
| 113 | + transaction_name: props.event.payload.transaction ?? '', |
| 114 | + op: traceOp.value, |
| 115 | + duration_ms: 0, |
| 116 | + span_count: 0, |
| 117 | + preview_spans: [] |
| 118 | +}) |
| 119 | +
|
| 120 | +const traceSummary = ref<SentryTraceSummary | null>(null) |
| 121 | +const { getTraceDetail } = useSentryRequests() |
| 122 | +
|
| 123 | +const loadTraceSummary = async (id: string) => { |
| 124 | + if (!id) { |
| 125 | + traceSummary.value = null |
| 126 | + return |
| 127 | + } |
| 128 | + // Default to the synthetic summary so the block still shows the trace id |
| 129 | + // even if the probe fails. |
| 130 | + traceSummary.value = buildSyntheticSummary() |
| 131 | + try { |
| 132 | + const detail = await getTraceDetail(id) |
| 133 | + const spans = detail.spans ?? [] |
| 134 | + traceSummary.value = { |
| 135 | + trace_id: detail.trace_id, |
| 136 | + transaction_name: |
| 137 | + detail.transaction?.transaction_name ?? props.event.payload.transaction ?? '', |
| 138 | + op: detail.transaction?.op ?? traceOp.value, |
| 139 | + duration_ms: detail.transaction?.duration_ms ?? 0, |
| 140 | + span_count: spans.length, |
| 141 | + preview_spans: spans.slice(0, 5).map((s) => ({ |
| 142 | + span_id: s.span_id, |
| 143 | + op: s.op ?? '', |
| 144 | + description: s.description ?? '', |
| 145 | + start_offset_ms: s.start_offset_ms ?? 0, |
| 146 | + duration_ms: s.duration_ms ?? 0, |
| 147 | + peer_type: normalizePeerType(s.peer_type), |
| 148 | + is_error: s.is_error ?? false |
| 149 | + })) |
| 150 | + } |
| 151 | + } catch { |
| 152 | + // 404 / network error → synthetic summary already in place, link hidden. |
| 153 | + } |
| 154 | +} |
| 155 | +
|
| 156 | +watch( |
| 157 | + traceId, |
| 158 | + (id) => { |
| 159 | + loadTraceSummary(id) |
| 160 | + }, |
| 161 | + { immediate: true } |
| 162 | +) |
82 | 163 | </script> |
83 | 164 |
|
84 | 165 | <template> |
@@ -235,15 +316,8 @@ const scrollToException = (idx: number) => { |
235 | 316 | <SentryPageTags :payload="event.payload" /> |
236 | 317 |
|
237 | 318 | <TraceContextBlock |
238 | | - v-if="hasTraceContext && event.payload.contexts?.trace" |
239 | | - :trace-summary="{ |
240 | | - trace_id: String(event.payload.contexts!.trace!.trace_id || ''), |
241 | | - transaction_name: String(event.payload.transaction || ''), |
242 | | - op: String(event.payload.contexts!.trace!.op || ''), |
243 | | - duration_ms: 0, |
244 | | - span_count: 0, |
245 | | - preview_spans: [] |
246 | | - }" |
| 319 | + v-if="hasTraceContext && traceSummary" |
| 320 | + :trace-summary="traceSummary" |
247 | 321 | /> |
248 | 322 |
|
249 | 323 | <SentryPageApp |
|
0 commit comments