@innertia-solutions/innertia-nuxt 0.1.56 → 0.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,8 @@
1
1
  <script setup>
2
- // Detalle de un caso de error (Sentry-like): header + criticidad, timeline de
3
- // ocurrencias, stack trace, contexto del request y traces de muestra.
4
- const props = defineProps({
5
- id: { type: [String, Number], required: true },
6
- })
2
+ // Detalle de un caso de error (Sentry-like) desde el snapshot durable del caso:
3
+ // stack completo (in-app resaltado + código fuente), contexto del request,
4
+ // cadena de excepciones y breadcrumbs. Timeline de ocurrencias arriba.
5
+ const props = defineProps({ id: { type: [String, Number], required: true } })
7
6
  const emit = defineEmits(['back'])
8
7
  const api = useApi()
9
8
 
@@ -13,12 +12,14 @@ const { data, pending } = await useAsyncData(
13
12
  { watch: [() => props.id] },
14
13
  )
15
14
 
16
- const exceptionEvent = computed(() => (data.value?.events ?? []).find(e => e.type === 'exception'))
17
- const stackFrames = computed(() => exceptionEvent.value?.payload?.stack ?? exceptionEvent.value?.payload?.frames ?? [])
15
+ const snap = computed(() => data.value?.snapshot ?? null)
18
16
 
19
17
  const timelineSeries = computed(() => [{ name: 'Ocurrencias', data: (data.value?.timeline ?? []).map(d => d.count) }])
20
18
  const timelineCats = computed(() => (data.value?.timeline ?? []).map(d => d.day))
21
19
 
20
+ const expanded = ref({})
21
+ function toggle(i) { expanded.value = { ...expanded.value, [i]: !expanded.value[i] } }
22
+
22
23
  const STATUS = { open: 'Abierto', resolved: 'Resuelto', ignored: 'Ignorado' }
23
24
  </script>
24
25
 
@@ -33,10 +34,14 @@ const STATUS = { open: 'Abierto', resolved: 'Resuelto', ignored: 'Ignorado' }
33
34
  <div class="flex flex-wrap items-center gap-2">
34
35
  <span v-if="data.is_critical" class="rounded-full bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300 px-2 py-0.5 text-xs font-semibold">Crítico</span>
35
36
  <span class="rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300 px-2 py-0.5 text-xs">{{ STATUS[data.case.status] ?? data.case.status }}</span>
37
+ <span v-if="data.case.origin" class="rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300 px-2 py-0.5 text-xs">{{ ({http:'HTTP',queue:'Worker',schedule:'Cron',console:'Consola'})[data.case.origin] ?? data.case.origin }}</span>
38
+ <span v-if="data.case.category" class="rounded-full bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300 px-2 py-0.5 text-xs">{{ ({database:'BD',http:'HTTP',auth:'Auth',validation:'Validación',runtime:'Runtime'})[data.case.category] ?? data.case.category }}</span>
39
+ <span v-for="(v, k) in (data.case.tags ?? {})" :key="k" class="rounded-full bg-primary/10 text-primary px-2 py-0.5 text-xs">{{ k }}: {{ v }}</span>
36
40
  <span class="text-xs text-muted-foreground">{{ data.case.times_seen }} ocurrencias</span>
37
41
  </div>
38
42
  <h2 class="mt-2 text-lg font-semibold text-foreground">{{ data.case.title }}</h2>
39
43
  <p class="text-sm text-muted-foreground font-mono">{{ data.case.exception_class }}</p>
44
+ <p v-if="snap?.message" class="mt-1 text-sm text-foreground">{{ snap.message }}</p>
40
45
  <p class="mt-1 text-xs text-muted-foreground">Primera vez: {{ data.case.first_seen }} · Última: {{ data.case.last_seen }}</p>
41
46
  </div>
42
47
 
