mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 08:17:22 +00:00
fix(ui): preserve distinct log detail rows
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user