@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.37

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 (138) hide show
  1. package/README.md +16 -7
  2. package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
  3. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
  4. package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
  5. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
  6. package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
  7. package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
  8. package/dist/{builder-yAlpiIqP.mjs → builder-Dl5Sdmc9.mjs} +96 -42
  9. package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
  10. package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
  11. package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
  12. package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
  13. package/dist/compat/mod.d.cts +2 -1
  14. package/dist/compat/mod.d.ts +2 -3
  15. package/dist/compat/mod.js +3 -3
  16. package/dist/compat/outgoing-jsonld.test.mjs +4 -4
  17. package/dist/compat/public-audience.test.mjs +4 -4
  18. package/dist/compat/transformers.test.mjs +5 -5
  19. package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
  20. package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
  21. package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
  22. package/dist/{deno-XBVlKnOX.mjs → deno-BID9qTPk.mjs} +1 -1
  23. package/dist/{docloader-DPp-X6gk.mjs → docloader-BYFuVwwi.mjs} +4 -4
  24. package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
  25. package/dist/federation/builder.test.mjs +163 -11
  26. package/dist/federation/circuit-breaker.test.d.mts +2 -0
  27. package/dist/federation/circuit-breaker.test.mjs +446 -0
  28. package/dist/federation/collection.test.mjs +3 -3
  29. package/dist/federation/handler.test.mjs +1770 -29
  30. package/dist/federation/idempotency.test.mjs +5 -5
  31. package/dist/federation/inbox.test.mjs +4 -4
  32. package/dist/federation/keycache.test.mjs +5 -5
  33. package/dist/federation/kv.test.mjs +2 -2
  34. package/dist/federation/metrics.test.d.mts +2 -0
  35. package/dist/federation/metrics.test.mjs +634 -0
  36. package/dist/federation/middleware.test.mjs +4036 -146
  37. package/dist/federation/mod.cjs +195 -14
  38. package/dist/federation/mod.d.cts +6 -4
  39. package/dist/federation/mod.d.ts +6 -6
  40. package/dist/federation/mod.js +192 -14
  41. package/dist/federation/mq.test.mjs +176 -15
  42. package/dist/federation/negotiation.test.mjs +4 -4
  43. package/dist/federation/retry.test.mjs +3 -3
  44. package/dist/federation/router.test.mjs +190 -9
  45. package/dist/federation/send.test.mjs +153 -15
  46. package/dist/federation/temporal.test.d.mts +2 -0
  47. package/dist/federation/temporal.test.mjs +71 -0
  48. package/dist/federation/webfinger.test.mjs +150 -5
  49. package/dist/{http-B4rrzxtg.js → http-B47Jg9iX.js} +994 -64
  50. package/dist/{http-CSlLA54v.mjs → http-CO59u1Yk.mjs} +146 -47
  51. package/dist/{http-BrGqJDXL.cjs → http-CjZwUPiT.cjs} +1099 -61
  52. package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
  53. package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
  54. package/dist/{key-Bzx--sHD.mjs → key-Dqdlo3mG.mjs} +43 -18
  55. package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
  56. package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BQ1-zDKX.cjs} +20 -3
  57. package/dist/{kv-cache-EMIqoIuB.js → kv-cache-Dh2ZH9Gr.js} +20 -3
  58. package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-DhLkLv0P.mjs} +21 -3
  59. package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
  60. package/dist/ld-JbCByP5P.mjs +626 -0
  61. package/dist/metrics-ColY3QKj.mjs +815 -0
  62. package/dist/{middleware-zjZ6AFCW.mjs → middleware-Buqzuiaf.mjs} +1 -1
  63. package/dist/{middleware-An4DjES4.cjs → middleware-CWQ3Yi07.cjs} +2318 -623
  64. package/dist/{middleware-K1g6bEkV.mjs → middleware-Cg4EyWsq.mjs} +1639 -373
  65. package/dist/{middleware-nidH7VZH.js → middleware-iAJY2aU-.js} +2260 -556
  66. package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
  67. package/dist/mod-C0F6kvgS.d.cts +182 -0
  68. package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
  69. package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
  70. package/dist/mod-vPYVoa5n.d.ts +182 -0
  71. package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
  72. package/dist/mod.cjs +9 -6
  73. package/dist/mod.d.cts +12 -10
  74. package/dist/mod.d.ts +12 -12
  75. package/dist/mod.js +11 -11
  76. package/dist/mq-D-nlpY04.d.ts +208 -0
  77. package/dist/mq-D8uSFzxe.d.cts +208 -0
  78. package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
  79. package/dist/nodeinfo/client.test.mjs +4 -4
  80. package/dist/nodeinfo/handler.test.mjs +4 -4
  81. package/dist/nodeinfo/mod.d.cts +2 -1
  82. package/dist/nodeinfo/mod.d.ts +2 -3
  83. package/dist/nodeinfo/mod.js +3 -3
  84. package/dist/nodeinfo/types.test.mjs +3 -3
  85. package/dist/otel/exporter.test.mjs +28 -25
  86. package/dist/otel/mod.cjs +6 -5
  87. package/dist/otel/mod.d.cts +5 -3
  88. package/dist/otel/mod.d.ts +5 -5
  89. package/dist/otel/mod.js +8 -7
  90. package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
  91. package/dist/{owner-C-tqfvxn.mjs → owner-B4LgOmzV.mjs} +2 -2
  92. package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
  93. package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
  94. package/dist/{proof-BlmRPzrK.mjs → proof-BoSGOq9q.mjs} +26 -8
  95. package/dist/{proof-Bbbwf8_x.js → proof-CzhZuawt.js} +378 -15
  96. package/dist/{proof-BW89QMVB.cjs → proof-DvMI-3vq.cjs} +432 -15
  97. package/dist/runtime/mod.d.cts +1 -0
  98. package/dist/runtime/mod.d.ts +1 -2
  99. package/dist/runtime/mod.js +3 -3
  100. package/dist/{send-05pJLcb6.mjs → send-ByK1Q7DK.mjs} +72 -26
  101. package/dist/sig/http.test.mjs +356 -33
  102. package/dist/sig/key.test.mjs +103 -6
  103. package/dist/sig/ld.test.mjs +696 -7
  104. package/dist/sig/mod.cjs +2 -2
  105. package/dist/sig/mod.d.cts +4 -3
  106. package/dist/sig/mod.d.ts +4 -5
  107. package/dist/sig/mod.js +4 -4
  108. package/dist/sig/owner.test.mjs +6 -6
  109. package/dist/sig/proof.test.mjs +169 -8
  110. package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
  111. package/dist/temporal-LpjyAjKb.mjs +95 -0
  112. package/dist/testing/mod.d.mts +110 -10
  113. package/dist/testing/mod.mjs +1 -2
  114. package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
  115. package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
  116. package/dist/utils/docloader.test.mjs +6 -6
  117. package/dist/utils/kv-cache.test.mjs +67 -2
  118. package/dist/utils/mod.cjs +1 -1
  119. package/dist/utils/mod.d.cts +2 -1
  120. package/dist/utils/mod.d.ts +2 -3
  121. package/dist/utils/mod.js +3 -3
  122. package/dist/vocab/cjs.test.mjs +1 -1
  123. package/dist/vocab/mod.d.cts +1 -0
  124. package/dist/vocab/mod.d.ts +1 -2
  125. package/dist/vocab/mod.js +2 -2
  126. package/package.json +16 -16
  127. package/dist/ld-DR78beiv.mjs +0 -279
  128. package/dist/middleware-BFBaR0mF.cjs +0 -4
  129. package/dist/mod-2d12ffz3.d.ts +0 -64
  130. package/dist/mod-D35TRn09.d.cts +0 -62
  131. package/dist/router-CrMLXoOr.mjs +0 -114
  132. package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
  133. package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
  134. package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
  135. package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
  136. package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
  137. package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
  138. /package/dist/{retry-bMXBL97A.mjs → retry-CXg_MBI-.mjs} +0 -0
