feat: add diagnostics and telemetry runtime
This commit is contained in:
@@ -3,6 +3,7 @@ import type {
|
||||
ApplicationApi,
|
||||
ColorSchemePreference,
|
||||
RenderFailureReport,
|
||||
RouteChangedReport,
|
||||
} from "./ports/in/application-api.js";
|
||||
import type { ApplicationOutputPorts } from "./ports/out/application-output-ports.js";
|
||||
import { decideChunkRecovery } from "./use-cases/decide-chunk-recovery.js";
|
||||
@@ -43,7 +44,20 @@ export function createApplication(
|
||||
const diagnostics = Object.freeze({
|
||||
reportRenderFailure(report: RenderFailureReport) {
|
||||
try {
|
||||
outputPorts.diagnostics.emit("ui.render.failed", {
|
||||
outputPorts.diagnostics.record({
|
||||
level: "error",
|
||||
eventId: "ui.render.failed",
|
||||
context: {
|
||||
route_id: report.routeId,
|
||||
build_id: report.buildId,
|
||||
component_boundary: report.boundaryName,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Diagnostics are best-effort and cannot become an application failure.
|
||||
}
|
||||
try {
|
||||
outputPorts.telemetry.emit("ui.render.failed", {
|
||||
route_id: report.routeId,
|
||||
build_id: report.buildId,
|
||||
component_boundary: report.boundaryName,
|
||||
@@ -52,6 +66,20 @@ export function createApplication(
|
||||
// Diagnostics are best-effort and cannot become an application failure.
|
||||
}
|
||||
},
|
||||
reportRouteChanged(report: RouteChangedReport) {
|
||||
try {
|
||||
outputPorts.diagnostics.record({
|
||||
level: "info",
|
||||
eventId: "route.changed",
|
||||
context: {
|
||||
route_id: report.routeId,
|
||||
build_id: report.buildId,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Diagnostics are best-effort and cannot become navigation failure.
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const runtime = Object.freeze({
|
||||
@@ -74,6 +102,37 @@ export function createApplication(
|
||||
try {
|
||||
const current = await outputPorts.releaseInfo.getCurrent();
|
||||
const active = await outputPorts.releaseInfo.refresh();
|
||||
if (
|
||||
current.buildId !== active.buildId ||
|
||||
current.releaseId !== active.releaseId
|
||||
) {
|
||||
const mismatchKind =
|
||||
current.buildId !== active.buildId
|
||||
? "BUILD_MISMATCH"
|
||||
: "RELEASE_MISMATCH";
|
||||
try {
|
||||
outputPorts.diagnostics.record({
|
||||
level: "warn",
|
||||
eventId: "release.mismatch.detected",
|
||||
context: {
|
||||
build_id: current.buildId,
|
||||
active_release_id: active.releaseId,
|
||||
mismatch_kind: mismatchKind,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// Recovery remains independent from diagnostics.
|
||||
}
|
||||
try {
|
||||
outputPorts.telemetry.emit("release.mismatch.detected", {
|
||||
build_id: current.buildId,
|
||||
active_release_id: active.releaseId,
|
||||
mismatch_kind: mismatchKind,
|
||||
});
|
||||
} catch {
|
||||
// Recovery remains independent from telemetry.
|
||||
}
|
||||
}
|
||||
if (!active.routeChunks[input.chunkId]) {
|
||||
return {
|
||||
action: "support" as const,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { DiagnosticRecordInput } from "../../contracts/diagnostics.js";
|
||||
|
||||
export type DiagnosticsPort = Readonly<{
|
||||
record(input: DiagnosticRecordInput): void;
|
||||
}>;
|
||||
@@ -11,6 +11,11 @@ export type RenderFailureReport = Readonly<{
|
||||
boundaryName: "route" | "feature";
|
||||
}>;
|
||||
|
||||
export type RouteChangedReport = Readonly<{
|
||||
routeId: string;
|
||||
buildId: string;
|
||||
}>;
|
||||
|
||||
export type ReleaseSummary = Readonly<{
|
||||
buildId: string;
|
||||
releaseId: string;
|
||||
@@ -34,6 +39,7 @@ export type ApplicationApi = Readonly<{
|
||||
}>;
|
||||
diagnostics: Readonly<{
|
||||
reportRenderFailure(report: RenderFailureReport): void;
|
||||
reportRouteChanged(report: RouteChangedReport): void;
|
||||
}>;
|
||||
runtime: Readonly<{
|
||||
getReleaseSummary(): Promise<ReleaseSummary>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AuthSessionPort } from "../auth-session-port.js";
|
||||
import type { ReleaseInfoPort } from "../release-info-port.js";
|
||||
import type { StoragePort } from "../storage-port.js";
|
||||
import type { TelemetryPort } from "../telemetry-port.js";
|
||||
import type { DiagnosticsPort } from "../diagnostics-port.js";
|
||||
|
||||
/**
|
||||
* Capabilities required by application use cases. Implementations live in
|
||||
@@ -13,7 +14,8 @@ export type ApplicationOutputPorts = Readonly<{
|
||||
"getState" | "subscribe" | "beginSignIn" | "signOut" | "recover"
|
||||
>;
|
||||
preferences: StoragePort;
|
||||
diagnostics: TelemetryPort;
|
||||
diagnostics: DiagnosticsPort;
|
||||
telemetry: TelemetryPort;
|
||||
releaseInfo: ReleaseInfoPort;
|
||||
navigation: Readonly<{ reload(): void }>;
|
||||
}>;
|
||||
|
||||
@@ -9,3 +9,4 @@ export type { QueryCachePort } from "../query-cache-port.js";
|
||||
export type { ReleaseInfoPort } from "../release-info-port.js";
|
||||
export type { StoragePort } from "../storage-port.js";
|
||||
export type { TelemetryPort } from "../telemetry-port.js";
|
||||
export type { DiagnosticsPort } from "../diagnostics-port.js";
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* @typedef {{ emit(eventName: string, attributes: Record<string, unknown>): void }} TelemetryPort
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { TelemetryEventName } from "../../contracts/telemetry.js";
|
||||
|
||||
export type { TelemetryEventName };
|
||||
|
||||
export type TelemetryPort = Readonly<{
|
||||
emit(
|
||||
eventName: TelemetryEventName,
|
||||
attributes: Record<string, unknown>,
|
||||
): void;
|
||||
}>;
|
||||
Reference in New Issue
Block a user