feat(analytics): BI-P103 — BI SLOs measured from AUD0's usage events, or declared unmeasured
CI (SIAX Cloud) / quality (push) Successful in 54s
CI (SIAX Cloud) / integration (push) Successful in 49s
CI (SIAX Cloud) / security (push) Successful in 28s
Security Scan (SIAX Cloud) / security (push) Successful in 30s
CI (SIAX Cloud) / quality (pull_request) Successful in 58s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m6s
CI (SIAX Cloud) / security (pull_request) Successful in 33s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 39s

aud0#34 merged seven BI usage event types and recorded that zero producers
emit them. This makes 0pt the producer and derives the SLOs from those events
rather than opening a second, private measurement path that would disagree
with aud0 during an incident.

Three honest verdicts instead of one number:

  insufficient_data     an availability objective over an empty window is
                        SILENCE, not 100%. Every objective in an empty window
                        comes back insufficient_data and fully_measured is
                        false. A started query with no terminal event counts
                        as unresolved — not as a success.
  not_measurable_here   dashboard FRESHNESS is a property of the dataset
                        (BI-P026/BI-P105 watermark) and report DELIVERY is
                        N0TIFY's receipt. Neither is in aud0's seven types, so
                        both are declared unmeasured rather than proxied by
                        completion.
  low_confidence        a p99 over 12 samples is the 12th-slowest request
                        wearing a percentile's clothes. Reported with its
                        sample size and not judged.

The producer is an allowlist built field by named field, never a spread. The
test feeds it an execution record carrying SQL text, a database error message
quoting a personnummer, filter VALUES, a tenant id and a result sample, then
greps the emitted events — after first proving the grep finds those strings in
the input, so a clean result cannot be a broken detector.

