autotel 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/attribute-redacting-processor.test.ts +0 -763
- package/src/attribute-redacting-processor.ts +0 -621
- package/src/attributes/attachers.ts +0 -161
- package/src/attributes/builders.ts +0 -529
- package/src/attributes/domains.ts +0 -42
- package/src/attributes/index.ts +0 -81
- package/src/attributes/registry.ts +0 -323
- package/src/attributes/types.ts +0 -211
- package/src/attributes/utils.ts +0 -64
- package/src/attributes/validators.ts +0 -266
- package/src/attributes.test.ts +0 -292
- package/src/auto.ts +0 -67
- package/src/autotel-logger.test.ts +0 -548
- package/src/autotel-logger.ts +0 -364
- package/src/baggage-span-processor.test.ts +0 -202
- package/src/baggage-span-processor.ts +0 -100
- package/src/business-baggage.test.ts +0 -500
- package/src/business-baggage.ts +0 -669
- package/src/circuit-breaker.test.ts +0 -341
- package/src/circuit-breaker.ts +0 -184
- package/src/config.test.ts +0 -94
- package/src/config.ts +0 -172
- package/src/correlated-events.test.ts +0 -151
- package/src/correlated-events.ts +0 -47
- package/src/correlation-id.test.ts +0 -163
- package/src/correlation-id.ts +0 -206
- package/src/db.test.ts +0 -252
- package/src/db.ts +0 -447
- package/src/decorators.test.ts +0 -153
- package/src/decorators.ts +0 -188
- package/src/define-event.test.ts +0 -41
- package/src/define-event.ts +0 -58
- package/src/devtools.ts +0 -60
- package/src/drain-pipeline.test.ts +0 -68
- package/src/drain-pipeline.ts +0 -199
- package/src/drain-toolkit.test.ts +0 -113
- package/src/drain-toolkit.ts +0 -129
- package/src/enricher-toolkit.test.ts +0 -67
- package/src/enricher-toolkit.ts +0 -79
- package/src/enrichers.test.ts +0 -150
- package/src/enrichers.ts +0 -145
- package/src/env-config.test.ts +0 -323
- package/src/env-config.ts +0 -309
- package/src/error-catalog.test.ts +0 -133
- package/src/error-catalog.ts +0 -262
- package/src/event-queue.test.ts +0 -864
- package/src/event-queue.ts +0 -699
- package/src/event-subscriber.ts +0 -262
- package/src/event-testing.ts +0 -197
- package/src/event.test.ts +0 -1104
- package/src/event.ts +0 -988
- package/src/events-config.ts +0 -235
- package/src/exporters.ts +0 -165
- package/src/filtering-span-processor.test.ts +0 -281
- package/src/filtering-span-processor.ts +0 -111
- package/src/flatten-attributes.test.ts +0 -76
- package/src/flatten-attributes.ts +0 -80
- package/src/functional.strict-types.typecheck.ts +0 -53
- package/src/functional.test.ts +0 -1464
- package/src/functional.ts +0 -2539
- package/src/functional.types.test.ts +0 -135
- package/src/hook.mjs +0 -15
- package/src/http.test.ts +0 -485
- package/src/http.ts +0 -424
- package/src/index.ts +0 -433
- package/src/init-auto-redactor.test.ts +0 -53
- package/src/init-redactor.test.ts +0 -8
- package/src/init.customization.test.ts +0 -665
- package/src/init.integrations.test.ts +0 -399
- package/src/init.openllmetry.test.ts +0 -194
- package/src/init.protocol.test.ts +0 -215
- package/src/init.ts +0 -2439
- package/src/instrumentation.test.ts +0 -108
- package/src/instrumentation.ts +0 -319
- package/src/logger.test.ts +0 -125
- package/src/logger.ts +0 -341
- package/src/messaging-adapters.test.ts +0 -595
- package/src/messaging-adapters.ts +0 -583
- package/src/messaging-testing.test.ts +0 -573
- package/src/messaging-testing.ts +0 -935
- package/src/messaging.test.ts +0 -1646
- package/src/messaging.ts +0 -2245
- package/src/metric-helpers.ts +0 -47
- package/src/metric-testing.ts +0 -197
- package/src/metric.ts +0 -446
- package/src/metrics.test.ts +0 -241
- package/src/node-require.ts +0 -123
- package/src/operation-context.ts +0 -93
- package/src/parse-error.test.ts +0 -73
- package/src/parse-error.ts +0 -112
- package/src/posthog-logs.test.ts +0 -115
- package/src/posthog-logs.ts +0 -77
- package/src/pretty-console-exporter.test.ts +0 -545
- package/src/pretty-console-exporter.ts +0 -413
- package/src/pretty-log-formatter.test.ts +0 -123
- package/src/pretty-log-formatter.ts +0 -210
- package/src/processors/canonical-log-line-processor.test.ts +0 -523
- package/src/processors/canonical-log-line-processor.ts +0 -396
- package/src/processors.ts +0 -152
- package/src/rate-limiter.test.ts +0 -199
- package/src/rate-limiter.ts +0 -98
- package/src/redact-values.test.ts +0 -90
- package/src/redact-values.ts +0 -34
- package/src/register.ts +0 -37
- package/src/request-logger.test.ts +0 -545
- package/src/request-logger.ts +0 -342
- package/src/sampling.test.ts +0 -1060
- package/src/sampling.ts +0 -737
- package/src/security-schema.test.ts +0 -45
- package/src/security-schema.ts +0 -107
- package/src/semantic-conventions.ts +0 -15
- package/src/semantic-helpers.test.ts +0 -226
- package/src/semantic-helpers.ts +0 -438
- package/src/shutdown.test.ts +0 -364
- package/src/shutdown.ts +0 -246
- package/src/span-name-normalizer.test.ts +0 -377
- package/src/span-name-normalizer.ts +0 -213
- package/src/stable-hash.ts +0 -27
- package/src/structured-error.test.ts +0 -191
- package/src/structured-error.ts +0 -157
- package/src/stub.integration.test.ts +0 -361
- package/src/tail-sampling-processor.test.ts +0 -230
- package/src/tail-sampling-processor.ts +0 -55
- package/src/test-span-collector.test.ts +0 -234
- package/src/test-span-collector.ts +0 -150
- package/src/testing.ts +0 -705
- package/src/trace-context.test.ts +0 -73
- package/src/trace-context.ts +0 -567
- package/src/trace-helpers.new.test.ts +0 -278
- package/src/trace-helpers.test.ts +0 -290
- package/src/trace-helpers.ts +0 -710
- package/src/trace-hybrid.test.ts +0 -42
- package/src/trace-hybrid.ts +0 -37
- package/src/tracer-provider.test.ts +0 -183
- package/src/tracer-provider.ts +0 -266
- package/src/track.test.ts +0 -154
- package/src/track.ts +0 -216
- package/src/validate.test.ts +0 -287
- package/src/validate.ts +0 -307
- package/src/validation-attributes.ts +0 -43
- package/src/validation.test.ts +0 -330
- package/src/validation.ts +0 -246
- package/src/variable-name-inference.test.ts +0 -178
- package/src/variable-name-inference.ts +0 -242
- package/src/webhook.test.ts +0 -649
- package/src/webhook.ts +0 -637
- package/src/workflow-distributed.test.ts +0 -786
- package/src/workflow-distributed.ts +0 -916
- package/src/workflow.async-safety.integration.test.ts +0 -345
- package/src/workflow.test.ts +0 -647
- package/src/workflow.ts +0 -810
- package/src/yaml-config.test.ts +0 -373
- package/src/yaml-config.ts +0 -351
package/src/track.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global track() function for business events
|
|
3
|
-
*
|
|
4
|
-
* Simple, no instantiation needed, auto-attaches trace context
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { trace } from '@opentelemetry/api';
|
|
8
|
-
import { EventQueue } from './event-queue';
|
|
9
|
-
import {
|
|
10
|
-
getConfig,
|
|
11
|
-
warnIfNotInitialized,
|
|
12
|
-
isInitialized,
|
|
13
|
-
getValidationConfig,
|
|
14
|
-
getEventsConfig,
|
|
15
|
-
} from './init';
|
|
16
|
-
import { validateEvent } from './validation';
|
|
17
|
-
import { getOrCreateCorrelationId } from './correlation-id';
|
|
18
|
-
import type { EventTrackingOptions } from './event-subscriber';
|
|
19
|
-
import type { AutotelEventContext } from './event-subscriber';
|
|
20
|
-
|
|
21
|
-
// Global events queue (initialized on first track call)
|
|
22
|
-
let eventsQueue: EventQueue | null = null;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Build autotel event context for trace correlation
|
|
26
|
-
*
|
|
27
|
-
* Works in multiple contexts:
|
|
28
|
-
* 1. Inside a span → use current span's trace_id + span_id
|
|
29
|
-
* 2. Outside span → use correlation_id only
|
|
30
|
-
* 3. With trace URL config → include clickable trace URL
|
|
31
|
-
*/
|
|
32
|
-
function buildAutotelContext(
|
|
33
|
-
span: ReturnType<typeof trace.getActiveSpan>,
|
|
34
|
-
): AutotelEventContext | undefined {
|
|
35
|
-
const eventsConfig = getEventsConfig();
|
|
36
|
-
const config = getConfig();
|
|
37
|
-
|
|
38
|
-
// Always generate a correlation_id
|
|
39
|
-
const correlationId = getOrCreateCorrelationId();
|
|
40
|
-
|
|
41
|
-
// Return minimal context if trace context is not enabled
|
|
42
|
-
if (!eventsConfig?.includeTraceContext) {
|
|
43
|
-
return {
|
|
44
|
-
correlation_id: correlationId,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Build base context
|
|
49
|
-
const autotelContext: AutotelEventContext = {
|
|
50
|
-
correlation_id: correlationId,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Add trace context if inside a span
|
|
54
|
-
const spanContext = span?.spanContext();
|
|
55
|
-
if (spanContext) {
|
|
56
|
-
autotelContext.trace_id = spanContext.traceId;
|
|
57
|
-
autotelContext.span_id = spanContext.spanId;
|
|
58
|
-
|
|
59
|
-
// Trace flags as 2-char hex string (canonical format)
|
|
60
|
-
autotelContext.trace_flags = spanContext.traceFlags
|
|
61
|
-
.toString(16)
|
|
62
|
-
.padStart(2, '0');
|
|
63
|
-
|
|
64
|
-
// Tracestate if present
|
|
65
|
-
// Defensive: serialize() is standard OTel API but may be missing in some runtimes
|
|
66
|
-
const traceState = spanContext.traceState;
|
|
67
|
-
if (traceState) {
|
|
68
|
-
try {
|
|
69
|
-
if (typeof traceState.serialize === 'function') {
|
|
70
|
-
const traceStateStr = traceState.serialize();
|
|
71
|
-
if (traceStateStr) {
|
|
72
|
-
autotelContext.trace_state = traceStateStr;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} catch {
|
|
76
|
-
// Silently ignore serialization errors
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Generate trace URL if configured
|
|
81
|
-
if (eventsConfig.traceUrl && config) {
|
|
82
|
-
const traceUrl = eventsConfig.traceUrl({
|
|
83
|
-
traceId: spanContext.traceId,
|
|
84
|
-
spanId: spanContext.spanId,
|
|
85
|
-
correlationId,
|
|
86
|
-
serviceName: config.service,
|
|
87
|
-
environment: config.environment,
|
|
88
|
-
});
|
|
89
|
-
if (traceUrl) {
|
|
90
|
-
autotelContext.trace_url = traceUrl;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
// Outside span but may still have trace URL generator
|
|
95
|
-
if (eventsConfig.traceUrl && config) {
|
|
96
|
-
const traceUrl = eventsConfig.traceUrl({
|
|
97
|
-
correlationId,
|
|
98
|
-
serviceName: config.service,
|
|
99
|
-
environment: config.environment,
|
|
100
|
-
});
|
|
101
|
-
if (traceUrl) {
|
|
102
|
-
autotelContext.trace_url = traceUrl;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return autotelContext;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Initialize events queue lazily
|
|
112
|
-
*/
|
|
113
|
-
function getOrCreateQueue(): EventQueue | null {
|
|
114
|
-
if (!isInitialized()) {
|
|
115
|
-
warnIfNotInitialized('track()');
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!eventsQueue) {
|
|
120
|
-
const config = getConfig();
|
|
121
|
-
if (!config?.subscribers || config.subscribers.length === 0) {
|
|
122
|
-
// No subscribers configured - no-op
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
eventsQueue = new EventQueue(config.subscribers);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return eventsQueue;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Track a business events event
|
|
134
|
-
*
|
|
135
|
-
* Features:
|
|
136
|
-
* - Auto-attaches traceId and spanId if in active span
|
|
137
|
-
* - Batched sending with retry
|
|
138
|
-
* - Type-safe with optional generic
|
|
139
|
-
* - No-op if init() not called or no subscribers configured
|
|
140
|
-
*
|
|
141
|
-
* @example Basic usage
|
|
142
|
-
* ```typescript
|
|
143
|
-
* track('user.signup', { userId: '123', plan: 'pro' })
|
|
144
|
-
* ```
|
|
145
|
-
*
|
|
146
|
-
* @example With type safety
|
|
147
|
-
* ```typescript
|
|
148
|
-
* interface EventDatas {
|
|
149
|
-
* 'user.signup': { userId: string; plan: string }
|
|
150
|
-
* 'plan.upgraded': { userId: string; revenue: number }
|
|
151
|
-
* }
|
|
152
|
-
*
|
|
153
|
-
* track<EventDatas>('user.signup', { userId: '123', plan: 'pro' })
|
|
154
|
-
* ```
|
|
155
|
-
*
|
|
156
|
-
* @example Trace correlation (automatic)
|
|
157
|
-
* ```typescript
|
|
158
|
-
* @Instrumented()
|
|
159
|
-
* class UserService {
|
|
160
|
-
* async createUser(data: CreateUserData) {
|
|
161
|
-
* // This track call automatically includes traceId + spanId
|
|
162
|
-
* track('user.signup', { userId: data.id })
|
|
163
|
-
* }
|
|
164
|
-
* }
|
|
165
|
-
* ```
|
|
166
|
-
*/
|
|
167
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
-
export function track<Events extends Record<string, any> = Record<string, any>>(
|
|
169
|
-
event: keyof Events & string,
|
|
170
|
-
data?: Events[typeof event],
|
|
171
|
-
options?: EventTrackingOptions,
|
|
172
|
-
): void {
|
|
173
|
-
const queue = getOrCreateQueue();
|
|
174
|
-
if (!queue) return; // No-op if not initialized or no subscribers
|
|
175
|
-
|
|
176
|
-
// Validate and sanitize input (with custom config if provided)
|
|
177
|
-
const validationConfig = getValidationConfig();
|
|
178
|
-
const validated = validateEvent(event, data, validationConfig || undefined);
|
|
179
|
-
|
|
180
|
-
// Auto-attach trace context if available (free win!)
|
|
181
|
-
const span = trace.getActiveSpan();
|
|
182
|
-
const enrichedData = span
|
|
183
|
-
? {
|
|
184
|
-
...validated.attributes,
|
|
185
|
-
traceId: span.spanContext().traceId,
|
|
186
|
-
spanId: span.spanContext().spanId,
|
|
187
|
-
}
|
|
188
|
-
: validated.attributes;
|
|
189
|
-
|
|
190
|
-
// Build autotel context (same as Event class)
|
|
191
|
-
const autotelContext = buildAutotelContext(span);
|
|
192
|
-
|
|
193
|
-
queue.enqueue({
|
|
194
|
-
name: validated.eventName,
|
|
195
|
-
attributes: enrichedData,
|
|
196
|
-
timestamp: Date.now(),
|
|
197
|
-
autotel: autotelContext,
|
|
198
|
-
schema: options?.schema,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Get events queue (for flush/shutdown)
|
|
204
|
-
* @internal
|
|
205
|
-
*/
|
|
206
|
-
export function getEventQueue(): EventQueue | null {
|
|
207
|
-
return eventsQueue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Reset events queue (for shutdown/cleanup)
|
|
212
|
-
* @internal
|
|
213
|
-
*/
|
|
214
|
-
export function resetEventQueue(): void {
|
|
215
|
-
eventsQueue = null;
|
|
216
|
-
}
|
package/src/validate.test.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { trace } from '@opentelemetry/api';
|
|
3
|
-
|
|
4
|
-
const counterAdd = vi.hoisted(() => vi.fn());
|
|
5
|
-
vi.mock('./metric-helpers', () => ({
|
|
6
|
-
createCounter: () => ({ add: counterAdd }),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
defineValidator,
|
|
11
|
-
recordValidationMismatch,
|
|
12
|
-
formatValidationIssues,
|
|
13
|
-
onValidationMismatch,
|
|
14
|
-
type ValidationMismatch,
|
|
15
|
-
} from './validate';
|
|
16
|
-
import { VALIDATION_ATTR } from './validation-attributes';
|
|
17
|
-
|
|
18
|
-
/** A fake `SchemaLike` so tests don't depend on Zod. */
|
|
19
|
-
function schema<T>(
|
|
20
|
-
decide: (
|
|
21
|
-
input: unknown,
|
|
22
|
-
) => { success: true; data: T } | { success: false; error: unknown },
|
|
23
|
-
) {
|
|
24
|
-
return { safeParse: decide };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** A Zod-shaped error whose message/received embed a secret value. */
|
|
28
|
-
const SECRET = '123-45-6789';
|
|
29
|
-
const zodLikeError = {
|
|
30
|
-
issues: [
|
|
31
|
-
{
|
|
32
|
-
path: ['user', 'ssn'],
|
|
33
|
-
code: 'invalid_type',
|
|
34
|
-
expected: 'string',
|
|
35
|
-
received: SECRET, // value — must never escape
|
|
36
|
-
message: `Expected string, received ${SECRET}`, // value — must never escape
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
let setAttributes: ReturnType<typeof vi.fn>;
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
setAttributes = vi.fn();
|
|
45
|
-
vi.spyOn(trace, 'getActiveSpan').mockReturnValue({
|
|
46
|
-
setAttributes,
|
|
47
|
-
} as never);
|
|
48
|
-
counterAdd.mockClear();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
afterEach(() => {
|
|
52
|
-
vi.restoreAllMocks();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('formatValidationIssues — PII guard', () => {
|
|
56
|
-
it('keeps only path, code, and declared type — never values or messages', () => {
|
|
57
|
-
const issues = formatValidationIssues(zodLikeError);
|
|
58
|
-
expect(issues).toEqual([
|
|
59
|
-
{ path: 'user.ssn', code: 'invalid_type', expected: 'string' },
|
|
60
|
-
]);
|
|
61
|
-
// The secret must not appear anywhere in the serialized output.
|
|
62
|
-
expect(JSON.stringify(issues)).not.toContain(SECRET);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('handles a generic { errors: [...] } shape', () => {
|
|
66
|
-
const issues = formatValidationIssues({
|
|
67
|
-
errors: [{ path: ['a'], code: 'custom' }],
|
|
68
|
-
});
|
|
69
|
-
expect(issues).toEqual([{ path: 'a', code: 'custom' }]);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('returns [] for unrecognised errors', () => {
|
|
73
|
-
expect(formatValidationIssues(new Error('boom'))).toEqual([]);
|
|
74
|
-
expect(formatValidationIssues()).toEqual([]);
|
|
75
|
-
expect(formatValidationIssues('nope')).toEqual([]);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('defaults a missing code and root path', () => {
|
|
79
|
-
expect(formatValidationIssues({ issues: [{}] })).toEqual([
|
|
80
|
-
{ path: '', code: 'invalid' },
|
|
81
|
-
]);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('recordValidationMismatch', () => {
|
|
86
|
-
const mismatch: ValidationMismatch = {
|
|
87
|
-
name: 'POST /orders',
|
|
88
|
-
boundary: 'http',
|
|
89
|
-
mode: 'reject',
|
|
90
|
-
issues: [
|
|
91
|
-
{ path: 'a', code: 'invalid_type' },
|
|
92
|
-
{ path: 'b', code: 'too_small' },
|
|
93
|
-
{ path: 'c', code: 'invalid_type' },
|
|
94
|
-
],
|
|
95
|
-
hash: 'abc123',
|
|
96
|
-
severity: 'warning',
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
it('sets validation.* attributes on the active span', () => {
|
|
100
|
-
recordValidationMismatch(mismatch);
|
|
101
|
-
expect(setAttributes).toHaveBeenCalledWith(
|
|
102
|
-
expect.objectContaining({
|
|
103
|
-
[VALIDATION_ATTR.name]: 'POST /orders',
|
|
104
|
-
[VALIDATION_ATTR.boundary]: 'http',
|
|
105
|
-
[VALIDATION_ATTR.mode]: 'reject',
|
|
106
|
-
[VALIDATION_ATTR.issueCount]: 3,
|
|
107
|
-
[VALIDATION_ATTR.issuePaths]: 'a,b,c',
|
|
108
|
-
[VALIDATION_ATTR.issueCodes]: 'invalid_type,too_small', // deduped
|
|
109
|
-
[VALIDATION_ATTR.hash]: 'abc123',
|
|
110
|
-
[VALIDATION_ATTR.severity]: 'warning',
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('increments the mismatch counter with boundary/validation/mode labels', () => {
|
|
116
|
-
recordValidationMismatch(mismatch);
|
|
117
|
-
expect(counterAdd).toHaveBeenCalledWith(1, {
|
|
118
|
-
boundary: 'http',
|
|
119
|
-
validation: 'POST /orders',
|
|
120
|
-
mode: 'reject',
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('skips span attributes when there is no active span (fail-open)', () => {
|
|
125
|
-
vi.spyOn(trace, 'getActiveSpan').mockReturnValue();
|
|
126
|
-
expect(() => recordValidationMismatch(mismatch)).not.toThrow();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('never throws even if the span sink throws', () => {
|
|
130
|
-
setAttributes.mockImplementation(() => {
|
|
131
|
-
throw new Error('span boom');
|
|
132
|
-
});
|
|
133
|
-
expect(() => recordValidationMismatch(mismatch)).not.toThrow();
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('onValidationMismatch', () => {
|
|
138
|
-
const mismatch = (name: string): ValidationMismatch => ({
|
|
139
|
-
name,
|
|
140
|
-
boundary: 'event',
|
|
141
|
-
mode: 'observe',
|
|
142
|
-
issues: [],
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// The listener registry is module-global; track every unsubscribe so a test
|
|
146
|
-
// can't leak a subscriber into the next one.
|
|
147
|
-
const cleanups: Array<() => void> = [];
|
|
148
|
-
const register = (handler: (m: ValidationMismatch) => void) => {
|
|
149
|
-
const off = onValidationMismatch(handler);
|
|
150
|
-
cleanups.push(off);
|
|
151
|
-
return off;
|
|
152
|
-
};
|
|
153
|
-
afterEach(() => {
|
|
154
|
-
while (cleanups.length > 0) cleanups.pop()!();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('invokes a registered listener and can unsubscribe', () => {
|
|
158
|
-
const seen: ValidationMismatch[] = [];
|
|
159
|
-
const off = register((m) => seen.push(m));
|
|
160
|
-
recordValidationMismatch(mismatch('x'));
|
|
161
|
-
expect(seen).toHaveLength(1);
|
|
162
|
-
off();
|
|
163
|
-
recordValidationMismatch(mismatch('y'));
|
|
164
|
-
expect(seen).toHaveLength(1); // not called after unsubscribe
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('delivers each mismatch to every simultaneous subscriber', () => {
|
|
168
|
-
// The real case: autotel-audit registers a security bridge while the app
|
|
169
|
-
// registers its own webhook/logger — both must fire.
|
|
170
|
-
const audit: string[] = [];
|
|
171
|
-
const webhook: string[] = [];
|
|
172
|
-
register((m) => audit.push(m.name));
|
|
173
|
-
register((m) => webhook.push(m.name));
|
|
174
|
-
|
|
175
|
-
recordValidationMismatch(mismatch('POST /login'));
|
|
176
|
-
|
|
177
|
-
expect(audit).toEqual(['POST /login']);
|
|
178
|
-
expect(webhook).toEqual(['POST /login']);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('unsubscribes each subscriber independently', () => {
|
|
182
|
-
const a: string[] = [];
|
|
183
|
-
const b: string[] = [];
|
|
184
|
-
const offA = register((m) => a.push(m.name));
|
|
185
|
-
register((m) => b.push(m.name));
|
|
186
|
-
|
|
187
|
-
recordValidationMismatch(mismatch('first'));
|
|
188
|
-
offA(); // remove only A
|
|
189
|
-
recordValidationMismatch(mismatch('second'));
|
|
190
|
-
|
|
191
|
-
expect(a).toEqual(['first']); // A stopped after unsubscribe
|
|
192
|
-
expect(b).toEqual(['first', 'second']); // B keeps firing
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('isolates faults: a throwing subscriber neither throws nor starves peers', () => {
|
|
196
|
-
const survivor: string[] = [];
|
|
197
|
-
register(() => {
|
|
198
|
-
throw new Error('subscriber boom');
|
|
199
|
-
});
|
|
200
|
-
register((m) => survivor.push(m.name));
|
|
201
|
-
|
|
202
|
-
expect(() => recordValidationMismatch(mismatch('z'))).not.toThrow();
|
|
203
|
-
expect(survivor).toEqual(['z']); // the healthy subscriber still fired
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('treats a re-registered identical handler as a single subscription', () => {
|
|
207
|
-
const seen: string[] = [];
|
|
208
|
-
const handler = (m: ValidationMismatch) => seen.push(m.name);
|
|
209
|
-
register(handler);
|
|
210
|
-
register(handler); // Set semantics → still one
|
|
211
|
-
recordValidationMismatch(mismatch('once'));
|
|
212
|
-
expect(seen).toEqual(['once']);
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe('defineValidator', () => {
|
|
217
|
-
const ok = schema<{ a: number }>(() => ({ success: true, data: { a: 1 } }));
|
|
218
|
-
const bad = schema<{ a: number }>(() => ({
|
|
219
|
-
success: false,
|
|
220
|
-
error: zodLikeError,
|
|
221
|
-
}));
|
|
222
|
-
|
|
223
|
-
it('reject mode (default): records then throws a 400 structured error', () => {
|
|
224
|
-
const v = defineValidator('POST /orders', bad, { boundary: 'http' });
|
|
225
|
-
expect(v.mode).toBe('reject');
|
|
226
|
-
try {
|
|
227
|
-
v.parse({ user: { ssn: SECRET } });
|
|
228
|
-
throw new Error('should have thrown');
|
|
229
|
-
} catch (error) {
|
|
230
|
-
const e = error as { status?: number; code?: string; message: string };
|
|
231
|
-
expect(e.status).toBe(400);
|
|
232
|
-
expect(e.code).toBe('validation_failed');
|
|
233
|
-
// even the thrown error must not leak the value
|
|
234
|
-
expect(JSON.stringify({ m: e.message })).not.toContain(SECRET);
|
|
235
|
-
}
|
|
236
|
-
expect(setAttributes).toHaveBeenCalled();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('observe mode: records then returns the raw input (no throw)', () => {
|
|
240
|
-
const v = defineValidator('order.placed', bad, {
|
|
241
|
-
boundary: 'event',
|
|
242
|
-
onMismatch: 'observe',
|
|
243
|
-
});
|
|
244
|
-
const raw = { user: { ssn: SECRET } };
|
|
245
|
-
expect(v.parse(raw)).toBe(raw);
|
|
246
|
-
expect(setAttributes).toHaveBeenCalledWith(
|
|
247
|
-
expect.objectContaining({ [VALIDATION_ATTR.mode]: 'observe' }),
|
|
248
|
-
);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('returns parsed data and records nothing on success', () => {
|
|
252
|
-
const v = defineValidator('ok', ok);
|
|
253
|
-
expect(v.parse({})).toEqual({ a: 1 });
|
|
254
|
-
expect(setAttributes).not.toHaveBeenCalled();
|
|
255
|
-
expect(counterAdd).not.toHaveBeenCalled();
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('safeParse returns a discriminated result and never throws', () => {
|
|
259
|
-
const v = defineValidator('POST /orders', bad);
|
|
260
|
-
const result = v.safeParse({});
|
|
261
|
-
expect(result.success).toBe(false);
|
|
262
|
-
if (!result.success) {
|
|
263
|
-
expect(result.issues[0]).toEqual({
|
|
264
|
-
path: 'user.ssn',
|
|
265
|
-
code: 'invalid_type',
|
|
266
|
-
expected: 'string',
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('honors a custom onReject error builder', () => {
|
|
272
|
-
const v = defineValidator('x', bad, {
|
|
273
|
-
onReject: () => new Error('custom reject'),
|
|
274
|
-
});
|
|
275
|
-
expect(() => v.parse({})).toThrow('custom reject');
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('emits a stable validation.hash when toJsonSchema is provided', () => {
|
|
279
|
-
const v = defineValidator('x', bad, {
|
|
280
|
-
toJsonSchema: () => ({ type: 'object' }),
|
|
281
|
-
});
|
|
282
|
-
v.safeParse({});
|
|
283
|
-
const attrs = setAttributes.mock.calls[0][0];
|
|
284
|
-
expect(typeof attrs[VALIDATION_ATTR.hash]).toBe('string');
|
|
285
|
-
expect(attrs[VALIDATION_ATTR.hash]).toHaveLength(64); // sha256 hex
|
|
286
|
-
});
|
|
287
|
-
});
|