@fedify/fedify 2.3.0-dev.1158 → 2.3.0-dev.1172

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/{builder-B66L9i5E.mjs → builder-JoFBmqfM.mjs} +2 -2
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{deno-O_rwum1q.mjs → deno-Cb_y5qEi.mjs} +1 -1
  4. package/dist/{docloader-Ct8PhKFS.mjs → docloader-Bv4TW6eo.mjs} +2 -2
  5. package/dist/federation/builder.test.mjs +1 -1
  6. package/dist/federation/handler.test.mjs +305 -3
  7. package/dist/federation/idempotency.test.mjs +2 -2
  8. package/dist/federation/metrics.test.mjs +80 -1
  9. package/dist/federation/middleware.test.mjs +20 -6
  10. package/dist/federation/mod.cjs +1 -1
  11. package/dist/federation/mod.js +1 -1
  12. package/dist/federation/send.test.mjs +3 -3
  13. package/dist/federation/temporal.test.mjs +1 -1
  14. package/dist/federation/webfinger.test.mjs +1 -1
  15. package/dist/{http-BoRhhcgB.mjs → http--aE0vk2u.mjs} +3 -3
  16. package/dist/{http-CFP8WMMv.js → http-C0XZv7iH.js} +92 -2
  17. package/dist/{http-DlPd_LYM.cjs → http-D_HNhC57.cjs} +115 -1
  18. package/dist/{key-DyATZSWG.mjs → key-Cl_bixZo.mjs} +2 -2
  19. package/dist/{kv-cache-BJo6COYN.cjs → kv-cache-CdOuPFgC.cjs} +1 -1
  20. package/dist/{kv-cache-DeJE8EeD.mjs → kv-cache-DQUblF4f.mjs} +1 -1
  21. package/dist/{kv-cache-dH0biV98.js → kv-cache-DsbVBK7Y.js} +1 -1
  22. package/dist/{ld-B8wjsKDJ.mjs → ld-xVq6y31b.mjs} +3 -3
  23. package/dist/{metrics-oMUWaw6W.mjs → metrics-CKticT28.mjs} +92 -2
  24. package/dist/{middleware-DwZ1ofL9.js → middleware-BSuEI4Qf.js} +318 -107
  25. package/dist/{middleware-BzOa0ncb.mjs → middleware-BmPIKmb4.mjs} +1 -1
  26. package/dist/{middleware-xtTRaiJL.mjs → middleware-DHM2Pjqf.mjs} +327 -116
  27. package/dist/{middleware-CcJyVEpv.cjs → middleware-hxnyAewn.cjs} +318 -107
  28. package/dist/mod.cjs +4 -4
  29. package/dist/mod.js +4 -4
  30. package/dist/nodeinfo/handler.test.mjs +1 -1
  31. package/dist/{owner-BxjgK8PG.mjs → owner-DmU2qEh_.mjs} +2 -2
  32. package/dist/{proof-42Q9NiqN.mjs → proof-1XBgQ0Z0.mjs} +3 -3
  33. package/dist/{proof-B_6gAVQ2.js → proof-CmS6yxgt.js} +1 -1
  34. package/dist/{proof-CfttNzWW.cjs → proof-wm6UxUoM.cjs} +1 -1
  35. package/dist/{send-R1_K46CH.mjs → send-CmtB8w5D.mjs} +3 -3
  36. package/dist/sig/http.test.mjs +2 -2
  37. package/dist/sig/key.test.mjs +1 -1
  38. package/dist/sig/ld.test.mjs +2 -2
  39. package/dist/sig/mod.cjs +2 -2
  40. package/dist/sig/mod.js +2 -2
  41. package/dist/sig/owner.test.mjs +1 -1
  42. package/dist/sig/proof.test.mjs +1 -1
  43. package/dist/{temporal-8kDX3E4q.mjs → temporal-DE9_a2nI.mjs} +1 -1
  44. package/dist/utils/docloader.test.mjs +2 -2
  45. package/dist/utils/kv-cache.test.mjs +1 -1
  46. package/dist/utils/mod.cjs +1 -1
  47. package/dist/utils/mod.js +1 -1
  48. package/package.json +6 -6
@@ -1,7 +1,7 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-O_rwum1q.mjs";
4
+ import { n as version, t as name } from "./deno-Cb_y5qEi.mjs";
5
5
  import { t as ActivityListenerSet } from "./activity-listener-tztVvlNb.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
@@ -73,7 +73,7 @@ var FederationBuilderImpl = class {
73
73
  this.collectionTypeIds = {};
74
74
  }