@@ -45,38 +50,71 @@ const STATUS = { open: 'Abierto', resolved: 'Resuelto', ignored: 'Ignorado' }
45
50
  <Chart type="bar" :series="timelineSeries" :categories="timelineCats" :height="160" />
46
51
  </section>
47
52
 
48
- <section v-if="data.trace" class="rounded-card border border-card-line bg-card p-4 text-sm">
49
- <h3 class="mb-2 text-sm font-semibold text-foreground">Contexto del request</h3>
50
- <dl class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
51
- <div><dt class="text-muted-foreground inline">Ruta:</dt> <dd class="inline text-foreground">{{ data.trace.method }} {{ data.trace.route ?? data.trace.url }}</dd></div>
52
- <div><dt class="text-muted-foreground inline">Status:</dt> <dd class="inline text-foreground">{{ data.trace.status ?? '—' }}</dd></div>
53
- <div><dt class="text-muted-foreground inline">Usuario:</dt> <dd class="inline text-foreground">{{ data.trace.user_id ?? '—' }}</dd></div>
54
- <div><dt class="text-muted-foreground inline">Tenant:</dt> <dd class="inline text-foreground">{{ data.trace.tenant ?? '—' }}</dd></div>
55
- <div><dt class="text-muted-foreground inline">IP:</dt> <dd class="inline text-foreground">{{ data.trace.ip ?? '—' }}</dd></div>
56
- <div><dt class="text-muted-foreground inline">Duración:</dt> <dd class="inline text-foreground">{{ data.trace.duration_ms != null ? data.trace.duration_ms + ' ms' : '—' }}</dd></div>
57
- </dl>
58
- </section>
53
+ <div v-if="!snap" class="rounded-card border border-amber-200 bg-amber-50 p-3 text-xs text-amber-700 dark:border-amber-800/50 dark:bg-amber-900/20 dark:text-amber-400">
54
+ Este caso no tiene snapshot (ocurrió antes de habilitarse la captura durable). Volverá a completarse la próxima vez que el error reincida.
55
+ </div>
59
56
 
60
- <section v-if="stackFrames.length">
61
- <h3 class="mb-2 text-sm font-semibold text-foreground">Stack trace</h3>
62
- <ol class="rounded-card border border-card-line bg-card divide-y divide-card-line text-xs font-mono">
63
- <li v-for="(f, i) in stackFrames" :key="i" class="px-3 py-1.5">
64
- <span class="text-foreground">{{ f.function ?? f.call ?? '' }}</span>
65
- <span class="text-muted-foreground"> — {{ f.file ?? '' }}<span v-if="f.line">:{{ f.line }}</span></span>
66
- </li>
67
- </ol>
68
- </section>
69
- <section v-else-if="exceptionEvent">
70
- <h3 class="mb-2 text-sm font-semibold text-foreground">Excepción</h3>
71
- <pre class="rounded-card border border-card-line bg-card p-3 text-xs whitespace-pre-wrap">{{ exceptionEvent.payload?.message ?? data.case.title }}</pre>
72
- </section>
57
+ <template v-else>
58
+ <section v-if="snap.previous?.length">
59
+ <h3 class="mb-2 text-sm font-semibold text-foreground">Causado por</h3>
60
+ <ul class="space-y-1 text-xs">
61
+ <li v-for="(p, i) in snap.previous" :key="i" class="rounded-card border border-card-line bg-card px-3 py-1.5">
62
+ <span class="font-mono text-foreground">{{ p.class }}</span>
63
+ <span class="text-muted-foreground"> — {{ p.message }} <span class="opacity-70">({{ p.file }}:{{ p.line }})</span></span>
64
+ </li>
65
+ </ul>
66
+ </section>
73
67
 