@@ -0,0 +1,634 @@
1
+ import "@js-temporal/polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
5
+ import "../std__assert-BBjXFNOb.mjs";
6
+ import { t as assertRejects } from "../assert_rejects-DN60FHPX.mjs";
7
+ 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-ColY3QKj.mjs";
8
+ import { createTestMeterProvider, test } from "@fedify/fixture";
9
+ import { FetchError } from "@fedify/vocab-runtime";
10
+ import { DataPointType, MeterProvider, MetricReader } from "@opentelemetry/sdk-metrics";
11
+ //#region src/federation/metrics.test.ts
12
+ var TestMetricReader = class extends MetricReader {
13
+ onShutdown() {
14
+ return Promise.resolve();
15
+ }
16
+ onForceFlush() {
17
+ return Promise.resolve();
18
+ }
19
+ };
20
+ const noopQueue = {
21
+ enqueue() {
22
+ return Promise.resolve();
23
+ },
24
+ listen() {
25
+ return Promise.resolve();
26
+ }
27
+ };
28
+ test("getRemoteHost() includes non-default ports", () => {
29
+ assertEquals(getRemoteHost(new URL("https://example.com/inbox")), "example.com");
30
+ assertEquals(getRemoteHost(new URL("https://example.com:8443/inbox")), "example.com:8443");
31
+ assertEquals(getRemoteHost(new URL("https://example.com:443/inbox")), "example.com");
32
+ });
33
+ test("recordFanoutRecipients() records the recipient count with activity type", () => {
34
+ const [meterProvider, recorder] = createTestMeterProvider();
35
+ recordFanoutRecipients(meterProvider, 7, "https://www.w3.org/ns/activitystreams#Create");
36
+ const measurements = recorder.getMeasurements("activitypub.fanout.recipients");
37
+ assertEquals(measurements.length, 1);
38
+ assertEquals(measurements[0].type, "histogram");
39
+ assertEquals(measurements[0].value, 7);
40
+ assertEquals(measurements[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
41
+ });
42
+ test("recordFanoutRecipients() omits activity type when unknown", () => {
43
+ const [meterProvider, recorder] = createTestMeterProvider();
44
+ recordFanoutRecipients(meterProvider, 0);
45
+ const measurements = recorder.getMeasurements("activitypub.fanout.recipients");
46
+ assertEquals(measurements.length, 1);
47
+ assertEquals(measurements[0].value, 0);
48
+ assertEquals("activitypub.activity.type" in measurements[0].attributes, false);
49
+ });
50
+ test("signature verification duration uses explicit low-latency buckets", async () => {
51
+ const reader = new TestMetricReader();
52
+ const meterProvider = new MeterProvider({ readers: [reader] });
53
+ try {
54
+ getFederationMetrics(meterProvider).recordSignatureVerificationDuration(7, "http", "verified");
55
+ const metric = (await reader.collect()).resourceMetrics.scopeMetrics.flatMap((scope) => scope.metrics).find((metric) => metric.descriptor.name === "activitypub.signature.verification.duration");
56
+ assertEquals(metric?.dataPointType, DataPointType.HISTOGRAM);
57
+ assertEquals(metric?.dataPoints[0].value.buckets.boundaries, [
58
+ .1,
59
+ .25,
60
+ .5,
61
+ 1,
62
+ 2.5,
63
+ 5,
64
+ 10,
65
+ 25,
66
+ 50,
67
+ 100,
68
+ 250,
69
+ 500,
70
+ 1e3
71
+ ]);
72
+ } finally {
73
+ await meterProvider.shutdown();
74
+ }
75
+ });
76
+ test("registerQueueDepthGauge() skips unavailable depth snapshots", async () => {
77
+ const reader = new TestMetricReader();
78
+ const meterProvider = new MeterProvider({ readers: [reader] });
79
+ try {
80
+ registerQueueDepthGauge(meterProvider, [
81
+ {
82
+ role: "inbox",
83
+ queue: {
84
+ enqueue() {
85
+ return Promise.resolve();
86
+ },
87
+ listen() {
88
+ return Promise.resolve();
89
+ },
90
+ getDepth() {
91
+ throw new TypeError("backend unavailable");
92
+ }
93
+ }
94
+ },
95
+ {
96
+ role: "outbox",
97
+ queue: {
98
+ enqueue() {
99
+ return Promise.resolve();
100
+ },
101
+ listen() {
102
+ return Promise.resolve();
103
+ },
104
+ getDepth() {
105
+ return Promise.resolve(null);
106
+ }
107
+ }
108
+ },
109
+ {
110
+ role: "fanout",
111
+ queue: {
112
+ enqueue() {
113
+ return Promise.resolve();
114
+ },
115
+ listen() {
116
+ return Promise.resolve();
117
+ },
118
+ getDepth() {
119
+ return Promise.resolve({ queued: 7 });
120
+ }
121
+ }
122
+ }
123
+ ]);
124
+ const result = await reader.collect();
125
+ assertEquals(result.errors, []);
126
+ const queueDepth = result.resourceMetrics.scopeMetrics.flatMap((scope) => scope.metrics).find((metric) => metric.descriptor.name === "fedify.queue.depth");
127
+ assertEquals(queueDepth?.dataPointType, DataPointType.GAUGE);
128
+ assertEquals(queueDepth?.dataPoints.map((point) => ({
129
+ state: point.attributes["fedify.queue.depth.state"],
130
+ role: point.attributes["fedify.queue.role"],
131
+ value: point.value
132
+ })), [{
133
+ state: "queued",
134
+ role: "fanout",
135
+ value: 7
136
+ }]);
137
+ } finally {
138
+ await meterProvider.shutdown();
139
+ }
140
+ });
141
+ test("registerQueueDepthGauge() queries queue depths in parallel", async () => {
142
+ const reader = new TestMetricReader();
143
+ const meterProvider = new MeterProvider({ readers: [reader] });
144
+ let releaseSlowDepth;
145
+ let fastDepthStarted = false;
146
+ try {
147
+ registerQueueDepthGauge(meterProvider, [{
148
+ role: "inbox",
149
+ queue: {
150
+ enqueue() {
151
+ return Promise.resolve();
152
+ },
153
+ listen() {
154
+ return Promise.resolve();
155
+ },
156
+ getDepth() {
157
+ return new Promise((resolve) => {
158
+ releaseSlowDepth = resolve;
159
+ });
160
+ }
161
+ }
162
+ }, {
163
+ role: "outbox",
164
+ queue: {
165
+ enqueue() {
166
+ return Promise.resolve();
167
+ },
168
+ listen() {
169
+ return Promise.resolve();
170
+ },
171
+ getDepth() {
172
+ fastDepthStarted = true;
173
+ return Promise.resolve({ queued: 5 });
174
+ }
175
+ }
176
+ }]);
177
+ const collection = reader.collect();
178
+ await Promise.resolve();
179
+ assertEquals(fastDepthStarted, true);
180
+ releaseSlowDepth?.({ queued: 3 });
181
+ 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]);
182
+ } finally {
183
+ releaseSlowDepth?.({ queued: 0 });
184
+ await meterProvider.shutdown();
185
+ }
186
+ });
187
+ test("recordInboxActivity() records counter with result and activity type", () => {
188
+ const [meterProvider, recorder] = createTestMeterProvider();
189
+ for (const result of [
190
+ "queued",
191
+ "processed",
192
+ "retried",
193
+ "rejected",
194
+ "abandoned"
195
+ ]) recordInboxActivity(meterProvider, result, "https://www.w3.org/ns/activitystreams#Follow");
196
+ const measurements = recorder.getMeasurements("activitypub.inbox.activity");
197
+ assertEquals(measurements.length, 5);
198
+ for (const m of measurements) {
199
+ assertEquals(m.type, "counter");
200
+ assertEquals(m.value, 1);
201
+ assertEquals(m.attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Follow");
202
+ }
203
+ assertEquals(measurements.map((m) => m.attributes["activitypub.processing.result"]), [
204
+ "queued",
205
+ "processed",
206
+ "retried",
207
+ "rejected",
208
+ "abandoned"
209
+ ]);
210
+ });
211
+ test("recordInboxActivity() omits activity type when unknown", () => {
212
+ const [meterProvider, recorder] = createTestMeterProvider();
213
+ recordInboxActivity(meterProvider, "rejected");
214
+ const measurements = recorder.getMeasurements("activitypub.inbox.activity");
215
+ assertEquals(measurements.length, 1);
216
+ assertEquals(measurements[0].attributes["activitypub.processing.result"], "rejected");
217
+ assertEquals("activitypub.activity.type" in measurements[0].attributes, false);
218
+ });
219
+ test("recordOutboxEnqueue() also records activitypub.outbox.activity{queued} on initial enqueue", () => {
220
+ const [meterProvider, recorder] = createTestMeterProvider();
221
+ recordOutboxEnqueue(meterProvider, noopQueue, {
222
+ activityType: "https://www.w3.org/ns/activitystreams#Create",
223
+ attempt: 0
224
+ });
225
+ const queued = recorder.getMeasurements("activitypub.outbox.activity");
226
+ assertEquals(queued.length, 1);
227
+ assertEquals(queued[0].type, "counter");
228
+ assertEquals(queued[0].attributes["activitypub.processing.result"], "queued");
229
+ assertEquals(queued[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
230
+ });
231
+ test("recordOutboxEnqueue() does not record outbox.activity{queued} on retry enqueues", () => {
232
+ const [meterProvider, recorder] = createTestMeterProvider();
233
+ recordOutboxEnqueue(meterProvider, noopQueue, {
234
+ activityType: "https://www.w3.org/ns/activitystreams#Create",
235
+ attempt: 1
236
+ });
237
+ assertEquals(recorder.getMeasurements("activitypub.outbox.activity").length, 0);
238
+ });
239
+ test("recordOutboxActivity() records counter with result and activity type", () => {
240
+ const [meterProvider, recorder] = createTestMeterProvider();
241
+ for (const result of [
242
+ "queued",
243
+ "retried",
244
+ "abandoned"
245
+ ]) recordOutboxActivity(meterProvider, result, "https://www.w3.org/ns/activitystreams#Announce");
246
+ const measurements = recorder.getMeasurements("activitypub.outbox.activity");
247
+ assertEquals(measurements.length, 3);
248
+ for (const m of measurements) {
249
+ assertEquals(m.type, "counter");
250
+ assertEquals(m.value, 1);
251
+ assertEquals(m.attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Announce");
252
+ }
253
+ assertEquals(measurements.map((m) => m.attributes["activitypub.processing.result"]), [
254
+ "queued",
255
+ "retried",
256
+ "abandoned"
257
+ ]);
258
+ });
259
+ test("recordCircuitBreakerStateChange() records counter with bounded attributes", () => {
260
+ const [meterProvider, recorder] = createTestMeterProvider();
261
+ recordCircuitBreakerStateChange(meterProvider, "remote.example", "half_open");
262
+ const measurements = recorder.getMeasurements("activitypub.circuit_breaker.state_change");
263
+ assertEquals(measurements.length, 1);
264
+ assertEquals(measurements[0].type, "counter");
265
+ assertEquals(measurements[0].value, 1);
266
+ assertEquals(measurements[0].attributes["activitypub.remote.host"], "remote.example");
267
+ assertEquals(measurements[0].attributes["activitypub.circuit_breaker.state"], "half_open");
268
+ });
269
+ test("recordKeyLookup() records counter and duration with all attributes", () => {
270
+ const [meterProvider, recorder] = createTestMeterProvider();
271
+ recordKeyLookup(meterProvider, {
272
+ durationMs: 42,
273
+ result: "fetched",
274
+ remoteUrl: new URL("https://example.com/users/alice#main-key"),
275
+ cacheEnabled: true,
276
+ statusCode: 200
277
+ });
278
+ const counters = recorder.getMeasurements("activitypub.key.lookup");
279
+ assertEquals(counters.length, 1);
280
+ assertEquals(counters[0].type, "counter");
281
+ assertEquals(counters[0].value, 1);
282
+ assertEquals(counters[0].attributes["activitypub.lookup.kind"], "public_key");
283
+ assertEquals(counters[0].attributes["activitypub.lookup.result"], "fetched");
284
+ assertEquals(counters[0].attributes["activitypub.remote.host"], "example.com");
285
+ assertEquals(counters[0].attributes["activitypub.cache.enabled"], true);
286
+ assertEquals(counters[0].attributes["http.response.status_code"], 200);
287
+ const durations = recorder.getMeasurements("activitypub.key.lookup.duration");
288
+ assertEquals(durations.length, 1);
289
+ assertEquals(durations[0].type, "histogram");
290
+ assertEquals(durations[0].value, 42);
291
+ assertEquals(durations[0].attributes["activitypub.lookup.kind"], "public_key");
292
+ assertEquals(durations[0].attributes["activitypub.lookup.result"], "fetched");
293
+ assertEquals(durations[0].attributes["activitypub.remote.host"], "example.com");
294
+ assertEquals(durations[0].attributes["activitypub.cache.enabled"], true);
295
+ assertEquals(durations[0].attributes["http.response.status_code"], 200);
296
+ });
297
+ test("recordKeyLookup() omits optional attributes when not provided", () => {
298
+ const [meterProvider, recorder] = createTestMeterProvider();
299
+ recordKeyLookup(meterProvider, {
300
+ durationMs: 0,
301
+ result: "error",
302
+ cacheEnabled: false
303
+ });
304
+ const counter = recorder.getMeasurement("activitypub.key.lookup");
305
+ assertEquals(counter?.attributes["activitypub.lookup.result"], "error");
306
+ assertEquals(counter?.attributes["activitypub.cache.enabled"], false);
307
+ assertEquals("activitypub.remote.host" in counter.attributes, false);
308
+ assertEquals("http.response.status_code" in counter.attributes, false);
309
+ const duration = recorder.getMeasurement("activitypub.key.lookup.duration");
310
+ assertEquals("activitypub.remote.host" in duration.attributes, false);
311
+ assertEquals("http.response.status_code" in duration.attributes, false);
312
+ });
313
+ test("recordDocumentFetch() records counter and duration with all attributes", () => {
314
+ const [meterProvider, recorder] = createTestMeterProvider();
315
+ recordDocumentFetch(meterProvider, {
316
+ durationMs: 123,
317
+ kind: "object",
318
+ result: "not_found",
319
+ remoteUrl: new URL("https://remote.test/objects/123"),
320
+ cacheEnabled: true,
321
+ statusCode: 404
322
+ });
323
+ const counter = recorder.getMeasurement("activitypub.document.fetch");
324
+ assertEquals(counter?.type, "counter");
325
+ assertEquals(counter?.value, 1);
326
+ assertEquals(counter?.attributes["activitypub.lookup.kind"], "object");
327
+ assertEquals(counter?.attributes["activitypub.lookup.result"], "not_found");
328
+ assertEquals(counter?.attributes["activitypub.remote.host"], "remote.test");
329
+ assertEquals(counter?.attributes["activitypub.cache.enabled"], true);
330
+ assertEquals(counter?.attributes["http.response.status_code"], 404);
331
+ const duration = recorder.getMeasurement("activitypub.document.fetch.duration");
332
+ assertEquals(duration?.type, "histogram");
333
+ assertEquals(duration?.value, 123);
334
+ assertEquals(duration?.attributes["activitypub.lookup.kind"], "object");
335
+ assertEquals(duration?.attributes["activitypub.lookup.result"], "not_found");
336
+ });
337
+ test("recordDocumentFetch() omits optional attributes when not provided", () => {
338
+ const [meterProvider, recorder] = createTestMeterProvider();
339
+ recordDocumentFetch(meterProvider, {
340
+ durationMs: 5,
341
+ kind: "context",
342
+ result: "fetched"
343
+ });
344
+ const counter = recorder.getMeasurement("activitypub.document.fetch");
345
+ assertEquals(counter?.attributes["activitypub.lookup.kind"], "context");
346
+ assertEquals(counter?.attributes["activitypub.lookup.result"], "fetched");
347
+ assertEquals("activitypub.remote.host" in counter.attributes, false);
348
+ assertEquals("activitypub.cache.enabled" in counter.attributes, false);
349
+ assertEquals("http.response.status_code" in counter.attributes, false);
350
+ assertEquals(recorder.getMeasurement("activitypub.document.fetch.duration")?.value, 5);
351
+ });
352
+ test("recordDocumentCache() records hit and miss as a counter", () => {
353
+ const [meterProvider, recorder] = createTestMeterProvider();
354
+ recordDocumentCache(meterProvider, {
355
+ kind: "object",
356
+ result: "hit",
357
+ remoteUrl: new URL("https://remote.test/objects/1")
358
+ });
359
+ recordDocumentCache(meterProvider, {
360
+ kind: "context",
361
+ result: "miss",
362
+ remoteUrl: new URL("https://w3id.org/security/v1")
363
+ });
364
+ const measurements = recorder.getMeasurements("activitypub.document.cache");
365
+ assertEquals(measurements.length, 2);
366
+ for (const m of measurements) {
367
+ assertEquals(m.type, "counter");
368
+ assertEquals(m.value, 1);
369
+ }
370
+ assertEquals(measurements[0].attributes["activitypub.lookup.kind"], "object");
371
+ assertEquals(measurements[0].attributes["activitypub.lookup.result"], "hit");
372
+ assertEquals(measurements[0].attributes["activitypub.remote.host"], "remote.test");
373
+ assertEquals(measurements[1].attributes["activitypub.lookup.kind"], "context");
374
+ assertEquals(measurements[1].attributes["activitypub.lookup.result"], "miss");
375
+ assertEquals(measurements[1].attributes["activitypub.remote.host"], "w3id.org");
376
+ });
377
+ test("recordWebFingerHandle() records counter and duration with all attributes", () => {
378
+ const [meterProvider, recorder] = createTestMeterProvider();
379
+ recordWebFingerHandle(meterProvider, {
380
+ durationMs: 17,
381
+ result: "resolved",
382
+ scheme: "acct",
383
+ statusCode: 200
384
+ });
385
+ const counters = recorder.getMeasurements("webfinger.handle");
386
+ assertEquals(counters.length, 1);
387
+ assertEquals(counters[0].type, "counter");
388
+ assertEquals(counters[0].value, 1);
389
+ assertEquals(counters[0].attributes["webfinger.handle.result"], "resolved");
390
+ assertEquals(counters[0].attributes["webfinger.resource.scheme"], "acct");
391
+ assertEquals(counters[0].attributes["http.response.status_code"], 200);
392
+ const durations = recorder.getMeasurements("webfinger.handle.duration");
393
+ assertEquals(durations.length, 1);
394
+ assertEquals(durations[0].type, "histogram");
395
+ assertEquals(durations[0].value, 17);
396
+ assertEquals(durations[0].attributes["webfinger.handle.result"], "resolved");
397
+ assertEquals(durations[0].attributes["webfinger.resource.scheme"], "acct");
398
+ assertEquals(durations[0].attributes["http.response.status_code"], 200);
399
+ });
400
+ test("recordWebFingerHandle() records each non-resolved result with the matching status code", () => {
401
+ const [meterProvider, recorder] = createTestMeterProvider();
402
+ for (const [result, statusCode] of [
403
+ ["invalid", 400],
404
+ ["not_found", 404],
405
+ ["tombstoned", 410]
406
+ ]) recordWebFingerHandle(meterProvider, {
407
+ durationMs: 1,
408
+ result,
409
+ scheme: "acct",
410
+ statusCode
411
+ });
412
+ const counters = recorder.getMeasurements("webfinger.handle");
413
+ assertEquals(counters.length, 3);
414
+ assertEquals(counters.map((m) => m.attributes["webfinger.handle.result"]), [
415
+ "invalid",
416
+ "not_found",
417
+ "tombstoned"
418
+ ]);
419
+ assertEquals(counters.map((m) => m.attributes["http.response.status_code"]), [
420
+ 400,
421
+ 404,
422
+ 410
423
+ ]);
424
+ });
425
+ test("recordWebFingerHandle() omits optional attributes when not provided", () => {
426
+ const [meterProvider, recorder] = createTestMeterProvider();
427
+ recordWebFingerHandle(meterProvider, {
428
+ durationMs: 0,
429
+ result: "error"
430
+ });
431
+ const counter = recorder.getMeasurement("webfinger.handle");
432
+ assertEquals(counter?.attributes["webfinger.handle.result"], "error");
433
+ assertEquals("webfinger.resource.scheme" in (counter?.attributes ?? {}), false);
434
+ assertEquals("http.response.status_code" in (counter?.attributes ?? {}), false);
435
+ });
436
+ test("recordCollectionRequest() records counter with bounded attributes", () => {
437
+ const [meterProvider, recorder] = createTestMeterProvider();
438
+ recordCollectionRequest(meterProvider, {
439
+ kind: "followers",
440
+ page: true,
441
+ dispatcher: "built_in",
442
+ result: "served",
443
+ statusCode: 200
444
+ });
445
+ const counter = recorder.getMeasurement("activitypub.collection.request");
446
+ assertEquals(counter?.type, "counter");
447
+ assertEquals(counter?.value, 1);
448
+ assertEquals(counter?.attributes["activitypub.collection.kind"], "followers");
449
+ assertEquals(counter?.attributes["activitypub.collection.page"], true);
450
+ assertEquals(counter?.attributes["fedify.collection.dispatcher"], "built_in");
451
+ assertEquals(counter?.attributes["activitypub.collection.result"], "served");
452
+ assertEquals(counter?.attributes["http.response.status_code"], 200);
453
+ });
454
+ test("recordCollectionRequest() omits status code when unavailable", () => {
455
+ const [meterProvider, recorder] = createTestMeterProvider();
456
+ recordCollectionRequest(meterProvider, {
457
+ kind: "custom",
458
+ page: false,
459
+ dispatcher: "custom",
460
+ result: "error"
461
+ });
462
+ const counter = recorder.getMeasurement("activitypub.collection.request");
463
+ assertEquals(counter?.attributes["activitypub.collection.kind"], "custom");
464
+ assertEquals(counter?.attributes["activitypub.collection.page"], false);
465
+ assertEquals(counter?.attributes["fedify.collection.dispatcher"], "custom");
466
+ assertEquals(counter?.attributes["activitypub.collection.result"], "error");
467
+ assertEquals("http.response.status_code" in (counter?.attributes ?? {}), false);
468
+ });
469
+ test("recordCollectionDispatchDuration() records histogram", () => {
470
+ const [meterProvider, recorder] = createTestMeterProvider();
471
+ recordCollectionDispatchDuration(meterProvider, 12, {
472
+ kind: "outbox",
473
+ page: false,
474
+ dispatcher: "built_in",
475
+ result: "served"
476
+ });
477
+ const duration = recorder.getMeasurement("activitypub.collection.dispatch.duration");
478
+ assertEquals(duration?.type, "histogram");
479
+ assertEquals(duration?.value, 12);
480
+ assertEquals(duration?.attributes["activitypub.collection.kind"], "outbox");
481
+ assertEquals(duration?.attributes["activitypub.collection.page"], false);
482
+ assertEquals(duration?.attributes["fedify.collection.dispatcher"], "built_in");
483
+ assertEquals(duration?.attributes["activitypub.collection.result"], "served");
484
+ });
485
+ test("recordCollectionPageItems() records item count histogram", () => {
486
+ const [meterProvider, recorder] = createTestMeterProvider();
487
+ recordCollectionPageItems(meterProvider, 3, {
488
+ kind: "featured_tags",
489
+ page: true,
490
+ dispatcher: "built_in",
491
+ result: "served",
492
+ statusCode: 200
493
+ });
494
+ const items = recorder.getMeasurement("activitypub.collection.page.items");
495
+ assertEquals(items?.type, "histogram");
496
+ assertEquals(items?.value, 3);
497
+ assertEquals(items?.attributes["activitypub.collection.kind"], "featured_tags");
498
+ assertEquals(items?.attributes["activitypub.collection.page"], true);
499
+ assertEquals(items?.attributes["http.response.status_code"], 200);
500
+ });
501
+ test("recordCollectionTotalItems() records total item histogram", () => {
502
+ const [meterProvider, recorder] = createTestMeterProvider();
503
+ recordCollectionTotalItems(meterProvider, 42, {
504
+ kind: "liked",
505
+ page: false,
506
+ dispatcher: "built_in",
507
+ result: "served"
508
+ });
509
+ const total = recorder.getMeasurement("activitypub.collection.total_items");
510
+ assertEquals(total?.type, "histogram");
511
+ assertEquals(total?.value, 42);
512
+ assertEquals(total?.attributes["activitypub.collection.kind"], "liked");
513
+ assertEquals(total?.attributes["activitypub.collection.page"], false);
514
+ });
515
+ test("classifyFetchError() classifies FetchError with 404 as not_found", () => {
516
+ assertEquals(classifyFetchError(new FetchError("https://example.com/k", "not found", new Response("", { status: 404 }))), {
517
+ result: "not_found",
518
+ statusCode: 404
519
+ });
520
+ });
521
+ test("classifyFetchError() classifies FetchError with 410 as not_found", () => {
522
+ assertEquals(classifyFetchError(new FetchError("https://example.com/k", "gone", new Response("", { status: 410 }))), {
523
+ result: "not_found",
524
+ statusCode: 410
525
+ });
526
+ });
527
+ test("classifyFetchError() classifies FetchError with 500 as error", () => {
528
+ assertEquals(classifyFetchError(new FetchError("https://example.com/k", "server error", new Response("", { status: 500 }))), {
529
+ result: "error",
530
+ statusCode: 500
531
+ });
532
+ });
533
+ test("classifyFetchError() classifies FetchError without response as network_error", () => {
534
+ assertEquals(classifyFetchError(new FetchError("https://example.com/k", "boom")), { result: "network_error" });
535
+ });
536
+ test("classifyFetchError() classifies a bare TypeError as network_error", () => {
537
+ assertEquals(classifyFetchError(/* @__PURE__ */ new TypeError("connect failed")), { result: "network_error" });
538
+ });
539
+ test("classifyFetchError() classifies an AbortError as network_error", () => {
540
+ const abort = /* @__PURE__ */ new Error("aborted");
541
+ abort.name = "AbortError";
542
+ assertEquals(classifyFetchError(abort), { result: "network_error" });
543
+ });
544
+ test("classifyFetchError() classifies any other thrown value as error", () => {
545
+ assertEquals(classifyFetchError(/* @__PURE__ */ new Error("nope")), { result: "error" });
546
+ assertEquals(classifyFetchError("string error"), { result: "error" });
547
+ assertEquals(classifyFetchError(void 0), { result: "error" });
548
+ });
549
+ test("instrumentDocumentLoader() records fetched on success", async () => {
550
+ const [meterProvider, recorder] = createTestMeterProvider();
551
+ const inner = (url) => Promise.resolve({
552
+ contextUrl: null,
553
+ documentUrl: url,
554
+ document: { ok: true }
555
+ });
556
+ assertEquals((await instrumentDocumentLoader(inner, {
557
+ meterProvider,
558
+ kind: "object",
559
+ cacheEnabled: true
560
+ })("https://example.com/o")).document, { ok: true });
561
+ const counter = recorder.getMeasurement("activitypub.document.fetch");
562
+ assertEquals(counter?.attributes["activitypub.lookup.kind"], "object");
563
+ assertEquals(counter?.attributes["activitypub.lookup.result"], "fetched");
564
+ assertEquals(counter?.attributes["activitypub.remote.host"], "example.com");
565
+ assertEquals(counter?.attributes["activitypub.cache.enabled"], true);
566
+ assertEquals("http.response.status_code" in counter.attributes, false);
567
+ const duration = recorder.getMeasurement("activitypub.document.fetch.duration");
568
+ assertEquals(duration?.type, "histogram");
569
+ assertEquals(duration?.attributes["activitypub.lookup.result"], "fetched");
570
+ });
571
+ test("instrumentDocumentLoader() records not_found on FetchError 404", async () => {
572
+ const [meterProvider, recorder] = createTestMeterProvider();
573
+ const inner = (url) => Promise.reject(new FetchError(url, "HTTP 404", new Response("", { status: 404 })));
574
+ const wrapped = instrumentDocumentLoader(inner, {
575
+ meterProvider,
576
+ kind: "context",
577
+ cacheEnabled: false
578
+ });
579
+ await assertRejects(() => wrapped("https://example.com/missing"), FetchError);
580
+ const counter = recorder.getMeasurement("activitypub.document.fetch");
581
+ assertEquals(counter?.attributes["activitypub.lookup.kind"], "context");
582
+ assertEquals(counter?.attributes["activitypub.lookup.result"], "not_found");
583
+ assertEquals(counter?.attributes["activitypub.remote.host"], "example.com");
584
+ assertEquals(counter?.attributes["activitypub.cache.enabled"], false);
585
+ assertEquals(counter?.attributes["http.response.status_code"], 404);
586
+ });
587
+ test("instrumentDocumentLoader() records network_error on TypeError", async () => {
588
+ const [meterProvider, recorder] = createTestMeterProvider();
589
+ const inner = () => Promise.reject(/* @__PURE__ */ new TypeError("fetch failed"));
590
+ const wrapped = instrumentDocumentLoader(inner, {
591
+ meterProvider,
592
+ kind: "object"
593
+ });
594
+ await assertRejects(() => wrapped("https://example.com/o"), TypeError);
595
+ const counter = recorder.getMeasurement("activitypub.document.fetch");
596
+ assertEquals(counter?.attributes["activitypub.lookup.result"], "network_error");
597
+ assertEquals("activitypub.cache.enabled" in counter.attributes, false);
598
+ });
599
+ test("instrumentDocumentLoader() returns inner loader unchanged when meterProvider is omitted", async () => {
600
+ const [, recorder] = createTestMeterProvider();
601
+ let callCount = 0;
602
+ const inner = (url) => {
603
+ callCount++;
604
+ return Promise.resolve({
605
+ contextUrl: null,
606
+ documentUrl: url,
607
+ document: { ok: true }
608
+ });
609
+ };
610
+ const wrapped = instrumentDocumentLoader(inner, { kind: "object" });
611
+ assertEquals(wrapped, inner);
612
+ await wrapped("https://example.com/o");
613
+ assertEquals(callCount, 1);
614
+ assertEquals(recorder.getMeasurements("activitypub.document.fetch").length, 0);
615
+ assertEquals(recorder.getMeasurements("activitypub.document.fetch.duration").length, 0);
616
+ });
617
+ test("instrumentDocumentLoader() omits remote.host when URL is unparseable", async () => {
618
+ const [meterProvider, recorder] = createTestMeterProvider();
619
+ const inner = (url) => Promise.resolve({
620
+ contextUrl: null,
621
+ documentUrl: url,
622
+ document: {}
623
+ });
624
+ await instrumentDocumentLoader(inner, {
625
+ meterProvider,
626
+ kind: "other"
627
+ })("not a url");
628
+ const counter = recorder.getMeasurement("activitypub.document.fetch");
629
+ assertEquals(counter?.attributes["activitypub.lookup.kind"], "other");
630
+ assertEquals(counter?.attributes["activitypub.lookup.result"], "fetched");
631
+ assertEquals("activitypub.remote.host" in counter.attributes, false);
632
+ });
633
+ //#endregion
634
+ export {};