Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

123 lines
4.7 KiB
TypeScript

import { AggregatedSpanRow } from '~/queries/schema/schema-general'
// Shared with TraceCompareFlame's node coloring so the table's Regressed/Improved buckets and
// the flame's red/green bars agree on what counts as a change: ±20% on the row's p95 (the
// flame colors per-node on p50, same threshold).
export const CHANGE_THRESHOLD = 0.2
// Rows with fewer baseline samples than this classify as unchanged — percentage deltas on a
// handful of spans are noise, and they'd otherwise dominate the change-first sort.
export const MIN_BASELINE_COUNT = 10
export type CompareRowStatus = 'regressed' | 'improved' | 'new' | 'gone' | 'unchanged'
export interface CompareRow {
service_name: string
name: string
current: AggregatedSpanRow | null
previous: AggregatedSpanRow | null
status: CompareRowStatus
}
// NUL separator, not a space: span names routinely contain spaces, so a printable separator
// would let ("web api", "x") and ("web", "api x") collide on one key.
export const compareRowKey = (row: { service_name: string; name: string }): string => `${row.service_name}\0${row.name}`
export function buildRows(current: AggregatedSpanRow[], previous: AggregatedSpanRow[] | null): CompareRow[] {
const previousByKey = new Map<string, AggregatedSpanRow>()
for (const row of previous ?? []) {
previousByKey.set(compareRowKey(row), row)
}
// A null `previous` means there is no baseline dataset at all (e.g. the compare fetch is
// still in flight) — that must not classify every row as 'new'.
const hasBaseline = previous !== null
const makeRow = (
base: { service_name: string; name: string },
currentRow: AggregatedSpanRow | null,
previousRow: AggregatedSpanRow | null
): CompareRow => ({
service_name: base.service_name,
name: base.name,
current: currentRow,
previous: previousRow,
status: hasBaseline ? classifyRow({ current: currentRow, previous: previousRow }) : 'unchanged',
})
const rows: CompareRow[] = current.map((row) => makeRow(row, row, previousByKey.get(compareRowKey(row)) ?? null))
// Append rows that existed in the previous window but disappeared in the current window —
// useful for spotting fully regressed call sites.
const currentKeys = new Set(current.map(compareRowKey))
for (const row of previous ?? []) {
if (!currentKeys.has(compareRowKey(row))) {
rows.push(makeRow(row, null, row))
}
}
return rows
}
/** Relative change (0.5 = +50%), or null when there's no meaningful baseline. */
function relativeDelta(current: number | null | undefined, previous: number | null | undefined): number | null {
if (current === null || current === undefined || previous === null || previous === undefined || previous === 0) {
return null
}
return (current - previous) / previous
}
type CompareRowSides = Pick<CompareRow, 'current' | 'previous'>
/**
* Percentage deltas on a handful of spans are noise on either side: a row that dropped from
* 1000 calls to 2 slow ones is not a p95 regression any more than a 2-sample baseline is.
*/
export function isLowSample(row: CompareRowSides): boolean {
return !row.current || !row.previous || Math.min(row.current.count, row.previous.count) < MIN_BASELINE_COUNT
}
/** The row's p95 relative delta with the low-sample noise guard applied. */
function guardedP95Delta(row: CompareRowSides): number | null {
if (!row.current || !row.previous || isLowSample(row)) {
return null
}
return relativeDelta(row.current.p95_duration_nano, row.previous.p95_duration_nano)
}
export function classifyRow(row: CompareRowSides): CompareRowStatus {
if (!row.current) {
return 'gone'
}
if (!row.previous) {
return 'new'
}
const delta = guardedP95Delta(row)
if (delta === null) {
return 'unchanged'
}
if (delta > CHANGE_THRESHOLD) {
return 'regressed'
}
if (delta < -CHANGE_THRESHOLD) {
return 'improved'
}
return 'unchanged'
}
/**
* Sort key for the default change-first ordering: biggest movers (either direction) first.
* New rows are by definition maximal change; gone rows sort below everything, including
* unchanged rows, so vanished call sites collect at the bottom under a descending sort.
*/
export function changeMagnitude(row: CompareRowSides): number {
if (!row.current) {
return -1
}
if (!row.previous) {
// Sparse new rows (unparameterized URLs and the like) would otherwise flood the top
// of the change sort — the same noise the low-sample guard suppresses elsewhere.
return row.current.count >= MIN_BASELINE_COUNT ? Number.MAX_VALUE : 0
}
return Math.abs(guardedP95Delta(row) ?? 0)
}