@stainlessdev/xray-core 0.1.0-branch.bg-publish-to-npm.18b1cb1

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/dist/index.cjs ADDED
@@ -0,0 +1,1191 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ XrayConfigError: () => XrayConfigError,
24
+ createEmitter: () => createEmitter,
25
+ normalizeConfig: () => normalizeConfig
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/route.ts
30
+ function normalizeRoutePattern(route) {
31
+ if (!route) {
32
+ return "/";
33
+ }
34
+ const cleaned = stripQueryAndFragment(route).trim();
35
+ if (!cleaned) {
36
+ return "/";
37
+ }
38
+ const leading = cleaned.startsWith("/") ? cleaned : `/${cleaned}`;
39
+ const segments = leading.split("/").filter(Boolean);
40
+ if (segments.length === 0) {
41
+ return "/";
42
+ }
43
+ const normalized = segments.map(normalizeRouteSegment).join("/");
44
+ return `/${normalized}`;
45
+ }
46
+ function stripQueryAndFragment(value) {
47
+ const hashIndex = value.indexOf("#");
48
+ const beforeHash = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
49
+ const queryIndex = beforeHash.indexOf("?");
50
+ return queryIndex >= 0 ? beforeHash.slice(0, queryIndex) : beforeHash;
51
+ }
52
+ function normalizeRouteSegment(segment) {
53
+ if (segment === "*") {
54
+ return "*";
55
+ }
56
+ if (segment.startsWith("[") && segment.endsWith("]")) {
57
+ const inner = segment.slice(1, -1);
58
+ if (inner.startsWith("...") || inner.startsWith("[...")) {
59
+ return "*";
60
+ }
61
+ return `:${inner}`;
62
+ }
63
+ if (segment.startsWith("{") && segment.endsWith("}")) {
64
+ return `:${segment.slice(1, -1)}`;
65
+ }
66
+ if (segment.startsWith("$")) {
67
+ return `:${segment.slice(1)}`;
68
+ }
69
+ if (segment.startsWith(":")) {
70
+ return `:${segment.slice(1)}`;
71
+ }
72
+ return segment;
73
+ }
74
+
75
+ // src/config.ts
76
+ var defaultCapture = {
77
+ requestHeaders: true,
78
+ responseHeaders: true,
79
+ requestBody: "none",
80
+ responseBody: "none",
81
+ maxBodyBytes: 65536
82
+ };
83
+ var defaultRedaction = {
84
+ headers: ["authorization", "cookie", "set-cookie", "x-api-key"],
85
+ queryParams: [],
86
+ bodyJsonPaths: [],
87
+ replacement: "[REDACTED]"
88
+ };
89
+ var defaultRequestId = {
90
+ header: "x-request-id",
91
+ generate: true
92
+ };
93
+ var defaultRoute = {
94
+ normalize: true,
95
+ normalizer: normalizeRoutePattern
96
+ };
97
+ var defaultOtelBase = {
98
+ endpointUrl: void 0,
99
+ headers: {},
100
+ timeoutMs: 5e3,
101
+ spanProcessor: "batch",
102
+ sampler: { type: "ratio", ratio: 1 }
103
+ };
104
+ var XrayConfigError = class extends Error {
105
+ constructor(code, message) {
106
+ super(message);
107
+ this.code = code;
108
+ }
109
+ };
110
+ function normalizeConfig(config) {
111
+ if (!config || !config.serviceName || !config.serviceName.trim()) {
112
+ throw new XrayConfigError("INVALID_CONFIG", "serviceName is required");
113
+ }
114
+ const logger = config.logger ?? console;
115
+ const logLevel = config.logLevel ?? "warn";
116
+ const capture = normalizeCapture(config.capture);
117
+ const redaction = normalizeRedaction(config.redaction);
118
+ const requestId = normalizeRequestId(config.requestId);
119
+ const route = normalizeRoute(config.route);
120
+ const otel = normalizeOtel(config.otel);
121
+ return {
122
+ serviceName: config.serviceName.trim(),
123
+ environment: config.environment?.trim() || void 0,
124
+ version: config.version?.trim() || void 0,
125
+ logger,
126
+ logLevel,
127
+ otel,
128
+ capture,
129
+ redaction,
130
+ requestId,
131
+ route
132
+ };
133
+ }
134
+ function normalizeCapture(cfg) {
135
+ const capture = {
136
+ ...defaultCapture,
137
+ ...cfg
138
+ };
139
+ if (!["none", "text", "base64"].includes(capture.requestBody)) {
140
+ throw new XrayConfigError(
141
+ "INVALID_CONFIG",
142
+ "capture.requestBody must be none, text, or base64"
143
+ );
144
+ }
145
+ if (!["none", "text", "base64"].includes(capture.responseBody)) {
146
+ throw new XrayConfigError(
147
+ "INVALID_CONFIG",
148
+ "capture.responseBody must be none, text, or base64"
149
+ );
150
+ }
151
+ if (!Number.isFinite(capture.maxBodyBytes) || capture.maxBodyBytes < 0) {
152
+ throw new XrayConfigError("INVALID_CONFIG", "capture.maxBodyBytes must be >= 0");
153
+ }
154
+ return capture;
155
+ }
156
+ function normalizeRedaction(cfg) {
157
+ const redaction = {
158
+ ...defaultRedaction,
159
+ ...cfg
160
+ };
161
+ redaction.headers = normalizeStringList(redaction.headers);
162
+ redaction.queryParams = normalizeStringList(redaction.queryParams);
163
+ redaction.bodyJsonPaths = normalizeStringList(redaction.bodyJsonPaths);
164
+ redaction.replacement = redaction.replacement || defaultRedaction.replacement;
165
+ if (!redaction.replacement) {
166
+ throw new XrayConfigError("INVALID_REDACTION", "redaction.replacement must be non-empty");
167
+ }
168
+ return redaction;
169
+ }
170
+ function normalizeRequestId(cfg) {
171
+ const requestId = {
172
+ ...defaultRequestId,
173
+ ...cfg
174
+ };
175
+ requestId.header = requestId.header.trim().toLowerCase();
176
+ if (!requestId.header) {
177
+ throw new XrayConfigError("INVALID_CONFIG", "requestId.header must be non-empty");
178
+ }
179
+ return requestId;
180
+ }
181
+ function normalizeRoute(cfg) {
182
+ const route = {
183
+ ...defaultRoute,
184
+ ...cfg
185
+ };
186
+ if (route.normalize && !route.normalizer) {
187
+ route.normalizer = normalizeRoutePattern;
188
+ }
189
+ return route;
190
+ }
191
+ function normalizeOtel(cfg) {
192
+ const endpointUrl = cfg?.endpointUrl?.trim() || void 0;
193
+ const enabled = cfg?.enabled ?? !!endpointUrl;
194
+ const otel = {
195
+ ...defaultOtelBase,
196
+ ...cfg,
197
+ enabled,
198
+ endpointUrl,
199
+ headers: cfg?.headers ?? defaultOtelBase.headers,
200
+ timeoutMs: cfg?.timeoutMs ?? defaultOtelBase.timeoutMs,
201
+ spanProcessor: cfg?.spanProcessor ?? defaultOtelBase.spanProcessor,
202
+ sampler: cfg?.sampler ?? defaultOtelBase.sampler
203
+ };
204
+ if (otel.enabled && !otel.endpointUrl) {
205
+ throw new XrayConfigError("INVALID_OTEL", "otel.endpointUrl is required when enabled");
206
+ }
207
+ if (otel.sampler.type === "ratio") {
208
+ const ratio = otel.sampler.ratio ?? 1;
209
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
210
+ throw new XrayConfigError("INVALID_OTEL", "otel.sampler.ratio must be between 0 and 1");
211
+ }
212
+ otel.sampler = { type: "ratio", ratio };
213
+ }
214
+ return otel;
215
+ }
216
+ function normalizeStringList(values) {
217
+ if (!values) {
218
+ return [];
219
+ }
220
+ return values.map((entry) => entry.trim()).filter(Boolean);
221
+ }
222
+
223
+ // src/logger.ts
224
+ var logLevelOrder = {
225
+ debug: 10,
226
+ info: 20,
227
+ warn: 30,
228
+ error: 40
229
+ };
230
+ function logWithLevel(logger, level, threshold, message, fields) {
231
+ if (logLevelOrder[level] < logLevelOrder[threshold]) {
232
+ return;
233
+ }
234
+ const fn = logger[level] ?? logger.warn ?? logger.info ?? logger.debug ?? logger.error ?? console.log;
235
+ try {
236
+ fn.call(logger, message, fields);
237
+ } catch {
238
+ }
239
+ }
240
+
241
+ // src/header_redaction.ts
242
+ var headerNameCompactor = /[-_.]/g;
243
+ var defaultHeaderMatcher = newHeaderRedactionMatcher(
244
+ defaultSensitiveHeaderNames(),
245
+ defaultSensitiveKeywords()
246
+ );
247
+ function authSchemePrefix(value) {
248
+ if (!value) {
249
+ return "";
250
+ }
251
+ const lower = value.toLowerCase();
252
+ if (lower.startsWith("basic")) {
253
+ return value.slice(0, "basic".length);
254
+ }
255
+ if (lower.startsWith("bearer")) {
256
+ return value.slice(0, "bearer".length);
257
+ }
258
+ if (lower.startsWith("digest")) {
259
+ return value.slice(0, "digest".length);
260
+ }
261
+ if (lower.startsWith("negotiate")) {
262
+ return value.slice(0, "negotiate".length);
263
+ }
264
+ return "";
265
+ }
266
+ function redactCookieValue(value, replacement) {
267
+ if (!value) {
268
+ return replacement;
269
+ }
270
+ const parts = value.split(";");
271
+ const redacted = [];
272
+ for (const part of parts) {
273
+ const segment = part.trim();
274
+ if (!segment) {
275
+ redacted.push(replacement);
276
+ continue;
277
+ }
278
+ const idx = segment.indexOf("=");
279
+ if (idx <= 0) {
280
+ redacted.push(replacement);
281
+ continue;
282
+ }
283
+ const name = segment.slice(0, idx);
284
+ if (!name) {
285
+ redacted.push(replacement);
286
+ continue;
287
+ }
288
+ redacted.push(`${name}=${replacement}`);
289
+ }
290
+ return redacted.join("; ");
291
+ }
292
+ function redactSetCookieValue(value, replacement) {
293
+ if (!value) {
294
+ return replacement;
295
+ }
296
+ const parts = value.split(";");
297
+ const first = parts.shift() ?? "";
298
+ const idx = first.indexOf("=");
299
+ if (idx <= 0) {
300
+ return replacement;
301
+ }
302
+ const name = first.slice(0, idx);
303
+ if (!name) {
304
+ return replacement;
305
+ }
306
+ const redacted = `${name}=${replacement}`;
307
+ if (parts.length === 0) {
308
+ return redacted;
309
+ }
310
+ return `${redacted};${parts.join(";")}`;
311
+ }
312
+ function addSensitiveHeaderNames(target, headers) {
313
+ for (const header of headers) {
314
+ const normalized = normalizeHeaderName(header);
315
+ if (normalized) {
316
+ target.add(normalized);
317
+ }
318
+ }
319
+ }
320
+ function buildKeywordSets(keywords) {
321
+ const tokens = /* @__PURE__ */ new Set();
322
+ const compacted = /* @__PURE__ */ new Set();
323
+ for (const keyword of keywords) {
324
+ const normalized = normalizeHeaderName(keyword);
325
+ if (!normalized) {
326
+ continue;
327
+ }
328
+ const compactedKeyword = compactNormalizedHeaderName(normalized);
329
+ if (compactedKeyword) {
330
+ compacted.add(compactedKeyword);
331
+ }
332
+ if (!normalized.includes("-") && !normalized.includes("_") && !normalized.includes(".")) {
333
+ tokens.add(normalized);
334
+ }
335
+ }
336
+ return {
337
+ compacted: Array.from(compacted).sort(),
338
+ tokens
339
+ };
340
+ }
341
+ function compactNormalizedHeaderName(normalized) {
342
+ if (!normalized) {
343
+ return "";
344
+ }
345
+ return normalized.replace(headerNameCompactor, "");
346
+ }
347
+ function defaultSensitiveHeaderNames() {
348
+ return [
349
+ "authorization",
350
+ "cookie",
351
+ "proxy-authenticate",
352
+ "proxy-authorization",
353
+ "set-cookie",
354
+ "www-authenticate"
355
+ ];
356
+ }
357
+ function defaultSensitiveKeywords() {
358
+ return [
359
+ "api-key",
360
+ "api_key",
361
+ "apikey",
362
+ "auth",
363
+ "authenticate",
364
+ "authorization",
365
+ "credential",
366
+ "password",
367
+ "passwd",
368
+ "private-key",
369
+ "private_key",
370
+ "privatekey",
371
+ "secret",
372
+ "session",
373
+ "sessionid",
374
+ "signature",
375
+ "token"
376
+ ];
377
+ }
378
+ function newHeaderRedactionMatcher(names, keywords) {
379
+ const exactSensitive = /* @__PURE__ */ new Set();
380
+ addSensitiveHeaderNames(exactSensitive, names);
381
+ const { compacted, tokens } = buildKeywordSets(keywords);
382
+ return {
383
+ exactSensitive,
384
+ keywordCompacted: compacted,
385
+ keywordTokens: tokens
386
+ };
387
+ }
388
+ function normalizeHeaderName(name) {
389
+ return name.trim().toLowerCase();
390
+ }
391
+
392
+ // src/redaction.ts
393
+ function applyRedaction(config, log) {
394
+ const redacted = { ...log };
395
+ if (redacted.requestHeaders) {
396
+ redacted.requestHeaders = redactHeaders(redacted.requestHeaders, config);
397
+ }
398
+ if (redacted.responseHeaders) {
399
+ redacted.responseHeaders = redactHeaders(redacted.responseHeaders, config);
400
+ }
401
+ redacted.url = redactUrl(redacted.url, config);
402
+ if (redacted.requestBody) {
403
+ redacted.requestBody = redactBody(redacted.requestBody, redacted.requestHeaders, config);
404
+ }
405
+ if (redacted.responseBody) {
406
+ redacted.responseBody = redactBody(redacted.responseBody, redacted.responseHeaders, config);
407
+ }
408
+ return redacted;
409
+ }
410
+ function redactHeaders(headers, config) {
411
+ const list = new Set(config.headers.map((name) => name.toLowerCase()));
412
+ const result = {};
413
+ for (const [name, value] of Object.entries(headers)) {
414
+ const lower = name.toLowerCase();
415
+ if (!list.has(lower)) {
416
+ result[name] = value;
417
+ continue;
418
+ }
419
+ const redactValue = (entry) => redactHeaderValue(lower, entry, config.replacement);
420
+ if (Array.isArray(value)) {
421
+ result[name] = value.map(redactValue);
422
+ } else {
423
+ result[name] = redactValue(value);
424
+ }
425
+ }
426
+ return result;
427
+ }
428
+ function redactHeaderValue(name, value, replacement) {
429
+ switch (name) {
430
+ case "authorization":
431
+ case "proxy-authorization": {
432
+ const scheme = authSchemePrefix(value);
433
+ if (!scheme) {
434
+ return replacement;
435
+ }
436
+ return `${scheme} ${replacement}`;
437
+ }
438
+ case "cookie":
439
+ return redactCookieValue(value, replacement);
440
+ case "set-cookie":
441
+ return redactSetCookieValue(value, replacement);
442
+ default:
443
+ return replacement;
444
+ }
445
+ }
446
+ function redactUrl(value, config) {
447
+ if (!value || config.queryParams.length === 0) {
448
+ return value;
449
+ }
450
+ try {
451
+ const parsed = new URL(value);
452
+ const params = parsed.searchParams;
453
+ if (!params || params.size === 0) {
454
+ return value;
455
+ }
456
+ const redact = new Set(config.queryParams.map((key) => key.toLowerCase()));
457
+ const next = new URLSearchParams();
458
+ params.forEach((val, key) => {
459
+ if (redact.has(key.toLowerCase())) {
460
+ next.append(key, config.replacement);
461
+ } else {
462
+ next.append(key, val);
463
+ }
464
+ });
465
+ parsed.search = next.toString();
466
+ return parsed.toString();
467
+ } catch {
468
+ return value;
469
+ }
470
+ }
471
+ function redactBody(body, headers, config) {
472
+ if (config.bodyJsonPaths.length === 0) {
473
+ return body;
474
+ }
475
+ if (!body.value || body.encoding !== "utf8") {
476
+ return body;
477
+ }
478
+ if (!isJsonContentType(headers)) {
479
+ return body;
480
+ }
481
+ let parsed;
482
+ try {
483
+ parsed = JSON.parse(body.value);
484
+ } catch {
485
+ return body;
486
+ }
487
+ for (const path of config.bodyJsonPaths) {
488
+ const segments = parseJsonPath(path);
489
+ if (!segments) {
490
+ continue;
491
+ }
492
+ redactJsonPath(parsed, segments, config.replacement);
493
+ }
494
+ const next = { ...body };
495
+ next.value = JSON.stringify(parsed);
496
+ return next;
497
+ }
498
+ function isJsonContentType(headers) {
499
+ if (!headers) {
500
+ return false;
501
+ }
502
+ const contentType = headers["content-type"] || headers["Content-Type"];
503
+ const value = Array.isArray(contentType) ? contentType[0] : contentType;
504
+ if (!value) {
505
+ return false;
506
+ }
507
+ const normalized = value.split(";")[0];
508
+ if (!normalized) {
509
+ return false;
510
+ }
511
+ const trimmed = normalized.trim().toLowerCase();
512
+ return trimmed === "application/json" || trimmed.endsWith("+json");
513
+ }
514
+ function parseJsonPath(path) {
515
+ const trimmed = path.trim();
516
+ if (!trimmed) {
517
+ return null;
518
+ }
519
+ const normalized = trimmed.startsWith("$.") ? trimmed.slice(2) : trimmed;
520
+ if (!normalized) {
521
+ return null;
522
+ }
523
+ const segments = [];
524
+ const parts = normalized.split(".");
525
+ for (const part of parts) {
526
+ if (!part) {
527
+ continue;
528
+ }
529
+ let cursor = part;
530
+ const bracketIndex = cursor.indexOf("[");
531
+ if (bracketIndex === -1) {
532
+ segments.push(cursor);
533
+ continue;
534
+ }
535
+ const name = cursor.slice(0, bracketIndex);
536
+ if (name) {
537
+ segments.push(name);
538
+ }
539
+ cursor = cursor.slice(bracketIndex);
540
+ const matches = cursor.match(/\[(\d+)\]/g);
541
+ if (!matches) {
542
+ continue;
543
+ }
544
+ for (const match of matches) {
545
+ const indexValue = match.slice(1, -1);
546
+ const index = Number.parseInt(indexValue, 10);
547
+ if (Number.isFinite(index)) {
548
+ segments.push(index);
549
+ }
550
+ }
551
+ }
552
+ return segments.length > 0 ? segments : null;
553
+ }
554
+ function redactJsonPath(value, segments, replacement) {
555
+ if (!value || segments.length === 0) {
556
+ return;
557
+ }
558
+ let current = value;
559
+ for (let i = 0; i < segments.length; i += 1) {
560
+ const segment = segments[i];
561
+ if (segment === void 0) {
562
+ return;
563
+ }
564
+ const isLast = i === segments.length - 1;
565
+ if (typeof segment === "number") {
566
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) {
567
+ return;
568
+ }
569
+ if (isLast) {
570
+ current[segment] = replacement;
571
+ return;
572
+ }
573
+ current = current[segment];
574
+ continue;
575
+ }
576
+ if (!current || typeof current !== "object") {
577
+ return;
578
+ }
579
+ const record = current;
580
+ if (!(segment in record)) {
581
+ return;
582
+ }
583
+ if (isLast) {
584
+ record[segment] = replacement;
585
+ return;
586
+ }
587
+ current = record[segment];
588
+ }
589
+ }
590
+
591
+ // src/encoding.ts
592
+ var utf8Decoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-8", { fatal: true }) : null;
593
+ var utf8DecoderLenient = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-8") : null;
594
+ var maybeBuffer = globalThis.Buffer;
595
+
596
+ // src/request_log.ts
597
+ var controlChars = /[\x00-\x1F\x7F]/g;
598
+ function sanitizeLogString(value) {
599
+ if (!value) {
600
+ return value;
601
+ }
602
+ return value.replace(controlChars, "");
603
+ }
604
+ function sanitizeHeaderValues(headers) {
605
+ if (!headers) {
606
+ return void 0;
607
+ }
608
+ const sanitized = {};
609
+ for (const [key, value] of Object.entries(headers)) {
610
+ const name = sanitizeLogString(key);
611
+ if (Array.isArray(value)) {
612
+ sanitized[name] = value.map((entry) => sanitizeLogString(entry));
613
+ } else {
614
+ sanitized[name] = sanitizeLogString(value);
615
+ }
616
+ }
617
+ return sanitized;
618
+ }
619
+
620
+ // src/attrkey.ts
621
+ var AttributeKeyRequestBody = "http.request.body";
622
+ var AttributeKeyRequestBodyEncoding = "http.request.body.encoding";
623
+ var AttributeKeyRequestBodyTruncated = "http.request.body.truncated";
624
+ var AttributeKeyRequestID = "http.request.id";
625
+ var AttributeKeyResponseBody = "http.response.body";
626
+ var AttributeKeyResponseBodyEncoding = "http.response.body.encoding";
627
+ var AttributeKeyResponseBodyTruncated = "http.response.body.truncated";
628
+ var AttributeKeySpanDrop = "stainlessxray.internal.drop";
629
+
630
+ // src/attributes.ts
631
+ var attributeKeyEndUserId = "enduser.id";
632
+ var attributeKeyHttpRequestBodySize = "http.request.body.size";
633
+ var attributeKeyHttpRequestMethod = "http.request.method";
634
+ var attributeKeyHttpResponseBodySize = "http.response.body.size";
635
+ var attributeKeyHttpResponseStatusCode = "http.response.status_code";
636
+ var attributeKeyHttpRoute = "http.route";
637
+ var attributeKeyUrlPath = "url.path";
638
+ var attributeKeyUrlFull = "url.full";
639
+ function setHeaderAttributes(span, headers, prefix) {
640
+ if (!headers) {
641
+ return;
642
+ }
643
+ const keys = Object.keys(headers).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
644
+ for (const key of keys) {
645
+ const values = headers[key];
646
+ if (!values || Array.isArray(values) && values.length === 0) {
647
+ continue;
648
+ }
649
+ span.setAttribute(prefix + key.toLowerCase(), Array.isArray(values) ? values : [values]);
650
+ }
651
+ }
652
+ function setRequestAttributes(span, method, urlFull) {
653
+ span.setAttribute(attributeKeyHttpRequestMethod, method);
654
+ if (urlFull) {
655
+ span.setAttribute(attributeKeyUrlFull, urlFull);
656
+ const path = extractPath(urlFull);
657
+ if (path) {
658
+ span.setAttribute(attributeKeyUrlPath, path);
659
+ }
660
+ }
661
+ }
662
+ function extractPath(url) {
663
+ try {
664
+ return new URL(url).pathname;
665
+ } catch {
666
+ const match = url.match(/^[^?#]*/);
667
+ return match?.[0] || void 0;
668
+ }
669
+ }
670
+ function setRequestBodyAttributes(span, body) {
671
+ if (!body.value) {
672
+ return;
673
+ }
674
+ span.setAttribute(AttributeKeyRequestBody, body.value);
675
+ span.setAttribute(AttributeKeyRequestBodyEncoding, body.encoding);
676
+ if (body.truncated) {
677
+ span.setAttribute(AttributeKeyRequestBodyTruncated, true);
678
+ }
679
+ }
680
+ function setRequestBodySizeAttribute(span, size) {
681
+ span.setAttribute(attributeKeyHttpRequestBodySize, size);
682
+ }
683
+ function setResponseBodyAttributes(span, body) {
684
+ if (!body.value) {
685
+ return;
686
+ }
687
+ span.setAttribute(AttributeKeyResponseBody, body.value);
688
+ span.setAttribute(AttributeKeyResponseBodyEncoding, body.encoding);
689
+ if (body.truncated) {
690
+ span.setAttribute(AttributeKeyResponseBodyTruncated, true);
691
+ }
692
+ }
693
+ function setResponseBodySizeAttribute(span, size) {
694
+ span.setAttribute(attributeKeyHttpResponseBodySize, size);
695
+ }
696
+ function setResponseStatusAttribute(span, statusCode) {
697
+ span.setAttribute(attributeKeyHttpResponseStatusCode, statusCode);
698
+ }
699
+ function setRouteAttribute(span, route) {
700
+ if (route) {
701
+ span.setAttribute(attributeKeyHttpRoute, route);
702
+ }
703
+ }
704
+ function setUserIdAttribute(span, userId) {
705
+ span.setAttribute(attributeKeyEndUserId, userId);
706
+ }
707
+ function setRequestIdAttribute(span, requestId) {
708
+ span.setAttribute(AttributeKeyRequestID, requestId);
709
+ }
710
+
711
+ // src/otel.ts
712
+ var import_api = require("@opentelemetry/api");
713
+ var import_core = require("@opentelemetry/core");
714
+ var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
715
+ var import_resources = require("@opentelemetry/resources");
716
+ var import_semantic_conventions = require("@opentelemetry/semantic-conventions");
717
+ var import_otlp_transformer = require("@opentelemetry/otlp-transformer");
718
+ var defaultAttributeCountLimit = 128;
719
+ function createTracerProvider(config) {
720
+ if (!config.otel.enabled || !config.otel.endpointUrl) {
721
+ return null;
722
+ }
723
+ if (config.otel.endpointUrl.startsWith("http://")) {
724
+ import_api.diag.warn("xray: OTLP endpoint uses plaintext HTTP");
725
+ }
726
+ const attributeValueLengthLimit = Math.max(1, Math.ceil(config.capture.maxBodyBytes * 4 / 3));
727
+ const resource = (0, import_resources.resourceFromAttributes)({
728
+ [import_semantic_conventions.ATTR_SERVICE_NAME]: config.serviceName,
729
+ [import_semantic_conventions.ATTR_TELEMETRY_SDK_LANGUAGE]: isNodeRuntime() ? "nodejs" : "webjs",
730
+ [import_semantic_conventions.ATTR_TELEMETRY_SDK_NAME]: "stainless-xray",
731
+ [import_semantic_conventions.ATTR_TELEMETRY_SDK_VERSION]: sdkVersion()
732
+ });
733
+ const exporter = new FetchSpanExporter({
734
+ endpointUrl: config.otel.endpointUrl,
735
+ headers: config.otel.headers ?? {},
736
+ timeoutMillis: config.otel.timeoutMs
737
+ });
738
+ const sampler = createSampler(config);
739
+ const spanProcessor = createSpanProcessor(config.otel.spanProcessor, exporter);
740
+ const dropProcessor = new DropFilterSpanProcessor(spanProcessor);
741
+ const provider = new import_sdk_trace_base.BasicTracerProvider({
742
+ forceFlushTimeoutMillis: 3e4,
743
+ generalLimits: {
744
+ attributeCountLimit: defaultAttributeCountLimit,
745
+ attributeValueLengthLimit
746
+ },
747
+ resource,
748
+ sampler,
749
+ spanLimits: {
750
+ attributeCountLimit: defaultAttributeCountLimit,
751
+ attributePerEventCountLimit: defaultAttributeCountLimit,
752
+ attributePerLinkCountLimit: defaultAttributeCountLimit,
753
+ attributeValueLengthLimit,
754
+ eventCountLimit: defaultAttributeCountLimit,
755
+ linkCountLimit: defaultAttributeCountLimit
756
+ },
757
+ spanProcessors: [dropProcessor]
758
+ });
759
+ return provider;
760
+ }
761
+ function tracerFromProvider(provider) {
762
+ return provider.getTracer("stainless-xray");
763
+ }
764
+ function spanFromTracer(tracer, name) {
765
+ return tracer.startSpan(name, { kind: import_api.SpanKind.SERVER }, import_api.ROOT_CONTEXT);
766
+ }
767
+ function spanStatusFromError(span, err) {
768
+ if (err instanceof Error) {
769
+ span.recordException(err);
770
+ } else if (typeof err === "string") {
771
+ span.recordException(err);
772
+ } else {
773
+ span.recordException({ message: String(err) });
774
+ }
775
+ span.setStatus({ code: import_api.SpanStatusCode.ERROR });
776
+ }
777
+ var DropFilterSpanProcessor = class {
778
+ constructor(next) {
779
+ this.next = next;
780
+ }
781
+ forceFlush() {
782
+ return this.next.forceFlush();
783
+ }
784
+ onEnd(span) {
785
+ if (span.attributes[AttributeKeySpanDrop] === true) {
786
+ return;
787
+ }
788
+ this.next.onEnd(span);
789
+ }
790
+ onStart(span, parentContext) {
791
+ this.next.onStart(span, parentContext);
792
+ }
793
+ shutdown() {
794
+ return this.next.shutdown();
795
+ }
796
+ };
797
+ var FetchSpanExporter = class {
798
+ constructor(options) {
799
+ this.endpointUrl = options.endpointUrl;
800
+ this.headers = { ...options.headers };
801
+ this.timeoutMillis = options.timeoutMillis;
802
+ this.isShutdown = false;
803
+ const protobufSerializer = import_otlp_transformer.ProtobufTraceSerializer && typeof import_otlp_transformer.ProtobufTraceSerializer.serializeRequest === "function" ? import_otlp_transformer.ProtobufTraceSerializer : null;
804
+ this.serializer = protobufSerializer ?? import_otlp_transformer.JsonTraceSerializer;
805
+ this.contentType = protobufSerializer ? "application/x-protobuf" : "application/json";
806
+ }
807
+ export(spans, resultCallback) {
808
+ if (this.isShutdown) {
809
+ resultCallback({ code: import_core.ExportResultCode.FAILED });
810
+ return;
811
+ }
812
+ const payload = this.serializer.serializeRequest(spans);
813
+ if (!payload) {
814
+ resultCallback({
815
+ code: import_core.ExportResultCode.FAILED,
816
+ error: new Error("OTLP export failed: empty payload")
817
+ });
818
+ return;
819
+ }
820
+ const headers = {
821
+ ...this.headers,
822
+ "Content-Type": this.contentType
823
+ };
824
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
825
+ let timeout;
826
+ if (controller) {
827
+ timeout = setTimeout(() => controller.abort(), this.timeoutMillis);
828
+ }
829
+ const doExport = async () => {
830
+ const response = await fetch(this.endpointUrl, {
831
+ method: "POST",
832
+ headers,
833
+ body: payload,
834
+ signal: controller?.signal
835
+ });
836
+ if (!response.ok) {
837
+ throw new Error(`OTLP export failed: ${response.status}`);
838
+ }
839
+ };
840
+ doExport().then(() => {
841
+ if (timeout) {
842
+ clearTimeout(timeout);
843
+ }
844
+ resultCallback({ code: import_core.ExportResultCode.SUCCESS });
845
+ }).catch((err) => {
846
+ if (timeout) {
847
+ clearTimeout(timeout);
848
+ }
849
+ import_api.diag.error("OTLP export failed", err);
850
+ resultCallback({ code: import_core.ExportResultCode.FAILED, error: err });
851
+ });
852
+ }
853
+ async forceFlush() {
854
+ return;
855
+ }
856
+ async shutdown() {
857
+ this.isShutdown = true;
858
+ }
859
+ };
860
+ function createSpanProcessor(mode, exporter) {
861
+ if (mode === "simple") {
862
+ return new import_sdk_trace_base.SimpleSpanProcessor(exporter);
863
+ }
864
+ return new import_sdk_trace_base.BatchSpanProcessor(exporter, {
865
+ maxQueueSize: 2048,
866
+ maxExportBatchSize: 512,
867
+ scheduledDelayMillis: 5e3,
868
+ exportTimeoutMillis: 3e4
869
+ });
870
+ }
871
+ function createSampler(config) {
872
+ const sampler = config.otel.sampler;
873
+ if (sampler.type === "always_off") {
874
+ return new import_sdk_trace_base.AlwaysOffSampler();
875
+ }
876
+ if (sampler.type === "ratio") {
877
+ return new import_sdk_trace_base.TraceIdRatioBasedSampler(sampler.ratio ?? 1);
878
+ }
879
+ return new import_sdk_trace_base.AlwaysOnSampler();
880
+ }
881
+ function sdkVersion() {
882
+ if (true) {
883
+ return "0.1.0";
884
+ }
885
+ return "unknown";
886
+ }
887
+ function isNodeRuntime() {
888
+ const maybeProcess = globalThis.process;
889
+ return !!maybeProcess?.versions?.node;
890
+ }
891
+
892
+ // src/uuid.ts
893
+ function uuidv7() {
894
+ const bytes = new Uint8Array(16);
895
+ crypto.getRandomValues(bytes);
896
+ const timestamp = BigInt(Date.now());
897
+ bytes[0] = Number(timestamp >> 40n & 0xffn);
898
+ bytes[1] = Number(timestamp >> 32n & 0xffn);
899
+ bytes[2] = Number(timestamp >> 24n & 0xffn);
900
+ bytes[3] = Number(timestamp >> 16n & 0xffn);
901
+ bytes[4] = Number(timestamp >> 8n & 0xffn);
902
+ bytes[5] = Number(timestamp & 0xffn);
903
+ const byte6 = bytes[6] ?? 0;
904
+ const byte8 = bytes[8] ?? 0;
905
+ bytes[6] = byte6 & 15 | 112;
906
+ bytes[8] = byte8 & 63 | 128;
907
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
908
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
909
+ }
910
+
911
+ // src/state.ts
912
+ var contextMap = /* @__PURE__ */ new WeakMap();
913
+ function bindContext(ctx, state) {
914
+ contextMap.set(ctx, state);
915
+ }
916
+ function getContextState(ctx) {
917
+ return contextMap.get(ctx);
918
+ }
919
+
920
+ // src/emitter.ts
921
+ function createEmitter(config) {
922
+ const resolved = normalizeConfig(config);
923
+ const tracerProvider = createTracerProvider(resolved);
924
+ const tracer = tracerProvider ? tracerFromProvider(tracerProvider) : null;
925
+ return {
926
+ config: resolved,
927
+ startRequest: (req) => startRequest(resolved, tracer, req),
928
+ endRequest: (ctx, res, err) => endRequest(resolved, ctx, res, err),
929
+ flush: () => tracerProvider ? tracerProvider.forceFlush() : Promise.resolve(),
930
+ shutdown: () => tracerProvider ? tracerProvider.shutdown() : Promise.resolve()
931
+ };
932
+ }
933
+ function startRequest(config, tracer, req) {
934
+ const startTimeMs = Number.isFinite(req.startTimeMs) ? req.startTimeMs : Date.now();
935
+ req.startTimeMs = startTimeMs;
936
+ const requestId = resolveRequestId(config, req.requestId, req.headers);
937
+ req.requestId = requestId;
938
+ if (req.route && config.route.normalize) {
939
+ req.route = config.route.normalizer ? config.route.normalizer(req.route) : normalizeRoutePattern(req.route);
940
+ }
941
+ const span = tracer ? spanFromTracer(tracer, spanNameFromRequest(req)) : void 0;
942
+ const context = {
943
+ requestId,
944
+ traceId: span?.spanContext().traceId,
945
+ spanId: span?.spanContext().spanId,
946
+ setUserId: (id) => {
947
+ const state2 = getContextState(context);
948
+ if (!state2) {
949
+ return;
950
+ }
951
+ state2.userId = id;
952
+ if (span && id) {
953
+ try {
954
+ setUserIdAttribute(span, id);
955
+ } catch {
956
+ }
957
+ }
958
+ },
959
+ setSessionId: (id) => {
960
+ const state2 = getContextState(context);
961
+ if (!state2) {
962
+ return;
963
+ }
964
+ state2.sessionId = id;
965
+ },
966
+ setAttribute: (key, value) => {
967
+ const state2 = getContextState(context);
968
+ if (!state2) {
969
+ return;
970
+ }
971
+ state2.attributes[key] = value;
972
+ if (span) {
973
+ try {
974
+ span.setAttribute(key, value);
975
+ } catch {
976
+ }
977
+ }
978
+ },
979
+ addEvent: (name, attributes) => {
980
+ const state2 = getContextState(context);
981
+ if (!state2) {
982
+ return;
983
+ }
984
+ state2.events.push({ name, attributes });
985
+ if (span) {
986
+ try {
987
+ span.addEvent(name, attributes);
988
+ } catch {
989
+ }
990
+ }
991
+ },
992
+ setError: (err) => {
993
+ const state2 = getContextState(context);
994
+ if (!state2) {
995
+ return;
996
+ }
997
+ state2.error = err;
998
+ if (span) {
999
+ try {
1000
+ spanStatusFromError(span, err);
1001
+ } catch {
1002
+ }
1003
+ }
1004
+ }
1005
+ };
1006
+ const state = {
1007
+ request: req,
1008
+ config,
1009
+ span,
1010
+ context,
1011
+ attributes: {},
1012
+ events: []
1013
+ };
1014
+ bindContext(context, state);
1015
+ return context;
1016
+ }
1017
+ function endRequest(config, ctx, res, err) {
1018
+ const state = getContextState(ctx);
1019
+ const endTimeMs = Number.isFinite(res.endTimeMs) ? res.endTimeMs : Date.now();
1020
+ res.endTimeMs = endTimeMs;
1021
+ if (!state) {
1022
+ const fallbackLog = {
1023
+ requestId: ctx.requestId,
1024
+ serviceName: config.serviceName,
1025
+ method: res.statusCode ? "UNKNOWN" : "UNKNOWN",
1026
+ url: "",
1027
+ durationMs: 0,
1028
+ statusCode: res.statusCode,
1029
+ timestamp: new Date(endTimeMs).toISOString()
1030
+ };
1031
+ return fallbackLog;
1032
+ }
1033
+ const request = state.request;
1034
+ const capture = resolveCapture(config.capture, state.captureOverride);
1035
+ const redaction = resolveRedaction(config.redaction, state.redactionOverride);
1036
+ const route = request.route;
1037
+ const url = sanitizeLogString(request.url);
1038
+ const log = {
1039
+ requestId: request.requestId ?? ctx.requestId,
1040
+ traceId: state.span?.spanContext().traceId,
1041
+ spanId: state.span?.spanContext().spanId,
1042
+ serviceName: config.serviceName,
1043
+ method: request.method,
1044
+ url,
1045
+ route,
1046
+ statusCode: res.statusCode,
1047
+ durationMs: Math.max(0, endTimeMs - request.startTimeMs),
1048
+ requestHeaders: capture.requestHeaders ? sanitizeHeaderValues(request.headers) : void 0,
1049
+ responseHeaders: capture.responseHeaders ? sanitizeHeaderValues(res.headers) : void 0,
1050
+ requestBody: capture.requestBody === "none" ? void 0 : request.body,
1051
+ responseBody: capture.responseBody === "none" ? void 0 : res.body,
1052
+ userId: state.userId ?? void 0,
1053
+ sessionId: state.sessionId ?? void 0,
1054
+ error: buildError(err ?? state.error),
1055
+ attributes: Object.keys(state.attributes).length > 0 ? { ...state.attributes } : void 0,
1056
+ timestamp: new Date(endTimeMs).toISOString()
1057
+ };
1058
+ const redacted = applyRedaction(redaction, log);
1059
+ const span = state.span;
1060
+ if (span) {
1061
+ try {
1062
+ setRequestAttributes(span, request.method, redacted.url);
1063
+ setRequestIdAttribute(span, redacted.requestId);
1064
+ span.setAttribute("service.name", config.serviceName);
1065
+ if (redacted.statusCode != null) {
1066
+ setResponseStatusAttribute(span, redacted.statusCode);
1067
+ }
1068
+ if (redacted.route) {
1069
+ const normalized = config.route.normalize ? config.route.normalizer ? config.route.normalizer(redacted.route) : normalizeRoutePattern(redacted.route) : redacted.route;
1070
+ setRouteAttribute(span, normalized);
1071
+ span.updateName(`${request.method} ${normalized}`);
1072
+ } else {
1073
+ span.updateName(spanNameFromRequest(request));
1074
+ }
1075
+ if (redacted.requestHeaders) {
1076
+ setHeaderAttributes(span, redacted.requestHeaders, "http.request.header.");
1077
+ }
1078
+ if (redacted.responseHeaders) {
1079
+ setHeaderAttributes(span, redacted.responseHeaders, "http.response.header.");
1080
+ }
1081
+ if (redacted.requestBody) {
1082
+ setRequestBodyAttributes(span, redacted.requestBody);
1083
+ setRequestBodySizeAttribute(span, redacted.requestBody.bytes);
1084
+ }
1085
+ if (redacted.responseBody) {
1086
+ setResponseBodyAttributes(span, redacted.responseBody);
1087
+ setResponseBodySizeAttribute(span, redacted.responseBody.bytes);
1088
+ }
1089
+ if (state.userId) {
1090
+ setUserIdAttribute(span, state.userId);
1091
+ }
1092
+ if (err ?? state.error) {
1093
+ spanStatusFromError(span, err ?? state.error);
1094
+ }
1095
+ span.end();
1096
+ } catch (spanErr) {
1097
+ logWithLevel(config.logger, "warn", config.logLevel, "xray: span finalize failed", {
1098
+ error: spanErr instanceof Error ? spanErr.message : String(spanErr)
1099
+ });
1100
+ }
1101
+ }
1102
+ return redacted;
1103
+ }
1104
+ function resolveRequestId(config, requestId, headers) {
1105
+ if (requestId) {
1106
+ return requestId;
1107
+ }
1108
+ const headerName = config.requestId.header.toLowerCase();
1109
+ const headerValue = headers[headerName];
1110
+ if (headerValue) {
1111
+ const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
1112
+ if (value && value.trim()) {
1113
+ return value.trim();
1114
+ }
1115
+ }
1116
+ if (!config.requestId.generate) {
1117
+ return uuidv7();
1118
+ }
1119
+ return uuidv7();
1120
+ }
1121
+ function resolveCapture(base, override) {
1122
+ if (!override) {
1123
+ return base;
1124
+ }
1125
+ return {
1126
+ ...base,
1127
+ ...override
1128
+ };
1129
+ }
1130
+ function resolveRedaction(base, override) {
1131
+ const merged = {
1132
+ ...base,
1133
+ ...override
1134
+ };
1135
+ merged.headers = normalizeLowercaseList(merged.headers);
1136
+ merged.queryParams = normalizeLowercaseList(merged.queryParams);
1137
+ merged.bodyJsonPaths = normalizeList(merged.bodyJsonPaths);
1138
+ merged.replacement = merged.replacement || base.replacement;
1139
+ return merged;
1140
+ }
1141
+ function normalizeLowercaseList(values) {
1142
+ if (!values) {
1143
+ return [];
1144
+ }
1145
+ return values.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
1146
+ }
1147
+ function normalizeList(values) {
1148
+ if (!values) {
1149
+ return [];
1150
+ }
1151
+ return values.map((entry) => entry.trim()).filter(Boolean);
1152
+ }
1153
+ function buildError(err) {
1154
+ if (!err) {
1155
+ return void 0;
1156
+ }
1157
+ if (err instanceof Error) {
1158
+ return {
1159
+ message: err.message || "Error",
1160
+ type: err.name || "Error",
1161
+ stack: err.stack
1162
+ };
1163
+ }
1164
+ return {
1165
+ message: String(err)
1166
+ };
1167
+ }
1168
+ function spanNameFromRequest(req) {
1169
+ const method = req.method || "GET";
1170
+ if (req.route) {
1171
+ return `${method} ${req.route}`;
1172
+ }
1173
+ const path = safePath(req.url);
1174
+ return `${method} ${path}`;
1175
+ }
1176
+ function safePath(url) {
1177
+ try {
1178
+ const parsed = new URL(url);
1179
+ return parsed.pathname || "/";
1180
+ } catch {
1181
+ const rawPath = url.split("?")[0] || "/";
1182
+ return rawPath || "/";
1183
+ }
1184
+ }
1185
+ // Annotate the CommonJS export names for ESM import in node:
1186
+ 0 && (module.exports = {
1187
+ XrayConfigError,
1188
+ createEmitter,
1189
+ normalizeConfig
1190
+ });
1191
+ //# sourceMappingURL=index.cjs.map