74
- <section v-if="data.samples?.length">
75
- <h3 class="mb-2 text-sm font-semibold text-foreground">Ocurrencias de muestra</h3>
76
- <ul class="text-xs text-muted-foreground space-y-1">
77
- <li v-for="s in data.samples" :key="s.trace_id">{{ s.occurred_at }} · {{ s.method }} {{ s.route ?? s.url }} · {{ s.status ?? '—' }}</li>
78
- </ul>
79
- </section>
68
+ <section v-if="snap.request" class="rounded-card border border-card-line bg-card p-4 text-sm">
69
+ <h3 class="mb-2 text-sm font-semibold text-foreground">Contexto del request</h3>
70
+ <dl class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
71
+ <div><dt class="text-muted-foreground inline">Ruta:</dt> <dd class="inline text-foreground">{{ snap.request.method }} {{ snap.request.route ?? snap.request.url }}</dd></div>
72
+ <div><dt class="text-muted-foreground inline">Status:</dt> <dd class="inline text-foreground">{{ data.trace?.status ?? '—' }}</dd></div>
73
+ <div><dt class="text-muted-foreground inline">Usuario:</dt> <dd class="inline text-foreground">{{ snap.request.user_id ?? '—' }}</dd></div>
74
+ <div><dt class="text-muted-foreground inline">IP:</dt> <dd class="inline text-foreground">{{ snap.request.ip ?? '—' }}</dd></div>
75
+ <div class="col-span-2"><dt class="text-muted-foreground inline">User-Agent:</dt> <dd class="inline text-foreground break-all">{{ snap.request.user_agent ?? '—' }}</dd></div>
76
+ </dl>
77
+ <details v-if="snap.request.query && Object.keys(snap.request.query).length" class="mt-2 text-xs">
78
+ <summary class="cursor-pointer text-muted-foreground">Query params</summary>
79
+ <pre class="mt-1 overflow-x-auto rounded bg-slate-50 dark:bg-slate-900/40 p-2 text-[11px]">{{ JSON.stringify(snap.request.query, null, 2) }}</pre>
80
+ </details>
81
+ <details v-if="snap.request.body && Object.keys(snap.request.body).length" class="mt-2 text-xs">
82
+ <summary class="cursor-pointer text-muted-foreground">Body</summary>
83
+ <pre class="mt-1 overflow-x-auto rounded bg-slate-50 dark:bg-slate-900/40 p-2 text-[11px]">{{ JSON.stringify(snap.request.body, null, 2) }}</pre>
84
+ </details>
85
+ <details v-if="snap.request.headers && Object.keys(snap.request.headers).length" class="mt-2 text-xs">
86
+ <summary class="cursor-pointer text-muted-foreground">Headers</summary>
87
+ <pre class="mt-1 overflow-x-auto rounded bg-slate-50 dark:bg-slate-900/40 p-2 text-[11px]">{{ JSON.stringify(snap.request.headers, null, 2) }}</pre>
88
+ </details>
89
+ </section>
90
+
91
+ <section v-if="snap.stack?.length">
92
+ <h3 class="mb-2 text-sm font-semibold text-foreground">Stack trace</h3>
93
+ <div class="rounded-card border border-card-line bg-card divide-y divide-card-line overflow-hidden">
94
+ <div v-for="(f, i) in snap.stack" :key="i" :class="f.in_app ? '' : 'opacity-60'">
95
+ <button type="button" class="w-full flex items-baseline gap-2 px-3 py-1.5 text-left text-xs hover:bg-slate-50 dark:hover:bg-slate-800/50" @click="toggle(i)">
96
+ <span v-if="f.in_app" class="mt-0.5 inline-block size-1.5 rounded-full bg-primary shrink-0" title="in-app" />
97
+ <span class="font-mono text-foreground">{{ f.function || '{main}' }}</span>
98
+ <span class="font-mono text-muted-foreground ml-auto shrink-0">{{ f.file }}<span v-if="f.line">:{{ f.line }}</span></span>
99
+ </button>
100
+ <div v-if="f.context && (expanded[i] || f.in_app)" class="bg-slate-50 dark:bg-slate-900/40 overflow-x-auto">
101
+ <pre class="text-[11px] leading-5 font-mono"><template v-for="ln in f.context" :key="ln.n"><div :class="ln.error ? 'bg-red-100 dark:bg-red-500/20' : ''" class="px-3"><span class="inline-block w-8 select-none text-muted-foreground text-right pr-2">{{ ln.n }}</span><span :class="ln.error ? 'text-red-700 dark:text-red-300' : 'text-foreground'">{{ ln.code }}</span></div></template></pre>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </section>
106
+
107
+ <section v-if="snap.breadcrumbs?.length">
108
+ <h3 class="mb-2 text-sm font-semibold text-foreground">Breadcrumbs (antes del error)</h3>
109
+ <ul class="rounded-card border border-card-line bg-card divide-y divide-card-line text-xs">
110
+ <li v-for="(b, i) in snap.breadcrumbs" :key="i" class="flex items-baseline gap-2 px-3 py-1.5">
111
+ <span class="rounded bg-slate-100 dark:bg-slate-700 px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground shrink-0">{{ b.type }}</span>
112
+ <span class="font-mono text-foreground truncate">{{ b.summary }}</span>
113
+ <span v-if="b.duration_ms != null" class="ml-auto text-muted-foreground shrink-0">{{ b.duration_ms }} ms</span>
114
+ </li>
115
+ </ul>
116
+ </section>
117
+ </template>
80
118
  </template>
