autotel 2.1.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/LICENSE +21 -0
- package/README.md +1946 -0
- package/dist/chunk-2LNRY4QK.js +273 -0
- package/dist/chunk-2LNRY4QK.js.map +1 -0
- package/dist/chunk-3HENGDW2.js +587 -0
- package/dist/chunk-3HENGDW2.js.map +1 -0
- package/dist/chunk-4OAT42CA.cjs +73 -0
- package/dist/chunk-4OAT42CA.cjs.map +1 -0
- package/dist/chunk-5GWX5LFW.js +70 -0
- package/dist/chunk-5GWX5LFW.js.map +1 -0
- package/dist/chunk-5R2M36QB.js +195 -0
- package/dist/chunk-5R2M36QB.js.map +1 -0
- package/dist/chunk-5ZN622AO.js +73 -0
- package/dist/chunk-5ZN622AO.js.map +1 -0
- package/dist/chunk-77MSMAUQ.cjs +498 -0
- package/dist/chunk-77MSMAUQ.cjs.map +1 -0
- package/dist/chunk-ABPEQ6RK.cjs +596 -0
- package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
- package/dist/chunk-BWYGJKRB.js +95 -0
- package/dist/chunk-BWYGJKRB.js.map +1 -0
- package/dist/chunk-BZHG5IZ4.js +73 -0
- package/dist/chunk-BZHG5IZ4.js.map +1 -0
- package/dist/chunk-G7VZBCD6.cjs +35 -0
- package/dist/chunk-G7VZBCD6.cjs.map +1 -0
- package/dist/chunk-GVLK7YUU.cjs +30 -0
- package/dist/chunk-GVLK7YUU.cjs.map +1 -0
- package/dist/chunk-HCCXC7XG.js +205 -0
- package/dist/chunk-HCCXC7XG.js.map +1 -0
- package/dist/chunk-HE6T6FIX.cjs +203 -0
- package/dist/chunk-HE6T6FIX.cjs.map +1 -0
- package/dist/chunk-KIXWPOCO.cjs +100 -0
- package/dist/chunk-KIXWPOCO.cjs.map +1 -0
- package/dist/chunk-KVGNW3FC.js +87 -0
- package/dist/chunk-KVGNW3FC.js.map +1 -0
- package/dist/chunk-LITNXTTT.js +3 -0
- package/dist/chunk-LITNXTTT.js.map +1 -0
- package/dist/chunk-M4ANN7RL.js +114 -0
- package/dist/chunk-M4ANN7RL.js.map +1 -0
- package/dist/chunk-NC52UBR2.cjs +32 -0
- package/dist/chunk-NC52UBR2.cjs.map +1 -0
- package/dist/chunk-NHCNRQD3.cjs +212 -0
- package/dist/chunk-NHCNRQD3.cjs.map +1 -0
- package/dist/chunk-NZ72VDNY.cjs +4 -0
- package/dist/chunk-NZ72VDNY.cjs.map +1 -0
- package/dist/chunk-P6JUDYNO.js +57 -0
- package/dist/chunk-P6JUDYNO.js.map +1 -0
- package/dist/chunk-RJYY7BWX.js +1349 -0
- package/dist/chunk-RJYY7BWX.js.map +1 -0
- package/dist/chunk-TRI4V5BF.cjs +126 -0
- package/dist/chunk-TRI4V5BF.cjs.map +1 -0
- package/dist/chunk-UL33I6IS.js +139 -0
- package/dist/chunk-UL33I6IS.js.map +1 -0
- package/dist/chunk-URRW6M2C.cjs +61 -0
- package/dist/chunk-URRW6M2C.cjs.map +1 -0
- package/dist/chunk-UY3UYPBZ.cjs +77 -0
- package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
- package/dist/chunk-W3253FGB.cjs +277 -0
- package/dist/chunk-W3253FGB.cjs.map +1 -0
- package/dist/chunk-W7LHZVQF.js +26 -0
- package/dist/chunk-W7LHZVQF.js.map +1 -0
- package/dist/chunk-WBWNM6LB.cjs +1360 -0
- package/dist/chunk-WBWNM6LB.cjs.map +1 -0
- package/dist/chunk-WFJ7L2RV.js +494 -0
- package/dist/chunk-WFJ7L2RV.js.map +1 -0
- package/dist/chunk-X4RMFFMR.js +28 -0
- package/dist/chunk-X4RMFFMR.js.map +1 -0
- package/dist/chunk-Y4Y2S7BM.cjs +92 -0
- package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
- package/dist/chunk-YLPNXZFI.cjs +143 -0
- package/dist/chunk-YLPNXZFI.cjs.map +1 -0
- package/dist/chunk-YTXEZ4SD.cjs +77 -0
- package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
- package/dist/chunk-Z6ZWNWWR.js +30 -0
- package/dist/chunk-Z6ZWNWWR.js.map +1 -0
- package/dist/config.cjs +26 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +75 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -0
- package/dist/db.cjs +233 -0
- package/dist/db.cjs.map +1 -0
- package/dist/db.d.cts +123 -0
- package/dist/db.d.ts +123 -0
- package/dist/db.js +228 -0
- package/dist/db.js.map +1 -0
- package/dist/decorators.cjs +67 -0
- package/dist/decorators.cjs.map +1 -0
- package/dist/decorators.d.cts +91 -0
- package/dist/decorators.d.ts +91 -0
- package/dist/decorators.js +65 -0
- package/dist/decorators.js.map +1 -0
- package/dist/event-subscriber.cjs +6 -0
- package/dist/event-subscriber.cjs.map +1 -0
- package/dist/event-subscriber.d.cts +116 -0
- package/dist/event-subscriber.d.ts +116 -0
- package/dist/event-subscriber.js +3 -0
- package/dist/event-subscriber.js.map +1 -0
- package/dist/event-testing.cjs +21 -0
- package/dist/event-testing.cjs.map +1 -0
- package/dist/event-testing.d.cts +110 -0
- package/dist/event-testing.d.ts +110 -0
- package/dist/event-testing.js +4 -0
- package/dist/event-testing.js.map +1 -0
- package/dist/event.cjs +30 -0
- package/dist/event.cjs.map +1 -0
- package/dist/event.d.cts +282 -0
- package/dist/event.d.ts +282 -0
- package/dist/event.js +13 -0
- package/dist/event.js.map +1 -0
- package/dist/exporters.cjs +17 -0
- package/dist/exporters.cjs.map +1 -0
- package/dist/exporters.d.cts +1 -0
- package/dist/exporters.d.ts +1 -0
- package/dist/exporters.js +4 -0
- package/dist/exporters.js.map +1 -0
- package/dist/functional.cjs +46 -0
- package/dist/functional.cjs.map +1 -0
- package/dist/functional.d.cts +478 -0
- package/dist/functional.d.ts +478 -0
- package/dist/functional.js +13 -0
- package/dist/functional.js.map +1 -0
- package/dist/http.cjs +189 -0
- package/dist/http.cjs.map +1 -0
- package/dist/http.d.cts +169 -0
- package/dist/http.d.ts +169 -0
- package/dist/http.js +184 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +333 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +758 -0
- package/dist/index.d.ts +758 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.cjs +182 -0
- package/dist/instrumentation.cjs.map +1 -0
- package/dist/instrumentation.d.cts +49 -0
- package/dist/instrumentation.d.ts +49 -0
- package/dist/instrumentation.js +179 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/logger.cjs +19 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +146 -0
- package/dist/logger.d.ts +146 -0
- package/dist/logger.js +6 -0
- package/dist/logger.js.map +1 -0
- package/dist/metric-helpers.cjs +31 -0
- package/dist/metric-helpers.cjs.map +1 -0
- package/dist/metric-helpers.d.cts +13 -0
- package/dist/metric-helpers.d.ts +13 -0
- package/dist/metric-helpers.js +6 -0
- package/dist/metric-helpers.js.map +1 -0
- package/dist/metric-testing.cjs +21 -0
- package/dist/metric-testing.cjs.map +1 -0
- package/dist/metric-testing.d.cts +110 -0
- package/dist/metric-testing.d.ts +110 -0
- package/dist/metric-testing.js +4 -0
- package/dist/metric-testing.js.map +1 -0
- package/dist/metric.cjs +26 -0
- package/dist/metric.cjs.map +1 -0
- package/dist/metric.d.cts +240 -0
- package/dist/metric.d.ts +240 -0
- package/dist/metric.js +9 -0
- package/dist/metric.js.map +1 -0
- package/dist/processors.cjs +17 -0
- package/dist/processors.cjs.map +1 -0
- package/dist/processors.d.cts +1 -0
- package/dist/processors.d.ts +1 -0
- package/dist/processors.js +4 -0
- package/dist/processors.js.map +1 -0
- package/dist/sampling.cjs +40 -0
- package/dist/sampling.cjs.map +1 -0
- package/dist/sampling.d.cts +260 -0
- package/dist/sampling.d.ts +260 -0
- package/dist/sampling.js +7 -0
- package/dist/sampling.js.map +1 -0
- package/dist/semantic-helpers.cjs +35 -0
- package/dist/semantic-helpers.cjs.map +1 -0
- package/dist/semantic-helpers.d.cts +442 -0
- package/dist/semantic-helpers.d.ts +442 -0
- package/dist/semantic-helpers.js +14 -0
- package/dist/semantic-helpers.js.map +1 -0
- package/dist/tail-sampling-processor.cjs +13 -0
- package/dist/tail-sampling-processor.cjs.map +1 -0
- package/dist/tail-sampling-processor.d.cts +27 -0
- package/dist/tail-sampling-processor.d.ts +27 -0
- package/dist/tail-sampling-processor.js +4 -0
- package/dist/tail-sampling-processor.js.map +1 -0
- package/dist/testing.cjs +286 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +291 -0
- package/dist/testing.d.ts +291 -0
- package/dist/testing.js +263 -0
- package/dist/testing.js.map +1 -0
- package/dist/trace-context-DRZdUvVY.d.cts +181 -0
- package/dist/trace-context-DRZdUvVY.d.ts +181 -0
- package/dist/trace-helpers.cjs +54 -0
- package/dist/trace-helpers.cjs.map +1 -0
- package/dist/trace-helpers.d.cts +524 -0
- package/dist/trace-helpers.d.ts +524 -0
- package/dist/trace-helpers.js +5 -0
- package/dist/trace-helpers.js.map +1 -0
- package/dist/tracer-provider.cjs +21 -0
- package/dist/tracer-provider.cjs.map +1 -0
- package/dist/tracer-provider.d.cts +169 -0
- package/dist/tracer-provider.d.ts +169 -0
- package/dist/tracer-provider.js +4 -0
- package/dist/tracer-provider.js.map +1 -0
- package/package.json +280 -0
- package/src/baggage-span-processor.test.ts +202 -0
- package/src/baggage-span-processor.ts +98 -0
- package/src/circuit-breaker.test.ts +341 -0
- package/src/circuit-breaker.ts +184 -0
- package/src/config.test.ts +94 -0
- package/src/config.ts +169 -0
- package/src/db.test.ts +252 -0
- package/src/db.ts +447 -0
- package/src/decorators.test.ts +203 -0
- package/src/decorators.ts +188 -0
- package/src/env-config.test.ts +246 -0
- package/src/env-config.ts +158 -0
- package/src/event-queue.test.ts +222 -0
- package/src/event-queue.ts +203 -0
- package/src/event-subscriber.ts +136 -0
- package/src/event-testing.ts +197 -0
- package/src/event.test.ts +718 -0
- package/src/event.ts +556 -0
- package/src/exporters.ts +96 -0
- package/src/functional.test.ts +1059 -0
- package/src/functional.ts +2295 -0
- package/src/http.test.ts +487 -0
- package/src/http.ts +424 -0
- package/src/index.ts +158 -0
- package/src/init.customization.test.ts +210 -0
- package/src/init.integrations.test.ts +366 -0
- package/src/init.openllmetry.test.ts +282 -0
- package/src/init.protocol.test.ts +215 -0
- package/src/init.ts +1426 -0
- package/src/instrumentation.test.ts +108 -0
- package/src/instrumentation.ts +308 -0
- package/src/logger.test.ts +117 -0
- package/src/logger.ts +246 -0
- package/src/metric-helpers.ts +47 -0
- package/src/metric-testing.ts +197 -0
- package/src/metric.ts +434 -0
- package/src/metrics.test.ts +205 -0
- package/src/operation-context.ts +93 -0
- package/src/processors.ts +106 -0
- package/src/rate-limiter.test.ts +199 -0
- package/src/rate-limiter.ts +98 -0
- package/src/sampling.test.ts +513 -0
- package/src/sampling.ts +428 -0
- package/src/semantic-helpers.test.ts +311 -0
- package/src/semantic-helpers.ts +584 -0
- package/src/shutdown.test.ts +311 -0
- package/src/shutdown.ts +222 -0
- package/src/stub.integration.test.ts +361 -0
- package/src/tail-sampling-processor.test.ts +226 -0
- package/src/tail-sampling-processor.ts +51 -0
- package/src/testing.ts +670 -0
- package/src/trace-context.ts +470 -0
- package/src/trace-helpers.new.test.ts +278 -0
- package/src/trace-helpers.test.ts +242 -0
- package/src/trace-helpers.ts +690 -0
- package/src/tracer-provider.test.ts +183 -0
- package/src/tracer-provider.ts +266 -0
- package/src/track.test.ts +153 -0
- package/src/track.ts +120 -0
- package/src/validation.test.ts +306 -0
- package/src/validation.ts +239 -0
- package/src/variable-name-inference.test.ts +178 -0
- package/src/variable-name-inference.ts +242 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
// src/validation.ts
|
|
4
|
+
var DEFAULT_CONFIG = {
|
|
5
|
+
maxEventNameLength: 100,
|
|
6
|
+
maxAttributeKeyLength: 100,
|
|
7
|
+
maxAttributeValueLength: 1e3,
|
|
8
|
+
maxAttributeCount: 50,
|
|
9
|
+
maxNestingDepth: 3,
|
|
10
|
+
sensitivePatterns: [
|
|
11
|
+
/password/i,
|
|
12
|
+
/secret/i,
|
|
13
|
+
/token/i,
|
|
14
|
+
/api[_-]?key/i,
|
|
15
|
+
/access[_-]?key/i,
|
|
16
|
+
/private[_-]?key/i,
|
|
17
|
+
/auth/i,
|
|
18
|
+
/credential/i,
|
|
19
|
+
/ssn/i,
|
|
20
|
+
/credit[_-]?card/i
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
var ValidationError = class extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "ValidationError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function validateEventName(eventName, config = DEFAULT_CONFIG) {
|
|
30
|
+
if (typeof eventName !== "string") {
|
|
31
|
+
throw new ValidationError(
|
|
32
|
+
`Event name must be a string, got ${typeof eventName}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const trimmed = eventName.trim();
|
|
36
|
+
if (trimmed.length === 0) {
|
|
37
|
+
throw new ValidationError("Event name cannot be empty");
|
|
38
|
+
}
|
|
39
|
+
if (trimmed.length > config.maxEventNameLength) {
|
|
40
|
+
throw new ValidationError(
|
|
41
|
+
`Event name too long (${trimmed.length} chars). Max: ${config.maxEventNameLength}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) {
|
|
45
|
+
throw new ValidationError(
|
|
46
|
+
`Event name contains invalid characters: "${trimmed}". Use only letters, numbers, dots, underscores, and hyphens.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return trimmed;
|
|
50
|
+
}
|
|
51
|
+
function validateAttributes(attributes, config = DEFAULT_CONFIG) {
|
|
52
|
+
if (attributes === void 0 || attributes === null) {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
if (typeof attributes !== "object" || Array.isArray(attributes)) {
|
|
56
|
+
throw new ValidationError("Attributes must be an object");
|
|
57
|
+
}
|
|
58
|
+
const keys = Object.keys(attributes);
|
|
59
|
+
if (keys.length > config.maxAttributeCount) {
|
|
60
|
+
throw new ValidationError(
|
|
61
|
+
`Too many attributes (${keys.length}). Max: ${config.maxAttributeCount}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const sanitized = {};
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
if (key.length > config.maxAttributeKeyLength) {
|
|
67
|
+
throw new ValidationError(
|
|
68
|
+
`Attribute key too long: "${key.slice(0, 20)}..." (${key.length} chars). Max: ${config.maxAttributeKeyLength}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const isSensitive = config.sensitivePatterns.some(
|
|
72
|
+
(pattern) => pattern.test(key)
|
|
73
|
+
);
|
|
74
|
+
if (isSensitive) {
|
|
75
|
+
sanitized[key] = "[REDACTED]";
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const value = attributes[key];
|
|
79
|
+
sanitized[key] = sanitizeValue(value, config, 1);
|
|
80
|
+
}
|
|
81
|
+
return sanitized;
|
|
82
|
+
}
|
|
83
|
+
function sanitizeValue(value, config, depth) {
|
|
84
|
+
if (depth > config.maxNestingDepth) {
|
|
85
|
+
return "[MAX_DEPTH_EXCEEDED]";
|
|
86
|
+
}
|
|
87
|
+
if (value === null || value === void 0) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value === "string") {
|
|
91
|
+
if (value.length > config.maxAttributeValueLength) {
|
|
92
|
+
return value.slice(0, config.maxAttributeValueLength) + "...";
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
return value.map((item) => sanitizeValue(item, config, depth + 1));
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === "object") {
|
|
103
|
+
try {
|
|
104
|
+
JSON.stringify(value);
|
|
105
|
+
const sanitized = {};
|
|
106
|
+
for (const key in value) {
|
|
107
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
108
|
+
sanitized[key] = sanitizeValue(
|
|
109
|
+
value[key],
|
|
110
|
+
config,
|
|
111
|
+
depth + 1
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return sanitized;
|
|
116
|
+
} catch {
|
|
117
|
+
return "[CIRCULAR]";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return `[${typeof value}]`;
|
|
121
|
+
}
|
|
122
|
+
function validateEvent(eventName, attributes, config) {
|
|
123
|
+
const fullConfig = { ...DEFAULT_CONFIG, ...config };
|
|
124
|
+
return {
|
|
125
|
+
eventName: validateEventName(eventName, fullConfig),
|
|
126
|
+
attributes: validateAttributes(attributes, fullConfig)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
var operationStorage = new AsyncLocalStorage();
|
|
130
|
+
function getOperationContext() {
|
|
131
|
+
return operationStorage.getStore();
|
|
132
|
+
}
|
|
133
|
+
function runInOperationContext(name, fn) {
|
|
134
|
+
return operationStorage.run({ name }, fn);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { getOperationContext, runInOperationContext, validateEvent };
|
|
138
|
+
//# sourceMappingURL=chunk-UL33I6IS.js.map
|
|
139
|
+
//# sourceMappingURL=chunk-UL33I6IS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validation.ts","../src/operation-context.ts"],"names":[],"mappings":"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","file":"chunk-UL33I6IS.js","sourcesContent":["/**\n * Input validation for events events and attributes\n *\n * Prevents:\n * - Invalid event names\n * - Oversized payloads\n * - Circular references\n * - Sensitive data leaks\n */\n\nimport type { EventAttributes } from './event-subscriber';\n\nexport interface ValidationConfig {\n /** Max event name length (default: 100) */\n maxEventNameLength: number;\n /** Max attribute key length (default: 100) */\n maxAttributeKeyLength: number;\n /** Max attribute value length for strings (default: 1000) */\n maxAttributeValueLength: number;\n /** Max total attributes per event (default: 50) */\n maxAttributeCount: number;\n /** Max nesting depth for objects (default: 3) */\n maxNestingDepth: number;\n /** Sensitive field patterns to redact */\n sensitivePatterns: RegExp[];\n}\n\nconst DEFAULT_CONFIG: ValidationConfig = {\n maxEventNameLength: 100,\n maxAttributeKeyLength: 100,\n maxAttributeValueLength: 1000,\n maxAttributeCount: 50,\n maxNestingDepth: 3,\n sensitivePatterns: [\n /password/i,\n /secret/i,\n /token/i,\n /api[_-]?key/i,\n /access[_-]?key/i,\n /private[_-]?key/i,\n /auth/i,\n /credential/i,\n /ssn/i,\n /credit[_-]?card/i,\n ],\n};\n\nexport class ValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Validate and sanitize event name\n * Throws ValidationError if invalid\n */\nexport function validateEventName(\n eventName: string,\n config: ValidationConfig = DEFAULT_CONFIG,\n): string {\n // Check type\n if (typeof eventName !== 'string') {\n throw new ValidationError(\n `Event name must be a string, got ${typeof eventName}`,\n );\n }\n\n // Check non-empty\n const trimmed = eventName.trim();\n if (trimmed.length === 0) {\n throw new ValidationError('Event name cannot be empty');\n }\n\n // Check length\n if (trimmed.length > config.maxEventNameLength) {\n throw new ValidationError(\n `Event name too long (${trimmed.length} chars). ` +\n `Max: ${config.maxEventNameLength}`,\n );\n }\n\n // Check valid characters (alphanumeric, dots, underscores, hyphens)\n if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) {\n throw new ValidationError(\n `Event name contains invalid characters: \"${trimmed}\". ` +\n 'Use only letters, numbers, dots, underscores, and hyphens.',\n );\n }\n\n return trimmed;\n}\n\n/**\n * Validate and sanitize attributes\n * Returns sanitized attributes (sensitive data redacted)\n */\nexport function validateAttributes(\n attributes: EventAttributes | undefined,\n config: ValidationConfig = DEFAULT_CONFIG,\n): EventAttributes | undefined {\n if (attributes === undefined || attributes === null) {\n return undefined;\n }\n\n // Check type\n if (typeof attributes !== 'object' || Array.isArray(attributes)) {\n throw new ValidationError('Attributes must be an object');\n }\n\n // Count attributes\n const keys = Object.keys(attributes);\n if (keys.length > config.maxAttributeCount) {\n throw new ValidationError(\n `Too many attributes (${keys.length}). ` +\n `Max: ${config.maxAttributeCount}`,\n );\n }\n\n // Validate and sanitize each attribute\n const sanitized: EventAttributes = {};\n\n for (const key of keys) {\n // Validate key\n if (key.length > config.maxAttributeKeyLength) {\n throw new ValidationError(\n `Attribute key too long: \"${key.slice(0, 20)}...\" ` +\n `(${key.length} chars). Max: ${config.maxAttributeKeyLength}`,\n );\n }\n\n // Check for sensitive field\n const isSensitive = config.sensitivePatterns.some((pattern) =>\n pattern.test(key),\n );\n\n if (isSensitive) {\n // Redact sensitive data\n sanitized[key] = '[REDACTED]';\n continue;\n }\n\n // Sanitize value\n const value = attributes[key];\n sanitized[key] = sanitizeValue(value, config, 1) as\n | string\n | number\n | boolean;\n }\n\n return sanitized;\n}\n\n/**\n * Sanitize attribute value (recursive)\n */\nfunction sanitizeValue(\n value: unknown,\n config: ValidationConfig,\n depth: number,\n): unknown {\n // Check nesting depth\n if (depth > config.maxNestingDepth) {\n return '[MAX_DEPTH_EXCEEDED]';\n }\n\n // Handle null/undefined\n if (value === null || value === undefined) {\n return value;\n }\n\n // Handle primitives\n if (typeof value === 'string') {\n if (value.length > config.maxAttributeValueLength) {\n return value.slice(0, config.maxAttributeValueLength) + '...';\n }\n return value;\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n // Handle arrays\n if (Array.isArray(value)) {\n return value.map((item) => sanitizeValue(item, config, depth + 1));\n }\n\n // Handle objects\n if (typeof value === 'object') {\n try {\n // Check for circular references\n JSON.stringify(value);\n\n const sanitized: Record<string, unknown> = {};\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n sanitized[key] = sanitizeValue(\n (value as Record<string, unknown>)[key],\n config,\n depth + 1,\n );\n }\n }\n return sanitized;\n } catch {\n // Circular reference detected\n return '[CIRCULAR]';\n }\n }\n\n // Unsupported type (function, symbol, etc.)\n return `[${typeof value}]`;\n}\n\n/**\n * Validate and sanitize an events event\n * Returns { eventName, attributes } with sanitized values\n */\nexport function validateEvent(\n eventName: string,\n attributes?: EventAttributes,\n config?: Partial<ValidationConfig>,\n): { eventName: string; attributes?: EventAttributes } {\n const fullConfig = { ...DEFAULT_CONFIG, ...config };\n\n return {\n eventName: validateEventName(eventName, fullConfig),\n attributes: validateAttributes(attributes, fullConfig),\n };\n}\n\n/**\n * Get default validation config (for testing/customization)\n */\nexport function getDefaultValidationConfig(): ValidationConfig {\n return { ...DEFAULT_CONFIG };\n}\n","/**\n * Operation context tracking using AsyncLocalStorage\n *\n * This module provides a way to track operation names across async boundaries\n * so they can be automatically captured in events events.\n *\n * We cannot read span attributes from OpenTelemetry's API (it's write-only),\n * so we maintain our own async context storage.\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\n/**\n * Operation context that flows through async operations\n */\nexport interface OperationContext {\n /**\n * The name of the current operation\n * This is set by trace() or span() and can be read by events\n */\n name: string;\n}\n\n/**\n * AsyncLocalStorage instance for tracking operation context\n */\nconst operationStorage = new AsyncLocalStorage<OperationContext>();\n\n/**\n * Get the current operation context (if any)\n *\n * @returns The current operation context, or undefined if not in an operation\n *\n * @example\n * ```typescript\n * const ctx = getOperationContext();\n * if (ctx) {\n * console.log('Current operation:', ctx.name);\n * }\n * ```\n */\nexport function getOperationContext(): OperationContext | undefined {\n return operationStorage.getStore();\n}\n\n/**\n * Run a function within an operation context\n *\n * This sets the operation name for the duration of the function execution,\n * including all async operations spawned from it.\n *\n * @param name - The operation name to set\n * @param fn - The function to execute within the context\n * @returns The result of the function\n *\n * @example\n * ```typescript\n * const result = await runInOperationContext('user.create', async () => {\n * // Any events.trackEvent() calls here will automatically capture\n * // 'operation.name': 'user.create'\n * await createUser();\n * return 'success';\n * });\n * ```\n */\nexport function runInOperationContext<T>(name: string, fn: () => T): T {\n return operationStorage.run({ name }, fn);\n}\n\n/**\n * Update the operation name in the current context\n *\n * This is useful when you want to change the operation name within\n * an already-established context (e.g., when entering a nested span).\n *\n * @param name - The new operation name\n *\n * @example\n * ```typescript\n * runInOperationContext('parent.operation', () => {\n * // operation.name is 'parent.operation'\n *\n * updateOperationName('nested.operation');\n * // operation.name is now 'nested.operation'\n * });\n * ```\n */\nexport function updateOperationName(name: string): void {\n const store = operationStorage.getStore();\n if (store) {\n store.name = name;\n }\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var api = require('@opentelemetry/api');
|
|
4
|
+
|
|
5
|
+
// src/tracer-provider.ts
|
|
6
|
+
var AUTOLEMETRY_GLOBAL_SYMBOL = Symbol.for("autotel");
|
|
7
|
+
function createState() {
|
|
8
|
+
return {
|
|
9
|
+
isolatedTracerProvider: null
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function getGlobalState() {
|
|
13
|
+
const initialState = createState();
|
|
14
|
+
try {
|
|
15
|
+
const g = globalThis;
|
|
16
|
+
if (typeof g !== "object" || g === null) {
|
|
17
|
+
console.warn(
|
|
18
|
+
"[autotel] globalThis is not available, using fallback state"
|
|
19
|
+
);
|
|
20
|
+
return initialState;
|
|
21
|
+
}
|
|
22
|
+
if (!g[AUTOLEMETRY_GLOBAL_SYMBOL]) {
|
|
23
|
+
Object.defineProperty(g, AUTOLEMETRY_GLOBAL_SYMBOL, {
|
|
24
|
+
value: initialState,
|
|
25
|
+
writable: false,
|
|
26
|
+
// Lock the slot (not the contents)
|
|
27
|
+
configurable: false,
|
|
28
|
+
enumerable: false
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return g[AUTOLEMETRY_GLOBAL_SYMBOL];
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
console.error(
|
|
35
|
+
`[autotel] Failed to access global state: ${error.message}`
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
console.error(
|
|
39
|
+
`[autotel] Failed to access global state: ${String(error)}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return initialState;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function setAutotelTracerProvider(provider) {
|
|
46
|
+
getGlobalState().isolatedTracerProvider = provider;
|
|
47
|
+
}
|
|
48
|
+
function getAutotelTracerProvider() {
|
|
49
|
+
const { isolatedTracerProvider } = getGlobalState();
|
|
50
|
+
if (isolatedTracerProvider) return isolatedTracerProvider;
|
|
51
|
+
return api.trace.getTracerProvider();
|
|
52
|
+
}
|
|
53
|
+
function getAutotelTracer(name = "autotel", version) {
|
|
54
|
+
return getAutotelTracerProvider().getTracer(name, version);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
exports.getAutotelTracer = getAutotelTracer;
|
|
58
|
+
exports.getAutotelTracerProvider = getAutotelTracerProvider;
|
|
59
|
+
exports.setAutotelTracerProvider = setAutotelTracerProvider;
|
|
60
|
+
//# sourceMappingURL=chunk-URRW6M2C.cjs.map
|
|
61
|
+
//# sourceMappingURL=chunk-URRW6M2C.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tracer-provider.ts"],"names":["trace"],"mappings":";;;;;AAqBA,IAAM,yBAAA,GAA4B,MAAA,CAAO,GAAA,CAAI,SAAS,CAAA;AAYtD,SAAS,WAAA,GAAkC;AACzC,EAAA,OAAO;AAAA,IACL,sBAAA,EAAwB;AAAA,GAC1B;AACF;AAaA,SAAS,cAAA,GAAqC;AAC5C,EAAA,MAAM,eAAe,WAAA,EAAY;AAEjC,EAAA,IAAI;AACF,IAAA,MAAM,CAAA,GAAI,UAAA;AAEV,IAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,IAAA,EAAM;AACvC,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN;AAAA,OACF;AACA,MAAA,OAAO,YAAA;AAAA,IACT;AAEA,IAAA,IAAI,CAAC,CAAA,CAAE,yBAAyB,CAAA,EAAG;AACjC,MAAA,MAAA,CAAO,cAAA,CAAe,GAAG,yBAAA,EAA2B;AAAA,QAClD,KAAA,EAAO,YAAA;AAAA,QACP,QAAA,EAAU,KAAA;AAAA;AAAA,QACV,YAAA,EAAc,KAAA;AAAA,QACd,UAAA,EAAY;AAAA,OACb,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,EAAE,yBAAyB,CAAA;AAAA,EACpC,SAAS,KAAA,EAAO;AACd,IAAA,IAAI,iBAAiB,KAAA,EAAO;AAC1B,MAAA,OAAA,CAAQ,KAAA;AAAA,QACN,CAAA,yCAAA,EAA4C,MAAM,OAAO,CAAA;AAAA,OAC3D;AAAA,IACF,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,KAAA;AAAA,QACN,CAAA,yCAAA,EAA4C,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,OAC3D;AAAA,IACF;AAEA,IAAA,OAAO,YAAA;AAAA,EACT;AACF;AA+FO,SAAS,yBACd,QAAA,EACM;AACN,EAAA,cAAA,GAAiB,sBAAA,GAAyB,QAAA;AAC5C;AAsCO,SAAS,wBAAA,GAA2C;AACzD,EAAA,MAAM,EAAE,sBAAA,EAAuB,GAAI,cAAA,EAAe;AAElD,EAAA,IAAI,wBAAwB,OAAO,sBAAA;AAEnC,EAAA,OAAOA,UAAM,iBAAA,EAAkB;AACjC;AAkCO,SAAS,gBAAA,CAAiB,IAAA,GAAO,SAAA,EAAW,OAAA,EAAkB;AACnE,EAAA,OAAO,wBAAA,EAAyB,CAAE,SAAA,CAAU,IAAA,EAAM,OAAO,CAAA;AAC3D","file":"chunk-URRW6M2C.cjs","sourcesContent":["/**\n * Isolated tracer provider support for Autotel\n *\n * Allows Autotel to use a separate TracerProvider instance, avoiding conflicts\n * with other OpenTelemetry instrumentation in the application.\n *\n * **Use Case:** Library authors who want to use Autotel without interfering\n * with the application's global OpenTelemetry setup.\n *\n * **Limitation:** While this isolates span processing and export, OpenTelemetry\n * context (trace IDs, parent spans) is still shared globally. Spans created with\n * the isolated provider may inherit trace context from global spans.\n */\n\nimport { trace } from '@opentelemetry/api';\nimport type { TracerProvider } from '@opentelemetry/api';\n\n/**\n * Symbol for storing isolated tracer provider in global scope\n * Using Symbol.for() ensures the same symbol across module boundaries\n */\nconst AUTOLEMETRY_GLOBAL_SYMBOL = Symbol.for('autotel');\n\n/**\n * Global state for Autotel\n */\ntype AutotelGlobalState = {\n isolatedTracerProvider: TracerProvider | null;\n};\n\n/**\n * Create initial state\n */\nfunction createState(): AutotelGlobalState {\n return {\n isolatedTracerProvider: null,\n };\n}\n\n/**\n * Extend globalThis to include our symbol\n */\ninterface GlobalThis {\n [AUTOLEMETRY_GLOBAL_SYMBOL]?: AutotelGlobalState;\n}\n\n/**\n * Get the global state, creating it if it doesn't exist\n * Handles edge cases like missing globalThis\n */\nfunction getGlobalState(): AutotelGlobalState {\n const initialState = createState();\n\n try {\n const g = globalThis as typeof globalThis & GlobalThis;\n\n if (typeof g !== 'object' || g === null) {\n console.warn(\n '[autotel] globalThis is not available, using fallback state',\n );\n return initialState;\n }\n\n if (!g[AUTOLEMETRY_GLOBAL_SYMBOL]) {\n Object.defineProperty(g, AUTOLEMETRY_GLOBAL_SYMBOL, {\n value: initialState,\n writable: false, // Lock the slot (not the contents)\n configurable: false,\n enumerable: false,\n });\n }\n\n return g[AUTOLEMETRY_GLOBAL_SYMBOL]!;\n } catch (error) {\n if (error instanceof Error) {\n console.error(\n `[autotel] Failed to access global state: ${error.message}`,\n );\n } else {\n console.error(\n `[autotel] Failed to access global state: ${String(error)}`,\n );\n }\n\n return initialState;\n }\n}\n\n/**\n * Sets an isolated TracerProvider for Autotel tracing operations.\n *\n * This allows Autotel to use its own TracerProvider instance, separate from\n * the global OpenTelemetry TracerProvider. This is useful for avoiding conflicts\n * with other OpenTelemetry instrumentation in the application.\n *\n * **Limitation: Span Context Sharing**\n *\n * While this function isolates span processing and export, it does NOT provide\n * complete trace isolation. OpenTelemetry context (trace IDs, parent spans) is\n * still shared between the global and isolated providers. This means:\n *\n * - Spans created with the isolated provider inherit trace IDs from global spans\n * - Spans created with the isolated provider inherit parent relationships from global spans\n * - This can result in spans from different providers being part of the same logical trace\n *\n * **Why this happens:**\n * OpenTelemetry uses a global context propagation mechanism that operates at the\n * JavaScript runtime level, independent of individual TracerProvider instances.\n * The context (containing trace ID, span ID) flows through async boundaries and\n * is inherited by all spans created within that context, regardless of which\n * TracerProvider creates them.\n *\n * **When to use this:**\n * - Library code that ships with embedded Autotel\n * - SDKs that want observability without requiring users to set up OpenTelemetry\n * - Applications that need separate span processing for different subsystems\n * - Testing scenarios where you want to isolate trace collection\n *\n * @param provider - The TracerProvider instance to use, or null to clear the isolated provider\n *\n * @example Library with embedded Autotel\n * ```typescript\n * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'\n * import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'\n * import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\n * import { setAutolem\n\netryTracerProvider } from 'autotel/tracer-provider'\n *\n * // Create provider with span processors in constructor\n * const exporter = new OTLPTraceExporter({\n * url: 'https://your-backend.com/v1/traces'\n * })\n *\n * const provider = new NodeTracerProvider()\n * provider.addSpanProcessor(new BatchSpanProcessor(exporter))\n *\n * // Set as Autotel's isolated provider (doesn't call provider.register())\n * setAutotelTracerProvider(provider)\n *\n * // Now all Autotel trace() calls use this provider\n * // But won't interfere with the application's global OpenTelemetry setup\n * ```\n *\n * @example Testing with isolated provider\n * ```typescript\n * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'\n * import { InMemorySpanExporter } from '@opentelemetry/sdk-trace-base'\n * import { setAutotelTracerProvider } from 'autotel/tracer-provider'\n *\n * // Test setup\n * const exporter = new InMemorySpanExporter()\n * const provider = new NodeTracerProvider()\n * provider.addSpanProcessor(new SimpleSpanProcessor(exporter))\n *\n * setAutotelTracerProvider(provider)\n *\n * // Run tests...\n * const spans = exporter.getFinishedSpans()\n *\n * // Cleanup\n * setAutotelTracerProvider(null)\n * ```\n *\n * @example Multiple subsystems with different exporters\n * ```typescript\n * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'\n * import { setAutotelTracerProvider } from 'autotel/tracer-provider'\n *\n * // Payment subsystem - send to payment team's backend\n * const paymentProvider = new NodeTracerProvider()\n * paymentProvider.addSpanProcessor(new BatchSpanProcessor(\n * new OTLPTraceExporter({ url: 'https://payment-team-backend.com/v1/traces' })\n * ))\n *\n * // In payment module initialization\n * setAutotelTracerProvider(paymentProvider)\n * ```\n *\n * @public\n */\nexport function setAutotelTracerProvider(\n provider: TracerProvider | null,\n): void {\n getGlobalState().isolatedTracerProvider = provider;\n}\n\n/**\n * Gets the TracerProvider for Autotel tracing operations.\n *\n * Returns the isolated TracerProvider if one has been set via setAutotelTracerProvider(),\n * otherwise falls back to the global OpenTelemetry TracerProvider.\n *\n * This function is used internally by Autotel's trace functions. Most users\n * will not need to call this directly.\n *\n * @returns The TracerProvider instance to use for Autotel tracing\n *\n * @example Getting the current provider\n * ```typescript\n * import { getAutotelTracerProvider } from 'autotel/tracer-provider'\n *\n * const provider = getAutotelTracerProvider()\n * const tracer = provider.getTracer('my-service', '1.0.0')\n * ```\n *\n * @example Checking if isolated provider is active\n * ```typescript\n * import { getAutotelTracerProvider, setAutotelTracerProvider } from 'autotel/tracer-provider'\n * import { trace } from '@opentelemetry/api'\n *\n * const currentProvider = getAutotelTracerProvider()\n * const globalProvider = trace.getTracerProvider()\n *\n * if (currentProvider === globalProvider) {\n * console.log('Using global provider')\n * } else {\n * console.log('Using isolated provider')\n * }\n * ```\n *\n * @public\n */\nexport function getAutotelTracerProvider(): TracerProvider {\n const { isolatedTracerProvider } = getGlobalState();\n\n if (isolatedTracerProvider) return isolatedTracerProvider;\n\n return trace.getTracerProvider();\n}\n\n/**\n * Gets the OpenTelemetry tracer instance for Autotel.\n *\n * This function returns a tracer specifically configured for Autotel\n * with the correct tracer name and version. Used internally by all\n * Autotel tracing functions to ensure consistent trace creation.\n *\n * Uses the isolated provider if set, otherwise uses the global provider.\n *\n * @param name - Tracer name (default: 'autotel')\n * @param version - Optional version string\n * @returns The Autotel OpenTelemetry tracer instance\n *\n * @example Basic usage\n * ```typescript\n * import { getAutotelTracer } from 'autotel/tracer-provider'\n *\n * const tracer = getAutotelTracer()\n * const span = tracer.startSpan('my-operation')\n * // ... use span\n * span.end()\n * ```\n *\n * @example Custom tracer name\n * ```typescript\n * import { getAutotelTracer } from 'autotel/tracer-provider'\n *\n * const tracer = getAutotelTracer('my-library', '2.1.0')\n * ```\n *\n * @public\n */\nexport function getAutotelTracer(name = 'autotel', version?: string) {\n return getAutotelTracerProvider().getTracer(name, version);\n}\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/event-testing.ts
|
|
4
|
+
function createEventCollector() {
|
|
5
|
+
const events = [];
|
|
6
|
+
const funnelSteps = [];
|
|
7
|
+
const outcomes = [];
|
|
8
|
+
const values = [];
|
|
9
|
+
return {
|
|
10
|
+
getEvents() {
|
|
11
|
+
return [...events];
|
|
12
|
+
},
|
|
13
|
+
getFunnelSteps() {
|
|
14
|
+
return [...funnelSteps];
|
|
15
|
+
},
|
|
16
|
+
getOutcomes() {
|
|
17
|
+
return [...outcomes];
|
|
18
|
+
},
|
|
19
|
+
getValues() {
|
|
20
|
+
return [...values];
|
|
21
|
+
},
|
|
22
|
+
clear() {
|
|
23
|
+
events.length = 0;
|
|
24
|
+
funnelSteps.length = 0;
|
|
25
|
+
outcomes.length = 0;
|
|
26
|
+
values.length = 0;
|
|
27
|
+
},
|
|
28
|
+
recordEvent(event) {
|
|
29
|
+
events.push(event);
|
|
30
|
+
},
|
|
31
|
+
recordFunnelStep(step) {
|
|
32
|
+
funnelSteps.push(step);
|
|
33
|
+
},
|
|
34
|
+
recordOutcome(outcome) {
|
|
35
|
+
outcomes.push(outcome);
|
|
36
|
+
},
|
|
37
|
+
recordValue(value) {
|
|
38
|
+
values.push(value);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function assertEventTracked(options) {
|
|
43
|
+
const events = options.collector.getEvents();
|
|
44
|
+
const matching = events.filter((e) => e.event === options.eventName);
|
|
45
|
+
if (matching.length === 0) {
|
|
46
|
+
throw new Error(`No events found with name: ${options.eventName}`);
|
|
47
|
+
}
|
|
48
|
+
if (options.attributes) {
|
|
49
|
+
const matchingWithAttrs = matching.filter(
|
|
50
|
+
(e) => Object.entries(options.attributes).every(
|
|
51
|
+
([key, value]) => e.attributes && e.attributes[key] === value
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
if (matchingWithAttrs.length === 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Event ${options.eventName} found but attributes don't match: ${JSON.stringify(options.attributes)}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function assertOutcomeTracked(options) {
|
|
62
|
+
const outcomes = options.collector.getOutcomes();
|
|
63
|
+
const matching = outcomes.filter(
|
|
64
|
+
(o) => o.operation === options.operation && o.status === options.status
|
|
65
|
+
);
|
|
66
|
+
if (matching.length === 0) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`No outcomes found with operation: ${options.operation} and status: ${options.status}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
exports.assertEventTracked = assertEventTracked;
|
|
74
|
+
exports.assertOutcomeTracked = assertOutcomeTracked;
|
|
75
|
+
exports.createEventCollector = createEventCollector;
|
|
76
|
+
//# sourceMappingURL=chunk-UY3UYPBZ.cjs.map
|
|
77
|
+
//# sourceMappingURL=chunk-UY3UYPBZ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/event-testing.ts"],"names":[],"mappings":"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","file":"chunk-UY3UYPBZ.cjs","sourcesContent":["/**\n * Testing utilities for Events\n *\n * Provides in-memory collection of events for testing purposes.\n */\n\nimport type {\n EventAttributes,\n FunnelStatus,\n OutcomeStatus,\n} from './event-subscriber';\n\nexport interface EventData {\n event: string;\n attributes?: EventAttributes;\n service: string;\n timestamp: number;\n}\n\nexport interface EventsFunnelStep {\n funnel: string;\n status: FunnelStatus;\n attributes?: EventAttributes;\n service: string;\n timestamp: number;\n}\n\nexport interface EventsOutcome {\n operation: string;\n status: OutcomeStatus;\n attributes?: EventAttributes;\n service: string;\n timestamp: number;\n}\n\nexport interface EventsValue {\n metric: string;\n value: number;\n attributes?: EventAttributes;\n service: string;\n timestamp: number;\n}\n\n/**\n * In-memory events collector for testing\n */\nexport interface EventCollector {\n /** Get all collected events */\n getEvents(): EventData[];\n /** Get all collected funnel steps */\n getFunnelSteps(): EventsFunnelStep[];\n /** Get all collected outcomes */\n getOutcomes(): EventsOutcome[];\n /** Get all collected values */\n getValues(): EventsValue[];\n /** Clear all collected events */\n clear(): void;\n /** Record an event (internal use) */\n recordEvent(event: EventData): void;\n /** Record a funnel step (internal use) */\n recordFunnelStep(step: EventsFunnelStep): void;\n /** Record an outcome (internal use) */\n recordOutcome(outcome: EventsOutcome): void;\n /** Record a value (internal use) */\n recordValue(value: EventsValue): void;\n}\n\n/**\n * Create an in-memory events collector for testing\n *\n * @example\n * ```typescript\n * const collector = createEventCollector()\n *\n * const events = new Event('test-service', { collector })\n * events.trackEvent('application.submitted', { jobId: '123' })\n *\n * const event =collector.getEvents()\n * expect(events).toHaveLength(1)\n * expect(events[0].event).toBe('application.submitted')\n * ```\n */\nexport function createEventCollector(): EventCollector {\n const events: EventData[] = [];\n const funnelSteps: EventsFunnelStep[] = [];\n const outcomes: EventsOutcome[] = [];\n const values: EventsValue[] = [];\n\n return {\n getEvents(): EventData[] {\n return [...events];\n },\n\n getFunnelSteps(): EventsFunnelStep[] {\n return [...funnelSteps];\n },\n\n getOutcomes(): EventsOutcome[] {\n return [...outcomes];\n },\n\n getValues(): EventsValue[] {\n return [...values];\n },\n\n clear(): void {\n events.length = 0;\n funnelSteps.length = 0;\n outcomes.length = 0;\n values.length = 0;\n },\n\n recordEvent(event: EventData): void {\n events.push(event);\n },\n\n recordFunnelStep(step: EventsFunnelStep): void {\n funnelSteps.push(step);\n },\n\n recordOutcome(outcome: EventsOutcome): void {\n outcomes.push(outcome);\n },\n\n recordValue(value: EventsValue): void {\n values.push(value);\n },\n };\n}\n\n/**\n * Assert that an events event was tracked\n *\n * @example\n * ```typescript\n * assertEventTracked({\n * collector,\n * eventName: 'application.submitted',\n * attributes: { jobId: '123' }\n * })\n * ```\n */\nexport function assertEventTracked(options: {\n collector: EventCollector;\n eventName: string;\n attributes?: Record<string, unknown>;\n}): void {\n const events = options.collector.getEvents();\n const matching = events.filter((e) => e.event === options.eventName);\n\n if (matching.length === 0) {\n throw new Error(`No events found with name: ${options.eventName}`);\n }\n\n if (options.attributes) {\n const matchingWithAttrs = matching.filter((e) =>\n Object.entries(options.attributes!).every(\n ([key, value]) => e.attributes && e.attributes[key] === value,\n ),\n );\n\n if (matchingWithAttrs.length === 0) {\n throw new Error(\n `Event ${options.eventName} found but attributes don't match: ${JSON.stringify(options.attributes)}`,\n );\n }\n }\n}\n\n/**\n * Assert that an outcome was tracked\n *\n * @example\n * ```typescript\n * assertOutcomeTracked({\n * collector,\n * operation: 'email.delivery',\n * status: 'success'\n * })\n * ```\n */\nexport function assertOutcomeTracked(options: {\n collector: EventCollector;\n operation: string;\n status: 'success' | 'failure' | 'partial';\n}): void {\n const outcomes = options.collector.getOutcomes();\n const matching = outcomes.filter(\n (o) => o.operation === options.operation && o.status === options.status,\n );\n\n if (matching.length === 0) {\n throw new Error(\n `No outcomes found with operation: ${options.operation} and status: ${options.status}`,\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkY4Y2S7BM_cjs = require('./chunk-Y4Y2S7BM.cjs');
|
|
4
|
+
require('@opentelemetry/api');
|
|
5
|
+
|
|
6
|
+
var Metric = class {
|
|
7
|
+
serviceName;
|
|
8
|
+
eventCounter;
|
|
9
|
+
funnelCounter;
|
|
10
|
+
outcomeCounter;
|
|
11
|
+
valueHistogram;
|
|
12
|
+
logger;
|
|
13
|
+
collector;
|
|
14
|
+
/**
|
|
15
|
+
* Create a new Metrics instance
|
|
16
|
+
*
|
|
17
|
+
* @param serviceName - Service name for metric namespacing
|
|
18
|
+
* @param options - Optional configuration (logger, collector, namespace, metrics)
|
|
19
|
+
*
|
|
20
|
+
* @example Basic usage (default 'metrics' namespace)
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const metrics = new Metric('checkout');
|
|
23
|
+
* // Creates: checkout.metrics.events, checkout.metrics.funnel, etc.
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Custom namespace
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const metrics = new Metric('api', { namespace: 'business' });
|
|
29
|
+
* // Creates: api.business.events, api.business.funnel, etc.
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example Custom metric names and descriptions
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const metrics = new Metric('payments', {
|
|
35
|
+
* metrics: {
|
|
36
|
+
* outcomes: {
|
|
37
|
+
* name: 'payments.transactions',
|
|
38
|
+
* description: 'Payment transaction outcomes',
|
|
39
|
+
* unit: 'transactions'
|
|
40
|
+
* },
|
|
41
|
+
* value: {
|
|
42
|
+
* name: 'payments.revenue',
|
|
43
|
+
* description: 'Payment revenue in USD',
|
|
44
|
+
* unit: 'USD'
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
constructor(serviceName, options = {}) {
|
|
51
|
+
this.serviceName = serviceName;
|
|
52
|
+
this.logger = options.logger;
|
|
53
|
+
this.collector = options.collector;
|
|
54
|
+
const config = chunkY4Y2S7BM_cjs.getConfig();
|
|
55
|
+
const meter = config.meter;
|
|
56
|
+
const namespace = options.namespace || "metrics";
|
|
57
|
+
const metricsConfig = options.metrics || {};
|
|
58
|
+
const eventsConfig = metricsConfig.events || {};
|
|
59
|
+
this.eventCounter = meter.createCounter(
|
|
60
|
+
eventsConfig.name || `${serviceName}.${namespace}.events`,
|
|
61
|
+
{
|
|
62
|
+
description: eventsConfig.description || "Count of business events",
|
|
63
|
+
unit: eventsConfig.unit || "1"
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
const funnelConfig = metricsConfig.funnel || {};
|
|
67
|
+
this.funnelCounter = meter.createCounter(
|
|
68
|
+
funnelConfig.name || `${serviceName}.${namespace}.funnel`,
|
|
69
|
+
{
|
|
70
|
+
description: funnelConfig.description || "Conversion funnel tracking",
|
|
71
|
+
unit: funnelConfig.unit || "1"
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
const outcomesConfig = metricsConfig.outcomes || {};
|
|
75
|
+
this.outcomeCounter = meter.createCounter(
|
|
76
|
+
outcomesConfig.name || `${serviceName}.${namespace}.outcomes`,
|
|
77
|
+
{
|
|
78
|
+
description: outcomesConfig.description || "Outcome tracking (success/failure)",
|
|
79
|
+
unit: outcomesConfig.unit || "1"
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
const valueConfig = metricsConfig.value || {};
|
|
83
|
+
this.valueHistogram = meter.createHistogram(
|
|
84
|
+
valueConfig.name || `${serviceName}.${namespace}.value`,
|
|
85
|
+
{
|
|
86
|
+
description: valueConfig.description || "Value metrics (revenue, counts, etc.)",
|
|
87
|
+
unit: valueConfig.unit || "1"
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Track a business event as a metric
|
|
93
|
+
*
|
|
94
|
+
* Use this for tracking user actions, business events, product usage as metrics:
|
|
95
|
+
* - "user.signup"
|
|
96
|
+
* - "order.completed"
|
|
97
|
+
* - "feature.used"
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* // Track user signup as metric
|
|
102
|
+
* metrics.trackEvent('user.signup', {
|
|
103
|
+
* userId: '123',
|
|
104
|
+
* plan: 'pro'
|
|
105
|
+
* })
|
|
106
|
+
*
|
|
107
|
+
* // Track order as metric
|
|
108
|
+
* metrics.trackEvent('order.completed', {
|
|
109
|
+
* orderId: 'ord_123',
|
|
110
|
+
* amount: 99.99
|
|
111
|
+
* })
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
trackEvent(eventName, attributes) {
|
|
115
|
+
const attrs = {
|
|
116
|
+
service: this.serviceName,
|
|
117
|
+
event: eventName,
|
|
118
|
+
...attributes
|
|
119
|
+
};
|
|
120
|
+
this.eventCounter.add(1, attrs);
|
|
121
|
+
this.logger?.info("Metric event tracked", {
|
|
122
|
+
event: eventName,
|
|
123
|
+
attributes
|
|
124
|
+
});
|
|
125
|
+
this.collector?.recordEvent({
|
|
126
|
+
event: eventName,
|
|
127
|
+
attributes,
|
|
128
|
+
service: this.serviceName,
|
|
129
|
+
timestamp: Date.now()
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Track conversion funnel steps as metrics
|
|
134
|
+
*
|
|
135
|
+
* Monitor where users drop off in multi-step processes.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* // Track signup funnel
|
|
140
|
+
* metrics.trackFunnelStep('signup', 'started', { userId: '123' })
|
|
141
|
+
* metrics.trackFunnelStep('signup', 'email_verified', { userId: '123' })
|
|
142
|
+
* metrics.trackFunnelStep('signup', 'completed', { userId: '123' })
|
|
143
|
+
*
|
|
144
|
+
* // Track checkout flow
|
|
145
|
+
* metrics.trackFunnelStep('checkout', 'started', { cartValue: 99.99 })
|
|
146
|
+
* metrics.trackFunnelStep('checkout', 'payment_info', { cartValue: 99.99 })
|
|
147
|
+
* metrics.trackFunnelStep('checkout', 'completed', { cartValue: 99.99 })
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
trackFunnelStep(funnelName, status, attributes) {
|
|
151
|
+
const attrs = {
|
|
152
|
+
service: this.serviceName,
|
|
153
|
+
funnel: funnelName,
|
|
154
|
+
status,
|
|
155
|
+
...attributes
|
|
156
|
+
};
|
|
157
|
+
this.funnelCounter.add(1, attrs);
|
|
158
|
+
this.logger?.info("Funnel step tracked", {
|
|
159
|
+
funnel: funnelName,
|
|
160
|
+
status,
|
|
161
|
+
attributes
|
|
162
|
+
});
|
|
163
|
+
this.collector?.recordFunnelStep({
|
|
164
|
+
funnel: funnelName,
|
|
165
|
+
status,
|
|
166
|
+
attributes,
|
|
167
|
+
service: this.serviceName,
|
|
168
|
+
timestamp: Date.now()
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Track outcomes (success/failure/partial) as metrics
|
|
173
|
+
*
|
|
174
|
+
* Monitor success rates of critical operations.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* // Track email delivery
|
|
179
|
+
* metrics.trackOutcome('email.delivery', 'success', {
|
|
180
|
+
* recipientType: 'user',
|
|
181
|
+
* emailType: 'welcome'
|
|
182
|
+
* })
|
|
183
|
+
*
|
|
184
|
+
* metrics.trackOutcome('email.delivery', 'failure', {
|
|
185
|
+
* recipientType: 'user',
|
|
186
|
+
* errorCode: 'invalid_email'
|
|
187
|
+
* })
|
|
188
|
+
*
|
|
189
|
+
* // Track payment processing
|
|
190
|
+
* metrics.trackOutcome('payment.process', 'success', { amount: 99.99 })
|
|
191
|
+
* metrics.trackOutcome('payment.process', 'failure', { error: 'insufficient_funds' })
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
trackOutcome(operationName, status, attributes) {
|
|
195
|
+
const attrs = {
|
|
196
|
+
service: this.serviceName,
|
|
197
|
+
operation: operationName,
|
|
198
|
+
status,
|
|
199
|
+
...attributes
|
|
200
|
+
};
|
|
201
|
+
this.outcomeCounter.add(1, attrs);
|
|
202
|
+
this.logger?.info("Outcome tracked", {
|
|
203
|
+
operation: operationName,
|
|
204
|
+
status,
|
|
205
|
+
attributes
|
|
206
|
+
});
|
|
207
|
+
this.collector?.recordOutcome({
|
|
208
|
+
operation: operationName,
|
|
209
|
+
status,
|
|
210
|
+
attributes,
|
|
211
|
+
service: this.serviceName,
|
|
212
|
+
timestamp: Date.now()
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Track value metrics
|
|
217
|
+
*
|
|
218
|
+
* Record numerical values like revenue, transaction amounts,
|
|
219
|
+
* item counts, processing times, engagement scores, etc.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* // Track revenue
|
|
224
|
+
* metrics.trackValue('order.revenue', 149.99, {
|
|
225
|
+
* currency: 'USD',
|
|
226
|
+
* productCategory: 'electronics'
|
|
227
|
+
* })
|
|
228
|
+
*
|
|
229
|
+
* // Track items per cart
|
|
230
|
+
* metrics.trackValue('cart.item_count', 5, {
|
|
231
|
+
* userId: '123'
|
|
232
|
+
* })
|
|
233
|
+
*
|
|
234
|
+
* // Track processing time
|
|
235
|
+
* metrics.trackValue('api.response_time', 250, {
|
|
236
|
+
* unit: 'ms',
|
|
237
|
+
* endpoint: '/api/checkout'
|
|
238
|
+
* })
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
trackValue(metricName, value, attributes) {
|
|
242
|
+
const attrs = {
|
|
243
|
+
service: this.serviceName,
|
|
244
|
+
metric: metricName,
|
|
245
|
+
...attributes
|
|
246
|
+
};
|
|
247
|
+
this.valueHistogram.record(value, attrs);
|
|
248
|
+
this.logger?.debug("Value metric tracked", {
|
|
249
|
+
metric: metricName,
|
|
250
|
+
value,
|
|
251
|
+
attributes
|
|
252
|
+
});
|
|
253
|
+
this.collector?.recordValue({
|
|
254
|
+
metric: metricName,
|
|
255
|
+
value,
|
|
256
|
+
attributes,
|
|
257
|
+
service: this.serviceName,
|
|
258
|
+
timestamp: Date.now()
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var metricsInstances = /* @__PURE__ */ new Map();
|
|
263
|
+
function getMetrics(serviceName, logger) {
|
|
264
|
+
if (!metricsInstances.has(serviceName)) {
|
|
265
|
+
metricsInstances.set(serviceName, new Metric(serviceName, { logger }));
|
|
266
|
+
}
|
|
267
|
+
return metricsInstances.get(serviceName);
|
|
268
|
+
}
|
|
269
|
+
function resetMetrics() {
|
|
270
|
+
metricsInstances.clear();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
exports.Metric = Metric;
|
|
274
|
+
exports.getMetrics = getMetrics;
|
|
275
|
+
exports.resetMetrics = resetMetrics;
|
|
276
|
+
//# sourceMappingURL=chunk-W3253FGB.cjs.map
|
|
277
|
+
//# sourceMappingURL=chunk-W3253FGB.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/metric.ts"],"names":["getConfig"],"mappings":"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","file":"chunk-W3253FGB.cjs","sourcesContent":["/**\n * Metrics API for OpenTelemetry\n *\n * Track business metrics for OpenTelemetry (Prometheus/Grafana).\n * For business people who think in metrics.\n *\n * @example Track business metrics\n * ```typescript\n * const metrics = new Metric('checkout')\n *\n * // Track events as metrics\n * metrics.trackEvent('order.completed', {\n * amount: 99.99,\n * currency: 'USD'\n * })\n *\n * // Track conversion funnels\n * metrics.trackFunnelStep('checkout', 'started', { cartValue: 99.99 })\n * metrics.trackFunnelStep('checkout', 'completed', { cartValue: 99.99 })\n *\n * // Track outcomes\n * metrics.trackOutcome('payment.process', 'success', { amount: 99.99 })\n * metrics.trackOutcome('payment.process', 'failure', { error: 'insufficient_funds' })\n *\n * // Track values\n * metrics.trackValue('revenue', 149.99, { currency: 'USD' })\n * ```\n */\n\nimport {\n type Counter,\n type Histogram,\n type Attributes,\n} from '@opentelemetry/api';\nimport { getConfig } from './config';\nimport { type Logger } from './logger';\nimport {\n type EventAttributes,\n type FunnelStatus,\n type OutcomeStatus,\n} from './event-subscriber';\nimport { type MetricsCollector } from './metric-testing';\n\n// Re-export types for convenience\nexport type {\n EventAttributes,\n FunnelStatus,\n OutcomeStatus,\n} from './event-subscriber';\n\n/**\n * Metrics class for tracking business metrics in OpenTelemetry\n *\n * Track critical business indicators such as:\n * - User events (signups, purchases, feature usage) as metrics\n * - Conversion funnels (signup → activation → purchase)\n * - Business outcomes (success/failure rates)\n * - Value metrics (revenue, counts, etc.)\n *\n * All metrics are sent to OpenTelemetry (OTLP/Prometheus/Grafana).\n */\n/**\n * Metric configuration for customizing metric names and descriptions\n */\nexport interface MetricConfig {\n /** Metric name (e.g., 'metrics.events' or 'custom.events') */\n name?: string;\n /** Metric description */\n description?: string;\n /** Metric unit (default: '1') */\n unit?: string;\n}\n\n/**\n * Metrics options\n */\nexport interface MetricsOptions {\n /** Optional logger for audit trail */\n logger?: Logger;\n /** Optional collector for testing (captures metrics in memory) */\n collector?: MetricsCollector;\n\n /**\n * Namespace for metrics (default: 'metrics')\n * Results in metrics like: {serviceName}.{namespace}.events\n */\n namespace?: string;\n\n /**\n * Custom metric configurations\n * Override metric names, descriptions, and units\n */\n metrics?: {\n events?: MetricConfig;\n funnel?: MetricConfig;\n outcomes?: MetricConfig;\n value?: MetricConfig;\n };\n}\n\nexport class Metric {\n private serviceName: string;\n private eventCounter: Counter;\n private funnelCounter: Counter;\n private outcomeCounter: Counter;\n private valueHistogram: Histogram;\n private logger?: Logger;\n private collector?: MetricsCollector;\n\n /**\n * Create a new Metrics instance\n *\n * @param serviceName - Service name for metric namespacing\n * @param options - Optional configuration (logger, collector, namespace, metrics)\n *\n * @example Basic usage (default 'metrics' namespace)\n * ```typescript\n * const metrics = new Metric('checkout');\n * // Creates: checkout.metrics.events, checkout.metrics.funnel, etc.\n * ```\n *\n * @example Custom namespace\n * ```typescript\n * const metrics = new Metric('api', { namespace: 'business' });\n * // Creates: api.business.events, api.business.funnel, etc.\n * ```\n *\n * @example Custom metric names and descriptions\n * ```typescript\n * const metrics = new Metric('payments', {\n * metrics: {\n * outcomes: {\n * name: 'payments.transactions',\n * description: 'Payment transaction outcomes',\n * unit: 'transactions'\n * },\n * value: {\n * name: 'payments.revenue',\n * description: 'Payment revenue in USD',\n * unit: 'USD'\n * }\n * }\n * });\n * ```\n */\n constructor(serviceName: string, options: MetricsOptions = {}) {\n this.serviceName = serviceName;\n this.logger = options.logger;\n this.collector = options.collector;\n\n const config = getConfig();\n const meter = config.meter;\n\n // Default namespace and metric configurations\n const namespace = options.namespace || 'metrics';\n const metricsConfig = options.metrics || {};\n\n // Event counter configuration\n const eventsConfig = metricsConfig.events || {};\n this.eventCounter = meter.createCounter(\n eventsConfig.name || `${serviceName}.${namespace}.events`,\n {\n description: eventsConfig.description || 'Count of business events',\n unit: eventsConfig.unit || '1',\n },\n );\n\n // Funnel counter configuration\n const funnelConfig = metricsConfig.funnel || {};\n this.funnelCounter = meter.createCounter(\n funnelConfig.name || `${serviceName}.${namespace}.funnel`,\n {\n description: funnelConfig.description || 'Conversion funnel tracking',\n unit: funnelConfig.unit || '1',\n },\n );\n\n // Outcome counter configuration\n const outcomesConfig = metricsConfig.outcomes || {};\n this.outcomeCounter = meter.createCounter(\n outcomesConfig.name || `${serviceName}.${namespace}.outcomes`,\n {\n description:\n outcomesConfig.description || 'Outcome tracking (success/failure)',\n unit: outcomesConfig.unit || '1',\n },\n );\n\n // Value histogram configuration\n const valueConfig = metricsConfig.value || {};\n this.valueHistogram = meter.createHistogram(\n valueConfig.name || `${serviceName}.${namespace}.value`,\n {\n description:\n valueConfig.description || 'Value metrics (revenue, counts, etc.)',\n unit: valueConfig.unit || '1',\n },\n );\n }\n\n /**\n * Track a business event as a metric\n *\n * Use this for tracking user actions, business events, product usage as metrics:\n * - \"user.signup\"\n * - \"order.completed\"\n * - \"feature.used\"\n *\n * @example\n * ```typescript\n * // Track user signup as metric\n * metrics.trackEvent('user.signup', {\n * userId: '123',\n * plan: 'pro'\n * })\n *\n * // Track order as metric\n * metrics.trackEvent('order.completed', {\n * orderId: 'ord_123',\n * amount: 99.99\n * })\n * ```\n */\n trackEvent(eventName: string, attributes?: EventAttributes): void {\n const attrs: Attributes = {\n service: this.serviceName,\n event: eventName,\n ...attributes,\n };\n\n this.eventCounter.add(1, attrs);\n\n this.logger?.info('Metric event tracked', {\n event: eventName,\n attributes,\n });\n\n // Record for testing\n this.collector?.recordEvent({\n event: eventName,\n attributes,\n service: this.serviceName,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Track conversion funnel steps as metrics\n *\n * Monitor where users drop off in multi-step processes.\n *\n * @example\n * ```typescript\n * // Track signup funnel\n * metrics.trackFunnelStep('signup', 'started', { userId: '123' })\n * metrics.trackFunnelStep('signup', 'email_verified', { userId: '123' })\n * metrics.trackFunnelStep('signup', 'completed', { userId: '123' })\n *\n * // Track checkout flow\n * metrics.trackFunnelStep('checkout', 'started', { cartValue: 99.99 })\n * metrics.trackFunnelStep('checkout', 'payment_info', { cartValue: 99.99 })\n * metrics.trackFunnelStep('checkout', 'completed', { cartValue: 99.99 })\n * ```\n */\n trackFunnelStep(\n funnelName: string,\n status: FunnelStatus,\n attributes?: EventAttributes,\n ): void {\n const attrs: Attributes = {\n service: this.serviceName,\n funnel: funnelName,\n status,\n ...attributes,\n };\n\n this.funnelCounter.add(1, attrs);\n\n this.logger?.info('Funnel step tracked', {\n funnel: funnelName,\n status,\n attributes,\n });\n\n // Record for testing\n this.collector?.recordFunnelStep({\n funnel: funnelName,\n status,\n attributes,\n service: this.serviceName,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Track outcomes (success/failure/partial) as metrics\n *\n * Monitor success rates of critical operations.\n *\n * @example\n * ```typescript\n * // Track email delivery\n * metrics.trackOutcome('email.delivery', 'success', {\n * recipientType: 'user',\n * emailType: 'welcome'\n * })\n *\n * metrics.trackOutcome('email.delivery', 'failure', {\n * recipientType: 'user',\n * errorCode: 'invalid_email'\n * })\n *\n * // Track payment processing\n * metrics.trackOutcome('payment.process', 'success', { amount: 99.99 })\n * metrics.trackOutcome('payment.process', 'failure', { error: 'insufficient_funds' })\n * ```\n */\n trackOutcome(\n operationName: string,\n status: OutcomeStatus,\n attributes?: EventAttributes,\n ): void {\n const attrs: Attributes = {\n service: this.serviceName,\n operation: operationName,\n status,\n ...attributes,\n };\n\n this.outcomeCounter.add(1, attrs);\n\n this.logger?.info('Outcome tracked', {\n operation: operationName,\n status,\n attributes,\n });\n\n // Record for testing\n this.collector?.recordOutcome({\n operation: operationName,\n status,\n attributes,\n service: this.serviceName,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Track value metrics\n *\n * Record numerical values like revenue, transaction amounts,\n * item counts, processing times, engagement scores, etc.\n *\n * @example\n * ```typescript\n * // Track revenue\n * metrics.trackValue('order.revenue', 149.99, {\n * currency: 'USD',\n * productCategory: 'electronics'\n * })\n *\n * // Track items per cart\n * metrics.trackValue('cart.item_count', 5, {\n * userId: '123'\n * })\n *\n * // Track processing time\n * metrics.trackValue('api.response_time', 250, {\n * unit: 'ms',\n * endpoint: '/api/checkout'\n * })\n * ```\n */\n trackValue(\n metricName: string,\n value: number,\n attributes?: EventAttributes,\n ): void {\n const attrs: Attributes = {\n service: this.serviceName,\n metric: metricName,\n ...attributes,\n };\n\n this.valueHistogram.record(value, attrs);\n\n this.logger?.debug('Value metric tracked', {\n metric: metricName,\n value,\n attributes,\n });\n\n // Record for testing\n this.collector?.recordValue({\n metric: metricName,\n value,\n attributes,\n service: this.serviceName,\n timestamp: Date.now(),\n });\n }\n}\n\n/**\n * Global metrics instances (singleton pattern)\n */\nconst metricsInstances = new Map<string, Metric>();\n\n/**\n * Get or create a Metrics instance for a service\n *\n * @param serviceName - Service name for metric namespacing\n * @param logger - Optional logger\n * @returns Metrics instance\n *\n * @example\n * ```typescript\n * const metrics = getMetrics('checkout')\n * metrics.trackEvent('order.completed', { orderId: '123' })\n * ```\n */\nexport function getMetrics(serviceName: string, logger?: Logger): Metric {\n if (!metricsInstances.has(serviceName)) {\n metricsInstances.set(serviceName, new Metric(serviceName, { logger }));\n }\n return metricsInstances.get(serviceName)!;\n}\n\n/**\n * Reset all metrics instances (mainly for testing)\n */\nexport function resetMetrics(): void {\n metricsInstances.clear();\n}\n"]}
|