autotel-tanstack 1.13.44 → 1.13.45

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/dist/context.d.ts CHANGED
@@ -91,6 +91,19 @@ declare function runInContext<T>(parentContext: Context, fn: () => T): T;
91
91
  * @returns The current active OpenTelemetry context
92
92
  */
93
93
  declare function getActiveContext(): Context;
94
+ /**
95
+ * The `traceparent` header value for the active context, or `''` when there is
96
+ * no active span. Use it to log the id you send downstream.
97
+ */
98
+ declare function getTraceParent(ctx?: Context): string;
99
+ /**
100
+ * The `tracestate` header value for the active context, or `''` when unset.
101
+ */
102
+ declare function getTraceState(ctx?: Context): string;
103
+ /** Trace id of the active span, or `undefined` outside a span. */
104
+ declare function getCurrentTraceId(ctx?: Context): string | undefined;
105
+ /** Span id of the active span, or `undefined` outside a span. */
106
+ declare function getCurrentSpanId(ctx?: Context): string | undefined;
94
107
  //#endregion
95
- export { HeadersInitType, createTracedHeaders, extractContextFromRequest, getActiveContext, injectContextToHeaders, runInContext };
108
+ export { HeadersInitType, createTracedHeaders, extractContextFromRequest, getActiveContext, getCurrentSpanId, getCurrentTraceId, getTraceParent, getTraceState, injectContextToHeaders, runInContext };
96
109
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","names":[],"sources":["../src/context.ts"],"mappings":";;;;;;KAWY,kBACV,UAAU;;;;;;;;;;;;;;;;;;;;iBAqBI,0BAA0B,SAAS,UAAU;;;;;;;;;;;;;;;;;;;;iBAuC7C,uBACd,SAAS,SACT,MAAM,UACL;;;;;;;;;;;;;;;;;;;;;iBA+Ba,oBACd,kBAAkB,iBAClB,MAAM,UACL;;;;;;;;;;;;;;;;;;;iBAuBa,aAAa,GAAG,eAAe,SAAS,UAAU,IAAI;;;;;;iBAStD,oBAAoB"}
1
+ {"version":3,"file":"context.d.ts","names":[],"sources":["../src/context.ts"],"mappings":";;;;;;KAYY,kBACV,UAAU;;;;;;;;;;;;;;;;;;;;iBAqBI,0BAA0B,SAAS,UAAU;;;;;;;;;;;;;;;;;;;;iBAuC7C,uBACd,SAAS,SACT,MAAM,UACL;;;;;;;;;;;;;;;;;;;;;iBA+Ba,oBACd,kBAAkB,iBAClB,MAAM,UACL;;;;;;;;;;;;;;;;;;;iBAuBa,aAAa,GAAG,eAAe,SAAS,UAAU,IAAI;;;;;;iBAStD,oBAAoB;;;;;iBAQpB,eAAe,MAAM;;;;iBASrB,cAAc,MAAM;;iBAOpB,kBAAkB,MAAM;;iBAKxB,iBAAiB,MAAM"}
package/dist/context.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ROOT_CONTEXT, context, propagation } from "@opentelemetry/api";
1
+ import { ROOT_CONTEXT, context, propagation, trace } from "@opentelemetry/api";
2
2
 
3
3
  //#region src/context.ts
4
4
  /**
@@ -108,7 +108,32 @@ function runInContext(parentContext, fn) {
108
108
  function getActiveContext() {
109
109
  return context.active();
110
110
  }
111
+ /**
112
+ * The `traceparent` header value for the active context, or `''` when there is
113
+ * no active span. Use it to log the id you send downstream.
114
+ */
115
+ function getTraceParent(ctx) {
116
+ const carrier = {};
117
+ propagation.inject(ctx ?? context.active(), carrier);
118
+ return carrier.traceparent ?? "";
119
+ }
120
+ /**
121
+ * The `tracestate` header value for the active context, or `''` when unset.
122
+ */
123
+ function getTraceState(ctx) {
124
+ const carrier = {};
125
+ propagation.inject(ctx ?? context.active(), carrier);
126
+ return carrier.tracestate ?? "";
127
+ }
128
+ /** Trace id of the active span, or `undefined` outside a span. */
129
+ function getCurrentTraceId(ctx) {
130
+ return trace.getSpanContext(ctx ?? context.active())?.traceId;
131
+ }
132
+ /** Span id of the active span, or `undefined` outside a span. */
133
+ function getCurrentSpanId(ctx) {
134
+ return trace.getSpanContext(ctx ?? context.active())?.spanId;
135
+ }
111
136
 
112
137
  //#endregion
