autotel-tanstack 1.13.35 → 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/package.json +4 -5
- package/src/auto.test.ts +0 -114
- package/src/auto.ts +0 -60
- package/src/browser/context.ts +0 -88
- package/src/browser/debug-headers.ts +0 -19
- package/src/browser/error-reporting.ts +0 -64
- package/src/browser/handlers.ts +0 -23
- package/src/browser/index.ts +0 -66
- package/src/browser/loaders.ts +0 -62
- package/src/browser/metrics.ts +0 -86
- package/src/browser/middleware.ts +0 -77
- package/src/browser/server-functions.ts +0 -31
- package/src/browser/testing.ts +0 -130
- package/src/browser/types.ts +0 -100
- package/src/context.test.ts +0 -90
- package/src/context.ts +0 -145
- package/src/debug-headers.ts +0 -109
- package/src/env.ts +0 -56
- package/src/error-reporting.ts +0 -204
- package/src/handlers.ts +0 -306
- package/src/index.ts +0 -97
- package/src/instrument.test.ts +0 -131
- package/src/instrument.ts +0 -97
- package/src/loaders.test.ts +0 -123
- package/src/loaders.ts +0 -356
- package/src/metrics.ts +0 -184
- package/src/middleware.test.ts +0 -198
- package/src/middleware.ts +0 -435
- package/src/route-filter.test.ts +0 -28
- package/src/route-filter.ts +0 -40
- package/src/server-functions.test.ts +0 -86
- package/src/server-functions.ts +0 -188
- package/src/testing.test.ts +0 -205
- package/src/testing.ts +0 -430
- package/src/types.test.ts +0 -46
- package/src/types.ts +0 -182
package/src/server-functions.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
-
import { trace, type TraceContext } from 'autotel';
|
|
3
|
-
import { isServerSide } from './env';
|
|
4
|
-
import { type TraceServerFnConfig, SPAN_ATTRIBUTES } from './types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Wrap a TanStack server function with OpenTelemetry tracing
|
|
8
|
-
*
|
|
9
|
-
* This function wraps a server function to automatically create spans
|
|
10
|
-
* for each invocation. It captures function name, arguments (optionally),
|
|
11
|
-
* results (optionally), and errors.
|
|
12
|
-
*
|
|
13
|
-
* @param serverFn - The server function to wrap
|
|
14
|
-
* @param config - Configuration options
|
|
15
|
-
* @returns Wrapped server function with tracing
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* import { createServerFn } from '@tanstack/react-start';
|
|
20
|
-
* import { traceServerFn } from 'autotel-tanstack/server-functions';
|
|
21
|
-
*
|
|
22
|
-
* const getUserBase = createServerFn({ method: 'GET' })
|
|
23
|
-
* .handler(async ({ data: id }) => {
|
|
24
|
-
* return await db.users.findUnique({ where: { id } });
|
|
25
|
-
* });
|
|
26
|
-
*
|
|
27
|
-
* export const getUser = traceServerFn(getUserBase, { name: 'getUser' });
|
|
28
|
-
* ```
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```typescript
|
|
32
|
-
* // With argument and result capture (careful with PII!)
|
|
33
|
-
* export const createUser = traceServerFn(
|
|
34
|
-
* createServerFn({ method: 'POST' })
|
|
35
|
-
* .handler(async ({ data }) => {
|
|
36
|
-
* return await db.users.create({ data });
|
|
37
|
-
* }),
|
|
38
|
-
* {
|
|
39
|
-
* name: 'createUser',
|
|
40
|
-
* captureArgs: true,
|
|
41
|
-
* captureResults: false, // Don't capture for PII reasons
|
|
42
|
-
* }
|
|
43
|
-
* );
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
export function traceServerFn<T extends (...args: any[]) => any>(
|
|
47
|
-
serverFn: T,
|
|
48
|
-
config: TraceServerFnConfig = {},
|
|
49
|
-
): T {
|
|
50
|
-
const fnName = config.name || serverFn.name || 'serverFn';
|
|
51
|
-
const captureArgs = config.captureArgs ?? true;
|
|
52
|
-
const captureResults = config.captureResults ?? false;
|
|
53
|
-
|
|
54
|
-
return new Proxy(serverFn, {
|
|
55
|
-
apply(target, thisArg, argArray) {
|
|
56
|
-
// If we're in the browser, just call the function without tracing
|
|
57
|
-
// Server functions should never run in the browser, but this prevents
|
|
58
|
-
// autotel (which uses Node.js APIs) from being executed if it somehow does
|
|
59
|
-
if (!isServerSide()) {
|
|
60
|
-
return target.apply(thisArg, argArray);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return trace(`tanstack.serverFn.${fnName}`, async (ctx: TraceContext) => {
|
|
64
|
-
ctx.setAttributes({
|
|
65
|
-
[SPAN_ATTRIBUTES.RPC_SYSTEM]: 'tanstack-start',
|
|
66
|
-
[SPAN_ATTRIBUTES.RPC_METHOD]: fnName,
|
|
67
|
-
[SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'serverFn',
|
|
68
|
-
[SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_NAME]: fnName,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Capture arguments if configured
|
|
72
|
-
if (captureArgs && argArray.length > 0) {
|
|
73
|
-
const args = argArray[0];
|
|
74
|
-
if (args !== undefined) {
|
|
75
|
-
try {
|
|
76
|
-
ctx.setAttribute(
|
|
77
|
-
SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS,
|
|
78
|
-
JSON.stringify(args),
|
|
79
|
-
);
|
|
80
|
-
} catch {
|
|
81
|
-
ctx.setAttribute(
|
|
82
|
-
SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS,
|
|
83
|
-
'[non-serializable]',
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const result = await Reflect.apply(target, thisArg, argArray);
|
|
91
|
-
|
|
92
|
-
// Capture result if configured
|
|
93
|
-
if (captureResults && result !== undefined) {
|
|
94
|
-
try {
|
|
95
|
-
ctx.setAttribute(
|
|
96
|
-
SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,
|
|
97
|
-
JSON.stringify(result),
|
|
98
|
-
);
|
|
99
|
-
} catch {
|
|
100
|
-
ctx.setAttribute(
|
|
101
|
-
SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,
|
|
102
|
-
'[non-serializable]',
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
ctx.setStatus({ code: SpanStatusCode.OK });
|
|
108
|
-
return result;
|
|
109
|
-
} catch (error) {
|
|
110
|
-
if ('recordError' in ctx && typeof ctx.recordError === 'function') {
|
|
111
|
-
ctx.recordError(error);
|
|
112
|
-
} else if (
|
|
113
|
-
'recordException' in ctx &&
|
|
114
|
-
typeof ctx.recordException === 'function'
|
|
115
|
-
) {
|
|
116
|
-
ctx.recordException(error);
|
|
117
|
-
}
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
get(target, prop, receiver) {
|
|
124
|
-
return Reflect.get(target, prop, receiver);
|
|
125
|
-
},
|
|
126
|
-
}) as T;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Create a traced version of createServerFn
|
|
131
|
-
*
|
|
132
|
-
* This higher-order function wraps TanStack's createServerFn to automatically
|
|
133
|
-
* add tracing to all created server functions.
|
|
134
|
-
*
|
|
135
|
-
* @param createServerFnOriginal - The original createServerFn from TanStack
|
|
136
|
-
* @param defaultConfig - Default configuration for all server functions
|
|
137
|
-
* @returns Wrapped createServerFn that produces traced server functions
|
|
138
|
-
*
|
|
139
|
-
* @example
|
|
140
|
-
* ```typescript
|
|
141
|
-
* import { createServerFn as originalCreateServerFn } from '@tanstack/react-start';
|
|
142
|
-
* import { createTracedServerFnFactory } from 'autotel-tanstack/server-functions';
|
|
143
|
-
*
|
|
144
|
-
* export const createServerFn = createTracedServerFnFactory(originalCreateServerFn);
|
|
145
|
-
*
|
|
146
|
-
* // Now all server functions created with createServerFn are automatically traced
|
|
147
|
-
* export const getUser = createServerFn({ method: 'GET' })
|
|
148
|
-
* .handler(async ({ data: id }) => {
|
|
149
|
-
* return await db.users.findUnique({ where: { id } });
|
|
150
|
-
* });
|
|
151
|
-
* ```
|
|
152
|
-
*/
|
|
153
|
-
export function createTracedServerFnFactory<
|
|
154
|
-
TCreateServerFn extends (...args: any[]) => any,
|
|
155
|
-
>(
|
|
156
|
-
createServerFnOriginal: TCreateServerFn,
|
|
157
|
-
defaultConfig: Omit<TraceServerFnConfig, 'name'> = {},
|
|
158
|
-
): TCreateServerFn {
|
|
159
|
-
return new Proxy(createServerFnOriginal, {
|
|
160
|
-
apply(target, thisArg, argArray) {
|
|
161
|
-
const result = Reflect.apply(target, thisArg, argArray);
|
|
162
|
-
|
|
163
|
-
// If the result has a .handler method, wrap it
|
|
164
|
-
if (
|
|
165
|
-
result &&
|
|
166
|
-
typeof result === 'object' &&
|
|
167
|
-
'handler' in result &&
|
|
168
|
-
typeof result.handler === 'function'
|
|
169
|
-
) {
|
|
170
|
-
const originalHandler = result.handler.bind(result);
|
|
171
|
-
|
|
172
|
-
result.handler = function tracedHandler(handlerFn: unknown) {
|
|
173
|
-
const wrappedHandler = originalHandler(handlerFn as never);
|
|
174
|
-
|
|
175
|
-
// Try to infer function name from the handler
|
|
176
|
-
const fnName = (handlerFn as { name?: string })?.name || 'serverFn';
|
|
177
|
-
|
|
178
|
-
return traceServerFn(wrappedHandler, {
|
|
179
|
-
...defaultConfig,
|
|
180
|
-
name: fnName,
|
|
181
|
-
});
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return result;
|
|
186
|
-
},
|
|
187
|
-
}) as TCreateServerFn;
|
|
188
|
-
}
|
package/src/testing.test.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
createMockRequest,
|
|
4
|
-
generateTraceparent,
|
|
5
|
-
createTestSpansHandlers,
|
|
6
|
-
type SerializedSpan,
|
|
7
|
-
} from './testing';
|
|
8
|
-
|
|
9
|
-
describe('testing utilities', () => {
|
|
10
|
-
describe('createMockRequest', () => {
|
|
11
|
-
it('should create a GET request', () => {
|
|
12
|
-
const request = createMockRequest('GET', '/api/users');
|
|
13
|
-
|
|
14
|
-
expect(request.method).toBe('GET');
|
|
15
|
-
expect(request.url).toBe('http://localhost/api/users');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should create a POST request', () => {
|
|
19
|
-
const request = createMockRequest('POST', '/api/users', {
|
|
20
|
-
body: JSON.stringify({ name: 'Test' }),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
expect(request.method).toBe('POST');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should include custom headers', () => {
|
|
27
|
-
const request = createMockRequest('GET', '/api/users', {
|
|
28
|
-
headers: { 'x-request-id': 'test-123' },
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
expect(request.headers.get('x-request-id')).toBe('test-123');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should include traceparent header', () => {
|
|
35
|
-
const traceparent =
|
|
36
|
-
'00-12345678901234567890123456789012-1234567890123456-01';
|
|
37
|
-
const request = createMockRequest('GET', '/api/users', { traceparent });
|
|
38
|
-
|
|
39
|
-
expect(request.headers.get('traceparent')).toBe(traceparent);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('generateTraceparent', () => {
|
|
44
|
-
it('should generate valid traceparent format', () => {
|
|
45
|
-
const traceparent = generateTraceparent();
|
|
46
|
-
|
|
47
|
-
// Format: version-traceId-spanId-flags
|
|
48
|
-
const parts = traceparent.split('-');
|
|
49
|
-
expect(parts).toHaveLength(4);
|
|
50
|
-
expect(parts[0]).toBe('00'); // version
|
|
51
|
-
expect(parts[1]).toHaveLength(32); // trace ID
|
|
52
|
-
expect(parts[2]).toHaveLength(16); // span ID
|
|
53
|
-
expect(parts[3]).toBe('01'); // sampled flag
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should use provided trace ID', () => {
|
|
57
|
-
const traceId = '12345678901234567890123456789012';
|
|
58
|
-
const traceparent = generateTraceparent(traceId);
|
|
59
|
-
|
|
60
|
-
expect(traceparent).toContain(traceId);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should use provided span ID', () => {
|
|
64
|
-
const spanId = '1234567890123456';
|
|
65
|
-
const traceparent = generateTraceparent(undefined, spanId);
|
|
66
|
-
|
|
67
|
-
expect(traceparent).toContain(spanId);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should generate different values each time', () => {
|
|
71
|
-
const tp1 = generateTraceparent();
|
|
72
|
-
const tp2 = generateTraceparent();
|
|
73
|
-
|
|
74
|
-
expect(tp1).not.toBe(tp2);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Helper to read Response JSON
|
|
80
|
-
async function json(res: Response): Promise<unknown> {
|
|
81
|
-
return res.json();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
describe('createTestSpansHandlers', () => {
|
|
85
|
-
beforeEach(() => {
|
|
86
|
-
delete (globalThis as Record<string, unknown>).__testSpanExporter;
|
|
87
|
-
delete process.env.E2E;
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('GET returns 404 when not in E2E mode (raw Request)', async () => {
|
|
91
|
-
const { GET } = createTestSpansHandlers();
|
|
92
|
-
const res = GET(new Request('http://localhost/api/test-spans'));
|
|
93
|
-
expect(res.status).toBe(404);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('GET returns 404 when not in E2E mode (context object)', async () => {
|
|
97
|
-
const { GET } = createTestSpansHandlers();
|
|
98
|
-
const res = GET({
|
|
99
|
-
request: new Request('http://localhost/api/test-spans'),
|
|
100
|
-
});
|
|
101
|
-
expect(res.status).toBe(404);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('GET returns 500 when exporter not initialized', async () => {
|
|
105
|
-
process.env.E2E = '1';
|
|
106
|
-
const { GET } = createTestSpansHandlers();
|
|
107
|
-
const res = GET(new Request('http://localhost/api/test-spans'));
|
|
108
|
-
expect(res.status).toBe(500);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('GET returns serialized spans', async () => {
|
|
112
|
-
process.env.E2E = '1';
|
|
113
|
-
const mockSpan = {
|
|
114
|
-
name: 'sendMoney.handler',
|
|
115
|
-
spanContext: () => ({ spanId: 'abc123', traceId: 'trace456' }),
|
|
116
|
-
parentSpanContext: { spanId: 'parent789' },
|
|
117
|
-
attributes: { 'transfer.amount': 100 },
|
|
118
|
-
status: { code: 0 },
|
|
119
|
-
duration: [0, 500_000_000], // 500ms in [seconds, nanoseconds]
|
|
120
|
-
};
|
|
121
|
-
(globalThis as Record<string, unknown>).__testSpanExporter = {
|
|
122
|
-
getFinishedSpans: () => [mockSpan],
|
|
123
|
-
reset: () => {},
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const { GET } = createTestSpansHandlers();
|
|
127
|
-
const res = GET(new Request('http://localhost/api/test-spans'));
|
|
128
|
-
expect(res.status).toBe(200);
|
|
129
|
-
const body = (await json(res)) as { spans: SerializedSpan[] };
|
|
130
|
-
expect(body.spans).toHaveLength(1);
|
|
131
|
-
expect(body.spans[0].name).toBe('sendMoney.handler');
|
|
132
|
-
expect(body.spans[0].spanId).toBe('abc123');
|
|
133
|
-
expect(body.spans[0].traceId).toBe('trace456');
|
|
134
|
-
expect(body.spans[0].parentSpanId).toBe('parent789');
|
|
135
|
-
expect(body.spans[0].attributes?.['transfer.amount']).toBe(100);
|
|
136
|
-
expect(body.spans[0].durationMs).toBeCloseTo(500, 0);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('GET omits parentSpanId when no parent', async () => {
|
|
140
|
-
process.env.E2E = '1';
|
|
141
|
-
const mockSpan = {
|
|
142
|
-
name: 'root',
|
|
143
|
-
spanContext: () => ({ spanId: 'abc123', traceId: 'trace456' }),
|
|
144
|
-
parentSpanContext: undefined,
|
|
145
|
-
attributes: {},
|
|
146
|
-
status: { code: 0 },
|
|
147
|
-
duration: [0, 0],
|
|
148
|
-
};
|
|
149
|
-
(globalThis as Record<string, unknown>).__testSpanExporter = {
|
|
150
|
-
getFinishedSpans: () => [mockSpan],
|
|
151
|
-
reset: () => {},
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const { GET } = createTestSpansHandlers();
|
|
155
|
-
const body = (await json(
|
|
156
|
-
GET(new Request('http://localhost/api/test-spans')),
|
|
157
|
-
)) as { spans: SerializedSpan[] };
|
|
158
|
-
expect(body.spans[0].parentSpanId).toBeUndefined();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('DELETE returns 404 when not in E2E mode (raw Request)', async () => {
|
|
162
|
-
const { DELETE } = createTestSpansHandlers();
|
|
163
|
-
const res = DELETE(
|
|
164
|
-
new Request('http://localhost/api/test-spans', { method: 'DELETE' }),
|
|
165
|
-
);
|
|
166
|
-
expect(res.status).toBe(404);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('DELETE returns 404 when not in E2E mode (context object)', async () => {
|
|
170
|
-
const { DELETE } = createTestSpansHandlers();
|
|
171
|
-
const res = DELETE({
|
|
172
|
-
request: new Request('http://localhost/api/test-spans', {
|
|
173
|
-
method: 'DELETE',
|
|
174
|
-
}),
|
|
175
|
-
});
|
|
176
|
-
expect(res.status).toBe(404);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('DELETE returns 500 when exporter not initialized', async () => {
|
|
180
|
-
process.env.E2E = '1';
|
|
181
|
-
const { DELETE } = createTestSpansHandlers();
|
|
182
|
-
const res = DELETE(
|
|
183
|
-
new Request('http://localhost/api/test-spans', { method: 'DELETE' }),
|
|
184
|
-
);
|
|
185
|
-
expect(res.status).toBe(500);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('DELETE resets exporter and returns ok', async () => {
|
|
189
|
-
process.env.E2E = '1';
|
|
190
|
-
const reset = vi.fn();
|
|
191
|
-
(globalThis as Record<string, unknown>).__testSpanExporter = {
|
|
192
|
-
getFinishedSpans: () => [],
|
|
193
|
-
reset,
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const { DELETE } = createTestSpansHandlers();
|
|
197
|
-
const res = DELETE(
|
|
198
|
-
new Request('http://localhost/api/test-spans', { method: 'DELETE' }),
|
|
199
|
-
);
|
|
200
|
-
expect(res.status).toBe(200);
|
|
201
|
-
expect(reset).toHaveBeenCalledOnce();
|
|
202
|
-
const body = (await json(res)) as { ok: boolean };
|
|
203
|
-
expect(body.ok).toBe(true);
|
|
204
|
-
});
|
|
205
|
-
});
|