14 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude Opus 5
2026-09-07 04:01:16 +02:00
parent 96ff601248
commit ed6e62809d
3 changed files with 1003 additions and 0 deletions
+501
View File
@@ -0,0 +1,501 @@
/**
* src/analytics/slo/bi-slo.ts
* ============================
* BI-P103 — BI SLO. DoD: "measured baselines exist".
*
* An SLO you cannot measure from real telemetry is a wish, so every objective
* here declares WHERE its number comes from, and the evaluator refuses to
* produce a number it does not have. Three ways that refusal shows up, each of
* which is a lie the usual implementation tells:
*
* insufficient_data nothing (or too little) was observed in the window.
* An availability objective evaluated over zero events
* is NOT 100% attainment — it is silence. This is the
* single most common false green in SLO reporting, and
* it is the same shape BI-P025 found in a quality
* check over an empty table.
* not_measurable_here the objective is real but these events cannot
* measure it. Dashboard data FRESHNESS is a property
* of the dataset (BI-P026 / BI-P105 watermarks), not
* of a usage event, and REPORT DELIVERY is N0TIFY's
* fact — aud0's seven types carry no delivery event.
* Saying so beats fabricating a proxy.
* low_confidence a p95 or p99 computed from a handful of samples. The
* number is reported WITH its sample size and marked,
* because a p99 over 12 requests is the 12th-slowest
* request wearing a percentile's clothes.
*
* The measurement path is aud0's seven BI usage event types (BI-P076, merged
* in aud0#34), produced by src/analytics/slo/usage-events.ts. There is
* deliberately no second telemetry path: two measurement systems disagree, and
* the disagreement is discovered during an incident.
*/
import type { Aud0BiEvent } from './usage-events';
export const BI_WORKLOAD_CLASSES = ['interactive_dashboard', 'exploration', 'report'] as const;
export type BiWorkloadClass = (typeof BI_WORKLOAD_CLASSES)[number];
export const SLO_OBJECTIVE_KINDS = [
'availability',
'latency_p95_first',
'latency_p95_cached',
'latency_p95',
'latency_p99',
'error_rate',
'freshness',
'max_result_rows',
'timeout',
'completion_sla',
'delivery_sla',
] as const;
export type SloObjectiveKind = (typeof SLO_OBJECTIVE_KINDS)[number];
/** Where the number comes from. `none` is an admission, not a placeholder. */
export const MEASUREMENT_SOURCES = [
'aud0_bi_usage_events',
'analytics_freshness_watermark',
'n0tify_delivery_receipt',
'engine_configuration',
] as const;
export type MeasurementSource = (typeof MEASUREMENT_SOURCES)[number];
export interface SloObjective {
objective_id: string;
workload: BiWorkloadClass;
kind: SloObjectiveKind;
/** Target value; unit is fixed by kind (ms, ratio, rows, seconds). */
target: number;
unit: 'ms' | 'ratio' | 'rows' | 'seconds';
/** Higher-is-better (availability) vs lower-is-better (latency). */
direction: 'at_least' | 'at_most';
measured_from: MeasurementSource;
/** Fewer samples than this and the verdict carries low_confidence. */
min_samples: number;
rationale: string;
}
/**
* The catalogue, field-for-field with what BI-P103 lists per workload class.
* Targets are STARTING POINTS chosen to be honest about what has been
* measured: they are the thresholds the evaluator compares against, and
* evidence/bi-p103 records the first baselines rather than pretending the
* targets were derived from a year of production traffic.
*/
export const BI_SLO_CATALOGUE: readonly SloObjective[] = [
// ── interactive dashboard ────────────────────────────────────────────────
{
objective_id: 'dash.availability',
workload: 'interactive_dashboard',
kind: 'availability',
target: 0.995,
unit: 'ratio',
direction: 'at_least',
measured_from: 'aud0_bi_usage_events',
min_samples: 100,
rationale: 'completed / (completed + failed) for dashboard-surface queries.',
},
{
objective_id: 'dash.p95_first_query',
workload: 'interactive_dashboard',
kind: 'latency_p95_first',
target: 3000,
unit: 'ms',
direction: 'at_most',
measured_from: 'aud0_bi_usage_events',
min_samples: 50,
rationale: 'cache_hit=false: what a user waits for when they open the dashboard cold.',
},
{
objective_id: 'dash.p95_cached',
workload: 'interactive_dashboard',
kind: 'latency_p95_cached',
target: 400,
unit: 'ms',
direction: 'at_most',
measured_from: 'aud0_bi_usage_events',
min_samples: 50,
rationale:
'cache_hit=true: BI-P020 cache hits, measured separately so a high hit rate cannot hide a slow cold path.',
},
{
objective_id: 'dash.error_rate',
workload: 'interactive_dashboard',
kind: 'error_rate',
target: 0.005,
unit: 'ratio',
direction: 'at_most',
measured_from: 'aud0_bi_usage_events',
min_samples: 100,
rationale: 'failed / terminal. Cancellations count as failures here: the user did not get an answer.',
},
{
objective_id: 'dash.freshness',
workload: 'interactive_dashboard',
kind: 'freshness',
target: 900,
unit: 'seconds',
direction: 'at_most',
measured_from: 'analytics_freshness_watermark',
min_samples: 1,
rationale:
'Data age is a property of the DATASET (BI-P026 freshness, BI-P105 watermark), not of a usage ' +
'event. Usage events cannot measure it and this evaluator does not pretend to.',
},
// ── exploration ──────────────────────────────────────────────────────────
{
objective_id: 'explore.p95',
workload: 'exploration',
kind: 'latency_p95',
target: 10_000,
unit: 'ms',
direction: 'at_most',
measured_from: 'aud0_bi_usage_events',
min_samples: 50,
rationale: 'explore-surface queries; an analyst tolerates more than a dashboard viewer.',
},
{
objective_id: 'explore.p99',
workload: 'exploration',
kind: 'latency_p99',
target: 60_000,
unit: 'ms',
direction: 'at_most',
measured_from: 'aud0_bi_usage_events',
min_samples: 200,
rationale: 'p99 needs a genuinely large sample; below min_samples it is reported as low confidence.',
},
{
objective_id: 'explore.max_result',
workload: 'exploration',
kind: 'max_result_rows',
target: 1_000_000,
unit: 'rows',
direction: 'at_most',
measured_from: 'aud0_bi_usage_events',
min_samples: 1,
rationale: 'BI-P012 truncates at the engine ceiling; this watches what actually came back.',
},
{
objective_id: 'explore.timeout',
workload: 'exploration',
kind: 'timeout',
target: 300,
unit: 'seconds',
direction: 'at_most',
measured_from: 'engine_configuration',
min_samples: 1,
rationale: 'The configured ceiling (BI-P012 settings profile), not an observation.',
},
// ── reports ──────────────────────────────────────────────────────────────
{
objective_id: 'report.completion_sla',
workload: 'report',
kind: 'completion_sla',
target: 900_000,
unit: 'ms',
direction: 'at_most',
measured_from: 'aud0_bi_usage_events',
min_samples: 20,
rationale: 'aud0.report_generated duration_ms: submitted to produced.',
},
{
objective_id: 'report.delivery_sla',
workload: 'report',
kind: 'delivery_sla',
target: 300_000,
unit: 'ms',
direction: 'at_most',
measured_from: 'n0tify_delivery_receipt',
min_samples: 20,
rationale:
"Produced is not delivered. aud0 has no delivery event; the receipt is N0TIFY's, so this " +
'objective is declared and reported unmeasured rather than proxied by completion.',
},
];
export const SLO_STATUSES = ['met', 'breached', 'insufficient_data', 'not_measurable_here'] as const;
export type SloStatus = (typeof SLO_STATUSES)[number];
export interface SloResult {
objective_id: string;
workload: BiWorkloadClass;
kind: SloObjectiveKind;
status: SloStatus;
target: number;
unit: SloObjective['unit'];
/** The measured value, or undefined when nothing could be measured. */
observed?: number;
sample_size: number;
/** True when a percentile was computed from fewer samples than min_samples. */
low_confidence?: boolean;
detail: string;
}
export interface SloReport {
window_from: string;
window_to: string;
results: SloResult[];
/** Queries that started and never reached a terminal event in the window. */
unresolved_queries: number;
/** True only when EVERY objective produced a real number. */
fully_measured: boolean;
}
interface QueryFact {
query_id: string;
surface?: string;
started_at: number;
outcome?: 'completed' | 'failed';
duration_ms?: number;
row_count?: number;
cache_hit?: boolean;
}
function num(v: unknown): number | undefined {
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
}
function str(v: unknown): string | undefined {
return typeof v === 'string' ? v : undefined;
}
/**
* Nearest-rank percentile. No interpolation: with small samples an
* interpolated p99 invents a value that no request ever experienced.
*/
export function percentile(sorted: number[], q: number): number | undefined {
if (sorted.length === 0) return undefined;
const rank = Math.ceil(q * sorted.length);
return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
}
/** Fold the seven event types into per-query facts. */
function foldQueries(events: Aud0BiEvent[]): Map<string, QueryFact> {
const facts = new Map<string, QueryFact>();
for (const e of events) {
const qid = str(e.payload.query_id);
if (!qid) continue;
const at = Date.parse(e.occurred_at);
const existing = facts.get(qid) ?? { query_id: qid, started_at: Number.isNaN(at) ? 0 : at };
if (e.payload.surface !== undefined) existing.surface = str(e.payload.surface);
if (e.event_type === 'aud0.analytics_query_started' && !Number.isNaN(at)) existing.started_at = at;
if (e.event_type === 'aud0.analytics_query_completed') {
existing.outcome = 'completed';
existing.duration_ms = num(e.payload.duration_ms);
existing.row_count = num(e.payload.row_count);
existing.cache_hit = e.payload.cache_hit === true;
}
if (e.event_type === 'aud0.analytics_query_failed') {
existing.outcome = 'failed';
existing.duration_ms = num(e.payload.duration_ms);
}
facts.set(qid, existing);
}
return facts;
}
function result(
o: SloObjective,
status: SloStatus,
sample_size: number,
detail: string,
observed?: number,
low_confidence?: boolean,
): SloResult {
const r: SloResult = {
objective_id: o.objective_id,
workload: o.workload,
kind: o.kind,
status,
target: o.target,
unit: o.unit,
sample_size,
detail,
};
if (observed !== undefined) r.observed = observed;
if (low_confidence) r.low_confidence = true;
return r;
}
function compare(o: SloObjective, observed: number): SloStatus {
return o.direction === 'at_least'
? observed >= o.target
? 'met'
: 'breached'
: observed <= o.target
? 'met'
: 'breached';
}
/**
* Evaluate the catalogue over a window of AUD0 BI usage events.
*
* Events are DATA: they arrive from aud0's read model, they were written by
* other services, and nothing in them is treated as an instruction or as a
* schema. Unknown event types and unknown payload keys are ignored, never
* spread.
*/
export function evaluateBiSlos(args: {
events: Aud0BiEvent[];
from: Date;
to: Date;
objectives?: readonly SloObjective[];
}): SloReport {
const objectives = args.objectives ?? BI_SLO_CATALOGUE;
const fromMs = args.from.getTime();
const toMs = args.to.getTime();
const inWindow = args.events.filter((e) => {
const t = Date.parse(e.occurred_at);
return !Number.isNaN(t) && t >= fromMs && t <= toMs;
});
const queries = [...foldQueries(inWindow).values()];
const dashboardQueries = queries.filter((q) => q.surface === 'dashboard');
const exploreQueries = queries.filter((q) => q.surface === 'explore');
const terminal = (qs: QueryFact[]) => qs.filter((q) => q.outcome !== undefined);
const durations = (qs: QueryFact[]) =>
qs
.map((q) => q.duration_ms)
.filter((d): d is number => d !== undefined)
.sort((a, b) => a - b);
const reportDurations = inWindow
.filter((e) => e.event_type === 'aud0.report_generated')
.map((e) => num(e.payload.duration_ms))
.filter((d): d is number => d !== undefined)
.sort((a, b) => a - b);
const results = objectives.map((o) => {
if (o.measured_from !== 'aud0_bi_usage_events') {
return result(
o,
'not_measurable_here',
0,
`measured from ${o.measured_from}; BI usage events cannot answer it — ${o.rationale}`,
);
}
switch (o.kind) {
case 'availability':
case 'error_rate': {
const t = terminal(dashboardQueries);
if (t.length === 0) {
return result(o, 'insufficient_data', 0, 'no terminal dashboard query events in the window');
}
const failed = t.filter((q) => q.outcome === 'failed').length;
const observed = o.kind === 'availability' ? (t.length - failed) / t.length : failed / t.length;
const status = compare(o, observed);
return result(
o,
t.length < o.min_samples ? 'insufficient_data' : status,
t.length,
t.length < o.min_samples
? `${t.length} terminal queries is below the ${o.min_samples} this objective requires; ` +
`the ratio ${observed.toFixed(4)} is reported but not judged`
: `${failed} failed of ${t.length} terminal queries`,
observed,
t.length < o.min_samples,
);
}
case 'latency_p95_first':
case 'latency_p95_cached': {
const wanted = o.kind === 'latency_p95_cached';
const sample = durations(dashboardQueries.filter((q) => (q.cache_hit === true) === wanted));
const p95 = percentile(sample, 0.95);
if (p95 === undefined) {
return result(
o,
'insufficient_data',
0,
`no ${wanted ? 'cached' : 'uncached'} dashboard durations`,
);
}
const low = sample.length < o.min_samples;
return result(
o,
low ? 'insufficient_data' : compare(o, p95),
sample.length,
low
? `p95 over ${sample.length} samples (needs ${o.min_samples}) — reported, not judged`
: `p95 over ${sample.length} samples`,
p95,
low,
);
}
case 'latency_p95':
case 'latency_p99': {
const sample = durations(exploreQueries);
const q = o.kind === 'latency_p99' ? 0.99 : 0.95;
const value = percentile(sample, q);
if (value === undefined) return result(o, 'insufficient_data', 0, 'no exploration durations');
const low = sample.length < o.min_samples;
return result(
o,
low ? 'insufficient_data' : compare(o, value),
sample.length,
low
? `p${q * 100} over ${sample.length} samples (needs ${o.min_samples}); with this few samples ` +
`it is the ${Math.ceil((1 - q) * sample.length) || 1}-slowest request, not a percentile`
: `p${q * 100} over ${sample.length} samples`,
value,
low,
);
}
case 'max_result_rows': {
const rows = exploreQueries
.map((q) => q.row_count)
.filter((r): r is number => r !== undefined)
.sort((a, b) => b - a);
if (rows.length === 0) return result(o, 'insufficient_data', 0, 'no exploration row counts');
return result(o, compare(o, rows[0]), rows.length, `largest result seen in the window`, rows[0]);
}
case 'completion_sla': {
const p95 = percentile(reportDurations, 0.95);
if (p95 === undefined) return result(o, 'insufficient_data', 0, 'no report_generated events');
const low = reportDurations.length < o.min_samples;
return result(
o,
low ? 'insufficient_data' : compare(o, p95),
reportDurations.length,
low ? `p95 over ${reportDurations.length} reports (needs ${o.min_samples})` : 'p95 report duration',
p95,
low,
);
}
default:
return result(o, 'not_measurable_here', 0, `no evaluator for kind "${o.kind}"`);
}
});
const unresolved = queries.filter((q) => q.outcome === undefined).length;
return {
window_from: args.from.toISOString(),
window_to: args.to.toISOString(),
results,
unresolved_queries: unresolved,
fully_measured: results.every((r) => r.status === 'met' || r.status === 'breached'),
};
}
/**
* One line an operator can read: how many objectives are actually being
* measured. "9 of 11 met" is meaningless if the other two were never
* measured, so the summary counts them separately and never rolls them in.
*/
export function summariseSloReport(report: SloReport): {
met: number;
breached: number;
insufficient_data: number;
not_measurable_here: number;
measured_fraction: number;
} {
const count = (s: SloStatus) => report.results.filter((r) => r.status === s).length;
const measured = count('met') + count('breached');
return {
met: count('met'),
breached: count('breached'),
insufficient_data: count('insufficient_data'),
not_measurable_here: count('not_measurable_here'),
measured_fraction: report.results.length === 0 ? 0 : measured / report.results.length,
};
}
+209
View File
@@ -0,0 +1,209 @@
/**
* src/analytics/slo/usage-events.ts
* ==================================
* BI-P103's measurement path: 0pt emits the AUD0 BI usage events (aud0#34,
* BI-P076) that the SLO evaluator then reads.
*
* aud0 says it plainly in docs/BI_ANALYTICS_USAGE.md: "Zero real producers
* emit these seven types today." An SLO computed from a second, private
* telemetry path would be a number nobody else can audit, so 0pt becomes a
* producer instead of inventing one.
*
* THE PAYLOAD IS AN ALLOWLIST, BUILT FIELD BY NAMED FIELD. AUD0's ingest
* refuses result-bearing payloads with a 400 and its schemas are `.strict()`,
* so a leak here is a 400 rather than a breach — but a producer that has to be
* refused is a producer that is broken. `biUsageEventsForExecution` therefore
* never spreads an execution record; it names each field, and
* tests/analytics/bi-slo.test.ts feeds it a record carrying SQL text, an error
* message, a tenant id and a result sample, then asserts none of it appears in
* the output.
*
* WHAT IS DELIBERATELY NOT SENT: `compiled_query_hash` goes out as
* `query_hash` (aud0 requires a 64-hex digest and nothing else), the statement
* never leaves 0pt at all, `error_code` travels but `error_message` does not,
* and `row_count` travels but no row does.
*/
/** The seven event types, named exactly as aud0 registers them. */
export const AUD0_BI_EVENT_TYPES = [
'aud0.analytics_query_started',
'aud0.analytics_query_completed',
'aud0.analytics_query_failed',
'aud0.dashboard_viewed',
'aud0.report_generated',
'aud0.dataset_exported',
'aud0.metric_queried',
] as const;
export type Aud0BiEventType = (typeof AUD0_BI_EVENT_TYPES)[number];
/** aud0's closed surface list (BI_SURFACES). */
export const BI_SURFACES = ['dashboard', 'report', 'explore', 'api', 'scheduled', 'notebook'] as const;
export type BiSurface = (typeof BI_SURFACES)[number];
export interface Aud0BiEvent {
event_type: Aud0BiEventType;
/** Bounded, allowlisted metadata. Never a result, a statement or a message. */
payload: Record<string, string | number | boolean | string[]>;
/** Occurrence time, used by the SLO window. */
occurred_at: string;
}
/**
* The 0pt-side view of one execution. Intentionally a SUPERSET of what may be
* emitted — it carries the fields 0pt genuinely has, including the ones that
* must never leave — so that the mapping below is a real filter rather than a
* rename.
*/
export interface ExecutionTelemetry {
query_id: string;
/** 64-hex; anything else is dropped rather than reshaped. */
compiled_query_hash?: string;
/** NEVER emitted. Present here because 0pt has it. */
compiled_sql?: string;
/** NEVER emitted. */
error_message?: string;
error_code?: string;
/** NEVER emitted — a consumer must not be able to name a tenant, BI-P016. */
tenant_id?: string;
dataset_id?: string;
engine?: string;
surface?: BiSurface;
state: 'succeeded' | 'failed' | 'cancelled' | 'running';
started_at: string;
finished_at?: string;
duration_ms?: number;
row_count?: number;
bytes_scanned?: number;
cache_hit?: boolean;
filter_keys?: string[];
/** NEVER emitted: filter VALUES are facts about a subject. */
filter_values?: Record<string, string[]>;
/** NEVER emitted. */
result_sample?: unknown;
metric_name?: string;
dimension_keys?: string[];
granularity?: string;
dashboard_id?: string;
widget_count?: number;
}
const SHA256 = /^[0-9a-f]{64}$/;
const MAX_KEY_NAMES = 50;
const MAX_SHORT_TEXT = 120;
function shortText(v: string | undefined): string | undefined {
if (!v) return undefined;
return v.slice(0, MAX_SHORT_TEXT);
}
function keyNames(v: string[] | undefined): string[] | undefined {
if (!v || v.length === 0) return undefined;
return v.slice(0, MAX_KEY_NAMES).map((k) => k.slice(0, MAX_SHORT_TEXT));
}
function put(
payload: Record<string, string | number | boolean | string[]>,
key: string,
value: string | number | boolean | string[] | undefined,
): void {
if (value === undefined) return;
payload[key] = value;
}
/**
* Map one execution to its AUD0 usage events.
*
* A running execution emits only `started`: a query that has not finished is
* neither a success nor a failure, and the SLO evaluator counts it as
* unresolved rather than quietly as one or the other.
*/
export function biUsageEventsForExecution(t: ExecutionTelemetry): Aud0BiEvent[] {
const base: Record<string, string | number | boolean | string[]> = {};
put(base, 'query_id', t.query_id);
if (t.compiled_query_hash && SHA256.test(t.compiled_query_hash)) {
put(base, 'query_hash', t.compiled_query_hash);
}
put(base, 'dataset_id', t.dataset_id);
put(base, 'engine', shortText(t.engine));
put(base, 'surface', t.surface);
put(base, 'filter_keys', keyNames(t.filter_keys));
const events: Aud0BiEvent[] = [
{ event_type: 'aud0.analytics_query_started', payload: { ...base }, occurred_at: t.started_at },
];
if (t.state === 'succeeded') {
const payload = { ...base };
put(payload, 'duration_ms', t.duration_ms);
put(payload, 'row_count', t.row_count);
put(payload, 'bytes_scanned', t.bytes_scanned);
put(payload, 'cache_hit', t.cache_hit);
events.push({
event_type: 'aud0.analytics_query_completed',
payload,
occurred_at: t.finished_at ?? t.started_at,
});
} else if (t.state === 'failed' || t.state === 'cancelled') {
const payload = { ...base };
put(payload, 'duration_ms', t.duration_ms);
// A CODE. `error_message` is never mapped, at any length.
put(
payload,
'error_code',
shortText(t.error_code ?? (t.state === 'cancelled' ? 'CANCELLED' : undefined)),
);
events.push({
event_type: 'aud0.analytics_query_failed',
payload,
occurred_at: t.finished_at ?? t.started_at,
});
}
if (t.metric_name) {
const payload: Record<string, string | number | boolean | string[]> = {};
put(payload, 'metric_name', shortText(t.metric_name));
put(payload, 'dataset_id', t.dataset_id);
put(payload, 'surface', t.surface);
put(payload, 'engine', shortText(t.engine));
put(payload, 'dimension_keys', keyNames(t.dimension_keys));
put(payload, 'granularity', shortText(t.granularity));
put(payload, 'row_count', t.row_count);
put(payload, 'duration_ms', t.duration_ms);
events.push({
event_type: 'aud0.metric_queried',
payload,
occurred_at: t.finished_at ?? t.started_at,
});
}
if (t.dashboard_id) {
const payload: Record<string, string | number | boolean | string[]> = {};
put(payload, 'dashboard_id', t.dashboard_id);
put(payload, 'widget_count', t.widget_count);
put(payload, 'duration_ms', t.duration_ms);
put(payload, 'surface', t.surface ?? 'dashboard');
events.push({ event_type: 'aud0.dashboard_viewed', payload, occurred_at: t.started_at });
}
return events;
}
/** Every payload key this producer can ever emit. Pinned by test. */
export const EMITTABLE_PAYLOAD_KEYS = [
'query_id',
'query_hash',
'dataset_id',
'engine',
'surface',
'filter_keys',
'duration_ms',
'row_count',
'bytes_scanned',
'cache_hit',
'error_code',
'metric_name',
'dimension_keys',
'granularity',
'dashboard_id',
'widget_count',
] as const;
+293
View File
@@ -0,0 +1,293 @@
/**
* tests/analytics/bi-slo.test.ts
* ===============================
* BI-P103 — BI SLO, and the AUD0 usage events it is measured from.
*
* Two failure modes are attacked directly, because both look like a healthy
* dashboard:
*
* 1. AN SLO REPORT OVER SILENCE. Zero events must not read as 100%
* availability. Every objective in an empty window must come back
* `insufficient_data`, and `fully_measured` must be false.
* 2. A PRODUCER THAT LEAKS. The emitter is handed an execution record
* carrying SQL text, a database error message quoting a personnummer,
* filter VALUES, a tenant id and a result sample — everything AUD0's
* BI-P076 ingest exists to refuse — and none of it may appear in any
* emitted payload. The test greps the serialized events, and first
* proves the grep finds those strings in the INPUT so a clean result
* cannot be a broken detector.
*/
import { describe, expect, it } from 'vitest';
import {
BI_SLO_CATALOGUE,
evaluateBiSlos,
percentile,
summariseSloReport,
type SloResult,
} from '@/src/analytics/slo/bi-slo';
import {
biUsageEventsForExecution,
EMITTABLE_PAYLOAD_KEYS,
type Aud0BiEvent,
type ExecutionTelemetry,
} from '@/src/analytics/slo/usage-events';
const FROM = new Date('2026-09-07T00:00:00.000Z');
const TO = new Date('2026-09-07T23:59:59.000Z');
function queryEvents(args: {
n: number;
surface: 'dashboard' | 'explore';
outcome: 'completed' | 'failed' | 'running';
durationMs: (i: number) => number;
cacheHit?: boolean;
idPrefix?: string;
}): Aud0BiEvent[] {
const out: Aud0BiEvent[] = [];
for (let i = 0; i < args.n; i += 1) {
const t: ExecutionTelemetry = {
query_id: `${args.idPrefix ?? args.surface}_${i}`,
surface: args.surface,
state: args.outcome === 'completed' ? 'succeeded' : args.outcome === 'failed' ? 'failed' : 'running',
started_at: new Date(FROM.getTime() + i * 1000).toISOString(),
finished_at: new Date(FROM.getTime() + i * 1000 + args.durationMs(i)).toISOString(),
duration_ms: args.durationMs(i),
...(args.cacheHit !== undefined ? { cache_hit: args.cacheHit } : {}),
...(args.outcome === 'failed' ? { error_code: 'TIMEOUT' } : {}),
};
out.push(...biUsageEventsForExecution(t));
}
return out;
}
function byId(results: SloResult[], id: string): SloResult {
const r = results.find((x) => x.objective_id === id);
if (!r) throw new Error(`no result for ${id}`);
return r;
}
describe('BI-P103 — an SLO over silence is not a met SLO', () => {
it('an empty window makes every measurable objective insufficient_data, never 100%', () => {
const report = evaluateBiSlos({ events: [], from: FROM, to: TO });
expect(report.fully_measured).toBe(false);
for (const r of report.results) {
expect(['insufficient_data', 'not_measurable_here']).toContain(r.status);
expect(r.status).not.toBe('met');
}
const summary = summariseSloReport(report);
expect(summary.met).toBe(0);
expect(summary.measured_fraction).toBe(0);
});
it('a started query that never finished is unresolved, not a success', () => {
const events = queryEvents({ n: 200, surface: 'dashboard', outcome: 'running', durationMs: () => 100 });
const report = evaluateBiSlos({ events, from: FROM, to: TO });
expect(report.unresolved_queries).toBe(200);
expect(byId(report.results, 'dash.availability').status).toBe('insufficient_data');
expect(byId(report.results, 'dash.availability').observed).toBeUndefined();
});
it('a percentile below min_samples is reported WITH its sample size and not judged', () => {
const events = queryEvents({
n: 12,
surface: 'explore',
outcome: 'completed',
durationMs: (i) => 1000 + i * 10,
});
const p99 = byId(evaluateBiSlos({ events, from: FROM, to: TO }).results, 'explore.p99');
expect(p99.status).toBe('insufficient_data');
expect(p99.low_confidence).toBe(true);
expect(p99.sample_size).toBe(12);
expect(p99.observed).toBeDefined(); // reported…
expect(p99.detail).toMatch(/not a percentile/); // …with the caveat attached
});
it('freshness and delivery are declared not_measurable_here rather than proxied', () => {
const report = evaluateBiSlos({ events: [], from: FROM, to: TO });
const freshness = byId(report.results, 'dash.freshness');
const delivery = byId(report.results, 'report.delivery_sla');
expect(freshness.status).toBe('not_measurable_here');
expect(freshness.detail).toMatch(/analytics_freshness_watermark/);
expect(delivery.status).toBe('not_measurable_here');
expect(delivery.detail).toMatch(/n0tify_delivery_receipt/);
});
});
describe('BI-P103 — real measurement from real events', () => {
it('computes availability, error rate and both dashboard latencies', () => {
const events = [
...queryEvents({
n: 180,
surface: 'dashboard',
outcome: 'completed',
cacheHit: false,
durationMs: (i) => 500 + i * 10,
idPrefix: 'cold',
}),
...queryEvents({
n: 120,
surface: 'dashboard',
outcome: 'completed',
cacheHit: true,
durationMs: () => 120,
idPrefix: 'warm',
}),
...queryEvents({
n: 3,
surface: 'dashboard',
outcome: 'failed',
durationMs: () => 900,
idPrefix: 'bad',
}),
];
const { results } = evaluateBiSlos({ events, from: FROM, to: TO });
const availability = byId(results, 'dash.availability');
expect(availability.sample_size).toBe(303);
expect(availability.observed).toBeCloseTo(300 / 303, 6);
expect(availability.status).toBe('breached'); // 0.9901 < 0.995 target
const errorRate = byId(results, 'dash.error_rate');
expect(errorRate.observed).toBeCloseTo(3 / 303, 6);
const cold = byId(results, 'dash.p95_first_query');
expect(cold.sample_size).toBe(183); // 180 cold + 3 failures, which had no cache hit
expect(cold.status).toBe('met');
const cached = byId(results, 'dash.p95_cached');
expect(cached.sample_size).toBe(120);
expect(cached.observed).toBe(120);
expect(cached.status).toBe('met');
});
it('a breach is reported as a breach', () => {
const events = queryEvents({
n: 60,
surface: 'dashboard',
outcome: 'completed',
cacheHit: false,
durationMs: () => 9000,
});
expect(
byId(evaluateBiSlos({ events, from: FROM, to: TO }).results, 'dash.p95_first_query'),
).toMatchObject({ status: 'breached', observed: 9000 });
});
it('events outside the window do not count', () => {
const events = queryEvents({ n: 200, surface: 'dashboard', outcome: 'completed', durationMs: () => 100 });
const report = evaluateBiSlos({
events,
from: new Date('2026-09-08T00:00:00.000Z'),
to: new Date('2026-09-08T23:59:59.000Z'),
});
expect(byId(report.results, 'dash.availability').status).toBe('insufficient_data');
});
it('percentile is nearest-rank — it never invents a value nobody experienced', () => {
const sample = [10, 20, 30, 40];
expect(percentile(sample, 0.95)).toBe(40);
expect(percentile(sample, 0.5)).toBe(20);
expect(percentile([], 0.95)).toBeUndefined();
});
it('the catalogue covers all three workload classes and every field BI-P103 names', () => {
const kinds = new Set(BI_SLO_CATALOGUE.map((o) => `${o.workload}:${o.kind}`));
for (const expected of [
'interactive_dashboard:availability',
'interactive_dashboard:latency_p95_first',
'interactive_dashboard:latency_p95_cached',
'interactive_dashboard:error_rate',
'interactive_dashboard:freshness',
'exploration:latency_p95',
'exploration:latency_p99',
'exploration:max_result_rows',
'exploration:timeout',
'report:completion_sla',
'report:delivery_sla',
]) {
expect(kinds.has(expected), expected).toBe(true);
}
});
});
describe('BI-P103 — the producer cannot leak a result into AUD0', () => {
const hostile: ExecutionTelemetry = {
query_id: 'q_1',
compiled_query_hash: 'f'.repeat(64),
compiled_sql: "SELECT * FROM customers WHERE personnummer = '19800101-1234'",
error_message: 'duplicate key value violates unique constraint (personnummer)=(19800101-1234)',
error_code: 'CONFLICT',
tenant_id: 't_alpha',
dataset_id: 'ds_sales',
engine: 'clickhouse',
surface: 'dashboard',
state: 'failed',
started_at: '2026-09-07T10:00:00.000Z',
finished_at: '2026-09-07T10:00:02.000Z',
duration_ms: 2000,
filter_keys: ['region'],
filter_values: { region: ['Gotland'] },
result_sample: [{ customer: 'Anna Andersson', revenue: 42 }],
metric_name: 'revenue',
dashboard_id: 'dash_sales',
widget_count: 4,
};
it('emits none of the forbidden material', () => {
const events = biUsageEventsForExecution(hostile);
const serialized = JSON.stringify(events);
// The detector must be able to find these strings — they are in the input.
const input = JSON.stringify(hostile);
for (const secret of ['19800101-1234', 'Anna Andersson', 'Gotland', 't_alpha', 'SELECT *']) {
expect(input, `detector check for ${secret}`).toContain(secret);
}
for (const secret of [
'19800101-1234',
'Anna Andersson',
'Gotland',
't_alpha',
'SELECT *',
'duplicate key',
]) {
expect(serialized, secret).not.toContain(secret);
}
});
it('every emitted key is on the allowlist AUD0 accepts', () => {
const events = biUsageEventsForExecution(hostile);
for (const e of events) {
for (const key of Object.keys(e.payload)) {
expect(EMITTABLE_PAYLOAD_KEYS, `${e.event_type}.${key}`).toContain(key);
}
}
});
it('a query_hash that is not a sha256 digest is dropped, not reshaped', () => {
const events = biUsageEventsForExecution({ ...hostile, compiled_query_hash: 'SELECT 1' });
for (const e of events) expect(e.payload.query_hash).toBeUndefined();
});
it('a running query emits only "started" — it is neither a success nor a failure', () => {
const events = biUsageEventsForExecution({
...hostile,
state: 'running',
metric_name: undefined,
dashboard_id: undefined,
});
expect(events.map((e) => e.event_type)).toEqual(['aud0.analytics_query_started']);
});
it('a cancelled query is reported as failed with a CODE', () => {
const events = biUsageEventsForExecution({
...hostile,
state: 'cancelled',
error_code: undefined,
metric_name: undefined,
dashboard_id: undefined,
});
const failed = events.find((e) => e.event_type === 'aud0.analytics_query_failed');
expect(failed?.payload.error_code).toBe('CANCELLED');
expect(failed?.payload.error_message).toBeUndefined();
});
});