fix(ui): preserve distinct log detail rows

This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-30 15:29:47 -04:00
committed by GitHub
parent 9eb0201b79
commit c0f5413523
2 changed files with 83 additions and 8 deletions
+24 -8
View File
@@ -17,20 +17,30 @@ export interface LeafItem {
export type DerivedItem = LeafItem | TraceGroup;
function stableStringify(value: unknown): string {
if (value === undefined) return '';
if (value === null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
return `{${Object.entries(value as Record<string, unknown>)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
.join(',')}}`;
}
/**
* Tuple key for coalescing standalone leaves. Includes `message` and
* `source` so two adjacent logs that share event/module/level but report
* different content (or come from a different service) stay distinct
* (e.g. `User logged in: alice` and `User logged in: bob`). Excludes
* `latencyMs` and `metadata` because those drift per request even on
* truly redundant polls — including them would prevent any coalescing.
* Tuple key for coalescing standalone leaves. The row/detail UI exposes more
* than event/module/level, so every inspectable payload field must participate
* in equality; otherwise two adjacent logs with different metadata, context,
* error, or latency could collapse into one selectable row. `timestamp` stays
* out of the key because collapsed rows surface their time span separately.
*
* NB: this only applies to *leaves* (entries without `requestId`). Trace
* children render uncoalesced so retries and duplicated-stage emissions
* stay individually inspectable.
*/
function coalesceKey(entry: LogsEntry): string {
return [
return JSON.stringify([
entry.event ?? '',
entry.message ?? '',
entry.stage ?? '',
@@ -38,7 +48,13 @@ function coalesceKey(entry: LogsEntry): string {
entry.level,
entry.requestId ?? '',
entry.source ?? '',
].join(' ');
entry.runId ?? '',
String(entry.processId ?? ''),
String(entry.latencyMs ?? ''),
stableStringify(entry.context),
stableStringify(entry.metadata),
stableStringify(entry.error),
]);
}
/**
@@ -141,6 +141,65 @@ describe('deriveTraceGroups', () => {
expect(result.every((r) => r.kind === 'leaf' && r.repeatCount === undefined)).toBe(true);
});
it('keeps adjacent leaves with different detail payloads as separate rows', () => {
const result = deriveTraceGroups(
leafEntries(
{
id: '1',
timestamp: 't1',
event: 'poll',
message: 'same',
latencyMs: 25,
metadata: { attempt: 1, nested: { status: 'warm' } },
context: { account: 'alpha' },
error: { code: 'E_ONE', message: 'first' },
},
{
id: '2',
timestamp: 't2',
event: 'poll',
message: 'same',
latencyMs: 50,
metadata: { attempt: 2, nested: { status: 'cold' } },
context: { account: 'beta' },
error: { code: 'E_TWO', message: 'second' },
}
)
);
expect(result).toHaveLength(2);
expect(result.every((r) => r.kind === 'leaf' && r.repeatCount === undefined)).toBe(true);
expect(result.map((r) => (r.kind === 'leaf' ? r.entry.id : 'trace'))).toEqual(['2', '1']);
});
it('coalesces leaves with semantically identical structured payloads', () => {
const result = deriveTraceGroups(
leafEntries(
{
id: '1',
timestamp: 't1',
event: 'poll',
message: 'tick',
latencyMs: 25,
metadata: { b: 2, a: 1 },
context: { nested: { b: false, a: true } },
},
{
id: '2',
timestamp: 't2',
event: 'poll',
message: 'tick',
latencyMs: 25,
metadata: { a: 1, b: 2 },
context: { nested: { a: true, b: false } },
}
)
);
expect(result).toHaveLength(1);
expect((result[0] as LeafItem).repeatCount).toBe(2);
});
it('display-sorts items reverse-chronologically', () => {
// Use distinct events so leaves don't coalesce — testing display sort,
// not coalesce.