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/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/instrument.test.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
// Hoisted so the values exist when the (also-hoisted) vi.mock factories run,
|
|
4
|
-
// since instrument is imported statically below.
|
|
5
|
-
const {
|
|
6
|
-
mockInit,
|
|
7
|
-
mockIsInitialized,
|
|
8
|
-
mockExporter,
|
|
9
|
-
MockInMemorySpanExporter,
|
|
10
|
-
MockSimpleSpanProcessor,
|
|
11
|
-
} = vi.hoisted(() => {
|
|
12
|
-
const exporter = { reset: vi.fn(), getFinishedSpans: vi.fn(() => []) };
|
|
13
|
-
return {
|
|
14
|
-
mockInit: vi.fn(),
|
|
15
|
-
mockIsInitialized: vi.fn(() => false),
|
|
16
|
-
mockExporter: exporter,
|
|
17
|
-
MockInMemorySpanExporter: vi.fn(function () {
|
|
18
|
-
return exporter;
|
|
19
|
-
}),
|
|
20
|
-
MockSimpleSpanProcessor: vi.fn(function (exp: unknown) {
|
|
21
|
-
return { exporter: exp };
|
|
22
|
-
}),
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
vi.mock('autotel', () => ({
|
|
27
|
-
init: mockInit,
|
|
28
|
-
isInitialized: mockIsInitialized,
|
|
29
|
-
}));
|
|
30
|
-
vi.mock('autotel/exporters', () => ({
|
|
31
|
-
InMemorySpanExporter: MockInMemorySpanExporter,
|
|
32
|
-
}));
|
|
33
|
-
vi.mock('autotel/processors', () => ({
|
|
34
|
-
SimpleSpanProcessor: MockSimpleSpanProcessor,
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
import { instrument } from './instrument';
|
|
38
|
-
|
|
39
|
-
type InitCall = {
|
|
40
|
-
service?: string;
|
|
41
|
-
endpoint?: string;
|
|
42
|
-
debug?: boolean | 'pretty';
|
|
43
|
-
logs?: unknown;
|
|
44
|
-
subscribers?: unknown;
|
|
45
|
-
spanProcessors?: unknown[];
|
|
46
|
-
};
|
|
47
|
-
const lastInit = () => mockInit.mock.calls[0][0] as InitCall;
|
|
48
|
-
|
|
49
|
-
describe('instrument()', () => {
|
|
50
|
-
const originalEnv = { ...process.env };
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
mockInit.mockReset();
|
|
54
|
-
mockIsInitialized.mockReturnValue(false);
|
|
55
|
-
MockInMemorySpanExporter.mockClear();
|
|
56
|
-
MockSimpleSpanProcessor.mockClear();
|
|
57
|
-
delete (globalThis as Record<string, unknown>).__testSpanExporter;
|
|
58
|
-
delete process.env.E2E;
|
|
59
|
-
delete process.env.AUTOTEL_DEBUG;
|
|
60
|
-
delete process.env.OTEL_SERVICE_NAME;
|
|
61
|
-
delete process.env.NODE_ENV;
|
|
62
|
-
});
|
|
63
|
-
afterEach(() => {
|
|
64
|
-
process.env = { ...originalEnv };
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('is idempotent — does nothing when already initialized', () => {
|
|
68
|
-
mockIsInitialized.mockReturnValue(true);
|
|
69
|
-
instrument({ service: 'x' });
|
|
70
|
-
expect(mockInit).not.toHaveBeenCalled();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('defaults the service name to tanstack-start', () => {
|
|
74
|
-
instrument();
|
|
75
|
-
expect(lastInit().service).toBe('tanstack-start');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('prefers an explicit service over OTEL_SERVICE_NAME', () => {
|
|
79
|
-
process.env.OTEL_SERVICE_NAME = 'from-env';
|
|
80
|
-
instrument({ service: 'explicit' });
|
|
81
|
-
expect(lastInit().service).toBe('explicit');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('falls back to OTEL_SERVICE_NAME', () => {
|
|
85
|
-
process.env.OTEL_SERVICE_NAME = 'from-env';
|
|
86
|
-
instrument();
|
|
87
|
-
expect(lastInit().service).toBe('from-env');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('passes options straight through to init in normal mode', () => {
|
|
91
|
-
const subscribers = [{ name: 'sub' } as never];
|
|
92
|
-
instrument({
|
|
93
|
-
endpoint: 'http://collector:4318',
|
|
94
|
-
subscribers,
|
|
95
|
-
logs: true,
|
|
96
|
-
});
|
|
97
|
-
const call = lastInit();
|
|
98
|
-
expect(call.endpoint).toBe('http://collector:4318');
|
|
99
|
-
expect(call.subscribers).toBe(subscribers);
|
|
100
|
-
expect(call.logs).toBe(true);
|
|
101
|
-
expect(call.spanProcessors).toBeUndefined();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('resolves debug from AUTOTEL_DEBUG', () => {
|
|
105
|
-
process.env.AUTOTEL_DEBUG = 'pretty';
|
|
106
|
-
instrument({ endpoint: 'http://c:4318' });
|
|
107
|
-
expect(lastInit().debug).toBe('pretty');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('pretty-prints in dev when no endpoint is set', () => {
|
|
111
|
-
process.env.NODE_ENV = 'development';
|
|
112
|
-
instrument();
|
|
113
|
-
expect(lastInit().debug).toBe('pretty');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('captures spans in memory under E2E=1 and skips OTLP + logs', () => {
|
|
117
|
-
process.env.E2E = '1';
|
|
118
|
-
const subscribers = [{ name: 'sub' } as never];
|
|
119
|
-
instrument({ endpoint: 'http://c:4318', subscribers, logs: true });
|
|
120
|
-
|
|
121
|
-
expect(MockInMemorySpanExporter).toHaveBeenCalledOnce();
|
|
122
|
-
const call = lastInit();
|
|
123
|
-
expect(call.spanProcessors).toHaveLength(1);
|
|
124
|
-
expect(call.endpoint).toBeUndefined(); // no OTLP shipping in tests
|
|
125
|
-
expect(call.logs).toBeUndefined(); // logs off in E2E
|
|
126
|
-
expect(call.subscribers).toBe(subscribers); // subscribers still flow
|
|
127
|
-
expect((globalThis as Record<string, unknown>).__testSpanExporter).toBe(
|
|
128
|
-
mockExporter,
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
});
|
package/src/instrument.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configurable tracing setup for TanStack Start.
|
|
3
|
-
*
|
|
4
|
-
* `instrument(options)` is a thin wrapper over autotel's `init` that adds the
|
|
5
|
-
* TanStack defaults so apps don't hand-roll them:
|
|
6
|
-
*
|
|
7
|
-
* - service name defaults to `OTEL_SERVICE_NAME` or `'tanstack-start'`;
|
|
8
|
-
* - `debug` resolves from `AUTOTEL_DEBUG` (`pretty` in dev when no endpoint set);
|
|
9
|
-
* - when `E2E=1`, spans are captured by an `InMemorySpanExporter` exposed as
|
|
10
|
-
* `globalThis.__testSpanExporter` (for span assertions) instead of shipping
|
|
11
|
-
* over OTLP;
|
|
12
|
-
* - it's idempotent — a second call is a no-op.
|
|
13
|
-
*
|
|
14
|
-
* Everything else — `endpoint`, `headers`, `subscribers`, `logs`,
|
|
15
|
-
* `canonicalLogLines`, extra `spanProcessors`, … — passes straight through to
|
|
16
|
-
* `init`, and the standard `OTEL_*` env vars are resolved by autotel core, so
|
|
17
|
-
* apps never re-parse them.
|
|
18
|
-
*
|
|
19
|
-
* The zero-config `autotel-tanstack/auto` side-effect module is just
|
|
20
|
-
* `instrument()` with no options.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* import { instrument } from 'autotel-tanstack';
|
|
25
|
-
* import { PostHogSubscriber } from 'autotel-subscribers';
|
|
26
|
-
*
|
|
27
|
-
* instrument({
|
|
28
|
-
* subscribers: process.env.POSTHOG_KEY
|
|
29
|
-
* ? [new PostHogSubscriber({ apiKey: process.env.POSTHOG_KEY })]
|
|
30
|
-
* : [],
|
|
31
|
-
* logs: true,
|
|
32
|
-
* canonicalLogLines: { enabled: true, rootSpansOnly: true },
|
|
33
|
-
* });
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* @module
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
import { init, isInitialized, type AutotelConfig } from 'autotel';
|
|
40
|
-
import { InMemorySpanExporter } from 'autotel/exporters';
|
|
41
|
-
import { SimpleSpanProcessor } from 'autotel/processors';
|
|
42
|
-
|
|
43
|
-
// `service` is optional here — instrument() defaults it to OTEL_SERVICE_NAME or
|
|
44
|
-
// 'tanstack-start'. Everything else matches autotel's init config.
|
|
45
|
-
export type InstrumentOptions = Omit<AutotelConfig, 'service'> & {
|
|
46
|
-
service?: string;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const DEFAULT_SERVICE = 'tanstack-start';
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Resolve span-debug output: an explicit option wins; otherwise `AUTOTEL_DEBUG`
|
|
53
|
-
* (`pretty` | `true`/`1` | `false`/`0`); otherwise pretty-print in development
|
|
54
|
-
* when there's no OTLP endpoint, so spans are visible immediately.
|
|
55
|
-
*/
|
|
56
|
-
function resolveDebug(
|
|
57
|
-
explicit: AutotelConfig['debug'],
|
|
58
|
-
endpoint: string | undefined,
|
|
59
|
-
): boolean | 'pretty' {
|
|
60
|
-
if (explicit !== undefined) return explicit;
|
|
61
|
-
const env = process.env.AUTOTEL_DEBUG;
|
|
62
|
-
if (env === 'pretty') return 'pretty';
|
|
63
|
-
if (env === 'true' || env === '1') return true;
|
|
64
|
-
if (env === 'false' || env === '0') return false;
|
|
65
|
-
if (!endpoint && process.env.NODE_ENV === 'development') return 'pretty';
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function instrument(options: InstrumentOptions = {}): void {
|
|
70
|
-
// Idempotent: tolerate the instrumentation module being evaluated more than
|
|
71
|
-
// once (HMR, multiple entry points) without re-initializing.
|
|
72
|
-
if (isInitialized()) return;
|
|
73
|
-
|
|
74
|
-
const service =
|
|
75
|
-
options.service ?? process.env.OTEL_SERVICE_NAME ?? DEFAULT_SERVICE;
|
|
76
|
-
|
|
77
|
-
if (process.env.E2E === '1') {
|
|
78
|
-
// Capture spans in memory for test assertions; skip OTLP/logs entirely.
|
|
79
|
-
const exporter = new InMemorySpanExporter();
|
|
80
|
-
(globalThis as Record<string, unknown>).__testSpanExporter = exporter;
|
|
81
|
-
init({
|
|
82
|
-
service,
|
|
83
|
-
subscribers: options.subscribers,
|
|
84
|
-
spanProcessors: [
|
|
85
|
-
new SimpleSpanProcessor(exporter),
|
|
86
|
-
...(options.spanProcessors ?? []),
|
|
87
|
-
],
|
|
88
|
-
});
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
init({
|
|
93
|
-
...options,
|
|
94
|
-
service,
|
|
95
|
-
debug: resolveDebug(options.debug, options.endpoint),
|
|
96
|
-
});
|
|
97
|
-
}
|
package/src/loaders.test.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { traceLoader, traceBeforeLoad, createTracedRoute } from './loaders';
|
|
3
|
-
|
|
4
|
-
// Mock autotel
|
|
5
|
-
vi.mock('autotel', () => ({
|
|
6
|
-
trace: vi.fn((name, fn) =>
|
|
7
|
-
fn({
|
|
8
|
-
setAttributes: vi.fn(),
|
|
9
|
-
setAttribute: vi.fn(),
|
|
10
|
-
setStatus: vi.fn(),
|
|
11
|
-
recordException: vi.fn(),
|
|
12
|
-
}),
|
|
13
|
-
),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
describe('loaders', () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.clearAllMocks();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('traceLoader', () => {
|
|
22
|
-
it('should wrap a loader function', async () => {
|
|
23
|
-
const loaderFn = vi.fn().mockResolvedValue({ data: 'test' });
|
|
24
|
-
const tracedLoader = traceLoader(loaderFn);
|
|
25
|
-
|
|
26
|
-
const context = {
|
|
27
|
-
params: { userId: '123' },
|
|
28
|
-
route: { id: '/users/$userId' },
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const result = await tracedLoader(context);
|
|
32
|
-
|
|
33
|
-
expect(loaderFn).toHaveBeenCalledWith(context);
|
|
34
|
-
expect(result).toEqual({ data: 'test' });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should use custom name if provided', async () => {
|
|
38
|
-
const loaderFn = vi.fn().mockResolvedValue({ data: 'test' });
|
|
39
|
-
const tracedLoader = traceLoader(loaderFn, { name: 'customLoader' });
|
|
40
|
-
|
|
41
|
-
await tracedLoader({ route: { id: '/test' } });
|
|
42
|
-
expect(loaderFn).toHaveBeenCalled();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should propagate errors', async () => {
|
|
46
|
-
const error = new Error('Loader error');
|
|
47
|
-
const loaderFn = vi.fn().mockRejectedValue(error);
|
|
48
|
-
const tracedLoader = traceLoader(loaderFn);
|
|
49
|
-
|
|
50
|
-
await expect(tracedLoader({})).rejects.toThrow('Loader error');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should handle missing route id', async () => {
|
|
54
|
-
const loaderFn = vi.fn().mockResolvedValue({ data: 'test' });
|
|
55
|
-
const tracedLoader = traceLoader(loaderFn);
|
|
56
|
-
|
|
57
|
-
const result = await tracedLoader({});
|
|
58
|
-
expect(result).toEqual({ data: 'test' });
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('traceBeforeLoad', () => {
|
|
63
|
-
it('should wrap a beforeLoad function', async () => {
|
|
64
|
-
const beforeLoadFn = vi.fn().mockResolvedValue({ auth: true });
|
|
65
|
-
const tracedBeforeLoad = traceBeforeLoad(beforeLoadFn);
|
|
66
|
-
|
|
67
|
-
const context = {
|
|
68
|
-
params: { userId: '123' },
|
|
69
|
-
route: { id: '/users/$userId' },
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const result = await tracedBeforeLoad(context);
|
|
73
|
-
|
|
74
|
-
expect(beforeLoadFn).toHaveBeenCalledWith(context);
|
|
75
|
-
expect(result).toEqual({ auth: true });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should handle redirect errors gracefully', async () => {
|
|
79
|
-
const redirectError = new Error('Redirect');
|
|
80
|
-
redirectError.name = 'RedirectError';
|
|
81
|
-
const beforeLoadFn = vi.fn().mockRejectedValue(redirectError);
|
|
82
|
-
const tracedBeforeLoad = traceBeforeLoad(beforeLoadFn);
|
|
83
|
-
|
|
84
|
-
await expect(tracedBeforeLoad({})).rejects.toThrow('Redirect');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should handle notFound errors gracefully', async () => {
|
|
88
|
-
const notFoundError = new Error('Not Found');
|
|
89
|
-
notFoundError.name = 'NotFoundError';
|
|
90
|
-
const beforeLoadFn = vi.fn().mockRejectedValue(notFoundError);
|
|
91
|
-
const tracedBeforeLoad = traceBeforeLoad(beforeLoadFn);
|
|
92
|
-
|
|
93
|
-
await expect(tracedBeforeLoad({})).rejects.toThrow('Not Found');
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('createTracedRoute', () => {
|
|
98
|
-
it('should create loader and beforeLoad wrappers', () => {
|
|
99
|
-
const traced = createTracedRoute('/users/$userId');
|
|
100
|
-
|
|
101
|
-
expect(traced.loader).toBeDefined();
|
|
102
|
-
expect(traced.beforeLoad).toBeDefined();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should wrap loader with route id in span name', async () => {
|
|
106
|
-
const traced = createTracedRoute('/users/$userId');
|
|
107
|
-
const loaderFn = vi.fn().mockResolvedValue({ user: {} });
|
|
108
|
-
const tracedLoader = traced.loader(loaderFn);
|
|
109
|
-
|
|
110
|
-
await tracedLoader({ params: { userId: '123' } });
|
|
111
|
-
expect(loaderFn).toHaveBeenCalled();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should wrap beforeLoad with route id in span name', async () => {
|
|
115
|
-
const traced = createTracedRoute('/dashboard');
|
|
116
|
-
const beforeLoadFn = vi.fn().mockResolvedValue({});
|
|
117
|
-
const tracedBeforeLoad = traced.beforeLoad(beforeLoadFn);
|
|
118
|
-
|
|
119
|
-
await tracedBeforeLoad({});
|
|
120
|
-
expect(beforeLoadFn).toHaveBeenCalled();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
});
|
package/src/loaders.ts
DELETED
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
-
import { trace, type TraceContext } from 'autotel';
|
|
3
|
-
import { isServerSide } from './env';
|
|
4
|
-
import { type TraceLoaderConfig, SPAN_ATTRIBUTES } from './types';
|
|
5
|
-
|
|
6
|
-
// Re-export types from @tanstack/react-router for consumers who need them
|
|
7
|
-
export type { LoaderFnContext } from '@tanstack/react-router';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Internal type for extracting route info from TanStack context.
|
|
11
|
-
* This is a minimal interface used only for instrumentation - the actual
|
|
12
|
-
* TanStack types flow through the generic parameter.
|
|
13
|
-
*/
|
|
14
|
-
interface TanStackContextInternal {
|
|
15
|
-
route?: { id?: string };
|
|
16
|
-
params?: Record<string, string>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Wrap a TanStack route loader with OpenTelemetry tracing
|
|
21
|
-
*
|
|
22
|
-
* This function wraps a loader function to automatically create spans
|
|
23
|
-
* for each invocation. It captures route ID, params (optionally),
|
|
24
|
-
* and errors.
|
|
25
|
-
*
|
|
26
|
-
* The generic type TLoaderFn preserves the full TanStack Router type inference,
|
|
27
|
-
* including typed params, context, and return types.
|
|
28
|
-
*
|
|
29
|
-
* @param loaderFn - The loader function to wrap
|
|
30
|
-
* @param config - Configuration options
|
|
31
|
-
* @returns Wrapped loader function with tracing (preserves original types)
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* import { createFileRoute } from '@tanstack/react-router';
|
|
36
|
-
* import { traceLoader } from 'autotel-tanstack/loaders';
|
|
37
|
-
*
|
|
38
|
-
* export const Route = createFileRoute('/users/$userId')({
|
|
39
|
-
* // Types are fully preserved - params.userId is typed as string
|
|
40
|
-
* loader: traceLoader(async ({ params }) => {
|
|
41
|
-
* return await db.users.findUnique({ where: { id: params.userId } });
|
|
42
|
-
* }),
|
|
43
|
-
* });
|
|
44
|
-
* ```
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```typescript
|
|
48
|
-
* // Sync loaders are also supported
|
|
49
|
-
* export const Route = createFileRoute('/static')({
|
|
50
|
-
* loader: traceLoader(({ context }) => ({
|
|
51
|
-
* message: `Welcome, ${context.userId}!`,
|
|
52
|
-
* })),
|
|
53
|
-
* });
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
export function traceLoader<TLoaderFn extends (ctx: any) => any>(
|
|
57
|
-
loaderFn: TLoaderFn,
|
|
58
|
-
config: TraceLoaderConfig = {},
|
|
59
|
-
): TLoaderFn {
|
|
60
|
-
const captureParams = config.captureParams ?? true;
|
|
61
|
-
const captureResult = config.captureResult ?? false;
|
|
62
|
-
|
|
63
|
-
const wrapped = (context: TanStackContextInternal) => {
|
|
64
|
-
// If we're in the browser, just call the loader without tracing
|
|
65
|
-
// This prevents autotel (which uses Node.js APIs) from being executed in the browser
|
|
66
|
-
if (!isServerSide()) {
|
|
67
|
-
return loaderFn(context);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const routeId = context?.route?.id || 'unknown';
|
|
71
|
-
const spanName = config.name || `tanstack.loader.${routeId}`;
|
|
72
|
-
|
|
73
|
-
// Handle both sync and async loaders
|
|
74
|
-
const result = loaderFn(context);
|
|
75
|
-
const isPromise = result instanceof Promise;
|
|
76
|
-
|
|
77
|
-
if (!isPromise) {
|
|
78
|
-
// Sync loader - wrap in trace synchronously
|
|
79
|
-
return trace(spanName, (ctx: TraceContext) => {
|
|
80
|
-
ctx.setAttributes({
|
|
81
|
-
[SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'loader',
|
|
82
|
-
[SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,
|
|
83
|
-
[SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'loader',
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (captureParams && context?.params) {
|
|
87
|
-
try {
|
|
88
|
-
ctx.setAttribute(
|
|
89
|
-
SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,
|
|
90
|
-
JSON.stringify(context.params),
|
|
91
|
-
);
|
|
92
|
-
} catch {
|
|
93
|
-
ctx.setAttribute(
|
|
94
|
-
SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,
|
|
95
|
-
'[non-serializable]',
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (captureResult && result !== undefined) {
|
|
101
|
-
try {
|
|
102
|
-
ctx.setAttribute('tanstack.loader.result', JSON.stringify(result));
|
|
103
|
-
} catch {
|
|
104
|
-
ctx.setAttribute('tanstack.loader.result', '[non-serializable]');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
ctx.setStatus({ code: SpanStatusCode.OK });
|
|
109
|
-
return result;
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Async loader
|
|
114
|
-
return trace(spanName, async (ctx: TraceContext) => {
|
|
115
|
-
ctx.setAttributes({
|
|
116
|
-
[SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'loader',
|
|
117
|
-
[SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,
|
|
118
|
-
[SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'loader',
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
if (captureParams && context?.params) {
|
|
122
|
-
try {
|
|
123
|
-
ctx.setAttribute(
|
|
124
|
-
SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,
|
|
125
|
-
JSON.stringify(context.params),
|
|
126
|
-
);
|
|
127
|
-
} catch {
|
|
128
|
-
ctx.setAttribute(
|
|
129
|
-
SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,
|
|
130
|
-
'[non-serializable]',
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const asyncResult = await result;
|
|
137
|
-
|
|
138
|
-
if (captureResult && asyncResult !== undefined) {
|
|
139
|
-
try {
|
|
140
|
-
ctx.setAttribute(
|
|
141
|
-
'tanstack.loader.result',
|
|
142
|
-
JSON.stringify(asyncResult),
|
|
143
|
-
);
|
|
144
|
-
} catch {
|
|
145
|
-
ctx.setAttribute('tanstack.loader.result', '[non-serializable]');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
ctx.setStatus({ code: SpanStatusCode.OK });
|
|
150
|
-
return asyncResult;
|
|
151
|
-
} catch (error) {
|
|
152
|
-
if ('recordError' in ctx && typeof ctx.recordError === 'function') {
|
|
153
|
-
ctx.recordError(error);
|
|
154
|
-
} else if (
|
|
155
|
-
'recordException' in ctx &&
|
|
156
|
-
typeof ctx.recordException === 'function'
|
|
157
|
-
) {
|
|
158
|
-
ctx.recordException(error);
|
|
159
|
-
}
|
|
160
|
-
throw error;
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
return wrapped as TLoaderFn;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Wrap a TanStack route beforeLoad function with OpenTelemetry tracing
|
|
170
|
-
*
|
|
171
|
-
* This function wraps a beforeLoad function to automatically create spans.
|
|
172
|
-
* beforeLoad runs before the route component renders and is typically
|
|
173
|
-
* used for auth checks, redirects, or data prefetching.
|
|
174
|
-
*
|
|
175
|
-
* The generic type TBeforeLoadFn preserves the full TanStack Router type inference,
|
|
176
|
-
* including typed params, context, search, and return types.
|
|
177
|
-
*
|
|
178
|
-
* @param beforeLoadFn - The beforeLoad function to wrap
|
|
179
|
-
* @param config - Configuration options
|
|
180
|
-
* @returns Wrapped beforeLoad function with tracing (preserves original types)
|
|
181
|
-
*
|
|
182
|
-
* @example
|
|
183
|
-
* ```typescript
|
|
184
|
-
* import { createFileRoute, redirect } from '@tanstack/react-router';
|
|
185
|
-
* import { traceBeforeLoad } from 'autotel-tanstack/loaders';
|
|
186
|
-
*
|
|
187
|
-
* export const Route = createFileRoute('/dashboard')({
|
|
188
|
-
* // Types are fully preserved - context, params, search are all typed
|
|
189
|
-
* beforeLoad: traceBeforeLoad(async ({ context, params }) => {
|
|
190
|
-
* if (!context.auth.isAuthenticated) {
|
|
191
|
-
* throw redirect({ to: '/login' });
|
|
192
|
-
* }
|
|
193
|
-
* return { userId: params.userId }; // Return type flows to loader context
|
|
194
|
-
* }),
|
|
195
|
-
* loader: ({ context }) => {
|
|
196
|
-
* // context.userId is typed from beforeLoad return
|
|
197
|
-
* return { user: context.userId };
|
|
198
|
-
* },
|
|
199
|
-
* });
|
|
200
|
-
* ```
|
|
201
|
-
*/
|
|
202
|
-
export function traceBeforeLoad<TBeforeLoadFn extends (opts: any) => any>(
|
|
203
|
-
beforeLoadFn: TBeforeLoadFn,
|
|
204
|
-
config: TraceLoaderConfig = {},
|
|
205
|
-
): TBeforeLoadFn {
|
|
206
|
-
const captureParams = config.captureParams ?? true;
|
|
207
|
-
|
|
208
|
-
const wrapped = (input: TanStackContextInternal) => {
|
|
209
|
-
// Skip tracing in browser
|
|
210
|
-
if (!isServerSide()) {
|
|
211
|
-
return beforeLoadFn(input);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const routeId = input?.route?.id || 'unknown';
|
|
215
|
-
const spanName = config.name || `tanstack.beforeLoad.${routeId}`;
|
|
216
|
-
|
|
217
|
-
// Handle both sync and async beforeLoad
|
|
218
|
-
const result = beforeLoadFn(input);
|
|
219
|
-
const isPromise = result instanceof Promise;
|
|
220
|
-
|
|
221
|
-
if (!isPromise) {
|
|
222
|
-
// Sync beforeLoad
|
|
223
|
-
return trace(spanName, (ctx: TraceContext) => {
|
|
224
|
-
ctx.setAttributes({
|
|
225
|
-
[SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'beforeLoad',
|
|
226
|
-
[SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,
|
|
227
|
-
[SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'beforeLoad',
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
if (captureParams && input?.params) {
|
|
231
|
-
try {
|
|
232
|
-
ctx.setAttribute(
|
|
233
|
-
SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,
|
|
234
|
-
JSON.stringify(input.params),
|
|
235
|
-
);
|
|
236
|
-
} catch {
|
|
237
|
-
ctx.setAttribute(
|
|
238
|
-
SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,
|
|
239
|
-
'[non-serializable]',
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
ctx.setStatus({ code: SpanStatusCode.OK });
|
|
245
|
-
return result;
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Async beforeLoad
|
|
250
|
-
return trace(spanName, async (ctx: TraceContext) => {
|
|
251
|
-
ctx.setAttributes({
|
|
252
|
-
[SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'beforeLoad',
|
|
253
|
-
[SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,
|
|
254
|
-
[SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'beforeLoad',
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
if (captureParams && input?.params) {
|
|
258
|
-
try {
|
|
259
|
-
ctx.setAttribute(
|
|
260
|
-
SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,
|
|
261
|
-
JSON.stringify(input.params),
|
|
262
|
-
);
|
|
263
|
-
} catch {
|
|
264
|
-
ctx.setAttribute(
|
|
265
|
-
SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,
|
|
266
|
-
'[non-serializable]',
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const asyncResult = await result;
|
|
273
|
-
ctx.setStatus({ code: SpanStatusCode.OK });
|
|
274
|
-
return asyncResult;
|
|
275
|
-
} catch (error) {
|
|
276
|
-
// Check if this is a redirect or notFound (expected control flow)
|
|
277
|
-
const errorName = (error as Error).name;
|
|
278
|
-
if (errorName === 'RedirectError' || errorName === 'NotFoundError') {
|
|
279
|
-
// Mark as OK since these are expected control flow
|
|
280
|
-
ctx.setAttribute('tanstack.beforeLoad.redirect', true);
|
|
281
|
-
ctx.setStatus({ code: SpanStatusCode.OK });
|
|
282
|
-
} else {
|
|
283
|
-
if ('recordError' in ctx && typeof ctx.recordError === 'function') {
|
|
284
|
-
ctx.recordError(error);
|
|
285
|
-
} else if (
|
|
286
|
-
'recordException' in ctx &&
|
|
287
|
-
typeof ctx.recordException === 'function'
|
|
288
|
-
) {
|
|
289
|
-
ctx.recordException(error);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
return wrapped as TBeforeLoadFn;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Create a traced route configuration helper
|
|
302
|
-
*
|
|
303
|
-
* This higher-order function helps create route configurations
|
|
304
|
-
* with automatic tracing for both loader and beforeLoad.
|
|
305
|
-
*
|
|
306
|
-
* @param routeId - The route identifier
|
|
307
|
-
* @param config - Tracing configuration
|
|
308
|
-
* @returns Object with traced loader and beforeLoad wrappers
|
|
309
|
-
*
|
|
310
|
-
* @example
|
|
311
|
-
* ```typescript
|
|
312
|
-
* import { createFileRoute } from '@tanstack/react-router';
|
|
313
|
-
* import { createTracedRoute } from 'autotel-tanstack/loaders';
|
|
314
|
-
*
|
|
315
|
-
* const traced = createTracedRoute('/users/$userId');
|
|
316
|
-
*
|
|
317
|
-
* export const Route = createFileRoute('/users/$userId')({
|
|
318
|
-
* beforeLoad: traced.beforeLoad(async ({ context }) => {
|
|
319
|
-
* // Auth check
|
|
320
|
-
* }),
|
|
321
|
-
* loader: traced.loader(async ({ params }) => {
|
|
322
|
-
* return await getUser(params.userId);
|
|
323
|
-
* }),
|
|
324
|
-
* });
|
|
325
|
-
* ```
|
|
326
|
-
*/
|
|
327
|
-
export function createTracedRoute(
|
|
328
|
-
routeId: string,
|
|
329
|
-
config: Omit<TraceLoaderConfig, 'name'> = {},
|
|
330
|
-
) {
|
|
331
|
-
return {
|
|
332
|
-
/**
|
|
333
|
-
* Wrap a loader function with tracing
|
|
334
|
-
*/
|
|
335
|
-
loader<TLoaderFn extends (ctx: any) => any>(
|
|
336
|
-
loaderFn: TLoaderFn,
|
|
337
|
-
): TLoaderFn {
|
|
338
|
-
return traceLoader(loaderFn, {
|
|
339
|
-
...config,
|
|
340
|
-
name: `tanstack.loader.${routeId}`,
|
|
341
|
-
});
|
|
342
|
-
},
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Wrap a beforeLoad function with tracing
|
|
346
|
-
*/
|
|
347
|
-
beforeLoad<TBeforeLoadFn extends (opts: any) => any>(
|
|
348
|
-
beforeLoadFn: TBeforeLoadFn,
|
|
349
|
-
): TBeforeLoadFn {
|
|
350
|
-
return traceBeforeLoad(beforeLoadFn, {
|
|
351
|
-
...config,
|
|
352
|
-
name: `tanstack.beforeLoad.${routeId}`,
|
|
353
|
-
});
|
|
354
|
-
},
|
|
355
|
-
};
|
|
356
|
-
}
|