autotel-tanstack 1.13.34 → 1.13.36

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/src/metrics.ts DELETED
@@ -1,184 +0,0 @@
1
- /**
2
- * Performance metrics collection for TanStack Start
3
- *
4
- * Provides utilities to collect and expose performance metrics
5
- * following the patterns from TanStack Start observability guide.
6
- */
7
-
8
- /**
9
- * Performance timing data
10
- */
11
- export interface TimingStats {
12
- count: number;
13
- avg: number;
14
- p50: number;
15
- p95: number;
16
- min: number;
17
- max: number;
18
- }
19
-
20
- /**
21
- * Metrics collector for performance data
22
- *
23
- * Collects timing metrics and provides statistical analysis.
24
- * Thread-safe for concurrent access.
25
- *
26
- * @example
27
- * ```typescript
28
- * import { metricsCollector } from 'autotel-tanstack/metrics';
29
- *
30
- * // Record a timing
31
- * metricsCollector.recordTiming('serverFn.getUser', 150);
32
- *
33
- * // Get stats
34
- * const stats = metricsCollector.getStats('serverFn.getUser');
35
- * console.log(`Average: ${stats.avg}ms, P95: ${stats.p95}ms`);
36
- * ```
37
- */
38
- class MetricsCollector {
39
- private metrics = new Map<string, number[]>();
40
- private readonly maxSamples = 1000; // Limit memory usage
41
-
42
- /**
43
- * Record a timing measurement
44
- */
45
- recordTiming(name: string, duration: number): void {
46
- if (!this.metrics.has(name)) {
47
- this.metrics.set(name, []);
48
- }
49
-
50
- const timings = this.metrics.get(name)!;
51
- timings.push(duration);
52
-
53
- // Limit samples to prevent memory issues
54
- if (timings.length > this.maxSamples) {
55
- timings.shift(); // Remove oldest
56
- }
57
- }
58
-
59
- /**
60
- * Get statistics for a metric
61
- */
62
- getStats(name: string): TimingStats | null {
63
- const timings = this.metrics.get(name);
64
- if (!timings || timings.length === 0) {
65
- return null;
66
- }
67
-
68
- const sorted = [...timings].toSorted((a, b) => a - b);
69
- const sum = timings.reduce((a, b) => a + b, 0);
70
-
71
- return {
72
- count: timings.length,
73
- avg: sum / timings.length,
74
- p50: sorted.at(Math.floor(sorted.length * 0.5)) ?? 0,
75
- p95: sorted.at(Math.floor(sorted.length * 0.95)) ?? 0,
76
- min: sorted[0] ?? 0,
77
- max: sorted.at(-1) ?? 0,
78
- };
79
- }
80
-
81
- /**
82
- * Get all collected metrics
83
- */
84
- getAllStats(): Record<string, TimingStats> {
85
- const stats: Record<string, TimingStats> = {};
86
- for (const [name] of this.metrics) {
87
- const stat = this.getStats(name);
88
- if (stat) {
89
- stats[name] = stat;
90
- }
91
- }
92
- return stats;
93
- }
94
-
95
- /**
96
- * Reset all metrics
97
- */
98
- reset(): void {
99
- this.metrics.clear();
100
- }
101
-
102
- /**
103
- * Reset a specific metric
104
- */
105
- resetMetric(name: string): void {
106
- this.metrics.delete(name);
107
- }
108
- }
109
-
110
- /**
111
- * Global metrics collector instance
112
- */
113
- export const metricsCollector = new MetricsCollector();
114
-
115
- /**
116
- * Helper to create a metrics endpoint handler
117
- *
118
- * Returns a handler that exposes metrics in JSON format.
119
- * Use this to create a `/metrics` endpoint.
120
- *
121
- * @example
122
- * ```typescript
123
- * // routes/metrics.ts
124
- * import { createFileRoute } from '@tanstack/react-router';
125
- * import { json } from '@tanstack/react-start';
126
- * import { createMetricsHandler } from 'autotel-tanstack/metrics';
127
- *
128
- * export const Route = createFileRoute('/metrics')({
129
- * server: {
130
- * handlers: {
131
- * GET: createMetricsHandler(),
132
- * },
133
- * },
134
- * });
135
- * ```
136
- */
137
- export function createMetricsHandler() {
138
- return async () => {
139
- const { json } = await import('@tanstack/react-start');
140
-
141
- return json({
142
- system: {
143
- uptime: process.uptime(),
144
- memory: process.memoryUsage(),
145
- timestamp: new Date().toISOString(),
146
- },
147
- application: metricsCollector.getAllStats(),
148
- });
149
- };
150
- }
151
-
152
- /**
153
- * Auto-record timing from a function execution
154
- *
155
- * Wraps a function to automatically record its execution time.
156
- *
157
- * @example
158
- * ```typescript
159
- * import { recordTiming } from 'autotel-tanstack/metrics';
160
- *
161
- * const getUser = createServerFn({ method: 'GET' })
162
- * .handler(recordTiming('serverFn.getUser', async ({ data: id }) => {
163
- * return await db.users.findUnique({ where: { id } });
164
- * }));
165
- * ```
166
- */
167
- export function recordTiming<T extends (...args: any[]) => any>(
168
- metricName: string,
169
- fn: T,
170
- ): T {
171
- return (async (...args: Parameters<T>) => {
172
- const startTime = Date.now();
173
- try {
174
- const result = await fn(...args);
175
- const duration = Date.now() - startTime;
176
- metricsCollector.recordTiming(metricName, duration);
177
- return result as ReturnType<T>;
178
- } catch (error) {
179
- const duration = Date.now() - startTime;
180
- metricsCollector.recordTiming(`${metricName}.error`, duration);
181
- throw error;
182
- }
183
- }) as T;
184
- }
@@ -1,198 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import {
3
- createTracingMiddleware,
4
- tracingMiddleware,
5
- functionTracingMiddleware,
6
- } from './middleware';
7
-
8
- // Mock autotel
9
- vi.mock('autotel', () => ({
10
- trace: vi.fn((name, fn) =>
11
- fn({
12
- setAttributes: vi.fn(),
13
- setAttribute: vi.fn(),
14
- setStatus: vi.fn(),
15
- recordException: vi.fn(),
16
- }),
17
- ),
18
- }));
19
-
20
- // Mock context module
21
- vi.mock('./context.js', () => ({
22
- extractContextFromRequest: vi.fn(() => ({})),
23
- }));
24
-
25
- describe('middleware', () => {
26
- beforeEach(() => {
27
- vi.clearAllMocks();
28
- });
29
-
30
- describe('createTracingMiddleware', () => {
31
- it('should create request middleware by default', async () => {
32
- const middleware = createTracingMiddleware();
33
- const request = new Request('http://localhost/api/users');
34
- const next = vi.fn().mockResolvedValue({ status: 200 });
35
-
36
- await middleware({
37
- next,
38
- request,
39
- pathname: '/api/users',
40
- context: {},
41
- });
42
-
43
- expect(next).toHaveBeenCalled();
44
- });
45
-
46
- it('should create function middleware when type is "function"', async () => {
47
- const middleware = createTracingMiddleware({ type: 'function' });
48
- const next = vi.fn().mockResolvedValue({ data: 'test' });
49
-
50
- await middleware({
51
- next,
52
- context: {},
53
- data: { id: '123' },
54
- functionId: 'getUser',
55
- method: 'GET',
56
- });
57
-
58
- expect(next).toHaveBeenCalled();
59
- });
60
-
61
- it('should skip excluded paths', async () => {
62
- const middleware = createTracingMiddleware({
63
- excludePaths: ['/health', '/metrics'],
64
- });
65
- const request = new Request('http://localhost/health');
66
- const next = vi.fn().mockResolvedValue({ status: 200 });
67
-
68
- await middleware({
69
- next,
70
- request,
71
- pathname: '/health',
72
- context: {},
73
- });
74
-
75
- expect(next).toHaveBeenCalled();
76
- });
77
-
78
- it('should handle glob patterns in excludePaths', async () => {
79
- const middleware = createTracingMiddleware({
80
- excludePaths: ['/api/internal/*'],
81
- });
82
- const request = new Request('http://localhost/api/internal/debug');
83
- const next = vi.fn().mockResolvedValue({ status: 200 });
84
-
85
- await middleware({
86
- next,
87
- request,
88
- pathname: '/api/internal/debug',
89
- context: {},
90
- });
91
-
92
- expect(next).toHaveBeenCalled();
93
- });
94
-
95
- it('should handle regex patterns in excludePaths', async () => {
96
- const middleware = createTracingMiddleware({
97
- excludePaths: [/^\/api\/v\d+\/health$/],
98
- });
99
- const request = new Request('http://localhost/api/v1/health');
100
- const next = vi.fn().mockResolvedValue({ status: 200 });
101
-
102
- await middleware({
103
- next,
104
- request,
105
- pathname: '/api/v1/health',
106
- context: {},
107
- });
108
-
109
- expect(next).toHaveBeenCalled();
110
- });
111
-
112
- it('should propagate errors from next()', async () => {
113
- const middleware = createTracingMiddleware();
114
- const error = new Error('Handler error');
115
- const next = vi.fn().mockRejectedValue(error);
116
- const request = new Request('http://localhost/api/users');
117
- // Middleware reports the rejected error via console.error in
118
- // error-reporting.ts — that's the contract under test (errors don't
119
- // get silently swallowed). Silence the noise; the rejection assertion
120
- // proves the propagation.
121
- const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
122
-
123
- await expect(
124
- middleware({
125
- next,
126
- request,
127
- pathname: '/api/users',
128
- context: {},
129
- }),
130
- ).rejects.toThrow('Handler error');
131
-
132
- errSpy.mockRestore();
133
- });
134
-
135
- it('should pass through when no request is available', async () => {
136
- const middleware = createTracingMiddleware();
137
- const next = vi.fn().mockResolvedValue({ data: 'test' });
138
-
139
- const result = await middleware({
140
- next,
141
- context: {},
142
- });
143
-
144
- expect(next).toHaveBeenCalled();
145
- expect(result).toEqual({ data: 'test' });
146
- });
147
- });
148
-
149
- describe('tracingMiddleware', () => {
150
- it('should create middleware with sensible defaults', async () => {
151
- const middleware = tracingMiddleware();
152
- expect(middleware).toBeDefined();
153
- });
154
-
155
- it('should exclude common health check paths', async () => {
156
- const middleware = tracingMiddleware();
157
- const request = new Request('http://localhost/healthz');
158
- const next = vi.fn().mockResolvedValue({ status: 200 });
159
-
160
- await middleware({
161
- next,
162
- request,
163
- pathname: '/healthz',
164
- context: {},
165
- });
166
-
167
- expect(next).toHaveBeenCalled();
168
- });
169
-
170
- it('should allow config overrides', async () => {
171
- const middleware = tracingMiddleware({
172
- captureHeaders: ['x-custom-header'],
173
- });
174
- expect(middleware).toBeDefined();
175
- });
176
- });
177
-
178
- describe('functionTracingMiddleware', () => {
179
- it('should create function middleware', async () => {
180
- const middleware = functionTracingMiddleware();
181
- const next = vi.fn().mockResolvedValue({ data: 'test' });
182
-
183
- await middleware({
184
- next,
185
- context: {},
186
- data: { id: '123' },
187
- functionId: 'testFn',
188
- });
189
-
190
- expect(next).toHaveBeenCalled();
191
- });
192
-
193
- it('should not require type in config', () => {
194
- const middleware = functionTracingMiddleware({ captureArgs: false });
195
- expect(middleware).toBeDefined();
196
- });
197
- });
198
- });