@okfetch/otel 0.5.0

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,125 @@
1
+ # @okfetch/otel
2
+
3
+ `@okfetch/otel` is an [OpenTelemetry](https://opentelemetry.io/) tracing plugin for okfetch request lifecycles.
4
+
5
+ It gives you a ready-made `OkfetchPlugin` that records **one `CLIENT` span per request**, covering every retry attempt, with:
6
+
7
+ - the request method, URL, path, and query string
8
+ - explicitly selected request and response headers, with sensitive values redacted
9
+ - the response status code
10
+ - optional request and response body sizes when they can be observed accurately
11
+ - a `traceparent` header on the outgoing request so downstream services join the trace
12
+
13
+ Request and response bodies are never recorded.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ bun add @okfetch/otel @okfetch/fetch @opentelemetry/api
19
+ ```
20
+
21
+ ```bash
22
+ npm install @okfetch/otel @okfetch/fetch @opentelemetry/api
23
+ ```
24
+
25
+ You also need an OpenTelemetry SDK registered as the global tracer provider (for example `@opentelemetry/sdk-node`). Without one, the plugin is a no-op.
26
+
27
+ ## Usage
28
+
29
+ ```ts
30
+ import { okfetch } from "@okfetch/fetch";
31
+ import { otel } from "@okfetch/otel";
32
+
33
+ const result = await okfetch("https://api.example.com/todos/:id", {
34
+ params: { id: 1 },
35
+ plugins: [otel()],
36
+ query: { include: "owner" },
37
+ });
38
+ ```
39
+
40
+ Put `otel()` after plugins that add headers if you want those headers captured on the span. Header capture is opt-in, as required by the OpenTelemetry security guidance.
41
+
42
+ ## API
43
+
44
+ `otel(options?)`
45
+
46
+ Options:
47
+
48
+ - `tracer?: Tracer` - tracer used to start spans. Defaults to `trace.getTracer("@okfetch/otel")` from the global provider.
49
+ - `captureRequestHeaders?: boolean | readonly string[]` - request header names to record as `http.request.header.<name>`. Defaults to none. Pass `true` to explicitly capture every request header.
50
+ - `captureResponseHeaders?: boolean | readonly string[]` - response header names to record as `http.response.header.<name>`. Defaults to none. Pass `true` to explicitly capture every response header.
51
+ - `captureBodySizes?: boolean` - record payload body sizes when Fetch exposes enough information to do so accurately. Defaults to `false`.
52
+ - `knownMethods?: readonly string[]` - case-sensitive methods known to the instrumentation. Replaces `DEFAULT_KNOWN_HTTP_METHODS` entirely.
53
+ - `propagateTraceContext?: boolean` - inject W3C `traceparent` / `tracestate` headers into the request. Defaults to `true`.
54
+ - `redact?: { headers?, queryParams?, values? }` - what to redact. `headers` and `queryParams` match names; `values` matches header and query values regardless of name. Each entry is either an array that **replaces** the defaults, or a function that **receives the defaults** and returns the list to use.
55
+
56
+ ```ts
57
+ otel({
58
+ captureRequestHeaders: ["content-type", "x-tenant"],
59
+ captureResponseHeaders: ["content-type", "x-request-id"],
60
+ redact: {
61
+ // extend the defaults
62
+ headers: (defaults) => [...defaults, "x-tenant", /^x-internal-/i],
63
+ // replace the defaults entirely
64
+ queryParams: ["customer_id"],
65
+ // also redact any value that looks like an internal ticket id
66
+ values: (defaults) => [...defaults, /^TKT-/],
67
+ },
68
+ });
69
+ ```
70
+
71
+ Exports:
72
+
73
+ - `DEFAULT_REDACTED_HEADERS` - `authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `x-api-key`, `x-auth-token`, `api-key`, `x-amz-security-token`, `x-amz-credential`, `x-amz-signature`, plus `DEFAULT_REDACTED_NAME_PATTERN`
74
+ - `DEFAULT_KNOWN_HTTP_METHODS` - the RFC 9110 methods plus `PATCH` and `QUERY`
75
+ - `DEFAULT_REDACTED_QUERY_PARAMS` - common credential parameter names such as `token`, `access_token`, `api_key`, `password`, `secret`, `signature`, the OAuth grant parameters `code`, `code_verifier`, `client_assertion`, `assertion`, the AWS SigV4 presigned-URL fields `X-Amz-Credential`, `X-Amz-Security-Token`, `X-Amz-Signature`, plus `DEFAULT_REDACTED_NAME_PATTERN`
76
+ - `DEFAULT_REDACTED_NAME_PATTERN` - a pattern included in both default lists; any name containing `auth`, `bearer`, `cred`, `jwt`, `otp`, `passw`, `private`, `secret`, `session`, `sig`, `token`, or `api-key` is redacted even when not listed explicitly. Replacing a list with an array drops it, so include it yourself if you still want it.
77
+ - `DEFAULT_REDACTED_VALUE_PATTERNS` - patterns applied to every header and query value whatever its name: JWTs (`eyJ...` with three segments) and HTTP authentication credentials (`Bearer`, `Basic`, `Digest`, `Negotiate`, `Token`, `OAuth`, `AWS4-HMAC-SHA256` prefixes)
78
+ - `RedactionMatcher`, `RedactionList`, `RedactionOption`, `ValuePatternList`, `ValuePatternOption` - the types behind the `redact` option
79
+ - `HeaderCaptureOption` - the type accepted by the request and response header capture options
80
+ - `REDACTED_VALUE` - the `REDACTED` placeholder written in place of redacted values
81
+
82
+ Name matching is case-insensitive for both headers and query parameters. Redaction errs on the side of hiding too much: a name such as `X-Session-Id` is redacted because it matches the default pattern, and a JWT sent under a harmless-looking name is redacted because of its value.
83
+
84
+ ## What It Records
85
+
86
+ Span name: `{method}`, or `{method} {path template}` when the request uses `params` (for example `GET /todos/:id`).
87
+
88
+ Attributes follow the OpenTelemetry HTTP semantic conventions where one exists:
89
+
90
+ | Attribute | Value |
91
+ | ------------------------------ | ---------------------------------------------------------------------------- |
92
+ | `http.request.method` | Known request method, or `_OTHER` |
93
+ | `http.request.method_original` | Original method when `http.request.method` is `_OTHER` |
94
+ | `http.request.body.size` | Request payload bytes, when enabled and accurately observable |
95
+ | `http.request.header.<name>` | Explicitly selected request header values, with sensitive values redacted |
96
+ | `http.request.resend_count` | Number of retries performed |
97
+ | `http.response.body.size` | Response payload bytes from `Content-Length`, when enabled and applicable |
98
+ | `http.response.header.<name>` | Explicitly selected response header values, with sensitive values redacted |
99
+ | `http.response.status_code` | Status code of the final response |
100
+ | `url.full` | Full URL with redacted query parameters and credentials, fragment dropped |
101
+ | `url.scheme` | URL scheme |
102
+ | `url.path` | URL path |
103
+ | `url.query` | Query string with redacted parameters (omitted when empty) |
104
+ | `url.template` | Path template when `params` are used, without query, fragment or credentials |
105
+ | `server.address` | Hostname |
106
+ | `server.port` | Explicit or scheme-default server port |
107
+ | `error.type` | Status code for API errors, otherwise the okfetch error tag |
108
+ | `okfetch.error.tag` | okfetch error tag (`ApiError`, `FetchError`, ...) |
109
+ | `okfetch.validation.issues` | Formatted schema issues for validation failures |
110
+
111
+ Failure handling:
112
+
113
+ - `ApiError` (non-2xx): span status `ERROR`; `http.response.status_code` carries the reason, so no redundant status description is set
114
+ - `FetchError`, `TimeoutError`, `ParseError`, `PluginError`: span status `ERROR` with the error message, plus an `exception` event via `span.recordException`
115
+ - `ValidationError`: the same failure details, with formatted schema issues added to the status message, exception message and `okfetch.validation.issues`
116
+
117
+ Every retry adds an `okfetch.retry` event carrying the attempt number, the error tag, and the status code when a response was received.
118
+
119
+ ### HTTP registry coverage
120
+
121
+ The plugin emits every HTTP registry attribute that applies to a Fetch client and can be observed accurately. `http.connection.state` belongs to connection-pool metrics, and `http.route` belongs to server spans, so neither applies. Fetch does not expose protocol framing or total bytes on the wire, so `http.request.size` and `http.response.size` are intentionally omitted rather than estimated. Response body size is omitted for `HEAD`, `204`, and `304` responses and whenever no valid `Content-Length` is available. Deprecated HTTP attributes are never emitted.
122
+
123
+ ## Relationship To `@okfetch/fetch`
124
+
125
+ This package is just a plugin built on top of the public `OkfetchPlugin` interface from `@okfetch/fetch`. It only depends on `@opentelemetry/api`, so it works with any OpenTelemetry SDK setup.
package/dist/index.cjs ADDED
@@ -0,0 +1,354 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _opentelemetry_api = require("@opentelemetry/api");
3
+ //#region packages/otel/index.ts
4
+ const PLUGIN_NAME = "otel";
5
+ const PLUGIN_VERSION = "0.1.0";
6
+ const TRACER_NAME = "@okfetch/otel";
7
+ /** HTTP methods known by default, as defined by the HTTP semantic conventions. */
8
+ const DEFAULT_KNOWN_HTTP_METHODS = [
9
+ "CONNECT",
10
+ "DELETE",
11
+ "GET",
12
+ "HEAD",
13
+ "OPTIONS",
14
+ "PATCH",
15
+ "POST",
16
+ "PUT",
17
+ "QUERY",
18
+ "TRACE"
19
+ ];
20
+ /** Value written in place of redacted headers and query parameters. */
21
+ const REDACTED_VALUE = "REDACTED";
22
+ /**
23
+ * Header and query parameter names matching this pattern are redacted even
24
+ * when they are not listed explicitly, so vendor-specific credential fields
25
+ * such as `X-Amz-Security-Token` or `X-Goog-Signature` never leak. It is part
26
+ * of both default lists.
27
+ */
28
+ const DEFAULT_REDACTED_NAME_PATTERN = /auth|bearer|cred|jwt|otp|passw|private|secret|session|sig|token|api[-_]?key/i;
29
+ /**
30
+ * Header and query parameter values matching one of these patterns are
31
+ * redacted whatever their name, so a credential carried under an arbitrary
32
+ * name (`X-JWT`, `blob`, ...) still never reaches telemetry. Covers JWTs and
33
+ * HTTP authentication credentials (`Bearer`, `Basic`, `Digest`, ...).
34
+ */
35
+ const DEFAULT_REDACTED_VALUE_PATTERNS = [/^eyJ[\w-]+\.[\w-]+\.[\w-]*$/, /^(?:Bearer|Basic|Digest|Negotiate|Token|OAuth|AWS4-HMAC-SHA256)\s/i];
36
+ /** Request headers whose values are never recorded on spans. */
37
+ const DEFAULT_REDACTED_HEADERS = [
38
+ "authorization",
39
+ "proxy-authorization",
40
+ "cookie",
41
+ "set-cookie",
42
+ "x-api-key",
43
+ "x-auth-token",
44
+ "api-key",
45
+ "x-amz-security-token",
46
+ "x-amz-credential",
47
+ "x-amz-signature",
48
+ DEFAULT_REDACTED_NAME_PATTERN
49
+ ];
50
+ /** Query parameters whose values are never recorded on spans. */
51
+ const DEFAULT_REDACTED_QUERY_PARAMS = [
52
+ "access_token",
53
+ "api_key",
54
+ "apikey",
55
+ "assertion",
56
+ "auth",
57
+ "authorization",
58
+ "awsaccesskeyid",
59
+ "client_assertion",
60
+ "client_secret",
61
+ "code",
62
+ "code_verifier",
63
+ "id_token",
64
+ "key",
65
+ "password",
66
+ "refresh_token",
67
+ "secret",
68
+ "sig",
69
+ "signature",
70
+ "token",
71
+ "x-amz-credential",
72
+ "x-amz-security-token",
73
+ "x-amz-signature",
74
+ "x-goog-signature",
75
+ DEFAULT_REDACTED_NAME_PATTERN
76
+ ];
77
+ const stateKey = Symbol.for("@okfetch/otel:state");
78
+ const getState = (carrier) => carrier[stateKey];
79
+ const pathParamPattern = /:[A-Za-z_]\w*(?=[/?#]|$)/;
80
+ const userInfoPattern = /^([a-z][a-z\d+.-]*:\/\/)[^/]*@/i;
81
+ const contentLengthPattern = /^\d+$/;
82
+ /**
83
+ * Reduces a raw request URL to its low-cardinality absolute-path template.
84
+ * Query values, fragments, origins, and embedded credentials are excluded.
85
+ */
86
+ const toTemplate = (rawUrl) => {
87
+ const [withoutQuery = ""] = rawUrl.split(/[?#]/, 1);
88
+ const withoutUserInfo = withoutQuery.replace(userInfoPattern, "$1");
89
+ if (!pathParamPattern.test(withoutUserInfo)) return;
90
+ try {
91
+ return new URL(withoutUserInfo).pathname;
92
+ } catch {
93
+ return withoutUserInfo;
94
+ }
95
+ };
96
+ const headersSetter = { set: (carrier, key, value) => {
97
+ carrier.set(key, value);
98
+ } };
99
+ const resolveList = (defaults, option) => {
100
+ if (option === void 0) return defaults;
101
+ if (Array.isArray(option)) return option;
102
+ return option(defaults);
103
+ };
104
+ const testPattern = (pattern, input) => {
105
+ pattern.lastIndex = 0;
106
+ return pattern.test(input);
107
+ };
108
+ const createValueMatcher = (patterns) => (value) => patterns.some((pattern) => testPattern(pattern, value));
109
+ const createNameMatcher = (list) => {
110
+ const names = /* @__PURE__ */ new Set();
111
+ const patterns = [];
112
+ for (const matcher of list) if (typeof matcher === "string") names.add(matcher.toLowerCase());
113
+ else patterns.push(matcher);
114
+ return (name) => names.has(name.toLowerCase()) || patterns.some((pattern) => testPattern(pattern, name));
115
+ };
116
+ const resolveCapturedHeaders = (option) => option === true ? true : new Set(option === false || option === void 0 ? [] : option.map((name) => name.toLowerCase()));
117
+ const resolveOptions = (options) => ({
118
+ captureBodySizes: options?.captureBodySizes ?? false,
119
+ capturedRequestHeaders: resolveCapturedHeaders(options?.captureRequestHeaders),
120
+ capturedResponseHeaders: resolveCapturedHeaders(options?.captureResponseHeaders),
121
+ propagateTraceContext: options?.propagateTraceContext ?? true,
122
+ isRedactedHeader: createNameMatcher(resolveList(DEFAULT_REDACTED_HEADERS, options?.redact?.headers)),
123
+ isRedactedQueryParam: createNameMatcher(resolveList(DEFAULT_REDACTED_QUERY_PARAMS, options?.redact?.queryParams)),
124
+ isRedactedValue: createValueMatcher(resolveList(DEFAULT_REDACTED_VALUE_PATTERNS, options?.redact?.values)),
125
+ knownMethods: new Set(options?.knownMethods ?? DEFAULT_KNOWN_HTTP_METHODS),
126
+ tracer: options?.tracer ?? _opentelemetry_api.trace.getTracer(TRACER_NAME, PLUGIN_VERSION)
127
+ });
128
+ const getMethodAttributes = (method, knownMethods) => {
129
+ const semanticMethod = knownMethods.has(method) ? method : "_OTHER";
130
+ const attributes = { "http.request.method": semanticMethod };
131
+ if (method !== semanticMethod) attributes["http.request.method_original"] = method;
132
+ return attributes;
133
+ };
134
+ const getContentLength = (headers) => {
135
+ const value = headers.get("content-length")?.trim();
136
+ if (!value || !contentLengthPattern.test(value)) return;
137
+ const size = Number(value);
138
+ return Number.isSafeInteger(size) ? size : void 0;
139
+ };
140
+ const getRequestBodySize = (body, headers) => {
141
+ if (body === void 0) return;
142
+ const contentLength = getContentLength(headers);
143
+ if (contentLength !== void 0) return contentLength;
144
+ if (headers.has("content-encoding")) return;
145
+ if (typeof body === "string") return new TextEncoder().encode(body).byteLength;
146
+ if (body instanceof URLSearchParams) return new TextEncoder().encode(body.toString()).byteLength;
147
+ if (body instanceof Blob) return body.size;
148
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return body.byteLength;
149
+ };
150
+ const shouldCaptureHeader = (capturedHeaders, name) => capturedHeaders === true || capturedHeaders.has(name);
151
+ const captureHeaders = (attributes, prefix, headers, capturedHeaders, options) => {
152
+ for (const [name, value] of headers) {
153
+ if (!shouldCaptureHeader(capturedHeaders, name)) continue;
154
+ const shouldRedact = options.isRedactedHeader(name) || options.isRedactedValue(value);
155
+ attributes[`${prefix}.${name}`] = [shouldRedact ? REDACTED_VALUE : value];
156
+ }
157
+ };
158
+ const getServerPort = (url) => {
159
+ if (url.port) return Number(url.port);
160
+ if (url.protocol === "http:") return 80;
161
+ if (url.protocol === "https:") return 443;
162
+ };
163
+ const redactUrl = (url, options) => {
164
+ const redacted = new URL(url.toString());
165
+ redacted.hash = "";
166
+ if (redacted.username) redacted.username = REDACTED_VALUE;
167
+ if (redacted.password) redacted.password = REDACTED_VALUE;
168
+ for (const name of new Set(redacted.searchParams.keys())) if (options.isRedactedQueryParam(name) || redacted.searchParams.getAll(name).some(options.isRedactedValue)) redacted.searchParams.set(name, REDACTED_VALUE);
169
+ return redacted;
170
+ };
171
+ const buildRequestAttributes = (ctx, template, options) => {
172
+ const url = redactUrl(ctx.url, options);
173
+ const attributes = {
174
+ ...getMethodAttributes(ctx.method, options.knownMethods),
175
+ "server.address": url.hostname,
176
+ "url.full": url.toString(),
177
+ "url.path": url.pathname,
178
+ "url.scheme": url.protocol.replace(/:$/, "")
179
+ };
180
+ const serverPort = getServerPort(url);
181
+ if (serverPort !== void 0) attributes["server.port"] = serverPort;
182
+ if (url.search) attributes["url.query"] = url.search.slice(1);
183
+ if (template) attributes["url.template"] = template;
184
+ if (options.captureBodySizes) {
185
+ const bodySize = getRequestBodySize(ctx.body, ctx.headers);
186
+ if (bodySize !== void 0) attributes["http.request.body.size"] = bodySize;
187
+ }
188
+ captureHeaders(attributes, "http.request.header", ctx.headers, options.capturedRequestHeaders, options);
189
+ return attributes;
190
+ };
191
+ const startSpan = (ctx, state, options) => {
192
+ const method = options.knownMethods.has(ctx.method) ? ctx.method : "HTTP";
193
+ const name = state.template ? `${method} ${state.template}` : method;
194
+ return options.tracer.startSpan(name, {
195
+ attributes: buildRequestAttributes(ctx, state.template, options),
196
+ kind: _opentelemetry_api.SpanKind.CLIENT
197
+ }, _opentelemetry_api.context.active());
198
+ };
199
+ /**
200
+ * Injects trace context headers into a copy of the request headers. A
201
+ * propagator failure is recorded on the span and the request proceeds without
202
+ * propagation headers: telemetry must never fail the request or leave the
203
+ * span open.
204
+ */
205
+ const injectTraceContext = (ctx, span) => {
206
+ const headers = new Headers(ctx.headers);
207
+ try {
208
+ _opentelemetry_api.propagation.inject(_opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), span), headers, headersSetter);
209
+ } catch (error) {
210
+ span.addEvent("okfetch.propagation_failed", { "exception.message": error instanceof Error ? error.message : String(error) });
211
+ return ctx;
212
+ }
213
+ return {
214
+ ...ctx,
215
+ headers
216
+ };
217
+ };
218
+ const endSpan = (state) => {
219
+ if (state.ended) return;
220
+ state.ended = true;
221
+ state.span?.end();
222
+ };
223
+ const recordResponse = (span, ctx, response, options) => {
224
+ const attributes = { "http.response.status_code": response.status };
225
+ if (options.captureBodySizes && ctx.method !== "HEAD" && response.status !== 204 && response.status !== 304) {
226
+ const bodySize = getContentLength(response.headers);
227
+ if (bodySize !== void 0) attributes["http.response.body.size"] = bodySize;
228
+ }
229
+ captureHeaders(attributes, "http.response.header", response.headers, options.capturedResponseHeaders, options);
230
+ span.setAttributes(attributes);
231
+ };
232
+ const recordFailure = (span, ctx, response, error, options) => {
233
+ span.setAttribute("okfetch.error.tag", error._tag);
234
+ if (response) recordResponse(span, ctx, response, options);
235
+ if (error._tag === "ApiError") {
236
+ span.setAttributes({
237
+ "error.type": String(error.statusCode),
238
+ "http.response.status_code": error.statusCode
239
+ });
240
+ span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
241
+ return;
242
+ }
243
+ if (error._tag === "ValidationError") {
244
+ const issues = error.issues.map((issue) => {
245
+ const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
246
+ return path ? `${path}: ${issue.message}` : issue.message;
247
+ });
248
+ const message = issues.length > 0 ? `${error.message}: ${issues.join("; ")}` : error.message;
249
+ span.setAttribute("okfetch.validation.issues", issues);
250
+ span.setAttribute("error.type", error._tag);
251
+ span.recordException({
252
+ message,
253
+ name: error._tag,
254
+ stack: error.stack
255
+ });
256
+ span.setStatus({
257
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
258
+ message
259
+ });
260
+ return;
261
+ }
262
+ span.setAttribute("error.type", error._tag);
263
+ span.recordException(error);
264
+ span.setStatus({
265
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
266
+ message: error.message
267
+ });
268
+ };
269
+ /**
270
+ * Creates an OpenTelemetry plugin that records a single `CLIENT` span per
271
+ * okfetch request, spanning every retry attempt.
272
+ *
273
+ * The span records HTTP client attributes available through Fetch and marks
274
+ * failures using the OpenTelemetry HTTP span conventions. Request and response
275
+ * bodies are never recorded.
276
+ */
277
+ const otel = (options) => {
278
+ const resolved = resolveOptions(options);
279
+ const init = ({ options: requestOptions, url }) => {
280
+ const state = {
281
+ ended: false,
282
+ resendCount: 0,
283
+ template: requestOptions.params === void 0 ? void 0 : toTemplate(url)
284
+ };
285
+ return {
286
+ options: {
287
+ ...requestOptions,
288
+ [stateKey]: state
289
+ },
290
+ url
291
+ };
292
+ };
293
+ const onRequest = (ctx) => {
294
+ const state = getState(ctx) ?? {
295
+ ended: false,
296
+ resendCount: 0
297
+ };
298
+ let nextCtx = {
299
+ ...ctx,
300
+ [stateKey]: state
301
+ };
302
+ if (state.span) {
303
+ state.resendCount += 1;
304
+ state.span.setAttribute("http.request.resend_count", state.resendCount);
305
+ } else state.span = startSpan(nextCtx, state, resolved);
306
+ if (resolved.propagateTraceContext) nextCtx = {
307
+ ...injectTraceContext(nextCtx, state.span),
308
+ [stateKey]: state
309
+ };
310
+ return nextCtx;
311
+ };
312
+ return {
313
+ name: PLUGIN_NAME,
314
+ version: PLUGIN_VERSION,
315
+ init,
316
+ hooks: {
317
+ onRequest,
318
+ onSuccess(ctx, response) {
319
+ const state = getState(ctx);
320
+ if (!state?.span) return;
321
+ recordResponse(state.span, ctx, response, resolved);
322
+ endSpan(state);
323
+ },
324
+ onFail(ctx, response, error) {
325
+ const state = getState(ctx);
326
+ if (!state) return;
327
+ state.span ??= startSpan(ctx, state, resolved);
328
+ recordFailure(state.span, ctx, response, error, resolved);
329
+ endSpan(state);
330
+ },
331
+ onRetry(ctx, response, error, attempt) {
332
+ const state = getState(ctx);
333
+ if (!state?.span) return;
334
+ const attributes = {
335
+ "error.type": error._tag === "ApiError" ? String(error.statusCode) : error._tag,
336
+ "okfetch.error.tag": error._tag,
337
+ "okfetch.retry.attempt": attempt + 1
338
+ };
339
+ if (response) attributes["http.response.status_code"] = response.status;
340
+ state.span.addEvent("okfetch.retry", attributes);
341
+ }
342
+ }
343
+ };
344
+ };
345
+ //#endregion
346
+ exports.DEFAULT_KNOWN_HTTP_METHODS = DEFAULT_KNOWN_HTTP_METHODS;
347
+ exports.DEFAULT_REDACTED_HEADERS = DEFAULT_REDACTED_HEADERS;
348
+ exports.DEFAULT_REDACTED_NAME_PATTERN = DEFAULT_REDACTED_NAME_PATTERN;
349
+ exports.DEFAULT_REDACTED_QUERY_PARAMS = DEFAULT_REDACTED_QUERY_PARAMS;
350
+ exports.DEFAULT_REDACTED_VALUE_PATTERNS = DEFAULT_REDACTED_VALUE_PATTERNS;
351
+ exports.REDACTED_VALUE = REDACTED_VALUE;
352
+ exports.otel = otel;
353
+
354
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["trace","SpanKind","context","SpanStatusCode"],"sources":["../index.ts"],"sourcesContent":["import type {\n OkfetchError,\n OkfetchOptions,\n OkfetchPlugin,\n OkfetchPluginInitInput,\n OkfetchRequestContext,\n} from \"@okfetch/fetch\";\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from \"@opentelemetry/api\";\nimport type {\n Attributes,\n Span,\n TextMapSetter,\n Tracer,\n} from \"@opentelemetry/api\";\n\nconst PLUGIN_NAME = \"otel\";\nconst PLUGIN_VERSION = \"0.1.0\";\nconst TRACER_NAME = \"@okfetch/otel\";\n\n/** HTTP methods known by default, as defined by the HTTP semantic conventions. */\nexport const DEFAULT_KNOWN_HTTP_METHODS = [\n \"CONNECT\",\n \"DELETE\",\n \"GET\",\n \"HEAD\",\n \"OPTIONS\",\n \"PATCH\",\n \"POST\",\n \"PUT\",\n \"QUERY\",\n \"TRACE\",\n] as const;\n\n/** Value written in place of redacted headers and query parameters. */\nexport const REDACTED_VALUE = \"REDACTED\";\n\n/** A header or query parameter name, or a pattern tested against names. */\nexport type RedactionMatcher = string | RegExp;\n\n/** A list of names and patterns whose values are redacted. */\nexport type RedactionList = readonly RedactionMatcher[];\n\n/**\n * Configures what gets redacted. An array replaces the defaults entirely; a\n * function receives the defaults and returns the list to use, which makes\n * extending them a one-liner: `(defaults) => [...defaults, \"x-tenant\"]`.\n */\nexport type RedactionOption =\n | RedactionList\n | ((defaults: RedactionList) => RedactionList);\n\n/**\n * Header and query parameter names matching this pattern are redacted even\n * when they are not listed explicitly, so vendor-specific credential fields\n * such as `X-Amz-Security-Token` or `X-Goog-Signature` never leak. It is part\n * of both default lists.\n */\nexport const DEFAULT_REDACTED_NAME_PATTERN =\n /auth|bearer|cred|jwt|otp|passw|private|secret|session|sig|token|api[-_]?key/i;\n\n/** A list of patterns tested against header and query parameter values. */\nexport type ValuePatternList = readonly RegExp[];\n\n/**\n * Configures value-based redaction. An array replaces the defaults; a\n * function receives the defaults and returns the list to use.\n */\nexport type ValuePatternOption =\n | ValuePatternList\n | ((defaults: ValuePatternList) => ValuePatternList);\n\n/** Header names to capture, or `true` to explicitly capture every header. */\nexport type HeaderCaptureOption = boolean | readonly string[];\n\n/**\n * Header and query parameter values matching one of these patterns are\n * redacted whatever their name, so a credential carried under an arbitrary\n * name (`X-JWT`, `blob`, ...) still never reaches telemetry. Covers JWTs and\n * HTTP authentication credentials (`Bearer`, `Basic`, `Digest`, ...).\n */\nexport const DEFAULT_REDACTED_VALUE_PATTERNS: ValuePatternList = [\n /^eyJ[\\w-]+\\.[\\w-]+\\.[\\w-]*$/,\n /^(?:Bearer|Basic|Digest|Negotiate|Token|OAuth|AWS4-HMAC-SHA256)\\s/i,\n];\n\n/** Request headers whose values are never recorded on spans. */\nexport const DEFAULT_REDACTED_HEADERS: RedactionList = [\n \"authorization\",\n \"proxy-authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"api-key\",\n \"x-amz-security-token\",\n \"x-amz-credential\",\n \"x-amz-signature\",\n DEFAULT_REDACTED_NAME_PATTERN,\n];\n\n/** Query parameters whose values are never recorded on spans. */\nexport const DEFAULT_REDACTED_QUERY_PARAMS: RedactionList = [\n \"access_token\",\n \"api_key\",\n \"apikey\",\n \"assertion\",\n \"auth\",\n \"authorization\",\n \"awsaccesskeyid\",\n \"client_assertion\",\n \"client_secret\",\n \"code\",\n \"code_verifier\",\n \"id_token\",\n \"key\",\n \"password\",\n \"refresh_token\",\n \"secret\",\n \"sig\",\n \"signature\",\n \"token\",\n \"x-amz-credential\",\n \"x-amz-security-token\",\n \"x-amz-signature\",\n \"x-goog-signature\",\n DEFAULT_REDACTED_NAME_PATTERN,\n];\n\nexport type OtelOptions = {\n /**\n * Tracer used to start spans. Defaults to a tracer named `@okfetch/otel`\n * obtained from the global tracer provider.\n */\n tracer?: Tracer;\n /**\n * Request headers to record as `http.request.header.<name>` attributes.\n * Sensitive values are always redacted. Defaults to no headers. Pass `true`\n * to explicitly capture every request header.\n */\n captureRequestHeaders?: HeaderCaptureOption;\n /**\n * Response headers to record as `http.response.header.<name>` attributes.\n * Sensitive values are always redacted. Defaults to no headers. Pass `true`\n * to explicitly capture every response header.\n */\n captureResponseHeaders?: HeaderCaptureOption;\n /**\n * Whether to record request and response payload sizes when Fetch exposes\n * enough information to calculate them accurately. Defaults to `false`.\n */\n captureBodySizes?: boolean;\n /**\n * Case-sensitive HTTP methods known to this instrumentation. This is a full\n * replacement for `DEFAULT_KNOWN_HTTP_METHODS`.\n */\n knownMethods?: readonly string[];\n /**\n * Whether W3C trace context headers (`traceparent`, `tracestate`) are\n * injected into the outgoing request. Defaults to `true`.\n */\n propagateTraceContext?: boolean;\n /**\n * What to redact from recorded headers and query parameters. Each entry\n * accepts an array (replaces the defaults) or a function (receives the\n * defaults and returns the list to use). Name matching is case-insensitive.\n */\n redact?: {\n /** Defaults to `DEFAULT_REDACTED_HEADERS`. */\n headers?: RedactionOption;\n /** Defaults to `DEFAULT_REDACTED_QUERY_PARAMS`. */\n queryParams?: RedactionOption;\n /**\n * Patterns matched against header and query parameter values, applied\n * regardless of name. Defaults to `DEFAULT_REDACTED_VALUE_PATTERNS`.\n */\n values?: ValuePatternOption;\n };\n};\n\ntype OtelState = {\n ended: boolean;\n resendCount: number;\n span?: Span;\n template?: string;\n};\n\ntype NameMatcher = (name: string) => boolean;\ntype ValueMatcher = (value: string) => boolean;\n\ntype ResolvedOptions = {\n captureBodySizes: boolean;\n capturedRequestHeaders: true | ReadonlySet<string>;\n capturedResponseHeaders: true | ReadonlySet<string>;\n propagateTraceContext: boolean;\n isRedactedHeader: NameMatcher;\n isRedactedQueryParam: NameMatcher;\n isRedactedValue: ValueMatcher;\n knownMethods: ReadonlySet<string>;\n tracer: Tracer;\n};\n\nconst stateKey = Symbol.for(\"@okfetch/otel:state\");\n\ntype WithState<T> = T & { [stateKey]?: OtelState };\n\nconst getState = (\n carrier: OkfetchRequestContext | OkfetchOptions\n): OtelState | undefined => (carrier as WithState<typeof carrier>)[stateKey];\n\nconst pathParamPattern = /:[A-Za-z_]\\w*(?=[/?#]|$)/;\nconst userInfoPattern = /^([a-z][a-z\\d+.-]*:\\/\\/)[^/]*@/i;\nconst contentLengthPattern = /^\\d+$/;\n\n/**\n * Reduces a raw request URL to its low-cardinality absolute-path template.\n * Query values, fragments, origins, and embedded credentials are excluded.\n */\nconst toTemplate = (rawUrl: string): string | undefined => {\n const [withoutQuery = \"\"] = rawUrl.split(/[?#]/, 1);\n const withoutUserInfo = withoutQuery.replace(userInfoPattern, \"$1\");\n if (!pathParamPattern.test(withoutUserInfo)) {\n return undefined;\n }\n\n try {\n return new URL(withoutUserInfo).pathname;\n } catch {\n return withoutUserInfo;\n }\n};\n\nconst headersSetter: TextMapSetter<Headers> = {\n set: (carrier, key, value) => {\n carrier.set(key, value);\n },\n};\n\nconst resolveList = <T extends readonly unknown[]>(\n defaults: T,\n option: T | ((defaults: T) => T) | undefined\n): T => {\n if (option === undefined) {\n return defaults;\n }\n if (Array.isArray(option)) {\n return option as T;\n }\n\n return (option as (defaults: T) => T)(defaults);\n};\n\nconst testPattern = (pattern: RegExp, input: string): boolean => {\n pattern.lastIndex = 0;\n return pattern.test(input);\n};\n\nconst createValueMatcher =\n (patterns: ValuePatternList): ValueMatcher =>\n (value) =>\n patterns.some((pattern) => testPattern(pattern, value));\n\nconst createNameMatcher = (list: RedactionList): NameMatcher => {\n const names = new Set<string>();\n const patterns: RegExp[] = [];\n\n for (const matcher of list) {\n if (typeof matcher === \"string\") {\n names.add(matcher.toLowerCase());\n } else {\n patterns.push(matcher);\n }\n }\n\n return (name) =>\n names.has(name.toLowerCase()) ||\n patterns.some((pattern) => testPattern(pattern, name));\n};\n\nconst resolveCapturedHeaders = (\n option: HeaderCaptureOption | undefined\n): true | ReadonlySet<string> =>\n option === true\n ? true\n : new Set(\n option === false || option === undefined\n ? []\n : option.map((name) => name.toLowerCase())\n );\n\nconst resolveOptions = (options: OtelOptions | undefined): ResolvedOptions => ({\n captureBodySizes: options?.captureBodySizes ?? false,\n capturedRequestHeaders: resolveCapturedHeaders(\n options?.captureRequestHeaders\n ),\n capturedResponseHeaders: resolveCapturedHeaders(\n options?.captureResponseHeaders\n ),\n propagateTraceContext: options?.propagateTraceContext ?? true,\n isRedactedHeader: createNameMatcher(\n resolveList(DEFAULT_REDACTED_HEADERS, options?.redact?.headers)\n ),\n isRedactedQueryParam: createNameMatcher(\n resolveList(DEFAULT_REDACTED_QUERY_PARAMS, options?.redact?.queryParams)\n ),\n isRedactedValue: createValueMatcher(\n resolveList(DEFAULT_REDACTED_VALUE_PATTERNS, options?.redact?.values)\n ),\n knownMethods: new Set(options?.knownMethods ?? DEFAULT_KNOWN_HTTP_METHODS),\n tracer: options?.tracer ?? trace.getTracer(TRACER_NAME, PLUGIN_VERSION),\n});\n\nconst getMethodAttributes = (\n method: string,\n knownMethods: ReadonlySet<string>\n): Attributes => {\n const semanticMethod = knownMethods.has(method) ? method : \"_OTHER\";\n const attributes: Attributes = {\n \"http.request.method\": semanticMethod,\n };\n\n if (method !== semanticMethod) {\n attributes[\"http.request.method_original\"] = method;\n }\n\n return attributes;\n};\n\nconst getContentLength = (headers: Headers): number | undefined => {\n const value = headers.get(\"content-length\")?.trim();\n if (!value || !contentLengthPattern.test(value)) {\n return undefined;\n }\n\n const size = Number(value);\n return Number.isSafeInteger(size) ? size : undefined;\n};\n\nconst getRequestBodySize = (\n body: OkfetchRequestContext[\"body\"],\n headers: Headers\n): number | undefined => {\n if (body === undefined) {\n return undefined;\n }\n\n const contentLength = getContentLength(headers);\n if (contentLength !== undefined) {\n return contentLength;\n }\n if (headers.has(\"content-encoding\")) {\n return undefined;\n }\n if (typeof body === \"string\") {\n return new TextEncoder().encode(body).byteLength;\n }\n if (body instanceof URLSearchParams) {\n return new TextEncoder().encode(body.toString()).byteLength;\n }\n if (body instanceof Blob) {\n return body.size;\n }\n if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {\n return body.byteLength;\n }\n\n return undefined;\n};\n\nconst shouldCaptureHeader = (\n capturedHeaders: true | ReadonlySet<string>,\n name: string\n): boolean => capturedHeaders === true || capturedHeaders.has(name);\n\nconst captureHeaders = (\n attributes: Attributes,\n prefix: \"http.request.header\" | \"http.response.header\",\n headers: Headers,\n capturedHeaders: true | ReadonlySet<string>,\n options: ResolvedOptions\n): void => {\n for (const [name, value] of headers) {\n if (!shouldCaptureHeader(capturedHeaders, name)) {\n continue;\n }\n\n const shouldRedact =\n options.isRedactedHeader(name) || options.isRedactedValue(value);\n attributes[`${prefix}.${name}`] = [shouldRedact ? REDACTED_VALUE : value];\n }\n};\n\nconst getServerPort = (url: URL): number | undefined => {\n if (url.port) {\n return Number(url.port);\n }\n if (url.protocol === \"http:\") {\n return 80;\n }\n if (url.protocol === \"https:\") {\n return 443;\n }\n\n return undefined;\n};\n\nconst redactUrl = (url: URL, options: ResolvedOptions): URL => {\n const redacted = new URL(url.toString());\n redacted.hash = \"\";\n\n if (redacted.username) {\n redacted.username = REDACTED_VALUE;\n }\n if (redacted.password) {\n redacted.password = REDACTED_VALUE;\n }\n\n for (const name of new Set(redacted.searchParams.keys())) {\n const shouldRedact =\n options.isRedactedQueryParam(name) ||\n redacted.searchParams.getAll(name).some(options.isRedactedValue);\n if (shouldRedact) {\n redacted.searchParams.set(name, REDACTED_VALUE);\n }\n }\n\n return redacted;\n};\n\nconst buildRequestAttributes = (\n ctx: OkfetchRequestContext,\n template: string | undefined,\n options: ResolvedOptions\n): Attributes => {\n const url = redactUrl(ctx.url, options);\n const attributes: Attributes = {\n ...getMethodAttributes(ctx.method, options.knownMethods),\n \"server.address\": url.hostname,\n \"url.full\": url.toString(),\n \"url.path\": url.pathname,\n \"url.scheme\": url.protocol.replace(/:$/, \"\"),\n };\n const serverPort = getServerPort(url);\n if (serverPort !== undefined) {\n attributes[\"server.port\"] = serverPort;\n }\n\n if (url.search) {\n attributes[\"url.query\"] = url.search.slice(1);\n }\n if (template) {\n attributes[\"url.template\"] = template;\n }\n\n if (options.captureBodySizes) {\n const bodySize = getRequestBodySize(ctx.body, ctx.headers);\n if (bodySize !== undefined) {\n attributes[\"http.request.body.size\"] = bodySize;\n }\n }\n captureHeaders(\n attributes,\n \"http.request.header\",\n ctx.headers,\n options.capturedRequestHeaders,\n options\n );\n\n return attributes;\n};\n\nconst startSpan = (\n ctx: OkfetchRequestContext,\n state: OtelState,\n options: ResolvedOptions\n): Span => {\n const method = options.knownMethods.has(ctx.method) ? ctx.method : \"HTTP\";\n const name = state.template ? `${method} ${state.template}` : method;\n\n return options.tracer.startSpan(\n name,\n {\n attributes: buildRequestAttributes(ctx, state.template, options),\n kind: SpanKind.CLIENT,\n },\n context.active()\n );\n};\n\n/**\n * Injects trace context headers into a copy of the request headers. A\n * propagator failure is recorded on the span and the request proceeds without\n * propagation headers: telemetry must never fail the request or leave the\n * span open.\n */\nconst injectTraceContext = (\n ctx: OkfetchRequestContext,\n span: Span\n): OkfetchRequestContext => {\n const headers = new Headers(ctx.headers);\n\n try {\n propagation.inject(\n trace.setSpan(context.active(), span),\n headers,\n headersSetter\n );\n } catch (error) {\n span.addEvent(\"okfetch.propagation_failed\", {\n \"exception.message\":\n error instanceof Error ? error.message : String(error),\n });\n return ctx;\n }\n\n return { ...ctx, headers };\n};\n\nconst endSpan = (state: OtelState): void => {\n if (state.ended) {\n return;\n }\n\n state.ended = true;\n state.span?.end();\n};\n\nconst recordResponse = (\n span: Span,\n ctx: OkfetchRequestContext,\n response: Response,\n options: ResolvedOptions\n): void => {\n const attributes: Attributes = {\n \"http.response.status_code\": response.status,\n };\n\n if (\n options.captureBodySizes &&\n ctx.method !== \"HEAD\" &&\n response.status !== 204 &&\n response.status !== 304\n ) {\n const bodySize = getContentLength(response.headers);\n if (bodySize !== undefined) {\n attributes[\"http.response.body.size\"] = bodySize;\n }\n }\n captureHeaders(\n attributes,\n \"http.response.header\",\n response.headers,\n options.capturedResponseHeaders,\n options\n );\n span.setAttributes(attributes);\n};\n\nconst recordFailure = (\n span: Span,\n ctx: OkfetchRequestContext,\n response: Response | undefined,\n error: OkfetchError<unknown>,\n options: ResolvedOptions\n): void => {\n span.setAttribute(\"okfetch.error.tag\", error._tag);\n\n if (response) {\n recordResponse(span, ctx, response, options);\n }\n\n if (error._tag === \"ApiError\") {\n span.setAttributes({\n \"error.type\": String(error.statusCode),\n \"http.response.status_code\": error.statusCode,\n });\n span.setStatus({ code: SpanStatusCode.ERROR });\n return;\n }\n\n if (error._tag === \"ValidationError\") {\n const issues = error.issues.map((issue) => {\n const path = issue.path\n ?.map((segment) =>\n String(typeof segment === \"object\" ? segment.key : segment)\n )\n .join(\".\");\n return path ? `${path}: ${issue.message}` : issue.message;\n });\n const message =\n issues.length > 0\n ? `${error.message}: ${issues.join(\"; \")}`\n : error.message;\n\n span.setAttribute(\"okfetch.validation.issues\", issues);\n span.setAttribute(\"error.type\", error._tag);\n span.recordException({ message, name: error._tag, stack: error.stack });\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n return;\n }\n\n span.setAttribute(\"error.type\", error._tag);\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n};\n\n/**\n * Creates an OpenTelemetry plugin that records a single `CLIENT` span per\n * okfetch request, spanning every retry attempt.\n *\n * The span records HTTP client attributes available through Fetch and marks\n * failures using the OpenTelemetry HTTP span conventions. Request and response\n * bodies are never recorded.\n */\nexport const otel = (options?: OtelOptions): OkfetchPlugin => {\n const resolved = resolveOptions(options);\n\n const init = ({\n options: requestOptions,\n url,\n }: OkfetchPluginInitInput): OkfetchPluginInitInput => {\n const state: OtelState = {\n ended: false,\n resendCount: 0,\n template:\n requestOptions.params === undefined ? undefined : toTemplate(url),\n };\n const nextOptions: WithState<OkfetchOptions> = {\n ...requestOptions,\n [stateKey]: state,\n };\n\n return { options: nextOptions, url };\n };\n\n const onRequest = (ctx: OkfetchRequestContext): OkfetchRequestContext => {\n const state = getState(ctx) ?? { ended: false, resendCount: 0 };\n let nextCtx: WithState<OkfetchRequestContext> = {\n ...ctx,\n [stateKey]: state,\n };\n if (state.span) {\n state.resendCount += 1;\n state.span.setAttribute(\"http.request.resend_count\", state.resendCount);\n } else {\n state.span = startSpan(nextCtx, state, resolved);\n }\n\n if (resolved.propagateTraceContext) {\n nextCtx = {\n ...injectTraceContext(nextCtx, state.span),\n [stateKey]: state,\n };\n }\n\n return nextCtx;\n };\n\n return {\n name: PLUGIN_NAME,\n version: PLUGIN_VERSION,\n init,\n hooks: {\n onRequest,\n onSuccess(ctx, response) {\n const state = getState(ctx);\n if (!state?.span) {\n return;\n }\n\n recordResponse(state.span, ctx, response, resolved);\n endSpan(state);\n },\n onFail(ctx, response, error) {\n const state = getState(ctx);\n if (!state) {\n return;\n }\n\n state.span ??= startSpan(ctx, state, resolved);\n recordFailure(state.span, ctx, response, error, resolved);\n endSpan(state);\n },\n onRetry(ctx, response, error, attempt) {\n const state = getState(ctx);\n if (!state?.span) {\n return;\n }\n\n const attributes: Attributes = {\n \"error.type\":\n error._tag === \"ApiError\" ? String(error.statusCode) : error._tag,\n \"okfetch.error.tag\": error._tag,\n \"okfetch.retry.attempt\": attempt + 1,\n };\n if (response) {\n attributes[\"http.response.status_code\"] = response.status;\n }\n\n state.span.addEvent(\"okfetch.retry\", attributes);\n },\n },\n } satisfies OkfetchPlugin;\n};\n"],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ import { OkfetchPlugin } from "@okfetch/fetch";
2
+ import { Tracer } from "@opentelemetry/api";
3
+ //#region packages/otel/index.d.ts
4
+ /** HTTP methods known by default, as defined by the HTTP semantic conventions. */
5
+ declare const DEFAULT_KNOWN_HTTP_METHODS: readonly ["CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "QUERY", "TRACE"];
6
+ /** Value written in place of redacted headers and query parameters. */
7
+ declare const REDACTED_VALUE = "REDACTED";
8
+ /** A header or query parameter name, or a pattern tested against names. */
9
+ type RedactionMatcher = string | RegExp;
10
+ /** A list of names and patterns whose values are redacted. */
11
+ type RedactionList = readonly RedactionMatcher[];
12
+ /**
13
+ * Configures what gets redacted. An array replaces the defaults entirely; a
14
+ * function receives the defaults and returns the list to use, which makes
15
+ * extending them a one-liner: `(defaults) => [...defaults, "x-tenant"]`.
16
+ */
17
+ type RedactionOption = RedactionList | ((defaults: RedactionList) => RedactionList);
18
+ /**
19
+ * Header and query parameter names matching this pattern are redacted even
20
+ * when they are not listed explicitly, so vendor-specific credential fields
21
+ * such as `X-Amz-Security-Token` or `X-Goog-Signature` never leak. It is part
22
+ * of both default lists.
23
+ */
24
+ declare const DEFAULT_REDACTED_NAME_PATTERN: RegExp;
25
+ /** A list of patterns tested against header and query parameter values. */
26
+ type ValuePatternList = readonly RegExp[];
27
+ /**
28
+ * Configures value-based redaction. An array replaces the defaults; a
29
+ * function receives the defaults and returns the list to use.
30
+ */
31
+ type ValuePatternOption = ValuePatternList | ((defaults: ValuePatternList) => ValuePatternList);
32
+ /** Header names to capture, or `true` to explicitly capture every header. */
33
+ type HeaderCaptureOption = boolean | readonly string[];
34
+ /**
35
+ * Header and query parameter values matching one of these patterns are
36
+ * redacted whatever their name, so a credential carried under an arbitrary
37
+ * name (`X-JWT`, `blob`, ...) still never reaches telemetry. Covers JWTs and
38
+ * HTTP authentication credentials (`Bearer`, `Basic`, `Digest`, ...).
39
+ */
40
+ declare const DEFAULT_REDACTED_VALUE_PATTERNS: ValuePatternList;
41
+ /** Request headers whose values are never recorded on spans. */
42
+ declare const DEFAULT_REDACTED_HEADERS: RedactionList;
43
+ /** Query parameters whose values are never recorded on spans. */
44
+ declare const DEFAULT_REDACTED_QUERY_PARAMS: RedactionList;
45
+ type OtelOptions = {
46
+ /**
47
+ * Tracer used to start spans. Defaults to a tracer named `@okfetch/otel`
48
+ * obtained from the global tracer provider.
49
+ */
50
+ tracer?: Tracer;
51
+ /**
52
+ * Request headers to record as `http.request.header.<name>` attributes.
53
+ * Sensitive values are always redacted. Defaults to no headers. Pass `true`
54
+ * to explicitly capture every request header.
55
+ */
56
+ captureRequestHeaders?: HeaderCaptureOption;
57
+ /**
58
+ * Response headers to record as `http.response.header.<name>` attributes.
59
+ * Sensitive values are always redacted. Defaults to no headers. Pass `true`
60
+ * to explicitly capture every response header.
61
+ */
62
+ captureResponseHeaders?: HeaderCaptureOption;
63
+ /**
64
+ * Whether to record request and response payload sizes when Fetch exposes
65
+ * enough information to calculate them accurately. Defaults to `false`.
66
+ */
67
+ captureBodySizes?: boolean;
68
+ /**
69
+ * Case-sensitive HTTP methods known to this instrumentation. This is a full
70
+ * replacement for `DEFAULT_KNOWN_HTTP_METHODS`.
71
+ */
72
+ knownMethods?: readonly string[];
73
+ /**
74
+ * Whether W3C trace context headers (`traceparent`, `tracestate`) are
75
+ * injected into the outgoing request. Defaults to `true`.
76
+ */
77
+ propagateTraceContext?: boolean;
78
+ /**
79
+ * What to redact from recorded headers and query parameters. Each entry
80
+ * accepts an array (replaces the defaults) or a function (receives the
81
+ * defaults and returns the list to use). Name matching is case-insensitive.
82
+ */
83
+ redact?: {
84
+ /** Defaults to `DEFAULT_REDACTED_HEADERS`. */
85
+ headers?: RedactionOption;
86
+ /** Defaults to `DEFAULT_REDACTED_QUERY_PARAMS`. */
87
+ queryParams?: RedactionOption;
88
+ /**
89
+ * Patterns matched against header and query parameter values, applied
90
+ * regardless of name. Defaults to `DEFAULT_REDACTED_VALUE_PATTERNS`.
91
+ */
92
+ values?: ValuePatternOption;
93
+ };
94
+ };
95
+ /**
96
+ * Creates an OpenTelemetry plugin that records a single `CLIENT` span per
97
+ * okfetch request, spanning every retry attempt.
98
+ *
99
+ * The span records HTTP client attributes available through Fetch and marks
100
+ * failures using the OpenTelemetry HTTP span conventions. Request and response
101
+ * bodies are never recorded.
102
+ */
103
+ declare const otel: (options?: OtelOptions) => OkfetchPlugin;
104
+ //#endregion
105
+ export { DEFAULT_KNOWN_HTTP_METHODS, DEFAULT_REDACTED_HEADERS, DEFAULT_REDACTED_NAME_PATTERN, DEFAULT_REDACTED_QUERY_PARAMS, DEFAULT_REDACTED_VALUE_PATTERNS, HeaderCaptureOption, OtelOptions, REDACTED_VALUE, RedactionList, RedactionMatcher, RedactionOption, ValuePatternList, ValuePatternOption, otel };
106
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,106 @@
1
+ import { Tracer } from "@opentelemetry/api";
2
+ import { OkfetchPlugin } from "@okfetch/fetch";
3
+ //#region packages/otel/index.d.ts
4
+ /** HTTP methods known by default, as defined by the HTTP semantic conventions. */
5
+ declare const DEFAULT_KNOWN_HTTP_METHODS: readonly ["CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "QUERY", "TRACE"];
6
+ /** Value written in place of redacted headers and query parameters. */
7
+ declare const REDACTED_VALUE = "REDACTED";
8
+ /** A header or query parameter name, or a pattern tested against names. */
9
+ type RedactionMatcher = string | RegExp;
10
+ /** A list of names and patterns whose values are redacted. */
11
+ type RedactionList = readonly RedactionMatcher[];
12
+ /**
13
+ * Configures what gets redacted. An array replaces the defaults entirely; a
14
+ * function receives the defaults and returns the list to use, which makes
15
+ * extending them a one-liner: `(defaults) => [...defaults, "x-tenant"]`.
16
+ */
17
+ type RedactionOption = RedactionList | ((defaults: RedactionList) => RedactionList);
18
+ /**
19
+ * Header and query parameter names matching this pattern are redacted even
20
+ * when they are not listed explicitly, so vendor-specific credential fields
21
+ * such as `X-Amz-Security-Token` or `X-Goog-Signature` never leak. It is part
22
+ * of both default lists.
23
+ */
24
+ declare const DEFAULT_REDACTED_NAME_PATTERN: RegExp;
25
+ /** A list of patterns tested against header and query parameter values. */
26
+ type ValuePatternList = readonly RegExp[];
27
+ /**
28
+ * Configures value-based redaction. An array replaces the defaults; a
29
+ * function receives the defaults and returns the list to use.
30
+ */
31
+ type ValuePatternOption = ValuePatternList | ((defaults: ValuePatternList) => ValuePatternList);
32
+ /** Header names to capture, or `true` to explicitly capture every header. */
33
+ type HeaderCaptureOption = boolean | readonly string[];
34
+ /**
35
+ * Header and query parameter values matching one of these patterns are
36
+ * redacted whatever their name, so a credential carried under an arbitrary
37
+ * name (`X-JWT`, `blob`, ...) still never reaches telemetry. Covers JWTs and
38
+ * HTTP authentication credentials (`Bearer`, `Basic`, `Digest`, ...).
39
+ */
40
+ declare const DEFAULT_REDACTED_VALUE_PATTERNS: ValuePatternList;
41
+ /** Request headers whose values are never recorded on spans. */
42
+ declare const DEFAULT_REDACTED_HEADERS: RedactionList;
43
+ /** Query parameters whose values are never recorded on spans. */
44
+ declare const DEFAULT_REDACTED_QUERY_PARAMS: RedactionList;
45
+ type OtelOptions = {
46
+ /**
47
+ * Tracer used to start spans. Defaults to a tracer named `@okfetch/otel`
48
+ * obtained from the global tracer provider.
49
+ */
50
+ tracer?: Tracer;
51
+ /**
52
+ * Request headers to record as `http.request.header.<name>` attributes.
53
+ * Sensitive values are always redacted. Defaults to no headers. Pass `true`
54
+ * to explicitly capture every request header.
55
+ */
56
+ captureRequestHeaders?: HeaderCaptureOption;
57
+ /**
58
+ * Response headers to record as `http.response.header.<name>` attributes.
59
+ * Sensitive values are always redacted. Defaults to no headers. Pass `true`
60
+ * to explicitly capture every response header.
61
+ */
62
+ captureResponseHeaders?: HeaderCaptureOption;
63
+ /**
64
+ * Whether to record request and response payload sizes when Fetch exposes
65
+ * enough information to calculate them accurately. Defaults to `false`.
66
+ */
67
+ captureBodySizes?: boolean;
68
+ /**
69
+ * Case-sensitive HTTP methods known to this instrumentation. This is a full
70
+ * replacement for `DEFAULT_KNOWN_HTTP_METHODS`.
71
+ */
72
+ knownMethods?: readonly string[];
73
+ /**
74
+ * Whether W3C trace context headers (`traceparent`, `tracestate`) are
75
+ * injected into the outgoing request. Defaults to `true`.
76
+ */
77
+ propagateTraceContext?: boolean;
78
+ /**
79
+ * What to redact from recorded headers and query parameters. Each entry
80
+ * accepts an array (replaces the defaults) or a function (receives the
81
+ * defaults and returns the list to use). Name matching is case-insensitive.
82
+ */
83
+ redact?: {
84
+ /** Defaults to `DEFAULT_REDACTED_HEADERS`. */
85
+ headers?: RedactionOption;
86
+ /** Defaults to `DEFAULT_REDACTED_QUERY_PARAMS`. */
87
+ queryParams?: RedactionOption;
88
+ /**
89
+ * Patterns matched against header and query parameter values, applied
90
+ * regardless of name. Defaults to `DEFAULT_REDACTED_VALUE_PATTERNS`.
91
+ */
92
+ values?: ValuePatternOption;
93
+ };
94
+ };
95
+ /**
96
+ * Creates an OpenTelemetry plugin that records a single `CLIENT` span per
97
+ * okfetch request, spanning every retry attempt.
98
+ *
99
+ * The span records HTTP client attributes available through Fetch and marks
100
+ * failures using the OpenTelemetry HTTP span conventions. Request and response
101
+ * bodies are never recorded.
102
+ */
103
+ declare const otel: (options?: OtelOptions) => OkfetchPlugin;
104
+ //#endregion
105
+ export { DEFAULT_KNOWN_HTTP_METHODS, DEFAULT_REDACTED_HEADERS, DEFAULT_REDACTED_NAME_PATTERN, DEFAULT_REDACTED_QUERY_PARAMS, DEFAULT_REDACTED_VALUE_PATTERNS, HeaderCaptureOption, OtelOptions, REDACTED_VALUE, RedactionList, RedactionMatcher, RedactionOption, ValuePatternList, ValuePatternOption, otel };
106
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,347 @@
1
+ import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
2
+ //#region packages/otel/index.ts
3
+ const PLUGIN_NAME = "otel";
4
+ const PLUGIN_VERSION = "0.1.0";
5
+ const TRACER_NAME = "@okfetch/otel";
6
+ /** HTTP methods known by default, as defined by the HTTP semantic conventions. */
7
+ const DEFAULT_KNOWN_HTTP_METHODS = [
8
+ "CONNECT",
9
+ "DELETE",
10
+ "GET",
11
+ "HEAD",
12
+ "OPTIONS",
13
+ "PATCH",
14
+ "POST",
15
+ "PUT",
16
+ "QUERY",
17
+ "TRACE"
18
+ ];
19
+ /** Value written in place of redacted headers and query parameters. */
20
+ const REDACTED_VALUE = "REDACTED";
21
+ /**
22
+ * Header and query parameter names matching this pattern are redacted even
23
+ * when they are not listed explicitly, so vendor-specific credential fields
24
+ * such as `X-Amz-Security-Token` or `X-Goog-Signature` never leak. It is part
25
+ * of both default lists.
26
+ */
27
+ const DEFAULT_REDACTED_NAME_PATTERN = /auth|bearer|cred|jwt|otp|passw|private|secret|session|sig|token|api[-_]?key/i;
28
+ /**
29
+ * Header and query parameter values matching one of these patterns are
30
+ * redacted whatever their name, so a credential carried under an arbitrary
31
+ * name (`X-JWT`, `blob`, ...) still never reaches telemetry. Covers JWTs and
32
+ * HTTP authentication credentials (`Bearer`, `Basic`, `Digest`, ...).
33
+ */
34
+ const DEFAULT_REDACTED_VALUE_PATTERNS = [/^eyJ[\w-]+\.[\w-]+\.[\w-]*$/, /^(?:Bearer|Basic|Digest|Negotiate|Token|OAuth|AWS4-HMAC-SHA256)\s/i];
35
+ /** Request headers whose values are never recorded on spans. */
36
+ const DEFAULT_REDACTED_HEADERS = [
37
+ "authorization",
38
+ "proxy-authorization",
39
+ "cookie",
40
+ "set-cookie",
41
+ "x-api-key",
42
+ "x-auth-token",
43
+ "api-key",
44
+ "x-amz-security-token",
45
+ "x-amz-credential",
46
+ "x-amz-signature",
47
+ DEFAULT_REDACTED_NAME_PATTERN
48
+ ];
49
+ /** Query parameters whose values are never recorded on spans. */
50
+ const DEFAULT_REDACTED_QUERY_PARAMS = [
51
+ "access_token",
52
+ "api_key",
53
+ "apikey",
54
+ "assertion",
55
+ "auth",
56
+ "authorization",
57
+ "awsaccesskeyid",
58
+ "client_assertion",
59
+ "client_secret",
60
+ "code",
61
+ "code_verifier",
62
+ "id_token",
63
+ "key",
64
+ "password",
65
+ "refresh_token",
66
+ "secret",
67
+ "sig",
68
+ "signature",
69
+ "token",
70
+ "x-amz-credential",
71
+ "x-amz-security-token",
72
+ "x-amz-signature",
73
+ "x-goog-signature",
74
+ DEFAULT_REDACTED_NAME_PATTERN
75
+ ];
76
+ const stateKey = Symbol.for("@okfetch/otel:state");
77
+ const getState = (carrier) => carrier[stateKey];
78
+ const pathParamPattern = /:[A-Za-z_]\w*(?=[/?#]|$)/;
79
+ const userInfoPattern = /^([a-z][a-z\d+.-]*:\/\/)[^/]*@/i;
80
+ const contentLengthPattern = /^\d+$/;
81
+ /**
82
+ * Reduces a raw request URL to its low-cardinality absolute-path template.
83
+ * Query values, fragments, origins, and embedded credentials are excluded.
84
+ */
85
+ const toTemplate = (rawUrl) => {
86
+ const [withoutQuery = ""] = rawUrl.split(/[?#]/, 1);
87
+ const withoutUserInfo = withoutQuery.replace(userInfoPattern, "$1");
88
+ if (!pathParamPattern.test(withoutUserInfo)) return;
89
+ try {
90
+ return new URL(withoutUserInfo).pathname;
91
+ } catch {
92
+ return withoutUserInfo;
93
+ }
94
+ };
95
+ const headersSetter = { set: (carrier, key, value) => {
96
+ carrier.set(key, value);
97
+ } };
98
+ const resolveList = (defaults, option) => {
99
+ if (option === void 0) return defaults;
100
+ if (Array.isArray(option)) return option;
101
+ return option(defaults);
102
+ };
103
+ const testPattern = (pattern, input) => {
104
+ pattern.lastIndex = 0;
105
+ return pattern.test(input);
106
+ };
107
+ const createValueMatcher = (patterns) => (value) => patterns.some((pattern) => testPattern(pattern, value));
108
+ const createNameMatcher = (list) => {
109
+ const names = /* @__PURE__ */ new Set();
110
+ const patterns = [];
111
+ for (const matcher of list) if (typeof matcher === "string") names.add(matcher.toLowerCase());
112
+ else patterns.push(matcher);
113
+ return (name) => names.has(name.toLowerCase()) || patterns.some((pattern) => testPattern(pattern, name));
114
+ };
115
+ const resolveCapturedHeaders = (option) => option === true ? true : new Set(option === false || option === void 0 ? [] : option.map((name) => name.toLowerCase()));
116
+ const resolveOptions = (options) => ({
117
+ captureBodySizes: options?.captureBodySizes ?? false,
118
+ capturedRequestHeaders: resolveCapturedHeaders(options?.captureRequestHeaders),
119
+ capturedResponseHeaders: resolveCapturedHeaders(options?.captureResponseHeaders),
120
+ propagateTraceContext: options?.propagateTraceContext ?? true,
121
+ isRedactedHeader: createNameMatcher(resolveList(DEFAULT_REDACTED_HEADERS, options?.redact?.headers)),
122
+ isRedactedQueryParam: createNameMatcher(resolveList(DEFAULT_REDACTED_QUERY_PARAMS, options?.redact?.queryParams)),
123
+ isRedactedValue: createValueMatcher(resolveList(DEFAULT_REDACTED_VALUE_PATTERNS, options?.redact?.values)),
124
+ knownMethods: new Set(options?.knownMethods ?? DEFAULT_KNOWN_HTTP_METHODS),
125
+ tracer: options?.tracer ?? trace.getTracer(TRACER_NAME, PLUGIN_VERSION)
126
+ });
127
+ const getMethodAttributes = (method, knownMethods) => {
128
+ const semanticMethod = knownMethods.has(method) ? method : "_OTHER";
129
+ const attributes = { "http.request.method": semanticMethod };
130
+ if (method !== semanticMethod) attributes["http.request.method_original"] = method;
131
+ return attributes;
132
+ };
133
+ const getContentLength = (headers) => {
134
+ const value = headers.get("content-length")?.trim();
135
+ if (!value || !contentLengthPattern.test(value)) return;
136
+ const size = Number(value);
137
+ return Number.isSafeInteger(size) ? size : void 0;
138
+ };
139
+ const getRequestBodySize = (body, headers) => {
140
+ if (body === void 0) return;
141
+ const contentLength = getContentLength(headers);
142
+ if (contentLength !== void 0) return contentLength;
143
+ if (headers.has("content-encoding")) return;
144
+ if (typeof body === "string") return new TextEncoder().encode(body).byteLength;
145
+ if (body instanceof URLSearchParams) return new TextEncoder().encode(body.toString()).byteLength;
146
+ if (body instanceof Blob) return body.size;
147
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return body.byteLength;
148
+ };
149
+ const shouldCaptureHeader = (capturedHeaders, name) => capturedHeaders === true || capturedHeaders.has(name);
150
+ const captureHeaders = (attributes, prefix, headers, capturedHeaders, options) => {
151
+ for (const [name, value] of headers) {
152
+ if (!shouldCaptureHeader(capturedHeaders, name)) continue;
153
+ const shouldRedact = options.isRedactedHeader(name) || options.isRedactedValue(value);
154
+ attributes[`${prefix}.${name}`] = [shouldRedact ? REDACTED_VALUE : value];
155
+ }
156
+ };
157
+ const getServerPort = (url) => {
158
+ if (url.port) return Number(url.port);
159
+ if (url.protocol === "http:") return 80;
160
+ if (url.protocol === "https:") return 443;
161
+ };
162
+ const redactUrl = (url, options) => {
163
+ const redacted = new URL(url.toString());
164
+ redacted.hash = "";
165
+ if (redacted.username) redacted.username = REDACTED_VALUE;
166
+ if (redacted.password) redacted.password = REDACTED_VALUE;
167
+ for (const name of new Set(redacted.searchParams.keys())) if (options.isRedactedQueryParam(name) || redacted.searchParams.getAll(name).some(options.isRedactedValue)) redacted.searchParams.set(name, REDACTED_VALUE);
168
+ return redacted;
169
+ };
170
+ const buildRequestAttributes = (ctx, template, options) => {
171
+ const url = redactUrl(ctx.url, options);
172
+ const attributes = {
173
+ ...getMethodAttributes(ctx.method, options.knownMethods),
174
+ "server.address": url.hostname,
175
+ "url.full": url.toString(),
176
+ "url.path": url.pathname,
177
+ "url.scheme": url.protocol.replace(/:$/, "")
178
+ };
179
+ const serverPort = getServerPort(url);
180
+ if (serverPort !== void 0) attributes["server.port"] = serverPort;
181
+ if (url.search) attributes["url.query"] = url.search.slice(1);
182
+ if (template) attributes["url.template"] = template;
183
+ if (options.captureBodySizes) {
184
+ const bodySize = getRequestBodySize(ctx.body, ctx.headers);
185
+ if (bodySize !== void 0) attributes["http.request.body.size"] = bodySize;
186
+ }
187
+ captureHeaders(attributes, "http.request.header", ctx.headers, options.capturedRequestHeaders, options);
188
+ return attributes;
189
+ };
190
+ const startSpan = (ctx, state, options) => {
191
+ const method = options.knownMethods.has(ctx.method) ? ctx.method : "HTTP";
192
+ const name = state.template ? `${method} ${state.template}` : method;
193
+ return options.tracer.startSpan(name, {
194
+ attributes: buildRequestAttributes(ctx, state.template, options),
195
+ kind: SpanKind.CLIENT
196
+ }, context.active());
197
+ };
198
+ /**
199
+ * Injects trace context headers into a copy of the request headers. A
200
+ * propagator failure is recorded on the span and the request proceeds without
201
+ * propagation headers: telemetry must never fail the request or leave the
202
+ * span open.
203
+ */
204
+ const injectTraceContext = (ctx, span) => {
205
+ const headers = new Headers(ctx.headers);
206
+ try {
207
+ propagation.inject(trace.setSpan(context.active(), span), headers, headersSetter);
208
+ } catch (error) {
209
+ span.addEvent("okfetch.propagation_failed", { "exception.message": error instanceof Error ? error.message : String(error) });
210
+ return ctx;
211
+ }
212
+ return {
213
+ ...ctx,
214
+ headers
215
+ };
216
+ };
217
+ const endSpan = (state) => {
218
+ if (state.ended) return;
219
+ state.ended = true;
220
+ state.span?.end();
221
+ };
222
+ const recordResponse = (span, ctx, response, options) => {
223
+ const attributes = { "http.response.status_code": response.status };
224
+ if (options.captureBodySizes && ctx.method !== "HEAD" && response.status !== 204 && response.status !== 304) {
225
+ const bodySize = getContentLength(response.headers);
226
+ if (bodySize !== void 0) attributes["http.response.body.size"] = bodySize;
227
+ }
228
+ captureHeaders(attributes, "http.response.header", response.headers, options.capturedResponseHeaders, options);
229
+ span.setAttributes(attributes);
230
+ };
231
+ const recordFailure = (span, ctx, response, error, options) => {
232
+ span.setAttribute("okfetch.error.tag", error._tag);
233
+ if (response) recordResponse(span, ctx, response, options);
234
+ if (error._tag === "ApiError") {
235
+ span.setAttributes({
236
+ "error.type": String(error.statusCode),
237
+ "http.response.status_code": error.statusCode
238
+ });
239
+ span.setStatus({ code: SpanStatusCode.ERROR });
240
+ return;
241
+ }
242
+ if (error._tag === "ValidationError") {
243
+ const issues = error.issues.map((issue) => {
244
+ const path = issue.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
245
+ return path ? `${path}: ${issue.message}` : issue.message;
246
+ });
247
+ const message = issues.length > 0 ? `${error.message}: ${issues.join("; ")}` : error.message;
248
+ span.setAttribute("okfetch.validation.issues", issues);
249
+ span.setAttribute("error.type", error._tag);
250
+ span.recordException({
251
+ message,
252
+ name: error._tag,
253
+ stack: error.stack
254
+ });
255
+ span.setStatus({
256
+ code: SpanStatusCode.ERROR,
257
+ message
258
+ });
259
+ return;
260
+ }
261
+ span.setAttribute("error.type", error._tag);
262
+ span.recordException(error);
263
+ span.setStatus({
264
+ code: SpanStatusCode.ERROR,
265
+ message: error.message
266
+ });
267
+ };
268
+ /**
269
+ * Creates an OpenTelemetry plugin that records a single `CLIENT` span per
270
+ * okfetch request, spanning every retry attempt.
271
+ *
272
+ * The span records HTTP client attributes available through Fetch and marks
273
+ * failures using the OpenTelemetry HTTP span conventions. Request and response
274
+ * bodies are never recorded.
275
+ */
276
+ const otel = (options) => {
277
+ const resolved = resolveOptions(options);
278
+ const init = ({ options: requestOptions, url }) => {
279
+ const state = {
280
+ ended: false,
281
+ resendCount: 0,
282
+ template: requestOptions.params === void 0 ? void 0 : toTemplate(url)
283
+ };
284
+ return {
285
+ options: {
286
+ ...requestOptions,
287
+ [stateKey]: state
288
+ },
289
+ url
290
+ };
291
+ };
292
+ const onRequest = (ctx) => {
293
+ const state = getState(ctx) ?? {
294
+ ended: false,
295
+ resendCount: 0
296
+ };
297
+ let nextCtx = {
298
+ ...ctx,
299
+ [stateKey]: state
300
+ };
301
+ if (state.span) {
302
+ state.resendCount += 1;
303
+ state.span.setAttribute("http.request.resend_count", state.resendCount);
304
+ } else state.span = startSpan(nextCtx, state, resolved);
305
+ if (resolved.propagateTraceContext) nextCtx = {
306
+ ...injectTraceContext(nextCtx, state.span),
307
+ [stateKey]: state
308
+ };
309
+ return nextCtx;
310
+ };
311
+ return {
312
+ name: PLUGIN_NAME,
313
+ version: PLUGIN_VERSION,
314
+ init,
315
+ hooks: {
316
+ onRequest,
317
+ onSuccess(ctx, response) {
318
+ const state = getState(ctx);
319
+ if (!state?.span) return;
320
+ recordResponse(state.span, ctx, response, resolved);
321
+ endSpan(state);
322
+ },
323
+ onFail(ctx, response, error) {
324
+ const state = getState(ctx);
325
+ if (!state) return;
326
+ state.span ??= startSpan(ctx, state, resolved);
327
+ recordFailure(state.span, ctx, response, error, resolved);
328
+ endSpan(state);
329
+ },
330
+ onRetry(ctx, response, error, attempt) {
331
+ const state = getState(ctx);
332
+ if (!state?.span) return;
333
+ const attributes = {
334
+ "error.type": error._tag === "ApiError" ? String(error.statusCode) : error._tag,
335
+ "okfetch.error.tag": error._tag,
336
+ "okfetch.retry.attempt": attempt + 1
337
+ };
338
+ if (response) attributes["http.response.status_code"] = response.status;
339
+ state.span.addEvent("okfetch.retry", attributes);
340
+ }
341
+ }
342
+ };
343
+ };
344
+ //#endregion
345
+ export { DEFAULT_KNOWN_HTTP_METHODS, DEFAULT_REDACTED_HEADERS, DEFAULT_REDACTED_NAME_PATTERN, DEFAULT_REDACTED_QUERY_PARAMS, DEFAULT_REDACTED_VALUE_PATTERNS, REDACTED_VALUE, otel };
346
+
347
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../index.ts"],"sourcesContent":["import type {\n OkfetchError,\n OkfetchOptions,\n OkfetchPlugin,\n OkfetchPluginInitInput,\n OkfetchRequestContext,\n} from \"@okfetch/fetch\";\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from \"@opentelemetry/api\";\nimport type {\n Attributes,\n Span,\n TextMapSetter,\n Tracer,\n} from \"@opentelemetry/api\";\n\nconst PLUGIN_NAME = \"otel\";\nconst PLUGIN_VERSION = \"0.1.0\";\nconst TRACER_NAME = \"@okfetch/otel\";\n\n/** HTTP methods known by default, as defined by the HTTP semantic conventions. */\nexport const DEFAULT_KNOWN_HTTP_METHODS = [\n \"CONNECT\",\n \"DELETE\",\n \"GET\",\n \"HEAD\",\n \"OPTIONS\",\n \"PATCH\",\n \"POST\",\n \"PUT\",\n \"QUERY\",\n \"TRACE\",\n] as const;\n\n/** Value written in place of redacted headers and query parameters. */\nexport const REDACTED_VALUE = \"REDACTED\";\n\n/** A header or query parameter name, or a pattern tested against names. */\nexport type RedactionMatcher = string | RegExp;\n\n/** A list of names and patterns whose values are redacted. */\nexport type RedactionList = readonly RedactionMatcher[];\n\n/**\n * Configures what gets redacted. An array replaces the defaults entirely; a\n * function receives the defaults and returns the list to use, which makes\n * extending them a one-liner: `(defaults) => [...defaults, \"x-tenant\"]`.\n */\nexport type RedactionOption =\n | RedactionList\n | ((defaults: RedactionList) => RedactionList);\n\n/**\n * Header and query parameter names matching this pattern are redacted even\n * when they are not listed explicitly, so vendor-specific credential fields\n * such as `X-Amz-Security-Token` or `X-Goog-Signature` never leak. It is part\n * of both default lists.\n */\nexport const DEFAULT_REDACTED_NAME_PATTERN =\n /auth|bearer|cred|jwt|otp|passw|private|secret|session|sig|token|api[-_]?key/i;\n\n/** A list of patterns tested against header and query parameter values. */\nexport type ValuePatternList = readonly RegExp[];\n\n/**\n * Configures value-based redaction. An array replaces the defaults; a\n * function receives the defaults and returns the list to use.\n */\nexport type ValuePatternOption =\n | ValuePatternList\n | ((defaults: ValuePatternList) => ValuePatternList);\n\n/** Header names to capture, or `true` to explicitly capture every header. */\nexport type HeaderCaptureOption = boolean | readonly string[];\n\n/**\n * Header and query parameter values matching one of these patterns are\n * redacted whatever their name, so a credential carried under an arbitrary\n * name (`X-JWT`, `blob`, ...) still never reaches telemetry. Covers JWTs and\n * HTTP authentication credentials (`Bearer`, `Basic`, `Digest`, ...).\n */\nexport const DEFAULT_REDACTED_VALUE_PATTERNS: ValuePatternList = [\n /^eyJ[\\w-]+\\.[\\w-]+\\.[\\w-]*$/,\n /^(?:Bearer|Basic|Digest|Negotiate|Token|OAuth|AWS4-HMAC-SHA256)\\s/i,\n];\n\n/** Request headers whose values are never recorded on spans. */\nexport const DEFAULT_REDACTED_HEADERS: RedactionList = [\n \"authorization\",\n \"proxy-authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n \"x-auth-token\",\n \"api-key\",\n \"x-amz-security-token\",\n \"x-amz-credential\",\n \"x-amz-signature\",\n DEFAULT_REDACTED_NAME_PATTERN,\n];\n\n/** Query parameters whose values are never recorded on spans. */\nexport const DEFAULT_REDACTED_QUERY_PARAMS: RedactionList = [\n \"access_token\",\n \"api_key\",\n \"apikey\",\n \"assertion\",\n \"auth\",\n \"authorization\",\n \"awsaccesskeyid\",\n \"client_assertion\",\n \"client_secret\",\n \"code\",\n \"code_verifier\",\n \"id_token\",\n \"key\",\n \"password\",\n \"refresh_token\",\n \"secret\",\n \"sig\",\n \"signature\",\n \"token\",\n \"x-amz-credential\",\n \"x-amz-security-token\",\n \"x-amz-signature\",\n \"x-goog-signature\",\n DEFAULT_REDACTED_NAME_PATTERN,\n];\n\nexport type OtelOptions = {\n /**\n * Tracer used to start spans. Defaults to a tracer named `@okfetch/otel`\n * obtained from the global tracer provider.\n */\n tracer?: Tracer;\n /**\n * Request headers to record as `http.request.header.<name>` attributes.\n * Sensitive values are always redacted. Defaults to no headers. Pass `true`\n * to explicitly capture every request header.\n */\n captureRequestHeaders?: HeaderCaptureOption;\n /**\n * Response headers to record as `http.response.header.<name>` attributes.\n * Sensitive values are always redacted. Defaults to no headers. Pass `true`\n * to explicitly capture every response header.\n */\n captureResponseHeaders?: HeaderCaptureOption;\n /**\n * Whether to record request and response payload sizes when Fetch exposes\n * enough information to calculate them accurately. Defaults to `false`.\n */\n captureBodySizes?: boolean;\n /**\n * Case-sensitive HTTP methods known to this instrumentation. This is a full\n * replacement for `DEFAULT_KNOWN_HTTP_METHODS`.\n */\n knownMethods?: readonly string[];\n /**\n * Whether W3C trace context headers (`traceparent`, `tracestate`) are\n * injected into the outgoing request. Defaults to `true`.\n */\n propagateTraceContext?: boolean;\n /**\n * What to redact from recorded headers and query parameters. Each entry\n * accepts an array (replaces the defaults) or a function (receives the\n * defaults and returns the list to use). Name matching is case-insensitive.\n */\n redact?: {\n /** Defaults to `DEFAULT_REDACTED_HEADERS`. */\n headers?: RedactionOption;\n /** Defaults to `DEFAULT_REDACTED_QUERY_PARAMS`. */\n queryParams?: RedactionOption;\n /**\n * Patterns matched against header and query parameter values, applied\n * regardless of name. Defaults to `DEFAULT_REDACTED_VALUE_PATTERNS`.\n */\n values?: ValuePatternOption;\n };\n};\n\ntype OtelState = {\n ended: boolean;\n resendCount: number;\n span?: Span;\n template?: string;\n};\n\ntype NameMatcher = (name: string) => boolean;\ntype ValueMatcher = (value: string) => boolean;\n\ntype ResolvedOptions = {\n captureBodySizes: boolean;\n capturedRequestHeaders: true | ReadonlySet<string>;\n capturedResponseHeaders: true | ReadonlySet<string>;\n propagateTraceContext: boolean;\n isRedactedHeader: NameMatcher;\n isRedactedQueryParam: NameMatcher;\n isRedactedValue: ValueMatcher;\n knownMethods: ReadonlySet<string>;\n tracer: Tracer;\n};\n\nconst stateKey = Symbol.for(\"@okfetch/otel:state\");\n\ntype WithState<T> = T & { [stateKey]?: OtelState };\n\nconst getState = (\n carrier: OkfetchRequestContext | OkfetchOptions\n): OtelState | undefined => (carrier as WithState<typeof carrier>)[stateKey];\n\nconst pathParamPattern = /:[A-Za-z_]\\w*(?=[/?#]|$)/;\nconst userInfoPattern = /^([a-z][a-z\\d+.-]*:\\/\\/)[^/]*@/i;\nconst contentLengthPattern = /^\\d+$/;\n\n/**\n * Reduces a raw request URL to its low-cardinality absolute-path template.\n * Query values, fragments, origins, and embedded credentials are excluded.\n */\nconst toTemplate = (rawUrl: string): string | undefined => {\n const [withoutQuery = \"\"] = rawUrl.split(/[?#]/, 1);\n const withoutUserInfo = withoutQuery.replace(userInfoPattern, \"$1\");\n if (!pathParamPattern.test(withoutUserInfo)) {\n return undefined;\n }\n\n try {\n return new URL(withoutUserInfo).pathname;\n } catch {\n return withoutUserInfo;\n }\n};\n\nconst headersSetter: TextMapSetter<Headers> = {\n set: (carrier, key, value) => {\n carrier.set(key, value);\n },\n};\n\nconst resolveList = <T extends readonly unknown[]>(\n defaults: T,\n option: T | ((defaults: T) => T) | undefined\n): T => {\n if (option === undefined) {\n return defaults;\n }\n if (Array.isArray(option)) {\n return option as T;\n }\n\n return (option as (defaults: T) => T)(defaults);\n};\n\nconst testPattern = (pattern: RegExp, input: string): boolean => {\n pattern.lastIndex = 0;\n return pattern.test(input);\n};\n\nconst createValueMatcher =\n (patterns: ValuePatternList): ValueMatcher =>\n (value) =>\n patterns.some((pattern) => testPattern(pattern, value));\n\nconst createNameMatcher = (list: RedactionList): NameMatcher => {\n const names = new Set<string>();\n const patterns: RegExp[] = [];\n\n for (const matcher of list) {\n if (typeof matcher === \"string\") {\n names.add(matcher.toLowerCase());\n } else {\n patterns.push(matcher);\n }\n }\n\n return (name) =>\n names.has(name.toLowerCase()) ||\n patterns.some((pattern) => testPattern(pattern, name));\n};\n\nconst resolveCapturedHeaders = (\n option: HeaderCaptureOption | undefined\n): true | ReadonlySet<string> =>\n option === true\n ? true\n : new Set(\n option === false || option === undefined\n ? []\n : option.map((name) => name.toLowerCase())\n );\n\nconst resolveOptions = (options: OtelOptions | undefined): ResolvedOptions => ({\n captureBodySizes: options?.captureBodySizes ?? false,\n capturedRequestHeaders: resolveCapturedHeaders(\n options?.captureRequestHeaders\n ),\n capturedResponseHeaders: resolveCapturedHeaders(\n options?.captureResponseHeaders\n ),\n propagateTraceContext: options?.propagateTraceContext ?? true,\n isRedactedHeader: createNameMatcher(\n resolveList(DEFAULT_REDACTED_HEADERS, options?.redact?.headers)\n ),\n isRedactedQueryParam: createNameMatcher(\n resolveList(DEFAULT_REDACTED_QUERY_PARAMS, options?.redact?.queryParams)\n ),\n isRedactedValue: createValueMatcher(\n resolveList(DEFAULT_REDACTED_VALUE_PATTERNS, options?.redact?.values)\n ),\n knownMethods: new Set(options?.knownMethods ?? DEFAULT_KNOWN_HTTP_METHODS),\n tracer: options?.tracer ?? trace.getTracer(TRACER_NAME, PLUGIN_VERSION),\n});\n\nconst getMethodAttributes = (\n method: string,\n knownMethods: ReadonlySet<string>\n): Attributes => {\n const semanticMethod = knownMethods.has(method) ? method : \"_OTHER\";\n const attributes: Attributes = {\n \"http.request.method\": semanticMethod,\n };\n\n if (method !== semanticMethod) {\n attributes[\"http.request.method_original\"] = method;\n }\n\n return attributes;\n};\n\nconst getContentLength = (headers: Headers): number | undefined => {\n const value = headers.get(\"content-length\")?.trim();\n if (!value || !contentLengthPattern.test(value)) {\n return undefined;\n }\n\n const size = Number(value);\n return Number.isSafeInteger(size) ? size : undefined;\n};\n\nconst getRequestBodySize = (\n body: OkfetchRequestContext[\"body\"],\n headers: Headers\n): number | undefined => {\n if (body === undefined) {\n return undefined;\n }\n\n const contentLength = getContentLength(headers);\n if (contentLength !== undefined) {\n return contentLength;\n }\n if (headers.has(\"content-encoding\")) {\n return undefined;\n }\n if (typeof body === \"string\") {\n return new TextEncoder().encode(body).byteLength;\n }\n if (body instanceof URLSearchParams) {\n return new TextEncoder().encode(body.toString()).byteLength;\n }\n if (body instanceof Blob) {\n return body.size;\n }\n if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {\n return body.byteLength;\n }\n\n return undefined;\n};\n\nconst shouldCaptureHeader = (\n capturedHeaders: true | ReadonlySet<string>,\n name: string\n): boolean => capturedHeaders === true || capturedHeaders.has(name);\n\nconst captureHeaders = (\n attributes: Attributes,\n prefix: \"http.request.header\" | \"http.response.header\",\n headers: Headers,\n capturedHeaders: true | ReadonlySet<string>,\n options: ResolvedOptions\n): void => {\n for (const [name, value] of headers) {\n if (!shouldCaptureHeader(capturedHeaders, name)) {\n continue;\n }\n\n const shouldRedact =\n options.isRedactedHeader(name) || options.isRedactedValue(value);\n attributes[`${prefix}.${name}`] = [shouldRedact ? REDACTED_VALUE : value];\n }\n};\n\nconst getServerPort = (url: URL): number | undefined => {\n if (url.port) {\n return Number(url.port);\n }\n if (url.protocol === \"http:\") {\n return 80;\n }\n if (url.protocol === \"https:\") {\n return 443;\n }\n\n return undefined;\n};\n\nconst redactUrl = (url: URL, options: ResolvedOptions): URL => {\n const redacted = new URL(url.toString());\n redacted.hash = \"\";\n\n if (redacted.username) {\n redacted.username = REDACTED_VALUE;\n }\n if (redacted.password) {\n redacted.password = REDACTED_VALUE;\n }\n\n for (const name of new Set(redacted.searchParams.keys())) {\n const shouldRedact =\n options.isRedactedQueryParam(name) ||\n redacted.searchParams.getAll(name).some(options.isRedactedValue);\n if (shouldRedact) {\n redacted.searchParams.set(name, REDACTED_VALUE);\n }\n }\n\n return redacted;\n};\n\nconst buildRequestAttributes = (\n ctx: OkfetchRequestContext,\n template: string | undefined,\n options: ResolvedOptions\n): Attributes => {\n const url = redactUrl(ctx.url, options);\n const attributes: Attributes = {\n ...getMethodAttributes(ctx.method, options.knownMethods),\n \"server.address\": url.hostname,\n \"url.full\": url.toString(),\n \"url.path\": url.pathname,\n \"url.scheme\": url.protocol.replace(/:$/, \"\"),\n };\n const serverPort = getServerPort(url);\n if (serverPort !== undefined) {\n attributes[\"server.port\"] = serverPort;\n }\n\n if (url.search) {\n attributes[\"url.query\"] = url.search.slice(1);\n }\n if (template) {\n attributes[\"url.template\"] = template;\n }\n\n if (options.captureBodySizes) {\n const bodySize = getRequestBodySize(ctx.body, ctx.headers);\n if (bodySize !== undefined) {\n attributes[\"http.request.body.size\"] = bodySize;\n }\n }\n captureHeaders(\n attributes,\n \"http.request.header\",\n ctx.headers,\n options.capturedRequestHeaders,\n options\n );\n\n return attributes;\n};\n\nconst startSpan = (\n ctx: OkfetchRequestContext,\n state: OtelState,\n options: ResolvedOptions\n): Span => {\n const method = options.knownMethods.has(ctx.method) ? ctx.method : \"HTTP\";\n const name = state.template ? `${method} ${state.template}` : method;\n\n return options.tracer.startSpan(\n name,\n {\n attributes: buildRequestAttributes(ctx, state.template, options),\n kind: SpanKind.CLIENT,\n },\n context.active()\n );\n};\n\n/**\n * Injects trace context headers into a copy of the request headers. A\n * propagator failure is recorded on the span and the request proceeds without\n * propagation headers: telemetry must never fail the request or leave the\n * span open.\n */\nconst injectTraceContext = (\n ctx: OkfetchRequestContext,\n span: Span\n): OkfetchRequestContext => {\n const headers = new Headers(ctx.headers);\n\n try {\n propagation.inject(\n trace.setSpan(context.active(), span),\n headers,\n headersSetter\n );\n } catch (error) {\n span.addEvent(\"okfetch.propagation_failed\", {\n \"exception.message\":\n error instanceof Error ? error.message : String(error),\n });\n return ctx;\n }\n\n return { ...ctx, headers };\n};\n\nconst endSpan = (state: OtelState): void => {\n if (state.ended) {\n return;\n }\n\n state.ended = true;\n state.span?.end();\n};\n\nconst recordResponse = (\n span: Span,\n ctx: OkfetchRequestContext,\n response: Response,\n options: ResolvedOptions\n): void => {\n const attributes: Attributes = {\n \"http.response.status_code\": response.status,\n };\n\n if (\n options.captureBodySizes &&\n ctx.method !== \"HEAD\" &&\n response.status !== 204 &&\n response.status !== 304\n ) {\n const bodySize = getContentLength(response.headers);\n if (bodySize !== undefined) {\n attributes[\"http.response.body.size\"] = bodySize;\n }\n }\n captureHeaders(\n attributes,\n \"http.response.header\",\n response.headers,\n options.capturedResponseHeaders,\n options\n );\n span.setAttributes(attributes);\n};\n\nconst recordFailure = (\n span: Span,\n ctx: OkfetchRequestContext,\n response: Response | undefined,\n error: OkfetchError<unknown>,\n options: ResolvedOptions\n): void => {\n span.setAttribute(\"okfetch.error.tag\", error._tag);\n\n if (response) {\n recordResponse(span, ctx, response, options);\n }\n\n if (error._tag === \"ApiError\") {\n span.setAttributes({\n \"error.type\": String(error.statusCode),\n \"http.response.status_code\": error.statusCode,\n });\n span.setStatus({ code: SpanStatusCode.ERROR });\n return;\n }\n\n if (error._tag === \"ValidationError\") {\n const issues = error.issues.map((issue) => {\n const path = issue.path\n ?.map((segment) =>\n String(typeof segment === \"object\" ? segment.key : segment)\n )\n .join(\".\");\n return path ? `${path}: ${issue.message}` : issue.message;\n });\n const message =\n issues.length > 0\n ? `${error.message}: ${issues.join(\"; \")}`\n : error.message;\n\n span.setAttribute(\"okfetch.validation.issues\", issues);\n span.setAttribute(\"error.type\", error._tag);\n span.recordException({ message, name: error._tag, stack: error.stack });\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n return;\n }\n\n span.setAttribute(\"error.type\", error._tag);\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n};\n\n/**\n * Creates an OpenTelemetry plugin that records a single `CLIENT` span per\n * okfetch request, spanning every retry attempt.\n *\n * The span records HTTP client attributes available through Fetch and marks\n * failures using the OpenTelemetry HTTP span conventions. Request and response\n * bodies are never recorded.\n */\nexport const otel = (options?: OtelOptions): OkfetchPlugin => {\n const resolved = resolveOptions(options);\n\n const init = ({\n options: requestOptions,\n url,\n }: OkfetchPluginInitInput): OkfetchPluginInitInput => {\n const state: OtelState = {\n ended: false,\n resendCount: 0,\n template:\n requestOptions.params === undefined ? undefined : toTemplate(url),\n };\n const nextOptions: WithState<OkfetchOptions> = {\n ...requestOptions,\n [stateKey]: state,\n };\n\n return { options: nextOptions, url };\n };\n\n const onRequest = (ctx: OkfetchRequestContext): OkfetchRequestContext => {\n const state = getState(ctx) ?? { ended: false, resendCount: 0 };\n let nextCtx: WithState<OkfetchRequestContext> = {\n ...ctx,\n [stateKey]: state,\n };\n if (state.span) {\n state.resendCount += 1;\n state.span.setAttribute(\"http.request.resend_count\", state.resendCount);\n } else {\n state.span = startSpan(nextCtx, state, resolved);\n }\n\n if (resolved.propagateTraceContext) {\n nextCtx = {\n ...injectTraceContext(nextCtx, state.span),\n [stateKey]: state,\n };\n }\n\n return nextCtx;\n };\n\n return {\n name: PLUGIN_NAME,\n version: PLUGIN_VERSION,\n init,\n hooks: {\n onRequest,\n onSuccess(ctx, response) {\n const state = getState(ctx);\n if (!state?.span) {\n return;\n }\n\n recordResponse(state.span, ctx, response, resolved);\n endSpan(state);\n },\n onFail(ctx, response, error) {\n const state = getState(ctx);\n if (!state) {\n return;\n }\n\n state.span ??= startSpan(ctx, state, resolved);\n recordFailure(state.span, ctx, response, error, resolved);\n endSpan(state);\n },\n onRetry(ctx, response, error, attempt) {\n const state = getState(ctx);\n if (!state?.span) {\n return;\n }\n\n const attributes: Attributes = {\n \"error.type\":\n error._tag === \"ApiError\" ? String(error.statusCode) : error._tag,\n \"okfetch.error.tag\": error._tag,\n \"okfetch.retry.attempt\": attempt + 1,\n };\n if (response) {\n attributes[\"http.response.status_code\"] = response.status;\n }\n\n state.span.addEvent(\"okfetch.retry\", attributes);\n },\n },\n } satisfies OkfetchPlugin;\n};\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@okfetch/otel",
3
+ "version": "0.5.0",
4
+ "description": "An OpenTelemetry tracing plugin for okfetch request lifecycles.",
5
+ "keywords": [
6
+ "fetch",
7
+ "opentelemetry",
8
+ "otel",
9
+ "plugin",
10
+ "tracing",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/aldotestino/okfetch#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/aldotestino/okfetch/issues"
16
+ },
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/aldotestino/okfetch.git"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.mjs",
29
+ "types": "./dist/index.d.mts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.mts",
33
+ "import": "./dist/index.mjs",
34
+ "require": "./dist/index.cjs",
35
+ "default": "./dist/index.mjs"
36
+ }
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "devDependencies": {
42
+ "@okfetch/fetch": "^0.5.0",
43
+ "@opentelemetry/api": "^1.9.0",
44
+ "@opentelemetry/context-async-hooks": "^2.0.0",
45
+ "@opentelemetry/core": "^2.0.0",
46
+ "@opentelemetry/sdk-trace-base": "^2.0.0",
47
+ "@types/bun": "latest",
48
+ "better-result": "^3.0.1"
49
+ },
50
+ "peerDependencies": {
51
+ "@okfetch/fetch": "^0.5.0",
52
+ "@opentelemetry/api": "^1.9.0"
53
+ }
54
+ }