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
1 change: 1 addition & 0 deletions src/entities/sentry/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./use-sentry-traces";
export * from "./use-sentry-logs";
export * from "./use-sentry-counts";
export * from "./use-sentry-events-bus";
export * from "./resolve-exceptions";
42 changes: 42 additions & 0 deletions src/entities/sentry/lib/resolve-exceptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Sentry, SentryException, SentryFrame } from '../types'

type Stacktrace = { frames?: SentryFrame[] }

const hasFrames = (st?: Stacktrace): boolean => Boolean(st?.frames && st.frames.length > 0)

const messageText = (payload?: Sentry): string => {
const message = payload?.message as unknown
if (typeof message === 'string') return message
if (message && typeof message === 'object') {
const obj = message as { formatted?: string; message?: string }
return obj.formatted || obj.message || ''
}
return payload?.logentry?.message || ''
}

const topLevelStacktrace = (payload?: Sentry): Stacktrace | undefined => {
const top = (payload as { stacktrace?: Stacktrace } | undefined)?.stacktrace
if (hasFrames(top)) return top

const threads = (payload as { threads?: { values?: Array<{ stacktrace?: Stacktrace }> } } | undefined)
?.threads
for (const thread of threads?.values ?? []) {
if (hasFrames(thread.stacktrace)) return thread.stacktrace
}
return undefined
}

// resolveExceptionValues returns the event's exception values, or — for a message
// event whose stacktrace is attached at the top level or under threads (PHP
// captureMessage with attach_stacktrace, some JS SDKs) — a single synthesized
// exception so the trace renders like any other.
export const resolveExceptionValues = (payload?: Sentry): SentryException[] => {
const values = payload?.exception?.values
if (values && values.length > 0) return values

const stacktrace = topLevelStacktrace(payload)
if (stacktrace) {
return [{ type: 'Message', value: messageText(payload), stacktrace } as SentryException]
}
return []
}
3 changes: 2 additions & 1 deletion src/entities/sentry/ui/preview-card/preview-card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Ref } from 'vue'
import { RouterLink } from 'vue-router'
import type { NormalizedEvent } from '@/shared/types'
import { PreviewCard } from '@/shared/ui'
import { resolveExceptionValues } from '../../lib'
import type { Sentry, SentryException as Exception } from '../../types'
import { SentryException } from '../sentry-exception'

Expand All @@ -20,7 +21,7 @@ const props = withDefaults(defineProps<Props>(), {

const eventLink = computed(() => `/sentry/event/${props.event.id}`)

const exceptionValues = computed(() => props.event?.payload?.exception?.values || [])
const exceptionValues = computed(() => resolveExceptionValues(props.event?.payload))

const hasException = computed(() => exceptionValues.value.length > 0)

Expand Down
16 changes: 14 additions & 2 deletions src/entities/sentry/ui/sentry-exception/sentry-exception-frame.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ const { buildLink } = useIdeLink()

const ideLink = computed(() => buildLink(props.frame.filename ?? 'unknown', props.frame.lineno))

// Sentry's node/RewriteFrames integration normalizes server paths to the
// `app://` scheme with an empty authority, which renders as a confusing
// "app:///+page.server.js". Show the clean app-relative path instead. URLs and
// node: builtins are left as-is.
const displayFilename = computed(() => {
const f = props.frame.filename
if (!f) return 'unknown'
const appScheme = f.match(/^app:\/\/+(.*)$/)
if (appScheme) return appScheme[1]
return f
})

const hasBody = computed(() => {
const f = props.frame
return Boolean(
Expand Down Expand Up @@ -181,12 +193,12 @@ const toggleOpen = () => {
class="frame__fn frame__fn--link"
@click.stop
>{{
frame.filename
displayFilename
}}</a>
<span
v-else
class="frame__fn"
>{{ frame.filename }}</span>
>{{ displayFilename }}</span>
<span
v-if="frame.function"
class="frame__meta"
Expand Down
16 changes: 15 additions & 1 deletion src/entities/sentry/ui/sentry-exception/sentry-exception.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ const exceptionFrames = computed(() => {

return frames.slice().reverse()
})

// Open the most relevant frame by default: the first one carrying source code,
// then the first application frame, then the top frame. JS stacktraces often put
// a library/anonymous frame on top, so opening index 0 would show no code.
const openFrameIndex = computed(() => {
const frames = exceptionFrames.value
const withCode = frames.findIndex(
(f) => f.context_line || f.pre_context?.length || f.post_context?.length
)
if (withCode !== -1) return withCode

const inApp = frames.findIndex((f) => f.in_app)
return inApp !== -1 ? inApp : 0
})
</script>

<template>
Expand Down Expand Up @@ -58,7 +72,7 @@ const exceptionFrames = computed(() => {
>
<SentryExceptionFrame
:frame="frame"
:is-open="index === 0"
:is-open="index === openFrameIndex"
/>
</template>
</div>
Expand Down
16 changes: 8 additions & 8 deletions src/entities/sentry/ui/sentry-page/sentry-page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import moment from 'moment'
import { computed, ref, watch } from 'vue'
import type { NormalizedEvent } from '@/shared/types'
import { EventDetailLayout, PageTabs, PageTab } from '@/shared/ui'
import { resolveExceptionValues } from '../../lib/resolve-exceptions'
import { useSentryRequests } from '../../lib/use-sentry-requests'
import type { Sentry, SentryTraceSummary } from '../../types'
import { SentryException } from '../sentry-exception'
Expand All @@ -29,8 +30,9 @@ const formattedTimestamp = computed(() => {
return ''
})

const mainException = computed(() => props.event.payload?.exception?.values?.[0])
const exceptionsLength = computed(() => props.event?.payload?.exception?.values?.length || 0)
const exceptionValues = computed(() => resolveExceptionValues(props.event.payload))
const mainException = computed(() => exceptionValues.value[0])
const exceptionsLength = computed(() => exceptionValues.value.length)

// Header helpers
const platform = computed(() => props.event.payload.platform || 'unknown')
Expand All @@ -48,9 +50,7 @@ const isHandled = computed(() => {
})

// Tab visibility
const hasException = computed(
() => props.event.payload.exception?.values && props.event.payload.exception.values.length > 0
)
const hasException = computed(() => exceptionValues.value.length > 0)
const hasBreadcrumbs = computed(
() => props.event.payload.breadcrumbs?.values && props.event.payload.breadcrumbs.values.length > 0
)
Expand Down Expand Up @@ -228,7 +228,7 @@ watch(
<div class="sentry-tab-content">
<div class="sentry-exceptions">
<div
v-for="(e, idx) in event.payload.exception!.values"
v-for="(e, idx) in exceptionValues"
:ref="
(el) => {
if (el) exceptionRefs[idx] = el as HTMLElement
Expand All @@ -241,7 +241,7 @@ watch(
<div class="sentry-exceptions__rail">
<!-- Dot with arrow (clickable) or plain dot (last) -->
<button
v-if="idx < event.payload.exception!.values.length - 1"
v-if="idx < exceptionValues.length - 1"
class="sentry-exceptions__dot"
:class="{ 'sentry-exceptions__dot--first': idx === 0 }"
title="Scroll to next exception"
Expand All @@ -268,7 +268,7 @@ watch(

<!-- Connecting line to next dot -->
<div
v-if="idx < event.payload.exception!.values.length - 1"
v-if="idx < exceptionValues.length - 1"
class="sentry-exceptions__line"
/>
</div>
Expand Down
Loading
Loading