113
- export { createTracedHeaders, extractContextFromRequest, getActiveContext, injectContextToHeaders, runInContext };
138
+ export { createTracedHeaders, extractContextFromRequest, getActiveContext, getCurrentSpanId, getCurrentTraceId, getTraceParent, getTraceState, injectContextToHeaders, runInContext };
114
139
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":[],"sources":["../src/context.ts"],"sourcesContent":["import {\n context,\n propagation,\n type Context,\n ROOT_CONTEXT,\n} from '@opentelemetry/api';\n\n/**\n * Type representing values that can be used to initialize Headers\n * This is equivalent to the DOM's HeadersInit type but works in Node.js\n */\nexport type HeadersInitType =\n Headers | Record<string, string> | [string, string][];\n\n/**\n * Extract OpenTelemetry context from HTTP request headers\n *\n * This function extracts W3C Trace Context (traceparent, tracestate)\n * and Baggage from request headers to enable distributed tracing.\n *\n * @param request - The incoming HTTP request\n * @returns OpenTelemetry context with extracted trace information\n *\n * @example\n * ```typescript\n * const parentContext = extractContextFromRequest(request);\n * context.with(parentContext, async () => {\n * // Spans created here will be children of the extracted context\n * await trace('my-operation', async ()=> {\n const ctx = getActiveTraceContext()!; ... })();\n * });\n * ```\n */\nexport function extractContextFromRequest(request: Request): Context {\n const carrier: Record<string, string> = {};\n\n // Extract W3C Trace Context headers\n const traceparent = request.headers.get('traceparent');\n const tracestate = request.headers.get('tracestate');\n const baggage = request.headers.get('baggage');\n\n if (traceparent) carrier.traceparent = traceparent;\n if (tracestate) carrier.tracestate = tracestate;\n if (baggage) carrier.baggage = baggage;\n\n // Return ROOT_CONTEXT if no trace headers present\n if (Object.keys(carrier).length === 0) {\n return ROOT_CONTEXT;\n }\n\n return propagation.extract(context.active(), carrier);\n}\n\n/**\n * Inject OpenTelemetry context into HTTP headers\n *\n * This function injects W3C Trace Context (traceparent, tracestate)\n * and Baggage into headers for outgoing requests.\n *\n * @param headers - Headers object to inject context into\n * @param ctx - Optional context to inject (defaults to active context)\n * @returns The headers object with injected trace context\n *\n * @example\n * ```typescript\n * const headers = new Headers();\n * injectContextToHeaders(headers);\n *\n * // Now use headers in outgoing fetch\n * await fetch('https://api.example.com', { headers });\n * ```\n */\nexport function injectContextToHeaders(\n headers: Headers,\n ctx?: Context,\n): Headers {\n const carrier: Record<string, string> = {};\n propagation.inject(ctx ?? context.active(), carrier);\n\n for (const [key, value] of Object.entries(carrier)) {\n headers.set(key, value);\n }\n\n return headers;\n}\n\n/**\n * Create a new Headers object with injected trace context\n *\n * Convenience function that creates a new Headers object\n * with the current trace context already injected.\n *\n * @param existingHeaders - Optional existing headers to include\n * @param ctx - Optional context to inject (defaults to active context)\n * @returns New Headers object with trace context\n *\n * @example\n * ```typescript\n * const headers = createTracedHeaders({ 'Content-Type': 'application/json' });\n * await fetch('https://api.example.com', {\n * method: 'POST',\n * headers,\n * body: JSON.stringify(data),\n * });\n * ```\n */\nexport function createTracedHeaders(\n existingHeaders?: HeadersInitType,\n ctx?: Context,\n): Headers {\n const headers = new Headers(existingHeaders);\n return injectContextToHeaders(headers, ctx);\n}\n\n/**\n * Run a function within a specific OpenTelemetry context\n *\n * This is a convenience wrapper around context.with() that\n * provides better TypeScript inference.\n *\n * @param parentContext - The context to run within\n * @param fn - The function to execute\n * @returns The result of the function\n *\n * @example\n * ```typescript\n * const parentContext = extractContextFromRequest(request);\n * const result = await runInContext(parentContext, async () => {\n * return await processRequest();\n * });\n * ```\n */\nexport function runInContext<T>(parentContext: Context, fn: () => T): T {\n return context.with(parentContext, fn);\n}\n\n/**\n * Get the current active context\n *\n * @returns The current active OpenTelemetry context\n */\nexport function getActiveContext(): Context {\n return context.active();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,0BAA0B,SAA2B;CACnE,MAAM,UAAkC,CAAC;CAGzC,MAAM,cAAc,QAAQ,QAAQ,IAAI,aAAa;CACrD,MAAM,aAAa,QAAQ,QAAQ,IAAI,YAAY;CACnD,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;CAE7C,IAAI,aAAa,QAAQ,cAAc;CACvC,IAAI,YAAY,QAAQ,aAAa;CACrC,IAAI,SAAS,QAAQ,UAAU;CAG/B,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW,GAClC,OAAO;CAGT,OAAO,YAAY,QAAQ,QAAQ,OAAO,GAAG,OAAO;AACtD;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,uBACd,SACA,KACS;CACT,MAAM,UAAkC,CAAC;CACzC,YAAY,OAAO,OAAO,QAAQ,OAAO,GAAG,OAAO;CAEnD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAC/C,QAAQ,IAAI,KAAK,KAAK;CAGxB,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,oBACd,iBACA,KACS;CAET,OAAO,uBAAuB,IADV,QAAQ,eACQ,GAAG,GAAG;AAC5C;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aAAgB,eAAwB,IAAgB;CACtE,OAAO,QAAQ,KAAK,eAAe,EAAE;AACvC;;;;;;AAOA,SAAgB,mBAA4B;CAC1C,OAAO,QAAQ,OAAO;AACxB"}
1
+ {"version":3,"file":"context.js","names":["otelTrace"],"sources":["../src/context.ts"],"sourcesContent":["import {\n context,\n propagation,\n trace as otelTrace,\n type Context,\n ROOT_CONTEXT,\n} from '@opentelemetry/api';\n\n/**\n * Type representing values that can be used to initialize Headers\n * This is equivalent to the DOM's HeadersInit type but works in Node.js\n */\nexport type HeadersInitType =\n Headers | Record<string, string> | [string, string][];\n\n/**\n * Extract OpenTelemetry context from HTTP request headers\n *\n * This function extracts W3C Trace Context (traceparent, tracestate)\n * and Baggage from request headers to enable distributed tracing.\n *\n * @param request - The incoming HTTP request\n * @returns OpenTelemetry context with extracted trace information\n *\n * @example\n * ```typescript\n * const parentContext = extractContextFromRequest(request);\n * context.with(parentContext, async () => {\n * // Spans created here will be children of the extracted context\n * await trace('my-operation', async ()=> {\n const ctx = getActiveTraceContext()!; ... })();\n * });\n * ```\n */\nexport function extractContextFromRequest(request: Request): Context {\n const carrier: Record<string, string> = {};\n\n // Extract W3C Trace Context headers\n const traceparent = request.headers.get('traceparent');\n const tracestate = request.headers.get('tracestate');\n const baggage = request.headers.get('baggage');\n\n if (traceparent) carrier.traceparent = traceparent;\n if (tracestate) carrier.tracestate = tracestate;\n if (baggage) carrier.baggage = baggage;\n\n // Return ROOT_CONTEXT if no trace headers present\n if (Object.keys(carrier).length === 0) {\n return ROOT_CONTEXT;\n }\n\n return propagation.extract(context.active(), carrier);\n}\n\n/**\n * Inject OpenTelemetry context into HTTP headers\n *\n * This function injects W3C Trace Context (traceparent, tracestate)\n * and Baggage into headers for outgoing requests.\n *\n * @param headers - Headers object to inject context into\n * @param ctx - Optional context to inject (defaults to active context)\n * @returns The headers object with injected trace context\n *\n * @example\n * ```typescript\n * const headers = new Headers();\n * injectContextToHeaders(headers);\n *\n * // Now use headers in outgoing fetch\n * await fetch('https://api.example.com', { headers });\n * ```\n */\nexport function injectContextToHeaders(\n headers: Headers,\n ctx?: Context,\n): Headers {\n const carrier: Record<string, string> = {};\n propagation.inject(ctx ?? context.active(), carrier);\n\n for (const [key, value] of Object.entries(carrier)) {\n headers.set(key, value);\n }\n\n return headers;\n}\n\n/**\n * Create a new Headers object with injected trace context\n *\n * Convenience function that creates a new Headers object\n * with the current trace context already injected.\n *\n * @param existingHeaders - Optional existing headers to include\n * @param ctx - Optional context to inject (defaults to active context)\n * @returns New Headers object with trace context\n *\n * @example\n * ```typescript\n * const headers = createTracedHeaders({ 'Content-Type': 'application/json' });\n * await fetch('https://api.example.com', {\n * method: 'POST',\n * headers,\n * body: JSON.stringify(data),\n * });\n * ```\n */\nexport function createTracedHeaders(\n existingHeaders?: HeadersInitType,\n ctx?: Context,\n): Headers {\n const headers = new Headers(existingHeaders);\n return injectContextToHeaders(headers, ctx);\n}\n\n/**\n * Run a function within a specific OpenTelemetry context\n *\n * This is a convenience wrapper around context.with() that\n * provides better TypeScript inference.\n *\n * @param parentContext - The context to run within\n * @param fn - The function to execute\n * @returns The result of the function\n *\n * @example\n * ```typescript\n * const parentContext = extractContextFromRequest(request);\n * const result = await runInContext(parentContext, async () => {\n * return await processRequest();\n * });\n * ```\n */\nexport function runInContext<T>(parentContext: Context, fn: () => T): T {\n return context.with(parentContext, fn);\n}\n\n/**\n * Get the current active context\n *\n * @returns The current active OpenTelemetry context\n */\nexport function getActiveContext(): Context {\n return context.active();\n}\n\n/**\n * The `traceparent` header value for the active context, or `''` when there is\n * no active span. Use it to log the id you send downstream.\n */\nexport function getTraceParent(ctx?: Context): string {\n const carrier: Record<string, string> = {};\n propagation.inject(ctx ?? context.active(), carrier);\n return carrier.traceparent ?? '';\n}\n\n/**\n * The `tracestate` header value for the active context, or `''` when unset.\n */\nexport function getTraceState(ctx?: Context): string {\n const carrier: Record<string, string> = {};\n propagation.inject(ctx ?? context.active(), carrier);\n return carrier.tracestate ?? '';\n}\n\n/** Trace id of the active span, or `undefined` outside a span. */\nexport function getCurrentTraceId(ctx?: Context): string | undefined {\n return otelTrace.getSpanContext(ctx ?? context.active())?.traceId;\n}\n\n/** Span id of the active span, or `undefined` outside a span. */\nexport function getCurrentSpanId(ctx?: Context): string | undefined {\n return otelTrace.getSpanContext(ctx ?? context.active())?.spanId;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,0BAA0B,SAA2B;CACnE,MAAM,UAAkC,CAAC;CAGzC,MAAM,cAAc,QAAQ,QAAQ,IAAI,aAAa;CACrD,MAAM,aAAa,QAAQ,QAAQ,IAAI,YAAY;CACnD,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;CAE7C,IAAI,aAAa,QAAQ,cAAc;CACvC,IAAI,YAAY,QAAQ,aAAa;CACrC,IAAI,SAAS,QAAQ,UAAU;CAG/B,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW,GAClC,OAAO;CAGT,OAAO,YAAY,QAAQ,QAAQ,OAAO,GAAG,OAAO;AACtD;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,uBACd,SACA,KACS;CACT,MAAM,UAAkC,CAAC;CACzC,YAAY,OAAO,OAAO,QAAQ,OAAO,GAAG,OAAO;CAEnD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,GAC/C,QAAQ,IAAI,KAAK,KAAK;CAGxB,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,oBACd,iBACA,KACS;CAET,OAAO,uBAAuB,IADV,QAAQ,eACQ,GAAG,GAAG;AAC5C;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aAAgB,eAAwB,IAAgB;CACtE,OAAO,QAAQ,KAAK,eAAe,EAAE;AACvC;;;;;;AAOA,SAAgB,mBAA4B;CAC1C,OAAO,QAAQ,OAAO;AACxB;;;;;AAMA,SAAgB,eAAe,KAAuB;CACpD,MAAM,UAAkC,CAAC;CACzC,YAAY,OAAO,OAAO,QAAQ,OAAO,GAAG,OAAO;CACnD,OAAO,QAAQ,eAAe;AAChC;;;;AAKA,SAAgB,cAAc,KAAuB;CACnD,MAAM,UAAkC,CAAC;CACzC,YAAY,OAAO,OAAO,QAAQ,OAAO,GAAG,OAAO;CACnD,OAAO,QAAQ,cAAc;AAC/B;;AAGA,SAAgB,kBAAkB,KAAmC;CACnE,OAAOA,MAAU,eAAe,OAAO,QAAQ,OAAO,CAAC,CAAC,EAAE;AAC5D;;AAGA,SAAgB,iBAAiB,KAAmC;CAClE,OAAOA,MAAU,eAAe,OAAO,QAAQ,OAAO,CAAC,CAAC,EAAE;AAC5D"}
package/dist/handlers.js CHANGED
@@ -2,7 +2,7 @@ import { i as SPAN_ATTRIBUTES, n as isRealError, r as DEFAULT_CONFIG, t as isCon
2
2
  import { extractContextFromRequest } from "./context.js";
3
3
  import { t as isExcludedPath } from "./route-filter-dLg-j3jR.js";
4
4
  import { SpanStatusCode, context } from "@opentelemetry/api";
5
- import { getActiveTraceContext, init, trace } from "autotel";
5
+ import { getActiveTraceContext, init, trace as trace$1 } from "autotel";
6
6
 
7
7
  //#region src/handlers.ts
8
8
  /**
@@ -63,7 +63,7 @@ function wrapStartHandler(config = {}) {
63
63
  const parentContext = extractContextFromRequest(request);
64
64
  return context.with(parentContext, async () => {
65
65
  const spanName = `${request.method} ${url.pathname}`;
66
- return trace({
66
+ return trace$1({
67
67
  name: spanName,
68
68
  isError: isRealError
69
69
  }, async () => {
@@ -153,7 +153,7 @@ function createTracedHandler(config = {}) {
153
153
  const parentContext = extractContextFromRequest(request);
154
154
  return context.with(parentContext, async () => {
155
155
  const spanName = `${request.method} ${url.pathname}`;
156
- return trace({
156
+ return trace$1({
157
157
  name: spanName,
158
158
  isError: isRealError
159
159
  }, async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.js","names":[],"sources":["../src/handlers.ts"],"sourcesContent":["import { context, SpanStatusCode } from '@opentelemetry/api';\nimport { trace, init, getActiveTraceContext } from 'autotel';\nimport { extractContextFromRequest } from './context';\nimport { isControlFlowSignal, isRealError } from './control-flow';\nimport { isExcludedPath } from './route-filter';\nimport {\n type WrapStartHandlerConfig,\n DEFAULT_CONFIG,\n SPAN_ATTRIBUTES,\n} from './types';\n\n/**\n * Request handler type (compatible with TanStack Start handlers)\n */\ntype RequestHandler = (\n request: Request,\n opts?: { context?: Record<string, unknown> },\n) => Promise<Response> | Response;\n\n/**\n * Wrap a TanStack Start handler with OpenTelemetry tracing\n *\n * This function wraps the entire request handler to automatically create\n * spans for all incoming requests. It initializes OpenTelemetry and\n * provides comprehensive request tracing.\n *\n * @param config - Configuration options including OTLP endpoint and headers\n * @returns Function that wraps a request handler\n *\n * @example\n * ```typescript\n * // server.ts\n * import { createStartHandler, defaultStreamHandler } from '@tanstack/react-start/server';\n * import { wrapStartHandler } from 'autotel-tanstack/handlers';\n *\n * export default wrapStartHandler({\n * service: 'my-app',\n * endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,\n * headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY },\n * })(createStartHandler(defaultStreamHandler));\n * ```\n *\n * @example\n * ```typescript\n * // With env var configuration (recommended for production)\n * // Set OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS\n * export default wrapStartHandler()(createStartHandler(defaultStreamHandler));\n * ```\n */\nexport function wrapStartHandler(\n config: WrapStartHandlerConfig = {},\n): (handler: RequestHandler) => RequestHandler {\n const mergedConfig = { ...DEFAULT_CONFIG, ...config };\n\n // Initialize autotel with provided configuration\n const service =\n config.service || process.env.OTEL_SERVICE_NAME || 'tanstack-start';\n const endpoint = config.endpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;\n\n // Parse headers from env if not provided\n let headers = config.headers;\n if (!headers && process.env.OTEL_EXPORTER_OTLP_HEADERS) {\n headers = {};\n const pairs = process.env.OTEL_EXPORTER_OTLP_HEADERS.split(',');\n for (const pair of pairs) {\n const [key, value] = pair.split('=');\n if (key && value) {\n headers[key.trim()] = value.trim();\n }\n }\n }\n\n // Initialize OpenTelemetry\n init({\n service,\n endpoint,\n headers,\n });\n\n return function wrapHandler(handler: RequestHandler): RequestHandler {\n return async function tracedHandler(\n request: Request,\n opts?: { context?: Record<string, unknown> },\n ): Promise<Response> {\n const url = new URL(request.url);\n\n // Check if path should be excluded\n if (isExcludedPath(url.pathname, mergedConfig.excludePaths)) {\n return handler(request, opts);\n }\n\n // Extract parent context from request headers\n const parentContext = extractContextFromRequest(request);\n\n // Run within parent context\n return context.with(parentContext, async () => {\n const spanName = `${request.method} ${url.pathname}`;\n\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n // Set HTTP semantic attributes\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.HTTP_REQUEST_METHOD]: request.method,\n [SPAN_ATTRIBUTES.URL_PATH]: url.pathname,\n [SPAN_ATTRIBUTES.URL_FULL]: request.url,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'request',\n });\n\n if (url.search) {\n ctx.setAttribute(SPAN_ATTRIBUTES.URL_QUERY, url.search);\n }\n\n // Capture configured headers\n if (mergedConfig.captureHeaders) {\n for (const header of mergedConfig.captureHeaders) {\n const value = request.headers.get(header);\n if (value) {\n ctx.setAttribute(\n `http.request.header.${header.toLowerCase()}`,\n value,\n );\n }\n }\n }\n\n // Add custom attributes\n if (config.customAttributes) {\n const customAttrs = config.customAttributes({\n type: 'request',\n name: spanName,\n request,\n });\n ctx.setAttributes(\n customAttrs as Record<string, string | number | boolean>,\n );\n }\n\n const startTime = Date.now();\n\n try {\n const response = await handler(request, opts);\n const duration = Date.now() - startTime;\n\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n ctx.setAttribute(\n SPAN_ATTRIBUTES.HTTP_RESPONSE_STATUS_CODE,\n response.status,\n );\n\n // Set status based on HTTP status code\n if (response.status >= 400) {\n ctx.setStatus({\n code: SpanStatusCode.ERROR,\n message: `HTTP ${response.status}`,\n });\n } else {\n ctx.setStatus({ code: SpanStatusCode.OK });\n }\n\n return response;\n } catch (error) {\n const duration = Date.now() - startTime;\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n\n if (mergedConfig.captureErrors) {\n ctx.recordError(error);\n }\n\n throw error;\n }\n })();\n });\n };\n };\n}\n\n/**\n * Create a traced handler without auto-initialization\n *\n * Use this when you want to initialize autotel separately\n * (e.g., with more advanced configuration).\n *\n * @param config - Configuration options (excluding endpoint/headers)\n * @returns Function that wraps a request handler\n *\n * @example\n * ```typescript\n * import { init } from 'autotel';\n * import { createTracedHandler } from 'autotel-tanstack/handlers';\n *\n * // Initialize autotel with custom configuration\n * init({\n * service: 'my-app',\n * endpoint: 'https://api.honeycomb.io',\n * instrumentations: [/* custom instrumentations *\\/],\n * });\n *\n * // Wrap handler without re-initializing\n * export default createTracedHandler({\n * captureHeaders: ['x-request-id'],\n * })(createStartHandler(defaultStreamHandler));\n * ```\n */\nexport function createTracedHandler(\n config: Omit<WrapStartHandlerConfig, 'endpoint' | 'headers' | 'service'> = {},\n): (handler: RequestHandler) => RequestHandler {\n const mergedConfig = { ...DEFAULT_CONFIG, ...config };\n\n return function wrapHandler(handler: RequestHandler): RequestHandler {\n return async function tracedHandler(\n request: Request,\n opts?: { context?: Record<string, unknown> },\n ): Promise<Response> {\n const url = new URL(request.url);\n\n // Check if path should be excluded\n if (isExcludedPath(url.pathname, mergedConfig.excludePaths)) {\n return handler(request, opts);\n }\n\n const parentContext = extractContextFromRequest(request);\n\n return context.with(parentContext, async () => {\n const spanName = `${request.method} ${url.pathname}`;\n\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.HTTP_REQUEST_METHOD]: request.method,\n [SPAN_ATTRIBUTES.URL_PATH]: url.pathname,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'request',\n });\n\n if (url.search) {\n ctx.setAttribute(SPAN_ATTRIBUTES.URL_QUERY, url.search);\n }\n\n if (mergedConfig.captureHeaders) {\n for (const header of mergedConfig.captureHeaders) {\n const value = request.headers.get(header);\n if (value) {\n ctx.setAttribute(\n `http.request.header.${header.toLowerCase()}`,\n value,\n );\n }\n }\n }\n\n if (config.customAttributes) {\n const customAttrs = config.customAttributes({\n type: 'request',\n name: spanName,\n request,\n });\n ctx.setAttributes(\n customAttrs as Record<string, string | number | boolean>,\n );\n }\n\n const startTime = Date.now();\n\n try {\n const response = await handler(request, opts);\n const duration = Date.now() - startTime;\n\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n ctx.setAttribute(\n SPAN_ATTRIBUTES.HTTP_RESPONSE_STATUS_CODE,\n response.status,\n );\n\n if (response.status >= 400) {\n ctx.setStatus({\n code: SpanStatusCode.ERROR,\n message: `HTTP ${response.status}`,\n });\n } else {\n ctx.setStatus({ code: SpanStatusCode.OK });\n }\n\n return response;\n } catch (error) {\n const duration = Date.now() - startTime;\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n\n if (mergedConfig.captureErrors) {\n ctx.recordError(error);\n }\n\n throw error;\n }\n })();\n });\n };\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"handlers.js","names":["trace"],"sources":["../src/handlers.ts"],"sourcesContent":["import { context, SpanStatusCode } from '@opentelemetry/api';\nimport { trace, init, getActiveTraceContext } from 'autotel';\nimport { extractContextFromRequest } from './context';\nimport { isControlFlowSignal, isRealError } from './control-flow';\nimport { isExcludedPath } from './route-filter';\nimport {\n type WrapStartHandlerConfig,\n DEFAULT_CONFIG,\n SPAN_ATTRIBUTES,\n} from './types';\n\n/**\n * Request handler type (compatible with TanStack Start handlers)\n */\ntype RequestHandler = (\n request: Request,\n opts?: { context?: Record<string, unknown> },\n) => Promise<Response> | Response;\n\n/**\n * Wrap a TanStack Start handler with OpenTelemetry tracing\n *\n * This function wraps the entire request handler to automatically create\n * spans for all incoming requests. It initializes OpenTelemetry and\n * provides comprehensive request tracing.\n *\n * @param config - Configuration options including OTLP endpoint and headers\n * @returns Function that wraps a request handler\n *\n * @example\n * ```typescript\n * // server.ts\n * import { createStartHandler, defaultStreamHandler } from '@tanstack/react-start/server';\n * import { wrapStartHandler } from 'autotel-tanstack/handlers';\n *\n * export default wrapStartHandler({\n * service: 'my-app',\n * endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,\n * headers: { 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY },\n * })(createStartHandler(defaultStreamHandler));\n * ```\n *\n * @example\n * ```typescript\n * // With env var configuration (recommended for production)\n * // Set OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS\n * export default wrapStartHandler()(createStartHandler(defaultStreamHandler));\n * ```\n */\nexport function wrapStartHandler(\n config: WrapStartHandlerConfig = {},\n): (handler: RequestHandler) => RequestHandler {\n const mergedConfig = { ...DEFAULT_CONFIG, ...config };\n\n // Initialize autotel with provided configuration\n const service =\n config.service || process.env.OTEL_SERVICE_NAME || 'tanstack-start';\n const endpoint = config.endpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;\n\n // Parse headers from env if not provided\n let headers = config.headers;\n if (!headers && process.env.OTEL_EXPORTER_OTLP_HEADERS) {\n headers = {};\n const pairs = process.env.OTEL_EXPORTER_OTLP_HEADERS.split(',');\n for (const pair of pairs) {\n const [key, value] = pair.split('=');\n if (key && value) {\n headers[key.trim()] = value.trim();\n }\n }\n }\n\n // Initialize OpenTelemetry\n init({\n service,\n endpoint,\n headers,\n });\n\n return function wrapHandler(handler: RequestHandler): RequestHandler {\n return async function tracedHandler(\n request: Request,\n opts?: { context?: Record<string, unknown> },\n ): Promise<Response> {\n const url = new URL(request.url);\n\n // Check if path should be excluded\n if (isExcludedPath(url.pathname, mergedConfig.excludePaths)) {\n return handler(request, opts);\n }\n\n // Extract parent context from request headers\n const parentContext = extractContextFromRequest(request);\n\n // Run within parent context\n return context.with(parentContext, async () => {\n const spanName = `${request.method} ${url.pathname}`;\n\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n // Set HTTP semantic attributes\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.HTTP_REQUEST_METHOD]: request.method,\n [SPAN_ATTRIBUTES.URL_PATH]: url.pathname,\n [SPAN_ATTRIBUTES.URL_FULL]: request.url,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'request',\n });\n\n if (url.search) {\n ctx.setAttribute(SPAN_ATTRIBUTES.URL_QUERY, url.search);\n }\n\n // Capture configured headers\n if (mergedConfig.captureHeaders) {\n for (const header of mergedConfig.captureHeaders) {\n const value = request.headers.get(header);\n if (value) {\n ctx.setAttribute(\n `http.request.header.${header.toLowerCase()}`,\n value,\n );\n }\n }\n }\n\n // Add custom attributes\n if (config.customAttributes) {\n const customAttrs = config.customAttributes({\n type: 'request',\n name: spanName,\n request,\n });\n ctx.setAttributes(\n customAttrs as Record<string, string | number | boolean>,\n );\n }\n\n const startTime = Date.now();\n\n try {\n const response = await handler(request, opts);\n const duration = Date.now() - startTime;\n\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n ctx.setAttribute(\n SPAN_ATTRIBUTES.HTTP_RESPONSE_STATUS_CODE,\n response.status,\n );\n\n // Set status based on HTTP status code\n if (response.status >= 400) {\n ctx.setStatus({\n code: SpanStatusCode.ERROR,\n message: `HTTP ${response.status}`,\n });\n } else {\n ctx.setStatus({ code: SpanStatusCode.OK });\n }\n\n return response;\n } catch (error) {\n const duration = Date.now() - startTime;\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n\n if (mergedConfig.captureErrors) {\n ctx.recordError(error);\n }\n\n throw error;\n }\n })();\n });\n };\n };\n}\n\n/**\n * Create a traced handler without auto-initialization\n *\n * Use this when you want to initialize autotel separately\n * (e.g., with more advanced configuration).\n *\n * @param config - Configuration options (excluding endpoint/headers)\n * @returns Function that wraps a request handler\n *\n * @example\n * ```typescript\n * import { init } from 'autotel';\n * import { createTracedHandler } from 'autotel-tanstack/handlers';\n *\n * // Initialize autotel with custom configuration\n * init({\n * service: 'my-app',\n * endpoint: 'https://api.honeycomb.io',\n * instrumentations: [/* custom instrumentations *\\/],\n * });\n *\n * // Wrap handler without re-initializing\n * export default createTracedHandler({\n * captureHeaders: ['x-request-id'],\n * })(createStartHandler(defaultStreamHandler));\n * ```\n */\nexport function createTracedHandler(\n config: Omit<WrapStartHandlerConfig, 'endpoint' | 'headers' | 'service'> = {},\n): (handler: RequestHandler) => RequestHandler {\n const mergedConfig = { ...DEFAULT_CONFIG, ...config };\n\n return function wrapHandler(handler: RequestHandler): RequestHandler {\n return async function tracedHandler(\n request: Request,\n opts?: { context?: Record<string, unknown> },\n ): Promise<Response> {\n const url = new URL(request.url);\n\n // Check if path should be excluded\n if (isExcludedPath(url.pathname, mergedConfig.excludePaths)) {\n return handler(request, opts);\n }\n\n const parentContext = extractContextFromRequest(request);\n\n return context.with(parentContext, async () => {\n const spanName = `${request.method} ${url.pathname}`;\n\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.HTTP_REQUEST_METHOD]: request.method,\n [SPAN_ATTRIBUTES.URL_PATH]: url.pathname,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'request',\n });\n\n if (url.search) {\n ctx.setAttribute(SPAN_ATTRIBUTES.URL_QUERY, url.search);\n }\n\n if (mergedConfig.captureHeaders) {\n for (const header of mergedConfig.captureHeaders) {\n const value = request.headers.get(header);\n if (value) {\n ctx.setAttribute(\n `http.request.header.${header.toLowerCase()}`,\n value,\n );\n }\n }\n }\n\n if (config.customAttributes) {\n const customAttrs = config.customAttributes({\n type: 'request',\n name: spanName,\n request,\n });\n ctx.setAttributes(\n customAttrs as Record<string, string | number | boolean>,\n );\n }\n\n const startTime = Date.now();\n\n try {\n const response = await handler(request, opts);\n const duration = Date.now() - startTime;\n\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n ctx.setAttribute(\n SPAN_ATTRIBUTES.HTTP_RESPONSE_STATUS_CODE,\n response.status,\n );\n\n if (response.status >= 400) {\n ctx.setStatus({\n code: SpanStatusCode.ERROR,\n message: `HTTP ${response.status}`,\n });\n } else {\n ctx.setStatus({ code: SpanStatusCode.OK });\n }\n\n return response;\n } catch (error) {\n const duration = Date.now() - startTime;\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n\n if (mergedConfig.captureErrors) {\n ctx.recordError(error);\n }\n\n throw error;\n }\n })();\n });\n };\n };\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import { a as TraceServerFnConfig, i as TraceLoaderConfig, n as SPAN_ATTRIBUTES,
3
3
  import { MiddlewareHandler, createTracingMiddleware, createTracingServerHandler, functionTracingMiddleware, tracingMiddleware } from "./middleware.js";
4
4
  import { createTracedServerFnFactory, traceServerFn } from "./server-functions.js";
5
5
  import { n as traceBeforeLoad, r as traceLoader, t as createTracedRoute } from "./loaders-DMUCHc32.js";
6
- import { createTracedHeaders, extractContextFromRequest, getActiveContext, injectContextToHeaders, runInContext } from "./context.js";
6
+ import { HeadersInitType, createTracedHeaders, extractContextFromRequest, getActiveContext, getCurrentSpanId, getCurrentTraceId, getTraceParent, getTraceState, injectContextToHeaders, runInContext } from "./context.js";
7
7
  import { DebugHeadersConfig, debugHeadersMiddleware } from "./debug-headers.js";
8
8
  import { ErrorReport, createErrorReportingHandler, errorStore, reportError, withErrorReporting } from "./error-reporting.js";
9
9
  import { createTracedHandler, wrapStartHandler } from "./handlers.js";
@@ -30,5 +30,5 @@ declare function isNode(): boolean;
30
30
  */
31
31
  declare function isServerSide(): boolean;
32
32
  //#endregion
33
- export { DEFAULT_CONFIG, type DebugHeadersConfig, type ErrorReport, type InstrumentOptions, type MiddlewareHandler, SPAN_ATTRIBUTES, type TanStackInstrumentationConfig, type TimingStats, type TraceLoaderConfig, type TraceServerFnConfig, type TracingMiddlewareConfig, type WrapStartHandlerConfig, createErrorReportingHandler, createMetricsHandler, createTracedHandler, createTracedHeaders, createTracedRoute, createTracedServerFnFactory, createTracingMiddleware, createTracingServerHandler, debugHeadersMiddleware, errorStore, extractContextFromRequest, flush, functionTracingMiddleware, getActiveContext, init, injectContextToHeaders, instrument, isBrowser, isNode, isServerSide, metricsCollector, recordTiming, reportError, runInContext, shutdown, span, trace, traceBeforeLoad, traceLoader, traceServerFn, tracingMiddleware, withErrorReporting, wrapStartHandler };
33
+ export { DEFAULT_CONFIG, type DebugHeadersConfig, type ErrorReport, type HeadersInitType, type InstrumentOptions, type MiddlewareHandler, SPAN_ATTRIBUTES, type TanStackInstrumentationConfig, type TimingStats, type TraceLoaderConfig, type TraceServerFnConfig, type TracingMiddlewareConfig, type WrapStartHandlerConfig, createErrorReportingHandler, createMetricsHandler, createTracedHandler, createTracedHeaders, createTracedRoute, createTracedServerFnFactory, createTracingMiddleware, createTracingServerHandler, debugHeadersMiddleware, errorStore, extractContextFromRequest, flush, functionTracingMiddleware, getActiveContext, getCurrentSpanId, getCurrentTraceId, getTraceParent, getTraceState, init, injectContextToHeaders, instrument, isBrowser, isNode, isServerSide, metricsCollector, recordTiming, reportError, runInContext, shutdown, span, trace, traceBeforeLoad, traceLoader, traceServerFn, tracingMiddleware, withErrorReporting, wrapStartHandler };
34
34
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { i as SPAN_ATTRIBUTES, r as DEFAULT_CONFIG } from "./control-flow-p-dnko0G.js";
2
- import { createTracedHeaders, extractContextFromRequest, getActiveContext, injectContextToHeaders, runInContext } from "./context.js";
2
+ import { createTracedHeaders, extractContextFromRequest, getActiveContext, getCurrentSpanId, getCurrentTraceId, getTraceParent, getTraceState, injectContextToHeaders, runInContext } from "./context.js";
3
3
  import { n as isNode, r as isServerSide, t as isBrowser } from "./env-BpFWNnpL.js";
4
4
  import { createTracingMiddleware, createTracingServerHandler, functionTracingMiddleware, tracingMiddleware } from "./middleware.js";
5
5
  import { createTracedServerFnFactory, traceServerFn } from "./server-functions.js";
@@ -11,4 +11,4 @@ import { createErrorReportingHandler, errorStore, reportError, withErrorReportin
11
11
  import { t as instrument } from "./instrument-DdLlMfRi.js";
12
12
  import { flush, init, shutdown, span, trace } from "autotel";
13
13
 
14
- export { DEFAULT_CONFIG, SPAN_ATTRIBUTES, createErrorReportingHandler, createMetricsHandler, createTracedHandler, createTracedHeaders, createTracedRoute, createTracedServerFnFactory, createTracingMiddleware, createTracingServerHandler, debugHeadersMiddleware, errorStore, extractContextFromRequest, flush, functionTracingMiddleware, getActiveContext, init, injectContextToHeaders, instrument, isBrowser, isNode, isServerSide, metricsCollector, recordTiming, reportError, runInContext, shutdown, span, trace, traceBeforeLoad, traceLoader, traceServerFn, tracingMiddleware, withErrorReporting, wrapStartHandler };
14
+ export { DEFAULT_CONFIG, SPAN_ATTRIBUTES, createErrorReportingHandler, createMetricsHandler, createTracedHandler, createTracedHeaders, createTracedRoute, createTracedServerFnFactory, createTracingMiddleware, createTracingServerHandler, debugHeadersMiddleware, errorStore, extractContextFromRequest, flush, functionTracingMiddleware, getActiveContext, getCurrentSpanId, getCurrentTraceId, getTraceParent, getTraceState, init, injectContextToHeaders, instrument, isBrowser, isNode, isServerSide, metricsCollector, recordTiming, reportError, runInContext, shutdown, span, trace, traceBeforeLoad, traceLoader, traceServerFn, tracingMiddleware, withErrorReporting, wrapStartHandler };
package/dist/loaders.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { i as SPAN_ATTRIBUTES, n as isRealError, t as isControlFlowSignal } from "./control-flow-p-dnko0G.js";
2
2
  import { r as isServerSide } from "./env-BpFWNnpL.js";
3
3
  import { SpanStatusCode } from "@opentelemetry/api";
4
- import { getActiveTraceContext, trace } from "autotel";
4
+ import { getActiveTraceContext, trace as trace$1 } from "autotel";
5
5
 
6
6
  //#region src/loaders.ts
7
7
  /**
@@ -49,7 +49,7 @@ function traceLoader(loaderFn, config = {}) {
49
49
  const routeId = context?.route?.id || "unknown";
50
50
  const spanName = config.name || `tanstack.loader.${routeId}`;
51
51
  const result = loaderFn(context);
52
- if (!(result instanceof Promise)) return trace({
52
+ if (!(result instanceof Promise)) return trace$1({
53
53
  name: spanName,
54
54
  isError: isRealError
55
55
  }, () => {
@@ -72,7 +72,7 @@ function traceLoader(loaderFn, config = {}) {
72
72
  ctx.setStatus({ code: SpanStatusCode.OK });
73
73
  return result;
74
74
  })();
75
- return trace({
75
+ return trace$1({
76
76
  name: spanName,
77
77
  isError: isRealError
78
78
  }, async () => {
@@ -150,7 +150,7 @@ function traceBeforeLoad(beforeLoadFn, config = {}) {
150
150
  const routeId = input?.route?.id || "unknown";
151
151
  const spanName = config.name || `tanstack.beforeLoad.${routeId}`;
152
152
  const result = beforeLoadFn(input);
153
- if (!(result instanceof Promise)) return trace({
153
+ if (!(result instanceof Promise)) return trace$1({
154
154
  name: spanName,
155
155
  isError: isRealError
156
156
  }, () => {
@@ -168,7 +168,7 @@ function traceBeforeLoad(beforeLoadFn, config = {}) {
168
168
  ctx.setStatus({ code: SpanStatusCode.OK });
169
169
  return result;
170
170
  })();
171
- return trace({
171
+ return trace$1({
172
172
  name: spanName,
173
173
  isError: isRealError
174
174
  }, async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"loaders.js","names":[],"sources":["../src/loaders.ts"],"sourcesContent":["import { SpanStatusCode } from '@opentelemetry/api';\nimport { trace, getActiveTraceContext } from 'autotel';\nimport { isServerSide } from './env';\nimport { isControlFlowSignal, isRealError } from './control-flow';\nimport { type TraceLoaderConfig, SPAN_ATTRIBUTES } from './types';\n\n// Re-export types from @tanstack/react-router for consumers who need them\nexport type { LoaderFnContext } from '@tanstack/react-router';\n\n/**\n * Internal type for extracting route info from TanStack context.\n * This is a minimal interface used only for instrumentation - the actual\n * TanStack types flow through the generic parameter.\n */\ninterface TanStackContextInternal {\n route?: { id?: string };\n params?: Record<string, string>;\n}\n\n/**\n * Wrap a TanStack route loader with OpenTelemetry tracing\n *\n * This function wraps a loader function to automatically create spans\n * for each invocation. It captures route ID, params (optionally),\n * and errors.\n *\n * The generic type TLoaderFn preserves the full TanStack Router type inference,\n * including typed params, context, and return types.\n *\n * @param loaderFn - The loader function to wrap\n * @param config - Configuration options\n * @returns Wrapped loader function with tracing (preserves original types)\n *\n * @example\n * ```typescript\n * import { createFileRoute } from '@tanstack/react-router';\n * import { traceLoader } from 'autotel-tanstack/loaders';\n *\n * export const Route = createFileRoute('/users/$userId')({\n * // Types are fully preserved - params.userId is typed as string\n * loader: traceLoader(async ({ params }) => {\n * return await db.users.findUnique({ where: { id: params.userId } });\n * }),\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Sync loaders are also supported\n * export const Route = createFileRoute('/static')({\n * loader: traceLoader(({ context }) => ({\n * message: `Welcome, ${context.userId}!`,\n * })),\n * });\n * ```\n */\nexport function traceLoader<TLoaderFn extends (ctx: any) => any>(\n loaderFn: TLoaderFn,\n config: TraceLoaderConfig = {},\n): TLoaderFn {\n const captureParams = config.captureParams ?? true;\n const captureResult = config.captureResult ?? false;\n\n const wrapped = (context: TanStackContextInternal) => {\n // If we're in the browser, just call the loader without tracing\n // This prevents autotel (which uses Node.js APIs) from being executed in the browser\n if (!isServerSide()) {\n return loaderFn(context);\n }\n\n const routeId = context?.route?.id || 'unknown';\n const spanName = config.name || `tanstack.loader.${routeId}`;\n\n // Handle both sync and async loaders\n const result = loaderFn(context);\n const isPromise = result instanceof Promise;\n\n if (!isPromise) {\n // Sync loader - wrap in trace synchronously\n return trace({ name: spanName, isError: isRealError }, () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'loader',\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'loader',\n });\n\n if (captureParams && context?.params) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n JSON.stringify(context.params),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n '[non-serializable]',\n );\n }\n }\n\n if (captureResult && result !== undefined) {\n try {\n ctx.setAttribute('tanstack.loader.result', JSON.stringify(result));\n } catch {\n ctx.setAttribute('tanstack.loader.result', '[non-serializable]');\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n })();\n }\n\n // Async loader\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'loader',\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'loader',\n });\n\n if (captureParams && context?.params) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n JSON.stringify(context.params),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n '[non-serializable]',\n );\n }\n }\n\n try {\n const asyncResult = await result;\n\n if (captureResult && asyncResult !== undefined) {\n try {\n ctx.setAttribute(\n 'tanstack.loader.result',\n JSON.stringify(asyncResult),\n );\n } catch {\n ctx.setAttribute('tanstack.loader.result', '[non-serializable]');\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return asyncResult;\n } catch (error) {\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n if ('recordError' in ctx && typeof ctx.recordError === 'function') {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n throw error;\n }\n })();\n };\n\n return wrapped as TLoaderFn;\n}\n\n/**\n * Wrap a TanStack route beforeLoad function with OpenTelemetry tracing\n *\n * This function wraps a beforeLoad function to automatically create spans.\n * beforeLoad runs before the route component renders and is typically\n * used for auth checks, redirects, or data prefetching.\n *\n * The generic type TBeforeLoadFn preserves the full TanStack Router type inference,\n * including typed params, context, search, and return types.\n *\n * @param beforeLoadFn - The beforeLoad function to wrap\n * @param config - Configuration options\n * @returns Wrapped beforeLoad function with tracing (preserves original types)\n *\n * @example\n * ```typescript\n * import { createFileRoute, redirect } from '@tanstack/react-router';\n * import { traceBeforeLoad } from 'autotel-tanstack/loaders';\n *\n * export const Route = createFileRoute('/dashboard')({\n * // Types are fully preserved - context, params, search are all typed\n * beforeLoad: traceBeforeLoad(async ({ context, params }) => {\n * if (!context.auth.isAuthenticated) {\n * throw redirect({ to: '/login' });\n * }\n * return { userId: params.userId }; // Return type flows to loader context\n * }),\n * loader: ({ context }) => {\n * // context.userId is typed from beforeLoad return\n * return { user: context.userId };\n * },\n * });\n * ```\n */\nexport function traceBeforeLoad<TBeforeLoadFn extends (opts: any) => any>(\n beforeLoadFn: TBeforeLoadFn,\n config: TraceLoaderConfig = {},\n): TBeforeLoadFn {\n const captureParams = config.captureParams ?? true;\n\n const wrapped = (input: TanStackContextInternal) => {\n // Skip tracing in browser\n if (!isServerSide()) {\n return beforeLoadFn(input);\n }\n\n const routeId = input?.route?.id || 'unknown';\n const spanName = config.name || `tanstack.beforeLoad.${routeId}`;\n\n // Handle both sync and async beforeLoad\n const result = beforeLoadFn(input);\n const isPromise = result instanceof Promise;\n\n if (!isPromise) {\n // Sync beforeLoad\n return trace({ name: spanName, isError: isRealError }, () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'beforeLoad',\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'beforeLoad',\n });\n\n if (captureParams && input?.params) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n JSON.stringify(input.params),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n '[non-serializable]',\n );\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n })();\n }\n\n // Async beforeLoad\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'beforeLoad',\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'beforeLoad',\n });\n\n if (captureParams && input?.params) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n JSON.stringify(input.params),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n '[non-serializable]',\n );\n }\n }\n\n try {\n const asyncResult = await result;\n ctx.setStatus({ code: SpanStatusCode.OK });\n return asyncResult;\n } catch (error) {\n // redirect()/notFound() are expected control flow, not errors.\n if (isControlFlowSignal(error)) {\n ctx.setAttribute('tanstack.beforeLoad.redirect', true);\n ctx.setStatus({ code: SpanStatusCode.OK });\n } else {\n if ('recordError' in ctx && typeof ctx.recordError === 'function') {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n }\n throw error;\n }\n })();\n };\n\n return wrapped as TBeforeLoadFn;\n}\n\n/**\n * Create a traced route configuration helper\n *\n * This higher-order function helps create route configurations\n * with automatic tracing for both loader and beforeLoad.\n *\n * @param routeId - The route identifier\n * @param config - Tracing configuration\n * @returns Object with traced loader and beforeLoad wrappers\n *\n * @example\n * ```typescript\n * import { createFileRoute } from '@tanstack/react-router';\n * import { createTracedRoute } from 'autotel-tanstack/loaders';\n *\n * const traced = createTracedRoute('/users/$userId');\n *\n * export const Route = createFileRoute('/users/$userId')({\n * beforeLoad: traced.beforeLoad(async ({ context }) => {\n * // Auth check\n * }),\n * loader: traced.loader(async ({ params }) => {\n * return await getUser(params.userId);\n * }),\n * });\n * ```\n */\nexport function createTracedRoute(\n routeId: string,\n config: Omit<TraceLoaderConfig, 'name'> = {},\n) {\n return {\n /**\n * Wrap a loader function with tracing\n */\n loader<TLoaderFn extends (ctx: any) => any>(\n loaderFn: TLoaderFn,\n ): TLoaderFn {\n return traceLoader(loaderFn, {\n ...config,\n name: `tanstack.loader.${routeId}`,\n });\n },\n\n /**\n * Wrap a beforeLoad function with tracing\n */\n beforeLoad<TBeforeLoadFn extends (opts: any) => any>(\n beforeLoadFn: TBeforeLoadFn,\n ): TBeforeLoadFn {\n return traceBeforeLoad(beforeLoadFn, {\n ...config,\n name: `tanstack.beforeLoad.${routeId}`,\n });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"loaders.js","names":["trace"],"sources":["../src/loaders.ts"],"sourcesContent":["import { SpanStatusCode } from '@opentelemetry/api';\nimport { trace, getActiveTraceContext } from 'autotel';\nimport { isServerSide } from './env';\nimport { isControlFlowSignal, isRealError } from './control-flow';\nimport { type TraceLoaderConfig, SPAN_ATTRIBUTES } from './types';\n\n// Re-export types from @tanstack/react-router for consumers who need them\nexport type { LoaderFnContext } from '@tanstack/react-router';\n\n/**\n * Internal type for extracting route info from TanStack context.\n * This is a minimal interface used only for instrumentation - the actual\n * TanStack types flow through the generic parameter.\n */\ninterface TanStackContextInternal {\n route?: { id?: string };\n params?: Record<string, string>;\n}\n\n/**\n * Wrap a TanStack route loader with OpenTelemetry tracing\n *\n * This function wraps a loader function to automatically create spans\n * for each invocation. It captures route ID, params (optionally),\n * and errors.\n *\n * The generic type TLoaderFn preserves the full TanStack Router type inference,\n * including typed params, context, and return types.\n *\n * @param loaderFn - The loader function to wrap\n * @param config - Configuration options\n * @returns Wrapped loader function with tracing (preserves original types)\n *\n * @example\n * ```typescript\n * import { createFileRoute } from '@tanstack/react-router';\n * import { traceLoader } from 'autotel-tanstack/loaders';\n *\n * export const Route = createFileRoute('/users/$userId')({\n * // Types are fully preserved - params.userId is typed as string\n * loader: traceLoader(async ({ params }) => {\n * return await db.users.findUnique({ where: { id: params.userId } });\n * }),\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Sync loaders are also supported\n * export const Route = createFileRoute('/static')({\n * loader: traceLoader(({ context }) => ({\n * message: `Welcome, ${context.userId}!`,\n * })),\n * });\n * ```\n */\nexport function traceLoader<TLoaderFn extends (ctx: any) => any>(\n loaderFn: TLoaderFn,\n config: TraceLoaderConfig = {},\n): TLoaderFn {\n const captureParams = config.captureParams ?? true;\n const captureResult = config.captureResult ?? false;\n\n const wrapped = (context: TanStackContextInternal) => {\n // If we're in the browser, just call the loader without tracing\n // This prevents autotel (which uses Node.js APIs) from being executed in the browser\n if (!isServerSide()) {\n return loaderFn(context);\n }\n\n const routeId = context?.route?.id || 'unknown';\n const spanName = config.name || `tanstack.loader.${routeId}`;\n\n // Handle both sync and async loaders\n const result = loaderFn(context);\n const isPromise = result instanceof Promise;\n\n if (!isPromise) {\n // Sync loader - wrap in trace synchronously\n return trace({ name: spanName, isError: isRealError }, () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'loader',\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'loader',\n });\n\n if (captureParams && context?.params) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n JSON.stringify(context.params),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n '[non-serializable]',\n );\n }\n }\n\n if (captureResult && result !== undefined) {\n try {\n ctx.setAttribute('tanstack.loader.result', JSON.stringify(result));\n } catch {\n ctx.setAttribute('tanstack.loader.result', '[non-serializable]');\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n })();\n }\n\n // Async loader\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'loader',\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'loader',\n });\n\n if (captureParams && context?.params) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n JSON.stringify(context.params),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n '[non-serializable]',\n );\n }\n }\n\n try {\n const asyncResult = await result;\n\n if (captureResult && asyncResult !== undefined) {\n try {\n ctx.setAttribute(\n 'tanstack.loader.result',\n JSON.stringify(asyncResult),\n );\n } catch {\n ctx.setAttribute('tanstack.loader.result', '[non-serializable]');\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return asyncResult;\n } catch (error) {\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n if ('recordError' in ctx && typeof ctx.recordError === 'function') {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n throw error;\n }\n })();\n };\n\n return wrapped as TLoaderFn;\n}\n\n/**\n * Wrap a TanStack route beforeLoad function with OpenTelemetry tracing\n *\n * This function wraps a beforeLoad function to automatically create spans.\n * beforeLoad runs before the route component renders and is typically\n * used for auth checks, redirects, or data prefetching.\n *\n * The generic type TBeforeLoadFn preserves the full TanStack Router type inference,\n * including typed params, context, search, and return types.\n *\n * @param beforeLoadFn - The beforeLoad function to wrap\n * @param config - Configuration options\n * @returns Wrapped beforeLoad function with tracing (preserves original types)\n *\n * @example\n * ```typescript\n * import { createFileRoute, redirect } from '@tanstack/react-router';\n * import { traceBeforeLoad } from 'autotel-tanstack/loaders';\n *\n * export const Route = createFileRoute('/dashboard')({\n * // Types are fully preserved - context, params, search are all typed\n * beforeLoad: traceBeforeLoad(async ({ context, params }) => {\n * if (!context.auth.isAuthenticated) {\n * throw redirect({ to: '/login' });\n * }\n * return { userId: params.userId }; // Return type flows to loader context\n * }),\n * loader: ({ context }) => {\n * // context.userId is typed from beforeLoad return\n * return { user: context.userId };\n * },\n * });\n * ```\n */\nexport function traceBeforeLoad<TBeforeLoadFn extends (opts: any) => any>(\n beforeLoadFn: TBeforeLoadFn,\n config: TraceLoaderConfig = {},\n): TBeforeLoadFn {\n const captureParams = config.captureParams ?? true;\n\n const wrapped = (input: TanStackContextInternal) => {\n // Skip tracing in browser\n if (!isServerSide()) {\n return beforeLoadFn(input);\n }\n\n const routeId = input?.route?.id || 'unknown';\n const spanName = config.name || `tanstack.beforeLoad.${routeId}`;\n\n // Handle both sync and async beforeLoad\n const result = beforeLoadFn(input);\n const isPromise = result instanceof Promise;\n\n if (!isPromise) {\n // Sync beforeLoad\n return trace({ name: spanName, isError: isRealError }, () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'beforeLoad',\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'beforeLoad',\n });\n\n if (captureParams && input?.params) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n JSON.stringify(input.params),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n '[non-serializable]',\n );\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n })();\n }\n\n // Async beforeLoad\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'beforeLoad',\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_ROUTE_ID]: routeId,\n [SPAN_ATTRIBUTES.TANSTACK_LOADER_TYPE]: 'beforeLoad',\n });\n\n if (captureParams && input?.params) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n JSON.stringify(input.params),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_LOADER_PARAMS,\n '[non-serializable]',\n );\n }\n }\n\n try {\n const asyncResult = await result;\n ctx.setStatus({ code: SpanStatusCode.OK });\n return asyncResult;\n } catch (error) {\n // redirect()/notFound() are expected control flow, not errors.\n if (isControlFlowSignal(error)) {\n ctx.setAttribute('tanstack.beforeLoad.redirect', true);\n ctx.setStatus({ code: SpanStatusCode.OK });\n } else {\n if ('recordError' in ctx && typeof ctx.recordError === 'function') {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n }\n throw error;\n }\n })();\n };\n\n return wrapped as TBeforeLoadFn;\n}\n\n/**\n * Create a traced route configuration helper\n *\n * This higher-order function helps create route configurations\n * with automatic tracing for both loader and beforeLoad.\n *\n * @param routeId - The route identifier\n * @param config - Tracing configuration\n * @returns Object with traced loader and beforeLoad wrappers\n *\n * @example\n * ```typescript\n * import { createFileRoute } from '@tanstack/react-router';\n * import { createTracedRoute } from 'autotel-tanstack/loaders';\n *\n * const traced = createTracedRoute('/users/$userId');\n *\n * export const Route = createFileRoute('/users/$userId')({\n * beforeLoad: traced.beforeLoad(async ({ context }) => {\n * // Auth check\n * }),\n * loader: traced.loader(async ({ params }) => {\n * return await getUser(params.userId);\n * }),\n * });\n * ```\n */\nexport function createTracedRoute(\n routeId: string,\n config: Omit<TraceLoaderConfig, 'name'> = {},\n) {\n return {\n /**\n * Wrap a loader function with tracing\n */\n loader<TLoaderFn extends (ctx: any) => any>(\n loaderFn: TLoaderFn,\n ): TLoaderFn {\n return traceLoader(loaderFn, {\n ...config,\n name: `tanstack.loader.${routeId}`,\n });\n },\n\n /**\n * Wrap a beforeLoad function with tracing\n */\n beforeLoad<TBeforeLoadFn extends (opts: any) => any>(\n beforeLoadFn: TBeforeLoadFn,\n ): TBeforeLoadFn {\n return traceBeforeLoad(beforeLoadFn, {\n ...config,\n name: `tanstack.beforeLoad.${routeId}`,\n });\n },\n };\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { extractContextFromRequest } from "./context.js";
3
3
  import { r as isServerSide } from "./env-BpFWNnpL.js";
4
4
  import { t as isExcludedPath } from "./route-filter-dLg-j3jR.js";
5
5
  import { SpanStatusCode, context } from "@opentelemetry/api";
6
- import { getActiveTraceContext, trace } from "autotel";
6
+ import { getActiveTraceContext, trace as trace$1 } from "autotel";
7
7
 
8
8
  //#region src/middleware.ts
9
9
  /**
@@ -91,7 +91,7 @@ function createTracingMiddleware(config) {
91
91
  if (mergedConfig.type === "function") {
92
92
  const fnName = functionId || "unknown";
93
93
  const method = opts.method || "POST";
94
- return trace({
94
+ return trace$1({
95
95
  name: `tanstack.serverFn.${fnName}`,
96
96
  isError: isRealError
97
97
  }, async () => {
@@ -142,7 +142,7 @@ function createTracingMiddleware(config) {
142
142
  const parentContext = extractContextFromRequest(request);
143
143
  return context.with(parentContext, async () => {
144
144
  const spanName = `${request.method} ${pathname || url.pathname}`;
145
- return trace({
145
+ return trace$1({
146
146
  name: spanName,
147
147
  isError: isRealError
148
148
  }, async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["import { context, SpanStatusCode, type Attributes } from '@opentelemetry/api';\nimport { trace, getActiveTraceContext } from 'autotel';\nimport { extractContextFromRequest } from './context';\nimport { isServerSide } from './env';\nimport { isControlFlowSignal, isRealError } from './control-flow';\nimport { isExcludedPath } from './route-filter';\nimport {\n type TracingMiddlewareConfig,\n DEFAULT_CONFIG,\n SPAN_ATTRIBUTES,\n} from './types';\n\n/**\n * Build span attributes for HTTP requests\n */\nfunction buildRequestAttributes(\n request: Request,\n config: Required<\n Omit<TracingMiddlewareConfig, 'customAttributes' | 'service' | 'type'>\n >,\n): Attributes {\n const url = new URL(request.url);\n const attrs: Attributes = {\n [SPAN_ATTRIBUTES.HTTP_REQUEST_METHOD]: request.method,\n [SPAN_ATTRIBUTES.URL_PATH]: url.pathname,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'request',\n };\n\n if (url.search) {\n attrs[SPAN_ATTRIBUTES.URL_QUERY] = url.search;\n }\n\n // Capture configured headers\n if (config.captureHeaders) {\n for (const header of config.captureHeaders) {\n const value = request.headers.get(header);\n if (value) {\n attrs[`http.request.header.${header.toLowerCase()}`] = value;\n }\n }\n }\n\n return attrs;\n}\n\n/**\n * Build span attributes for server functions\n */\nfunction buildServerFnAttributes(\n functionName: string,\n method: string,\n args: unknown,\n config: Required<\n Omit<TracingMiddlewareConfig, 'customAttributes' | 'service' | 'type'>\n >,\n): Attributes {\n const attrs: Attributes = {\n [SPAN_ATTRIBUTES.RPC_SYSTEM]: 'tanstack-start',\n [SPAN_ATTRIBUTES.RPC_METHOD]: functionName,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'serverFn',\n [SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_NAME]: functionName,\n [SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_METHOD]: method,\n };\n\n if (config.captureArgs && args !== undefined) {\n try {\n attrs[SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS] = JSON.stringify(args);\n } catch {\n attrs[SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS] = '[non-serializable]';\n }\n }\n\n return attrs;\n}\n\n/**\n * Generic middleware handler type (compatible with TanStack's middleware pattern)\n *\n * This type represents the shape of TanStack middleware handlers.\n * We use a generic type to avoid direct dependency on TanStack packages.\n */\nexport interface MiddlewareHandler<TContext = unknown> {\n (opts: {\n next: (ctx?: Partial<TContext>) => Promise<TContext>;\n context: TContext;\n request?: Request;\n pathname?: string;\n data?: unknown;\n method?: string;\n filename?: string;\n functionId?: string;\n signal?: AbortSignal;\n }): Promise<TContext>;\n}\n\n/**\n * Create a TanStack-compatible tracing middleware\n *\n * This creates middleware that automatically traces all requests/server functions\n * with OpenTelemetry spans. Use with TanStack Start's middleware system.\n *\n * @param config - Configuration options\n * @returns Middleware handler compatible with TanStack Start\n *\n * @example\n * ```typescript\n * // Global request middleware in app/start.ts\n * import { createStart } from '@tanstack/react-start';\n * import { createTracingMiddleware } from 'autotel-tanstack/middleware';\n *\n * export const startInstance = createStart(() => ({\n * requestMiddleware: [\n * createTracingMiddleware({\n * captureHeaders: ['x-request-id', 'user-agent'],\n * excludePaths: ['/health', '/metrics'],\n * }),\n * ],\n * }));\n * ```\n *\n * @example\n * ```typescript\n * // Server function middleware\n * import { createServerFn } from '@tanstack/react-start';\n * import { createTracingMiddleware } from 'autotel-tanstack/middleware';\n *\n * export const getUser = createServerFn({ method: 'GET' })\n * .middleware([createTracingMiddleware({ type: 'function' })])\n * .handler(async ({ data: id }) => {\n * return await db.users.findUnique({ where: { id } });\n * });\n * ```\n */\nexport function createTracingMiddleware<TContext = unknown>(\n config?: TracingMiddlewareConfig,\n): MiddlewareHandler<TContext> {\n const mergedConfig = {\n ...DEFAULT_CONFIG,\n ...config,\n type: config?.type ?? 'request',\n };\n\n return async function tracingMiddleware(opts) {\n // If we're in the browser, return a no-op middleware\n // This prevents autotel (which uses Node.js APIs) from being bundled/executed in the browser\n if (!isServerSide()) {\n return opts.next();\n }\n const { next, request, pathname, data, functionId } = opts;\n\n // For function middleware\n if (mergedConfig.type === 'function') {\n const fnName = functionId || 'unknown';\n const method = (opts as { method?: string }).method || 'POST';\n\n return trace(\n { name: `tanstack.serverFn.${fnName}`, isError: isRealError },\n async () => {\n const ctx = getActiveTraceContext()!;\n const attrs = buildServerFnAttributes(\n fnName,\n method,\n data,\n mergedConfig,\n );\n ctx.setAttributes(attrs as Record<string, string | number | boolean>);\n\n // Add custom attributes if provided\n if (config?.customAttributes) {\n const customAttrs = config.customAttributes({\n type: 'serverFn',\n name: fnName,\n args: data,\n });\n ctx.setAttributes(\n customAttrs as Record<string, string | number | boolean>,\n );\n }\n\n try {\n const result = await next();\n\n // Capture result if configured\n if (mergedConfig.captureResults && result !== undefined) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,\n JSON.stringify(result),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,\n '[non-serializable]',\n );\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n if (mergedConfig.captureErrors) {\n if (\n 'recordError' in ctx &&\n typeof ctx.recordError === 'function'\n ) {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n\n // Report error to error store\n try {\n const { reportError } = await import('./error-reporting');\n reportError(error as Error, {\n type: 'serverFn',\n name: fnName,\n method,\n });\n } catch {\n // Error reporting not available, skip\n }\n }\n throw error;\n }\n },\n )() as Promise<TContext>;\n }\n\n // For request middleware\n if (!request) {\n // No request available, just pass through\n return next();\n }\n\n const url = new URL(request.url);\n\n // Check if path should be excluded\n if (isExcludedPath(url.pathname, mergedConfig.excludePaths)) {\n return next();\n }\n\n // Extract parent context from request headers\n const parentContext = extractContextFromRequest(request);\n\n // Run within parent context for distributed tracing\n return context.with(parentContext, async () => {\n const spanName = `${request.method} ${pathname || url.pathname}`;\n\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n const attrs = buildRequestAttributes(request, mergedConfig);\n ctx.setAttributes(attrs as Record<string, string | number | boolean>);\n\n // Add custom attributes if provided\n if (config?.customAttributes) {\n const customAttrs = config.customAttributes({\n type: 'request',\n name: spanName,\n request,\n });\n ctx.setAttributes(\n customAttrs as Record<string, string | number | boolean>,\n );\n }\n\n const startTime = Date.now();\n\n try {\n const result = await next();\n\n const duration = Date.now() - startTime;\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n\n // Record timing in metrics collector\n try {\n const { metricsCollector } = await import('./metrics');\n metricsCollector.recordTiming(spanName, duration);\n } catch {\n // Metrics not available, skip\n }\n\n // Try to get response status from result if it's a Response\n if (result && typeof result === 'object' && 'status' in result) {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.HTTP_RESPONSE_STATUS_CODE,\n (result as { status: number }).status,\n );\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n const duration = Date.now() - startTime;\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n\n if (mergedConfig.captureErrors) {\n if ('recordError' in ctx && typeof ctx.recordError === 'function') {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n\n // Report error to error store\n try {\n const { reportError } = await import('./error-reporting');\n reportError(error as Error, {\n type: 'request',\n method: request.method,\n pathname: url.pathname,\n });\n } catch {\n // Error reporting not available, skip\n }\n }\n throw error;\n }\n })() as Promise<TContext>;\n });\n };\n}\n\n/**\n * Pre-configured tracing middleware with sensible defaults\n *\n * Convenience export for quick setup. Uses adaptive sampling,\n * captures x-request-id header, and excludes common health check paths.\n *\n * @param config - Optional configuration overrides\n * @returns Middleware handler\n *\n * @example\n * ```typescript\n * import { createStart } from '@tanstack/react-start';\n * import { tracingMiddleware } from 'autotel-tanstack/middleware';\n *\n * export const startInstance = createStart(() => ({\n * requestMiddleware: [tracingMiddleware()],\n * }));\n * ```\n */\nexport function tracingMiddleware<TContext = unknown>(\n config?: TracingMiddlewareConfig,\n): MiddlewareHandler<TContext> {\n return createTracingMiddleware({\n sampling: 'adaptive',\n captureHeaders: ['x-request-id', 'user-agent'],\n excludePaths: ['/health', '/healthz', '/ready', '/metrics', '/_ping'],\n ...config,\n });\n}\n\n/**\n * Create function-specific tracing middleware\n *\n * Convenience wrapper for server function middleware.\n *\n * @param config - Optional configuration\n * @returns Middleware handler for server functions\n *\n * @example\n * ```typescript\n * import { createServerFn } from '@tanstack/react-start';\n * import { functionTracingMiddleware } from 'autotel-tanstack/middleware';\n *\n * export const getUser = createServerFn({ method: 'GET' })\n * .middleware([functionTracingMiddleware()])\n * .handler(async ({ data: id }) => {\n * return await db.users.findUnique({ where: { id } });\n * });\n * ```\n */\nexport function functionTracingMiddleware<TContext = unknown>(\n config?: Omit<TracingMiddlewareConfig, 'type'>,\n): MiddlewareHandler<TContext> {\n return createTracingMiddleware({\n ...config,\n type: 'function',\n });\n}\n\n/**\n * Create a tracing handler for use with TanStack's native createMiddleware()\n *\n * This provides the raw tracing logic that you can pass to createMiddleware().server().\n * Use this when you want full control over the middleware builder pattern.\n *\n * The handler accepts TanStack's middleware signature `{ next, context, request }`\n * and internally adapts it to our more flexible MiddlewareHandler interface.\n *\n * @param config - Configuration options\n * @returns Server handler function compatible with createMiddleware().server()\n *\n * @example\n * ```typescript\n * import { createStart, createMiddleware } from '@tanstack/react-start';\n * import { createTracingServerHandler } from 'autotel-tanstack/middleware';\n *\n * // TanStack-native middleware creation\n * const requestTracingMiddleware = createMiddleware().server(\n * createTracingServerHandler({ captureHeaders: ['x-request-id'] })\n * );\n *\n * export const start = createStart(() => ({\n * requestMiddleware: [requestTracingMiddleware],\n * }));\n * ```\n *\n * @example\n * ```typescript\n * // For server functions - use createMiddleware({ type: 'function' })\n * import { createStart, createMiddleware } from '@tanstack/react-start';\n * import { createTracingServerHandler } from 'autotel-tanstack/middleware';\n *\n * const functionTracingMiddleware = createMiddleware({ type: 'function' }).server(\n * createTracingServerHandler({ type: 'function', captureArgs: true })\n * );\n *\n * export const start = createStart(() => ({\n * functionMiddleware: [functionTracingMiddleware],\n * }));\n * ```\n */\nexport function createTracingServerHandler<TContext = unknown>(\n config?: TracingMiddlewareConfig,\n): (opts: any) => any {\n const handler = createTracingMiddleware<TContext>(config);\n\n // Adapt TanStack's signature to our handler\n return async (opts: any) => {\n return handler(opts);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"middleware.js","names":["trace"],"sources":["../src/middleware.ts"],"sourcesContent":["import { context, SpanStatusCode, type Attributes } from '@opentelemetry/api';\nimport { trace, getActiveTraceContext } from 'autotel';\nimport { extractContextFromRequest } from './context';\nimport { isServerSide } from './env';\nimport { isControlFlowSignal, isRealError } from './control-flow';\nimport { isExcludedPath } from './route-filter';\nimport {\n type TracingMiddlewareConfig,\n DEFAULT_CONFIG,\n SPAN_ATTRIBUTES,\n} from './types';\n\n/**\n * Build span attributes for HTTP requests\n */\nfunction buildRequestAttributes(\n request: Request,\n config: Required<\n Omit<TracingMiddlewareConfig, 'customAttributes' | 'service' | 'type'>\n >,\n): Attributes {\n const url = new URL(request.url);\n const attrs: Attributes = {\n [SPAN_ATTRIBUTES.HTTP_REQUEST_METHOD]: request.method,\n [SPAN_ATTRIBUTES.URL_PATH]: url.pathname,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'request',\n };\n\n if (url.search) {\n attrs[SPAN_ATTRIBUTES.URL_QUERY] = url.search;\n }\n\n // Capture configured headers\n if (config.captureHeaders) {\n for (const header of config.captureHeaders) {\n const value = request.headers.get(header);\n if (value) {\n attrs[`http.request.header.${header.toLowerCase()}`] = value;\n }\n }\n }\n\n return attrs;\n}\n\n/**\n * Build span attributes for server functions\n */\nfunction buildServerFnAttributes(\n functionName: string,\n method: string,\n args: unknown,\n config: Required<\n Omit<TracingMiddlewareConfig, 'customAttributes' | 'service' | 'type'>\n >,\n): Attributes {\n const attrs: Attributes = {\n [SPAN_ATTRIBUTES.RPC_SYSTEM]: 'tanstack-start',\n [SPAN_ATTRIBUTES.RPC_METHOD]: functionName,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'serverFn',\n [SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_NAME]: functionName,\n [SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_METHOD]: method,\n };\n\n if (config.captureArgs && args !== undefined) {\n try {\n attrs[SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS] = JSON.stringify(args);\n } catch {\n attrs[SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS] = '[non-serializable]';\n }\n }\n\n return attrs;\n}\n\n/**\n * Generic middleware handler type (compatible with TanStack's middleware pattern)\n *\n * This type represents the shape of TanStack middleware handlers.\n * We use a generic type to avoid direct dependency on TanStack packages.\n */\nexport interface MiddlewareHandler<TContext = unknown> {\n (opts: {\n next: (ctx?: Partial<TContext>) => Promise<TContext>;\n context: TContext;\n request?: Request;\n pathname?: string;\n data?: unknown;\n method?: string;\n filename?: string;\n functionId?: string;\n signal?: AbortSignal;\n }): Promise<TContext>;\n}\n\n/**\n * Create a TanStack-compatible tracing middleware\n *\n * This creates middleware that automatically traces all requests/server functions\n * with OpenTelemetry spans. Use with TanStack Start's middleware system.\n *\n * @param config - Configuration options\n * @returns Middleware handler compatible with TanStack Start\n *\n * @example\n * ```typescript\n * // Global request middleware in app/start.ts\n * import { createStart } from '@tanstack/react-start';\n * import { createTracingMiddleware } from 'autotel-tanstack/middleware';\n *\n * export const startInstance = createStart(() => ({\n * requestMiddleware: [\n * createTracingMiddleware({\n * captureHeaders: ['x-request-id', 'user-agent'],\n * excludePaths: ['/health', '/metrics'],\n * }),\n * ],\n * }));\n * ```\n *\n * @example\n * ```typescript\n * // Server function middleware\n * import { createServerFn } from '@tanstack/react-start';\n * import { createTracingMiddleware } from 'autotel-tanstack/middleware';\n *\n * export const getUser = createServerFn({ method: 'GET' })\n * .middleware([createTracingMiddleware({ type: 'function' })])\n * .handler(async ({ data: id }) => {\n * return await db.users.findUnique({ where: { id } });\n * });\n * ```\n */\nexport function createTracingMiddleware<TContext = unknown>(\n config?: TracingMiddlewareConfig,\n): MiddlewareHandler<TContext> {\n const mergedConfig = {\n ...DEFAULT_CONFIG,\n ...config,\n type: config?.type ?? 'request',\n };\n\n return async function tracingMiddleware(opts) {\n // If we're in the browser, return a no-op middleware\n // This prevents autotel (which uses Node.js APIs) from being bundled/executed in the browser\n if (!isServerSide()) {\n return opts.next();\n }\n const { next, request, pathname, data, functionId } = opts;\n\n // For function middleware\n if (mergedConfig.type === 'function') {\n const fnName = functionId || 'unknown';\n const method = (opts as { method?: string }).method || 'POST';\n\n return trace(\n { name: `tanstack.serverFn.${fnName}`, isError: isRealError },\n async () => {\n const ctx = getActiveTraceContext()!;\n const attrs = buildServerFnAttributes(\n fnName,\n method,\n data,\n mergedConfig,\n );\n ctx.setAttributes(attrs as Record<string, string | number | boolean>);\n\n // Add custom attributes if provided\n if (config?.customAttributes) {\n const customAttrs = config.customAttributes({\n type: 'serverFn',\n name: fnName,\n args: data,\n });\n ctx.setAttributes(\n customAttrs as Record<string, string | number | boolean>,\n );\n }\n\n try {\n const result = await next();\n\n // Capture result if configured\n if (mergedConfig.captureResults && result !== undefined) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,\n JSON.stringify(result),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,\n '[non-serializable]',\n );\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n if (mergedConfig.captureErrors) {\n if (\n 'recordError' in ctx &&\n typeof ctx.recordError === 'function'\n ) {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n\n // Report error to error store\n try {\n const { reportError } = await import('./error-reporting');\n reportError(error as Error, {\n type: 'serverFn',\n name: fnName,\n method,\n });\n } catch {\n // Error reporting not available, skip\n }\n }\n throw error;\n }\n },\n )() as Promise<TContext>;\n }\n\n // For request middleware\n if (!request) {\n // No request available, just pass through\n return next();\n }\n\n const url = new URL(request.url);\n\n // Check if path should be excluded\n if (isExcludedPath(url.pathname, mergedConfig.excludePaths)) {\n return next();\n }\n\n // Extract parent context from request headers\n const parentContext = extractContextFromRequest(request);\n\n // Run within parent context for distributed tracing\n return context.with(parentContext, async () => {\n const spanName = `${request.method} ${pathname || url.pathname}`;\n\n return trace({ name: spanName, isError: isRealError }, async () => {\n const ctx = getActiveTraceContext()!;\n const attrs = buildRequestAttributes(request, mergedConfig);\n ctx.setAttributes(attrs as Record<string, string | number | boolean>);\n\n // Add custom attributes if provided\n if (config?.customAttributes) {\n const customAttrs = config.customAttributes({\n type: 'request',\n name: spanName,\n request,\n });\n ctx.setAttributes(\n customAttrs as Record<string, string | number | boolean>,\n );\n }\n\n const startTime = Date.now();\n\n try {\n const result = await next();\n\n const duration = Date.now() - startTime;\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n\n // Record timing in metrics collector\n try {\n const { metricsCollector } = await import('./metrics');\n metricsCollector.recordTiming(spanName, duration);\n } catch {\n // Metrics not available, skip\n }\n\n // Try to get response status from result if it's a Response\n if (result && typeof result === 'object' && 'status' in result) {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.HTTP_RESPONSE_STATUS_CODE,\n (result as { status: number }).status,\n );\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n const duration = Date.now() - startTime;\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_REQUEST_DURATION_MS,\n duration,\n );\n\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n\n if (mergedConfig.captureErrors) {\n if ('recordError' in ctx && typeof ctx.recordError === 'function') {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n\n // Report error to error store\n try {\n const { reportError } = await import('./error-reporting');\n reportError(error as Error, {\n type: 'request',\n method: request.method,\n pathname: url.pathname,\n });\n } catch {\n // Error reporting not available, skip\n }\n }\n throw error;\n }\n })() as Promise<TContext>;\n });\n };\n}\n\n/**\n * Pre-configured tracing middleware with sensible defaults\n *\n * Convenience export for quick setup. Uses adaptive sampling,\n * captures x-request-id header, and excludes common health check paths.\n *\n * @param config - Optional configuration overrides\n * @returns Middleware handler\n *\n * @example\n * ```typescript\n * import { createStart } from '@tanstack/react-start';\n * import { tracingMiddleware } from 'autotel-tanstack/middleware';\n *\n * export const startInstance = createStart(() => ({\n * requestMiddleware: [tracingMiddleware()],\n * }));\n * ```\n */\nexport function tracingMiddleware<TContext = unknown>(\n config?: TracingMiddlewareConfig,\n): MiddlewareHandler<TContext> {\n return createTracingMiddleware({\n sampling: 'adaptive',\n captureHeaders: ['x-request-id', 'user-agent'],\n excludePaths: ['/health', '/healthz', '/ready', '/metrics', '/_ping'],\n ...config,\n });\n}\n\n/**\n * Create function-specific tracing middleware\n *\n * Convenience wrapper for server function middleware.\n *\n * @param config - Optional configuration\n * @returns Middleware handler for server functions\n *\n * @example\n * ```typescript\n * import { createServerFn } from '@tanstack/react-start';\n * import { functionTracingMiddleware } from 'autotel-tanstack/middleware';\n *\n * export const getUser = createServerFn({ method: 'GET' })\n * .middleware([functionTracingMiddleware()])\n * .handler(async ({ data: id }) => {\n * return await db.users.findUnique({ where: { id } });\n * });\n * ```\n */\nexport function functionTracingMiddleware<TContext = unknown>(\n config?: Omit<TracingMiddlewareConfig, 'type'>,\n): MiddlewareHandler<TContext> {\n return createTracingMiddleware({\n ...config,\n type: 'function',\n });\n}\n\n/**\n * Create a tracing handler for use with TanStack's native createMiddleware()\n *\n * This provides the raw tracing logic that you can pass to createMiddleware().server().\n * Use this when you want full control over the middleware builder pattern.\n *\n * The handler accepts TanStack's middleware signature `{ next, context, request }`\n * and internally adapts it to our more flexible MiddlewareHandler interface.\n *\n * @param config - Configuration options\n * @returns Server handler function compatible with createMiddleware().server()\n *\n * @example\n * ```typescript\n * import { createStart, createMiddleware } from '@tanstack/react-start';\n * import { createTracingServerHandler } from 'autotel-tanstack/middleware';\n *\n * // TanStack-native middleware creation\n * const requestTracingMiddleware = createMiddleware().server(\n * createTracingServerHandler({ captureHeaders: ['x-request-id'] })\n * );\n *\n * export const start = createStart(() => ({\n * requestMiddleware: [requestTracingMiddleware],\n * }));\n * ```\n *\n * @example\n * ```typescript\n * // For server functions - use createMiddleware({ type: 'function' })\n * import { createStart, createMiddleware } from '@tanstack/react-start';\n * import { createTracingServerHandler } from 'autotel-tanstack/middleware';\n *\n * const functionTracingMiddleware = createMiddleware({ type: 'function' }).server(\n * createTracingServerHandler({ type: 'function', captureArgs: true })\n * );\n *\n * export const start = createStart(() => ({\n * functionMiddleware: [functionTracingMiddleware],\n * }));\n * ```\n */\nexport function createTracingServerHandler<TContext = unknown>(\n config?: TracingMiddlewareConfig,\n): (opts: any) => any {\n const handler = createTracingMiddleware<TContext>(config);\n\n // Adapt TanStack's signature to our handler\n return async (opts: any) => {\n return handler(opts);\n };\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { i as SPAN_ATTRIBUTES, n as isRealError, t as isControlFlowSignal } from "./control-flow-p-dnko0G.js";
2
2
  import { r as isServerSide } from "./env-BpFWNnpL.js";
3
3
  import { SpanStatusCode } from "@opentelemetry/api";
4
- import { getActiveTraceContext, trace } from "autotel";
4
+ import { getActiveTraceContext, trace as trace$1 } from "autotel";
5
5
 
6
6
  //#region src/server-functions.ts
7
7
  /**
@@ -51,7 +51,7 @@ function traceServerFn(serverFn, config = {}) {
51
51
  return new Proxy(serverFn, {
52
52
  apply(target, thisArg, argArray) {
53
53
  if (!isServerSide()) return target.apply(thisArg, argArray);
54
- return trace({
54
+ return trace$1({
55
55
  name: `tanstack.serverFn.${fnName}`,
56
56
  isError: isRealError
57
57
  }, async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"server-functions.js","names":[],"sources":["../src/server-functions.ts"],"sourcesContent":["import { SpanStatusCode } from '@opentelemetry/api';\nimport { trace, getActiveTraceContext } from 'autotel';\nimport { isServerSide } from './env';\nimport { isControlFlowSignal, isRealError } from './control-flow';\nimport { type TraceServerFnConfig, SPAN_ATTRIBUTES } from './types';\n\n/**\n * Wrap a TanStack server function with OpenTelemetry tracing\n *\n * This function wraps a server function to automatically create spans\n * for each invocation. It captures function name, arguments (optionally),\n * results (optionally), and errors.\n *\n * @param serverFn - The server function to wrap\n * @param config - Configuration options\n * @returns Wrapped server function with tracing\n *\n * @example\n * ```typescript\n * import { createServerFn } from '@tanstack/react-start';\n * import { traceServerFn } from 'autotel-tanstack/server-functions';\n *\n * const getUserBase = createServerFn({ method: 'GET' })\n * .handler(async ({ data: id }) => {\n * return await db.users.findUnique({ where: { id } });\n * });\n *\n * export const getUser = traceServerFn(getUserBase, { name: 'getUser' });\n * ```\n *\n * @example\n * ```typescript\n * // With argument and result capture (careful with PII!)\n * export const createUser = traceServerFn(\n * createServerFn({ method: 'POST' })\n * .handler(async ({ data }) => {\n * return await db.users.create({ data });\n * }),\n * {\n * name: 'createUser',\n * captureArgs: true,\n * captureResults: false, // Don't capture for PII reasons\n * }\n * );\n * ```\n */\nexport function traceServerFn<T extends (...args: any[]) => any>(\n serverFn: T,\n config: TraceServerFnConfig = {},\n): T {\n const fnName = config.name || serverFn.name || 'serverFn';\n const captureArgs = config.captureArgs ?? true;\n const captureResults = config.captureResults ?? false;\n\n return new Proxy(serverFn, {\n apply(target, thisArg, argArray) {\n // If we're in the browser, just call the function without tracing\n // Server functions should never run in the browser, but this prevents\n // autotel (which uses Node.js APIs) from being executed if it somehow does\n if (!isServerSide()) {\n return target.apply(thisArg, argArray);\n }\n\n return trace(\n { name: `tanstack.serverFn.${fnName}`, isError: isRealError },\n async () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.RPC_SYSTEM]: 'tanstack-start',\n [SPAN_ATTRIBUTES.RPC_METHOD]: fnName,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'serverFn',\n [SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_NAME]: fnName,\n });\n\n // Capture arguments if configured\n if (captureArgs && argArray.length > 0) {\n const args = argArray[0];\n if (args !== undefined) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS,\n JSON.stringify(args),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS,\n '[non-serializable]',\n );\n }\n }\n }\n\n try {\n const result = await Reflect.apply(target, thisArg, argArray);\n\n // Capture result if configured\n if (captureResults && result !== undefined) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,\n JSON.stringify(result),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,\n '[non-serializable]',\n );\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n if ('recordError' in ctx && typeof ctx.recordError === 'function') {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n throw error;\n }\n },\n )();\n },\n\n get(target, prop, receiver) {\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n\n/**\n * Create a traced version of createServerFn\n *\n * This higher-order function wraps TanStack's createServerFn to automatically\n * add tracing to all created server functions.\n *\n * @param createServerFnOriginal - The original createServerFn from TanStack\n * @param defaultConfig - Default configuration for all server functions\n * @returns Wrapped createServerFn that produces traced server functions\n *\n * @example\n * ```typescript\n * import { createServerFn as originalCreateServerFn } from '@tanstack/react-start';\n * import { createTracedServerFnFactory } from 'autotel-tanstack/server-functions';\n *\n * export const createServerFn = createTracedServerFnFactory(originalCreateServerFn);\n *\n * // Now all server functions created with createServerFn are automatically traced\n * export const getUser = createServerFn({ method: 'GET' })\n * .handler(async ({ data: id }) => {\n * return await db.users.findUnique({ where: { id } });\n * });\n * ```\n */\nexport function createTracedServerFnFactory<\n TCreateServerFn extends (...args: any[]) => any,\n>(\n createServerFnOriginal: TCreateServerFn,\n defaultConfig: Omit<TraceServerFnConfig, 'name'> = {},\n): TCreateServerFn {\n return new Proxy(createServerFnOriginal, {\n apply(target, thisArg, argArray) {\n const result = Reflect.apply(target, thisArg, argArray);\n\n // If the result has a .handler method, wrap it\n if (\n result &&\n typeof result === 'object' &&\n 'handler' in result &&\n typeof result.handler === 'function'\n ) {\n const originalHandler = result.handler.bind(result);\n\n result.handler = function tracedHandler(handlerFn: unknown) {\n const wrappedHandler = originalHandler(handlerFn as never);\n\n // Try to infer function name from the handler\n const fnName = (handlerFn as { name?: string })?.name || 'serverFn';\n\n return traceServerFn(wrappedHandler, {\n ...defaultConfig,\n name: fnName,\n });\n };\n }\n\n return result;\n },\n }) as TCreateServerFn;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,cACd,UACA,SAA8B,CAAC,GAC5B;CACH,MAAM,SAAS,OAAO,QAAQ,SAAS,QAAQ;CAC/C,MAAM,cAAc,OAAO,eAAe;CAC1C,MAAM,iBAAiB,OAAO,kBAAkB;CAEhD,OAAO,IAAI,MAAM,UAAU;EACzB,MAAM,QAAQ,SAAS,UAAU;GAI/B,IAAI,CAAC,aAAa,GAChB,OAAO,OAAO,MAAM,SAAS,QAAQ;GAGvC,OAAO,MACL;IAAE,MAAM,qBAAqB;IAAU,SAAS;GAAY,GAC5D,YAAY;IACV,MAAM,MAAM,sBAAsB;IAClC,IAAI,cAAc;MACf,gBAAgB,aAAa;MAC7B,gBAAgB,aAAa;MAC7B,gBAAgB,gBAAgB;MAChC,gBAAgB,0BAA0B;IAC7C,CAAC;IAGD,IAAI,eAAe,SAAS,SAAS,GAAG;KACtC,MAAM,OAAO,SAAS;KACtB,IAAI,SAAS,QACX,IAAI;MACF,IAAI,aACF,gBAAgB,yBAChB,KAAK,UAAU,IAAI,CACrB;KACF,QAAQ;MACN,IAAI,aACF,gBAAgB,yBAChB,oBACF;KACF;IAEJ;IAEA,IAAI;KACF,MAAM,SAAS,MAAM,QAAQ,MAAM,QAAQ,SAAS,QAAQ;KAG5D,IAAI,kBAAkB,WAAW,QAC/B,IAAI;MACF,IAAI,aACF,gBAAgB,2BAChB,KAAK,UAAU,MAAM,CACvB;KACF,QAAQ;MACN,IAAI,aACF,gBAAgB,2BAChB,oBACF;KACF;KAGF,IAAI,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;KACzC,OAAO;IACT,SAAS,OAAO;KACd,IAAI,oBAAoB,KAAK,GAAG;MAC9B,IAAI,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;MACzC,MAAM;KACR;KACA,IAAI,iBAAiB,OAAO,OAAO,IAAI,gBAAgB,YACrD,IAAI,YAAY,KAAK;UAChB,IACL,qBAAqB,OACrB,OAAO,IAAI,oBAAoB,YAE/B,IAAI,gBAAgB,KAAK;KAE3B,MAAM;IACR;GACF,CACF,CAAC,CAAC;EACJ;EAEA,IAAI,QAAQ,MAAM,UAAU;GAC1B,OAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;EAC3C;CACF,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,4BAGd,wBACA,gBAAmD,CAAC,GACnC;CACjB,OAAO,IAAI,MAAM,wBAAwB,EACvC,MAAM,QAAQ,SAAS,UAAU;EAC/B,MAAM,SAAS,QAAQ,MAAM,QAAQ,SAAS,QAAQ;EAGtD,IACE,UACA,OAAO,WAAW,YAClB,aAAa,UACb,OAAO,OAAO,YAAY,YAC1B;GACA,MAAM,kBAAkB,OAAO,QAAQ,KAAK,MAAM;GAElD,OAAO,UAAU,SAAS,cAAc,WAAoB;IAC1D,MAAM,iBAAiB,gBAAgB,SAAkB;IAGzD,MAAM,SAAU,WAAiC,QAAQ;IAEzD,OAAO,cAAc,gBAAgB;KACnC,GAAG;KACH,MAAM;IACR,CAAC;GACH;EACF;EAEA,OAAO;CACT,EACF,CAAC;AACH"}
1
+ {"version":3,"file":"server-functions.js","names":["trace"],"sources":["../src/server-functions.ts"],"sourcesContent":["import { SpanStatusCode } from '@opentelemetry/api';\nimport { trace, getActiveTraceContext } from 'autotel';\nimport { isServerSide } from './env';\nimport { isControlFlowSignal, isRealError } from './control-flow';\nimport { type TraceServerFnConfig, SPAN_ATTRIBUTES } from './types';\n\n/**\n * Wrap a TanStack server function with OpenTelemetry tracing\n *\n * This function wraps a server function to automatically create spans\n * for each invocation. It captures function name, arguments (optionally),\n * results (optionally), and errors.\n *\n * @param serverFn - The server function to wrap\n * @param config - Configuration options\n * @returns Wrapped server function with tracing\n *\n * @example\n * ```typescript\n * import { createServerFn } from '@tanstack/react-start';\n * import { traceServerFn } from 'autotel-tanstack/server-functions';\n *\n * const getUserBase = createServerFn({ method: 'GET' })\n * .handler(async ({ data: id }) => {\n * return await db.users.findUnique({ where: { id } });\n * });\n *\n * export const getUser = traceServerFn(getUserBase, { name: 'getUser' });\n * ```\n *\n * @example\n * ```typescript\n * // With argument and result capture (careful with PII!)\n * export const createUser = traceServerFn(\n * createServerFn({ method: 'POST' })\n * .handler(async ({ data }) => {\n * return await db.users.create({ data });\n * }),\n * {\n * name: 'createUser',\n * captureArgs: true,\n * captureResults: false, // Don't capture for PII reasons\n * }\n * );\n * ```\n */\nexport function traceServerFn<T extends (...args: any[]) => any>(\n serverFn: T,\n config: TraceServerFnConfig = {},\n): T {\n const fnName = config.name || serverFn.name || 'serverFn';\n const captureArgs = config.captureArgs ?? true;\n const captureResults = config.captureResults ?? false;\n\n return new Proxy(serverFn, {\n apply(target, thisArg, argArray) {\n // If we're in the browser, just call the function without tracing\n // Server functions should never run in the browser, but this prevents\n // autotel (which uses Node.js APIs) from being executed if it somehow does\n if (!isServerSide()) {\n return target.apply(thisArg, argArray);\n }\n\n return trace(\n { name: `tanstack.serverFn.${fnName}`, isError: isRealError },\n async () => {\n const ctx = getActiveTraceContext()!;\n ctx.setAttributes({\n [SPAN_ATTRIBUTES.RPC_SYSTEM]: 'tanstack-start',\n [SPAN_ATTRIBUTES.RPC_METHOD]: fnName,\n [SPAN_ATTRIBUTES.TANSTACK_TYPE]: 'serverFn',\n [SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_NAME]: fnName,\n });\n\n // Capture arguments if configured\n if (captureArgs && argArray.length > 0) {\n const args = argArray[0];\n if (args !== undefined) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS,\n JSON.stringify(args),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_ARGS,\n '[non-serializable]',\n );\n }\n }\n }\n\n try {\n const result = await Reflect.apply(target, thisArg, argArray);\n\n // Capture result if configured\n if (captureResults && result !== undefined) {\n try {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,\n JSON.stringify(result),\n );\n } catch {\n ctx.setAttribute(\n SPAN_ATTRIBUTES.TANSTACK_SERVER_FN_RESULT,\n '[non-serializable]',\n );\n }\n }\n\n ctx.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n if (isControlFlowSignal(error)) {\n ctx.setStatus({ code: SpanStatusCode.OK });\n throw error;\n }\n if ('recordError' in ctx && typeof ctx.recordError === 'function') {\n ctx.recordError(error);\n } else if (\n 'recordException' in ctx &&\n typeof ctx.recordException === 'function'\n ) {\n ctx.recordException(error);\n }\n throw error;\n }\n },\n )();\n },\n\n get(target, prop, receiver) {\n return Reflect.get(target, prop, receiver);\n },\n }) as T;\n}\n\n/**\n * Create a traced version of createServerFn\n *\n * This higher-order function wraps TanStack's createServerFn to automatically\n * add tracing to all created server functions.\n *\n * @param createServerFnOriginal - The original createServerFn from TanStack\n * @param defaultConfig - Default configuration for all server functions\n * @returns Wrapped createServerFn that produces traced server functions\n *\n * @example\n * ```typescript\n * import { createServerFn as originalCreateServerFn } from '@tanstack/react-start';\n * import { createTracedServerFnFactory } from 'autotel-tanstack/server-functions';\n *\n * export const createServerFn = createTracedServerFnFactory(originalCreateServerFn);\n *\n * // Now all server functions created with createServerFn are automatically traced\n * export const getUser = createServerFn({ method: 'GET' })\n * .handler(async ({ data: id }) => {\n * return await db.users.findUnique({ where: { id } });\n * });\n * ```\n */\nexport function createTracedServerFnFactory<\n TCreateServerFn extends (...args: any[]) => any,\n>(\n createServerFnOriginal: TCreateServerFn,\n defaultConfig: Omit<TraceServerFnConfig, 'name'> = {},\n): TCreateServerFn {\n return new Proxy(createServerFnOriginal, {\n apply(target, thisArg, argArray) {\n const result = Reflect.apply(target, thisArg, argArray);\n\n // If the result has a .handler method, wrap it\n if (\n result &&\n typeof result === 'object' &&\n 'handler' in result &&\n typeof result.handler === 'function'\n ) {\n const originalHandler = result.handler.bind(result);\n\n result.handler = function tracedHandler(handlerFn: unknown) {\n const wrappedHandler = originalHandler(handlerFn as never);\n\n // Try to infer function name from the handler\n const fnName = (handlerFn as { name?: string })?.name || 'serverFn';\n\n return traceServerFn(wrappedHandler, {\n ...defaultConfig,\n name: fnName,\n });\n };\n }\n\n return result;\n },\n }) as TCreateServerFn;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,cACd,UACA,SAA8B,CAAC,GAC5B;CACH,MAAM,SAAS,OAAO,QAAQ,SAAS,QAAQ;CAC/C,MAAM,cAAc,OAAO,eAAe;CAC1C,MAAM,iBAAiB,OAAO,kBAAkB;CAEhD,OAAO,IAAI,MAAM,UAAU;EACzB,MAAM,QAAQ,SAAS,UAAU;GAI/B,IAAI,CAAC,aAAa,GAChB,OAAO,OAAO,MAAM,SAAS,QAAQ;GAGvC,OAAOA,QACL;IAAE,MAAM,qBAAqB;IAAU,SAAS;GAAY,GAC5D,YAAY;IACV,MAAM,MAAM,sBAAsB;IAClC,IAAI,cAAc;MACf,gBAAgB,aAAa;MAC7B,gBAAgB,aAAa;MAC7B,gBAAgB,gBAAgB;MAChC,gBAAgB,0BAA0B;IAC7C,CAAC;IAGD,IAAI,eAAe,SAAS,SAAS,GAAG;KACtC,MAAM,OAAO,SAAS;KACtB,IAAI,SAAS,QACX,IAAI;MACF,IAAI,aACF,gBAAgB,yBAChB,KAAK,UAAU,IAAI,CACrB;KACF,QAAQ;MACN,IAAI,aACF,gBAAgB,yBAChB,oBACF;KACF;IAEJ;IAEA,IAAI;KACF,MAAM,SAAS,MAAM,QAAQ,MAAM,QAAQ,SAAS,QAAQ;KAG5D,IAAI,kBAAkB,WAAW,QAC/B,IAAI;MACF,IAAI,aACF,gBAAgB,2BAChB,KAAK,UAAU,MAAM,CACvB;KACF,QAAQ;MACN,IAAI,aACF,gBAAgB,2BAChB,oBACF;KACF;KAGF,IAAI,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;KACzC,OAAO;IACT,SAAS,OAAO;KACd,IAAI,oBAAoB,KAAK,GAAG;MAC9B,IAAI,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;MACzC,MAAM;KACR;KACA,IAAI,iBAAiB,OAAO,OAAO,IAAI,gBAAgB,YACrD,IAAI,YAAY,KAAK;UAChB,IACL,qBAAqB,OACrB,OAAO,IAAI,oBAAoB,YAE/B,IAAI,gBAAgB,KAAK;KAE3B,MAAM;IACR;GACF,CACF,CAAC,CAAC;EACJ;EAEA,IAAI,QAAQ,MAAM,UAAU;GAC1B,OAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;EAC3C;CACF,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,4BAGd,wBACA,gBAAmD,CAAC,GACnC;CACjB,OAAO,IAAI,MAAM,wBAAwB,EACvC,MAAM,QAAQ,SAAS,UAAU;EAC/B,MAAM,SAAS,QAAQ,MAAM,QAAQ,SAAS,QAAQ;EAGtD,IACE,UACA,OAAO,WAAW,YAClB,aAAa,UACb,OAAO,OAAO,YAAY,YAC1B;GACA,MAAM,kBAAkB,OAAO,QAAQ,KAAK,MAAM;GAElD,OAAO,UAAU,SAAS,cAAc,WAAoB;IAC1D,MAAM,iBAAiB,gBAAgB,SAAkB;IAGzD,MAAM,SAAU,WAAiC,QAAQ;IAEzD,OAAO,cAAc,gBAAgB;KACnC,GAAG;KACH,MAAM;IACR,CAAC;GACH;EACF;EAEA,OAAO;CACT,EACF,CAAC;AACH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-tanstack",
3
- "version": "1.13.44",
3
+ "version": "1.13.45",
4
4
  "description": "OpenTelemetry instrumentation for TanStack Start - automatic tracing for server functions, middleware, and route loaders",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -96,8 +96,7 @@
96
96
  },
97
97
  "files": [
98
98
  "dist",
99
- "README.md",
100
- "skills"
99
+ "README.md"
101
100
  ],
102
101
  "keywords": [
103
102
  "opentelemetry",
@@ -119,9 +118,9 @@
119
118
  "license": "Apache-2.0",
120
119
  "dependencies": {
121
120
  "@opentelemetry/api": "^1.9.1",
122
- "autotel-adapters": "2.0.0",
123
- "autotel": "5.0.0",
124
- "autotel-edge": "4.0.0"
121
+ "autotel": "6.0.0",
122
+ "autotel-adapters": "2.0.1",
123
+ "autotel-edge": "4.0.1"
125
124
  },
126
125
  "peerDependencies": {
127
126
  "@tanstack/react-start": "^1.168.32",
@@ -1,94 +0,0 @@
1
- ---
2
- name: autotel-tanstack
3
- description: >
4
- OpenTelemetry for TanStack Start. Trace server functions, route loaders, middleware, and request handlers. Supports zero-config, middleware-based, and explicit wrapper patterns.
5
- ---
6
-
7
- # autotel-tanstack
8
-
9
- OpenTelemetry instrumentation for TanStack Start (React Start and Solid Start).
10
-
11
- ## Quick Start: pick one approach
12
-
13
- ### Zero-config
14
-
15
- ```typescript
16
- import 'autotel-tanstack/auto';
17
- // Set env: OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS
18
- ```
19
-
20
- ### Middleware (recommended)
21
-
22
- ```typescript
23
- import { tracingMiddleware } from 'autotel-tanstack/middleware';
24
-
25
- // Request-level tracing
26
- export const startInstance = createStart(() => ({
27
- requestMiddleware: [tracingMiddleware()],
28
- }));
29
-
30
- // Server function tracing
31
- export const getUser = createServerFn({ method: 'GET' })
32
- .middleware([tracingMiddleware({ type: 'function' })])
33
- .handler(async ({ data: id }) => db.users.findUnique({ where: { id } }));
34
- ```
35
-
36
- ### Explicit wrappers
37
-
38
- ```typescript
39
- import { traceServerFn } from 'autotel-tanstack/server-functions';
40
- import { traceLoader } from 'autotel-tanstack/loaders';
41
- import { wrapStartHandler } from 'autotel-tanstack/handlers';
42
-
43
- // Server function
44
- export const getUser = traceServerFn(
45
- createServerFn({ method: 'GET' }).handler(async ({ data }) => { ... }),
46
- { name: 'getUser', captureArgs: true },
47
- );
48
-
49
- // Route loader
50
- export const Route = createFileRoute('/users/$userId')({
51
- loader: traceLoader(async ({ params }) => {
52
- return db.users.findUnique({ where: { id: params.userId } });
53
- }),
54
- });
55
-
56
- // Request handler
57
- export default wrapStartHandler({
58
- service: 'my-app',
59
- endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
60
- })(createStartHandler(defaultStreamHandler));
61
- ```
62
-
63
- ## Configuration Options
64
-
65
- ```typescript
66
- tracingMiddleware({
67
- captureHeaders: ['x-request-id'], // Request headers to capture
68
- captureArgs: true, // Capture server function arguments
69
- captureResults: false, // Capture return values
70
- captureParams: true, // Capture route params
71
- excludePaths: ['/health', /^\/api\/internal/],
72
- sampling: 'adaptive', // 'adaptive' | 'always' | 'never'
73
- customAttributes: (ctx) => ({ 'app.tenant': ctx.tenant }),
74
- });
75
- ```
76
-
77
- ## Entry Points (tree-shaking)
78
-
79
- | Import | What |
80
- | ----------------------------------- | ------------------------- |
81
- | `autotel-tanstack` | Everything |
82
- | `autotel-tanstack/auto` | Zero-config |
83
- | `autotel-tanstack/middleware` | Middleware only |
84
- | `autotel-tanstack/server-functions` | Server function wrappers |
85
- | `autotel-tanstack/loaders` | Loader wrappers |
86
- | `autotel-tanstack/handlers` | Handler wrappers |
87
- | `autotel-tanstack/context` | Trace context propagation |
88
-
89
- ## Common Mistakes
90
-
91
- - Do NOT use `tracingMiddleware()` in browser: it no-ops. Server functions run server-side only.
92
- - Do NOT forget to wrap the start handler: without it, there's no root span for requests.
93
- - Use `captureArgs: true` carefully: it serializes function arguments into span attributes. Avoid for large payloads.
94
- - Prefer middleware over explicit wrappers: middleware composes with TanStack's built-in patterns.