@peac/telemetry 0.10.8 → 0.10.10
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 +1 -1
- package/dist/index.cjs +82 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +72 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +19 -9
- package/.turbo/turbo-build.log +0 -4
- package/dist/attributes.js +0 -67
- package/dist/attributes.js.map +0 -1
- package/dist/index.js +0 -55
- package/dist/index.js.map +0 -1
- package/dist/noop.js +0 -21
- package/dist/noop.js.map +0 -1
- package/dist/provider.js +0 -65
- package/dist/provider.js.map +0 -1
- package/dist/types.js +0 -9
- package/dist/types.js.map +0 -1
- package/src/attributes.ts +0 -71
- package/src/index.ts +0 -63
- package/src/noop.ts +0 -20
- package/src/provider.ts +0 -64
- package/src/types.ts +0 -163
- package/tests/attributes.test.ts +0 -126
- package/tests/noop.test.ts +0 -109
- package/tests/provider.test.ts +0 -223
- package/tests/types.test.ts +0 -218
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -8
package/src/provider.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @peac/telemetry - Provider registry
|
|
3
|
-
*
|
|
4
|
-
* Zero-throw provider ref pattern for hot-path performance.
|
|
5
|
-
* When undefined, telemetry is disabled with NO function calls.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { TelemetryProvider } from './types.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Singleton provider reference for zero-overhead hot path.
|
|
12
|
-
*
|
|
13
|
-
* When undefined, telemetry is disabled with NO function calls
|
|
14
|
-
* beyond the initial `if (!p)` check.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```typescript
|
|
18
|
-
* // In hot path (issue/verify)
|
|
19
|
-
* const p = providerRef.current;
|
|
20
|
-
* if (p) {
|
|
21
|
-
* try {
|
|
22
|
-
* p.onReceiptIssued({ receiptHash: '...' });
|
|
23
|
-
* } catch {
|
|
24
|
-
* // Telemetry MUST NOT break core flow
|
|
25
|
-
* }
|
|
26
|
-
* }
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export const providerRef: { current?: TelemetryProvider } = {
|
|
30
|
-
current: undefined,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Set the telemetry provider.
|
|
35
|
-
*
|
|
36
|
-
* Idempotent, no-throw, safe to call multiple times.
|
|
37
|
-
* Pass undefined to disable telemetry.
|
|
38
|
-
*
|
|
39
|
-
* @param provider - The provider to use, or undefined to disable
|
|
40
|
-
*/
|
|
41
|
-
export function setTelemetryProvider(provider: TelemetryProvider | undefined): void {
|
|
42
|
-
providerRef.current = provider;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Get the current telemetry provider.
|
|
47
|
-
*
|
|
48
|
-
* Returns undefined if no provider is set (telemetry disabled).
|
|
49
|
-
*
|
|
50
|
-
* For hot paths, prefer direct access to `providerRef.current`
|
|
51
|
-
* to avoid the function call overhead.
|
|
52
|
-
*/
|
|
53
|
-
export function getTelemetryProvider(): TelemetryProvider | undefined {
|
|
54
|
-
return providerRef.current;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Check if telemetry is enabled.
|
|
59
|
-
*
|
|
60
|
-
* Convenience function for conditional logic outside hot paths.
|
|
61
|
-
*/
|
|
62
|
-
export function isTelemetryEnabled(): boolean {
|
|
63
|
-
return providerRef.current !== undefined;
|
|
64
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @peac/telemetry - Telemetry types and interfaces
|
|
3
|
-
*
|
|
4
|
-
* This module defines the core telemetry interfaces for PEAC protocol.
|
|
5
|
-
* These are runtime-portable and have no external dependencies.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Telemetry decision outcome
|
|
10
|
-
*/
|
|
11
|
-
export type TelemetryDecision = 'allow' | 'deny' | 'unknown';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Privacy mode for telemetry emission
|
|
15
|
-
*
|
|
16
|
-
* - strict: Hash all identifiers, emit minimal data (production default)
|
|
17
|
-
* - balanced: Hash identifiers but include rail + amounts (debugging)
|
|
18
|
-
* - custom: Use allowlist-based filtering
|
|
19
|
-
*/
|
|
20
|
-
export type PrivacyMode = 'strict' | 'balanced' | 'custom';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Telemetry configuration
|
|
24
|
-
*/
|
|
25
|
-
export interface TelemetryConfig {
|
|
26
|
-
/** Service name for resource identification */
|
|
27
|
-
serviceName: string;
|
|
28
|
-
|
|
29
|
-
/** Privacy mode: strict (hashes only), balanced, custom */
|
|
30
|
-
privacyMode?: PrivacyMode;
|
|
31
|
-
|
|
32
|
-
/** Allowlist for custom mode - only these attribute keys are emitted */
|
|
33
|
-
allowAttributes?: string[];
|
|
34
|
-
|
|
35
|
-
/** Custom redaction hook for edge cases */
|
|
36
|
-
redact?: (attrs: Record<string, unknown>) => Record<string, unknown>;
|
|
37
|
-
|
|
38
|
-
/** Enable experimental GenAI semantic conventions (default: false) */
|
|
39
|
-
enableExperimentalGenAI?: boolean;
|
|
40
|
-
|
|
41
|
-
/** Salt for privacy-preserving hashing (should be configured per deployment) */
|
|
42
|
-
hashSalt?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Input for receipt issued telemetry event
|
|
47
|
-
*/
|
|
48
|
-
export interface ReceiptIssuedInput {
|
|
49
|
-
/** Hash of the receipt (never raw content) */
|
|
50
|
-
receiptHash: string;
|
|
51
|
-
|
|
52
|
-
/** Hash of the policy used */
|
|
53
|
-
policyHash?: string;
|
|
54
|
-
|
|
55
|
-
/** Issuer identifier (may be hashed based on privacy mode) */
|
|
56
|
-
issuer?: string;
|
|
57
|
-
|
|
58
|
-
/** Key ID used for signing */
|
|
59
|
-
kid?: string;
|
|
60
|
-
|
|
61
|
-
/** HTTP context (privacy-safe: path only, no query) */
|
|
62
|
-
http?: HttpContext;
|
|
63
|
-
|
|
64
|
-
/** Duration of issue operation in milliseconds */
|
|
65
|
-
durationMs?: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Input for receipt verified telemetry event
|
|
70
|
-
*/
|
|
71
|
-
export interface ReceiptVerifiedInput {
|
|
72
|
-
/** Hash of the receipt */
|
|
73
|
-
receiptHash: string;
|
|
74
|
-
|
|
75
|
-
/** Issuer identifier */
|
|
76
|
-
issuer?: string;
|
|
77
|
-
|
|
78
|
-
/** Key ID used */
|
|
79
|
-
kid?: string;
|
|
80
|
-
|
|
81
|
-
/** Whether verification succeeded */
|
|
82
|
-
valid: boolean;
|
|
83
|
-
|
|
84
|
-
/** Reason code if verification failed */
|
|
85
|
-
reasonCode?: string;
|
|
86
|
-
|
|
87
|
-
/** HTTP context */
|
|
88
|
-
http?: HttpContext;
|
|
89
|
-
|
|
90
|
-
/** Duration of verify operation in milliseconds */
|
|
91
|
-
durationMs?: number;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Input for access decision telemetry event
|
|
96
|
-
*/
|
|
97
|
-
export interface AccessDecisionInput {
|
|
98
|
-
/** Hash of the receipt (if present) */
|
|
99
|
-
receiptHash?: string;
|
|
100
|
-
|
|
101
|
-
/** Hash of the policy evaluated */
|
|
102
|
-
policyHash?: string;
|
|
103
|
-
|
|
104
|
-
/** Decision outcome */
|
|
105
|
-
decision: TelemetryDecision;
|
|
106
|
-
|
|
107
|
-
/** Reason code for the decision */
|
|
108
|
-
reasonCode?: string;
|
|
109
|
-
|
|
110
|
-
/** Payment context (balanced/custom mode only) */
|
|
111
|
-
payment?: PaymentContext;
|
|
112
|
-
|
|
113
|
-
/** HTTP context */
|
|
114
|
-
http?: HttpContext;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* HTTP context for telemetry (privacy-safe subset)
|
|
119
|
-
*/
|
|
120
|
-
export interface HttpContext {
|
|
121
|
-
/** HTTP method */
|
|
122
|
-
method?: string;
|
|
123
|
-
|
|
124
|
-
/** URL path (no query string) */
|
|
125
|
-
path?: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Payment context for telemetry (balanced/custom mode only)
|
|
130
|
-
*/
|
|
131
|
-
export interface PaymentContext {
|
|
132
|
-
/** Payment rail identifier */
|
|
133
|
-
rail?: string;
|
|
134
|
-
|
|
135
|
-
/** Amount in minor units */
|
|
136
|
-
amount?: number;
|
|
137
|
-
|
|
138
|
-
/** Currency code */
|
|
139
|
-
currency?: string;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Telemetry provider interface
|
|
144
|
-
*
|
|
145
|
-
* Implementations SHOULD be no-throw. PEAC guards all calls,
|
|
146
|
-
* but well-behaved providers should not throw.
|
|
147
|
-
*/
|
|
148
|
-
export interface TelemetryProvider {
|
|
149
|
-
/**
|
|
150
|
-
* Called when a receipt is issued
|
|
151
|
-
*/
|
|
152
|
-
onReceiptIssued(input: ReceiptIssuedInput): void;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Called when a receipt is verified
|
|
156
|
-
*/
|
|
157
|
-
onReceiptVerified(input: ReceiptVerifiedInput): void;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Called when an access decision is made
|
|
161
|
-
*/
|
|
162
|
-
onAccessDecision(input: AccessDecisionInput): void;
|
|
163
|
-
}
|
package/tests/attributes.test.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @peac/telemetry - Attribute constants tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
PEAC_ATTRS,
|
|
8
|
-
PEAC_EVENTS,
|
|
9
|
-
PEAC_METRICS,
|
|
10
|
-
TRACE_CONTEXT_EXTENSIONS,
|
|
11
|
-
} from '../src/attributes.js';
|
|
12
|
-
|
|
13
|
-
describe('PEAC_ATTRS', () => {
|
|
14
|
-
it('should define core attributes', () => {
|
|
15
|
-
expect(PEAC_ATTRS.VERSION).toBe('peac.version');
|
|
16
|
-
expect(PEAC_ATTRS.EVENT).toBe('peac.event');
|
|
17
|
-
expect(PEAC_ATTRS.RECEIPT_HASH).toBe('peac.receipt.hash');
|
|
18
|
-
expect(PEAC_ATTRS.POLICY_HASH).toBe('peac.policy.hash');
|
|
19
|
-
expect(PEAC_ATTRS.DECISION).toBe('peac.decision');
|
|
20
|
-
expect(PEAC_ATTRS.REASON_CODE).toBe('peac.reason_code');
|
|
21
|
-
expect(PEAC_ATTRS.ISSUER).toBe('peac.issuer');
|
|
22
|
-
expect(PEAC_ATTRS.ISSUER_HASH).toBe('peac.issuer_hash');
|
|
23
|
-
expect(PEAC_ATTRS.KID).toBe('peac.kid');
|
|
24
|
-
expect(PEAC_ATTRS.VALID).toBe('peac.valid');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should use stable OTel semconv for HTTP', () => {
|
|
28
|
-
// These are stable OTel semantic conventions
|
|
29
|
-
expect(PEAC_ATTRS.HTTP_METHOD).toBe('http.request.method');
|
|
30
|
-
expect(PEAC_ATTRS.HTTP_PATH).toBe('url.path');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should define HTTP hash attributes', () => {
|
|
34
|
-
expect(PEAC_ATTRS.HTTP_HOST_HASH).toBe('peac.http.host_hash');
|
|
35
|
-
expect(PEAC_ATTRS.HTTP_CLIENT_HASH).toBe('peac.http.client_hash');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should define payment attributes', () => {
|
|
39
|
-
expect(PEAC_ATTRS.PAYMENT_RAIL).toBe('peac.payment.rail');
|
|
40
|
-
expect(PEAC_ATTRS.PAYMENT_AMOUNT).toBe('peac.payment.amount');
|
|
41
|
-
expect(PEAC_ATTRS.PAYMENT_CURRENCY).toBe('peac.payment.currency');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should define duration attribute', () => {
|
|
45
|
-
expect(PEAC_ATTRS.DURATION_MS).toBe('peac.duration_ms');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should be readonly (const assertion)', () => {
|
|
49
|
-
// TypeScript ensures this at compile time
|
|
50
|
-
// Runtime test: object should be frozen-like
|
|
51
|
-
const keys = Object.keys(PEAC_ATTRS);
|
|
52
|
-
expect(keys.length).toBeGreaterThan(0);
|
|
53
|
-
|
|
54
|
-
// All values should be strings
|
|
55
|
-
for (const key of keys) {
|
|
56
|
-
expect(typeof PEAC_ATTRS[key as keyof typeof PEAC_ATTRS]).toBe('string');
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should use peac. prefix consistently', () => {
|
|
61
|
-
const peacPrefixed = Object.values(PEAC_ATTRS).filter((v) => v.startsWith('peac.'));
|
|
62
|
-
const otherPrefixed = Object.values(PEAC_ATTRS).filter((v) => !v.startsWith('peac.'));
|
|
63
|
-
|
|
64
|
-
// Most should be peac. prefixed
|
|
65
|
-
expect(peacPrefixed.length).toBeGreaterThan(otherPrefixed.length);
|
|
66
|
-
|
|
67
|
-
// HTTP should use standard OTel conventions
|
|
68
|
-
expect(otherPrefixed).toContain('http.request.method');
|
|
69
|
-
expect(otherPrefixed).toContain('url.path');
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('PEAC_EVENTS', () => {
|
|
74
|
-
it('should define all event names', () => {
|
|
75
|
-
expect(PEAC_EVENTS.RECEIPT_ISSUED).toBe('peac.receipt.issued');
|
|
76
|
-
expect(PEAC_EVENTS.RECEIPT_VERIFIED).toBe('peac.receipt.verified');
|
|
77
|
-
expect(PEAC_EVENTS.ACCESS_DECISION).toBe('peac.access.decision');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should use peac. prefix', () => {
|
|
81
|
-
for (const event of Object.values(PEAC_EVENTS)) {
|
|
82
|
-
expect(event.startsWith('peac.')).toBe(true);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('PEAC_METRICS', () => {
|
|
88
|
-
it('should define counter metrics', () => {
|
|
89
|
-
expect(PEAC_METRICS.RECEIPTS_ISSUED).toBe('peac.receipts.issued');
|
|
90
|
-
expect(PEAC_METRICS.RECEIPTS_VERIFIED).toBe('peac.receipts.verified');
|
|
91
|
-
expect(PEAC_METRICS.ACCESS_DECISIONS).toBe('peac.access.decisions');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should define histogram metrics', () => {
|
|
95
|
-
expect(PEAC_METRICS.ISSUE_DURATION).toBe('peac.issue.duration');
|
|
96
|
-
expect(PEAC_METRICS.VERIFY_DURATION).toBe('peac.verify.duration');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should use peac. prefix', () => {
|
|
100
|
-
for (const metric of Object.values(PEAC_METRICS)) {
|
|
101
|
-
expect(metric.startsWith('peac.')).toBe(true);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('TRACE_CONTEXT_EXTENSIONS', () => {
|
|
107
|
-
it('should use w3c/ namespace (vendor-neutral)', () => {
|
|
108
|
-
expect(TRACE_CONTEXT_EXTENSIONS.TRACEPARENT).toBe('w3c/traceparent');
|
|
109
|
-
expect(TRACE_CONTEXT_EXTENSIONS.TRACESTATE).toBe('w3c/tracestate');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should match extension key pattern', () => {
|
|
113
|
-
// Pattern: ^[a-z0-9_.-]+/[a-z0-9_.-]+$
|
|
114
|
-
const pattern = /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/;
|
|
115
|
-
|
|
116
|
-
expect(pattern.test(TRACE_CONTEXT_EXTENSIONS.TRACEPARENT)).toBe(true);
|
|
117
|
-
expect(pattern.test(TRACE_CONTEXT_EXTENSIONS.TRACESTATE)).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should NOT use io.opentelemetry namespace', () => {
|
|
121
|
-
// Vendor-neutral: w3c/ not io.opentelemetry/
|
|
122
|
-
for (const key of Object.values(TRACE_CONTEXT_EXTENSIONS)) {
|
|
123
|
-
expect(key.startsWith('io.opentelemetry')).toBe(false);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
});
|
package/tests/noop.test.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @peac/telemetry - No-op provider tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
-
import { noopProvider } from '../src/noop.js';
|
|
7
|
-
import type { TelemetryProvider } from '../src/types.js';
|
|
8
|
-
|
|
9
|
-
describe('noopProvider', () => {
|
|
10
|
-
it('should implement TelemetryProvider interface', () => {
|
|
11
|
-
const provider: TelemetryProvider = noopProvider;
|
|
12
|
-
|
|
13
|
-
expect(typeof provider.onReceiptIssued).toBe('function');
|
|
14
|
-
expect(typeof provider.onReceiptVerified).toBe('function');
|
|
15
|
-
expect(typeof provider.onAccessDecision).toBe('function');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should not throw on onReceiptIssued', () => {
|
|
19
|
-
expect(() =>
|
|
20
|
-
noopProvider.onReceiptIssued({
|
|
21
|
-
receiptHash: 'sha256:test',
|
|
22
|
-
})
|
|
23
|
-
).not.toThrow();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should not throw on onReceiptVerified', () => {
|
|
27
|
-
expect(() =>
|
|
28
|
-
noopProvider.onReceiptVerified({
|
|
29
|
-
receiptHash: 'sha256:test',
|
|
30
|
-
valid: true,
|
|
31
|
-
})
|
|
32
|
-
).not.toThrow();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should not throw on onAccessDecision', () => {
|
|
36
|
-
expect(() =>
|
|
37
|
-
noopProvider.onAccessDecision({
|
|
38
|
-
decision: 'allow',
|
|
39
|
-
})
|
|
40
|
-
).not.toThrow();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should return undefined from all methods', () => {
|
|
44
|
-
const issued = noopProvider.onReceiptIssued({
|
|
45
|
-
receiptHash: 'sha256:test',
|
|
46
|
-
});
|
|
47
|
-
const verified = noopProvider.onReceiptVerified({
|
|
48
|
-
receiptHash: 'sha256:test',
|
|
49
|
-
valid: true,
|
|
50
|
-
});
|
|
51
|
-
const decision = noopProvider.onAccessDecision({
|
|
52
|
-
decision: 'allow',
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(issued).toBeUndefined();
|
|
56
|
-
expect(verified).toBeUndefined();
|
|
57
|
-
expect(decision).toBeUndefined();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should handle complex input without errors', () => {
|
|
61
|
-
expect(() =>
|
|
62
|
-
noopProvider.onReceiptIssued({
|
|
63
|
-
receiptHash: 'sha256:complex',
|
|
64
|
-
policyHash: 'sha256:policy',
|
|
65
|
-
issuer: 'https://api.example.com',
|
|
66
|
-
kid: '2025-01-01T00:00:00Z',
|
|
67
|
-
http: { method: 'POST', path: '/api/v1/resource' },
|
|
68
|
-
durationMs: 150,
|
|
69
|
-
})
|
|
70
|
-
).not.toThrow();
|
|
71
|
-
|
|
72
|
-
expect(() =>
|
|
73
|
-
noopProvider.onReceiptVerified({
|
|
74
|
-
receiptHash: 'sha256:complex',
|
|
75
|
-
issuer: 'https://api.example.com',
|
|
76
|
-
kid: '2025-01-01',
|
|
77
|
-
valid: false,
|
|
78
|
-
reasonCode: 'SIGNATURE_INVALID',
|
|
79
|
-
http: { method: 'GET', path: '/verify' },
|
|
80
|
-
durationMs: 25,
|
|
81
|
-
})
|
|
82
|
-
).not.toThrow();
|
|
83
|
-
|
|
84
|
-
expect(() =>
|
|
85
|
-
noopProvider.onAccessDecision({
|
|
86
|
-
receiptHash: 'sha256:complex',
|
|
87
|
-
policyHash: 'sha256:policy',
|
|
88
|
-
decision: 'deny',
|
|
89
|
-
reasonCode: 'INSUFFICIENT_PAYMENT',
|
|
90
|
-
payment: { rail: 'stripe', amount: 500, currency: 'USD' },
|
|
91
|
-
http: { method: 'POST', path: '/protected' },
|
|
92
|
-
})
|
|
93
|
-
).not.toThrow();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should be usable as default provider', () => {
|
|
97
|
-
// Simulate setting as default
|
|
98
|
-
let currentProvider: TelemetryProvider | undefined = noopProvider;
|
|
99
|
-
|
|
100
|
-
// Should work without errors
|
|
101
|
-
currentProvider.onReceiptIssued({ receiptHash: 'sha256:test' });
|
|
102
|
-
currentProvider.onReceiptVerified({ receiptHash: 'sha256:test', valid: true });
|
|
103
|
-
currentProvider.onAccessDecision({ decision: 'allow' });
|
|
104
|
-
|
|
105
|
-
// Should be replaceable
|
|
106
|
-
currentProvider = undefined;
|
|
107
|
-
expect(currentProvider).toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
});
|
package/tests/provider.test.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @peac/telemetry - Provider registry tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
providerRef,
|
|
8
|
-
setTelemetryProvider,
|
|
9
|
-
getTelemetryProvider,
|
|
10
|
-
isTelemetryEnabled,
|
|
11
|
-
} from '../src/provider.js';
|
|
12
|
-
import { noopProvider } from '../src/noop.js';
|
|
13
|
-
import type { TelemetryProvider } from '../src/types.js';
|
|
14
|
-
|
|
15
|
-
describe('providerRef', () => {
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
// Reset provider before each test
|
|
18
|
-
providerRef.current = undefined;
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should start with undefined (disabled)', () => {
|
|
22
|
-
expect(providerRef.current).toBeUndefined();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should allow direct assignment', () => {
|
|
26
|
-
providerRef.current = noopProvider;
|
|
27
|
-
expect(providerRef.current).toBe(noopProvider);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should allow setting to undefined', () => {
|
|
31
|
-
providerRef.current = noopProvider;
|
|
32
|
-
providerRef.current = undefined;
|
|
33
|
-
expect(providerRef.current).toBeUndefined();
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('setTelemetryProvider', () => {
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
providerRef.current = undefined;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should set the provider', () => {
|
|
43
|
-
setTelemetryProvider(noopProvider);
|
|
44
|
-
expect(providerRef.current).toBe(noopProvider);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should allow undefined to disable', () => {
|
|
48
|
-
setTelemetryProvider(noopProvider);
|
|
49
|
-
setTelemetryProvider(undefined);
|
|
50
|
-
expect(providerRef.current).toBeUndefined();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should be idempotent', () => {
|
|
54
|
-
setTelemetryProvider(noopProvider);
|
|
55
|
-
setTelemetryProvider(noopProvider);
|
|
56
|
-
setTelemetryProvider(noopProvider);
|
|
57
|
-
expect(providerRef.current).toBe(noopProvider);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should allow replacing provider', () => {
|
|
61
|
-
const provider1: TelemetryProvider = {
|
|
62
|
-
onReceiptIssued: vi.fn(),
|
|
63
|
-
onReceiptVerified: vi.fn(),
|
|
64
|
-
onAccessDecision: vi.fn(),
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const provider2: TelemetryProvider = {
|
|
68
|
-
onReceiptIssued: vi.fn(),
|
|
69
|
-
onReceiptVerified: vi.fn(),
|
|
70
|
-
onAccessDecision: vi.fn(),
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
setTelemetryProvider(provider1);
|
|
74
|
-
expect(providerRef.current).toBe(provider1);
|
|
75
|
-
|
|
76
|
-
setTelemetryProvider(provider2);
|
|
77
|
-
expect(providerRef.current).toBe(provider2);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should not throw', () => {
|
|
81
|
-
expect(() => setTelemetryProvider(noopProvider)).not.toThrow();
|
|
82
|
-
expect(() => setTelemetryProvider(undefined)).not.toThrow();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('getTelemetryProvider', () => {
|
|
87
|
-
beforeEach(() => {
|
|
88
|
-
providerRef.current = undefined;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should return undefined when no provider set', () => {
|
|
92
|
-
expect(getTelemetryProvider()).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should return the current provider', () => {
|
|
96
|
-
setTelemetryProvider(noopProvider);
|
|
97
|
-
expect(getTelemetryProvider()).toBe(noopProvider);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should return same value as providerRef.current', () => {
|
|
101
|
-
setTelemetryProvider(noopProvider);
|
|
102
|
-
expect(getTelemetryProvider()).toBe(providerRef.current);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('isTelemetryEnabled', () => {
|
|
107
|
-
beforeEach(() => {
|
|
108
|
-
providerRef.current = undefined;
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should return false when disabled', () => {
|
|
112
|
-
expect(isTelemetryEnabled()).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should return true when provider set', () => {
|
|
116
|
-
setTelemetryProvider(noopProvider);
|
|
117
|
-
expect(isTelemetryEnabled()).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should return false after disabling', () => {
|
|
121
|
-
setTelemetryProvider(noopProvider);
|
|
122
|
-
setTelemetryProvider(undefined);
|
|
123
|
-
expect(isTelemetryEnabled()).toBe(false);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('hot path pattern', () => {
|
|
128
|
-
beforeEach(() => {
|
|
129
|
-
providerRef.current = undefined;
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should skip telemetry when disabled', () => {
|
|
133
|
-
const mockFn = vi.fn();
|
|
134
|
-
|
|
135
|
-
// Hot path pattern
|
|
136
|
-
const p = providerRef.current;
|
|
137
|
-
if (p) {
|
|
138
|
-
mockFn();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
expect(mockFn).not.toHaveBeenCalled();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('should call telemetry when enabled', () => {
|
|
145
|
-
const mockProvider: TelemetryProvider = {
|
|
146
|
-
onReceiptIssued: vi.fn(),
|
|
147
|
-
onReceiptVerified: vi.fn(),
|
|
148
|
-
onAccessDecision: vi.fn(),
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
setTelemetryProvider(mockProvider);
|
|
152
|
-
|
|
153
|
-
// Hot path pattern
|
|
154
|
-
const p = providerRef.current;
|
|
155
|
-
if (p) {
|
|
156
|
-
p.onReceiptIssued({ receiptHash: 'sha256:test' });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
expect(mockProvider.onReceiptIssued).toHaveBeenCalledWith({
|
|
160
|
-
receiptHash: 'sha256:test',
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should guard against throwing providers', () => {
|
|
165
|
-
const throwingProvider: TelemetryProvider = {
|
|
166
|
-
onReceiptIssued: () => {
|
|
167
|
-
throw new Error('Provider error');
|
|
168
|
-
},
|
|
169
|
-
onReceiptVerified: vi.fn(),
|
|
170
|
-
onAccessDecision: vi.fn(),
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
setTelemetryProvider(throwingProvider);
|
|
174
|
-
|
|
175
|
-
// Hot path pattern with guard
|
|
176
|
-
const p = providerRef.current;
|
|
177
|
-
if (p) {
|
|
178
|
-
try {
|
|
179
|
-
p.onReceiptIssued({ receiptHash: 'sha256:test' });
|
|
180
|
-
} catch {
|
|
181
|
-
// Telemetry MUST NOT break core flow - swallow silently
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Should not throw, test passes if we get here
|
|
186
|
-
expect(true).toBe(true);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('should have zero overhead when disabled (structural)', () => {
|
|
190
|
-
// This test documents the performance contract structurally
|
|
191
|
-
// When providerRef.current is undefined, no provider methods are called
|
|
192
|
-
// Time-based assertions are avoided to prevent CI flakes
|
|
193
|
-
|
|
194
|
-
const iterations = 1000;
|
|
195
|
-
let callCount = 0;
|
|
196
|
-
|
|
197
|
-
for (let i = 0; i < iterations; i++) {
|
|
198
|
-
const p = providerRef.current;
|
|
199
|
-
if (p) {
|
|
200
|
-
callCount++;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Structural assertion: no calls when disabled
|
|
205
|
-
expect(callCount).toBe(0);
|
|
206
|
-
// The pattern above is the only overhead (single truthiness check)
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe('concurrent access', () => {
|
|
211
|
-
beforeEach(() => {
|
|
212
|
-
providerRef.current = undefined;
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should handle rapid enable/disable', () => {
|
|
216
|
-
for (let i = 0; i < 100; i++) {
|
|
217
|
-
setTelemetryProvider(noopProvider);
|
|
218
|
-
expect(isTelemetryEnabled()).toBe(true);
|
|
219
|
-
setTelemetryProvider(undefined);
|
|
220
|
-
expect(isTelemetryEnabled()).toBe(false);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
});
|