Skip to content
28 changes: 28 additions & 0 deletions src/observability/instruments/build-instruments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export interface BuildInstruments {
buildDuration: Histogram | null;
bundleSizeHistogram: Histogram | null;
bundleCounter: Counter | null;
dependencyArtifactBuildCounter: Counter | null;
dependencyArtifactBuildDuration: Histogram | null;
dependencyArtifactBuildBytes: Histogram | null;
dependencyArtifactBuildAssetCount: Histogram | null;
dependencyArtifactBuildExternalImportCount: Histogram | null;
}

export function createBuildInstruments(
Expand All @@ -32,5 +37,28 @@ export function createBuildInstruments(
description: "Total number of bundles created",
unit: "bundles",
}),
dependencyArtifactBuildCounter: meter.createCounter(
`${prefix}.dependency_artifact.builds`,
{ description: "Dependency artifact build lifecycle events", unit: "events" },
),
dependencyArtifactBuildDuration: meter.createHistogram(
`${prefix}.dependency_artifact.build.duration`,
{ description: "Dependency artifact build duration", unit: "ms" },
),
dependencyArtifactBuildBytes: meter.createHistogram(
`${prefix}.dependency_artifact.build.bytes`,
{ description: "Dependency artifact build output bytes", unit: "bytes" },
),
dependencyArtifactBuildAssetCount: meter.createHistogram(
`${prefix}.dependency_artifact.build.assets`,
{ description: "Dependency artifact build asset count", unit: "assets" },
),
dependencyArtifactBuildExternalImportCount: meter.createHistogram(
`${prefix}.dependency_artifact.build.external_imports`,
{
description: "Allowed external imports remaining after artifact materialization",
unit: "imports",
},
),
};
}
5 changes: 5 additions & 0 deletions src/observability/instruments/instruments-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export function createEmptyInstruments(): MetricsInstruments {
buildDuration: null,
bundleSizeHistogram: null,
bundleCounter: null,
dependencyArtifactBuildCounter: null,
dependencyArtifactBuildDuration: null,
dependencyArtifactBuildBytes: null,
dependencyArtifactBuildAssetCount: null,
dependencyArtifactBuildExternalImportCount: null,
dataFetchDuration: null,
dataFetchCounter: null,
dataFetchErrorCounter: null,
Expand Down
12 changes: 12 additions & 0 deletions src/observability/metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ export function recordBundle(
getRecorder()?.recordBundle(sizeKb, attributes);
}

/** Record one dependency artifact build lifecycle event. */
export function recordDependencyArtifactBuild(input: {
event: "claim" | "success" | "failure";
durationMs?: number;
totalBytes?: number;
assetCount?: number;
remainingExternalImportCount?: number;
failureCode?: string;
}): void {
getRecorder()?.recordDependencyArtifactBuild(input);
}

/** Record data fetch. */
export function recordDataFetch(
durationMs: number,
Expand Down
124 changes: 124 additions & 0 deletions src/observability/metrics/recorder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ function createMockInstruments(): MetricsInstruments & {
_buildDuration: MockHistogram;
_bundleSizeHistogram: MockHistogram;
_bundleCounter: MockCounter;
_dependencyArtifactBuildCounter: MockCounter;
_dependencyArtifactBuildDuration: MockHistogram;
_dependencyArtifactBuildBytes: MockHistogram;
_dependencyArtifactBuildAssetCount: MockHistogram;
_dependencyArtifactBuildExternalImportCount: MockHistogram;
_dataFetchDuration: MockHistogram;
_dataFetchCounter: MockCounter;
_dataFetchErrorCounter: MockCounter;
Expand Down Expand Up @@ -87,6 +92,11 @@ function createMockInstruments(): MetricsInstruments & {
const buildDuration = createMockHistogram();
const bundleSizeHistogram = createMockHistogram();
const bundleCounter = createMockCounter();
const dependencyArtifactBuildCounter = createMockCounter();
const dependencyArtifactBuildDuration = createMockHistogram();
const dependencyArtifactBuildBytes = createMockHistogram();
const dependencyArtifactBuildAssetCount = createMockHistogram();
const dependencyArtifactBuildExternalImportCount = createMockHistogram();
const dataFetchDuration = createMockHistogram();
const dataFetchCounter = createMockCounter();
const dataFetchErrorCounter = createMockCounter();
Expand Down Expand Up @@ -116,6 +126,11 @@ function createMockInstruments(): MetricsInstruments & {
buildDuration: buildDuration as never,
bundleSizeHistogram: bundleSizeHistogram as never,
bundleCounter: bundleCounter as never,
dependencyArtifactBuildCounter: dependencyArtifactBuildCounter as never,
dependencyArtifactBuildDuration: dependencyArtifactBuildDuration as never,
dependencyArtifactBuildBytes: dependencyArtifactBuildBytes as never,
dependencyArtifactBuildAssetCount: dependencyArtifactBuildAssetCount as never,
dependencyArtifactBuildExternalImportCount: dependencyArtifactBuildExternalImportCount as never,
dataFetchDuration: dataFetchDuration as never,
dataFetchCounter: dataFetchCounter as never,
dataFetchErrorCounter: dataFetchErrorCounter as never,
Expand Down Expand Up @@ -164,6 +179,11 @@ function createMockInstruments(): MetricsInstruments & {
_buildDuration: buildDuration,
_bundleSizeHistogram: bundleSizeHistogram,
_bundleCounter: bundleCounter,
_dependencyArtifactBuildCounter: dependencyArtifactBuildCounter,
_dependencyArtifactBuildDuration: dependencyArtifactBuildDuration,
_dependencyArtifactBuildBytes: dependencyArtifactBuildBytes,
_dependencyArtifactBuildAssetCount: dependencyArtifactBuildAssetCount,
_dependencyArtifactBuildExternalImportCount: dependencyArtifactBuildExternalImportCount,
_dataFetchDuration: dataFetchDuration,
_dataFetchCounter: dataFetchCounter,
_dataFetchErrorCounter: dataFetchErrorCounter,
Expand Down Expand Up @@ -473,6 +493,103 @@ describe("observability/metrics/recorder", () => {
});
});

describe("recordDependencyArtifactBuild", () => {
it("should record lifecycle, output, and remaining external metrics", () => {
recorder.recordDependencyArtifactBuild({
event: "success",
durationMs: 120,
totalBytes: 2048,
assetCount: 3,
remainingExternalImportCount: 1,
});

assertEquals(instruments._dependencyArtifactBuildCounter._value, 1);
assertEquals(
instruments._dependencyArtifactBuildCounter._lastAttributes,
{ event: "success" },
);
assertEquals(instruments._dependencyArtifactBuildDuration._value, 120);
assertEquals(instruments._dependencyArtifactBuildBytes._value, 2048);
assertEquals(instruments._dependencyArtifactBuildAssetCount._value, 3);
assertEquals(
instruments._dependencyArtifactBuildExternalImportCount._value,
1,
);
});

it("should label failed builds without recording unavailable output", () => {
recorder.recordDependencyArtifactBuild({
event: "failure",
durationMs: 40,
failureCode: "dependency_artifact_graph_incomplete",
});

assertEquals(instruments._dependencyArtifactBuildCounter._value, 1);
assertEquals(
instruments._dependencyArtifactBuildCounter._lastAttributes,
{
event: "failure",
failure_code: "dependency_artifact_graph_incomplete",
},
);
assertEquals(instruments._dependencyArtifactBuildDuration._value, 40);
assertEquals(instruments._dependencyArtifactBuildBytes._value, 0);
assertEquals(instruments._dependencyArtifactBuildAssetCount._value, 0);
});

it("normalizes dependency artifact measurements", () => {
recorder.recordDependencyArtifactBuild({
event: "success",
durationMs: Number.POSITIVE_INFINITY,
totalBytes: Number.MAX_SAFE_INTEGER + 100,
assetCount: 3.9,
remainingExternalImportCount: -1,
});

assertEquals(instruments._dependencyArtifactBuildDuration._value, 0);
assertEquals(
instruments._dependencyArtifactBuildBytes._value,
Number.MAX_SAFE_INTEGER,
);
assertEquals(instruments._dependencyArtifactBuildAssetCount._value, 3);
assertEquals(
instruments._dependencyArtifactBuildExternalImportCount._value,
0,
);
});

it("isolates dependency artifact builds from telemetry backend failures", () => {
let attemptedWrites = 0;
instruments._dependencyArtifactBuildCounter.add = () => {
attemptedWrites += 1;
throw new Error("counter unavailable");
};
for (
const histogram of [
instruments._dependencyArtifactBuildDuration,
instruments._dependencyArtifactBuildBytes,
instruments._dependencyArtifactBuildAssetCount,
instruments._dependencyArtifactBuildExternalImportCount,
]
) {
histogram.record = () => {
attemptedWrites += 1;
throw new Error("histogram unavailable");
};
}

recorder.recordDependencyArtifactBuild({
event: "success",
durationMs: 120,
totalBytes: 2048,
assetCount: 3,
remainingExternalImportCount: 1,
});

assertEquals(attemptedWrites, 5);
});
});

describe("recordDataFetch", () => {
it("should record data fetch duration and increment counter", () => {
recorder.recordDataFetch(100);
Expand Down Expand Up @@ -527,6 +644,11 @@ describe("observability/metrics/recorder", () => {
buildDuration: null,
bundleSizeHistogram: null,
bundleCounter: null,
dependencyArtifactBuildCounter: null,
dependencyArtifactBuildDuration: null,
dependencyArtifactBuildBytes: null,
dependencyArtifactBuildAssetCount: null,
dependencyArtifactBuildExternalImportCount: null,
dataFetchDuration: null,
dataFetchCounter: null,
dataFetchErrorCounter: null,
Expand Down Expand Up @@ -574,10 +696,12 @@ describe("observability/metrics/recorder", () => {
nullRecorder.recordRSCError();
nullRecorder.recordBuild(100);
nullRecorder.recordBundle(100);
nullRecorder.recordDependencyArtifactBuild({ event: "claim" });
nullRecorder.recordDataFetch(100);
nullRecorder.recordDataFetchError();
nullRecorder.recordCorsRejection();
nullRecorder.recordSecurityHeaders();
nullRecorder.recordError();
});
});
});
53 changes: 53 additions & 0 deletions src/observability/metrics/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,59 @@ export class MetricsRecorder {
safelyRecord(() => this.instruments.bundleCounter?.add(1, attributes));
}

recordDependencyArtifactBuild(
input: {
event: "claim" | "success" | "failure";
durationMs?: number;
totalBytes?: number;
assetCount?: number;
remainingExternalImportCount?: number;
failureCode?: string;
},
): void {
const attributes = sanitizeTelemetryAttributes({
event: input.event,
...(input.failureCode ? { failure_code: input.failureCode } : {}),
});
safelyRecord(() => this.instruments.dependencyArtifactBuildCounter?.add(1, attributes));
const durationMs = input.durationMs;
if (durationMs !== undefined) {
safelyRecord(() =>
this.instruments.dependencyArtifactBuildDuration?.record(
nonNegativeFiniteMeasure(durationMs),
attributes,
)
);
}
const totalBytes = input.totalBytes;
if (totalBytes !== undefined) {
safelyRecord(() =>
this.instruments.dependencyArtifactBuildBytes?.record(
nonNegativeSafeInteger(totalBytes),
attributes,
)
);
}
const assetCount = input.assetCount;
if (assetCount !== undefined) {
safelyRecord(() =>
this.instruments.dependencyArtifactBuildAssetCount?.record(
nonNegativeSafeInteger(assetCount),
attributes,
)
);
}
const remainingExternalImportCount = input.remainingExternalImportCount;
if (remainingExternalImportCount !== undefined) {
safelyRecord(() =>
this.instruments.dependencyArtifactBuildExternalImportCount?.record(
nonNegativeSafeInteger(remainingExternalImportCount),
attributes,
)
);
}
}

recordDataFetch(
durationMs: number,
attributes?: Record<string, string>,
Expand Down
5 changes: 5 additions & 0 deletions src/observability/metrics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export interface MetricsInstruments {
buildDuration: Histogram | null;
bundleSizeHistogram: Histogram | null;
bundleCounter: Counter | null;
dependencyArtifactBuildCounter: Counter | null;
dependencyArtifactBuildDuration: Histogram | null;
dependencyArtifactBuildBytes: Histogram | null;
dependencyArtifactBuildAssetCount: Histogram | null;
dependencyArtifactBuildExternalImportCount: Histogram | null;

dataFetchDuration: Histogram | null;
dataFetchCounter: Counter | null;
Expand Down
36 changes: 36 additions & 0 deletions src/platform/adapters/veryfront-api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
VeryfrontAPIOperations,
} from "./operations.ts";
import { API_CLIENT_ERROR, type VeryfrontAPIConfig, VeryfrontError } from "./types.ts";
import type {
DependencyArtifactBuildResultBody,
DependencyArtifactContentType,
} from "#veryfront/release-assets/dependency-artifact-contracts.ts";

const logger = baseLogger.component("veryfront-api-client");

Expand Down Expand Up @@ -427,6 +431,38 @@ export class VeryfrontApiClient {
return this.operations.upsertStyleArtifact(projectRef, input);
}

// =============================================================================
// Dependency Artifact Build Operations
// =============================================================================

uploadDependencyArtifactAsset(
artifactId: string,
attemptCount: number,
contentHash: string,
contentType: DependencyArtifactContentType,
bytes: Uint8Array<ArrayBuffer>,
) {
return this.operations.uploadDependencyArtifactAsset(
artifactId,
attemptCount,
contentHash,
contentType,
bytes,
);
}

reportDependencyArtifactBuildResult(
artifactId: string,
attemptCount: number,
result: DependencyArtifactBuildResultBody,
) {
return this.operations.reportDependencyArtifactBuildResult(
artifactId,
attemptCount,
result,
);
}

// =============================================================================
// Release Asset Manifest Operations
// =============================================================================
Expand Down
Loading