@pingops/core 0.1.3 → 0.2.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/dist/index.cjs +68 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -47
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +37 -47
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +64 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -574,6 +574,46 @@ function normalizeHeaders(headers) {
|
|
|
574
574
|
return result;
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/filtering/body-decoder.ts
|
|
579
|
+
/**
|
|
580
|
+
* Minimal body handling: buffer to string for span attributes.
|
|
581
|
+
* No decompression or truncation; for compressed responses the instrumentation
|
|
582
|
+
* sends base64 + content-encoding so the backend can decompress.
|
|
583
|
+
*/
|
|
584
|
+
/** Span attribute for response content-encoding when body is sent as base64. */
|
|
585
|
+
const HTTP_RESPONSE_CONTENT_ENCODING = "http.response.content_encoding";
|
|
586
|
+
const COMPRESSED_ENCODINGS = new Set([
|
|
587
|
+
"gzip",
|
|
588
|
+
"br",
|
|
589
|
+
"deflate",
|
|
590
|
+
"x-gzip",
|
|
591
|
+
"x-deflate"
|
|
592
|
+
]);
|
|
593
|
+
function normalizeHeaderValue(v) {
|
|
594
|
+
if (v == null) return void 0;
|
|
595
|
+
return (Array.isArray(v) ? v.map(String).join(", ") : String(v)).trim() || void 0;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Returns true if the content-encoding header indicates a compressed body
|
|
599
|
+
* (gzip, br, deflate, x-gzip, x-deflate). Used to decide whether to send
|
|
600
|
+
* body as base64 + content-encoding for backend decompression.
|
|
601
|
+
*/
|
|
602
|
+
function isCompressedContentEncoding(headerValue) {
|
|
603
|
+
const raw = normalizeHeaderValue(headerValue);
|
|
604
|
+
if (!raw) return false;
|
|
605
|
+
const first = raw.split(",")[0].trim().toLowerCase();
|
|
606
|
+
return COMPRESSED_ENCODINGS.has(first);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Converts a buffer to a UTF-8 string for use as request/response body on spans.
|
|
610
|
+
* Returns null for null, undefined, or empty buffer.
|
|
611
|
+
*/
|
|
612
|
+
function bufferToBodyString(buffer) {
|
|
613
|
+
if (buffer == null || buffer.length === 0) return null;
|
|
614
|
+
return buffer.toString("utf8");
|
|
615
|
+
}
|
|
616
|
+
|
|
577
617
|
//#endregion
|
|
578
618
|
//#region src/utils/span-extractor.ts
|
|
579
619
|
/**
|
|
@@ -662,11 +702,10 @@ function extractSpanPayload(span, domainAllowList, globalHeadersAllowList, globa
|
|
|
662
702
|
* OpenTelemetry context keys for PingOps
|
|
663
703
|
*/
|
|
664
704
|
/**
|
|
665
|
-
* Context key for
|
|
666
|
-
*
|
|
667
|
-
* This allows wrapHttp to control which HTTP calls are captured.
|
|
705
|
+
* Context key for trace ID attribute.
|
|
706
|
+
* Used to propagate trace identifier to all spans in the context.
|
|
668
707
|
*/
|
|
669
|
-
const
|
|
708
|
+
const PINGOPS_TRACE_ID = (0, _opentelemetry_api.createContextKey)("pingops-trace-id");
|
|
670
709
|
/**
|
|
671
710
|
* Context key for user ID attribute.
|
|
672
711
|
* Used to propagate user identifier to all spans in the context.
|
|
@@ -690,13 +729,11 @@ const PINGOPS_METADATA = (0, _opentelemetry_api.createContextKey)("pingops-metad
|
|
|
690
729
|
/**
|
|
691
730
|
* Context key for capturing request body.
|
|
692
731
|
* When set, controls whether request bodies should be captured for HTTP spans.
|
|
693
|
-
* This allows wrapHttp to control body capture per-request.
|
|
694
732
|
*/
|
|
695
733
|
const PINGOPS_CAPTURE_REQUEST_BODY = (0, _opentelemetry_api.createContextKey)("pingops-capture-request-body");
|
|
696
734
|
/**
|
|
697
735
|
* Context key for capturing response body.
|
|
698
736
|
* When set, controls whether response bodies should be captured for HTTP spans.
|
|
699
|
-
* This allows wrapHttp to control body capture per-request.
|
|
700
737
|
*/
|
|
701
738
|
const PINGOPS_CAPTURE_RESPONSE_BODY = (0, _opentelemetry_api.createContextKey)("pingops-capture-response-body");
|
|
702
739
|
|
|
@@ -711,6 +748,8 @@ const PINGOPS_CAPTURE_RESPONSE_BODY = (0, _opentelemetry_api.createContextKey)("
|
|
|
711
748
|
*/
|
|
712
749
|
function getPropagatedAttributesFromContext(parentContext) {
|
|
713
750
|
const attributes = {};
|
|
751
|
+
const traceId = parentContext.getValue(PINGOPS_TRACE_ID);
|
|
752
|
+
if (traceId !== void 0 && typeof traceId === "string") attributes["pingops.trace_id"] = traceId;
|
|
714
753
|
const userId = parentContext.getValue(PINGOPS_USER_ID);
|
|
715
754
|
if (userId !== void 0 && typeof userId === "string") attributes["pingops.user_id"] = userId;
|
|
716
755
|
const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);
|
|
@@ -725,120 +764,54 @@ function getPropagatedAttributesFromContext(parentContext) {
|
|
|
725
764
|
}
|
|
726
765
|
|
|
727
766
|
//#endregion
|
|
728
|
-
//#region src/
|
|
767
|
+
//#region src/trace-id.ts
|
|
729
768
|
/**
|
|
730
|
-
*
|
|
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.
|
|
769
|
+
* Deterministic and random trace ID generation for PingOps
|
|
740
770
|
*/
|
|
741
|
-
const logger = createLogger("[PingOps wrapHttp]");
|
|
742
771
|
/**
|
|
743
|
-
*
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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);
|
|
772
|
+
* Converts a Uint8Array to a lowercase hex string.
|
|
773
|
+
*/
|
|
774
|
+
function uint8ArrayToHex(bytes) {
|
|
775
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
789
776
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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);
|
|
777
|
+
/**
|
|
778
|
+
* Creates a trace ID (32 hex chars).
|
|
779
|
+
* - If `seed` is provided: deterministic via SHA-256 of the seed (first 32 hex chars).
|
|
780
|
+
* - Otherwise: random 16 bytes as 32 hex chars.
|
|
781
|
+
*/
|
|
782
|
+
async function createTraceId(seed) {
|
|
783
|
+
if (seed) {
|
|
784
|
+
const data = new TextEncoder().encode(seed);
|
|
785
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
786
|
+
return uint8ArrayToHex(new Uint8Array(hashBuffer)).slice(0, 32);
|
|
806
787
|
}
|
|
807
|
-
return
|
|
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
|
-
});
|
|
788
|
+
return uint8ArrayToHex(crypto.getRandomValues(new Uint8Array(16)));
|
|
820
789
|
}
|
|
821
790
|
|
|
822
791
|
//#endregion
|
|
823
792
|
exports.DEFAULT_REDACTION_CONFIG = DEFAULT_REDACTION_CONFIG;
|
|
824
793
|
exports.DEFAULT_SENSITIVE_HEADER_PATTERNS = DEFAULT_SENSITIVE_HEADER_PATTERNS;
|
|
794
|
+
exports.HTTP_RESPONSE_CONTENT_ENCODING = HTTP_RESPONSE_CONTENT_ENCODING;
|
|
825
795
|
exports.HeaderRedactionStrategy = HeaderRedactionStrategy;
|
|
826
796
|
exports.PINGOPS_CAPTURE_REQUEST_BODY = PINGOPS_CAPTURE_REQUEST_BODY;
|
|
827
797
|
exports.PINGOPS_CAPTURE_RESPONSE_BODY = PINGOPS_CAPTURE_RESPONSE_BODY;
|
|
828
|
-
exports.PINGOPS_HTTP_ENABLED = PINGOPS_HTTP_ENABLED;
|
|
829
798
|
exports.PINGOPS_METADATA = PINGOPS_METADATA;
|
|
830
799
|
exports.PINGOPS_SESSION_ID = PINGOPS_SESSION_ID;
|
|
831
800
|
exports.PINGOPS_TAGS = PINGOPS_TAGS;
|
|
801
|
+
exports.PINGOPS_TRACE_ID = PINGOPS_TRACE_ID;
|
|
832
802
|
exports.PINGOPS_USER_ID = PINGOPS_USER_ID;
|
|
803
|
+
exports.bufferToBodyString = bufferToBodyString;
|
|
833
804
|
exports.createLogger = createLogger;
|
|
805
|
+
exports.createTraceId = createTraceId;
|
|
834
806
|
exports.extractHeadersFromAttributes = extractHeadersFromAttributes;
|
|
835
807
|
exports.extractSpanPayload = extractSpanPayload;
|
|
836
808
|
exports.filterHeaders = filterHeaders;
|
|
837
809
|
exports.getPropagatedAttributesFromContext = getPropagatedAttributesFromContext;
|
|
810
|
+
exports.isCompressedContentEncoding = isCompressedContentEncoding;
|
|
838
811
|
exports.isSensitiveHeader = isSensitiveHeader;
|
|
839
812
|
exports.isSpanEligible = isSpanEligible;
|
|
840
813
|
exports.normalizeHeaders = normalizeHeaders;
|
|
841
814
|
exports.redactHeaderValue = redactHeaderValue;
|
|
842
815
|
exports.shouldCaptureSpan = shouldCaptureSpan;
|
|
843
|
-
exports.
|
|
816
|
+
exports.uint8ArrayToHex = uint8ArrayToHex;
|
|
844
817
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["log","SpanKind","log","context"],"sources":["../src/logger.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/utils/span-extractor.ts","../src/context-keys.ts","../src/utils/context-extractor.ts","../src/wrap-http.ts"],"sourcesContent":["/**\n * Global logger utility for PingOps Core\n *\n * Provides consistent logging across all core components with support for\n * different log levels and debug mode control via PINGOPS_DEBUG environment variable.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\n/**\n * Creates a logger instance with a specific prefix\n *\n * @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')\n * @returns Logger instance\n */\nexport function createLogger(prefix: string): Logger {\n const isDebugEnabled = process.env.PINGOPS_DEBUG === \"true\";\n\n const formatMessage = (level: LogLevel, message: string): string => {\n const timestamp = new Date().toISOString();\n return `[${timestamp}] ${prefix} [${level.toUpperCase()}] ${message}`;\n };\n\n return {\n debug(message: string, ...args: unknown[]): void {\n if (isDebugEnabled) {\n console.debug(formatMessage(\"debug\", message), ...args);\n }\n },\n info(message: string, ...args: unknown[]): void {\n console.log(formatMessage(\"info\", message), ...args);\n },\n warn(message: string, ...args: unknown[]): void {\n console.warn(formatMessage(\"warn\", message), ...args);\n },\n error(message: string, ...args: unknown[]): void {\n console.error(formatMessage(\"error\", message), ...args);\n },\n };\n}\n","/**\n * Span filtering logic - determines if a span is eligible for capture\n */\n\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps SpanFilter]\");\n\n/**\n * Checks if a span is eligible for capture based on span kind and attributes.\n * A span is eligible if:\n * 1. span.kind === SpanKind.CLIENT\n * 2. AND has HTTP attributes (http.method, http.url, or server.address)\n * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)\n */\nexport function isSpanEligible(span: ReadableSpan): boolean {\n log.debug(\"Checking span eligibility\", {\n spanName: span.name,\n spanKind: span.kind,\n spanId: span.spanContext().spanId,\n traceId: span.spanContext().traceId,\n });\n\n // Must be a CLIENT span (outgoing request)\n if (span.kind !== SpanKind.CLIENT) {\n log.debug(\"Span not eligible: not CLIENT kind\", {\n spanName: span.name,\n spanKind: span.kind,\n });\n return false;\n }\n\n const attributes = span.attributes;\n\n // Check for HTTP attributes\n const hasHttpMethod = attributes[\"http.method\"] !== undefined;\n const hasHttpUrl = attributes[\"http.url\"] !== undefined;\n const hasServerAddress = attributes[\"server.address\"] !== undefined;\n\n const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;\n\n log.debug(\"Span eligibility check result\", {\n spanName: span.name,\n isEligible,\n httpAttributes: {\n hasMethod: hasHttpMethod,\n hasUrl: hasHttpUrl,\n hasServerAddress,\n },\n });\n\n return isEligible;\n}\n","/**\n * Domain filtering logic - applies allow/deny list rules\n */\n\nimport type { DomainRule } from \"../types\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps DomainFilter]\");\n\n/**\n * Extracts domain from a URL\n */\nfunction extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n const domain = urlObj.hostname;\n log.debug(\"Extracted domain from URL\", { url, domain });\n return domain;\n } catch {\n // If URL parsing fails, try to extract domain from string\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n const domain = match ? match[1] : \"\";\n log.debug(\"Extracted domain from URL (fallback)\", { url, domain });\n return domain;\n }\n}\n\n/**\n * Checks if a domain matches a rule (exact or suffix match)\n */\nfunction domainMatches(domain: string, ruleDomain: string): boolean {\n // Exact match\n if (domain === ruleDomain) {\n log.debug(\"Domain exact match\", { domain, ruleDomain });\n return true;\n }\n\n // Suffix match (e.g., .github.com matches api.github.com)\n if (ruleDomain.startsWith(\".\")) {\n const matches =\n domain.endsWith(ruleDomain) || domain === ruleDomain.slice(1);\n log.debug(\"Domain suffix match check\", { domain, ruleDomain, matches });\n return matches;\n }\n\n log.debug(\"Domain does not match\", { domain, ruleDomain });\n return false;\n}\n\n/**\n * Checks if a path matches any of the allowed paths (prefix match)\n */\nfunction pathMatches(path: string, allowedPaths?: string[]): boolean {\n if (!allowedPaths || allowedPaths.length === 0) {\n log.debug(\"No path restrictions, all paths match\", { path });\n return true; // No path restrictions means all paths match\n }\n\n const matches = allowedPaths.some((allowedPath) =>\n path.startsWith(allowedPath)\n );\n log.debug(\"Path match check\", { path, allowedPaths, matches });\n return matches;\n}\n\n/**\n * Determines if a span should be captured based on domain rules\n */\nexport function shouldCaptureSpan(\n url: string,\n domainAllowList?: DomainRule[],\n domainDenyList?: DomainRule[]\n): boolean {\n log.debug(\"Checking domain filter rules\", {\n url,\n hasAllowList: !!domainAllowList && domainAllowList.length > 0,\n hasDenyList: !!domainDenyList && domainDenyList.length > 0,\n allowListCount: domainAllowList?.length || 0,\n denyListCount: domainDenyList?.length || 0,\n });\n\n const domain = extractDomain(url);\n\n // Extract path from URL\n let path = \"/\";\n try {\n const urlObj = new URL(url);\n path = urlObj.pathname;\n } catch {\n // If URL parsing fails, try to extract path from string\n const pathMatch = url.match(/^(?:https?:\\/\\/)?[^/]+(\\/.*)?$/);\n path = pathMatch && pathMatch[1] ? pathMatch[1] : \"/\";\n }\n\n log.debug(\"Extracted domain and path\", { url, domain, path });\n\n // Deny list is evaluated first - if domain is denied, don't capture\n if (domainDenyList) {\n for (const rule of domainDenyList) {\n if (domainMatches(domain, rule.domain)) {\n log.info(\"Domain denied by deny list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return false;\n }\n }\n log.debug(\"Domain passed deny list check\", { domain });\n }\n\n // If no allow list, capture all (except denied)\n if (!domainAllowList || domainAllowList.length === 0) {\n log.debug(\"No allow list configured, capturing span\", { domain, url });\n return true;\n }\n\n // Check if domain matches any allow list rule\n for (const rule of domainAllowList) {\n if (domainMatches(domain, rule.domain)) {\n // If paths are specified, check path match\n if (rule.paths && rule.paths.length > 0) {\n const pathMatch = pathMatches(path, rule.paths);\n if (pathMatch) {\n log.info(\"Domain and path allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n url,\n });\n return true;\n } else {\n log.debug(\"Domain allowed but path not matched\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n });\n }\n } else {\n log.info(\"Domain allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return true;\n }\n }\n }\n\n // Domain not in allow list\n log.info(\"Domain not in allow list, filtering out\", { domain, url });\n return false;\n}\n","/**\n * Sensitive header patterns and redaction configuration\n */\n\n/**\n * Default patterns for sensitive headers that should be redacted\n * These are matched case-insensitively\n */\nexport const DEFAULT_SENSITIVE_HEADER_PATTERNS = [\n // Authentication & Authorization\n \"authorization\",\n \"www-authenticate\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"x-auth-token\",\n \"x-api-key\",\n \"x-api-token\",\n \"x-access-token\",\n \"x-auth-user\",\n \"x-auth-password\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n\n // API Keys & Access Tokens\n \"api-key\",\n \"apikey\",\n \"api_key\",\n \"access-key\",\n \"accesskey\",\n \"access_key\",\n \"secret-key\",\n \"secretkey\",\n \"secret_key\",\n \"private-key\",\n \"privatekey\",\n \"private_key\",\n\n // Session & Cookie tokens\n \"cookie\",\n \"set-cookie\",\n \"session-id\",\n \"sessionid\",\n \"session_id\",\n \"session-token\",\n \"sessiontoken\",\n \"session_token\",\n\n // OAuth & OAuth2\n \"oauth-token\",\n \"oauth_token\",\n \"oauth2-token\",\n \"oauth2_token\",\n \"bearer\",\n\n // AWS & Cloud credentials\n \"x-amz-security-token\",\n \"x-amz-signature\",\n \"x-aws-access-key\",\n \"x-aws-secret-key\",\n \"x-aws-session-token\",\n\n // Other common sensitive headers\n \"x-password\",\n \"x-secret\",\n \"x-token\",\n \"x-jwt\",\n \"x-jwt-token\",\n \"x-refresh-token\",\n \"x-client-secret\",\n \"x-client-id\",\n \"x-user-token\",\n \"x-service-key\",\n] as const;\n\n/**\n * Redaction strategies for sensitive header values\n */\nexport enum HeaderRedactionStrategy {\n /**\n * Replace the entire value with a fixed redaction string\n */\n REPLACE = \"replace\",\n /**\n * Show only the first N characters, redact the rest\n */\n PARTIAL = \"partial\",\n /**\n * Show only the last N characters, redact the rest\n */\n PARTIAL_END = \"partial_end\",\n /**\n * Remove the header entirely (same as deny list)\n */\n REMOVE = \"remove\",\n}\n\n/**\n * Configuration for header redaction\n */\nexport interface HeaderRedactionConfig {\n /**\n * Patterns to match sensitive headers (case-insensitive)\n * Defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS if not provided\n */\n sensitivePatterns?: readonly string[];\n\n /**\n * Redaction strategy to use\n * @default HeaderRedactionStrategy.REPLACE\n */\n strategy?: HeaderRedactionStrategy;\n\n /**\n * Redaction string used when strategy is REPLACE\n * @default \"[REDACTED]\"\n */\n redactionString?: string;\n\n /**\n * Number of characters to show when strategy is PARTIAL or PARTIAL_END\n * @default 4\n */\n visibleChars?: number;\n\n /**\n * Whether to enable redaction\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Default redaction configuration\n */\nexport const DEFAULT_REDACTION_CONFIG: Required<HeaderRedactionConfig> = {\n sensitivePatterns: DEFAULT_SENSITIVE_HEADER_PATTERNS,\n strategy: HeaderRedactionStrategy.REPLACE,\n redactionString: \"[REDACTED]\",\n visibleChars: 4,\n enabled: true,\n};\n\n/**\n * Checks if a header name matches any sensitive pattern\n * Uses case-insensitive matching with exact match, prefix/suffix, and substring matching\n *\n * @param headerName - The header name to check\n * @param patterns - Array of patterns to match against (defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS)\n * @returns true if the header matches any sensitive pattern\n */\nexport function isSensitiveHeader(\n headerName: string,\n patterns: readonly string[] = DEFAULT_SENSITIVE_HEADER_PATTERNS\n): boolean {\n if (!headerName || typeof headerName !== \"string\") {\n return false;\n }\n\n if (!patterns || patterns.length === 0) {\n return false;\n }\n\n const normalizedName = headerName.toLowerCase().trim();\n\n // Early return for empty string\n if (normalizedName.length === 0) {\n return false;\n }\n\n return patterns.some((pattern) => {\n if (!pattern || typeof pattern !== \"string\") {\n return false;\n }\n\n const normalizedPattern = pattern.toLowerCase().trim();\n\n // Empty pattern doesn't match\n if (normalizedPattern.length === 0) {\n return false;\n }\n\n // Exact match (most common case, check first)\n if (normalizedName === normalizedPattern) {\n return true;\n }\n\n // Check if header name contains the pattern (e.g., \"x-api-key\" contains \"api-key\")\n // This handles cases where patterns are embedded in header names\n if (normalizedName.includes(normalizedPattern)) {\n return true;\n }\n\n // Check if pattern contains the header name (for shorter patterns matching longer headers)\n // This is less common but handles edge cases\n if (normalizedPattern.includes(normalizedName)) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Redacts a header value based on the configuration\n */\nexport function redactHeaderValue(\n value: string | string[] | undefined,\n config: Required<HeaderRedactionConfig>\n): string | string[] | undefined {\n if (value === undefined || value === null) {\n return value;\n }\n\n // Handle array of values\n if (Array.isArray(value)) {\n return value.map((v) => redactSingleValue(v, config));\n }\n\n return redactSingleValue(value, config);\n}\n\n/**\n * Redacts a single string value based on the configured strategy\n *\n * @param value - The value to redact\n * @param config - Redaction configuration\n * @returns Redacted value\n */\nfunction redactSingleValue(\n value: string,\n config: Required<HeaderRedactionConfig>\n): string {\n // Validate input\n if (!value || typeof value !== \"string\") {\n return value;\n }\n\n // Ensure visibleChars is a positive integer\n const visibleChars = Math.max(0, Math.floor(config.visibleChars || 0));\n const trimmedValue = value.trim();\n\n // Handle empty or very short values\n if (trimmedValue.length === 0) {\n return config.redactionString;\n }\n\n switch (config.strategy) {\n case HeaderRedactionStrategy.REPLACE:\n return config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL:\n // Show first N characters, then redaction string\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return trimmedValue.substring(0, visibleChars) + config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL_END:\n // Show last N characters, with redaction string prefix\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return (\n config.redactionString +\n trimmedValue.substring(trimmedValue.length - visibleChars)\n );\n\n case HeaderRedactionStrategy.REMOVE:\n // This should be handled at the filter level, not here\n // But if we reach here, return redaction string as fallback\n return config.redactionString;\n\n default:\n // Unknown strategy - default to full redaction for safety\n return config.redactionString;\n }\n}\n","/**\n * Header filtering logic - applies allow/deny list rules and redaction\n */\n\nimport { createLogger } from \"../logger\";\nimport type { HeaderRedactionConfig } from \"./sensitive-headers\";\nimport {\n DEFAULT_REDACTION_CONFIG,\n isSensitiveHeader,\n redactHeaderValue,\n HeaderRedactionStrategy,\n} from \"./sensitive-headers\";\n\nconst log = createLogger(\"[PingOps HeaderFilter]\");\n\n/**\n * Normalizes header name to lowercase for case-insensitive matching\n */\nfunction normalizeHeaderName(name: string): string {\n return name.toLowerCase();\n}\n\n/**\n * Merges redaction config with defaults\n */\nfunction mergeRedactionConfig(\n config?: HeaderRedactionConfig\n): Required<HeaderRedactionConfig> {\n // If config is undefined, use default config (enabled by default)\n if (!config) {\n return DEFAULT_REDACTION_CONFIG;\n }\n\n // If explicitly disabled, return disabled config\n if (config.enabled === false) {\n return { ...DEFAULT_REDACTION_CONFIG, enabled: false };\n }\n\n // Otherwise, merge with defaults (enabled defaults to true)\n return {\n sensitivePatterns:\n config.sensitivePatterns ?? DEFAULT_REDACTION_CONFIG.sensitivePatterns,\n strategy: config.strategy ?? DEFAULT_REDACTION_CONFIG.strategy,\n redactionString:\n config.redactionString ?? DEFAULT_REDACTION_CONFIG.redactionString,\n visibleChars: config.visibleChars ?? DEFAULT_REDACTION_CONFIG.visibleChars,\n enabled: config.enabled ?? DEFAULT_REDACTION_CONFIG.enabled,\n };\n}\n\n/**\n * Filters headers based on allow/deny lists and applies redaction to sensitive headers\n * - Deny list always wins (if header is in deny list, exclude it)\n * - Allow list filters included headers (if specified, only include these)\n * - Sensitive headers are redacted after filtering (if redaction is enabled)\n * - Case-insensitive matching\n *\n * @param headers - Headers to filter\n * @param headersAllowList - Optional allow list of header names to include\n * @param headersDenyList - Optional deny list of header names to exclude\n * @param redactionConfig - Optional configuration for header value redaction\n * @returns Filtered and redacted headers\n */\nexport function filterHeaders(\n headers: Record<string, string | string[] | undefined>,\n headersAllowList?: string[],\n headersDenyList?: string[],\n redactionConfig?: HeaderRedactionConfig\n): Record<string, string | string[] | undefined> {\n const originalCount = Object.keys(headers).length;\n const redaction = mergeRedactionConfig(redactionConfig);\n\n log.debug(\"Filtering headers\", {\n originalHeaderCount: originalCount,\n hasAllowList: !!headersAllowList && headersAllowList.length > 0,\n hasDenyList: !!headersDenyList && headersDenyList.length > 0,\n allowListCount: headersAllowList?.length || 0,\n denyListCount: headersDenyList?.length || 0,\n redactionEnabled: redaction.enabled,\n redactionStrategy: redaction.strategy,\n });\n\n const normalizedDenyList = headersDenyList?.map(normalizeHeaderName) ?? [];\n const normalizedAllowList = headersAllowList?.map(normalizeHeaderName) ?? [];\n\n const filtered: Record<string, string | string[] | undefined> = {};\n const deniedHeaders: string[] = [];\n const excludedHeaders: string[] = [];\n const redactedHeaders: string[] = [];\n\n for (const [name, value] of Object.entries(headers)) {\n const normalizedName = normalizeHeaderName(name);\n\n // Deny list always wins\n if (normalizedDenyList.includes(normalizedName)) {\n deniedHeaders.push(name);\n log.debug(\"Header denied by deny list\", { headerName: name });\n continue;\n }\n\n // If allow list exists, only include headers in the list\n if (normalizedAllowList.length > 0) {\n if (!normalizedAllowList.includes(normalizedName)) {\n excludedHeaders.push(name);\n log.debug(\"Header excluded (not in allow list)\", { headerName: name });\n continue;\n }\n }\n\n // Apply redaction if enabled and header is sensitive\n let finalValue = value;\n if (redaction.enabled) {\n try {\n // Check if header matches sensitive patterns\n if (isSensitiveHeader(name, redaction.sensitivePatterns)) {\n // Handle REMOVE strategy at filter level\n if (redaction.strategy === HeaderRedactionStrategy.REMOVE) {\n log.debug(\"Header removed by redaction strategy\", {\n headerName: name,\n });\n continue;\n }\n\n // Redact the value\n finalValue = redactHeaderValue(value, redaction);\n redactedHeaders.push(name);\n log.debug(\"Header value redacted\", {\n headerName: name,\n strategy: redaction.strategy,\n });\n }\n } catch (error) {\n // Log error but don't fail - use original value as fallback\n log.warn(\"Error redacting header value\", {\n headerName: name,\n error: error instanceof Error ? error.message : String(error),\n });\n finalValue = value;\n }\n }\n\n filtered[name] = finalValue;\n }\n\n const filteredCount = Object.keys(filtered).length;\n log.info(\"Header filtering complete\", {\n originalCount,\n filteredCount,\n deniedCount: deniedHeaders.length,\n excludedCount: excludedHeaders.length,\n redactedCount: redactedHeaders.length,\n deniedHeaders: deniedHeaders.length > 0 ? deniedHeaders : undefined,\n excludedHeaders: excludedHeaders.length > 0 ? excludedHeaders : undefined,\n redactedHeaders: redactedHeaders.length > 0 ? redactedHeaders : undefined,\n });\n\n return filtered;\n}\n\n/**\n * Extracts and normalizes headers from OpenTelemetry span attributes\n *\n * Handles two formats:\n * 1. Flat array format (e.g., 'http.request.header.0', 'http.request.header.1')\n * - 'http.request.header.0': 'Content-Type'\n * - 'http.request.header.1': 'application/json'\n * 2. Direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n * - 'http.request.header.date': 'Mon, 12 Jan 2026 20:22:38 GMT'\n * - 'http.request.header.content-type': 'application/json'\n *\n * This function converts them to:\n * - { 'Content-Type': 'application/json', 'date': 'Mon, 12 Jan 2026 20:22:38 GMT' }\n */\nexport function extractHeadersFromAttributes(\n attributes: Record<string, unknown>,\n headerPrefix: \"http.request.header\" | \"http.response.header\"\n): Record<string, string | string[] | undefined> | null {\n const headerMap: Record<string, string | string[] | undefined> = {};\n const headerKeys: number[] = [];\n const directKeyValueHeaders: Array<{ key: string; headerName: string }> = [];\n\n const prefixPattern = `${headerPrefix}.`;\n const numericPattern = new RegExp(\n `^${headerPrefix.replace(/\\./g, \"\\\\.\")}\\\\.(\\\\d+)$`\n );\n\n // Find all keys matching the pattern\n for (const key in attributes) {\n if (key.startsWith(prefixPattern) && key !== headerPrefix) {\n // Check for numeric index format (flat array)\n const numericMatch = key.match(numericPattern);\n if (numericMatch) {\n const index = parseInt(numericMatch[1], 10);\n headerKeys.push(index);\n } else {\n // Check for direct key-value format (e.g., 'http.request.header.date')\n const headerName = key.substring(prefixPattern.length);\n if (headerName.length > 0) {\n directKeyValueHeaders.push({ key, headerName });\n }\n }\n }\n }\n\n // Process numeric index format (flat array)\n if (headerKeys.length > 0) {\n // Sort indices to process in order\n headerKeys.sort((a, b) => a - b);\n\n // Convert flat array to key-value pairs\n // Even indices are header names, odd indices are header values\n for (let i = 0; i < headerKeys.length; i += 2) {\n const nameIndex = headerKeys[i];\n const valueIndex = headerKeys[i + 1];\n\n if (valueIndex !== undefined) {\n const nameKey = `${headerPrefix}.${nameIndex}`;\n const valueKey = `${headerPrefix}.${valueIndex}`;\n\n const headerName = attributes[nameKey] as string | undefined;\n const headerValue = attributes[valueKey] as string | undefined;\n\n if (headerName && headerValue !== undefined) {\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, headerValue]\n : [existing as string, headerValue];\n } else {\n // Use original case for the first occurrence\n headerMap[headerName] = headerValue;\n }\n }\n }\n }\n }\n\n // Process direct key-value format (e.g., 'http.request.header.date')\n if (directKeyValueHeaders.length > 0) {\n for (const { key, headerName } of directKeyValueHeaders) {\n const headerValue = attributes[key];\n\n if (headerValue !== undefined && headerValue !== null) {\n // Convert to string if needed\n const stringValue =\n typeof headerValue === \"string\" ? headerValue : String(headerValue);\n\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, stringValue]\n : [existing as string, stringValue];\n } else {\n // Use the header name as stored (may be lowercase from instrumentation)\n headerMap[headerName] = stringValue;\n }\n }\n }\n }\n\n return Object.keys(headerMap).length > 0 ? headerMap : null;\n}\n\n/**\n * Type guard to check if value is a Headers-like object\n */\nfunction isHeadersLike(\n headers: unknown\n): headers is { entries: () => IterableIterator<[string, string]> } {\n return (\n typeof headers === \"object\" &&\n headers !== null &&\n \"entries\" in headers &&\n typeof (headers as { entries?: unknown }).entries === \"function\"\n );\n}\n\n/**\n * Normalizes headers from various sources into a proper key-value object\n */\nexport function normalizeHeaders(\n headers: unknown\n): Record<string, string | string[] | undefined> {\n const result: Record<string, string | string[] | undefined> = {};\n\n if (!headers) {\n return result;\n }\n\n try {\n // Handle Headers object (from fetch/undici)\n if (isHeadersLike(headers)) {\n for (const [key, value] of headers.entries()) {\n // Headers can have multiple values for the same key\n if (result[key]) {\n // Convert to array if not already\n const existing = result[key];\n result[key] = Array.isArray(existing)\n ? [...existing, value]\n : [existing, value];\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n // Handle plain object\n if (typeof headers === \"object\" && !Array.isArray(headers)) {\n for (const [key, value] of Object.entries(headers)) {\n // Skip numeric keys (array-like objects)\n if (!/^\\d+$/.test(key)) {\n result[key] = value as string | string[] | undefined;\n }\n }\n return result;\n }\n\n // Handle array (shouldn't happen, but handle gracefully)\n if (Array.isArray(headers)) {\n // Try to reconstruct from array pairs\n for (let i = 0; i < headers.length; i += 2) {\n if (i + 1 < headers.length) {\n const key = String(headers[i]);\n const value = headers[i + 1] as string | string[] | undefined;\n result[key] = value;\n }\n }\n return result;\n }\n } catch {\n // Fail silently - return empty object\n }\n\n return result;\n}\n","/**\n * Extracts structured data from spans for PingOps backend\n */\n\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport type { DomainRule, SpanPayload } from \"../types\";\nimport type { HeaderRedactionConfig } from \"../filtering/sensitive-headers\";\nimport {\n filterHeaders,\n extractHeadersFromAttributes,\n} from \"../filtering/header-filter\";\n\n/**\n * Extracts domain from URL\n */\nfunction extractDomainFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n return match ? match[1] : \"\";\n }\n}\n\n/**\n * Gets domain rule configuration for a given URL\n */\nfunction getDomainRule(\n url: string,\n domainAllowList?: DomainRule[]\n): DomainRule | undefined {\n if (!domainAllowList) {\n return undefined;\n }\n\n const domain = extractDomainFromUrl(url);\n for (const rule of domainAllowList) {\n if (\n domain === rule.domain ||\n domain.endsWith(`.${rule.domain}`) ||\n domain === rule.domain.slice(1)\n ) {\n return rule;\n }\n }\n return undefined;\n}\n\n/**\n * Determines if body should be captured based on priority:\n * domain rule > global config > default (false)\n */\nfunction shouldCaptureBody(\n domainRule: DomainRule | undefined,\n globalConfig: boolean | undefined,\n bodyType: \"request\" | \"response\"\n): boolean {\n // Check domain-specific rule first\n if (domainRule) {\n const domainValue =\n bodyType === \"request\"\n ? domainRule.captureRequestBody\n : domainRule.captureResponseBody;\n if (domainValue !== undefined) {\n return domainValue;\n }\n }\n // Fall back to global config\n if (globalConfig !== undefined) {\n return globalConfig;\n }\n // Default to false\n return false;\n}\n\n/**\n * Extracts structured payload from a span\n */\nexport function extractSpanPayload(\n span: ReadableSpan,\n domainAllowList?: DomainRule[],\n globalHeadersAllowList?: string[],\n globalHeadersDenyList?: string[],\n globalCaptureRequestBody?: boolean,\n globalCaptureResponseBody?: boolean,\n headerRedaction?: HeaderRedactionConfig\n): SpanPayload | null {\n const attributes = span.attributes;\n const url =\n (attributes[\"http.url\"] as string) || (attributes[\"url.full\"] as string);\n\n // Get domain-specific rule if available\n const domainRule = url ? getDomainRule(url, domainAllowList) : undefined;\n\n // Merge global and domain-specific header rules\n const headersAllowList =\n domainRule?.headersAllowList ?? globalHeadersAllowList;\n const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;\n\n // Determine if bodies should be captured\n const shouldCaptureReqBody = shouldCaptureBody(\n domainRule,\n globalCaptureRequestBody,\n \"request\"\n );\n const shouldCaptureRespBody = shouldCaptureBody(\n domainRule,\n globalCaptureResponseBody,\n \"response\"\n );\n\n // Extract HTTP headers if available\n let requestHeaders: Record<string, string | string[] | undefined> = {};\n let responseHeaders: Record<string, string | string[] | undefined> = {};\n\n // First, try to extract flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1')\n // or direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n const flatRequestHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.request.header\"\n );\n const flatResponseHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.response.header\"\n );\n\n // Try to get headers from attributes (format may vary by instrumentation)\n const httpRequestHeadersValue = attributes[\"http.request.header\"];\n const httpResponseHeadersValue = attributes[\"http.response.header\"];\n\n // Type guard: check if value is a record/object with string keys\n const isHeadersRecord = (\n value: unknown\n ): value is Record<string, string | string[] | undefined> => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value).every(\n (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((item) => typeof item === \"string\")) ||\n v === undefined\n )\n );\n };\n\n // Use flat array format if available, otherwise use direct attribute\n if (flatRequestHeaders) {\n requestHeaders = filterHeaders(\n flatRequestHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpRequestHeadersValue)) {\n requestHeaders = filterHeaders(\n httpRequestHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n if (flatResponseHeaders) {\n responseHeaders = filterHeaders(\n flatResponseHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpResponseHeadersValue)) {\n responseHeaders = filterHeaders(\n httpResponseHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n // Build attributes object\n const extractedAttributes: Record<string, unknown> = {\n ...attributes,\n };\n\n // Remove flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1', etc.)\n // and direct key-value format headers (e.g., 'http.request.header.date', 'http.request.header.content-type')\n // We'll replace them with the proper key-value format\n for (const key in extractedAttributes) {\n if (\n (key.startsWith(\"http.request.header.\") &&\n key !== \"http.request.header\") ||\n (key.startsWith(\"http.response.header.\") &&\n key !== \"http.response.header\")\n ) {\n // Remove both numeric index format and direct key-value format\n delete extractedAttributes[key];\n }\n }\n\n // Add filtered headers in proper key-value format\n if (Object.keys(requestHeaders).length > 0) {\n extractedAttributes[\"http.request.header\"] = requestHeaders;\n }\n\n if (Object.keys(responseHeaders).length > 0) {\n extractedAttributes[\"http.response.header\"] = responseHeaders;\n }\n\n // Remove body attributes if capture is disabled\n if (!shouldCaptureReqBody) {\n delete extractedAttributes[\"http.request.body\"];\n }\n\n if (!shouldCaptureRespBody) {\n delete extractedAttributes[\"http.response.body\"];\n }\n\n // Build span payload\n const spanContext = span.spanContext();\n // parentSpanId may not be available in all versions of ReadableSpan\n const parentSpanId =\n \"parentSpanId\" in span\n ? (span as ReadableSpan & { parentSpanId?: string }).parentSpanId\n : undefined;\n return {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n parentSpanId,\n name: span.name,\n kind: span.kind.toString(),\n startTime: new Date(\n span.startTime[0] * 1000 + span.startTime[1] / 1000000\n ).toISOString(),\n endTime: new Date(\n span.endTime[0] * 1000 + span.endTime[1] / 1000000\n ).toISOString(),\n duration:\n (span.endTime[0] - span.startTime[0]) * 1000 +\n (span.endTime[1] - span.startTime[1]) / 1000000,\n attributes: extractedAttributes,\n status: {\n code: span.status.code.toString(),\n message: span.status.message,\n },\n };\n}\n","/**\n * OpenTelemetry context keys for PingOps\n */\n\nimport { createContextKey } from \"@opentelemetry/api\";\n\n/**\n * Context key for enabling HTTP instrumentation.\n * When set to true, HTTP requests will be automatically instrumented.\n * This allows wrapHttp to control which HTTP calls are captured.\n */\nexport const PINGOPS_HTTP_ENABLED = createContextKey(\"pingops-http-enabled\");\n\n/**\n * Context key for user ID attribute.\n * Used to propagate user identifier to all spans in the context.\n */\nexport const PINGOPS_USER_ID = createContextKey(\"pingops-user-id\");\n\n/**\n * Context key for session ID attribute.\n * Used to propagate session identifier to all spans in the context.\n */\nexport const PINGOPS_SESSION_ID = createContextKey(\"pingops-session-id\");\n\n/**\n * Context key for tags attribute.\n * Used to propagate tags array to all spans in the context.\n */\nexport const PINGOPS_TAGS = createContextKey(\"pingops-tags\");\n\n/**\n * Context key for metadata attribute.\n * Used to propagate metadata object to all spans in the context.\n */\nexport const PINGOPS_METADATA = createContextKey(\"pingops-metadata\");\n\n/**\n * Context key for capturing request body.\n * When set, controls whether request bodies should be captured for HTTP spans.\n * This allows wrapHttp to control body capture per-request.\n */\nexport const PINGOPS_CAPTURE_REQUEST_BODY = createContextKey(\n \"pingops-capture-request-body\"\n);\n\n/**\n * Context key for capturing response body.\n * When set, controls whether response bodies should be captured for HTTP spans.\n * This allows wrapHttp to control body capture per-request.\n */\nexport const PINGOPS_CAPTURE_RESPONSE_BODY = createContextKey(\n \"pingops-capture-response-body\"\n);\n","/**\n * Extracts propagated attributes from OpenTelemetry context\n */\n\nimport type { Context } from \"@opentelemetry/api\";\nimport {\n PINGOPS_USER_ID,\n PINGOPS_SESSION_ID,\n PINGOPS_TAGS,\n PINGOPS_METADATA,\n} from \"../context-keys\";\n\n/**\n * Extracts propagated attributes from the given context and returns them\n * as span attributes that can be set on a span.\n *\n * @param parentContext - The OpenTelemetry context to extract attributes from\n * @returns Record of attribute key-value pairs to set on spans\n */\nexport function getPropagatedAttributesFromContext(\n parentContext: Context\n): Record<string, string | string[]> {\n const attributes: Record<string, string | string[]> = {};\n\n // Extract userId\n const userId = parentContext.getValue(PINGOPS_USER_ID);\n if (userId !== undefined && typeof userId === \"string\") {\n attributes[\"pingops.user_id\"] = userId;\n }\n\n // Extract sessionId\n const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);\n if (sessionId !== undefined && typeof sessionId === \"string\") {\n attributes[\"pingops.session_id\"] = sessionId;\n }\n\n // Extract tags\n const tags = parentContext.getValue(PINGOPS_TAGS);\n if (tags !== undefined && Array.isArray(tags)) {\n attributes[\"pingops.tags\"] = tags;\n }\n\n // Extract metadata\n const metadata = parentContext.getValue(PINGOPS_METADATA);\n if (\n metadata !== undefined &&\n typeof metadata === \"object\" &&\n metadata !== null &&\n !Array.isArray(metadata)\n ) {\n // Flatten metadata object into span attributes with prefix\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n attributes[`pingops.metadata.${key}`] = value;\n }\n }\n }\n\n return attributes;\n}\n","/**\n * wrapHttp - Wraps a function to set attributes on HTTP spans created within the wrapped block.\n *\n * This function sets attributes (userId, sessionId, tags, metadata) in the OpenTelemetry\n * context, which are automatically propagated to all spans created within the wrapped function.\n *\n * Instrumentation behavior:\n * - If `initializePingops` was called: All HTTP requests are instrumented by default.\n * `wrapHttp` only adds attributes to spans created within the wrapped block.\n * - If `initializePingops` was NOT called: Only HTTP requests within `wrapHttp` blocks\n * are instrumented. Requests outside `wrapHttp` are not instrumented.\n */\n\nimport { context } from \"@opentelemetry/api\";\nimport { createLogger } from \"./logger\";\nimport {\n PINGOPS_HTTP_ENABLED,\n PINGOPS_USER_ID,\n PINGOPS_SESSION_ID,\n PINGOPS_TAGS,\n PINGOPS_METADATA,\n PINGOPS_CAPTURE_REQUEST_BODY,\n PINGOPS_CAPTURE_RESPONSE_BODY,\n} from \"./context-keys\";\nimport type { WrapHttpAttributes } from \"./types\";\n\nconst logger = createLogger(\"[PingOps wrapHttp]\");\n\n/**\n * Options for wrapHttp function\n */\nexport interface WrapHttpOptions {\n attributes?: WrapHttpAttributes;\n /**\n * Callback to check if SDK is initialized.\n * Required to determine if global instrumentation is enabled.\n */\n checkInitialized: () => boolean;\n /**\n * Callback to check if global instrumentation is enabled.\n * Required to determine instrumentation behavior.\n */\n isGlobalInstrumentationEnabled: () => boolean;\n /**\n * Optional callback to ensure SDK is initialized (auto-initialization).\n * If not provided, wrapHttp will try to auto-initialize from environment variables.\n */\n ensureInitialized?: () => Promise<void>;\n}\n\n/**\n * Wraps a function to set attributes on HTTP spans created within the wrapped block.\n *\n * This function sets attributes (userId, sessionId, tags, metadata) in the OpenTelemetry\n * context, which are automatically propagated to all spans created within the wrapped function.\n *\n * Instrumentation behavior:\n * - If `initializePingops` was called: All HTTP requests are instrumented by default.\n * `wrapHttp` only adds attributes to spans created within the wrapped block.\n * - If `initializePingops` was NOT called: Only HTTP requests within `wrapHttp` blocks\n * are instrumented. Requests outside `wrapHttp` are not instrumented.\n *\n * Note: This is the low-level API. For a simpler API with automatic setup,\n * use `wrapHttp` from `@pingops/sdk` instead.\n *\n * @param options - Options including attributes and required callbacks\n * @param fn - Function to execute within the attribute context\n * @returns The result of the function\n */\nexport function wrapHttp<T>(\n options: WrapHttpOptions,\n fn: () => T | Promise<T>\n): T | Promise<T> {\n logger.debug(\"wrapHttp called\", {\n hasAttributes: !!options.attributes,\n hasUserId: !!options.attributes?.userId,\n hasSessionId: !!options.attributes?.sessionId,\n hasTags: !!options.attributes?.tags,\n hasMetadata: !!options.attributes?.metadata,\n });\n\n // Normalize options - if just attributes provided, it means callbacks are required\n // This is a type error at compile time, but we handle it gracefully\n const normalizedOptions: WrapHttpOptions =\n \"checkInitialized\" in options && \"isGlobalInstrumentationEnabled\" in options\n ? options\n : (() => {\n throw new Error(\n \"wrapHttp requires checkInitialized and isGlobalInstrumentationEnabled callbacks. Use wrapHttp from @pingops/sdk for automatic setup.\"\n );\n })();\n\n const { checkInitialized, ensureInitialized } = normalizedOptions;\n\n // Ensure SDK is initialized so that span processor can extract attributes\n // If already initialized, execute synchronously\n if (checkInitialized()) {\n logger.debug(\"SDK already initialized, executing wrapHttp synchronously\");\n return executeWrapHttpWithContext(normalizedOptions, fn);\n }\n\n // If not initialized, we need to initialize first (async)\n if (ensureInitialized) {\n logger.debug(\n \"SDK not initialized, using provided ensureInitialized callback\"\n );\n return ensureInitialized()\n .then(() => {\n logger.debug(\"SDK initialized, executing wrapHttp\");\n return executeWrapHttpWithContext(normalizedOptions, fn);\n })\n .catch((error) => {\n logger.error(\"Failed to initialize SDK for wrapHttp\", {\n error: error instanceof Error ? error.message : String(error),\n });\n throw error;\n });\n }\n\n // No ensureInitialized callback provided, execute without initialization\n logger.debug(\n \"SDK not initialized and no ensureInitialized callback provided, executing wrapHttp\"\n );\n return executeWrapHttpWithContext(normalizedOptions, fn);\n}\n\nfunction executeWrapHttpWithContext<T>(\n options: WrapHttpOptions,\n fn: () => T | Promise<T>\n): T | Promise<T> {\n const { attributes, isGlobalInstrumentationEnabled } = options;\n const globalInstrumentationEnabled = isGlobalInstrumentationEnabled();\n\n logger.debug(\"Executing wrapHttp context\", {\n hasAttributes: !!attributes,\n globalInstrumentationEnabled,\n });\n\n const activeContext = context.active();\n\n // If global instrumentation is not enabled, enable HTTP instrumentation\n // for this block only. If it's enabled, all requests are already instrumented.\n let contextWithAttributes = activeContext;\n if (!globalInstrumentationEnabled) {\n contextWithAttributes = contextWithAttributes.setValue(\n PINGOPS_HTTP_ENABLED,\n true\n );\n }\n\n // Set attributes in context if provided\n // These will be propagated to all spans created within the wrapped function\n if (attributes) {\n if (attributes.userId !== undefined) {\n contextWithAttributes = contextWithAttributes.setValue(\n PINGOPS_USER_ID,\n attributes.userId\n );\n }\n if (attributes.sessionId !== undefined) {\n contextWithAttributes = contextWithAttributes.setValue(\n PINGOPS_SESSION_ID,\n attributes.sessionId\n );\n }\n if (attributes.tags !== undefined) {\n contextWithAttributes = contextWithAttributes.setValue(\n PINGOPS_TAGS,\n attributes.tags\n );\n }\n if (attributes.metadata !== undefined) {\n contextWithAttributes = contextWithAttributes.setValue(\n PINGOPS_METADATA,\n attributes.metadata\n );\n }\n if (attributes.captureRequestBody !== undefined) {\n contextWithAttributes = contextWithAttributes.setValue(\n PINGOPS_CAPTURE_REQUEST_BODY,\n attributes.captureRequestBody\n );\n }\n if (attributes.captureResponseBody !== undefined) {\n contextWithAttributes = contextWithAttributes.setValue(\n PINGOPS_CAPTURE_RESPONSE_BODY,\n attributes.captureResponseBody\n );\n }\n }\n\n // Run user code inside the context with attributes\n return context.with(contextWithAttributes, () => {\n try {\n const result = fn();\n\n if (result instanceof Promise) {\n return result.catch((err) => {\n logger.error(\"Error in wrapHttp async execution\", {\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n });\n }\n\n return result;\n } catch (err) {\n logger.error(\"Error in wrapHttp sync execution\", {\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["log","SpanKind","log"],"sources":["../src/logger.ts","../src/filtering/span-filter.ts","../src/filtering/domain-filter.ts","../src/filtering/sensitive-headers.ts","../src/filtering/header-filter.ts","../src/filtering/body-decoder.ts","../src/utils/span-extractor.ts","../src/context-keys.ts","../src/utils/context-extractor.ts","../src/trace-id.ts"],"sourcesContent":["/**\n * Global logger utility for PingOps Core\n *\n * Provides consistent logging across all core components with support for\n * different log levels and debug mode control via PINGOPS_DEBUG environment variable.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\n/**\n * Creates a logger instance with a specific prefix\n *\n * @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')\n * @returns Logger instance\n */\nexport function createLogger(prefix: string): Logger {\n const isDebugEnabled = process.env.PINGOPS_DEBUG === \"true\";\n\n const formatMessage = (level: LogLevel, message: string): string => {\n const timestamp = new Date().toISOString();\n return `[${timestamp}] ${prefix} [${level.toUpperCase()}] ${message}`;\n };\n\n return {\n debug(message: string, ...args: unknown[]): void {\n if (isDebugEnabled) {\n console.debug(formatMessage(\"debug\", message), ...args);\n }\n },\n info(message: string, ...args: unknown[]): void {\n console.log(formatMessage(\"info\", message), ...args);\n },\n warn(message: string, ...args: unknown[]): void {\n console.warn(formatMessage(\"warn\", message), ...args);\n },\n error(message: string, ...args: unknown[]): void {\n console.error(formatMessage(\"error\", message), ...args);\n },\n };\n}\n","/**\n * Span filtering logic - determines if a span is eligible for capture\n */\n\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps SpanFilter]\");\n\n/**\n * Checks if a span is eligible for capture based on span kind and attributes.\n * A span is eligible if:\n * 1. span.kind === SpanKind.CLIENT\n * 2. AND has HTTP attributes (http.method, http.url, or server.address)\n * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)\n */\nexport function isSpanEligible(span: ReadableSpan): boolean {\n log.debug(\"Checking span eligibility\", {\n spanName: span.name,\n spanKind: span.kind,\n spanId: span.spanContext().spanId,\n traceId: span.spanContext().traceId,\n });\n\n // Must be a CLIENT span (outgoing request)\n if (span.kind !== SpanKind.CLIENT) {\n log.debug(\"Span not eligible: not CLIENT kind\", {\n spanName: span.name,\n spanKind: span.kind,\n });\n return false;\n }\n\n const attributes = span.attributes;\n\n // Check for HTTP attributes\n const hasHttpMethod = attributes[\"http.method\"] !== undefined;\n const hasHttpUrl = attributes[\"http.url\"] !== undefined;\n const hasServerAddress = attributes[\"server.address\"] !== undefined;\n\n const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;\n\n log.debug(\"Span eligibility check result\", {\n spanName: span.name,\n isEligible,\n httpAttributes: {\n hasMethod: hasHttpMethod,\n hasUrl: hasHttpUrl,\n hasServerAddress,\n },\n });\n\n return isEligible;\n}\n","/**\n * Domain filtering logic - applies allow/deny list rules\n */\n\nimport type { DomainRule } from \"../types\";\nimport { createLogger } from \"../logger\";\n\nconst log = createLogger(\"[PingOps DomainFilter]\");\n\n/**\n * Extracts domain from a URL\n */\nfunction extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n const domain = urlObj.hostname;\n log.debug(\"Extracted domain from URL\", { url, domain });\n return domain;\n } catch {\n // If URL parsing fails, try to extract domain from string\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n const domain = match ? match[1] : \"\";\n log.debug(\"Extracted domain from URL (fallback)\", { url, domain });\n return domain;\n }\n}\n\n/**\n * Checks if a domain matches a rule (exact or suffix match)\n */\nfunction domainMatches(domain: string, ruleDomain: string): boolean {\n // Exact match\n if (domain === ruleDomain) {\n log.debug(\"Domain exact match\", { domain, ruleDomain });\n return true;\n }\n\n // Suffix match (e.g., .github.com matches api.github.com)\n if (ruleDomain.startsWith(\".\")) {\n const matches =\n domain.endsWith(ruleDomain) || domain === ruleDomain.slice(1);\n log.debug(\"Domain suffix match check\", { domain, ruleDomain, matches });\n return matches;\n }\n\n log.debug(\"Domain does not match\", { domain, ruleDomain });\n return false;\n}\n\n/**\n * Checks if a path matches any of the allowed paths (prefix match)\n */\nfunction pathMatches(path: string, allowedPaths?: string[]): boolean {\n if (!allowedPaths || allowedPaths.length === 0) {\n log.debug(\"No path restrictions, all paths match\", { path });\n return true; // No path restrictions means all paths match\n }\n\n const matches = allowedPaths.some((allowedPath) =>\n path.startsWith(allowedPath)\n );\n log.debug(\"Path match check\", { path, allowedPaths, matches });\n return matches;\n}\n\n/**\n * Determines if a span should be captured based on domain rules\n */\nexport function shouldCaptureSpan(\n url: string,\n domainAllowList?: DomainRule[],\n domainDenyList?: DomainRule[]\n): boolean {\n log.debug(\"Checking domain filter rules\", {\n url,\n hasAllowList: !!domainAllowList && domainAllowList.length > 0,\n hasDenyList: !!domainDenyList && domainDenyList.length > 0,\n allowListCount: domainAllowList?.length || 0,\n denyListCount: domainDenyList?.length || 0,\n });\n\n const domain = extractDomain(url);\n\n // Extract path from URL\n let path = \"/\";\n try {\n const urlObj = new URL(url);\n path = urlObj.pathname;\n } catch {\n // If URL parsing fails, try to extract path from string\n const pathMatch = url.match(/^(?:https?:\\/\\/)?[^/]+(\\/.*)?$/);\n path = pathMatch && pathMatch[1] ? pathMatch[1] : \"/\";\n }\n\n log.debug(\"Extracted domain and path\", { url, domain, path });\n\n // Deny list is evaluated first - if domain is denied, don't capture\n if (domainDenyList) {\n for (const rule of domainDenyList) {\n if (domainMatches(domain, rule.domain)) {\n log.info(\"Domain denied by deny list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return false;\n }\n }\n log.debug(\"Domain passed deny list check\", { domain });\n }\n\n // If no allow list, capture all (except denied)\n if (!domainAllowList || domainAllowList.length === 0) {\n log.debug(\"No allow list configured, capturing span\", { domain, url });\n return true;\n }\n\n // Check if domain matches any allow list rule\n for (const rule of domainAllowList) {\n if (domainMatches(domain, rule.domain)) {\n // If paths are specified, check path match\n if (rule.paths && rule.paths.length > 0) {\n const pathMatch = pathMatches(path, rule.paths);\n if (pathMatch) {\n log.info(\"Domain and path allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n url,\n });\n return true;\n } else {\n log.debug(\"Domain allowed but path not matched\", {\n domain,\n ruleDomain: rule.domain,\n path,\n allowedPaths: rule.paths,\n });\n }\n } else {\n log.info(\"Domain allowed by allow list\", {\n domain,\n ruleDomain: rule.domain,\n url,\n });\n return true;\n }\n }\n }\n\n // Domain not in allow list\n log.info(\"Domain not in allow list, filtering out\", { domain, url });\n return false;\n}\n","/**\n * Sensitive header patterns and redaction configuration\n */\n\n/**\n * Default patterns for sensitive headers that should be redacted\n * These are matched case-insensitively\n */\nexport const DEFAULT_SENSITIVE_HEADER_PATTERNS = [\n // Authentication & Authorization\n \"authorization\",\n \"www-authenticate\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"x-auth-token\",\n \"x-api-key\",\n \"x-api-token\",\n \"x-access-token\",\n \"x-auth-user\",\n \"x-auth-password\",\n \"x-csrf-token\",\n \"x-xsrf-token\",\n\n // API Keys & Access Tokens\n \"api-key\",\n \"apikey\",\n \"api_key\",\n \"access-key\",\n \"accesskey\",\n \"access_key\",\n \"secret-key\",\n \"secretkey\",\n \"secret_key\",\n \"private-key\",\n \"privatekey\",\n \"private_key\",\n\n // Session & Cookie tokens\n \"cookie\",\n \"set-cookie\",\n \"session-id\",\n \"sessionid\",\n \"session_id\",\n \"session-token\",\n \"sessiontoken\",\n \"session_token\",\n\n // OAuth & OAuth2\n \"oauth-token\",\n \"oauth_token\",\n \"oauth2-token\",\n \"oauth2_token\",\n \"bearer\",\n\n // AWS & Cloud credentials\n \"x-amz-security-token\",\n \"x-amz-signature\",\n \"x-aws-access-key\",\n \"x-aws-secret-key\",\n \"x-aws-session-token\",\n\n // Other common sensitive headers\n \"x-password\",\n \"x-secret\",\n \"x-token\",\n \"x-jwt\",\n \"x-jwt-token\",\n \"x-refresh-token\",\n \"x-client-secret\",\n \"x-client-id\",\n \"x-user-token\",\n \"x-service-key\",\n] as const;\n\n/**\n * Redaction strategies for sensitive header values\n */\nexport enum HeaderRedactionStrategy {\n /**\n * Replace the entire value with a fixed redaction string\n */\n REPLACE = \"replace\",\n /**\n * Show only the first N characters, redact the rest\n */\n PARTIAL = \"partial\",\n /**\n * Show only the last N characters, redact the rest\n */\n PARTIAL_END = \"partial_end\",\n /**\n * Remove the header entirely (same as deny list)\n */\n REMOVE = \"remove\",\n}\n\n/**\n * Configuration for header redaction\n */\nexport interface HeaderRedactionConfig {\n /**\n * Patterns to match sensitive headers (case-insensitive)\n * Defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS if not provided\n */\n sensitivePatterns?: readonly string[];\n\n /**\n * Redaction strategy to use\n * @default HeaderRedactionStrategy.REPLACE\n */\n strategy?: HeaderRedactionStrategy;\n\n /**\n * Redaction string used when strategy is REPLACE\n * @default \"[REDACTED]\"\n */\n redactionString?: string;\n\n /**\n * Number of characters to show when strategy is PARTIAL or PARTIAL_END\n * @default 4\n */\n visibleChars?: number;\n\n /**\n * Whether to enable redaction\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Default redaction configuration\n */\nexport const DEFAULT_REDACTION_CONFIG: Required<HeaderRedactionConfig> = {\n sensitivePatterns: DEFAULT_SENSITIVE_HEADER_PATTERNS,\n strategy: HeaderRedactionStrategy.REPLACE,\n redactionString: \"[REDACTED]\",\n visibleChars: 4,\n enabled: true,\n};\n\n/**\n * Checks if a header name matches any sensitive pattern\n * Uses case-insensitive matching with exact match, prefix/suffix, and substring matching\n *\n * @param headerName - The header name to check\n * @param patterns - Array of patterns to match against (defaults to DEFAULT_SENSITIVE_HEADER_PATTERNS)\n * @returns true if the header matches any sensitive pattern\n */\nexport function isSensitiveHeader(\n headerName: string,\n patterns: readonly string[] = DEFAULT_SENSITIVE_HEADER_PATTERNS\n): boolean {\n if (!headerName || typeof headerName !== \"string\") {\n return false;\n }\n\n if (!patterns || patterns.length === 0) {\n return false;\n }\n\n const normalizedName = headerName.toLowerCase().trim();\n\n // Early return for empty string\n if (normalizedName.length === 0) {\n return false;\n }\n\n return patterns.some((pattern) => {\n if (!pattern || typeof pattern !== \"string\") {\n return false;\n }\n\n const normalizedPattern = pattern.toLowerCase().trim();\n\n // Empty pattern doesn't match\n if (normalizedPattern.length === 0) {\n return false;\n }\n\n // Exact match (most common case, check first)\n if (normalizedName === normalizedPattern) {\n return true;\n }\n\n // Check if header name contains the pattern (e.g., \"x-api-key\" contains \"api-key\")\n // This handles cases where patterns are embedded in header names\n if (normalizedName.includes(normalizedPattern)) {\n return true;\n }\n\n // Check if pattern contains the header name (for shorter patterns matching longer headers)\n // This is less common but handles edge cases\n if (normalizedPattern.includes(normalizedName)) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Redacts a header value based on the configuration\n */\nexport function redactHeaderValue(\n value: string | string[] | undefined,\n config: Required<HeaderRedactionConfig>\n): string | string[] | undefined {\n if (value === undefined || value === null) {\n return value;\n }\n\n // Handle array of values\n if (Array.isArray(value)) {\n return value.map((v) => redactSingleValue(v, config));\n }\n\n return redactSingleValue(value, config);\n}\n\n/**\n * Redacts a single string value based on the configured strategy\n *\n * @param value - The value to redact\n * @param config - Redaction configuration\n * @returns Redacted value\n */\nfunction redactSingleValue(\n value: string,\n config: Required<HeaderRedactionConfig>\n): string {\n // Validate input\n if (!value || typeof value !== \"string\") {\n return value;\n }\n\n // Ensure visibleChars is a positive integer\n const visibleChars = Math.max(0, Math.floor(config.visibleChars || 0));\n const trimmedValue = value.trim();\n\n // Handle empty or very short values\n if (trimmedValue.length === 0) {\n return config.redactionString;\n }\n\n switch (config.strategy) {\n case HeaderRedactionStrategy.REPLACE:\n return config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL:\n // Show first N characters, then redaction string\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return trimmedValue.substring(0, visibleChars) + config.redactionString;\n\n case HeaderRedactionStrategy.PARTIAL_END:\n // Show last N characters, with redaction string prefix\n if (trimmedValue.length <= visibleChars) {\n // If value is shorter than visible chars, just redact it all\n return config.redactionString;\n }\n return (\n config.redactionString +\n trimmedValue.substring(trimmedValue.length - visibleChars)\n );\n\n case HeaderRedactionStrategy.REMOVE:\n // This should be handled at the filter level, not here\n // But if we reach here, return redaction string as fallback\n return config.redactionString;\n\n default:\n // Unknown strategy - default to full redaction for safety\n return config.redactionString;\n }\n}\n","/**\n * Header filtering logic - applies allow/deny list rules and redaction\n */\n\nimport { createLogger } from \"../logger\";\nimport type { HeaderRedactionConfig } from \"./sensitive-headers\";\nimport {\n DEFAULT_REDACTION_CONFIG,\n isSensitiveHeader,\n redactHeaderValue,\n HeaderRedactionStrategy,\n} from \"./sensitive-headers\";\n\nconst log = createLogger(\"[PingOps HeaderFilter]\");\n\n/**\n * Normalizes header name to lowercase for case-insensitive matching\n */\nfunction normalizeHeaderName(name: string): string {\n return name.toLowerCase();\n}\n\n/**\n * Merges redaction config with defaults\n */\nfunction mergeRedactionConfig(\n config?: HeaderRedactionConfig\n): Required<HeaderRedactionConfig> {\n // If config is undefined, use default config (enabled by default)\n if (!config) {\n return DEFAULT_REDACTION_CONFIG;\n }\n\n // If explicitly disabled, return disabled config\n if (config.enabled === false) {\n return { ...DEFAULT_REDACTION_CONFIG, enabled: false };\n }\n\n // Otherwise, merge with defaults (enabled defaults to true)\n return {\n sensitivePatterns:\n config.sensitivePatterns ?? DEFAULT_REDACTION_CONFIG.sensitivePatterns,\n strategy: config.strategy ?? DEFAULT_REDACTION_CONFIG.strategy,\n redactionString:\n config.redactionString ?? DEFAULT_REDACTION_CONFIG.redactionString,\n visibleChars: config.visibleChars ?? DEFAULT_REDACTION_CONFIG.visibleChars,\n enabled: config.enabled ?? DEFAULT_REDACTION_CONFIG.enabled,\n };\n}\n\n/**\n * Filters headers based on allow/deny lists and applies redaction to sensitive headers\n * - Deny list always wins (if header is in deny list, exclude it)\n * - Allow list filters included headers (if specified, only include these)\n * - Sensitive headers are redacted after filtering (if redaction is enabled)\n * - Case-insensitive matching\n *\n * @param headers - Headers to filter\n * @param headersAllowList - Optional allow list of header names to include\n * @param headersDenyList - Optional deny list of header names to exclude\n * @param redactionConfig - Optional configuration for header value redaction\n * @returns Filtered and redacted headers\n */\nexport function filterHeaders(\n headers: Record<string, string | string[] | undefined>,\n headersAllowList?: string[],\n headersDenyList?: string[],\n redactionConfig?: HeaderRedactionConfig\n): Record<string, string | string[] | undefined> {\n const originalCount = Object.keys(headers).length;\n const redaction = mergeRedactionConfig(redactionConfig);\n\n log.debug(\"Filtering headers\", {\n originalHeaderCount: originalCount,\n hasAllowList: !!headersAllowList && headersAllowList.length > 0,\n hasDenyList: !!headersDenyList && headersDenyList.length > 0,\n allowListCount: headersAllowList?.length || 0,\n denyListCount: headersDenyList?.length || 0,\n redactionEnabled: redaction.enabled,\n redactionStrategy: redaction.strategy,\n });\n\n const normalizedDenyList = headersDenyList?.map(normalizeHeaderName) ?? [];\n const normalizedAllowList = headersAllowList?.map(normalizeHeaderName) ?? [];\n\n const filtered: Record<string, string | string[] | undefined> = {};\n const deniedHeaders: string[] = [];\n const excludedHeaders: string[] = [];\n const redactedHeaders: string[] = [];\n\n for (const [name, value] of Object.entries(headers)) {\n const normalizedName = normalizeHeaderName(name);\n\n // Deny list always wins\n if (normalizedDenyList.includes(normalizedName)) {\n deniedHeaders.push(name);\n log.debug(\"Header denied by deny list\", { headerName: name });\n continue;\n }\n\n // If allow list exists, only include headers in the list\n if (normalizedAllowList.length > 0) {\n if (!normalizedAllowList.includes(normalizedName)) {\n excludedHeaders.push(name);\n log.debug(\"Header excluded (not in allow list)\", { headerName: name });\n continue;\n }\n }\n\n // Apply redaction if enabled and header is sensitive\n let finalValue = value;\n if (redaction.enabled) {\n try {\n // Check if header matches sensitive patterns\n if (isSensitiveHeader(name, redaction.sensitivePatterns)) {\n // Handle REMOVE strategy at filter level\n if (redaction.strategy === HeaderRedactionStrategy.REMOVE) {\n log.debug(\"Header removed by redaction strategy\", {\n headerName: name,\n });\n continue;\n }\n\n // Redact the value\n finalValue = redactHeaderValue(value, redaction);\n redactedHeaders.push(name);\n log.debug(\"Header value redacted\", {\n headerName: name,\n strategy: redaction.strategy,\n });\n }\n } catch (error) {\n // Log error but don't fail - use original value as fallback\n log.warn(\"Error redacting header value\", {\n headerName: name,\n error: error instanceof Error ? error.message : String(error),\n });\n finalValue = value;\n }\n }\n\n filtered[name] = finalValue;\n }\n\n const filteredCount = Object.keys(filtered).length;\n log.info(\"Header filtering complete\", {\n originalCount,\n filteredCount,\n deniedCount: deniedHeaders.length,\n excludedCount: excludedHeaders.length,\n redactedCount: redactedHeaders.length,\n deniedHeaders: deniedHeaders.length > 0 ? deniedHeaders : undefined,\n excludedHeaders: excludedHeaders.length > 0 ? excludedHeaders : undefined,\n redactedHeaders: redactedHeaders.length > 0 ? redactedHeaders : undefined,\n });\n\n return filtered;\n}\n\n/**\n * Extracts and normalizes headers from OpenTelemetry span attributes\n *\n * Handles two formats:\n * 1. Flat array format (e.g., 'http.request.header.0', 'http.request.header.1')\n * - 'http.request.header.0': 'Content-Type'\n * - 'http.request.header.1': 'application/json'\n * 2. Direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n * - 'http.request.header.date': 'Mon, 12 Jan 2026 20:22:38 GMT'\n * - 'http.request.header.content-type': 'application/json'\n *\n * This function converts them to:\n * - { 'Content-Type': 'application/json', 'date': 'Mon, 12 Jan 2026 20:22:38 GMT' }\n */\nexport function extractHeadersFromAttributes(\n attributes: Record<string, unknown>,\n headerPrefix: \"http.request.header\" | \"http.response.header\"\n): Record<string, string | string[] | undefined> | null {\n const headerMap: Record<string, string | string[] | undefined> = {};\n const headerKeys: number[] = [];\n const directKeyValueHeaders: Array<{ key: string; headerName: string }> = [];\n\n const prefixPattern = `${headerPrefix}.`;\n const numericPattern = new RegExp(\n `^${headerPrefix.replace(/\\./g, \"\\\\.\")}\\\\.(\\\\d+)$`\n );\n\n // Find all keys matching the pattern\n for (const key in attributes) {\n if (key.startsWith(prefixPattern) && key !== headerPrefix) {\n // Check for numeric index format (flat array)\n const numericMatch = key.match(numericPattern);\n if (numericMatch) {\n const index = parseInt(numericMatch[1], 10);\n headerKeys.push(index);\n } else {\n // Check for direct key-value format (e.g., 'http.request.header.date')\n const headerName = key.substring(prefixPattern.length);\n if (headerName.length > 0) {\n directKeyValueHeaders.push({ key, headerName });\n }\n }\n }\n }\n\n // Process numeric index format (flat array)\n if (headerKeys.length > 0) {\n // Sort indices to process in order\n headerKeys.sort((a, b) => a - b);\n\n // Convert flat array to key-value pairs\n // Even indices are header names, odd indices are header values\n for (let i = 0; i < headerKeys.length; i += 2) {\n const nameIndex = headerKeys[i];\n const valueIndex = headerKeys[i + 1];\n\n if (valueIndex !== undefined) {\n const nameKey = `${headerPrefix}.${nameIndex}`;\n const valueKey = `${headerPrefix}.${valueIndex}`;\n\n const headerName = attributes[nameKey] as string | undefined;\n const headerValue = attributes[valueKey] as string | undefined;\n\n if (headerName && headerValue !== undefined) {\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, headerValue]\n : [existing as string, headerValue];\n } else {\n // Use original case for the first occurrence\n headerMap[headerName] = headerValue;\n }\n }\n }\n }\n }\n\n // Process direct key-value format (e.g., 'http.request.header.date')\n if (directKeyValueHeaders.length > 0) {\n for (const { key, headerName } of directKeyValueHeaders) {\n const headerValue = attributes[key];\n\n if (headerValue !== undefined && headerValue !== null) {\n // Convert to string if needed\n const stringValue =\n typeof headerValue === \"string\" ? headerValue : String(headerValue);\n\n // Handle multiple values for the same header name (case-insensitive)\n const normalizedName = headerName.toLowerCase();\n const existingKey = Object.keys(headerMap).find(\n (k) => k.toLowerCase() === normalizedName\n );\n\n if (existingKey) {\n const existing = headerMap[existingKey];\n headerMap[existingKey] = Array.isArray(existing)\n ? [...existing, stringValue]\n : [existing as string, stringValue];\n } else {\n // Use the header name as stored (may be lowercase from instrumentation)\n headerMap[headerName] = stringValue;\n }\n }\n }\n }\n\n return Object.keys(headerMap).length > 0 ? headerMap : null;\n}\n\n/**\n * Type guard to check if value is a Headers-like object\n */\nfunction isHeadersLike(\n headers: unknown\n): headers is { entries: () => IterableIterator<[string, string]> } {\n return (\n typeof headers === \"object\" &&\n headers !== null &&\n \"entries\" in headers &&\n typeof (headers as { entries?: unknown }).entries === \"function\"\n );\n}\n\n/**\n * Normalizes headers from various sources into a proper key-value object\n */\nexport function normalizeHeaders(\n headers: unknown\n): Record<string, string | string[] | undefined> {\n const result: Record<string, string | string[] | undefined> = {};\n\n if (!headers) {\n return result;\n }\n\n try {\n // Handle Headers object (from fetch/undici)\n if (isHeadersLike(headers)) {\n for (const [key, value] of headers.entries()) {\n // Headers can have multiple values for the same key\n if (result[key]) {\n // Convert to array if not already\n const existing = result[key];\n result[key] = Array.isArray(existing)\n ? [...existing, value]\n : [existing, value];\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n // Handle plain object\n if (typeof headers === \"object\" && !Array.isArray(headers)) {\n for (const [key, value] of Object.entries(headers)) {\n // Skip numeric keys (array-like objects)\n if (!/^\\d+$/.test(key)) {\n result[key] = value as string | string[] | undefined;\n }\n }\n return result;\n }\n\n // Handle array (shouldn't happen, but handle gracefully)\n if (Array.isArray(headers)) {\n // Try to reconstruct from array pairs\n for (let i = 0; i < headers.length; i += 2) {\n if (i + 1 < headers.length) {\n const key = String(headers[i]);\n const value = headers[i + 1] as string | string[] | undefined;\n result[key] = value;\n }\n }\n return result;\n }\n } catch {\n // Fail silently - return empty object\n }\n\n return result;\n}\n","/**\n * Minimal body handling: buffer to string for span attributes.\n * No decompression or truncation; for compressed responses the instrumentation\n * sends base64 + content-encoding so the backend can decompress.\n */\n\n/** Span attribute for response content-encoding when body is sent as base64. */\nexport const HTTP_RESPONSE_CONTENT_ENCODING = \"http.response.content_encoding\";\n\nconst COMPRESSED_ENCODINGS = new Set([\n \"gzip\",\n \"br\",\n \"deflate\",\n \"x-gzip\",\n \"x-deflate\",\n]);\n\nfunction normalizeHeaderValue(v: unknown): string | undefined {\n if (v == null) return undefined;\n const s = Array.isArray(v) ? v.map(String).join(\", \") : String(v);\n return s.trim() || undefined;\n}\n\n/**\n * Returns true if the content-encoding header indicates a compressed body\n * (gzip, br, deflate, x-gzip, x-deflate). Used to decide whether to send\n * body as base64 + content-encoding for backend decompression.\n */\nexport function isCompressedContentEncoding(headerValue: unknown): boolean {\n const raw = normalizeHeaderValue(headerValue);\n if (!raw) return false;\n const first = raw.split(\",\")[0].trim().toLowerCase();\n return COMPRESSED_ENCODINGS.has(first);\n}\n\n/**\n * Converts a buffer to a UTF-8 string for use as request/response body on spans.\n * Returns null for null, undefined, or empty buffer.\n */\nexport function bufferToBodyString(\n buffer: Buffer | null | undefined\n): string | null {\n if (buffer == null || buffer.length === 0) return null;\n return buffer.toString(\"utf8\");\n}\n","/**\n * Extracts structured data from spans for PingOps backend\n */\n\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport type { DomainRule, SpanPayload } from \"../types\";\nimport type { HeaderRedactionConfig } from \"../filtering/sensitive-headers\";\nimport {\n filterHeaders,\n extractHeadersFromAttributes,\n} from \"../filtering/header-filter\";\n\n/**\n * Extracts domain from URL\n */\nfunction extractDomainFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n const match = url.match(/^(?:https?:\\/\\/)?([^/]+)/);\n return match ? match[1] : \"\";\n }\n}\n\n/**\n * Gets domain rule configuration for a given URL\n */\nfunction getDomainRule(\n url: string,\n domainAllowList?: DomainRule[]\n): DomainRule | undefined {\n if (!domainAllowList) {\n return undefined;\n }\n\n const domain = extractDomainFromUrl(url);\n for (const rule of domainAllowList) {\n if (\n domain === rule.domain ||\n domain.endsWith(`.${rule.domain}`) ||\n domain === rule.domain.slice(1)\n ) {\n return rule;\n }\n }\n return undefined;\n}\n\n/**\n * Determines if body should be captured based on priority:\n * domain rule > global config > default (false)\n */\nfunction shouldCaptureBody(\n domainRule: DomainRule | undefined,\n globalConfig: boolean | undefined,\n bodyType: \"request\" | \"response\"\n): boolean {\n // Check domain-specific rule first\n if (domainRule) {\n const domainValue =\n bodyType === \"request\"\n ? domainRule.captureRequestBody\n : domainRule.captureResponseBody;\n if (domainValue !== undefined) {\n return domainValue;\n }\n }\n // Fall back to global config\n if (globalConfig !== undefined) {\n return globalConfig;\n }\n // Default to false\n return false;\n}\n\n/**\n * Extracts structured payload from a span\n */\nexport function extractSpanPayload(\n span: ReadableSpan,\n domainAllowList?: DomainRule[],\n globalHeadersAllowList?: string[],\n globalHeadersDenyList?: string[],\n globalCaptureRequestBody?: boolean,\n globalCaptureResponseBody?: boolean,\n headerRedaction?: HeaderRedactionConfig\n): SpanPayload | null {\n const attributes = span.attributes;\n const url =\n (attributes[\"http.url\"] as string) || (attributes[\"url.full\"] as string);\n\n // Get domain-specific rule if available\n const domainRule = url ? getDomainRule(url, domainAllowList) : undefined;\n\n // Merge global and domain-specific header rules\n const headersAllowList =\n domainRule?.headersAllowList ?? globalHeadersAllowList;\n const headersDenyList = domainRule?.headersDenyList ?? globalHeadersDenyList;\n\n // Determine if bodies should be captured\n const shouldCaptureReqBody = shouldCaptureBody(\n domainRule,\n globalCaptureRequestBody,\n \"request\"\n );\n const shouldCaptureRespBody = shouldCaptureBody(\n domainRule,\n globalCaptureResponseBody,\n \"response\"\n );\n\n // Extract HTTP headers if available\n let requestHeaders: Record<string, string | string[] | undefined> = {};\n let responseHeaders: Record<string, string | string[] | undefined> = {};\n\n // First, try to extract flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1')\n // or direct key-value format (e.g., 'http.request.header.date', 'http.request.header.content-type')\n const flatRequestHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.request.header\"\n );\n const flatResponseHeaders = extractHeadersFromAttributes(\n attributes,\n \"http.response.header\"\n );\n\n // Try to get headers from attributes (format may vary by instrumentation)\n const httpRequestHeadersValue = attributes[\"http.request.header\"];\n const httpResponseHeadersValue = attributes[\"http.response.header\"];\n\n // Type guard: check if value is a record/object with string keys\n const isHeadersRecord = (\n value: unknown\n ): value is Record<string, string | string[] | undefined> => {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value).every(\n (v) =>\n typeof v === \"string\" ||\n (Array.isArray(v) && v.every((item) => typeof item === \"string\")) ||\n v === undefined\n )\n );\n };\n\n // Use flat array format if available, otherwise use direct attribute\n if (flatRequestHeaders) {\n requestHeaders = filterHeaders(\n flatRequestHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpRequestHeadersValue)) {\n requestHeaders = filterHeaders(\n httpRequestHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n if (flatResponseHeaders) {\n responseHeaders = filterHeaders(\n flatResponseHeaders,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n } else if (isHeadersRecord(httpResponseHeadersValue)) {\n responseHeaders = filterHeaders(\n httpResponseHeadersValue,\n headersAllowList,\n headersDenyList,\n headerRedaction\n );\n }\n\n // Build attributes object\n const extractedAttributes: Record<string, unknown> = {\n ...attributes,\n };\n\n // Remove flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1', etc.)\n // and direct key-value format headers (e.g., 'http.request.header.date', 'http.request.header.content-type')\n // We'll replace them with the proper key-value format\n for (const key in extractedAttributes) {\n if (\n (key.startsWith(\"http.request.header.\") &&\n key !== \"http.request.header\") ||\n (key.startsWith(\"http.response.header.\") &&\n key !== \"http.response.header\")\n ) {\n // Remove both numeric index format and direct key-value format\n delete extractedAttributes[key];\n }\n }\n\n // Add filtered headers in proper key-value format\n if (Object.keys(requestHeaders).length > 0) {\n extractedAttributes[\"http.request.header\"] = requestHeaders;\n }\n\n if (Object.keys(responseHeaders).length > 0) {\n extractedAttributes[\"http.response.header\"] = responseHeaders;\n }\n\n // Remove body attributes if capture is disabled\n if (!shouldCaptureReqBody) {\n delete extractedAttributes[\"http.request.body\"];\n }\n\n if (!shouldCaptureRespBody) {\n delete extractedAttributes[\"http.response.body\"];\n }\n\n // Build span payload\n const spanContext = span.spanContext();\n // parentSpanId may not be available in all versions of ReadableSpan\n const parentSpanId =\n \"parentSpanId\" in span\n ? (span as ReadableSpan & { parentSpanId?: string }).parentSpanId\n : undefined;\n return {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n parentSpanId,\n name: span.name,\n kind: span.kind.toString(),\n startTime: new Date(\n span.startTime[0] * 1000 + span.startTime[1] / 1000000\n ).toISOString(),\n endTime: new Date(\n span.endTime[0] * 1000 + span.endTime[1] / 1000000\n ).toISOString(),\n duration:\n (span.endTime[0] - span.startTime[0]) * 1000 +\n (span.endTime[1] - span.startTime[1]) / 1000000,\n attributes: extractedAttributes,\n status: {\n code: span.status.code.toString(),\n message: span.status.message,\n },\n };\n}\n","/**\n * OpenTelemetry context keys for PingOps\n */\n\nimport { createContextKey } from \"@opentelemetry/api\";\n\n/**\n * Context key for trace ID attribute.\n * Used to propagate trace identifier to all spans in the context.\n */\nexport const PINGOPS_TRACE_ID = createContextKey(\"pingops-trace-id\");\n\n/**\n * Context key for user ID attribute.\n * Used to propagate user identifier to all spans in the context.\n */\nexport const PINGOPS_USER_ID = createContextKey(\"pingops-user-id\");\n\n/**\n * Context key for session ID attribute.\n * Used to propagate session identifier to all spans in the context.\n */\nexport const PINGOPS_SESSION_ID = createContextKey(\"pingops-session-id\");\n\n/**\n * Context key for tags attribute.\n * Used to propagate tags array to all spans in the context.\n */\nexport const PINGOPS_TAGS = createContextKey(\"pingops-tags\");\n\n/**\n * Context key for metadata attribute.\n * Used to propagate metadata object to all spans in the context.\n */\nexport const PINGOPS_METADATA = createContextKey(\"pingops-metadata\");\n\n/**\n * Context key for capturing request body.\n * When set, controls whether request bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_REQUEST_BODY = createContextKey(\n \"pingops-capture-request-body\"\n);\n\n/**\n * Context key for capturing response body.\n * When set, controls whether response bodies should be captured for HTTP spans.\n */\nexport const PINGOPS_CAPTURE_RESPONSE_BODY = createContextKey(\n \"pingops-capture-response-body\"\n);\n","/**\n * Extracts propagated attributes from OpenTelemetry context\n */\n\nimport type { Context } from \"@opentelemetry/api\";\nimport {\n PINGOPS_TRACE_ID,\n PINGOPS_USER_ID,\n PINGOPS_SESSION_ID,\n PINGOPS_TAGS,\n PINGOPS_METADATA,\n} from \"../context-keys\";\n\n/**\n * Extracts propagated attributes from the given context and returns them\n * as span attributes that can be set on a span.\n *\n * @param parentContext - The OpenTelemetry context to extract attributes from\n * @returns Record of attribute key-value pairs to set on spans\n */\nexport function getPropagatedAttributesFromContext(\n parentContext: Context\n): Record<string, string | string[]> {\n const attributes: Record<string, string | string[]> = {};\n\n // Extract traceId\n const traceId = parentContext.getValue(PINGOPS_TRACE_ID);\n if (traceId !== undefined && typeof traceId === \"string\") {\n attributes[\"pingops.trace_id\"] = traceId;\n }\n\n // Extract userId\n const userId = parentContext.getValue(PINGOPS_USER_ID);\n if (userId !== undefined && typeof userId === \"string\") {\n attributes[\"pingops.user_id\"] = userId;\n }\n\n // Extract sessionId\n const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);\n if (sessionId !== undefined && typeof sessionId === \"string\") {\n attributes[\"pingops.session_id\"] = sessionId;\n }\n\n // Extract tags\n const tags = parentContext.getValue(PINGOPS_TAGS);\n if (tags !== undefined && Array.isArray(tags)) {\n attributes[\"pingops.tags\"] = tags;\n }\n\n // Extract metadata\n const metadata = parentContext.getValue(PINGOPS_METADATA);\n if (\n metadata !== undefined &&\n typeof metadata === \"object\" &&\n metadata !== null &&\n !Array.isArray(metadata)\n ) {\n // Flatten metadata object into span attributes with prefix\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n attributes[`pingops.metadata.${key}`] = value;\n }\n }\n }\n\n return attributes;\n}\n","/**\n * Deterministic and random trace ID generation for PingOps\n */\n\n/**\n * Converts a Uint8Array to a lowercase hex string.\n */\nexport function uint8ArrayToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Creates a trace ID (32 hex chars).\n * - If `seed` is provided: deterministic via SHA-256 of the seed (first 32 hex chars).\n * - Otherwise: random 16 bytes as 32 hex chars.\n */\nexport async function createTraceId(seed?: string): Promise<string> {\n if (seed) {\n const data = new TextEncoder().encode(seed);\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const hashArray = new Uint8Array(hashBuffer);\n return uint8ArrayToHex(hashArray).slice(0, 32);\n }\n\n const randomValues = crypto.getRandomValues(new Uint8Array(16));\n return uint8ArrayToHex(randomValues);\n}\n"],"mappings":"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"}
|