iii-sdk 0.2.0 → 0.2.1

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 (36) hide show
  1. package/dist/{iii-7FpY7bi9.d.mts → iii-CAEmlG6f.d.mts} +18 -5
  2. package/dist/iii-CAEmlG6f.d.mts.map +1 -0
  3. package/dist/{iii-BhtLRYBs.d.cts → iii-DEbgvZtB.d.cts} +15 -2
  4. package/dist/iii-DEbgvZtB.d.cts.map +1 -0
  5. package/dist/index.cjs +9 -7
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +1 -1
  8. package/dist/index.d.mts +1 -1
  9. package/dist/index.mjs +9 -7
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/{otel-worker-gauges-Dka1CFDy.mjs → otel-worker-gauges-CHooGOLC.mjs} +144 -12
  12. package/dist/otel-worker-gauges-CHooGOLC.mjs.map +1 -0
  13. package/dist/{otel-worker-gauges-8AUNGQgJ.cjs → otel-worker-gauges-CLZyxI1P.cjs} +137 -5
  14. package/dist/otel-worker-gauges-CLZyxI1P.cjs.map +1 -0
  15. package/dist/stream-Bzpo5JNV.d.mts.map +1 -1
  16. package/dist/telemetry.cjs +1 -1
  17. package/dist/telemetry.d.cts +1 -1
  18. package/dist/telemetry.d.mts +1 -1
  19. package/dist/telemetry.mjs +1 -1
  20. package/package.json +1 -1
  21. package/tests/api-triggers.test.ts +163 -0
  22. package/tests/bridge.test.ts +84 -0
  23. package/tests/exports.test.ts +37 -0
  24. package/tests/fetch-instrumentation.test.ts +623 -0
  25. package/tests/fixtures/config-test.yaml +66 -0
  26. package/tests/healthcheck.test.ts +1 -2
  27. package/tests/kv-server.test.ts +82 -0
  28. package/tests/pubsub.test.ts +117 -0
  29. package/tests/setup.ts +2 -32
  30. package/tests/state.test.ts +1 -2
  31. package/tests/stream.test.ts +1 -2
  32. package/tests/utils.ts +2 -23
  33. package/dist/iii-7FpY7bi9.d.mts.map +0 -1
  34. package/dist/iii-BhtLRYBs.d.cts.map +0 -1
  35. package/dist/otel-worker-gauges-8AUNGQgJ.cjs.map +0 -1
  36. package/dist/otel-worker-gauges-Dka1CFDy.mjs.map +0 -1
