diff --git a/src/entities/sentry/ui/sentry-exception/sentry-exception-frame.stories.ts b/src/entities/sentry/ui/sentry-exception/sentry-exception-frame.stories.ts index c4611c9a..955ba930 100644 --- a/src/entities/sentry/ui/sentry-exception/sentry-exception-frame.stories.ts +++ b/src/entities/sentry/ui/sentry-exception/sentry-exception-frame.stories.ts @@ -17,3 +17,38 @@ export const Frame: StoryObj = { 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 = { + 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 = { + args: { + isOpen: false, + frame: { + filename: '[internal]', + function: 'spl_autoload_call', + lineno: 0, + in_app: false, + } as never, + }, +}; diff --git a/src/entities/sentry/ui/sentry-exception/sentry-exception-frame.vue b/src/entities/sentry/ui/sentry-exception/sentry-exception-frame.vue index d5c8c5c9..343be22d 100644 --- a/src/entities/sentry/ui/sentry-exception/sentry-exception-frame.vue +++ b/src/entities/sentry/ui/sentry-exception/sentry-exception-frame.vue @@ -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, @@ -198,7 +212,7 @@ const toggleOpen = () => {
+ + +
+ +
@@ -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; +} diff --git a/src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.stories.ts b/src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.stories.ts index 22303133..98091370 100644 --- a/src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.stories.ts +++ b/src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.stories.ts @@ -21,3 +21,30 @@ export const Spiral: StoryObj = { 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 = { + 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 = { + args: { + payload: { + event_id: 'env-only-1', + environment: 'staging', + logger: 'app.errors', + platform: 'php', + } as never, + }, +}; diff --git a/src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.vue b/src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.vue index f3375e8a..a0a01040 100644 --- a/src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.vue +++ b/src/entities/sentry/ui/sentry-page-tags/sentry-page-tags.vue @@ -21,37 +21,46 @@ 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) +})