feat: add diagnostics and telemetry runtime

This commit is contained in:
donghyeon-ka
2026-07-26 16:42:27 +09:00
parent 2fa0baa577
commit 5173b6c8d6
43 changed files with 1760 additions and 116 deletions
+60 -1
View File
@@ -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 }>;
}>;
+1
View File
@@ -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";
-5
View File
@@ -1,5 +0,0 @@
/**
* @typedef {{ emit(eventName: string, attributes: Record<string, unknown>): void }} TelemetryPort
*/
export {};
+10
View File
@@ -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;
}>;