@unireq/otel 1.0.0 → 1.0.2

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.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @unireq/otel
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@unireq/otel.svg)](https://www.npmjs.com/package/@unireq/otel)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ OpenTelemetry instrumentation for Unireq HTTP clients. Automatic tracing with W3C Trace Context propagation following OpenTelemetry semantic conventions.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pnpm add @unireq/otel
12
+
13
+ # Peer dependencies
14
+ pnpm add @opentelemetry/api @opentelemetry/semantic-conventions
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```typescript
20
+ import { client } from '@unireq/core';
21
+ import { http, parse } from '@unireq/http';
22
+ import { otel } from '@unireq/otel';
23
+ import { trace } from '@opentelemetry/api';
24
+
25
+ const tracer = trace.getTracer('my-service', '1.0.0');
26
+
27
+ const api = client(
28
+ http('https://api.example.com'),
29
+ otel({ tracer }),
30
+ parse.json(),
31
+ );
32
+
33
+ // All requests create spans
34
+ const response = await api.get('/users');
35
+ ```
36
+
37
+ ## Features
38
+
39
+ | Symbol | Description |
40
+ | --- | --- |
41
+ | `otel(options)` | Creates a tracing policy for HTTP requests |
42
+ | `OtelOptions` | Tracer, span name formatter, custom attributes |
43
+
44
+ ## Configuration
45
+
46
+ ```typescript
47
+ otel({
48
+ tracer,
49
+ spanNameFormatter: (ctx) => `${ctx.method} ${new URL(ctx.url).pathname}`,
50
+ recordRequestBodySize: true,
51
+ recordResponseBodySize: true,
52
+ customAttributes: {
53
+ 'service.name': 'user-service',
54
+ 'deployment.environment': 'production',
55
+ },
56
+ propagateContext: true, // W3C Trace Context headers
57
+ });
58
+ ```
59
+
60
+ ## Span Attributes
61
+
62
+ | Attribute | Description |
63
+ | --- | --- |
64
+ | `http.request.method` | HTTP method |
65
+ | `url.full` | Full request URL |
66
+ | `server.address` | Server hostname |
67
+ | `http.response.status_code` | Response status |
68
+ | `error.type` | Error type on failure |
69
+
70
+ ## Trace Context Propagation
71
+
72
+ Automatically injects `traceparent` and `tracestate` headers for distributed tracing.
73
+
74
+ ## Documentation
75
+
76
+ Full documentation available at [unireq.dev](https://oorabona.github.io/unireq/)
77
+
78
+ ## License
79
+
80
+ MIT
package/dist/index.js CHANGED
@@ -1,2 +1,6 @@
1
- var g="http.request.method",A="url.full",O="http.response.status_code",C="server.address",b="server.port",R="error.type",x="http.request.body.size",P="http.response.body.size",N=e=>`HTTP ${e.method}`;function q(e){try{let s=new URL(e),a=s.protocol==="https:"?443:80,i=s.port?Number.parseInt(s.port,10):a;return {host:s.hostname,port:i,protocol:s.protocol.replace(":","")}}catch{return {host:"unknown",port:void 0,protocol:"http"}}}function T(e){if(e){if(typeof e=="string")return new TextEncoder().encode(e).length;if(e instanceof ArrayBuffer)return e.byteLength;if(e instanceof Uint8Array)return e.length;if(typeof e=="object")try{return new TextEncoder().encode(JSON.stringify(e)).length}catch{return}}}function w(e){let{tracer:s,spanNameFormatter:a=N,recordRequestBodySize:i=false,recordResponseBodySize:m=false,customAttributes:S={},propagateContext:y=true}=e,n,p,c;return async(o,u)=>{if(!n)try{n=await import('@opentelemetry/api'),p=n.context,c=n.propagation;}catch{return u(o)}let{host:E,port:d}=q(o.url),h=a(o),r=s.startSpan(h,{kind:n.SpanKind.CLIENT,attributes:{[g]:o.method,[A]:o.url,[C]:E,...d!==void 0&&{[b]:d},...S}});if(i&&o.body){let t=T(o.body);t!==void 0&&r.setAttribute(x,t);}let f={...o.headers};if(y&&c&&p){let t=p.active();c.inject(n.trace.setSpan(t,r),f);}let _={...o,headers:f};try{let t=await u(_);if(r.setAttribute(O,t.status),m&&t.data){let l=T(t.data);l!==void 0&&r.setAttribute(P,l);}return t.status>=400?(r.setStatus({code:n.SpanStatusCode.ERROR,message:`HTTP ${t.status} ${t.statusText}`}),r.setAttribute(R,`${t.status}`)):r.setStatus({code:n.SpanStatusCode.OK}),t}catch(t){throw r.setStatus({code:n.SpanStatusCode.ERROR,message:t instanceof Error?t.message:"Unknown error"}),t instanceof Error&&(r.recordException(t),r.setAttribute(R,t.name)),t}finally{r.end();}}}export{w as otel};//# sourceMappingURL=index.js.map
1
+ var g="http.request.method",A="url.full",O="http.response.status_code",C="server.address",b="server.port",R="error.type",x="http.request.body.size",P="http.response.body.size",N=e=>`HTTP ${e.method}`;function q(e){try{let s=new URL(e);let a=s.protocol==="https:"?443:80,i=s.port?Number.parseInt(s.port,10):a;return {host:s.hostname,port:i,protocol:s.protocol.replace(":","")}}catch{return {host:"unknown",port:void 0,protocol:"http"}}}function T(e){if(e){if(typeof e=="string")return new TextEncoder().encode(e).length;if(e instanceof ArrayBuffer)return e.byteLength;if(e instanceof Uint8Array)return e.length;if(typeof e=="object")try{return new TextEncoder().encode(JSON.stringify(e)).length}catch{return}}}function w(e){let{tracer:s,spanNameFormatter:a=N,recordRequestBodySize:i=false,recordResponseBodySize:m=false,customAttributes:S={},propagateContext:y=true}=e,n,p,c;return async(o,u)=>{if(!n)try{n=await import('@opentelemetry/api'),p=n.context,c=n.propagation;}catch{return u(o)}let{host:E,port:d}=q(o.url),h=a(o),r=s.startSpan(h,{kind:n.SpanKind.CLIENT,attributes:{[g]:o.method,[A]:o.url,[C]:E,...d!==void 0&&{[b]:d},...S}});if(i&&o.body){let t=T(o.body);t!==void 0&&r.setAttribute(x,t);}let f={...o.headers};if(y&&c&&p){let t=p.active();c.inject(n.trace.setSpan(t,r),f);}let _={...o,headers:f};try{let t=await u(_);if(r.setAttribute(O,t.status),m&&t.data){let l=T(t.data);l!==void 0&&r.setAttribute(P,l);}return t.status>=400?(r.setStatus({code:n.SpanStatusCode.ERROR,message:`HTTP ${t.status} ${t.statusText}`}),r.setAttribute(R,`${t.status}`)):r.setStatus({code:n.SpanStatusCode.OK}),t}catch(t){throw r.setStatus({code:n.SpanStatusCode.ERROR,message:t instanceof Error?t.message:"Unknown error"}),t instanceof Error&&(r.recordException(t),r.setAttribute(R,t.name)),t}finally{r.end();}}}/* v8 ignore next -- @preserve ternary branch: https tests use http mock server */
2
+ /* v8 ignore next -- @preserve defensive: body is checked before calling this function */
3
+ /* v8 ignore next -- @preserve lazy-load pattern: second call skips initialization */
4
+ /* v8 ignore next 2 -- @preserve fallback when @opentelemetry/api is not installed */
5
+ /* v8 ignore next 3 -- @preserve defensive: size can be undefined for exotic body types */export{w as otel};//# sourceMappingURL=index.js.map
2
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tracing.ts"],"names":["ATTR_HTTP_REQUEST_METHOD","ATTR_URL_FULL","ATTR_HTTP_RESPONSE_STATUS_CODE","ATTR_SERVER_ADDRESS","ATTR_SERVER_PORT","ATTR_ERROR_TYPE","ATTR_HTTP_REQUEST_BODY_SIZE","ATTR_HTTP_RESPONSE_BODY_SIZE","defaultSpanNameFormatter","ctx","parseUrl","url","parsed","defaultPort","port","calculateBodySize","body","otel","options","tracer","spanNameFormatter","recordRequestBodySize","recordResponseBodySize","customAttributes","propagateContext","api","contextApi","propagation","next","host","spanName","span","size","headers","activeContext","enrichedCtx","response","error"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * OpenTelemetry tracing policy for unireq\n *\n * Creates spans for HTTP requests following OpenTelemetry semantic conventions.\n * Propagates trace context via W3C Trace Context headers.\n */\n\nimport type { Context, Tracer } from '@opentelemetry/api';\nimport type { Policy, RequestContext, Response } from '@unireq/core';\n\n// Use stable semantic convention attribute names (as of 1.28.0)\n// These are the stable HTTP client span attribute names per OpenTelemetry spec\nconst ATTR_HTTP_REQUEST_METHOD = 'http.request.method';\nconst ATTR_URL_FULL = 'url.full';\nconst ATTR_HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code';\nconst ATTR_SERVER_ADDRESS = 'server.address';\nconst ATTR_SERVER_PORT = 'server.port';\nconst ATTR_ERROR_TYPE = 'error.type';\nconst ATTR_HTTP_REQUEST_BODY_SIZE = 'http.request.body.size';\nconst ATTR_HTTP_RESPONSE_BODY_SIZE = 'http.response.body.size';\n\n/**\n * Function to format span names\n */\nexport type SpanNameFormatter = (ctx: RequestContext) => string;\n\n/**\n * Configuration options for OpenTelemetry tracing\n */\nexport interface OtelOptions {\n /**\n * OpenTelemetry tracer instance\n */\n readonly tracer: Tracer;\n\n /**\n * Custom span name formatter\n * @default (ctx) => `HTTP ${ctx.method}`\n */\n readonly spanNameFormatter?: SpanNameFormatter;\n\n /**\n * Record request body size in span attributes\n * @default false\n */\n readonly recordRequestBodySize?: boolean;\n\n /**\n * Record response body size in span attributes\n * @default false\n */\n readonly recordResponseBodySize?: boolean;\n\n /**\n * Custom attributes to add to every span\n */\n readonly customAttributes?: Record<string, string | number | boolean>;\n\n /**\n * Propagate trace context to downstream services\n * @default true\n */\n readonly propagateContext?: boolean;\n}\n\n/**\n * Default span name formatter\n */\nconst defaultSpanNameFormatter: SpanNameFormatter = (ctx) => `HTTP ${ctx.method}`;\n\n/**\n * Parse URL to extract host and port\n */\nfunction parseUrl(url: string): { host: string; port: number | undefined; protocol: string } {\n try {\n const parsed = new URL(url);\n const defaultPort = parsed.protocol === 'https:' ? 443 : 80;\n const port = parsed.port ? Number.parseInt(parsed.port, 10) : defaultPort;\n return {\n host: parsed.hostname,\n port,\n protocol: parsed.protocol.replace(':', ''),\n };\n } catch {\n return { host: 'unknown', port: undefined, protocol: 'http' };\n }\n}\n\n/**\n * Calculate body size for various body types\n */\nfunction calculateBodySize(body: unknown): number | undefined {\n if (!body) return undefined;\n\n if (typeof body === 'string') {\n return new TextEncoder().encode(body).length;\n }\n\n if (body instanceof ArrayBuffer) {\n return body.byteLength;\n }\n\n if (body instanceof Uint8Array) {\n return body.length;\n }\n\n if (typeof body === 'object') {\n try {\n return new TextEncoder().encode(JSON.stringify(body)).length;\n } catch {\n return undefined;\n }\n }\n\n return undefined;\n}\n\n/**\n * Create an OpenTelemetry tracing policy\n *\n * @example\n * ```typescript\n * import { client } from '@unireq/core';\n * import { http } from '@unireq/http';\n * import { otel } from '@unireq/otel';\n * import { trace } from '@opentelemetry/api';\n *\n * const tracer = trace.getTracer('my-service', '1.0.0');\n *\n * const api = client(\n * http('https://api.example.com'),\n * otel({\n * tracer,\n * spanNameFormatter: (ctx) => `${ctx.method} ${new URL(ctx.url).pathname}`,\n * customAttributes: { 'service.version': '1.0.0' },\n * }),\n * parse.json()\n * );\n * ```\n */\nexport function otel(options: OtelOptions): Policy {\n const {\n tracer,\n spanNameFormatter = defaultSpanNameFormatter,\n recordRequestBodySize = false,\n recordResponseBodySize = false,\n customAttributes = {},\n propagateContext = true,\n } = options;\n\n // Lazy import to avoid issues when OpenTelemetry is not available\n let api: typeof import('@opentelemetry/api') | undefined;\n let contextApi: { active: () => Context } | undefined;\n let propagation: { inject: (context: Context, carrier: Record<string, string>) => void } | undefined;\n\n return async (ctx: RequestContext, next): Promise<Response> => {\n // Lazy load OpenTelemetry API\n if (!api) {\n try {\n api = await import('@opentelemetry/api');\n contextApi = api.context;\n propagation = api.propagation;\n } catch {\n /* c8 ignore next 2 */ // Fallback when @opentelemetry/api is not installed\n return next(ctx);\n }\n }\n\n const { host, port } = parseUrl(ctx.url);\n const spanName = spanNameFormatter(ctx);\n\n // Create span with HTTP client kind\n const span = tracer.startSpan(spanName, {\n kind: api.SpanKind.CLIENT,\n attributes: {\n [ATTR_HTTP_REQUEST_METHOD]: ctx.method,\n [ATTR_URL_FULL]: ctx.url,\n [ATTR_SERVER_ADDRESS]: host,\n ...(port !== undefined && { [ATTR_SERVER_PORT]: port }),\n ...customAttributes,\n },\n });\n\n // Record request body size if enabled\n if (recordRequestBodySize && ctx.body) {\n const size = calculateBodySize(ctx.body);\n if (size !== undefined) {\n span.setAttribute(ATTR_HTTP_REQUEST_BODY_SIZE, size);\n }\n }\n\n // Inject trace context into request headers\n const headers = { ...ctx.headers };\n if (propagateContext && propagation && contextApi) {\n const activeContext = contextApi.active();\n propagation.inject(api.trace.setSpan(activeContext, span), headers);\n }\n\n const enrichedCtx: RequestContext = {\n ...ctx,\n headers,\n };\n\n try {\n const response = await next(enrichedCtx);\n\n // Record response attributes\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);\n\n // Record response body size if enabled\n if (recordResponseBodySize && response.data) {\n const size = calculateBodySize(response.data);\n if (size !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_BODY_SIZE, size);\n }\n }\n\n // Set span status based on response\n if (response.status >= 400) {\n span.setStatus({\n code: api.SpanStatusCode.ERROR,\n message: `HTTP ${response.status} ${response.statusText}`,\n });\n span.setAttribute(ATTR_ERROR_TYPE, `${response.status}`);\n } else {\n span.setStatus({ code: api.SpanStatusCode.OK });\n }\n\n return response;\n } catch (error) {\n // Record error\n span.setStatus({\n code: api.SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n\n if (error instanceof Error) {\n span.recordException(error);\n span.setAttribute(ATTR_ERROR_TYPE, error.name);\n }\n\n throw error;\n } finally {\n span.end();\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/tracing.ts"],"names":["ATTR_HTTP_REQUEST_METHOD","ATTR_URL_FULL","ATTR_HTTP_RESPONSE_STATUS_CODE","ATTR_SERVER_ADDRESS","ATTR_SERVER_PORT","ATTR_ERROR_TYPE","ATTR_HTTP_REQUEST_BODY_SIZE","ATTR_HTTP_RESPONSE_BODY_SIZE","defaultSpanNameFormatter","ctx","parseUrl","url","parsed","defaultPort","port","calculateBodySize","body","otel","options","tracer","spanNameFormatter","recordRequestBodySize","recordResponseBodySize","customAttributes","propagateContext","api","contextApi","propagation","next","host","spanName","span","size","headers","activeContext","enrichedCtx","response","error"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * OpenTelemetry tracing policy for unireq\n *\n * Creates spans for HTTP requests following OpenTelemetry semantic conventions.\n * Propagates trace context via W3C Trace Context headers.\n */\n\nimport type { Context, Tracer } from '@opentelemetry/api';\nimport type { Policy, RequestContext, Response } from '@unireq/core';\n\n// Use stable semantic convention attribute names (as of 1.28.0)\n// These are the stable HTTP client span attribute names per OpenTelemetry spec\nconst ATTR_HTTP_REQUEST_METHOD = 'http.request.method';\nconst ATTR_URL_FULL = 'url.full';\nconst ATTR_HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code';\nconst ATTR_SERVER_ADDRESS = 'server.address';\nconst ATTR_SERVER_PORT = 'server.port';\nconst ATTR_ERROR_TYPE = 'error.type';\nconst ATTR_HTTP_REQUEST_BODY_SIZE = 'http.request.body.size';\nconst ATTR_HTTP_RESPONSE_BODY_SIZE = 'http.response.body.size';\n\n/**\n * Function to format span names\n */\nexport type SpanNameFormatter = (ctx: RequestContext) => string;\n\n/**\n * Configuration options for OpenTelemetry tracing\n */\nexport interface OtelOptions {\n /**\n * OpenTelemetry tracer instance\n */\n readonly tracer: Tracer;\n\n /**\n * Custom span name formatter\n * @default (ctx) => `HTTP ${ctx.method}`\n */\n readonly spanNameFormatter?: SpanNameFormatter;\n\n /**\n * Record request body size in span attributes\n * @default false\n */\n readonly recordRequestBodySize?: boolean;\n\n /**\n * Record response body size in span attributes\n * @default false\n */\n readonly recordResponseBodySize?: boolean;\n\n /**\n * Custom attributes to add to every span\n */\n readonly customAttributes?: Record<string, string | number | boolean>;\n\n /**\n * Propagate trace context to downstream services\n * @default true\n */\n readonly propagateContext?: boolean;\n}\n\n/**\n * Default span name formatter\n */\nconst defaultSpanNameFormatter: SpanNameFormatter = (ctx) => `HTTP ${ctx.method}`;\n\n/**\n * Parse URL to extract host and port\n */\nfunction parseUrl(url: string): { host: string; port: number | undefined; protocol: string } {\n try {\n const parsed = new URL(url);\n /* v8 ignore next -- @preserve ternary branch: https tests use http mock server */\n const defaultPort = parsed.protocol === 'https:' ? 443 : 80;\n const port = parsed.port ? Number.parseInt(parsed.port, 10) : defaultPort;\n return {\n host: parsed.hostname,\n port,\n protocol: parsed.protocol.replace(':', ''),\n };\n } catch {\n return { host: 'unknown', port: undefined, protocol: 'http' };\n }\n}\n\n/**\n * Calculate body size for various body types\n */\nfunction calculateBodySize(body: unknown): number | undefined {\n /* v8 ignore next -- @preserve defensive: body is checked before calling this function */\n if (!body) return undefined;\n\n if (typeof body === 'string') {\n return new TextEncoder().encode(body).length;\n }\n\n if (body instanceof ArrayBuffer) {\n return body.byteLength;\n }\n\n if (body instanceof Uint8Array) {\n return body.length;\n }\n\n if (typeof body === 'object') {\n try {\n return new TextEncoder().encode(JSON.stringify(body)).length;\n } catch {\n return undefined;\n }\n }\n\n return undefined;\n}\n\n/**\n * Create an OpenTelemetry tracing policy\n *\n * @example\n * ```typescript\n * import { client } from '@unireq/core';\n * import { http } from '@unireq/http';\n * import { otel } from '@unireq/otel';\n * import { trace } from '@opentelemetry/api';\n *\n * const tracer = trace.getTracer('my-service', '1.0.0');\n *\n * const api = client(\n * http('https://api.example.com'),\n * otel({\n * tracer,\n * spanNameFormatter: (ctx) => `${ctx.method} ${new URL(ctx.url).pathname}`,\n * customAttributes: { 'service.version': '1.0.0' },\n * }),\n * parse.json()\n * );\n * ```\n */\nexport function otel(options: OtelOptions): Policy {\n const {\n tracer,\n spanNameFormatter = defaultSpanNameFormatter,\n recordRequestBodySize = false,\n recordResponseBodySize = false,\n customAttributes = {},\n propagateContext = true,\n } = options;\n\n // Lazy import to avoid issues when OpenTelemetry is not available\n let api: typeof import('@opentelemetry/api') | undefined;\n let contextApi: { active: () => Context } | undefined;\n let propagation: { inject: (context: Context, carrier: Record<string, string>) => void } | undefined;\n\n return async (ctx: RequestContext, next): Promise<Response> => {\n // Lazy load OpenTelemetry API\n /* v8 ignore next -- @preserve lazy-load pattern: second call skips initialization */\n if (!api) {\n try {\n api = await import('@opentelemetry/api');\n contextApi = api.context;\n propagation = api.propagation;\n } catch {\n /* v8 ignore next 2 -- @preserve fallback when @opentelemetry/api is not installed */\n return next(ctx);\n }\n }\n\n const { host, port } = parseUrl(ctx.url);\n const spanName = spanNameFormatter(ctx);\n\n // Create span with HTTP client kind\n const span = tracer.startSpan(spanName, {\n kind: api.SpanKind.CLIENT,\n attributes: {\n [ATTR_HTTP_REQUEST_METHOD]: ctx.method,\n [ATTR_URL_FULL]: ctx.url,\n [ATTR_SERVER_ADDRESS]: host,\n ...(port !== undefined && { [ATTR_SERVER_PORT]: port }),\n ...customAttributes,\n },\n });\n\n // Record request body size if enabled\n if (recordRequestBodySize && ctx.body) {\n const size = calculateBodySize(ctx.body);\n /* v8 ignore next 3 -- @preserve defensive: size can be undefined for exotic body types */\n if (size !== undefined) {\n span.setAttribute(ATTR_HTTP_REQUEST_BODY_SIZE, size);\n }\n }\n\n // Inject trace context into request headers\n const headers = { ...ctx.headers };\n if (propagateContext && propagation && contextApi) {\n const activeContext = contextApi.active();\n propagation.inject(api.trace.setSpan(activeContext, span), headers);\n }\n\n const enrichedCtx: RequestContext = {\n ...ctx,\n headers,\n };\n\n try {\n const response = await next(enrichedCtx);\n\n // Record response attributes\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);\n\n // Record response body size if enabled\n if (recordResponseBodySize && response.data) {\n const size = calculateBodySize(response.data);\n /* v8 ignore next 3 -- @preserve defensive: size can be undefined for exotic body types */\n if (size !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_BODY_SIZE, size);\n }\n }\n\n // Set span status based on response\n if (response.status >= 400) {\n span.setStatus({\n code: api.SpanStatusCode.ERROR,\n message: `HTTP ${response.status} ${response.statusText}`,\n });\n span.setAttribute(ATTR_ERROR_TYPE, `${response.status}`);\n } else {\n span.setStatus({ code: api.SpanStatusCode.OK });\n }\n\n return response;\n } catch (error) {\n // Record error\n span.setStatus({\n code: api.SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n\n if (error instanceof Error) {\n span.recordException(error);\n span.setAttribute(ATTR_ERROR_TYPE, error.name);\n }\n\n throw error;\n } finally {\n span.end();\n }\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unireq/otel",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenTelemetry instrumentation for unireq HTTP clients",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,18 +25,18 @@
25
25
  "author": "Olivier Orabona",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "@unireq/core": "1.0.0"
28
+ "@unireq/core": "1.0.2"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@opentelemetry/api": "^1.9.0",
32
- "@opentelemetry/semantic-conventions": "^1.28.0"
32
+ "@opentelemetry/semantic-conventions": "^1.40.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@opentelemetry/api": "^1.9.0",
36
- "@opentelemetry/semantic-conventions": "^1.28.0",
36
+ "@opentelemetry/semantic-conventions": "^1.40.0",
37
37
  "typescript": "^5.9.3",
38
38
  "tsup": "^8.5.1",
39
- "vitest": "^4.0.16"
39
+ "vitest": "^4.0.18"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=18.0.0"