chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
export const DIAGNOSTIC_LEVELS = Object.freeze([
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error",
|
||||
] as const);
|
||||
export { queueSizeBucket } from "./diagnostic-buckets.ts";
|
||||
export type DiagnosticLevel = (typeof DIAGNOSTIC_LEVELS)[number];
|
||||
|
||||
export const DIAGNOSTIC_EVENT_REGISTRY = Object.freeze({
|
||||
"app.boot.failed": Object.freeze({ level: "error" }),
|
||||
"http.request.completed": Object.freeze({ level: "info" }),
|
||||
"cache.operation.failed": Object.freeze({ level: "warn" }),
|
||||
"storage.operation.failed": Object.freeze({ level: "warn" }),
|
||||
"route.changed": Object.freeze({ level: "info" }),
|
||||
"ui.render.failed": Object.freeze({ level: "error" }),
|
||||
"release.mismatch.detected": Object.freeze({ level: "warn" }),
|
||||
"telemetry.delivery.dropped": Object.freeze({ level: "warn" }),
|
||||
});
|
||||
export type DiagnosticEventId = keyof typeof DIAGNOSTIC_EVENT_REGISTRY;
|
||||
|
||||
export const DIAGNOSTIC_CONTEXT_ALLOWLIST = Object.freeze([
|
||||
"app_version",
|
||||
"build_id",
|
||||
"release_id",
|
||||
"active_release_id",
|
||||
"config_schema_version",
|
||||
"api_contract_version",
|
||||
"route_id",
|
||||
"operation_id",
|
||||
"correlation_id",
|
||||
"error_kind",
|
||||
"outcome",
|
||||
"http_status_group",
|
||||
"attempt_count_bucket",
|
||||
"duration_bucket",
|
||||
"component_boundary",
|
||||
"mismatch_kind",
|
||||
"operation",
|
||||
"reason",
|
||||
"queue_size_bucket",
|
||||
] as const);
|
||||
export type DiagnosticContextKey =
|
||||
(typeof DIAGNOSTIC_CONTEXT_ALLOWLIST)[number];
|
||||
export type DiagnosticContext = Readonly<
|
||||
Partial<Record<DiagnosticContextKey, string | number | boolean>>
|
||||
>;
|
||||
|
||||
export type DiagnosticRecordInput = Readonly<{
|
||||
level: DiagnosticLevel;
|
||||
eventId: DiagnosticEventId;
|
||||
context?: Readonly<Record<string, unknown>>;
|
||||
}>;
|
||||
|
||||
export type DiagnosticRecord = Readonly<{
|
||||
level: DiagnosticLevel;
|
||||
eventId: DiagnosticEventId;
|
||||
timestamp: string;
|
||||
context: DiagnosticContext;
|
||||
}>;
|
||||
|
||||
const SAFE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,63}$/;
|
||||
|
||||
function projectDiagnosticRecordUnsafe(
|
||||
input: DiagnosticRecordInput,
|
||||
now: () => number = Date.now,
|
||||
):
|
||||
| Readonly<{ success: true; record: DiagnosticRecord }>
|
||||
| Readonly<{ success: false; reason: string }> {
|
||||
if (!Object.hasOwn(DIAGNOSTIC_EVENT_REGISTRY, input.eventId)) {
|
||||
return { success: false, reason: "unregistered-event" };
|
||||
}
|
||||
if (!DIAGNOSTIC_LEVELS.includes(input.level)) {
|
||||
return { success: false, reason: "invalid-level" };
|
||||
}
|
||||
const contextEntries = Object.entries(input.context ?? {});
|
||||
if (contextEntries.length > DIAGNOSTIC_CONTEXT_ALLOWLIST.length) {
|
||||
return { success: false, reason: "invalid-context" };
|
||||
}
|
||||
const projected: Partial<
|
||||
Record<DiagnosticContextKey, string | number | boolean>
|
||||
> = {};
|
||||
for (const [key, value] of contextEntries) {
|
||||
if (
|
||||
!DIAGNOSTIC_CONTEXT_ALLOWLIST.includes(key as DiagnosticContextKey)
|
||||
) {
|
||||
return { success: false, reason: "unknown-context" };
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (!SAFE_VALUE.test(value)) {
|
||||
return { success: false, reason: "invalid-context" };
|
||||
}
|
||||
projected[key as DiagnosticContextKey] = value;
|
||||
} else if (typeof value === "number" && Number.isFinite(value)) {
|
||||
projected[key as DiagnosticContextKey] = value;
|
||||
} else if (typeof value === "boolean") {
|
||||
projected[key as DiagnosticContextKey] = value;
|
||||
} else {
|
||||
return { success: false, reason: "invalid-context" };
|
||||
}
|
||||
}
|
||||
let timestamp: string;
|
||||
try {
|
||||
timestamp = new Date(now()).toISOString();
|
||||
} catch {
|
||||
timestamp = new Date(0).toISOString();
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
record: Object.freeze({
|
||||
level: input.level,
|
||||
eventId: input.eventId,
|
||||
timestamp,
|
||||
context: Object.freeze(projected),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function projectDiagnosticRecord(
|
||||
input: DiagnosticRecordInput,
|
||||
now: () => number = Date.now,
|
||||
): ReturnType<typeof projectDiagnosticRecordUnsafe> {
|
||||
try {
|
||||
return projectDiagnosticRecordUnsafe(input, now);
|
||||
} catch {
|
||||
return { success: false, reason: "serialization-failure" };
|
||||
}
|
||||
}
|
||||
|
||||
export function safeErrorKind(error: unknown): string {
|
||||
try {
|
||||
if (error && typeof error === "object") {
|
||||
const record = error as Readonly<Record<string, unknown>>;
|
||||
if (
|
||||
typeof record.kind === "string" &&
|
||||
/^[A-Z][A-Z0-9_]{0,63}$/.test(record.kind)
|
||||
) {
|
||||
return record.kind;
|
||||
}
|
||||
if (
|
||||
typeof record.name === "string" &&
|
||||
/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(record.name)
|
||||
) {
|
||||
const normalized = record.name
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
||||
.toUpperCase();
|
||||
return /^[A-Z][A-Z0-9_]{0,63}$/.test(normalized)
|
||||
? normalized
|
||||
: "UNKNOWN_FAILURE";
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return "UNKNOWN_FAILURE";
|
||||
}
|
||||
return "UNKNOWN_FAILURE";
|
||||
}
|
||||
|
||||
export function statusGroup(status: number | undefined): string {
|
||||
return typeof status === "number" && Number.isFinite(status)
|
||||
? `${Math.max(0, Math.min(9, Math.floor(status / 100)))}xx`
|
||||
: "none";
|
||||
}
|
||||
|
||||
export function attemptBucket(attemptCount: number): string {
|
||||
if (attemptCount <= 1) return "1";
|
||||
if (attemptCount === 2) return "2";
|
||||
if (attemptCount <= 4) return "3-4";
|
||||
return "5+";
|
||||
}
|
||||
|
||||
export function durationBucket(durationMs: number): string {
|
||||
if (!Number.isFinite(durationMs) || durationMs < 0) return "unknown";
|
||||
if (durationMs < 100) return "lt100ms";
|
||||
if (durationMs < 500) return "100-499ms";
|
||||
if (durationMs < 2_000) return "500-1999ms";
|
||||
return "gte2000ms";
|
||||
}
|
||||
Reference in New Issue
Block a user