@pingops/core 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,6 +28,35 @@ function createLogger(prefix) {
28
28
  };
29
29
  }
30
30
 
31
+ //#endregion
32
+ //#region src/utils/http-attributes.ts
33
+ /**
34
+ * Returns true when either legacy or modern HTTP method attribute is present.
35
+ */
36
+ function hasHttpMethodAttribute(attributes) {
37
+ return attributes["http.method"] !== void 0 || attributes["http.request.method"] !== void 0;
38
+ }
39
+ /**
40
+ * Returns true when either legacy or modern HTTP URL attribute is present.
41
+ */
42
+ function hasHttpUrlAttribute(attributes) {
43
+ return attributes["http.url"] !== void 0 || attributes["url.full"] !== void 0;
44
+ }
45
+ /**
46
+ * Extracts URL from known HTTP attributes with support for legacy + modern keys.
47
+ *
48
+ * If no explicit URL exists but server.address is available, falls back to a
49
+ * synthetic HTTPS URL for downstream domain filtering.
50
+ */
51
+ function getHttpUrlFromAttributes(attributes) {
52
+ const legacyUrl = attributes["http.url"];
53
+ if (typeof legacyUrl === "string" && legacyUrl.length > 0) return legacyUrl;
54
+ const modernUrl = attributes["url.full"];
55
+ if (typeof modernUrl === "string" && modernUrl.length > 0) return modernUrl;
56
+ const serverAddress = attributes["server.address"];
57
+ if (typeof serverAddress === "string" && serverAddress.length > 0) return `https://${serverAddress}`;
58
+ }
59
+
31
60
  //#endregion
32
61
  //#region src/filtering/span-filter.ts
33
62
  /**
@@ -38,7 +67,10 @@ const log$2 = createLogger("[PingOps SpanFilter]");
38
67
  * Checks if a span is eligible for capture based on span kind and attributes.
39
68
  * A span is eligible if:
40
69
  * 1. span.kind === SpanKind.CLIENT
41
- * 2. AND has HTTP attributes (http.method, http.url, or server.address)
70
+ * 2. AND has HTTP attributes
71
+ * - method: http.method or http.request.method
72
+ * - url: http.url or url.full
73
+ * - host: server.address
42
74
  * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)
43
75
  */
