mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +00:00
fix(ui): preserve distinct log detail rows
This commit is contained in:
1 parent
9eb0201b79
commit
c0f5413523
2 files changed
+83
-8
No files matched your search
@@ -17,20 +17,30 @@ export interface LeafItem {
|
|||||||
|
|
||||||
export type DerivedItem = LeafItem | TraceGroup;
|
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
|
* Tuple key for coalescing standalone leaves. The row/detail UI exposes more
|
||||||
* `source` so two adjacent logs that share event/module/level but report
|
* than event/module/level, so every inspectable payload field must participate
|
||||||
* different content (or come from a different service) stay distinct
|
* in equality; otherwise two adjacent logs with different metadata, context,
|
||||||
* (e.g. `User logged in: alice` and `User logged in: bob`). Excludes
|
* error, or latency could collapse into one selectable row. `timestamp` stays
|
||||||
* `latencyMs` and `metadata` because those drift per request even on
|
* out of the key because collapsed rows surface their time span separately.
|
||||||
* truly redundant polls — including them would prevent any coalescing.
|
|
||||||
*
|
*
|
||||||
* NB: this only applies to *leaves* (entries without `requestId`). Trace
|
* NB: this only applies to *leaves* (entries without `requestId`). Trace
|
||||||
* children render uncoalesced so retries and duplicated-stage emissions
|
* children render uncoalesced so retries and duplicated-stage emissions
|
||||||
* stay individually inspectable.
|
* stay individually inspectable.
|
||||||
*/
|
*/
|
||||||
function coalesceKey(entry: LogsEntry): string {
|
function coalesceKey(entry: LogsEntry): string {
|
||||||
return [
|
return JSON.stringify([
|
||||||
entry.event ?? '',
|
entry.event ?? '',
|
||||||
entry.message ?? '',
|
entry.message ?? '',
|
||||||
entry.stage ?? '',
|
entry.stage ?? '',
|
||||||
@@ -38,7 +48,13 @@ function coalesceKey(entry: LogsEntry): string {
|
|||||||
entry.level,
|
entry.level,
|
||||||
entry.requestId ?? '',
|
entry.requestId ?? '',
|
||||||
entry.source ?? '',
|
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);
|
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', () => {
|
it('display-sorts items reverse-chronologically', () => {
|
||||||
// Use distinct events so leaves don't coalesce — testing display sort,
|
// Use distinct events so leaves don't coalesce — testing display sort,
|
||||||
// not coalesce.
|
// not coalesce.
|
||||||
|
|||||||
Reference in new issue
Block a user