81
119
  </div>
82
120
  </template>
@@ -14,6 +14,17 @@ const columns = [
14
14
  { key: 'title', label: 'Error' },
15
15
  { key: 'exception_class', label: 'Clase' },
16
16
  { key: 'times_seen', label: 'Veces', sortable: true },
17
+ { key: 'origin', label: 'Origen', filterable: true, filterType: 'select',
18
+ filterOptions: [
19
+ { value: 'http', label: 'HTTP' }, { value: 'queue', label: 'Worker' },
20
+ { value: 'schedule', label: 'Cron' }, { value: 'console', label: 'Consola' },
21
+ ] },
22
+ { key: 'category', label: 'Tipo', filterable: true, filterType: 'select',
23
+ filterOptions: [
24
+ { value: 'database', label: 'Base de datos' }, { value: 'http', label: 'HTTP' },
25
+ { value: 'auth', label: 'Auth' }, { value: 'validation', label: 'Validación' },
26
+ { value: 'runtime', label: 'Runtime' },
27
+ ] },
17
28
  { key: 'status', label: 'Estado', filterable: true, filterType: 'select',
18
29
  filterOptions: [
19
30
  { value: 'open', label: 'Abierto' },
@@ -57,6 +68,15 @@ watch(() => props.touchedAt, () => { reloadKey.value++ })
57
68
  <span class="text-foreground">{{ row.title }}</span>
58
69
  </div>
59
70
  </template>
71
+ <template #origin="{ value }">
72
+ <span class="inline-flex items-center rounded-full bg-slate-100 dark:bg-slate-700 px-2 py-0.5 text-xs text-slate-600 dark:text-slate-300">{{ ({http:'HTTP',queue:'Worker',schedule:'Cron',console:'Consola'})[value] ?? value ?? '—' }}</span>
73
+ </template>
74
+ <template #category="{ value }">
75
+ <span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs"
76
+ :class="value === 'database' ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300' : value === 'auth' ? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300' : 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300'">
77
+ {{ ({database:'BD',http:'HTTP',auth:'Auth',validation:'Validación',runtime:'Runtime'})[value] ?? value ?? '—' }}
78
+ </span>
79
+ </template>
60
80
  <template #status="{ value }">{{ statusLabels[value] ?? value }}</template>
61
81
  <template #actions="{ row }">
62
82
  <div class="flex gap-2">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/innertia-nuxt",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "description": "Innertia Solutions — Nuxt layer unificada: core, auth, multitenancy, theme y app contexts",
5
5
  "keywords": [
6
6
  "nuxt",