@pingops/core 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/index.cjs +824 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.cts +306 -0
  4. package/dist/index.d.cts.map +1 -0
  5. package/dist/index.d.mts +306 -0
  6. package/dist/index.d.mts.map +1 -0
  7. package/dist/index.mjs +804 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +17 -5
  10. package/dist/context-keys.d.ts +0 -42
  11. package/dist/context-keys.d.ts.map +0 -1
  12. package/dist/context-keys.js +0 -43
  13. package/dist/context-keys.js.map +0 -1
  14. package/dist/filtering/domain-filter.d.ts +0 -9
  15. package/dist/filtering/domain-filter.d.ts.map +0 -1
  16. package/dist/filtering/domain-filter.js +0 -136
  17. package/dist/filtering/domain-filter.js.map +0 -1
  18. package/dist/filtering/header-filter.d.ts +0 -31
  19. package/dist/filtering/header-filter.d.ts.map +0 -1
  20. package/dist/filtering/header-filter.js +0 -187
  21. package/dist/filtering/header-filter.js.map +0 -1
  22. package/dist/filtering/span-filter.d.ts +0 -13
  23. package/dist/filtering/span-filter.d.ts.map +0 -1
  24. package/dist/filtering/span-filter.js +0 -46
  25. package/dist/filtering/span-filter.js.map +0 -1
  26. package/dist/index.d.ts +0 -13
  27. package/dist/index.d.ts.map +0 -1
  28. package/dist/index.js +0 -13
  29. package/dist/index.js.map +0 -1
  30. package/dist/logger.d.ts +0 -21
  31. package/dist/logger.d.ts.map +0 -1
  32. package/dist/logger.js +0 -36
  33. package/dist/logger.js.map +0 -1
  34. package/dist/types.d.ts +0 -46
  35. package/dist/types.d.ts.map +0 -1
  36. package/dist/types.js +0 -5
  37. package/dist/types.js.map +0 -1
  38. package/dist/utils/context-extractor.d.ts +0 -13
  39. package/dist/utils/context-extractor.d.ts.map +0 -1
  40. package/dist/utils/context-extractor.js +0 -44
  41. package/dist/utils/context-extractor.js.map +0 -1
  42. package/dist/utils/span-extractor.d.ts +0 -10
  43. package/dist/utils/span-extractor.d.ts.map +0 -1
  44. package/dist/utils/span-extractor.js +0 -156
  45. package/dist/utils/span-extractor.js.map +0 -1
  46. package/dist/wrap-http.d.ts +0 -55
  47. package/dist/wrap-http.d.ts.map +0 -1
  48. package/dist/wrap-http.js +0 -135
  49. package/dist/wrap-http.js.map +0 -1
