@@ -22,27 +22,47 @@ export function escapeHtml(str: string): string {
2222
2323export function looksLikeJsonString ( str : string ) : boolean {
2424 const t = str . trim ( )
25- return ( t . startsWith ( '{' ) && t . endsWith ( '}' ) ) || ( t . startsWith ( '[' ) && t . endsWith ( ']' ) )
25+ return (
26+ ( t . startsWith ( '{' ) && t . endsWith ( '}' ) ) ||
27+ ( t . startsWith ( '[' ) && t . endsWith ( ']' ) ) ||
28+ ( t . startsWith ( '"' ) && t . endsWith ( '"' ) )
29+ )
30+ }
31+
32+ /**
33+ * Parse JSON that may be stored as a string, including double-encoded hlog()
34+ * payloads (`"{\"cause\":...}"`). Returns the inner object/array, or null when
35+ * the input is not structured JSON.
36+ */
37+ export function parseJsonDeep ( input : unknown , maxDepth = 3 ) : unknown {
38+ let v : unknown = input
39+ for ( let i = 0 ; i <= maxDepth ; i ++ ) {
40+ if ( v !== null && typeof v === 'object' ) return v
41+ if ( typeof v !== 'string' ) return null
42+ const t = v . trim ( )
43+ if ( ! looksLikeJsonString ( t ) ) return null
44+ try {
45+ v = JSON . parse ( t )
46+ } catch {
47+ return null
48+ }
49+ }
50+ return null
2651}
2752
2853/** Parse a JSON string; return the original value when it is not JSON text. */
2954export function parseJsonLoose ( val : unknown ) : unknown {
3055 if ( val == null ) return val
3156 if ( typeof val === 'object' ) return val
3257 if ( typeof val !== 'string' ) return val
33- const t = val . trim ( )
34- if ( ! looksLikeJsonString ( t ) ) return val
35- try {
36- return JSON . parse ( t )
37- } catch {
38- return val
39- }
58+ const deep = parseJsonDeep ( val )
59+ return deep ?? val
4060}
4161
4262export function isJsonDisplayable ( val : unknown ) : boolean {
4363 if ( val == null || val === '' ) return false
4464 if ( typeof val === 'object' ) return true
45- if ( typeof val === 'string' ) return looksLikeJsonString ( val )
65+ if ( typeof val === 'string' ) return parseJsonDeep ( val ) !== null
4666 return false
4767}
4868
@@ -92,10 +112,23 @@ export function serializeRowForDisplay(row: Record<string, unknown>): string {
92112 }
93113}
94114
115+ function formatJsonForHighlight ( payload : unknown ) : string {
116+ if ( payload !== null && typeof payload === 'object' && ! Array . isArray ( payload ) ) {
117+ return JSON . stringify ( payload , expandEmbeddedJsonReplacer , 2 )
118+ }
119+ const parsed = parseJsonLoose ( payload )
120+ if ( parsed !== payload ) {
121+ return JSON . stringify ( parsed , null , 2 )
122+ }
123+ if ( typeof payload === 'string' ) {
124+ return payload
125+ }
126+ return JSON . stringify ( parsed , null , 2 )
127+ }
128+
95129export function highlightJSON ( payload : unknown ) : string {
96130 try {
97- const obj = parseJsonLoose ( payload )
98- const formatted = JSON . stringify ( obj , null , 2 )
131+ const formatted = formatJsonForHighlight ( payload )
99132 let html = escapeHtml ( formatted )
100133 // Value highlighting before keys — the key pass wraps colons and breaks later ": …" patterns.
101134 html = html . replace (
@@ -116,12 +149,34 @@ export function highlightJSON(payload: unknown): string {
116149 }
117150}
118151
152+ /** Primary payload column names on LOG / event rows (first match wins). */
153+ export const EVENT_PAYLOAD_FIELD_KEYS = [ 'payload' , 'message' , 'data' , 'body' ] as const
154+
119155/** First non-empty payload-like field on a LOG / event row. */
120156export function eventPayloadField ( row : Record < string , unknown > | null | undefined ) : unknown {
121- if ( ! row || typeof row !== 'object' ) return ''
122- for ( const k of [ 'payload' , 'message' , 'data' , 'body' ] ) {
157+ const key = eventPayloadFieldKey ( row )
158+ return key ? row [ key ] : ''
159+ }
160+
161+ /** Which payload column is populated on this row, if any. */
162+ export function eventPayloadFieldKey (
163+ row : Record < string , unknown > | null | undefined ,
164+ ) : ( typeof EVENT_PAYLOAD_FIELD_KEYS ) [ number ] | null {
165+ if ( ! row || typeof row !== 'object' ) return null
166+ for ( const k of EVENT_PAYLOAD_FIELD_KEYS ) {
123167 const v = row [ k ]
124- if ( v != null && v !== '' ) return v
168+ if ( v != null && v !== '' ) return k
125169 }
126- return ''
170+ return null
171+ }
172+
173+ /** Row copy without the primary payload column (for metadata-only panels). */
174+ export function rowWithoutEventPayload (
175+ row : Record < string , unknown > ,
176+ ) : Record < string , unknown > {
177+ const key = eventPayloadFieldKey ( row )
178+ if ( ! key ) return row
179+ const copy = { ...row }
180+ delete copy [ key ]
181+ return copy
127182}
0 commit comments