44
76
  function isSpanEligible(span) {
@@ -56,8 +88,8 @@ function isSpanEligible(span) {
56
88
  return false;
57
89
  }
58
90
  const attributes = span.attributes;
59
- const hasHttpMethod = attributes["http.method"] !== void 0;
60
- const hasHttpUrl = attributes["http.url"] !== void 0;
91
+ const hasHttpMethod = hasHttpMethodAttribute(attributes);
92
+ const hasHttpUrl = hasHttpUrlAttribute(attributes);
61
93
  const hasServerAddress = attributes["server.address"] !== void 0;
62
94
  const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;
63
95
  log$2.debug("Span eligibility check result", {
@@ -66,6 +98,10 @@ function isSpanEligible(span) {
66
98
  httpAttributes: {
67
99
  hasMethod: hasHttpMethod,
68
100
  hasUrl: hasHttpUrl,
101
+ hasLegacyMethod: attributes["http.method"] !== void 0,
102
+ hasModernMethod: attributes["http.request.method"] !== void 0,
103
+ hasLegacyUrl: attributes["http.url"] !== void 0,
104
+ hasModernUrl: attributes["url.full"] !== void 0,
69
105
  hasServerAddress
70
106
  }
71
107
  });
@@ -367,6 +403,10 @@ function redactSingleValue(value, config) {
367
403
  * Header filtering logic - applies allow/deny list rules and redaction
368
404
  */
369
405
  const log = createLogger("[PingOps HeaderFilter]");
406
+ function toHeaderString(value) {
407
+ if (typeof value === "string") return value;
408
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
409
+ }
370
410
  /**
371
411
  * Normalizes header name to lowercase for case-insensitive matching
372
412
  */
@@ -528,7 +568,8 @@ function extractHeadersFromAttributes(attributes, headerPrefix) {
528
568
  if (directKeyValueHeaders.length > 0) for (const { key, headerName } of directKeyValueHeaders) {
529
569
  const headerValue = attributes[key];
530
570
  if (headerValue !== void 0 && headerValue !== null) {
531
- const stringValue = typeof headerValue === "string" ? headerValue : String(headerValue);
571
+ const stringValue = toHeaderString(headerValue);
572
+ if (stringValue === void 0) continue;
532
573
  const normalizedName = headerName.toLowerCase();
533
574
  const existingKey = Object.keys(headerMap).find((k) => k.toLowerCase() === normalizedName);
534
575
  if (existingKey) {
@@ -552,6 +593,13 @@ function normalizeHeaders(headers) {
552
593
  const result = {};
553
594
  if (!headers) return result;
554
595
  try {
596
+ if (Array.isArray(headers)) {
597
+ for (let i = 0; i < headers.length; i += 2) if (i + 1 < headers.length) {
598
+ const key = String(headers[i]);
599
+ result[key] = headers[i + 1];
600
+ }
601
+ return result;
602
+ }
555
603
  if (isHeadersLike(headers)) {
556
604
  for (const [key, value] of headers.entries()) if (result[key]) {
557
605
  const existing = result[key];
@@ -563,17 +611,57 @@ function normalizeHeaders(headers) {
563
611
  for (const [key, value] of Object.entries(headers)) if (!/^\d+$/.test(key)) result[key] = value;
564
612
  return result;
565
613
  }
566
- if (Array.isArray(headers)) {
567
- for (let i = 0; i < headers.length; i += 2) if (i + 1 < headers.length) {
568
- const key = String(headers[i]);
569
- result[key] = headers[i + 1];
570
- }
571
- return result;
572
- }
573
614
  } catch {}
574
615
  return result;
575
616
  }
576
617
 
618
+ //#endregion
619
+ //#region src/filtering/body-decoder.ts
620
+ /**
621
+ * Minimal body handling: buffer to string for span attributes.
622
+ * No decompression or truncation; for compressed responses the instrumentation
623
+ * sends base64 + content-encoding so the backend can decompress.
624
+ */
625
+ /** Span attribute for response content-encoding when body is sent as base64. */
626
+ const HTTP_RESPONSE_CONTENT_ENCODING = "http.response.content_encoding";
627
+ const COMPRESSED_ENCODINGS = new Set([
628
+ "gzip",
629
+ "br",
630
+ "deflate",
631
+ "x-gzip",
632
+ "x-deflate"
633
+ ]);
634
+ function safeStringify(value) {
635
+ if (typeof value === "string") return value;
636
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
637
+ }
638
+ function normalizeHeaderValue(v) {
639
+ if (v == null) return void 0;
640
+ if (Array.isArray(v)) return v.map((item) => safeStringify(item)).filter((item) => item !== void 0).join(", ").trim() || void 0;
641
+ const s = safeStringify(v);
642
+ if (!s) return void 0;
643
+ return s.trim() || void 0;
644
+ }
645
+ /**
646
+ * Returns true if the content-encoding header indicates a compressed body
647
+ * (gzip, br, deflate, x-gzip, x-deflate). Used to decide whether to send
648
+ * body as base64 + content-encoding for backend decompression.
649
+ */
650
+ function isCompressedContentEncoding(headerValue) {
651
+ const raw = normalizeHeaderValue(headerValue);
652
+ if (!raw) return false;
653
+ const first = raw.split(",")[0].trim().toLowerCase();
654
+ return COMPRESSED_ENCODINGS.has(first);
655
+ }
656
+ /**
657
+ * Converts a buffer to a UTF-8 string for use as request/response body on spans.
658
+ * Returns null for null, undefined, or empty buffer.
659
+ */
660
+ function bufferToBodyString(buffer) {
661
+ if (buffer == null || buffer.length === 0) return null;
662
+ return buffer.toString("utf8");
663
+ }
664
+
577
665
  //#endregion
578
666
  //#region src/utils/span-extractor.ts
579
667
  /**
@@ -612,7 +700,7 @@ function shouldCaptureBody(domainRule, globalConfig, bodyType) {
612
700
  */
613
701
  function extractSpanPayload(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction) {
614
702
  const attributes = span.attributes;
615
- const url = attributes["http.url"] || attributes["url.full"];
703
+ const url = getHttpUrlFromAttributes(attributes);
616
704
  const domainRule = url ? getDomainRule(url, domainAllowList) : void 0;
617
705
  const headersAllowList = domainRule?.headersAllowList ?? globalHeadersAllowList;
618
706
  const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;
@@ -662,11 +750,10 @@ function extractSpanPayload(span, domainAllowList, globalHeadersAllowList, globa
662
750
  * OpenTelemetry context keys for PingOps
663
751
  */
664
752
  /**
665
- * Context key for enabling HTTP instrumentation.
666
- * When set to true, HTTP requests will be automatically instrumented.
667
- * This allows wrapHttp to control which HTTP calls are captured.
753
+ * Context key for trace ID attribute.
754
+ * Used to propagate trace identifier to all spans in the context.
668
755
  */
669
- const PINGOPS_HTTP_ENABLED = (0, _opentelemetry_api.createContextKey)("pingops-http-enabled");
756
+ const PINGOPS_TRACE_ID = (0, _opentelemetry_api.createContextKey)("pingops-trace-id");
670
757
  /**
671
758
  * Context key for user ID attribute.
672
759
  * Used to propagate user identifier to all spans in the context.
@@ -690,13 +777,11 @@ const PINGOPS_METADATA = (0, _opentelemetry_api.createContextKey)("pingops-metad
690
777
  /**
691
778
  * Context key for capturing request body.
692
779
  * When set, controls whether request bodies should be captured for HTTP spans.
693
- * This allows wrapHttp to control body capture per-request.
694
780
  */
695
781
  const PINGOPS_CAPTURE_REQUEST_BODY = (0, _opentelemetry_api.createContextKey)("pingops-capture-request-body");
696
782
  /**
697
783
  * Context key for capturing response body.
698
784
  * When set, controls whether response bodies should be captured for HTTP spans.
699
- * This allows wrapHttp to control body capture per-request.
700
785
  */
701
786
  const PINGOPS_CAPTURE_RESPONSE_BODY = (0, _opentelemetry_api.createContextKey)("pingops-capture-response-body");
702
787
 
@@ -711,6 +796,8 @@ const PINGOPS_CAPTURE_RESPONSE_BODY = (0, _opentelemetry_api.createContextKey)("
711
796
  */
712
797
  function getPropagatedAttributesFromContext(parentContext) {
713
798
  const attributes = {};
799
+ const traceId = parentContext.getValue(PINGOPS_TRACE_ID);
800
+ if (traceId !== void 0 && typeof traceId === "string") attributes["pingops.trace_id"] = traceId;
714
801
  const userId = parentContext.getValue(PINGOPS_USER_ID);
715
802
  if (userId !== void 0 && typeof userId === "string") attributes["pingops.user_id"] = userId;
716
803
  const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);
@@ -725,120 +812,57 @@ function getPropagatedAttributesFromContext(parentContext) {
725
812
  }
726
813
 
727
814
  //#endregion
728
- //#region src/wrap-http.ts
815
+ //#region src/trace-id.ts
729
816
  /**
730
- * wrapHttp - Wraps a function to set attributes on HTTP spans created within the wrapped block.
731
- *
732
- * This function sets attributes (userId, sessionId, tags, metadata) in the OpenTelemetry
733
- * context, which are automatically propagated to all spans created within the wrapped function.
734
- *
735
- * Instrumentation behavior:
736
- * - If `initializePingops` was called: All HTTP requests are instrumented by default.
737
- * `wrapHttp` only adds attributes to spans created within the wrapped block.
738
- * - If `initializePingops` was NOT called: Only HTTP requests within `wrapHttp` blocks
739
- * are instrumented. Requests outside `wrapHttp` are not instrumented.
817
+ * Deterministic and random trace ID generation for PingOps
740
818
  */
741
- const logger = createLogger("[PingOps wrapHttp]");
742
819
  /**
743
- * Wraps a function to set attributes on HTTP spans created within the wrapped block.
744
- *
745
- * This function sets attributes (userId, sessionId, tags, metadata) in the OpenTelemetry
746
- * context, which are automatically propagated to all spans created within the wrapped function.
747
- *
748
- * Instrumentation behavior:
749
- * - If `initializePingops` was called: All HTTP requests are instrumented by default.
750
- * `wrapHttp` only adds attributes to spans created within the wrapped block.
751
- * - If `initializePingops` was NOT called: Only HTTP requests within `wrapHttp` blocks
752
- * are instrumented. Requests outside `wrapHttp` are not instrumented.
753
- *
754
- * Note: This is the low-level API. For a simpler API with automatic setup,
755
- * use `wrapHttp` from `@pingops/sdk` instead.
756
- *
757
- * @param options - Options including attributes and required callbacks
758
- * @param fn - Function to execute within the attribute context
759
- * @returns The result of the function
760
- */
761
- function wrapHttp(options, fn) {
762
- logger.debug("wrapHttp called", {
763
- hasAttributes: !!options.attributes,
764
- hasUserId: !!options.attributes?.userId,
765
- hasSessionId: !!options.attributes?.sessionId,
766
- hasTags: !!options.attributes?.tags,
767
- hasMetadata: !!options.attributes?.metadata
768
- });
769
- const normalizedOptions = "checkInitialized" in options && "isGlobalInstrumentationEnabled" in options ? options : (() => {
770
- throw new Error("wrapHttp requires checkInitialized and isGlobalInstrumentationEnabled callbacks. Use wrapHttp from @pingops/sdk for automatic setup.");
771
- })();
772
- const { checkInitialized, ensureInitialized } = normalizedOptions;
773
- if (checkInitialized()) {
774
- logger.debug("SDK already initialized, executing wrapHttp synchronously");
775
- return executeWrapHttpWithContext(normalizedOptions, fn);
776
- }
777
- if (ensureInitialized) {
778
- logger.debug("SDK not initialized, using provided ensureInitialized callback");
779
- return ensureInitialized().then(() => {
780
- logger.debug("SDK initialized, executing wrapHttp");
781
- return executeWrapHttpWithContext(normalizedOptions, fn);
782
- }).catch((error) => {
783
- logger.error("Failed to initialize SDK for wrapHttp", { error: error instanceof Error ? error.message : String(error) });
784
- throw error;
785
- });
786
- }
787
- logger.debug("SDK not initialized and no ensureInitialized callback provided, executing wrapHttp");
788
- return executeWrapHttpWithContext(normalizedOptions, fn);
820
+ * Converts a Uint8Array to a lowercase hex string.
821
+ */
822
+ function uint8ArrayToHex(bytes) {
823
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
789
824
  }
790
- function executeWrapHttpWithContext(options, fn) {
791
- const { attributes, isGlobalInstrumentationEnabled } = options;
792
- const globalInstrumentationEnabled = isGlobalInstrumentationEnabled();
793
- logger.debug("Executing wrapHttp context", {
794
- hasAttributes: !!attributes,
795
- globalInstrumentationEnabled
796
- });
797
- let contextWithAttributes = _opentelemetry_api.context.active();
798
- if (!globalInstrumentationEnabled) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_HTTP_ENABLED, true);
799
- if (attributes) {
800
- if (attributes.userId !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_USER_ID, attributes.userId);
801
- if (attributes.sessionId !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_SESSION_ID, attributes.sessionId);
802
- if (attributes.tags !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_TAGS, attributes.tags);
803
- if (attributes.metadata !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_METADATA, attributes.metadata);
804
- if (attributes.captureRequestBody !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_CAPTURE_REQUEST_BODY, attributes.captureRequestBody);
805
- if (attributes.captureResponseBody !== void 0) contextWithAttributes = contextWithAttributes.setValue(PINGOPS_CAPTURE_RESPONSE_BODY, attributes.captureResponseBody);
825
+ /**
826
+ * Creates a trace ID (32 hex chars).
827
+ * - If `seed` is provided: deterministic via SHA-256 of the seed (first 32 hex chars).
828
+ * - Otherwise: random 16 bytes as 32 hex chars.
829
+ */
830
+ async function createTraceId(seed) {
831
+ if (seed) {
832
+ const data = new TextEncoder().encode(seed);
833
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
834
+ return uint8ArrayToHex(new Uint8Array(hashBuffer)).slice(0, 32);
806
835
  }
807
- return _opentelemetry_api.context.with(contextWithAttributes, () => {
808
- try {
809
- const result = fn();
810
- if (result instanceof Promise) return result.catch((err) => {
811
- logger.error("Error in wrapHttp async execution", { error: err instanceof Error ? err.message : String(err) });
812
- throw err;
813
- });
814
- return result;
815
- } catch (err) {
816
- logger.error("Error in wrapHttp sync execution", { error: err instanceof Error ? err.message : String(err) });
817
- throw err;
818
- }
819
- });
836
+ return uint8ArrayToHex(crypto.getRandomValues(new Uint8Array(16)));
820
837
  }
821
838
 
822
839
  //#endregion
823
840
  exports.DEFAULT_REDACTION_CONFIG = DEFAULT_REDACTION_CONFIG;
824
841
  exports.DEFAULT_SENSITIVE_HEADER_PATTERNS = DEFAULT_SENSITIVE_HEADER_PATTERNS;
842
+ exports.HTTP_RESPONSE_CONTENT_ENCODING = HTTP_RESPONSE_CONTENT_ENCODING;
825
843
  exports.HeaderRedactionStrategy = HeaderRedactionStrategy;
826
844
  exports.PINGOPS_CAPTURE_REQUEST_BODY = PINGOPS_CAPTURE_REQUEST_BODY;
827
845
  exports.PINGOPS_CAPTURE_RESPONSE_BODY = PINGOPS_CAPTURE_RESPONSE_BODY;
828
- exports.PINGOPS_HTTP_ENABLED = PINGOPS_HTTP_ENABLED;
829
846
  exports.PINGOPS_METADATA = PINGOPS_METADATA;
830
847
  exports.PINGOPS_SESSION_ID = PINGOPS_SESSION_ID;
831
848
  exports.PINGOPS_TAGS = PINGOPS_TAGS;
849
+ exports.PINGOPS_TRACE_ID = PINGOPS_TRACE_ID;
832
850
  exports.PINGOPS_USER_ID = PINGOPS_USER_ID;
851
+ exports.bufferToBodyString = bufferToBodyString;
833
852
  exports.createLogger = createLogger;
853
+ exports.createTraceId = createTraceId;
834
854
  exports.extractHeadersFromAttributes = extractHeadersFromAttributes;
835
855
  exports.extractSpanPayload = extractSpanPayload;
836
856
  exports.filterHeaders = filterHeaders;
857
+ exports.getHttpUrlFromAttributes = getHttpUrlFromAttributes;
837
858
  exports.getPropagatedAttributesFromContext = getPropagatedAttributesFromContext;
859
+ exports.hasHttpMethodAttribute = hasHttpMethodAttribute;
860
+ exports.hasHttpUrlAttribute = hasHttpUrlAttribute;
861
+ exports.isCompressedContentEncoding = isCompressedContentEncoding;
838
862
  exports.isSensitiveHeader = isSensitiveHeader;
839
863
  exports.isSpanEligible = isSpanEligible;
840
864
  exports.normalizeHeaders = normalizeHeaders;
841
865
  exports.redactHeaderValue = redactHeaderValue;
842
866
  exports.shouldCaptureSpan = shouldCaptureSpan;
843
- exports.wrapHttp = wrapHttp;
867
+ exports.uint8ArrayToHex = uint8ArrayToHex;
844
868
  //# sourceMappingURL=index.cjs.map