@fedify/fedify
Version:
An ActivityPub server framework
635 lines (634 loc) • 26.9 kB
JavaScript
import "@js-temporal/polyfill";
import "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
import "../std__assert-BBjXFNOb.mjs";
import { t as assertRejects } from "../assert_rejects-DN60FHPX.mjs";
import { _ as recordKeyLookup, a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, m as recordDocumentFetch, p as recordDocumentCache, r as getFederationMetrics, t as classifyFetchError, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "../metrics-Pue1x4ta.mjs";
import { createTestMeterProvider, test } from "@fedify/fixture";
import { FetchError } from "@fedify/vocab-runtime";
import { DataPointType, MeterProvider, MetricReader } from "@opentelemetry/sdk-metrics";
//#region src/federation/metrics.test.ts
var TestMetricReader = class extends MetricReader {
onShutdown() {
return Promise.resolve();
}
onForceFlush() {
return Promise.resolve();
}
};
const noopQueue = {
enqueue() {
return Promise.resolve();
},
listen() {
return Promise.resolve();
}
};
test("getRemoteHost() includes non-default ports", () => {
assertEquals(getRemoteHost(new URL("https://example.com/inbox")), "example.com");
assertEquals(getRemoteHost(new URL("https://example.com:8443/inbox")), "example.com:8443");
assertEquals(getRemoteHost(new URL("https://example.com:443/inbox")), "example.com");
});
test("recordFanoutRecipients() records the recipient count with activity type", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordFanoutRecipients(meterProvider, 7, "https://www.w3.org/ns/activitystreams#Create");
const measurements = recorder.getMeasurements("activitypub.fanout.recipients");
assertEquals(measurements.length, 1);
assertEquals(measurements[0].type, "histogram");
assertEquals(measurements[0].value, 7);
assertEquals(measurements[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
});
test("recordFanoutRecipients() omits activity type when unknown", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordFanoutRecipients(meterProvider, 0);
const measurements = recorder.getMeasurements("activitypub.fanout.recipients");
assertEquals(measurements.length, 1);
assertEquals(measurements[0].value, 0);
assertEquals("activitypub.activity.type" in measurements[0].attributes, false);
});
test("signature verification duration uses explicit low-latency buckets", async () => {
const reader = new TestMetricReader();
const meterProvider = new MeterProvider({ readers: [reader] });
try {
getFederationMetrics(meterProvider).recordSignatureVerificationDuration(7, "http", "verified");
const metric = (await reader.collect()).resourceMetrics.scopeMetrics.flatMap((scope) => scope.metrics).find((metric) => metric.descriptor.name === "activitypub.signature.verification.duration");
assertEquals(metric?.dataPointType, DataPointType.HISTOGRAM);
assertEquals(metric?.dataPoints[0].value.buckets.boundaries, [
.1,
.25,
.5,
1,
2.5,
5,
10,
25,
50,
100,
250,
500,
1e3
]);
} finally {
await meterProvider.shutdown();
}
});
test("registerQueueDepthGauge() skips unavailable depth snapshots", async () => {
const reader = new TestMetricReader();
const meterProvider = new MeterProvider({ readers: [reader] });
try {
registerQueueDepthGauge(meterProvider, [
{
role: "inbox",
queue: {
enqueue() {
return Promise.resolve();
},
listen() {
return Promise.resolve();
},
getDepth() {
throw new TypeError("backend unavailable");
}
}
},
{
role: "outbox",
queue: {
enqueue() {
return Promise.resolve();
},
listen() {
return Promise.resolve();
},
getDepth() {
return Promise.resolve(null);
}
}
},
{
role: "fanout",
queue: {
enqueue() {
return Promise.resolve();
},
listen() {
return Promise.resolve();
},
getDepth() {
return Promise.resolve({ queued: 7 });
}
}
}
]);
const result = await reader.collect();
assertEquals(result.errors, []);
const queueDepth = result.resourceMetrics.scopeMetrics.flatMap((scope) => scope.metrics).find((metric) => metric.descriptor.name === "fedify.queue.depth");
assertEquals(queueDepth?.dataPointType, DataPointType.GAUGE);
assertEquals(queueDepth?.dataPoints.map((point) => ({
state: point.attributes["fedify.queue.depth.state"],
role: point.attributes["fedify.queue.role"],
value: point.value
})), [{
state: "queued",
role: "fanout",
value: 7
}]);
} finally {
await meterProvider.shutdown();
}
});
test("registerQueueDepthGauge() queries queue depths in parallel", async () => {
const reader = new TestMetricReader();
const meterProvider = new MeterProvider({ readers: [reader] });
let releaseSlowDepth;
let fastDepthStarted = false;
try {
registerQueueDepthGauge(meterProvider, [{
role: "inbox",
queue: {
enqueue() {
return Promise.resolve();
},
listen() {
return Promise.resolve();
},
getDepth() {
return new Promise((resolve) => {
releaseSlowDepth = resolve;
});
}
}
}, {
role: "outbox",
queue: {
enqueue() {
return Promise.resolve();
},
listen() {
return Promise.resolve();
},
getDepth() {
fastDepthStarted = true;
return Promise.resolve({ queued: 5 });
}
}
}]);
const collection = reader.collect();
await Promise.resolve();
assertEquals(fastDepthStarted, true);
releaseSlowDepth?.({ queued: 3 });
assertEquals((await collection).resourceMetrics.scopeMetrics.flatMap((scope) => scope.metrics).find((metric) => metric.descriptor.name === "fedify.queue.depth")?.dataPoints.map((point) => point.value).sort(), [3, 5]);
} finally {
releaseSlowDepth?.({ queued: 0 });
await meterProvider.shutdown();
}
});
test("recordInboxActivity() records counter with result and activity type", () => {
const [meterProvider, recorder] = createTestMeterProvider();
for (const result of [
"queued",
"processed",
"retried",
"rejected",
"abandoned"
]) recordInboxActivity(meterProvider, result, "https://www.w3.org/ns/activitystreams#Follow");
const measurements = recorder.getMeasurements("activitypub.inbox.activity");
assertEquals(measurements.length, 5);
for (const m of measurements) {
assertEquals(m.type, "counter");
assertEquals(m.value, 1);
assertEquals(m.attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Follow");
}
assertEquals(measurements.map((m) => m.attributes["activitypub.processing.result"]), [
"queued",
"processed",
"retried",
"rejected",
"abandoned"
]);
});
test("recordInboxActivity() omits activity type when unknown", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordInboxActivity(meterProvider, "rejected");
const measurements = recorder.getMeasurements("activitypub.inbox.activity");
assertEquals(measurements.length, 1);
assertEquals(measurements[0].attributes["activitypub.processing.result"], "rejected");
assertEquals("activitypub.activity.type" in measurements[0].attributes, false);
});
test("recordOutboxEnqueue() also records activitypub.outbox.activity{queued} on initial enqueue", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordOutboxEnqueue(meterProvider, noopQueue, {
activityType: "https://www.w3.org/ns/activitystreams#Create",
attempt: 0
});
const queued = recorder.getMeasurements("activitypub.outbox.activity");
assertEquals(queued.length, 1);
assertEquals(queued[0].type, "counter");
assertEquals(queued[0].attributes["activitypub.processing.result"], "queued");
assertEquals(queued[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
});
test("recordOutboxEnqueue() does not record outbox.activity{queued} on retry enqueues", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordOutboxEnqueue(meterProvider, noopQueue, {
activityType: "https://www.w3.org/ns/activitystreams#Create",
attempt: 1
});
assertEquals(recorder.getMeasurements("activitypub.outbox.activity").length, 0);
});
test("recordOutboxActivity() records counter with result and activity type", () => {
const [meterProvider, recorder] = createTestMeterProvider();
for (const result of [
"queued",
"retried",
"abandoned"
]) recordOutboxActivity(meterProvider, result, "https://www.w3.org/ns/activitystreams#Announce");
const measurements = recorder.getMeasurements("activitypub.outbox.activity");
assertEquals(measurements.length, 3);
for (const m of measurements) {
assertEquals(m.type, "counter");
assertEquals(m.value, 1);
assertEquals(m.attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Announce");
}
assertEquals(measurements.map((m) => m.attributes["activitypub.processing.result"]), [
"queued",
"retried",
"abandoned"
]);
});
test("recordCircuitBreakerStateChange() records counter with bounded attributes", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordCircuitBreakerStateChange(meterProvider, "remote.example", "half_open");
const measurements = recorder.getMeasurements("activitypub.circuit_breaker.state_change");
assertEquals(measurements.length, 1);
assertEquals(measurements[0].type, "counter");
assertEquals(measurements[0].value, 1);
assertEquals(measurements[0].attributes["activitypub.remote.host"], "remote.example");
assertEquals(measurements[0].attributes["activitypub.circuit_breaker.state"], "half_open");
});
test("recordKeyLookup() records counter and duration with all attributes", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordKeyLookup(meterProvider, {
durationMs: 42,
result: "fetched",
remoteUrl: new URL("https://example.com/users/alice#main-key"),
cacheEnabled: true,
statusCode: 200
});
const counters = recorder.getMeasurements("activitypub.key.lookup");
assertEquals(counters.length, 1);
assertEquals(counters[0].type, "counter");
assertEquals(counters[0].value, 1);
assertEquals(counters[0].attributes["activitypub.lookup.kind"], "public_key");
assertEquals(counters[0].attributes["activitypub.lookup.result"], "fetched");
assertEquals(counters[0].attributes["activitypub.remote.host"], "example.com");
assertEquals(counters[0].attributes["activitypub.cache.enabled"], true);
assertEquals(counters[0].attributes["http.response.status_code"], 200);
const durations = recorder.getMeasurements("activitypub.key.lookup.duration");
assertEquals(durations.length, 1);
assertEquals(durations[0].type, "histogram");
assertEquals(durations[0].value, 42);
assertEquals(durations[0].attributes["activitypub.lookup.kind"], "public_key");
assertEquals(durations[0].attributes["activitypub.lookup.result"], "fetched");
assertEquals(durations[0].attributes["activitypub.remote.host"], "example.com");
assertEquals(durations[0].attributes["activitypub.cache.enabled"], true);
assertEquals(durations[0].attributes["http.response.status_code"], 200);
});
test("recordKeyLookup() omits optional attributes when not provided", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordKeyLookup(meterProvider, {
durationMs: 0,
result: "error",
cacheEnabled: false
});
const counter = recorder.getMeasurement("activitypub.key.lookup");
assertEquals(counter?.attributes["activitypub.lookup.result"], "error");
assertEquals(counter?.attributes["activitypub.cache.enabled"], false);
assertEquals("activitypub.remote.host" in counter.attributes, false);
assertEquals("http.response.status_code" in counter.attributes, false);
const duration = recorder.getMeasurement("activitypub.key.lookup.duration");
assertEquals("activitypub.remote.host" in duration.attributes, false);
assertEquals("http.response.status_code" in duration.attributes, false);
});
test("recordDocumentFetch() records counter and duration with all attributes", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordDocumentFetch(meterProvider, {
durationMs: 123,
kind: "object",
result: "not_found",
remoteUrl: new URL("https://remote.test/objects/123"),
cacheEnabled: true,
statusCode: 404
});
const counter = recorder.getMeasurement("activitypub.document.fetch");
assertEquals(counter?.type, "counter");
assertEquals(counter?.value, 1);
assertEquals(counter?.attributes["activitypub.lookup.kind"], "object");
assertEquals(counter?.attributes["activitypub.lookup.result"], "not_found");
assertEquals(counter?.attributes["activitypub.remote.host"], "remote.test");
assertEquals(counter?.attributes["activitypub.cache.enabled"], true);
assertEquals(counter?.attributes["http.response.status_code"], 404);
const duration = recorder.getMeasurement("activitypub.document.fetch.duration");
assertEquals(duration?.type, "histogram");
assertEquals(duration?.value, 123);
assertEquals(duration?.attributes["activitypub.lookup.kind"], "object");
assertEquals(duration?.attributes["activitypub.lookup.result"], "not_found");
});
test("recordDocumentFetch() omits optional attributes when not provided", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordDocumentFetch(meterProvider, {
durationMs: 5,
kind: "context",
result: "fetched"
});
const counter = recorder.getMeasurement("activitypub.document.fetch");
assertEquals(counter?.attributes["activitypub.lookup.kind"], "context");
assertEquals(counter?.attributes["activitypub.lookup.result"], "fetched");
assertEquals("activitypub.remote.host" in counter.attributes, false);
assertEquals("activitypub.cache.enabled" in counter.attributes, false);
assertEquals("http.response.status_code" in counter.attributes, false);
assertEquals(recorder.getMeasurement("activitypub.document.fetch.duration")?.value, 5);
});
test("recordDocumentCache() records hit and miss as a counter", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordDocumentCache(meterProvider, {
kind: "object",
result: "hit",
remoteUrl: new URL("https://remote.test/objects/1")
});
recordDocumentCache(meterProvider, {
kind: "context",
result: "miss",
remoteUrl: new URL("https://w3id.org/security/v1")
});
const measurements = recorder.getMeasurements("activitypub.document.cache");
assertEquals(measurements.length, 2);
for (const m of measurements) {
assertEquals(m.type, "counter");
assertEquals(m.value, 1);
}
assertEquals(measurements[0].attributes["activitypub.lookup.kind"], "object");
assertEquals(measurements[0].attributes["activitypub.lookup.result"], "hit");
assertEquals(measurements[0].attributes["activitypub.remote.host"], "remote.test");
assertEquals(measurements[1].attributes["activitypub.lookup.kind"], "context");
assertEquals(measurements[1].attributes["activitypub.lookup.result"], "miss");
assertEquals(measurements[1].attributes["activitypub.remote.host"], "w3id.org");
});
test("recordWebFingerHandle() records counter and duration with all attributes", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordWebFingerHandle(meterProvider, {
durationMs: 17,
result: "resolved",
scheme: "acct",
statusCode: 200
});
const counters = recorder.getMeasurements("webfinger.handle");
assertEquals(counters.length, 1);
assertEquals(counters[0].type, "counter");
assertEquals(counters[0].value, 1);
assertEquals(counters[0].attributes["webfinger.handle.result"], "resolved");
assertEquals(counters[0].attributes["webfinger.resource.scheme"], "acct");
assertEquals(counters[0].attributes["http.response.status_code"], 200);
const durations = recorder.getMeasurements("webfinger.handle.duration");
assertEquals(durations.length, 1);
assertEquals(durations[0].type, "histogram");
assertEquals(durations[0].value, 17);
assertEquals(durations[0].attributes["webfinger.handle.result"], "resolved");
assertEquals(durations[0].attributes["webfinger.resource.scheme"], "acct");
assertEquals(durations[0].attributes["http.response.status_code"], 200);
});
test("recordWebFingerHandle() records each non-resolved result with the matching status code", () => {
const [meterProvider, recorder] = createTestMeterProvider();
for (const [result, statusCode] of [
["invalid", 400],
["not_found", 404],
["tombstoned", 410]
]) recordWebFingerHandle(meterProvider, {
durationMs: 1,
result,
scheme: "acct",
statusCode
});
const counters = recorder.getMeasurements("webfinger.handle");
assertEquals(counters.length, 3);
assertEquals(counters.map((m) => m.attributes["webfinger.handle.result"]), [
"invalid",
"not_found",
"tombstoned"
]);
assertEquals(counters.map((m) => m.attributes["http.response.status_code"]), [
400,
404,
410
]);
});
test("recordWebFingerHandle() omits optional attributes when not provided", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordWebFingerHandle(meterProvider, {
durationMs: 0,
result: "error"
});
const counter = recorder.getMeasurement("webfinger.handle");
assertEquals(counter?.attributes["webfinger.handle.result"], "error");
assertEquals("webfinger.resource.scheme" in (counter?.attributes ?? {}), false);
assertEquals("http.response.status_code" in (counter?.attributes ?? {}), false);
});
test("recordCollectionRequest() records counter with bounded attributes", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordCollectionRequest(meterProvider, {
kind: "followers",
page: true,
dispatcher: "built_in",
result: "served",
statusCode: 200
});
const counter = recorder.getMeasurement("activitypub.collection.request");
assertEquals(counter?.type, "counter");
assertEquals(counter?.value, 1);
assertEquals(counter?.attributes["activitypub.collection.kind"], "followers");
assertEquals(counter?.attributes["activitypub.collection.page"], true);
assertEquals(counter?.attributes["fedify.collection.dispatcher"], "built_in");
assertEquals(counter?.attributes["activitypub.collection.result"], "served");
assertEquals(counter?.attributes["http.response.status_code"], 200);
});
test("recordCollectionRequest() omits status code when unavailable", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordCollectionRequest(meterProvider, {
kind: "custom",
page: false,
dispatcher: "custom",
result: "error"
});
const counter = recorder.getMeasurement("activitypub.collection.request");
assertEquals(counter?.attributes["activitypub.collection.kind"], "custom");
assertEquals(counter?.attributes["activitypub.collection.page"], false);
assertEquals(counter?.attributes["fedify.collection.dispatcher"], "custom");
assertEquals(counter?.attributes["activitypub.collection.result"], "error");
assertEquals("http.response.status_code" in (counter?.attributes ?? {}), false);
});
test("recordCollectionDispatchDuration() records histogram", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordCollectionDispatchDuration(meterProvider, 12, {
kind: "outbox",
page: false,
dispatcher: "built_in",
result: "served"
});
const duration = recorder.getMeasurement("activitypub.collection.dispatch.duration");
assertEquals(duration?.type, "histogram");
assertEquals(duration?.value, 12);
assertEquals(duration?.attributes["activitypub.collection.kind"], "outbox");
assertEquals(duration?.attributes["activitypub.collection.page"], false);
assertEquals(duration?.attributes["fedify.collection.dispatcher"], "built_in");
assertEquals(duration?.attributes["activitypub.collection.result"], "served");
});
test("recordCollectionPageItems() records item count histogram", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordCollectionPageItems(meterProvider, 3, {
kind: "featured_tags",
page: true,
dispatcher: "built_in",
result: "served",
statusCode: 200
});
const items = recorder.getMeasurement("activitypub.collection.page.items");
assertEquals(items?.type, "histogram");
assertEquals(items?.value, 3);
assertEquals(items?.attributes["activitypub.collection.kind"], "featured_tags");
assertEquals(items?.attributes["activitypub.collection.page"], true);
assertEquals(items?.attributes["http.response.status_code"], 200);
});
test("recordCollectionTotalItems() records total item histogram", () => {
const [meterProvider, recorder] = createTestMeterProvider();
recordCollectionTotalItems(meterProvider, 42, {
kind: "liked",
page: false,
dispatcher: "built_in",
result: "served"
});
const total = recorder.getMeasurement("activitypub.collection.total_items");
assertEquals(total?.type, "histogram");
assertEquals(total?.value, 42);
assertEquals(total?.attributes["activitypub.collection.kind"], "liked");
assertEquals(total?.attributes["activitypub.collection.page"], false);
});
test("classifyFetchError() classifies FetchError with 404 as not_found", () => {
assertEquals(classifyFetchError(new FetchError("https://example.com/k", "not found", new Response("", { status: 404 }))), {
result: "not_found",
statusCode: 404
});
});
test("classifyFetchError() classifies FetchError with 410 as not_found", () => {
assertEquals(classifyFetchError(new FetchError("https://example.com/k", "gone", new Response("", { status: 410 }))), {
result: "not_found",
statusCode: 410
});
});
test("classifyFetchError() classifies FetchError with 500 as error", () => {
assertEquals(classifyFetchError(new FetchError("https://example.com/k", "server error", new Response("", { status: 500 }))), {
result: "error",
statusCode: 500
});
});
test("classifyFetchError() classifies FetchError without response as network_error", () => {
assertEquals(classifyFetchError(new FetchError("https://example.com/k", "boom")), { result: "network_error" });
});
test("classifyFetchError() classifies a bare TypeError as network_error", () => {
assertEquals(classifyFetchError(/* @__PURE__ */ new TypeError("connect failed")), { result: "network_error" });
});
test("classifyFetchError() classifies an AbortError as network_error", () => {
const abort = /* @__PURE__ */ new Error("aborted");
abort.name = "AbortError";
assertEquals(classifyFetchError(abort), { result: "network_error" });
});
test("classifyFetchError() classifies any other thrown value as error", () => {
assertEquals(classifyFetchError(/* @__PURE__ */ new Error("nope")), { result: "error" });
assertEquals(classifyFetchError("string error"), { result: "error" });
assertEquals(classifyFetchError(void 0), { result: "error" });
});
test("instrumentDocumentLoader() records fetched on success", async () => {
const [meterProvider, recorder] = createTestMeterProvider();
const inner = (url) => Promise.resolve({
contextUrl: null,
documentUrl: url,
document: { ok: true }
});
assertEquals((await instrumentDocumentLoader(inner, {
meterProvider,
kind: "object",
cacheEnabled: true
})("https://example.com/o")).document, { ok: true });
const counter = recorder.getMeasurement("activitypub.document.fetch");
assertEquals(counter?.attributes["activitypub.lookup.kind"], "object");
assertEquals(counter?.attributes["activitypub.lookup.result"], "fetched");
assertEquals(counter?.attributes["activitypub.remote.host"], "example.com");
assertEquals(counter?.attributes["activitypub.cache.enabled"], true);
assertEquals("http.response.status_code" in counter.attributes, false);
const duration = recorder.getMeasurement("activitypub.document.fetch.duration");
assertEquals(duration?.type, "histogram");
assertEquals(duration?.attributes["activitypub.lookup.result"], "fetched");
});
test("instrumentDocumentLoader() records not_found on FetchError 404", async () => {
const [meterProvider, recorder] = createTestMeterProvider();
const inner = (url) => Promise.reject(new FetchError(url, "HTTP 404", new Response("", { status: 404 })));
const wrapped = instrumentDocumentLoader(inner, {
meterProvider,
kind: "context",
cacheEnabled: false
});
await assertRejects(() => wrapped("https://example.com/missing"), FetchError);
const counter = recorder.getMeasurement("activitypub.document.fetch");
assertEquals(counter?.attributes["activitypub.lookup.kind"], "context");
assertEquals(counter?.attributes["activitypub.lookup.result"], "not_found");
assertEquals(counter?.attributes["activitypub.remote.host"], "example.com");
assertEquals(counter?.attributes["activitypub.cache.enabled"], false);
assertEquals(counter?.attributes["http.response.status_code"], 404);
});
test("instrumentDocumentLoader() records network_error on TypeError", async () => {
const [meterProvider, recorder] = createTestMeterProvider();
const inner = () => Promise.reject(/* @__PURE__ */ new TypeError("fetch failed"));
const wrapped = instrumentDocumentLoader(inner, {
meterProvider,
kind: "object"
});
await assertRejects(() => wrapped("https://example.com/o"), TypeError);
const counter = recorder.getMeasurement("activitypub.document.fetch");
assertEquals(counter?.attributes["activitypub.lookup.result"], "network_error");
assertEquals("activitypub.cache.enabled" in counter.attributes, false);
});
test("instrumentDocumentLoader() returns inner loader unchanged when meterProvider is omitted", async () => {
const [, recorder] = createTestMeterProvider();
let callCount = 0;
const inner = (url) => {
callCount++;
return Promise.resolve({
contextUrl: null,
documentUrl: url,
document: { ok: true }
});
};
const wrapped = instrumentDocumentLoader(inner, { kind: "object" });
assertEquals(wrapped, inner);
await wrapped("https://example.com/o");
assertEquals(callCount, 1);
assertEquals(recorder.getMeasurements("activitypub.document.fetch").length, 0);
assertEquals(recorder.getMeasurements("activitypub.document.fetch.duration").length, 0);
});
test("instrumentDocumentLoader() omits remote.host when URL is unparseable", async () => {
const [meterProvider, recorder] = createTestMeterProvider();
const inner = (url) => Promise.resolve({
contextUrl: null,
documentUrl: url,
document: {}
});
await instrumentDocumentLoader(inner, {
meterProvider,
kind: "other"
})("not a url");
const counter = recorder.getMeasurement("activitypub.document.fetch");
assertEquals(counter?.attributes["activitypub.lookup.kind"], "other");
assertEquals(counter?.attributes["activitypub.lookup.result"], "fetched");
assertEquals("activitypub.remote.host" in counter.attributes, false);
});
//#endregion
export {};