@@ -1,187 +0,0 @@
1
- /**
2
- * Header filtering logic - applies allow/deny list rules
3
- */
4
- import { createLogger } from "../logger";
5
- const log = createLogger("[PingOps HeaderFilter]");
6
- /**
7
- * Normalizes header name to lowercase for case-insensitive matching
8
- */
9
- function normalizeHeaderName(name) {
10
- return name.toLowerCase();
11
- }
12
- /**
13
- * Filters headers based on allow/deny lists
14
- * - Deny list always wins (if header is in deny list, exclude it)
15
- * - Allow list filters included headers (if specified, only include these)
16
- * - Case-insensitive matching
17
- */
18
- export function filterHeaders(headers, headersAllowList, headersDenyList) {
19
- const originalCount = Object.keys(headers).length;
20
- log.debug("Filtering headers", {
21
- originalHeaderCount: originalCount,
22
- hasAllowList: !!headersAllowList && headersAllowList.length > 0,
23
- hasDenyList: !!headersDenyList && headersDenyList.length > 0,
24
- allowListCount: headersAllowList?.length || 0,
25
- denyListCount: headersDenyList?.length || 0,
26
- });
27
- const normalizedDenyList = headersDenyList?.map(normalizeHeaderName) ?? [];
28
- const normalizedAllowList = headersAllowList?.map(normalizeHeaderName) ?? [];
29
- const filtered = {};
30
- const deniedHeaders = [];
31
- const excludedHeaders = [];
32
- for (const [name, value] of Object.entries(headers)) {
33
- const normalizedName = normalizeHeaderName(name);
34
- // Deny list always wins
35
- if (normalizedDenyList.includes(normalizedName)) {
36
- deniedHeaders.push(name);
37
- log.debug("Header denied by deny list", { headerName: name });
38
- continue;
39
- }
40
- // If allow list exists, only include headers in the list
41
- if (normalizedAllowList.length > 0) {
42
- if (!normalizedAllowList.includes(normalizedName)) {
43
- excludedHeaders.push(name);
44
- log.debug("Header excluded (not in allow list)", { headerName: name });
45
- continue;
46
- }
47
- }
48
- filtered[name] = value;
49
- }
50
- const filteredCount = Object.keys(filtered).length;
51
- log.info("Header filtering complete", {
52
- originalCount,
53
- filteredCount,
54
- deniedCount: deniedHeaders.length,
55
- excludedCount: excludedHeaders.length,
56
- deniedHeaders: deniedHeaders.length > 0 ? deniedHeaders : undefined,
57
- excludedHeaders: excludedHeaders.length > 0 ? excludedHeaders : undefined,
58
- });
59
- return filtered;
60
- }
61
- /**
62
- * Extracts and normalizes headers from OpenTelemetry span attributes
63
- *
64
- * Handles flat array format headers (e.g., 'http.request.header.0', 'http.request.header.1')
65
- * and converts them to proper key-value objects.
66
- *
67
- * Some OpenTelemetry instrumentations store headers as flat arrays:
68
- * - 'http.request.header.0': 'Content-Type'
69
- * - 'http.request.header.1': 'application/json'
70
- * - 'http.request.header.2': 'Authorization'
71
- * - 'http.request.header.3': 'Bearer token'
72
- *
73
- * This function converts them to:
74
- * - { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' }
75
- */
76
- export function extractHeadersFromAttributes(attributes, headerPrefix) {
77
- const headerMap = {};
78
- const headerKeys = [];
79
- // Find all keys matching the pattern (e.g., 'http.request.header.0', 'http.request.header.1', etc.)
80
- for (const key in attributes) {
81
- if (key.startsWith(`${headerPrefix}.`) && key !== headerPrefix) {
82
- const match = key.match(new RegExp(`^${headerPrefix}\\.(\\d+)$`));
83
- if (match) {
84
- const index = parseInt(match[1], 10);
85
- headerKeys.push(index);
86
- }
87
- }
88
- }
89
- // If no flat array headers found, return null
90
- if (headerKeys.length === 0) {
91
- return null;
92
- }
93
- // Sort indices to process in order
94
- headerKeys.sort((a, b) => a - b);
95
- // Convert flat array to key-value pairs
96
- // Even indices are header names, odd indices are header values
97
- for (let i = 0; i < headerKeys.length; i += 2) {
98
- const nameIndex = headerKeys[i];
99
- const valueIndex = headerKeys[i + 1];
100
- if (valueIndex !== undefined) {
101
- const nameKey = `${headerPrefix}.${nameIndex}`;
102
- const valueKey = `${headerPrefix}.${valueIndex}`;
103
- const headerName = attributes[nameKey];
104
- const headerValue = attributes[valueKey];
105
- if (headerName && headerValue !== undefined) {
106
- // Handle multiple values for the same header name (case-insensitive)
107
- const normalizedName = headerName.toLowerCase();
108
- const existingKey = Object.keys(headerMap).find((k) => k.toLowerCase() === normalizedName);
109
- if (existingKey) {
110
- const existing = headerMap[existingKey];
111
- headerMap[existingKey] = Array.isArray(existing)
112
- ? [...existing, headerValue]
113
- : [existing, headerValue];
114
- }
115
- else {
116
- // Use original case for the first occurrence
117
- headerMap[headerName] = headerValue;
118
- }
119
- }
120
- }
121
- }
122
- return Object.keys(headerMap).length > 0 ? headerMap : null;
123
- }
124
- /**
125
- * Type guard to check if value is a Headers-like object
126
- */
127
- function isHeadersLike(headers) {
128
- return (typeof headers === "object" &&
129
- headers !== null &&
130
- "entries" in headers &&
131
- typeof headers.entries === "function");
132
- }
133
- /**
134
- * Normalizes headers from various sources into a proper key-value object
135
- */
136
- export function normalizeHeaders(headers) {
137
- const result = {};
138
- if (!headers) {
139
- return result;
140
- }
141
- try {
142
- // Handle Headers object (from fetch/undici)
143
- if (isHeadersLike(headers)) {
144
- for (const [key, value] of headers.entries()) {
145
- // Headers can have multiple values for the same key
146
- if (result[key]) {
147
- // Convert to array if not already
148
- const existing = result[key];
149
- result[key] = Array.isArray(existing)
150
- ? [...existing, value]
151
- : [existing, value];
152
- }
153
- else {
154
- result[key] = value;
155
- }
156
- }
157
- return result;
158
- }
159
- // Handle plain object
160
- if (typeof headers === "object" && !Array.isArray(headers)) {
161
- for (const [key, value] of Object.entries(headers)) {
162
- // Skip numeric keys (array-like objects)
163
- if (!/^\d+$/.test(key)) {
164
- result[key] = value;
165
- }
166
- }
167
- return result;
168
- }
169
- // Handle array (shouldn't happen, but handle gracefully)
170
- if (Array.isArray(headers)) {
171
- // Try to reconstruct from array pairs
172
- for (let i = 0; i < headers.length; i += 2) {
173
- if (i + 1 < headers.length) {
174
- const key = String(headers[i]);
175
- const value = headers[i + 1];
176
- result[key] = value;
177
- }
178
- }
179
- return result;
180
- }
181
- }
182
- catch {
183
- // Fail silently - return empty object
184
- }
185
- return result;
186
- }
187
- //# sourceMappingURL=header-filter.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"header-filter.js","sourceRoot":"","sources":["../../src/filtering/header-filter.ts"],"names":[],"mappings":"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"}
@@ -1,13 +0,0 @@
1
- /**
2
- * Span filtering logic - determines if a span is eligible for capture
3
- */
4
- import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
5
- /**
6
- * Checks if a span is eligible for capture based on span kind and attributes.
7
- * A span is eligible if:
8
- * 1. span.kind === SpanKind.CLIENT
9
- * 2. AND has HTTP attributes (http.method, http.url, or server.address)
10
- * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)
11
- */
12
- export declare function isSpanEligible(span: ReadableSpan): boolean;
13
- //# sourceMappingURL=span-filter.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"span-filter.d.ts","sourceRoot":"","sources":["../../src/filtering/span-filter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAKlE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAqC1D"}
@@ -1,46 +0,0 @@
1
- /**
2
- * Span filtering logic - determines if a span is eligible for capture
3
- */
4
- import { SpanKind } from "@opentelemetry/api";
5
- import { createLogger } from "../logger";
6
- const log = createLogger("[PingOps SpanFilter]");
7
- /**
8
- * Checks if a span is eligible for capture based on span kind and attributes.
9
- * A span is eligible if:
10
- * 1. span.kind === SpanKind.CLIENT
11
- * 2. AND has HTTP attributes (http.method, http.url, or server.address)
12
- * OR has GenAI attributes (gen_ai.system, gen_ai.operation.name)
13
- */
14
- export function isSpanEligible(span) {
15
- log.debug("Checking span eligibility", {
16
- spanName: span.name,
17
- spanKind: span.kind,
18
- spanId: span.spanContext().spanId,
19
- traceId: span.spanContext().traceId,
20
- });
21
- // Must be a CLIENT span (outgoing request)
22
- if (span.kind !== SpanKind.CLIENT) {
23
- log.debug("Span not eligible: not CLIENT kind", {
24
- spanName: span.name,
25
- spanKind: span.kind,
26
- });
27
- return false;
28
- }
29
- const attributes = span.attributes;
30
- // Check for HTTP attributes
31
- const hasHttpMethod = attributes["http.method"] !== undefined;
32
- const hasHttpUrl = attributes["http.url"] !== undefined;
33
- const hasServerAddress = attributes["server.address"] !== undefined;
34
- const isEligible = hasHttpMethod || hasHttpUrl || hasServerAddress;
35
- log.debug("Span eligibility check result", {
36
- spanName: span.name,
37
- isEligible,
38
- httpAttributes: {
39
- hasMethod: hasHttpMethod,
40
- hasUrl: hasHttpUrl,
41
- hasServerAddress,
42
- },
43
- });
44
- return isEligible;
45
- }
46
- //# sourceMappingURL=span-filter.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"span-filter.js","sourceRoot":"","sources":["../../src/filtering/span-filter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,GAAG,GAAG,YAAY,CAAC,sBAAsB,CAAC,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAkB;IAC/C,GAAG,CAAC,KAAK,CAAC,2BAA2B,EAAE;QACrC,QAAQ,EAAE,IAAI,CAAC,IAAI;QACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;QACnB,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM;QACjC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO;KACpC,CAAC,CAAC;IAEH,2CAA2C;IAC3C,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC;QAClC,GAAG,CAAC,KAAK,CAAC,oCAAoC,EAAE;YAC9C,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;SACpB,CAAC,CAAC;QACH,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;IAEnC,4BAA4B;IAC5B,MAAM,aAAa,GAAG,UAAU,CAAC,aAAa,CAAC,KAAK,SAAS,CAAC;IAC9D,MAAM,UAAU,GAAG,UAAU,CAAC,UAAU,CAAC,KAAK,SAAS,CAAC;IACxD,MAAM,gBAAgB,GAAG,UAAU,CAAC,gBAAgB,CAAC,KAAK,SAAS,CAAC;IAEpE,MAAM,UAAU,GAAG,aAAa,IAAI,UAAU,IAAI,gBAAgB,CAAC;IAEnE,GAAG,CAAC,KAAK,CAAC,+BAA+B,EAAE;QACzC,QAAQ,EAAE,IAAI,CAAC,IAAI;QACnB,UAAU;QACV,cAAc,EAAE;YACd,SAAS,EAAE,aAAa;YACxB,MAAM,EAAE,UAAU;YAClB,gBAAgB;SACjB;KACF,CAAC,CAAC;IAEH,OAAO,UAAU,CAAC;AACpB,CAAC"}
package/dist/index.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * @pingops/core - Internal shared utilities
3
- */
4
- export * from "./types";
5
- export * from "./filtering/span-filter";
6
- export * from "./filtering/domain-filter";
7
- export * from "./filtering/header-filter";
8
- export * from "./utils/span-extractor";
9
- export * from "./utils/context-extractor";
10
- export * from "./logger";
11
- export * from "./context-keys";
12
- export * from "./wrap-http";
13
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
package/dist/index.js DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * @pingops/core - Internal shared utilities
3
- */
4
- export * from "./types";
5
- export * from "./filtering/span-filter";
6
- export * from "./filtering/domain-filter";
7
- export * from "./filtering/header-filter";
8
- export * from "./utils/span-extractor";
9
- export * from "./utils/context-extractor";
10
- export * from "./logger";
11
- export * from "./context-keys";
12
- export * from "./wrap-http";
13
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
package/dist/logger.d.ts DELETED
@@ -1,21 +0,0 @@
1
- /**
2
- * Global logger utility for PingOps Core
3
- *
4
- * Provides consistent logging across all core components with support for
5
- * different log levels and debug mode control via PINGOPS_DEBUG environment variable.
6
- */
7
- export type LogLevel = "debug" | "info" | "warn" | "error";
8
- export interface Logger {
9
- debug(message: string, ...args: unknown[]): void;
10
- info(message: string, ...args: unknown[]): void;
11
- warn(message: string, ...args: unknown[]): void;
12
- error(message: string, ...args: unknown[]): void;
13
- }
14
- /**
15
- * Creates a logger instance with a specific prefix
16
- *
17
- * @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')
18
- * @returns Logger instance
19
- */
20
- export declare function createLogger(prefix: string): Logger;
21
- //# sourceMappingURL=logger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAwBnD"}
package/dist/logger.js DELETED
@@ -1,36 +0,0 @@
1
- /**
2
- * Global logger utility for PingOps Core
3
- *
4
- * Provides consistent logging across all core components with support for
5
- * different log levels and debug mode control via PINGOPS_DEBUG environment variable.
6
- */
7
- /**
8
- * Creates a logger instance with a specific prefix
9
- *
10
- * @param prefix - Prefix to add to all log messages (e.g., '[PingOps Filter]')
11
- * @returns Logger instance
12
- */
13
- export function createLogger(prefix) {
14
- const isDebugEnabled = process.env.PINGOPS_DEBUG === "true";
15
- const formatMessage = (level, message) => {
16
- const timestamp = new Date().toISOString();
17
- return `[${timestamp}] ${prefix} [${level.toUpperCase()}] ${message}`;
18
- };
19
- return {
20
- debug(message, ...args) {
21
- if (isDebugEnabled) {
22
- console.debug(formatMessage("debug", message), ...args);
23
- }
24
- },
25
- info(message, ...args) {
26
- console.log(formatMessage("info", message), ...args);
27
- },
28
- warn(message, ...args) {
29
- console.warn(formatMessage("warn", message), ...args);
30
- },
31
- error(message, ...args) {
32
- console.error(formatMessage("error", message), ...args);
33
- },
34
- };
35
- }
36
- //# sourceMappingURL=logger.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,MAAM,CAAC;IAE5D,MAAM,aAAa,GAAG,CAAC,KAAe,EAAE,OAAe,EAAU,EAAE;QACjE,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC3C,OAAO,IAAI,SAAS,KAAK,MAAM,KAAK,KAAK,CAAC,WAAW,EAAE,KAAK,OAAO,EAAE,CAAC;IACxE,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,OAAe,EAAE,GAAG,IAAe;YACvC,IAAI,cAAc,EAAE,CAAC;gBACnB,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QACD,IAAI,CAAC,OAAe,EAAE,GAAG,IAAe;YACtC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QACvD,CAAC;QACD,IAAI,CAAC,OAAe,EAAE,GAAG,IAAe;YACtC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QACxD,CAAC;QACD,KAAK,CAAC,OAAe,EAAE,GAAG,IAAe;YACvC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1D,CAAC;KACF,CAAC;AACJ,CAAC"}
package/dist/types.d.ts DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * Shared type definitions for PingOps SDK
3
- */
4
- export interface DomainRule {
5
- domain: string;
6
- paths?: string[];
7
- headersAllowList?: string[];
8
- headersDenyList?: string[];
9
- captureRequestBody?: boolean;
10
- captureResponseBody?: boolean;
11
- }
12
- export interface SpanPayload {
13
- traceId: string;
14
- spanId: string;
15
- parentSpanId?: string;
16
- name: string;
17
- kind: string;
18
- startTime: string;
19
- endTime: string;
20
- duration: number;
21
- attributes: Record<string, unknown>;
22
- status: {
23
- code: string;
24
- message?: string;
25
- };
26
- }
27
- /**
28
- * Attributes to propagate to HTTP spans
29
- */
30
- export interface WrapHttpAttributes {
31
- userId?: string;
32
- sessionId?: string;
33
- tags?: string[];
34
- metadata?: Record<string, string>;
35
- /**
36
- * Whether to capture request body for HTTP spans in this context.
37
- * Takes precedence over domain-specific rules and global config.
38
- */
39
- captureRequestBody?: boolean;
40
- /**
41
- * Whether to capture response body for HTTP spans in this context.
42
- * Takes precedence over domain-specific rules and global config.
43
- */
44
- captureResponseBody?: boolean;
45
- }
46
- //# sourceMappingURL=types.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B"}
package/dist/types.js DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * Shared type definitions for PingOps SDK
3
- */
4
- export {};
5
- //# sourceMappingURL=types.js.map
package/dist/types.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -1,13 +0,0 @@
1
- /**
2
- * Extracts propagated attributes from OpenTelemetry context
3
- */
4
- import type { Context } from "@opentelemetry/api";
5
- /**
6
- * Extracts propagated attributes from the given context and returns them
7
- * as span attributes that can be set on a span.
8
- *
9
- * @param parentContext - The OpenTelemetry context to extract attributes from
10
- * @returns Record of attribute key-value pairs to set on spans
11
- */
12
- export declare function getPropagatedAttributesFromContext(parentContext: Context): Record<string, string | string[]>;
13
- //# sourceMappingURL=context-extractor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"context-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/context-extractor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAQlD;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAChD,aAAa,EAAE,OAAO,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAsCnC"}
@@ -1,44 +0,0 @@
1
- /**
2
- * Extracts propagated attributes from OpenTelemetry context
3
- */
4
- import { PINGOPS_USER_ID, PINGOPS_SESSION_ID, PINGOPS_TAGS, PINGOPS_METADATA, } from "../context-keys";
5
- /**
6
- * Extracts propagated attributes from the given context and returns them
7
- * as span attributes that can be set on a span.
8
- *
9
- * @param parentContext - The OpenTelemetry context to extract attributes from
10
- * @returns Record of attribute key-value pairs to set on spans
11
- */
12
- export function getPropagatedAttributesFromContext(parentContext) {
13
- const attributes = {};
14
- // Extract userId
15
- const userId = parentContext.getValue(PINGOPS_USER_ID);
16
- if (userId !== undefined && typeof userId === "string") {
17
- attributes["pingops.user_id"] = userId;
18
- }
19
- // Extract sessionId
20
- const sessionId = parentContext.getValue(PINGOPS_SESSION_ID);
21
- if (sessionId !== undefined && typeof sessionId === "string") {
22
- attributes["pingops.session_id"] = sessionId;
23
- }
24
- // Extract tags
25
- const tags = parentContext.getValue(PINGOPS_TAGS);
26
- if (tags !== undefined && Array.isArray(tags)) {
27
- attributes["pingops.tags"] = tags;
28
- }
29
- // Extract metadata
30
- const metadata = parentContext.getValue(PINGOPS_METADATA);
31
- if (metadata !== undefined &&
32
- typeof metadata === "object" &&
33
- metadata !== null &&
34
- !Array.isArray(metadata)) {
35
- // Flatten metadata object into span attributes with prefix
36
- for (const [key, value] of Object.entries(metadata)) {
37
- if (typeof value === "string") {
38
- attributes[`pingops.metadata.${key}`] = value;
39
- }
40
- }
41
- }
42
- return attributes;
43
- }
44
- //# sourceMappingURL=context-extractor.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"context-extractor.js","sourceRoot":"","sources":["../../src/utils/context-extractor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,UAAU,kCAAkC,CAChD,aAAsB;IAEtB,MAAM,UAAU,GAAsC,EAAE,CAAC;IAEzD,iBAAiB;IACjB,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IACvD,IAAI,MAAM,KAAK,SAAS,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QACvD,UAAU,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC;IACzC,CAAC;IAED,oBAAoB;IACpB,MAAM,SAAS,GAAG,aAAa,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IAC7D,IAAI,SAAS,KAAK,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAC7D,UAAU,CAAC,oBAAoB,CAAC,GAAG,SAAS,CAAC;IAC/C,CAAC;IAED,eAAe;IACf,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAClD,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,UAAU,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC;IACpC,CAAC;IAED,mBAAmB;IACnB,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC1D,IACE,QAAQ,KAAK,SAAS;QACtB,OAAO,QAAQ,KAAK,QAAQ;QAC5B,QAAQ,KAAK,IAAI;QACjB,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EACxB,CAAC;QACD,2DAA2D;QAC3D,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC9B,UAAU,CAAC,oBAAoB,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC;YAChD,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -1,10 +0,0 @@
1
- /**
2
- * Extracts structured data from spans for PingOps backend
3
- */
4
- import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
5
- import type { DomainRule, SpanPayload } from "../types";
6
- /**
7
- * Extracts structured payload from a span
8
- */
9
- export declare function extractSpanPayload(span: ReadableSpan, domainAllowList?: DomainRule[], globalHeadersAllowList?: string[], globalHeadersDenyList?: string[], globalCaptureRequestBody?: boolean, globalCaptureResponseBody?: boolean): SpanPayload | null;
10
- //# sourceMappingURL=span-extractor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"span-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/span-extractor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAsExD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,YAAY,EAClB,eAAe,CAAC,EAAE,UAAU,EAAE,EAC9B,sBAAsB,CAAC,EAAE,MAAM,EAAE,EACjC,qBAAqB,CAAC,EAAE,MAAM,EAAE,EAChC,wBAAwB,CAAC,EAAE,OAAO,EAClC,yBAAyB,CAAC,EAAE,OAAO,GAClC,WAAW,GAAG,IAAI,CA6JpB"}