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
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,38 @@ export const Frame: StoryObj<typeof SentryExceptionFrame> = {
frame: normalizeSentryEvent(sentrySpiralMock).payload?.exception?.values?.[0]?.stacktrace?.frames?.[1],
}
};

// A frame that has variables but no source context (no context_line /
// pre_context / post_context). Before the fix, expanding such a frame
// produced an empty body — the vars are now rendered as a fallback list.
export const VarsOnlyNoSource: StoryObj<typeof SentryExceptionFrame> = {
args: {
isOpen: true,
frame: {
filename: 'vendor/lib/internal.php',
function: 'callUserFunction',
lineno: 42,
in_app: false,
vars: {
userId: 17,
attempts: 3,
config: { retries: 5, timeout: 1000 },
token: null,
},
} as never,
},
};

// A frame with no body and no vars must still render a single line with no
// expandable body (chevron hidden).
export const Bare: StoryObj<typeof SentryExceptionFrame> = {
args: {
isOpen: false,
frame: {
filename: '[internal]',
function: 'spl_autoload_call',
lineno: 0,
in_app: false,
} as never,
},
};
55 changes: 51 additions & 4 deletions src/entities/sentry/ui/sentry-exception/sentry-exception-frame.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,26 @@ const { buildLink } = useIdeLink()

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

const hasBody = computed(() =>
Boolean(props.frame.context_line || props.frame.post_context || props.frame.pre_context)
)
const hasBody = computed(() => {
const f = props.frame
return Boolean(
f.context_line ||
(Array.isArray(f.post_context) && f.post_context.length > 0) ||
(Array.isArray(f.pre_context) && f.pre_context.length > 0)
)
})

const hasVars = computed(() => props.frame.vars && Object.keys(props.frame.vars).length > 0)

const varEntries = computed(() =>
props.frame.vars
? Object.entries(props.frame.vars).map(([name, value]) => ({
name,
value: formatVarValue(value)
}))
: []
)

