Skip to content
Merged
23 changes: 23 additions & 0 deletions scripts/build/browser-safe-exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,26 @@ Deno.test("browser error adapters do not retain Node imports", async () => {
"the useAgent browser bundle must not retain a Node builtin import",
);
});

Deno.test("the public observability barrel does not eagerly import Node v8", async () => {
const output = await new Deno.Command(Deno.execPath(), {
args: [
"bundle",
"--platform=browser",
"--no-check",
"src/observability/index.ts",
],
cwd: new URL("../../", import.meta.url),
stdin: "null",
stdout: "piped",
stderr: "piped",
}).output();
const stderr = new TextDecoder().decode(output.stderr);
assert(output.success, `observability browser bundle failed:\n${stderr}`);

const bundle = new TextDecoder().decode(output.stdout);
assert(
!/\b(?:from|import)\s*["']node:v8["']/.test(bundle),
"the public observability barrel must not retain a browser-eager node:v8 import",
);
});
6 changes: 0 additions & 6 deletions scripts/lint/test-typecheck-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@
"src/middleware/core/pipeline/composer.test.ts",
"src/modules/import-map/preloader.test.ts",
"src/modules/react-loader/ssr-module-loader.stress.test.ts",
"src/observability/auto-instrument.test.ts",
"src/observability/auto-instrument/wrappers.test.ts",
"src/observability/instruments/error-instruments.test.ts",
"src/observability/log-buffer.test.ts",
"src/observability/metrics/recorder.test.ts",
"src/observability/tracing/span-operations.test.ts",
"src/platform/adapters/fs/veryfront/adapter-helpers.test.ts",
"src/platform/adapters/fs/veryfront/directory-operations.test.ts",
"src/platform/adapters/redis/node.test.ts",
Expand Down
41 changes: 41 additions & 0 deletions src/extensions/observability/application-error-reporter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {
ApplicationErrorContext as CanonicalApplicationErrorContext,
} from "#veryfront/observability/application-error-contract.ts";
import {
type ApplicationErrorContext,
type ApplicationErrorReporterInitializer,
ApplicationErrorReporterInitializerName,
} from "./index.ts";

Deno.test("application-error initializer re-exports the canonical context contract", async () => {
const extensionContext: ApplicationErrorContext = {
boundary: "worker.request",
processRole: "worker",
};
const canonicalContext: CanonicalApplicationErrorContext = extensionContext;
const roundTripContext: ApplicationErrorContext = canonicalContext;
let capturedProcessRole: string | undefined;

const initializer: ApplicationErrorReporterInitializer = {
initialize: () => ({
reporter: {
capture(_error, context) {
capturedProcessRole = context.processRole;
return "event-id";
},
flush: () => Promise.resolve(true),
},
dispose() {},
}),
};
const session = await initializer.initialize({ serviceName: "worker" });
if (!session) throw new Error("initializer unexpectedly disabled reporting");

const eventId = session.reporter.capture(new Error("failed"), roundTripContext);
if (eventId !== "event-id" || capturedProcessRole !== "worker") {
throw new Error("canonical application-error context was not preserved");
}
if (ApplicationErrorReporterInitializerName !== "ApplicationErrorReporterInitializer") {
throw new Error("application-error initializer contract name changed");
}
});
30 changes: 30 additions & 0 deletions src/extensions/observability/application-error-reporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ApplicationErrorReporter } from "#veryfront/observability/application-error-contract.ts";

export type {
ApplicationErrorContext,
ApplicationErrorReporter,
} from "#veryfront/observability/application-error-contract.ts";

/** Runtime context passed to an explicitly selected reporter initializer. */
export type ApplicationErrorReporterInitializationContext = {
serviceName: string;
};

/** Reporter and cleanup ownership returned by an application-selected initializer. */
export type ApplicationErrorReporterSession = {
reporter: ApplicationErrorReporter;
dispose(): void | Promise<void>;
};

/** Application-composition contract for an error-reporting implementation. */
export type ApplicationErrorReporterInitializer = {
initialize(
context: ApplicationErrorReporterInitializationContext,
):
| ApplicationErrorReporterSession
| undefined
| Promise<ApplicationErrorReporterSession | undefined>;
};

/** Contract name used when an application composes a reporter through extensions. */
export const ApplicationErrorReporterInitializerName = "ApplicationErrorReporterInitializer";
8 changes: 8 additions & 0 deletions src/extensions/observability/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ export type {
NodeTelemetryProvider,
} from "./node-telemetry-provider.ts";
export { NodeTelemetryProviderName } from "./node-telemetry-provider.ts";
export type {
ApplicationErrorContext,
ApplicationErrorReporter,
ApplicationErrorReporterInitializationContext,
ApplicationErrorReporterInitializer,
ApplicationErrorReporterSession,
} from "./application-error-reporter.ts";
export { ApplicationErrorReporterInitializerName } from "./application-error-reporter.ts";
43 changes: 38 additions & 5 deletions src/middleware/core/pipeline/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,40 @@ describe("middleware/core/pipeline/MiddlewarePipeline", () => {
const pipeline = new MiddlewarePipeline();
let cleanupCount = 0;

pipeline.onTeardown(() => {
cleanupCount++;
});
pipeline.use(() => {
throw new Error("middleware blew up");
});
// The pipeline turns a middleware failure into a 500, so the rejection
// has to come from the error response itself failing to build.
const adapter = {
env: {
get() {
throw new Error("adapter unavailable");
},
},
};

await assertRejects(
() =>
pipeline.execute(
new Request("http://localhost/"),
undefined,
undefined,
adapter as unknown as Parameters<MiddlewarePipeline["execute"]>[3],
),
Error,
"adapter unavailable",
);
assertEquals(cleanupCount, 1);
});

it("should keep execution working when the tracer provider fails", async () => {
const pipeline = new MiddlewarePipeline();
let cleanupCount = 0;

pipeline.onTeardown(() => {
cleanupCount++;
});
Expand All @@ -349,11 +383,10 @@ describe("middleware/core/pipeline/MiddlewarePipeline", () => {
},
});

await assertRejects(
() => pipeline.execute(new Request("http://localhost/")),
Error,
"tracing unavailable",
);
const response = await pipeline.execute(new Request("http://localhost/"));

assertEquals(response.status, 404);
assertEquals(await response.text(), "Not Found");
assertEquals(cleanupCount, 1);
});

Expand Down
Loading