@@ -0,0 +1,623 @@
1
+ /**
2
+ * Unit tests for global fetch auto-instrumentation behavior.
3
+ *
4
+ * These tests verify that globalThis.fetch is patched by default
5
+ * and can be disabled via fetchInstrumentationEnabled config.
6
+ */
7
+
8
+ import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'
9
+ import { type Tracer, SpanStatusCode } from '@opentelemetry/api'
10
+ import type { Instrumentation } from '@opentelemetry/instrumentation'
11
+
12
+ // Mock WebSocket to prevent real connections
13
+ vi.mock('ws', () => {
14
+ const MockWebSocket = vi.fn().mockImplementation(() => ({
15
+ on: vi.fn(),
16
+ close: vi.fn(),
17
+ send: vi.fn(),
18
+ readyState: 0,
19
+ }))
20
+ return { WebSocket: MockWebSocket, default: { WebSocket: MockWebSocket } }
21
+ })
22
+
23
+ // Mock NodeTracerProvider to avoid real tracer setup
24
+ vi.mock('@opentelemetry/sdk-trace-node', () => ({
25
+ NodeTracerProvider: vi.fn().mockImplementation(() => ({
26
+ register: vi.fn(),
27
+ shutdown: vi.fn().mockResolvedValue(undefined),
28
+ })),
29
+ }))
30
+
31
+ // Mock MeterProvider
32
+ vi.mock('@opentelemetry/sdk-metrics', () => ({
33
+ MeterProvider: vi.fn().mockImplementation(() => ({
34
+ getMeter: vi.fn().mockReturnValue({ createCounter: vi.fn() }),
35
+ shutdown: vi.fn().mockResolvedValue(undefined),
36
+ })),
37
+ PeriodicExportingMetricReader: vi.fn().mockImplementation(() => ({})),
38
+ }))
39
+
40
+ // Mock LoggerProvider
41
+ vi.mock('@opentelemetry/sdk-logs', () => ({
42
+ LoggerProvider: vi.fn().mockImplementation(() => ({
43
+ getLogger: vi.fn().mockReturnValue({ emit: vi.fn() }),
44
+ addLogRecordProcessor: vi.fn(),
45
+ shutdown: vi.fn().mockResolvedValue(undefined),
46
+ })),
47
+ SimpleLogRecordProcessor: vi.fn().mockImplementation(() => ({})),
48
+ }))
49
+
50
+ // Mock span processor
51
+ vi.mock('@opentelemetry/sdk-trace-base', () => ({
52
+ BatchSpanProcessor: vi.fn().mockImplementation(() => ({})),
53
+ }))
54
+
55
+ // Mock exporters
56
+ vi.mock('../src/telemetry-system/exporters', () => ({
57
+ EngineSpanExporter: vi.fn().mockImplementation(() => ({})),
58
+ EngineMetricsExporter: vi.fn().mockImplementation(() => ({})),
59
+ EngineLogExporter: vi.fn().mockImplementation(() => ({})),
60
+ }))
61
+
62
+ // Mock the shared connection
63
+ vi.mock('../src/telemetry-system/connection', () => ({
64
+ SharedEngineConnection: vi.fn().mockImplementation(() => ({
65
+ send: vi.fn(),
66
+ onConnected: vi.fn(),
67
+ getState: vi.fn().mockReturnValue('disconnected'),
68
+ shutdown: vi.fn().mockResolvedValue(undefined),
69
+ })),
70
+ }))
71
+
72
+ import { initOtel, shutdownOtel, getTracer } from '../src/telemetry-system/index'
73
+
74
+ describe('Fetch instrumentation', () => {
75
+ const originalEnv = process.env
76
+ const nativeFetch = globalThis.fetch
77
+
78
+ beforeEach(() => {
79
+ process.env = { ...originalEnv }
80
+ delete process.env.OTEL_ENABLED
81
+ // Ensure globalThis.fetch is the native version before each test
82
+ globalThis.fetch = nativeFetch
83
+ })
84
+
85
+ afterEach(async () => {
86
+ await shutdownOtel()
87
+ process.env = originalEnv
88
+ // Restore native fetch after shutdown
89
+ globalThis.fetch = nativeFetch
90
+ })
91
+
92
+ it('should initialize OTel successfully with fetch instrumentation enabled by default', () => {
93
+ expect(() => initOtel()).not.toThrow()
94
+ expect(getTracer()).not.toBeNull()
95
+ })
96
+
97
+ it('should patch globalThis.fetch when enabled by default', () => {
98
+ initOtel()
99
+ // After initOtel, globalThis.fetch should be patched (different from native)
100
+ expect(globalThis.fetch).not.toBe(nativeFetch)
101
+ })
102
+
103
+ it('should NOT patch globalThis.fetch when fetchInstrumentationEnabled is false', () => {
104
+ initOtel({ fetchInstrumentationEnabled: false })
105
+ // globalThis.fetch should remain the native version
106
+ expect(globalThis.fetch).toBe(nativeFetch)
107
+ })
108
+
109
+ it('should restore globalThis.fetch on shutdown', async () => {
110
+ initOtel()
111
+ expect(globalThis.fetch).not.toBe(nativeFetch)
112
+
113
+ await shutdownOtel()
114
+ expect(globalThis.fetch).toBe(nativeFetch)
115
+ })
116
+
117
+ it('should NOT patch globalThis.fetch when OTel is disabled', () => {
118
+ initOtel({ enabled: false })
119
+ expect(globalThis.fetch).toBe(nativeFetch)
120
+ expect(getTracer()).toBeNull()
121
+ })
122
+
123
+ it('should initialize OTel successfully when fetchInstrumentationEnabled is explicitly true', () => {
124
+ expect(() => initOtel({ fetchInstrumentationEnabled: true })).not.toThrow()
125
+ expect(getTracer()).not.toBeNull()
126
+ expect(globalThis.fetch).not.toBe(nativeFetch)
127
+ })
128
+
129
+ it('should accept user instrumentations alongside fetch patch', () => {
130
+ const userInstrumentation = {
131
+ instrumentationName: 'custom-test',
132
+ instrumentationVersion: '1.0.0',
133
+ getConfig: () => ({}),
134
+ setConfig: vi.fn(),
135
+ setTracerProvider: vi.fn(),
136
+ setMeterProvider: vi.fn(),
137
+ enable: vi.fn(),
138
+ disable: vi.fn(),
139
+ }
140
+ expect(() =>
141
+ initOtel({ instrumentations: [userInstrumentation as Instrumentation] }),
142
+ ).not.toThrow()
143
+ expect(getTracer()).not.toBeNull()
144
+ // Both fetch patch and user instrumentations should work
145
+ expect(globalThis.fetch).not.toBe(nativeFetch)
146
+ })
147
+
148
+ it('should accept user instrumentations when fetch instrumentation is disabled', () => {
149
+ const userInstrumentation = {
150
+ instrumentationName: 'custom-test',
151
+ instrumentationVersion: '1.0.0',
152
+ getConfig: () => ({}),
153
+ setConfig: vi.fn(),
154
+ setTracerProvider: vi.fn(),
155
+ setMeterProvider: vi.fn(),
156
+ enable: vi.fn(),
157
+ disable: vi.fn(),
158
+ }
159
+ expect(() =>
160
+ initOtel({
161
+ fetchInstrumentationEnabled: false,
162
+ instrumentations: [userInstrumentation as Instrumentation],
163
+ }),
164
+ ).not.toThrow()
165
+ expect(getTracer()).not.toBeNull()
166
+ // Fetch should NOT be patched
167
+ expect(globalThis.fetch).toBe(nativeFetch)
168
+ })
169
+ })
170
+
171
+ describe('Fetch span attributes', () => {
172
+ const nativeFetch = globalThis.fetch
173
+ let unpatch: () => void
174
+
175
+ afterEach(() => {
176
+ unpatch?.()
177
+ // restore in case the test replaced fetch manually
178
+ globalThis.fetch = nativeFetch
179
+ })
180
+
181
+ it('sets url.scheme, url.path, network.protocol.name, server.address and span name on success', async () => {
182
+ const spanMock = {
183
+ setAttribute: vi.fn(),
184
+ setStatus: vi.fn(),
185
+ recordException: vi.fn(),
186
+ end: vi.fn(),
187
+ }
188
+ const tracerMock = {
189
+ startActiveSpan: vi
190
+ .fn()
191
+ .mockImplementation(
192
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
193
+ fn(spanMock),
194
+ ),
195
+ }
196
+
197
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
198
+ '../src/telemetry-system/fetch-instrumentation'
199
+ )
200
+ unpatch = unpatchGlobalFetch
201
+
202
+ // Replace native fetch with a fake before patching so the patch wraps the fake
203
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
204
+ globalThis.fetch = fakeFetch
205
+
206
+ patchGlobalFetch(tracerMock as unknown as Tracer)
207
+
208
+ await globalThis.fetch('https://example.com/api/items?q=1')
209
+
210
+ expect(tracerMock.startActiveSpan).toHaveBeenCalledWith(
211
+ 'GET /api/items',
212
+ expect.objectContaining({
213
+ attributes: expect.objectContaining({
214
+ 'http.request.method': 'GET',
215
+ 'url.full': 'https://example.com/api/items?q=1',
216
+ 'url.scheme': 'https',
217
+ 'url.path': '/api/items',
218
+ 'network.protocol.name': 'http',
219
+ 'server.address': 'example.com',
220
+ }),
221
+ }),
222
+ expect.anything(),
223
+ expect.any(Function),
224
+ )
225
+
226
+ expect(spanMock.setAttribute).toHaveBeenCalledWith('http.response.status_code', 200)
227
+ expect(spanMock.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK })
228
+ })
229
+
230
+ it('sets url.query when query string is present', async () => {
231
+ const spanMock = {
232
+ setAttribute: vi.fn(),
233
+ setStatus: vi.fn(),
234
+ recordException: vi.fn(),
235
+ end: vi.fn(),
236
+ }
237
+ const tracerMock = {
238
+ startActiveSpan: vi
239
+ .fn()
240
+ .mockImplementation(
241
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
242
+ fn(spanMock),
243
+ ),
244
+ }
245
+
246
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
247
+ '../src/telemetry-system/fetch-instrumentation'
248
+ )
249
+ unpatch = unpatchGlobalFetch
250
+
251
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
252
+ globalThis.fetch = fakeFetch
253
+
254
+ patchGlobalFetch(tracerMock as unknown as Tracer)
255
+
256
+ await globalThis.fetch('https://example.com/api/items?q=1&page=2')
257
+
258
+ expect(tracerMock.startActiveSpan).toHaveBeenCalledWith(
259
+ 'GET /api/items',
260
+ expect.objectContaining({
261
+ attributes: expect.objectContaining({
262
+ 'url.query': 'q=1&page=2',
263
+ }),
264
+ }),
265
+ expect.anything(),
266
+ expect.any(Function),
267
+ )
268
+ })
269
+
270
+ it('does not set url.query when no query string', async () => {
271
+ const spanMock = {
272
+ setAttribute: vi.fn(),
273
+ setStatus: vi.fn(),
274
+ recordException: vi.fn(),
275
+ end: vi.fn(),
276
+ }
277
+ const tracerMock = {
278
+ startActiveSpan: vi
279
+ .fn()
280
+ .mockImplementation(
281
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
282
+ fn(spanMock),
283
+ ),
284
+ }
285
+
286
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
287
+ '../src/telemetry-system/fetch-instrumentation'
288
+ )
289
+ unpatch = unpatchGlobalFetch
290
+
291
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
292
+ globalThis.fetch = fakeFetch
293
+
294
+ patchGlobalFetch(tracerMock as unknown as Tracer)
295
+
296
+ await globalThis.fetch('https://example.com/api/items')
297
+
298
+ const callArgs = tracerMock.startActiveSpan.mock.calls[0][1] as {
299
+ attributes: Record<string, unknown>
300
+ }
301
+ expect(callArgs.attributes['url.query']).toBeUndefined()
302
+ })
303
+
304
+ it('sets http.request.body.size for string body', async () => {
305
+ const spanMock = {
306
+ setAttribute: vi.fn(),
307
+ setStatus: vi.fn(),
308
+ recordException: vi.fn(),
309
+ end: vi.fn(),
310
+ }
311
+ const tracerMock = {
312
+ startActiveSpan: vi
313
+ .fn()
314
+ .mockImplementation(
315
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
316
+ fn(spanMock),
317
+ ),
318
+ }
319
+
320
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
321
+ '../src/telemetry-system/fetch-instrumentation'
322
+ )
323
+ unpatch = unpatchGlobalFetch
324
+
325
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
326
+ globalThis.fetch = fakeFetch
327
+
328
+ patchGlobalFetch(tracerMock as unknown as Tracer)
329
+
330
+ await globalThis.fetch('https://example.com/api', {
331
+ method: 'POST',
332
+ body: 'hello',
333
+ })
334
+
335
+ // 'hello' is 5 bytes in UTF-8
336
+ expect(spanMock.setAttribute).toHaveBeenCalledWith('http.request.body.size', 5)
337
+ })
338
+
339
+ it('sets http.request.body.size for Uint8Array body', async () => {
340
+ const spanMock = {
341
+ setAttribute: vi.fn(),
342
+ setStatus: vi.fn(),
343
+ recordException: vi.fn(),
344
+ end: vi.fn(),
345
+ }
346
+ const tracerMock = {
347
+ startActiveSpan: vi
348
+ .fn()
349
+ .mockImplementation(
350
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
351
+ fn(spanMock),
352
+ ),
353
+ }
354
+
355
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
356
+ '../src/telemetry-system/fetch-instrumentation'
357
+ )
358
+ unpatch = unpatchGlobalFetch
359
+
360
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
361
+ globalThis.fetch = fakeFetch
362
+
363
+ patchGlobalFetch(tracerMock as unknown as Tracer)
364
+
365
+ await globalThis.fetch('https://example.com/api', {
366
+ method: 'POST',
367
+ body: new Uint8Array([1, 2, 3, 4]),
368
+ })
369
+
370
+ expect(spanMock.setAttribute).toHaveBeenCalledWith('http.request.body.size', 4)
371
+ })
372
+
373
+ it('does not set http.request.body.size when body is absent', async () => {
374
+ const spanMock = {
375
+ setAttribute: vi.fn(),
376
+ setStatus: vi.fn(),
377
+ recordException: vi.fn(),
378
+ end: vi.fn(),
379
+ }
380
+ const tracerMock = {
381
+ startActiveSpan: vi
382
+ .fn()
383
+ .mockImplementation(
384
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
385
+ fn(spanMock),
386
+ ),
387
+ }
388
+
389
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
390
+ '../src/telemetry-system/fetch-instrumentation'
391
+ )
392
+ unpatch = unpatchGlobalFetch
393
+
394
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
395
+ globalThis.fetch = fakeFetch
396
+
397
+ patchGlobalFetch(tracerMock as unknown as Tracer)
398
+
399
+ await globalThis.fetch('https://example.com/api')
400
+
401
+ const calls = spanMock.setAttribute.mock.calls.map(c => c[0])
402
+ expect(calls).not.toContain('http.request.body.size')
403
+ })
404
+
405
+ it('sets http.response.body.size from Content-Length response header', async () => {
406
+ const spanMock = {
407
+ setAttribute: vi.fn(),
408
+ setStatus: vi.fn(),
409
+ recordException: vi.fn(),
410
+ end: vi.fn(),
411
+ }
412
+ const tracerMock = {
413
+ startActiveSpan: vi
414
+ .fn()
415
+ .mockImplementation(
416
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
417
+ fn(spanMock),
418
+ ),
419
+ }
420
+
421
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
422
+ '../src/telemetry-system/fetch-instrumentation'
423
+ )
424
+ unpatch = unpatchGlobalFetch
425
+
426
+ const fakeFetch = vi.fn().mockResolvedValue(
427
+ new Response('hello world', {
428
+ status: 200,
429
+ headers: { 'content-length': '11' },
430
+ }),
431
+ )
432
+ globalThis.fetch = fakeFetch
433
+
434
+ patchGlobalFetch(tracerMock as unknown as Tracer)
435
+
436
+ await globalThis.fetch('https://example.com/api')
437
+
438
+ expect(spanMock.setAttribute).toHaveBeenCalledWith('http.response.body.size', 11)
439
+ })
440
+
441
+ it('does not set http.response.body.size when Content-Length header absent', async () => {
442
+ const spanMock = {
443
+ setAttribute: vi.fn(),
444
+ setStatus: vi.fn(),
445
+ recordException: vi.fn(),
446
+ end: vi.fn(),
447
+ }
448
+ const tracerMock = {
449
+ startActiveSpan: vi
450
+ .fn()
451
+ .mockImplementation(
452
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
453
+ fn(spanMock),
454
+ ),
455
+ }
456
+
457
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
458
+ '../src/telemetry-system/fetch-instrumentation'
459
+ )
460
+ unpatch = unpatchGlobalFetch
461
+
462
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
463
+ globalThis.fetch = fakeFetch
464
+
465
+ patchGlobalFetch(tracerMock as unknown as Tracer)
466
+
467
+ await globalThis.fetch('https://example.com/api')
468
+
469
+ const calls = spanMock.setAttribute.mock.calls.map(c => c[0])
470
+ expect(calls).not.toContain('http.response.body.size')
471
+ })
472
+
473
+ it('captures http.request.header.content-type and http.request.header.accept', async () => {
474
+ const spanMock = {
475
+ setAttribute: vi.fn(),
476
+ setStatus: vi.fn(),
477
+ recordException: vi.fn(),
478
+ end: vi.fn(),
479
+ }
480
+ const tracerMock = {
481
+ startActiveSpan: vi
482
+ .fn()
483
+ .mockImplementation(
484
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
485
+ fn(spanMock),
486
+ ),
487
+ }
488
+
489
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
490
+ '../src/telemetry-system/fetch-instrumentation'
491
+ )
492
+ unpatch = unpatchGlobalFetch
493
+
494
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('ok', { status: 200 }))
495
+ globalThis.fetch = fakeFetch
496
+
497
+ patchGlobalFetch(tracerMock as unknown as Tracer)
498
+
499
+ await globalThis.fetch('https://example.com/api', {
500
+ method: 'POST',
501
+ headers: {
502
+ 'content-type': 'application/json',
503
+ accept: 'application/json',
504
+ },
505
+ body: '{}',
506
+ })
507
+
508
+ expect(spanMock.setAttribute).toHaveBeenCalledWith(
509
+ 'http.request.header.content-type',
510
+ 'application/json',
511
+ )
512
+ expect(spanMock.setAttribute).toHaveBeenCalledWith(
513
+ 'http.request.header.accept',
514
+ 'application/json',
515
+ )
516
+ })
517
+
518
+ it('captures http.response.header.content-type', async () => {
519
+ const spanMock = {
520
+ setAttribute: vi.fn(),
521
+ setStatus: vi.fn(),
522
+ recordException: vi.fn(),
523
+ end: vi.fn(),
524
+ }
525
+ const tracerMock = {
526
+ startActiveSpan: vi
527
+ .fn()
528
+ .mockImplementation(
529
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
530
+ fn(spanMock),
531
+ ),
532
+ }
533
+
534
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
535
+ '../src/telemetry-system/fetch-instrumentation'
536
+ )
537
+ unpatch = unpatchGlobalFetch
538
+
539
+ const fakeFetch = vi.fn().mockResolvedValue(
540
+ new Response('{}', {
541
+ status: 200,
542
+ headers: { 'content-type': 'application/json' },
543
+ }),
544
+ )
545
+ globalThis.fetch = fakeFetch
546
+
547
+ patchGlobalFetch(tracerMock as unknown as Tracer)
548
+
549
+ await globalThis.fetch('https://example.com/api')
550
+
551
+ expect(spanMock.setAttribute).toHaveBeenCalledWith(
552
+ 'http.response.header.content-type',
553
+ 'application/json',
554
+ )
555
+ })
556
+
557
+ it('does not set header attributes when headers are absent', async () => {
558
+ const spanMock = {
559
+ setAttribute: vi.fn(),
560
+ setStatus: vi.fn(),
561
+ recordException: vi.fn(),
562
+ end: vi.fn(),
563
+ }
564
+ const tracerMock = {
565
+ startActiveSpan: vi
566
+ .fn()
567
+ .mockImplementation(
568
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
569
+ fn(spanMock),
570
+ ),
571
+ }
572
+
573
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
574
+ '../src/telemetry-system/fetch-instrumentation'
575
+ )
576
+ unpatch = unpatchGlobalFetch
577
+
578
+ // Use null body so the Response has no auto-set content-type header
579
+ const fakeFetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 }))
580
+ globalThis.fetch = fakeFetch
581
+
582
+ patchGlobalFetch(tracerMock as unknown as Tracer)
583
+
584
+ await globalThis.fetch('https://example.com/api')
585
+
586
+ const calls = spanMock.setAttribute.mock.calls.map(c => c[0])
587
+ expect(calls).not.toContain('http.request.header.content-type')
588
+ expect(calls).not.toContain('http.request.header.accept')
589
+ expect(calls).not.toContain('http.response.header.content-type')
590
+ })
591
+
592
+ it('sets error.type and ERROR status on 4xx response', async () => {
593
+ const spanMock = {
594
+ setAttribute: vi.fn(),
595
+ setStatus: vi.fn(),
596
+ recordException: vi.fn(),
597
+ end: vi.fn(),
598
+ }
599
+ const tracerMock = {
600
+ startActiveSpan: vi
601
+ .fn()
602
+ .mockImplementation(
603
+ (_name: string, _opts: unknown, _ctx: unknown, fn: (span: typeof spanMock) => unknown) =>
604
+ fn(spanMock),
605
+ ),
606
+ }
607
+
608
+ const { patchGlobalFetch, unpatchGlobalFetch } = await import(
609
+ '../src/telemetry-system/fetch-instrumentation'
610
+ )
611
+ unpatch = unpatchGlobalFetch
612
+
613
+ const fakeFetch = vi.fn().mockResolvedValue(new Response('not found', { status: 404 }))
614
+ globalThis.fetch = fakeFetch
615
+
616
+ patchGlobalFetch(tracerMock as unknown as Tracer)
617
+
618
+ await globalThis.fetch('https://example.com/missing')
619
+
620
+ expect(spanMock.setAttribute).toHaveBeenCalledWith('error.type', '404')
621
+ expect(spanMock.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR })
622
+ })
623
+ })
@@ -0,0 +1,66 @@
1
+ port: 49199
2
+ modules:
3
+ - class: modules::stream::StreamModule
4
+ config:
5
+ port: ${STREAMS_PORT:3112}
6
+ host: 0.0.0.0
7
+ adapter:
8
+ class: modules::stream::adapters::KvStore
9
+ config:
10
+ store_method: file_based
11
+ file_path: ./data/stream_store
12
+
13
+ - class: modules::state::StateModule
14
+ config:
15
+ adapter:
16
+ class: modules::state::adapters::KvStore
17
+ config:
18
+ store_method: file_based
19
+ file_path: ./data/state_store.db
20
+
21
+ - class: modules::api::RestApiModule
22
+ config:
23
+ host: 0.0.0.0
24
+ port: 3199
25
+ default_timeout: 30000
26
+
27
+ - class: modules::kv_server::KvServer
28
+ config:
29
+ store_method: file_based
30
+ file_path: ./data/kv_store
31
+ save_interval_ms: 1000
32
+
33
+ - class: modules::pubsub::PubSubModule
34
+ config:
35
+ adapter:
36
+ class: modules::pubsub::LocalAdapter
37
+
38
+ - class: modules::observability::OtelModule
39
+ config:
40
+ enabled: ${OTEL_ENABLED:true}
41
+ service_name: ${OTEL_SERVICE_NAME:iii-test}
42
+ service_version: ${SERVICE_VERSION:0.2.0}
43
+ exporter: ${OTEL_EXPORTER_TYPE:memory}
44
+ endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}
45
+ sampling_ratio: 1.0
46
+ memory_max_spans: ${OTEL_MEMORY_MAX_SPANS:10000}
47
+ metrics_enabled: true
48
+ metrics_exporter: ${OTEL_METRICS_EXPORTER:memory}
49
+ metrics_retention_seconds: 3600
50
+ metrics_max_count: 10000
51
+ logs_enabled: ${OTEL_LOGS_ENABLED:true}
52
+ logs_exporter: ${OTEL_LOGS_EXPORTER:memory}
53
+ logs_max_count: ${OTEL_LOGS_MAX_COUNT:1000}
54
+ logs_retention_seconds: ${OTEL_LOGS_RETENTION_SECONDS:3600}
55
+ logs_sampling_ratio: ${OTEL_LOGS_SAMPLING_RATIO:1.0}
56
+ logs_console_output: ${OTEL_LOGS_CONSOLE_OUTPUT:false}
57
+
58
+ - class: modules::queue::QueueModule
59
+ config:
60
+ adapter:
61
+ class: modules::queue::BuiltinQueueAdapter
62
+
63
+ - class: modules::cron::CronModule
64
+ config:
65
+ adapter:
66
+ class: modules::cron::KvCronAdapter
@@ -1,9 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import type { ApiRequest, ApiResponse } from '../src'
3
- import { skipIfServerUnavailable } from './setup'
4
3
  import { execute, httpRequest, iii } from './utils'
5
4
 
6
- describe.skipIf(skipIfServerUnavailable())('Healthcheck Endpoint', () => {
5
+ describe('Healthcheck Endpoint', () => {
7
6
  it('should register a healthcheck function and trigger', async () => {
8
7
  const fn = iii.registerFunction(
9
8
  { id: 'test.healthcheck' },