// Floating tooltip state
const tooltip = reactive({
visible: false,
Expand Down Expand Up @@ -198,7 +212,7 @@ const toggleOpen = () => {
</div>

<div
v-if="isFrameOpen && hasBody"
v-if="isFrameOpen && (hasBody || hasVars)"
class="frame__body"
>
<template v-if="frame.pre_context">
Expand Down Expand Up @@ -256,6 +270,25 @@ const toggleOpen = () => {
>{{ seg.text }}</span><template v-else>{{ seg.text }}</template></template></pre>
</div>
</template>

<!-- Vars fallback: shown when the frame has no source context lines.
When source IS available, vars are highlighted inline above. -->
<dl
v-if="hasVars && !hasBody"
class="frame__vars"
>
<template
v-for="entry in varEntries"
:key="entry.name"
>
<dt class="frame__var-name">
{{ entry.name }}
</dt>
<dd class="frame__var-value">
<pre>{{ entry.value }}</pre>
</dd>
</template>
</dl>
</div>

<!-- Fixed-position tooltip (teleported outside overflow) -->
Expand Down Expand Up @@ -356,6 +389,20 @@ const toggleOpen = () => {
@apply bg-amber-500/20;
}
}

/* Vars fallback list (shown when frame has no source context) */
.frame__vars {
@apply grid gap-x-3 gap-y-1 p-2 text-gray-200;
grid-template-columns: max-content 1fr;
}

.frame__var-name {
@apply text-amber-300 font-mono text-2xs self-start whitespace-nowrap;
}

.frame__var-value pre {
@apply text-2xs whitespace-pre-wrap break-words;
}
</style>

<!-- Global styles for teleported tooltip -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,30 @@ export const Spiral: StoryObj<typeof SentryPageTags> = {
payload: normalizeSentryEvent(sentrySpiralMock).payload,
}
};

// A payload that omits runtime / os / sdk and logger / server_name. The
// section should hide its empty context boxes and empty tag pills entirely
// rather than render placeholder rows with blank values.
export const MinimalNoContexts: StoryObj<typeof SentryPageTags> = {
args: {
payload: {
event_id: 'mini-1',
environment: 'production',
platform: 'php',
// No logger, server_name, contexts, sdk — everything else stripped.
} as never,
},
};

// A payload with only logger + env populated. Verifies that those tags appear
// without the runtime/os/server pills.
export const EnvAndLoggerOnly: StoryObj<typeof SentryPageTags> = {
args: {
payload: {
event_id: 'env-only-1',
environment: 'staging',
logger: 'app.errors',
platform: 'php',
} as never,
},
};
66 changes: 39 additions & 27 deletions src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,57 @@ const contextsOS = computed(() => {
return { name, version }
})

const boxes = computed(() => [
{
title: 'Runtime',
name: contextsRuntime.value.name,
version: contextsRuntime.value.version
},
{
title: 'OS',
name: contextsOS.value.name,
version: contextsOS.value.version
},
{
title: 'SDK',
name: props.payload.sdk?.name,
version: props.payload.sdk?.version
}
])

const tags = computed(() => [
{ name: 'env', value: props.payload.environment },
{ name: 'logger', value: props.payload.logger },
{ name: 'os', value: `${contextsOS.value.name} ${contextsOS.value.version}` },
{ name: 'runtime', value: `${contextsRuntime.value.name} ${contextsRuntime.value.version}` },
{ name: 'server', value: props.payload.server_name }
])
const boxes = computed(() =>
[
{
title: 'Runtime',
name: contextsRuntime.value.name,
version: contextsRuntime.value.version
},
{
title: 'OS',
name: contextsOS.value.name,
version: contextsOS.value.version
},
{
title: 'SDK',
name: props.payload.sdk?.name ?? '',
version: props.payload.sdk?.version ?? ''
}
].filter((b) => b.name || b.version)
)

const tags = computed(() => {
const os = `${contextsOS.value.name} ${contextsOS.value.version}`.trim()
const runtime = `${contextsRuntime.value.name} ${contextsRuntime.value.version}`.trim()
return [
{ name: 'env', value: props.payload.environment },
{ name: 'logger', value: props.payload.logger },
{ name: 'os', value: os },
{ name: 'runtime', value: runtime },
{ name: 'server', value: props.payload.server_name }
].filter((t) => t.value)
})
</script>

<template>
<section class="tags-section">
<!-- Context boxes -->
<div class="tags-section__boxes">
<div
v-if="boxes.length"
class="tags-section__boxes"
>
<div
v-for="box in boxes"
:key="box.title"
class="tags-section__box"
>
<span class="tags-section__box-label">{{ box.title }}</span>
<span class="tags-section__box-name">{{ box.name }}</span>
<span class="tags-section__box-version">{{ box.version }}</span>
<span
v-if="box.version"
class="tags-section__box-version"
>{{ box.version }}</span>
</div>
</div>

Expand Down
52 changes: 52 additions & 0 deletions src/entities/sentry/ui/sentry-page/sentry-page.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,55 @@ export const PagePythonLog: StoryObj<typeof SentryPage> = {
event: normalizeSentryEvent(sentryPythonLogMock as unknown as ServerEvent<Sentry>), // TODO: fix ServerEvent<Sentry>
}
};

// A minimal Sentry SDK v4 payload: an error event with contexts.trace.trace_id
// but no transaction was captured, and no runtime/os/sdk/logger metadata.
// Before the fixes this rendered:
// - "View full trace →" link that 404'd
// - empty Runtime / OS / SDK context boxes
// - timestamp in year 1970 (moment treated seconds as ms)
export const PageMinimalErrorNoTrace: StoryObj<typeof SentryPage> = {
args: {
event: normalizeSentryEvent({
uuid: 'mini-1',
type: 'sentry',
project: null,
timestamp: 1774960590,
payload: {
event_id: 'mini-1',
timestamp: 1774960590.323397,
platform: 'php',
level: 'error',
environment: 'staging',
transaction: 'POST /api/orders',
contexts: {
trace: {
trace_id: 'aabbccdd11223344eeff00112233',
span_id: 'spanid01',
},
},
exception: {
values: [
{
type: 'RuntimeException',
value: 'Order processing failed',
stacktrace: {
frames: [
{
filename: 'app/Orders/Processor.php',
function: 'process',
lineno: 42,
in_app: true,
context_line: ' $this->charge($order);',
pre_context: ['public function process(Order $order)', '{'],
post_context: [' $order->markPaid();', '}'],
},
],
},
},
],
},
},
} as unknown as ServerEvent<Sentry>),
}
};
Loading
Loading