75
75
  async build(options) {
76
- const { FederationImpl } = await import("./middleware-BzOa0ncb.mjs");
76
+ const { FederationImpl } = await import("./middleware-BmPIKmb4.mjs");
77
77
  const f = new FederationImpl(options);
78
78
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
79
79
  f.router = this.router.clone();
@@ -5,7 +5,7 @@ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs";
6
6
  import { t as assert } from "../assert-OguE97r2.mjs";
7
7
  import { t as MemoryKvStore } from "../kv-x2IvBUyq.mjs";
8
- import { n as FederationImpl, v as actorDehydrator, y as autoIdAssigner } from "../middleware-xtTRaiJL.mjs";
8
+ import { n as FederationImpl, v as actorDehydrator, y as autoIdAssigner } from "../middleware-DHM2Pjqf.mjs";
9
9
  import { Follow, Person } from "@fedify/vocab";
10
10
  import { test } from "@fedify/fixture";
11
11
  //#region src/compat/transformers.test.ts
@@ -3,6 +3,6 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  //#region deno.json
5
5
  var name = "@fedify/fedify";
6
- var version = "2.3.0-dev.1158+e7295772";
6
+ var version = "2.3.0-dev.1172+8cc8a23f";
7
7
  //#endregion
8
8
  export { version as n, name as t };
@@ -1,8 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { o as validateCryptoKey } from "./key-DyATZSWG.mjs";
5
- import { n as doubleKnock } from "./http-BoRhhcgB.mjs";
4
+ import { o as validateCryptoKey } from "./key-Cl_bixZo.mjs";
5
+ import { n as doubleKnock } from "./http--aE0vk2u.mjs";
6
6
  import { getLogger } from "@logtape/logtape";
7
7
  import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
8
8
  import { curry } from "es-toolkit";
@@ -5,7 +5,7 @@ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import { r as assertExists } from "../std__assert-BBjXFNOb.mjs";
6
6
  import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs";
7
7
  import { t as MemoryKvStore } from "../kv-x2IvBUyq.mjs";
8
- import { r as createFederationBuilder } from "../builder-B66L9i5E.mjs";
8
+ import { r as createFederationBuilder } from "../builder-JoFBmqfM.mjs";
9
9
  import { DisallowedOperatorError, DisallowedVarSpecModifierError, DuplicateRouteVariableError, RouteTemplateOptionsNotMatchedError, RouterError } from "@fedify/uri-template";
10
10
  import { Activity, Note, Person } from "@fedify/vocab";
11
11
  import { test } from "@fedify/fixture";
@@ -8,11 +8,11 @@ import { n as assertGreaterOrEqual, t as assertRejects } from "../assert_rejects
8
8
  import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs";
9
9
  import { t as assert } from "../assert-OguE97r2.mjs";
10
10
  import { r as parseAcceptSignature } from "../accept-CceiKpCy.mjs";
11
- import { s as signRequest } from "../http-BoRhhcgB.mjs";
11
+ import { s as signRequest } from "../http--aE0vk2u.mjs";
12
12
  import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-C3kae-6B.mjs";
13
- import { a as compactJsonLd, p as signJsonLd } from "../ld-B8wjsKDJ.mjs";
13
+ import { a as compactJsonLd, p as signJsonLd } from "../ld-xVq6y31b.mjs";
14
14
  import { t as MemoryKvStore } from "../kv-x2IvBUyq.mjs";
15
- import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-xtTRaiJL.mjs";
15
+ import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-DHM2Pjqf.mjs";
16
16
  import { t as ActivityListenerSet } from "../activity-listener-tztVvlNb.mjs";
17
17
  import { Activity, Create, Note, Person, Tombstone } from "@fedify/vocab";
18
18
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
@@ -928,6 +928,192 @@ test("handleCollection()", async () => {
928
928
  assertEquals(onNotFoundCalled, null);
929
929
  assertEquals(onUnauthorizedCalled, null);
930
930
  });
931
+ test("handleCollection() records OpenTelemetry collection metrics", async () => {
932
+ const [meterProvider, recorder] = createTestMeterProvider();
933
+ const context = createRequestContext({
934
+ federation: createFederation({
935
+ kv: new MemoryKvStore(),
936
+ meterProvider
937
+ }),
938
+ data: void 0,
939
+ url: new URL("https://example.com/users/someone/followers"),
940
+ request: new Request("https://example.com/users/someone/followers", { headers: { Accept: "application/activity+json" } }),
941
+ getActorUri(identifier) {
942
+ return new URL(`https://example.com/users/${identifier}`);
943
+ }
944
+ });
945
+ const dispatcher = (_ctx, identifier) => identifier === "someone" ? { items: [
946
+ new Create({ id: new URL("https://example.com/activities/1") }),
947
+ new Create({ id: new URL("https://example.com/activities/2") }),
948
+ new Create({ id: new URL("https://example.com/activities/3") })
949
+ ] } : null;
950
+ const counter = (_ctx, identifier) => identifier === "someone" ? 3 : null;
951
+ assertEquals((await handleCollection(context.request, {
952
+ context,
953
+ name: "followers",
954
+ identifier: "someone",
955
+ uriGetter(identifier) {
956
+ return new URL(`https://example.com/users/${identifier}/followers`);
957
+ },
958
+ collectionCallbacks: {
959
+ dispatcher,
960
+ counter
961
+ },
962
+ meterProvider,
963
+ onNotFound: () => new Response("Not found", { status: 404 }),
964
+ onUnauthorized: () => new Response("Unauthorized", { status: 401 })
965
+ })).status, 200);
966
+ const requests = recorder.getMeasurements("activitypub.collection.request");
967
+ assertEquals(requests.length, 1);
968
+ assertEquals(requests[0].type, "counter");
969
+ assertEquals(requests[0].value, 1);
970
+ assertEquals(requests[0].attributes["activitypub.collection.kind"], "followers");
971
+ assertEquals(requests[0].attributes["activitypub.collection.page"], false);
972
+ assertEquals(requests[0].attributes["fedify.collection.dispatcher"], "built_in");
973
+ assertEquals(requests[0].attributes["activitypub.collection.result"], "served");
974
+ assertEquals(requests[0].attributes["http.response.status_code"], 200);
975
+ assertEquals("activitypub.collection.id" in requests[0].attributes, false);
976
+ const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
977
+ assertEquals(durations.length, 1);
978
+ assertEquals(durations[0].type, "histogram");
979
+ assert(durations[0].value >= 0);
980
+ assertEquals(durations[0].attributes["activitypub.collection.result"], "served");
981
+ const items = recorder.getMeasurements("activitypub.collection.page.items");
982
+ assertEquals(items.length, 1);
983
+ assertEquals(items[0].type, "histogram");
984
+ assertEquals(items[0].value, 3);
985
+ assertEquals(items[0].attributes["activitypub.collection.page"], false);
986
+ const totalItems = recorder.getMeasurements("activitypub.collection.total_items");
987
+ assertEquals(totalItems.length, 1);
988
+ assertEquals(totalItems[0].type, "histogram");
989
+ assertEquals(totalItems[0].value, 3);
990
+ });
991
+ test("handleCollection() classifies camelCase collection metric names", async () => {
992
+ const [meterProvider, recorder] = createTestMeterProvider();
993
+ const context = createRequestContext({
994
+ federation: createFederation({
995
+ kv: new MemoryKvStore(),
996
+ meterProvider
997
+ }),
998
+ data: void 0,
999
+ url: new URL("https://example.com/users/someone/collections/featured"),
1000
+ request: new Request("https://example.com/users/someone/collections/featured", { headers: { Accept: "application/activity+json" } })
1001
+ });
1002
+ const dispatcher = () => ({ items: [] });
1003
+ assertEquals((await handleCollection(context.request, {
1004
+ context,
1005
+ name: "featuredTags",
1006
+ identifier: "someone",
1007
+ uriGetter(identifier) {
1008
+ return new URL(`https://example.com/users/${identifier}/collections/featured`);
1009
+ },
1010
+ collectionCallbacks: { dispatcher },
1011
+ meterProvider,
1012
+ onNotFound: () => new Response("Not found", { status: 404 }),
1013
+ onUnauthorized: () => new Response("Unauthorized", { status: 401 })
1014
+ })).status, 200);
1015
+ const requests = recorder.getMeasurements("activitypub.collection.request");
1016
+ assertEquals(requests.length, 1);
1017
+ assertEquals(requests[0].attributes["activitypub.collection.kind"], "featured_tags");
1018
+ });
1019
+ test("handleCollection() records filtered collection item metrics", async () => {
1020
+ const [meterProvider, recorder] = createTestMeterProvider();
1021
+ const context = createRequestContext({
1022
+ federation: createFederation({
1023
+ kv: new MemoryKvStore(),
1024
+ meterProvider
1025
+ }),
1026
+ data: void 0,
1027
+ url: new URL("https://example.com/users/someone/followers"),
1028
+ request: new Request("https://example.com/users/someone/followers", { headers: { Accept: "application/activity+json" } })
1029
+ });
1030
+ const dispatcher = () => ({ items: [new Create({ id: new URL("https://example.com/activities/1") }), new Create({ id: new URL("https://example.com/activities/2") })] });
1031
+ assertEquals((await handleCollection(context.request, {
1032
+ context,
1033
+ name: "followers",
1034
+ identifier: "someone",
1035
+ uriGetter(identifier) {
1036
+ return new URL(`https://example.com/users/${identifier}/followers`);
1037
+ },
1038
+ collectionCallbacks: { dispatcher },
1039
+ filterPredicate: (item) => item.id?.href === "https://example.com/activities/1",
1040
+ meterProvider,
1041
+ onNotFound: () => new Response("Not found", { status: 404 }),
1042
+ onUnauthorized: () => new Response("Unauthorized", { status: 401 })
1043
+ })).status, 200);
1044
+ const items = recorder.getMeasurements("activitypub.collection.page.items");
1045
+ assertEquals(items.length, 1);
1046
+ assertEquals(items[0].value, 1);
1047
+ assertEquals(items[0].attributes["activitypub.collection.page"], false);
1048
+ assertEquals(items[0].attributes["activitypub.collection.result"], "served");
1049
+ assertEquals(items[0].attributes["http.response.status_code"], 200);
1050
+ });
1051
+ test("handleCollection() records filtered collection page item metrics", async () => {
1052
+ const [meterProvider, recorder] = createTestMeterProvider();
1053
+ const context = createRequestContext({
1054
+ federation: createFederation({
1055
+ kv: new MemoryKvStore(),
1056
+ meterProvider
1057
+ }),
1058
+ data: void 0,
1059
+ url: new URL("https://example.com/users/someone/followers?cursor=2"),
1060
+ request: new Request("https://example.com/users/someone/followers?cursor=2", { headers: { Accept: "application/activity+json" } })
1061
+ });
1062
+ const dispatcher = () => ({ items: [new Create({ id: new URL("https://example.com/activities/1") }), new Create({ id: new URL("https://example.com/activities/2") })] });
1063
+ assertEquals((await handleCollection(context.request, {
1064
+ context,
1065
+ name: "followers",
1066
+ identifier: "someone",
1067
+ uriGetter(identifier) {
1068
+ return new URL(`https://example.com/users/${identifier}/followers`);
1069
+ },
1070
+ collectionCallbacks: { dispatcher },
1071
+ filterPredicate: (item) => item.id?.href === "https://example.com/activities/1",
1072
+ meterProvider,
1073
+ onNotFound: () => new Response("Not found", { status: 404 }),
1074
+ onUnauthorized: () => new Response("Unauthorized", { status: 401 })
1075
+ })).status, 200);
1076
+ const items = recorder.getMeasurements("activitypub.collection.page.items");
1077
+ assertEquals(items.length, 1);
1078
+ assertEquals(items[0].value, 1);
1079
+ assertEquals(items[0].attributes["activitypub.collection.page"], true);
1080
+ assertEquals(items[0].attributes["activitypub.collection.result"], "served");
1081
+ assertEquals(items[0].attributes["http.response.status_code"], 200);
1082
+ });
1083
+ test("handleCollection() records not_found collection metrics", async () => {
1084
+ const [meterProvider, recorder] = createTestMeterProvider();
1085
+ const context = createRequestContext({
1086
+ federation: createFederation({
1087
+ kv: new MemoryKvStore(),
1088
+ meterProvider
1089
+ }),
1090
+ data: void 0,
1091
+ url: new URL("https://example.com/users/nobody/outbox"),
1092
+ request: new Request("https://example.com/users/nobody/outbox", { headers: { Accept: "application/activity+json" } })
1093
+ });
1094
+ const dispatcher = () => null;
1095
+ assertEquals((await handleCollection(context.request, {
1096
+ context,
1097
+ name: "outbox",
1098
+ identifier: "nobody",
1099
+ uriGetter(identifier) {
1100
+ return new URL(`https://example.com/users/${identifier}/outbox`);
1101
+ },
1102
+ collectionCallbacks: { dispatcher },
1103
+ meterProvider,
1104
+ onNotFound: () => new Response("Not found", { status: 404 }),
1105
+ onUnauthorized: () => new Response("Unauthorized", { status: 401 })
1106
+ })).status, 404);
1107
+ const requests = recorder.getMeasurements("activitypub.collection.request");
1108
+ assertEquals(requests.length, 1);
1109
+ assertEquals(requests[0].attributes["activitypub.collection.kind"], "outbox");
1110
+ assertEquals(requests[0].attributes["activitypub.collection.result"], "not_found");
1111
+ assertEquals(requests[0].attributes["http.response.status_code"], 404);
1112
+ const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
1113
+ assertEquals(durations.length, 1);
1114
+ assertEquals(durations[0].attributes["activitypub.collection.result"], "not_found");
1115
+ assertEquals(recorder.getMeasurements("activitypub.collection.page.items").length, 0);
1116
+ });
931
1117
  test("handleInbox()", async () => {
932
1118
  const activity = new Create({
933
1119
  id: new URL("https://example.com/activities/1"),
@@ -3228,6 +3414,122 @@ test("handleCustomCollection()", async () => {
3228
3414
  assertEquals(onNotFoundCalled, null);
3229
3415
  assertEquals(onUnauthorizedCalled, null);
3230
3416
  });
3417
+ test("handleCustomCollection() records OpenTelemetry collection metrics", async () => {
3418
+ const [meterProvider, recorder] = createTestMeterProvider();
3419
+ const context = createRequestContext({
3420
+ federation: createFederation({
3421
+ kv: new MemoryKvStore(),
3422
+ meterProvider
3423
+ }),
3424
+ data: void 0,
3425
+ url: new URL("https://example.com/users/someone/custom"),
3426
+ request: new Request("https://example.com/users/someone/custom", { headers: { Accept: "application/activity+json" } })
3427
+ });
3428
+ const dispatcher = (_ctx, values) => values.identifier === "someone" ? { items: [new Create({ id: new URL("https://example.com/activities/1") }), new Create({ id: new URL("https://example.com/activities/2") })] } : null;
3429
+ const counter = (_ctx, values) => values.identifier === "someone" ? 2 : null;
3430
+ assertEquals((await handleCustomCollection(context.request, {
3431
+ context,
3432
+ name: "custom collection",
3433
+ values: { identifier: "someone" },
3434
+ collectionCallbacks: {
3435
+ dispatcher,
3436
+ counter
3437
+ },
3438
+ filterPredicate: (item) => item.id?.href === "https://example.com/activities/1",
3439
+ meterProvider,
3440
+ onNotFound: () => new Response("Not found", { status: 404 }),
3441
+ onUnauthorized: () => new Response("Unauthorized", { status: 401 })
3442
+ })).status, 200);
3443
+ const requests = recorder.getMeasurements("activitypub.collection.request");
3444
+ assertEquals(requests.length, 1);
3445
+ assertEquals(requests[0].attributes["activitypub.collection.kind"], "custom");
3446
+ assertEquals(requests[0].attributes["activitypub.collection.page"], false);
3447
+ assertEquals(requests[0].attributes["fedify.collection.dispatcher"], "custom");
3448
+ assertEquals(requests[0].attributes["activitypub.collection.result"], "served");
3449
+ assertEquals(requests[0].attributes["http.response.status_code"], 200);
3450
+ const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
3451
+ assertEquals(durations.length, 1);
3452
+ assertEquals(durations[0].attributes["fedify.collection.dispatcher"], "custom");
3453
+ assertEquals(durations[0].attributes["http.response.status_code"], 200);
3454
+ const items = recorder.getMeasurements("activitypub.collection.page.items");
3455
+ assertEquals(items.length, 1);
3456
+ assertEquals(items[0].value, 1);
3457
+ assertEquals(items[0].attributes["http.response.status_code"], 200);
3458
+ const totalItems = recorder.getMeasurements("activitypub.collection.total_items");
3459
+ assertEquals(totalItems.length, 1);
3460
+ assertEquals(totalItems[0].value, 2);
3461
+ assertEquals(totalItems[0].attributes["http.response.status_code"], 200);
3462
+ });
3463
+ test("handleCustomCollection() records not_found status on dispatch metrics", async () => {
3464
+ const [meterProvider, recorder] = createTestMeterProvider();
3465
+ const context = createRequestContext({
3466
+ federation: createFederation({
3467
+ kv: new MemoryKvStore(),
3468
+ meterProvider
3469
+ }),
3470
+ data: void 0,
3471
+ url: new URL("https://example.com/users/nobody/custom"),
3472
+ request: new Request("https://example.com/users/nobody/custom", { headers: { Accept: "application/activity+json" } })
3473
+ });
3474
+ const dispatcher = () => null;
3475
+ assertEquals((await handleCustomCollection(context.request, {
3476
+ context,
3477
+ name: "custom collection",
3478
+ values: { identifier: "nobody" },
3479
+ collectionCallbacks: { dispatcher },
3480
+ meterProvider,
3481
+ onNotFound: () => new Response("Not found", { status: 404 }),
3482
+ onUnauthorized: () => new Response("Unauthorized", { status: 401 })
3483
+ })).status, 404);
3484
+ const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
3485
+ assertEquals(durations.length, 1);
3486
+ assertEquals(durations[0].attributes["activitypub.collection.result"], "not_found");
3487
+ assertEquals(durations[0].attributes["http.response.status_code"], 404);
3488
+ });
3489
+ test("handleCustomCollection() classifies deferred collection metrics as error", async () => {
3490
+ const [meterProvider, recorder] = createTestMeterProvider();
3491
+ const context = createRequestContext({
3492
+ federation: createFederation({
3493
+ kv: new MemoryKvStore(),
3494
+ meterProvider
3495
+ }),
3496
+ data: void 0,
3497
+ url: new URL("https://example.com/users/someone/custom"),
3498
+ request: new Request("https://example.com/users/someone/custom", { headers: { Accept: "application/activity+json" } })
3499
+ });
3500
+ const brokenActivity = new Create({ id: new URL("https://example.com/activities/1") });
3501
+ globalThis.Object.defineProperty(brokenActivity, "toJsonLd", { value: () => {
3502
+ throw new Error("serialization failed");
3503
+ } });
3504
+ const dispatcher = () => ({ items: [brokenActivity] });
3505
+ const counter = () => 1;
3506
+ await assertRejects(() => handleCustomCollection(context.request, {
3507
+ context,
3508
+ name: "custom collection",
3509
+ values: { identifier: "someone" },
3510
+ collectionCallbacks: {
3511
+ dispatcher,
3512
+ counter
3513
+ },
3514
+ meterProvider,
3515
+ onNotFound: () => new Response("Not found", { status: 404 }),
3516
+ onUnauthorized: () => new Response("Unauthorized", { status: 401 })
3517
+ }), Error, "serialization failed");
3518
+ const requests = recorder.getMeasurements("activitypub.collection.request");
3519
+ assertEquals(requests.length, 1);
3520
+ assertEquals(requests[0].attributes["activitypub.collection.result"], "error");
3521
+ const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
3522
+ assertEquals(durations.length, 1);
3523
+ assertEquals(durations[0].attributes["activitypub.collection.result"], "error");
3524
+ const items = recorder.getMeasurements("activitypub.collection.page.items");
3525
+ assertEquals(items.length, 1);
3526
+ assertEquals(items[0].value, 1);
3527
+ assertEquals(items[0].attributes["activitypub.collection.result"], "error");
3528
+ const totalItems = recorder.getMeasurements("activitypub.collection.total_items");
3529
+ assertEquals(totalItems.length, 1);
3530
+ assertEquals(totalItems[0].value, 1);
3531
+ assertEquals(totalItems[0].attributes["activitypub.collection.result"], "error");
3532
+ });
3231
3533
  test("handleInbox() records OpenTelemetry span events", async () => {
3232
3534
  const [tracerProvider, exporter] = createTestTracerProvider();
3233
3535
  const [meterProvider, recorder] = createTestMeterProvider();
@@ -4,9 +4,9 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import "../std__assert-BBjXFNOb.mjs";
6
6
  import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-C3kae-6B.mjs";
7
- import { r as signObject } from "../proof-42Q9NiqN.mjs";
7
+ import { r as signObject } from "../proof-1XBgQ0Z0.mjs";
8
8
  import { t as MemoryKvStore } from "../kv-x2IvBUyq.mjs";
9
- import { o as createFederation } from "../middleware-xtTRaiJL.mjs";
9
+ import { o as createFederation } from "../middleware-DHM2Pjqf.mjs";
10
10
  import { Create, Follow, Person } from "@fedify/vocab";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
12
  //#region src/federation/idempotency.test.ts
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
5
  import "../std__assert-BBjXFNOb.mjs";
6
6
  import { t as assertRejects } from "../assert_rejects-DN60FHPX.mjs";
7
- import { a as instrumentDocumentLoader, c as recordDocumentCache, d as recordInboxActivity, f as recordKeyLookup, h as recordWebFingerHandle, l as recordDocumentFetch, m as recordOutboxEnqueue, p as recordOutboxActivity, t as classifyFetchError, u as recordFanoutRecipients } from "../metrics-oMUWaw6W.mjs";
7
+ import { _ as recordOutboxActivity, a as instrumentDocumentLoader, c as recordCollectionDispatchDuration, d as recordCollectionTotalItems, f as recordDocumentCache, g as recordKeyLookup, h as recordInboxActivity, l as recordCollectionPageItems, m as recordFanoutRecipients, p as recordDocumentFetch, t as classifyFetchError, u as recordCollectionRequest, v as recordOutboxEnqueue, y as recordWebFingerHandle } from "../metrics-CKticT28.mjs";
8
8
  import { createTestMeterProvider, test } from "@fedify/fixture";
9
9
  import { FetchError } from "@fedify/vocab-runtime";
10
10
  //#region src/federation/metrics.test.ts
@@ -272,6 +272,85 @@ test("recordWebFingerHandle() omits optional attributes when not provided", () =
272
272
  assertEquals("webfinger.resource.scheme" in (counter?.attributes ?? {}), false);
273
273
  assertEquals("http.response.status_code" in (counter?.attributes ?? {}), false);
274
274
  });
275
+ test("recordCollectionRequest() records counter with bounded attributes", () => {
276
+ const [meterProvider, recorder] = createTestMeterProvider();
277
+ recordCollectionRequest(meterProvider, {
278
+ kind: "followers",
279
+ page: true,
280
+ dispatcher: "built_in",
281
+ result: "served",
282
+ statusCode: 200
283
+ });
284
+ const counter = recorder.getMeasurement("activitypub.collection.request");
285
+ assertEquals(counter?.type, "counter");
286
+ assertEquals(counter?.value, 1);
287
+ assertEquals(counter?.attributes["activitypub.collection.kind"], "followers");
288
+ assertEquals(counter?.attributes["activitypub.collection.page"], true);
289
+ assertEquals(counter?.attributes["fedify.collection.dispatcher"], "built_in");
290
+ assertEquals(counter?.attributes["activitypub.collection.result"], "served");
291
+ assertEquals(counter?.attributes["http.response.status_code"], 200);
292
+ });
293
+ test("recordCollectionRequest() omits status code when unavailable", () => {
294
+ const [meterProvider, recorder] = createTestMeterProvider();
295
+ recordCollectionRequest(meterProvider, {
296
+ kind: "custom",
297
+ page: false,
298
+ dispatcher: "custom",
299
+ result: "error"
300
+ });
301
+ const counter = recorder.getMeasurement("activitypub.collection.request");
302
+ assertEquals(counter?.attributes["activitypub.collection.kind"], "custom");
303
+ assertEquals(counter?.attributes["activitypub.collection.page"], false);
304
+ assertEquals(counter?.attributes["fedify.collection.dispatcher"], "custom");
305
+ assertEquals(counter?.attributes["activitypub.collection.result"], "error");
306
+ assertEquals("http.response.status_code" in (counter?.attributes ?? {}), false);
307
+ });
308
+ test("recordCollectionDispatchDuration() records histogram", () => {
309
+ const [meterProvider, recorder] = createTestMeterProvider();
310
+ recordCollectionDispatchDuration(meterProvider, 12, {
311
+ kind: "outbox",
312
+ page: false,
313
+ dispatcher: "built_in",
314
+ result: "served"
315
+ });
316
+ const duration = recorder.getMeasurement("activitypub.collection.dispatch.duration");
317
+ assertEquals(duration?.type, "histogram");
318
+ assertEquals(duration?.value, 12);
319
+ assertEquals(duration?.attributes["activitypub.collection.kind"], "outbox");
320
+ assertEquals(duration?.attributes["activitypub.collection.page"], false);
321
+ assertEquals(duration?.attributes["fedify.collection.dispatcher"], "built_in");
322
+ assertEquals(duration?.attributes["activitypub.collection.result"], "served");
323
+ });
324
+ test("recordCollectionPageItems() records item count histogram", () => {
325
+ const [meterProvider, recorder] = createTestMeterProvider();
326
+ recordCollectionPageItems(meterProvider, 3, {
327
+ kind: "featured_tags",
328
+ page: true,
329
+ dispatcher: "built_in",
330
+ result: "served",
331
+ statusCode: 200
332
+ });
333
+ const items = recorder.getMeasurement("activitypub.collection.page.items");
334
+ assertEquals(items?.type, "histogram");
335
+ assertEquals(items?.value, 3);
336
+ assertEquals(items?.attributes["activitypub.collection.kind"], "featured_tags");
337
+ assertEquals(items?.attributes["activitypub.collection.page"], true);
338
+ assertEquals(items?.attributes["http.response.status_code"], 200);
339
+ });
340
+ test("recordCollectionTotalItems() records total item histogram", () => {
341
+ const [meterProvider, recorder] = createTestMeterProvider();
342
+ recordCollectionTotalItems(meterProvider, 42, {
343
+ kind: "liked",
344
+ page: false,
345
+ dispatcher: "built_in",
346
+ result: "served"
347
+ });
348
+ const total = recorder.getMeasurement("activitypub.collection.total_items");
349
+ assertEquals(total?.type, "histogram");
350
+ assertEquals(total?.value, 42);
351
+ assertEquals(total?.attributes["activitypub.collection.kind"], "liked");
352
+ assertEquals(total?.attributes["activitypub.collection.page"], false);
353
+ });
275
354
  test("classifyFetchError() classifies FetchError with 404 as not_found", () => {
276
355
  assertEquals(classifyFetchError(new FetchError("https://example.com/k", "not found", new Response("", { status: 404 }))), {
277
356
  result: "not_found",
@@ -10,14 +10,14 @@ import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs";
10
10
  import { t as assertStrictEquals } from "../assert_strict_equals-XEgZAlrj.mjs";
11
11
  import { t as assert } from "../assert-OguE97r2.mjs";
12
12
  import { t as esm_default } from "../esm-BQRw925N.mjs";
13
- import { l as verifyRequest, s as signRequest } from "../http-BoRhhcgB.mjs";
13
+ import { l as verifyRequest, s as signRequest } from "../http--aE0vk2u.mjs";
14
14
  import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-C3kae-6B.mjs";
15
- import { t as getAuthenticatedDocumentLoader } from "../docloader-Ct8PhKFS.mjs";
16
- import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-B8wjsKDJ.mjs";
17
- import { t as doesActorOwnKey } from "../owner-BxjgK8PG.mjs";
18
- import { i as verifyObject, r as signObject } from "../proof-42Q9NiqN.mjs";
15
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-Bv4TW6eo.mjs";
16
+ import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-xVq6y31b.mjs";
17
+ import { t as doesActorOwnKey } from "../owner-DmU2qEh_.mjs";
18
+ import { i as verifyObject, r as signObject } from "../proof-1XBgQ0Z0.mjs";
19
19
  import { t as MemoryKvStore } from "../kv-x2IvBUyq.mjs";
20
- import { i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-xtTRaiJL.mjs";
20
+ import { i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-DHM2Pjqf.mjs";
21
21
  import { configure, reset } from "@logtape/logtape";
22
22
  import { RouterError } from "@fedify/uri-template";
23
23
  import * as vocab from "@fedify/vocab";
@@ -1074,6 +1074,20 @@ test("Federation.fetch() records HTTP server request metrics", async (t) => {
1074
1074
  assertEquals(counts[0].attributes["http.response.status_code"], 406);
1075
1075
  assertEquals(counts[0].attributes["fedify.route.template"], "/users/{identifier}");
1076
1076
  });
1077
+ await t.step("records collection metrics for not_acceptable collection requests", async () => {
1078
+ const { federation, recorder } = createTestContext();
1079
+ assertEquals((await federation.fetch(new Request("https://example.com/users/alice/followers", {
1080
+ method: "GET",
1081
+ headers: { "Accept": "text/html" }
1082
+ }), { contextData: void 0 })).status, 406);
1083
+ const requests = recorder.getMeasurements("activitypub.collection.request");
1084
+ assertEquals(requests.length, 1);
1085
+ assertEquals(requests[0].attributes["activitypub.collection.kind"], "followers");
1086
+ assertEquals(requests[0].attributes["activitypub.collection.page"], false);
1087
+ assertEquals(requests[0].attributes["fedify.collection.dispatcher"], "built_in");
1088
+ assertEquals(requests[0].attributes["activitypub.collection.result"], "not_acceptable");
1089
+ assertEquals(requests[0].attributes["http.response.status_code"], 406);
1090
+ });
1077
1091
  await t.step("records thrown errors after classification with the matched endpoint", async () => {
1078
1092
  const { federation, recorder } = createTestContext();
1079
1093
  await assertRejects(() => federation.fetch(new Request("https://example.com/users/boom", {
@@ -2,7 +2,7 @@ const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  require("../chunk-DDcVe30Y.cjs");
5
- const require_middleware = require("../middleware-CcJyVEpv.cjs");
5
+ const require_middleware = require("../middleware-hxnyAewn.cjs");
6
6
  let _logtape_logtape = require("@logtape/logtape");
7
7
  let _fedify_uri_template = require("@fedify/uri-template");
8
8
  let es_toolkit = require("es-toolkit");
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { a as createExponentialBackoffPolicy, c as buildCollectionSynchronizationHeader, i as SendActivityError, l as digest, o as respondWithObject, r as handleWebFinger, s as respondWithObjectIfAcceptable, t as createFederation, u as createFederationBuilder } from "../middleware-DwZ1ofL9.js";
3
+ import { a as createExponentialBackoffPolicy, c as buildCollectionSynchronizationHeader, i as SendActivityError, l as digest, o as respondWithObject, r as handleWebFinger, s as respondWithObjectIfAcceptable, t as createFederation, u as createFederationBuilder } from "../middleware-BSuEI4Qf.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { Router as Router$1, RouterError as RouterError$1, assertPath, isPath } from "@fedify/uri-template";
6
6
  import { isEqual } from "es-toolkit";
@@ -9,10 +9,10 @@ import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs";
9
9
  import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs";
10
10
  import { t as assert } from "../assert-OguE97r2.mjs";
11
11
  import { t as esm_default } from "../esm-BQRw925N.mjs";
12
- import { l as verifyRequest } from "../http-BoRhhcgB.mjs";
12
+ import { l as verifyRequest } from "../http--aE0vk2u.mjs";
13
13
  import { i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-C3kae-6B.mjs";
14
- import { t as doesActorOwnKey } from "../owner-BxjgK8PG.mjs";
15
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-R1_K46CH.mjs";
14
+ import { t as doesActorOwnKey } from "../owner-DmU2qEh_.mjs";
15
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "../send-CmtB8w5D.mjs";
16
16
  import { Activity, Application, Endpoints, Group, Person, Service } from "@fedify/vocab";
17
17
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
18
18
  //#region ../../node_modules/.pnpm/@opentelemetry+sdk-metrics@2.7.1_@opentelemetry+api@1.9.1/node_modules/@opentelemetry/sdk-metrics/build/src/export/AggregationTemporality.js
@@ -4,7 +4,7 @@ globalThis.addEventListener = () => {};
4
4
  import "../std__assert-BBjXFNOb.mjs";
5
5
  import { r as assertFalse } from "../assert_rejects-DN60FHPX.mjs";
6
6
  import { t as assert } from "../assert-OguE97r2.mjs";
7
- import { t as hasMalformedKnownTemporalLiteral } from "../temporal-8kDX3E4q.mjs";
7
+ import { t as hasMalformedKnownTemporalLiteral } from "../temporal-DE9_a2nI.mjs";
8
8
  import { test } from "@fedify/fixture";
9
9
  //#region src/federation/temporal.test.ts
10
10
  test("hasMalformedKnownTemporalLiteral() detects expanded proof timestamps", async () => {
@@ -6,7 +6,7 @@ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
6
6
  import "../std__assert-BBjXFNOb.mjs";
7
7
  import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs";
8
8
  import { t as MemoryKvStore } from "../kv-x2IvBUyq.mjs";
9
- import { o as createFederation, s as handleWebFinger } from "../middleware-xtTRaiJL.mjs";
9
+ import { o as createFederation, s as handleWebFinger } from "../middleware-DHM2Pjqf.mjs";
10
10
  import { Image, Link, Person, Tombstone } from "@fedify/vocab";
11
11
  import { createTestMeterProvider, test } from "@fedify/fixture";
12
12
  //#region src/federation/webfinger.test.ts
@@ -1,10 +1,10 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-O_rwum1q.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-oMUWaw6W.mjs";
4
+ import { n as version, t as name } from "./deno-Cb_y5qEi.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CKticT28.mjs";
6
6
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CceiKpCy.mjs";
7
- import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-DyATZSWG.mjs";
7
+ import { o as validateCryptoKey, r as fetchKeyDetailed } from "./key-Cl_bixZo.mjs";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
  import { CryptographicKey } from "@fedify/vocab";
10
10
  import { SpanStatusCode, trace } from "@opentelemetry/api";