@senzops/apm-worker 1.0.0

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/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # **@senzops/apm-worker**
2
+
3
+ The official Serverless & Worker SDK for **Senzor APM**.
4
+
5
+ Designed specifically for **Cloudflare Workers**, **Cloudflare Pages**, **Nitro**, and modern Edge runtimes.
6
+
7
+ ## **✨ Features**
8
+
9
+ - **Zero-Config Auto-Instrumentation:** Automatically captures all global `fetch` calls.
10
+ - **Context Propagation:** Uses `AsyncLocalStorage` (via `nodejs_compat`) to track requests across async boundaries without manual passing.
11
+ - **Lightweight:** < 5KB gzip, zero external dependencies.
12
+ - **Non-Blocking:** Uses `ctx.waitUntil` to flush data without adding latency to user responses.
13
+ - **W3C Trace Context:** Automatically injects `traceparent` headers into outgoing requests for distributed tracing.
14
+
15
+ ## **📦 Installation**
16
+
17
+ ```sh
18
+ npm install @senzops/apm-worker
19
+ ```
20
+
21
+ ## **🚀 Quick Start (Cloudflare Workers)**
22
+
23
+ ### **1. Enable Node Compatibility**
24
+
25
+ Add the `nodejs_compat` flag to your `wrangler.toml`. This is **required** for the SDK to track context across async `fetch` calls.
26
+
27
+ ```toml
28
+ # wrangler.toml
29
+ compatibility_flags = [ "nodejs_compat" ]
30
+ compatibility_date = "2024-09-23"
31
+ ```
32
+
33
+ ### **2. Integrate the SDK**
34
+
35
+ Initialize Senzor in the global scope and wrap your `fetch` handler.
36
+
37
+ ```typescript
38
+ import Senzor from "@senzops/apm-worker";
39
+
40
+ // 1. Initialize (Global Scope)
41
+ Senzor.init({
42
+ apiKey: "sz_apm_...",
43
+ });
44
+
45
+ export default {
46
+ // 2. Wrap the fetch handler
47
+ fetch: Senzor.worker(async (request, env, ctx) => {
48
+ // ... your code ...
49
+ return new Response("Hello World!");
50
+ }),
51
+ };
52
+ ```
53
+
54
+ ---
55
+
56
+ ## **⚡ Usage with Nitro / Nuxt**
57
+
58
+ If you are using **Nitro** (standalone) or **Nuxt** deployed to Cloudflare Workers.
59
+
60
+ ### **1. Create a Server Plugin**
61
+
62
+ Create a file at `server/plugins/senzor.ts`:
63
+
64
+ ```typescript
65
+ import Senzor from "@senzops/apm-worker";
66
+
67
+ export default defineNitroPlugin((nitroApp) => {
68
+ // 1. Initialize
69
+ // Note: For Workers, 'env' vars might need runtime access or define replacement
70
+ Senzor.init({
71
+ apiKey: process.env.SENZOR_API_KEY || "sz_apm_...",
72
+ });
73
+
74
+ // 2. Hook into request handling
75
+ nitroApp.hooks.hook("request", (event) => {
76
+ // Nitro hooks don't easily allow wrapping the entire execution context for AsyncLocalStorage yet.
77
+ // For full auto-instrumentation, consider wrapping specific event handlers or using the middleware approach below.
78
+ });
79
+ });
80
+ ```
81
+
82
+ ### **Recommended: Wrap Event Handlers**
83
+
84
+ To ensure `fetch` auto-instrumentation works correctly with `AsyncLocalStorage`, wrap your event handlers.
85
+
86
+ ```typescript
87
+ // server/api/hello.ts
88
+ import Senzor from "@senzops/apm-worker";
89
+
90
+ export default Senzor.nitro(
91
+ defineEventHandler(async (event) => {
92
+ // ✅ This fetch is automatically tracked
93
+ await fetch("https://google.com");
94
+
95
+ return { hello: "world" };
96
+ }),
97
+ );
98
+ ```
99
+
100
+ ### **Configuration (nitro.config.ts)**
101
+
102
+ Ensure you enable the node compatibility for Cloudflare.
103
+
104
+ ```typescript
105
+ // nitro.config.ts
106
+ export default defineNitroConfig({
107
+ cloudflare: {
108
+ wrangler: {
109
+ compatibility_flags: ["nodejs_compat"],
110
+ },
111
+ },
112
+ });
113
+ ```
114
+
115
+ ---
116
+
117
+ ## **📋 Production Checklist**
118
+
119
+ ### **1. Environment Variables**
120
+
121
+ For security, do not commit your API key.
122
+
123
+ 1. Run `npx wrangler secret put SENZOR_API_KEY`.
124
+ 2. Update your worker to initialize lazily or use build-time variables.
125
+
126
+ ### **2. Distributed Tracing**
127
+
128
+ If your Worker calls other services (like a backend API), this SDK automatically adds the `traceparent` header.
129
+ Ensure your backend services (Node.js, Python, Go) are configured to extract this header to see a connected trace from Edge -> Backend.
130
+
131
+ ### **3. Error Tracking**
132
+
133
+ Any uncaught exception thrown in your handler is automatically captured, logged, and reported to Senzor with the stack trace.
@@ -0,0 +1,41 @@
1
+ interface SenzorOptions {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ debug?: boolean;
5
+ }
6
+ interface TraceController {
7
+ /**
8
+ * Starts a new span.
9
+ */
10
+ startSpan(name: string, type?: 'db' | 'http' | 'function' | 'custom'): {
11
+ end: (meta?: any, status?: number) => void;
12
+ };
13
+ /**
14
+ * Adds an error to the trace.
15
+ */
16
+ captureException(error: Error | any): void;
17
+ /**
18
+ * The trace ID for the current session
19
+ */
20
+ readonly traceId: string;
21
+ }
22
+
23
+ declare const Senzor: {
24
+ /**
25
+ * Initialize the SDK.
26
+ * Call this in the global scope of your Worker or Plugin.
27
+ */
28
+ init: (options: SenzorOptions) => void;
29
+ /**
30
+ * Wrap your Cloudflare Worker 'fetch' handler.
31
+ * Inject trace controller as the 4th argument.
32
+ */
33
+ worker: (handler: (request: Request, env: any, ctx: any, trace: TraceController) => Promise<Response>) => (request: Request, env: any, ctx: any) => Promise<Response>;
34
+ /**
35
+ * Wrap a Nitro/H3 event handler.
36
+ * Use this for Nuxt or pure Nitro server routes.
37
+ */
38
+ nitro: (handler: (event: any) => any) => (event: any) => Promise<any>;
39
+ };
40
+
41
+ export { Senzor, type TraceController, Senzor as default };
@@ -0,0 +1,41 @@
1
+ interface SenzorOptions {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ debug?: boolean;
5
+ }
6
+ interface TraceController {
7
+ /**
8
+ * Starts a new span.
9
+ */
10
+ startSpan(name: string, type?: 'db' | 'http' | 'function' | 'custom'): {
11
+ end: (meta?: any, status?: number) => void;
12
+ };
13
+ /**
14
+ * Adds an error to the trace.
15
+ */
16
+ captureException(error: Error | any): void;
17
+ /**
18
+ * The trace ID for the current session
19
+ */
20
+ readonly traceId: string;
21
+ }
22
+
23
+ declare const Senzor: {
24
+ /**
25
+ * Initialize the SDK.
26
+ * Call this in the global scope of your Worker or Plugin.
27
+ */
28
+ init: (options: SenzorOptions) => void;
29
+ /**
30
+ * Wrap your Cloudflare Worker 'fetch' handler.
31
+ * Inject trace controller as the 4th argument.
32
+ */
33
+ worker: (handler: (request: Request, env: any, ctx: any, trace: TraceController) => Promise<Response>) => (request: Request, env: any, ctx: any) => Promise<Response>;
34
+ /**
35
+ * Wrap a Nitro/H3 event handler.
36
+ * Use this for Nuxt or pure Nitro server routes.
37
+ */
38
+ nitro: (handler: (event: any) => any) => (event: any) => Promise<any>;
39
+ };
40
+
41
+ export { Senzor, type TraceController, Senzor as default };
@@ -0,0 +1,2 @@
1
+ "use strict";(()=>{var z=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(t,e)=>(typeof require<"u"?require:t)[e]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+r+'" is not supported')});var m=class{constructor(t){this.config=t;this.queue=[]}add(t){this.queue.push(t)}async flush(){if(this.queue.length===0)return;let t=[...this.queue];this.queue=[];try{let e=this.config.endpoint||"https://api.senzor.dev/api/ingest/apm";await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","x-service-api-key":this.config.apiKey},body:JSON.stringify(t),keepalive:!0}),this.config.debug&&console.log(`[Senzor] Flushed ${t.length} traces`)}catch(e){this.config.debug&&console.error("[Senzor] Ingestion Error:",e)}}};var T=z("async_hooks"),f=new T.AsyncLocalStorage;var w=!1,S=()=>{if(w)return;let r=globalThis.fetch;globalThis.fetch=async(t,e)=>{let n=f.getStore();if(!n)return r(t,e);let o="",a="GET";typeof t=="string"?o=t:t instanceof URL?o=t.toString():t instanceof Request&&(o=t.url,a=t.method),e&&e.method&&(a=e.method);let s=`HTTP ${a}`,c=n.startSpan(s,"http"),l=new Headers(e?.headers||(t instanceof Request?t.headers:{})),i=crypto.randomUUID().replace(/-/g,"").substring(0,16),h=`00-${n.traceId}-${i}-01`;l.set("traceparent",h);let u={...e,headers:l};try{let p=await r(t,u);return c.end({url:o,method:a,status:p.status},p.status),p}catch(p){throw c.end({url:o,method:a,error:p.message},500),p}},w=!0};var g=class{constructor(){this.transport=null;this.options=null}init(t){if(!t.apiKey){console.warn("[Senzor] API Key missing. SDK disabled.");return}this.options=t,this.transport=new m(t);try{S()}catch(e){t.debug&&console.warn("[Senzor] Failed to instrument fetch:",e)}t.debug&&console.log("[Senzor] Initialized for Serverless")}createTrace(t){if(!this.transport)return{controller:{startSpan:()=>({end:()=>{}}),captureException:()=>{},traceId:"00000000000000000000000000000000"},end:()=>{},flush:async()=>{}};let e=crypto.randomUUID().replace(/-/g,""),n=performance.now(),o=[];return{controller:{traceId:e,startSpan:(i,h="custom")=>{let u=performance.now(),p=u-n;return{end:(R,U)=>{o.push({name:i,type:h,startTime:p,duration:performance.now()-u,status:U,meta:R})}}},captureException:i=>{o.push({name:"exception",type:"custom",startTime:performance.now()-n,duration:0,status:500,meta:{error:i.message||String(i),stack:i.stack}})}},end:(i,h="UNKNOWN")=>{let u=performance.now()-n,p={traceId:e,method:t.method||"GET",route:h,path:t.path||"/",status:i,duration:u,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:o};this.transport?.add(p)},flush:async()=>{await this.transport?.flush()}}}track(t){if(!this.transport)return;let e={traceId:crypto.randomUUID(),method:t.method||"GET",route:t.route,path:t.path||"/",status:t.status,duration:t.duration,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:t.spans||[]};this.transport.add(e),this.transport.flush().catch(()=>{})}startTrace(t,e){return e()}endTrace(t,e){}},d=new g;var y=r=>!r||r==="/"?"/":r.replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,":uuid").replace(/[0-9a-fA-F]{24}/g,":objectId").replace(/\/(\d+)(?=\/|$)/g,"/:id").split("?")[0],b=(r,t)=>r.route&&r.route.path?(r.baseUrl||"")+r.route.path:r.context&&r.context.matchedRoute?r.context.matchedRoute.path:r.routerPath?r.routerPath:y(t);var x=r=>async(t,e,n)=>{let a=new URL(t.url).pathname,s=d.createTrace({method:t.method,path:a,userAgent:t.headers.get("user-agent")||void 0,ip:t.headers.get("cf-connecting-ip")||t.headers.get("x-forwarded-for")||void 0});return f.run(s.controller,async()=>{let c,l=500;try{return c=await r(t,e,n,s.controller),l=c.status,c}catch(i){throw s.controller.captureException(i),i}finally{s.end(l,y(a)),n&&typeof n.waitUntil=="function"?n.waitUntil(s.flush()):await s.flush()}})};var I=r=>async t=>{let e=t.node.req,n=e.originalUrl||e.url||"/",o=d.createTrace({method:e.method||"GET",path:n,ip:e.headers["x-forwarded-for"]||e.socket?.remoteAddress||t.context?.cf?.connectingIp,userAgent:e.headers["user-agent"]});return f.run(o.controller,async()=>{let a,s=200;try{return a=await r(t),t.node.res.statusCode&&(s=t.node.res.statusCode),a&&a.statusCode&&(s=a.statusCode),a}catch(c){throw s=c.statusCode||c.status||500,o.controller.captureException(c),c}finally{o.end(s,b(t,n));let l=(t.context?.cloudflare?.context||t.context?.cf)?.waitUntil||t.waitUntil;l&&typeof l=="function"?l(o.flush()):o.flush().catch(()=>{})}})};var A={init:r=>d.init(r),worker:x,nitro:I},Q=A;})();
2
+ //# sourceMappingURL=index.global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/transport.ts","../src/core/context.ts","../src/instrumentation/fetch.ts","../src/core/client.ts","../src/core/normalizer.ts","../src/wrappers/worker.ts","../src/wrappers/h3.ts","../src/index.ts"],"sourcesContent":["import { SenzorOptions, TraceData } from './types';\r\n\r\nexport class Transport {\r\n private queue: TraceData[] = [];\r\n\r\n constructor(private config: SenzorOptions) { }\r\n\r\n public add(trace: TraceData) {\r\n this.queue.push(trace);\r\n }\r\n\r\n /**\r\n * Flushes the queue to the API. \r\n * Returns a promise that should be passed to ctx.waitUntil()\r\n */\r\n public async flush(): Promise<void> {\r\n if (this.queue.length === 0) return;\r\n\r\n const batch = [...this.queue];\r\n this.queue = []; // Clear immediately\r\n\r\n try {\r\n const endpoint = this.config.endpoint || 'https://api.senzor.dev/api/ingest/apm';\r\n\r\n await fetch(endpoint, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'x-service-api-key': this.config.apiKey,\r\n },\r\n body: JSON.stringify(batch),\r\n // keepalive is not strictly necessary in workers if awaited/waitUntil'd, but good practice\r\n keepalive: true,\r\n });\r\n\r\n if (this.config.debug) console.log(`[Senzor] Flushed ${batch.length} traces`);\r\n } catch (err) {\r\n if (this.config.debug) console.error('[Senzor] Ingestion Error:', err);\r\n }\r\n }\r\n}\r\n","import { AsyncLocalStorage } from 'node:async_hooks';\r\nimport { TraceController } from './types';\r\n\r\n// This relies on the 'nodejs_compat' compatibility flag in Cloudflare Workers.\r\nexport const storage = new AsyncLocalStorage<TraceController>();\r\n","import { storage } from '../core/context';\r\n\r\nlet isInstrumented = false;\r\n\r\nexport const enableFetchInstrumentation = () => {\r\n if (isInstrumented) return;\r\n\r\n const originalFetch = globalThis.fetch;\r\n\r\n // Monkey-patch global fetch\r\n globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {\r\n // 1. Check for active trace context\r\n const controller = storage.getStore();\r\n\r\n // If no trace is active, just pass through\r\n if (!controller) {\r\n return originalFetch(input, init);\r\n }\r\n\r\n // 2. Resolve URL and Method\r\n let url = '';\r\n let method = 'GET';\r\n\r\n if (typeof input === 'string') {\r\n url = input;\r\n } else if (input instanceof URL) {\r\n url = input.toString();\r\n } else if (input instanceof Request) {\r\n url = input.url;\r\n method = input.method;\r\n }\r\n\r\n if (init && init.method) {\r\n method = init.method;\r\n }\r\n\r\n // 3. Start Span\r\n const spanName = `HTTP ${method}`;\r\n const span = controller.startSpan(spanName, 'http');\r\n\r\n // 4. Inject Trace Headers (W3C Trace Context)\r\n // We clone headers to avoid side effects on the input object\r\n const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : {}));\r\n\r\n // Generate a span ID for the outgoing request\r\n const spanId = crypto.randomUUID().replace(/-/g, '').substring(0, 16);\r\n const traceParent = `00-${controller.traceId}-${spanId}-01`;\r\n headers.set('traceparent', traceParent);\r\n\r\n const newInit: RequestInit = {\r\n ...init,\r\n headers\r\n };\r\n\r\n try {\r\n const response = await originalFetch(input, newInit);\r\n span.end({\r\n url,\r\n method,\r\n status: response.status\r\n }, response.status);\r\n return response;\r\n } catch (err: any) {\r\n span.end({\r\n url,\r\n method,\r\n error: err.message\r\n }, 500);\r\n throw err;\r\n }\r\n };\r\n\r\n isInstrumented = true;\r\n};\r\n","import { Transport } from './transport';\r\nimport { SenzorOptions, TraceData, Span, TraceController } from './types';\r\nimport { enableFetchInstrumentation } from '../instrumentation/fetch';\r\n\r\nexport class SenzorClient {\r\n private transport: Transport | null = null;\r\n private options: SenzorOptions | null = null;\r\n\r\n public init(options: SenzorOptions) {\r\n if (!options.apiKey) {\r\n console.warn('[Senzor] API Key missing. SDK disabled.');\r\n return;\r\n }\r\n this.options = options;\r\n this.transport = new Transport(options);\r\n\r\n // Auto-instrument global fetch\r\n try {\r\n enableFetchInstrumentation();\r\n } catch (e) {\r\n if (options.debug) console.warn('[Senzor] Failed to instrument fetch:', e);\r\n }\r\n\r\n if (options.debug) console.log('[Senzor] Initialized for Serverless');\r\n }\r\n\r\n /**\r\n * Creates a detached trace session.\r\n */\r\n public createTrace(data: Partial<TraceData>): { controller: TraceController, end: (status: number, route?: string) => void, flush: () => Promise<void> } {\r\n if (!this.transport) {\r\n // Return dummy if not initialized\r\n return {\r\n controller: {\r\n startSpan: () => ({ end: () => { } }),\r\n captureException: () => { },\r\n traceId: '00000000000000000000000000000000'\r\n },\r\n end: () => { },\r\n flush: async () => { }\r\n };\r\n }\r\n\r\n const traceId = crypto.randomUUID().replace(/-/g, ''); // 32 hex chars usually\r\n const startTime = performance.now();\r\n const spans: Span[] = [];\r\n\r\n const startSpan = (name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') => {\r\n const spanStartAbs = performance.now();\r\n const startRel = spanStartAbs - startTime;\r\n return {\r\n end: (meta?: any, status?: number) => {\r\n spans.push({\r\n name,\r\n type,\r\n startTime: startRel, // Relative to trace start\r\n duration: performance.now() - spanStartAbs,\r\n status,\r\n meta\r\n });\r\n }\r\n };\r\n };\r\n\r\n const controller: TraceController = {\r\n traceId,\r\n startSpan,\r\n captureException: (err: any) => {\r\n spans.push({\r\n name: 'exception',\r\n type: 'custom',\r\n startTime: performance.now() - startTime,\r\n duration: 0,\r\n status: 500,\r\n meta: { error: err.message || String(err), stack: err.stack }\r\n });\r\n }\r\n };\r\n\r\n const end = (status: number, route: string = 'UNKNOWN') => {\r\n const duration = performance.now() - startTime;\r\n const payload: TraceData = {\r\n traceId,\r\n method: data.method || 'GET',\r\n route,\r\n path: data.path || '/',\r\n status,\r\n duration,\r\n ip: data.ip,\r\n userAgent: data.userAgent,\r\n timestamp: new Date().toISOString(),\r\n spans\r\n };\r\n this.transport?.add(payload);\r\n };\r\n\r\n const flush = async () => {\r\n await this.transport?.flush();\r\n };\r\n\r\n return { controller, end, flush };\r\n }\r\n\r\n /**\r\n * Track a single request trace immediately.\r\n */\r\n public track(data: Partial<TraceData> & { status: number, duration: number, route: string }) {\r\n if (!this.transport) return;\r\n\r\n const payload: TraceData = {\r\n traceId: crypto.randomUUID(),\r\n method: data.method || 'GET',\r\n route: data.route,\r\n path: data.path || '/',\r\n status: data.status,\r\n duration: data.duration,\r\n ip: data.ip,\r\n userAgent: data.userAgent,\r\n timestamp: new Date().toISOString(),\r\n spans: data.spans || []\r\n };\r\n\r\n this.transport.add(payload);\r\n this.transport.flush().catch(() => { });\r\n }\r\n\r\n // Stubs for legacy Node support\r\n public startTrace<T>(data: Partial<TraceData>, callback: () => T): T { return callback(); }\r\n public endTrace(status: number, data?: { route?: string }) { }\r\n}\r\n\r\nexport const client = new SenzorClient();\r\n","/**\r\n * Heuristic URL Normalizer\r\n * Converts raw paths with IDs into generic patterns to prevent high cardinality.\r\n * Example: /users/123/orders/abc-def -> /users/:id/orders/:uuid\r\n */\r\nexport const normalizePath = (path: string): string => {\r\n if (!path || path === '/') return '/';\r\n\r\n return path\r\n // Replace UUIDs (long alphanumeric strings)\r\n .replace(\r\n /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,\r\n ':uuid'\r\n )\r\n // Replace MongoDB ObjectIds (24 hex chars)\r\n .replace(/[0-9a-fA-F]{24}/g, ':objectId')\r\n // Replace pure numeric IDs (e.g., /123)\r\n .replace(/\\/(\\d+)(?=\\/|$)/g, '/:id')\r\n // Remove query strings\r\n .split('?')[0];\r\n};\r\n\r\n/**\r\n * Tries to extract route from Framework internals, falls back to heuristic\r\n */\r\nexport const getRoute = (req: any, fallbackPath: string): string => {\r\n // Express / Connect\r\n if (req.route && req.route.path) {\r\n return (req.baseUrl || '') + req.route.path;\r\n }\r\n\r\n // H3 / Nitro (Nuxt)\r\n if (req.context && req.context.matchedRoute) {\r\n return req.context.matchedRoute.path;\r\n }\r\n\r\n // Fastify\r\n if (req.routerPath) {\r\n return req.routerPath;\r\n }\r\n\r\n // Fallback: Heuristic Normalization\r\n return normalizePath(fallbackPath);\r\n};","import { client } from '../core/client';\r\nimport { normalizePath } from '../core/normalizer';\r\nimport { TraceController } from '../core/types';\r\nimport { storage } from '../core/context';\r\n\r\ntype WorkerHandler = (request: Request, env: any, ctx: any, trace: TraceController) => Promise<Response>;\r\n\r\nexport const wrapWorker = (handler: WorkerHandler) => {\r\n return async (request: Request, env: any, ctx: any) => {\r\n // 2. Extract Request Info\r\n const url = new URL(request.url);\r\n const path = url.pathname;\r\n\r\n // 3. Start Trace\r\n const session = client.createTrace({\r\n method: request.method,\r\n path: path,\r\n userAgent: request.headers.get('user-agent') || undefined,\r\n ip: request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || undefined\r\n });\r\n\r\n // 4. Run Handler within Context (AsyncLocalStorage)\r\n // This enables the global fetch instrumentation to find the current trace controller\r\n return storage.run(session.controller, async () => {\r\n let response: Response;\r\n let status = 500;\r\n\r\n try {\r\n // Inject trace controller as 4th arg for manual usage\r\n response = await handler(request, env, ctx, session.controller);\r\n status = response.status;\r\n return response;\r\n } catch (err: any) {\r\n session.controller.captureException(err);\r\n throw err;\r\n } finally {\r\n // 5. End Trace\r\n session.end(status, normalizePath(path));\r\n\r\n // 6. Flush (Async WaitUntil)\r\n if (ctx && typeof ctx.waitUntil === 'function') {\r\n ctx.waitUntil(session.flush());\r\n } else {\r\n await session.flush();\r\n }\r\n }\r\n });\r\n };\r\n};\r\n","import { client } from '../core/client';\r\nimport { getRoute } from '../core/normalizer';\r\nimport { storage } from '../core/context';\r\n\r\n// Minimal types for H3 to avoid peer-deps\r\ntype EventHandler = (event: any) => any;\r\n\r\nexport const wrapH3 = (handler: EventHandler) => {\r\n return async (event: any) => {\r\n const req = event.node.req;\r\n const path = req.originalUrl || req.url || '/';\r\n\r\n // 1. Start Trace Session\r\n const session = client.createTrace({\r\n method: req.method || 'GET',\r\n path: path,\r\n ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress || event.context?.cf?.connectingIp,\r\n userAgent: req.headers['user-agent'],\r\n });\r\n\r\n // 2. Run Handler within AsyncLocalStorage Context\r\n // This ensures global fetch() auto-instrumentation works inside Nitro handlers\r\n return storage.run(session.controller, async () => {\r\n let response: any;\r\n let status = 200;\r\n\r\n try {\r\n response = await handler(event);\r\n\r\n // H3/Nitro response status handling\r\n if (event.node.res.statusCode) status = event.node.res.statusCode;\r\n if (response && response.statusCode) status = response.statusCode;\r\n\r\n return response;\r\n } catch (err: any) {\r\n status = err.statusCode || err.status || 500;\r\n session.controller.captureException(err);\r\n throw err;\r\n } finally {\r\n // 3. End Trace\r\n session.end(status, getRoute(event, path));\r\n\r\n // 4. Flush Data (Non-blocking for Cloudflare)\r\n // Nitro exposes Cloudflare context in event.context.cloudflare\r\n const cfCtx = event.context?.cloudflare?.context || event.context?.cf;\r\n // Or sometimes directly on event in newer H3 versions if adapter binds it\r\n const waitUntil = cfCtx?.waitUntil || event.waitUntil;\r\n\r\n if (waitUntil && typeof waitUntil === 'function') {\r\n waitUntil(session.flush());\r\n } else {\r\n // If not in a worker environment or waitUntil missing, flush async but don't block response significantly\r\n // (Note: without waitUntil, the runtime might kill the process before flush completes)\r\n session.flush().catch(() => { });\r\n }\r\n }\r\n });\r\n };\r\n};\r\n","import { client } from './core/client';\r\nimport { wrapWorker } from './wrappers/worker';\r\nimport { wrapH3 } from './wrappers/h3';\r\nimport { SenzorOptions } from './core/types';\r\n\r\nconst Senzor = {\r\n /**\r\n * Initialize the SDK.\r\n * Call this in the global scope of your Worker or Plugin.\r\n */\r\n init: (options: SenzorOptions) => client.init(options),\r\n\r\n /**\r\n * Wrap your Cloudflare Worker 'fetch' handler.\r\n * Inject trace controller as the 4th argument.\r\n */\r\n worker: wrapWorker,\r\n\r\n /**\r\n * Wrap a Nitro/H3 event handler.\r\n * Use this for Nuxt or pure Nitro server routes.\r\n */\r\n nitro: wrapH3\r\n};\r\n\r\nexport default Senzor;\r\nexport { Senzor };\r\nexport type { TraceController } from './core/types';\r\n"],"mappings":"4QAEO,IAAMA,EAAN,KAAgB,CAGrB,YAAoBC,EAAuB,CAAvB,YAAAA,EAFpB,KAAQ,MAAqB,CAAC,CAEe,CAEtC,IAAIC,EAAkB,CAC3B,KAAK,MAAM,KAAKA,CAAK,CACvB,CAMA,MAAa,OAAuB,CAClC,GAAI,KAAK,MAAM,SAAW,EAAG,OAE7B,IAAMC,EAAQ,CAAC,GAAG,KAAK,KAAK,EAC5B,KAAK,MAAQ,CAAC,EAEd,GAAI,CACF,IAAMC,EAAW,KAAK,OAAO,UAAY,wCAEzC,MAAM,MAAMA,EAAU,CACpB,OAAQ,OACR,QAAS,CACP,eAAgB,mBAChB,oBAAqB,KAAK,OAAO,MACnC,EACA,KAAM,KAAK,UAAUD,CAAK,EAE1B,UAAW,EACb,CAAC,EAEG,KAAK,OAAO,OAAO,QAAQ,IAAI,oBAAoBA,EAAM,MAAM,SAAS,CAC9E,OAASE,EAAK,CACR,KAAK,OAAO,OAAO,QAAQ,MAAM,4BAA6BA,CAAG,CACvE,CACF,CACF,ECxCA,IAAAC,EAAkC,iBAIrBC,EAAU,IAAI,oBCF3B,IAAIC,EAAiB,GAERC,EAA6B,IAAM,CAC9C,GAAID,EAAgB,OAEpB,IAAME,EAAgB,WAAW,MAGjC,WAAW,MAAQ,MAAOC,EAA0BC,IAAuB,CAEzE,IAAMC,EAAaC,EAAQ,SAAS,EAGpC,GAAI,CAACD,EACH,OAAOH,EAAcC,EAAOC,CAAI,EAIlC,IAAIG,EAAM,GACNC,EAAS,MAET,OAAOL,GAAU,SACnBI,EAAMJ,EACGA,aAAiB,IAC1BI,EAAMJ,EAAM,SAAS,EACZA,aAAiB,UAC1BI,EAAMJ,EAAM,IACZK,EAASL,EAAM,QAGbC,GAAQA,EAAK,SACfI,EAASJ,EAAK,QAIhB,IAAMK,EAAW,QAAQD,CAAM,GACzBE,EAAOL,EAAW,UAAUI,EAAU,MAAM,EAI5CE,EAAU,IAAI,QAAQP,GAAM,UAAYD,aAAiB,QAAUA,EAAM,QAAU,CAAC,EAAE,EAGtFS,EAAS,OAAO,WAAW,EAAE,QAAQ,KAAM,EAAE,EAAE,UAAU,EAAG,EAAE,EAC9DC,EAAc,MAAMR,EAAW,OAAO,IAAIO,CAAM,MACtDD,EAAQ,IAAI,cAAeE,CAAW,EAEtC,IAAMC,EAAuB,CAC3B,GAAGV,EACH,QAAAO,CACF,EAEA,GAAI,CACF,IAAMI,EAAW,MAAMb,EAAcC,EAAOW,CAAO,EACnD,OAAAJ,EAAK,IAAI,CACP,IAAAH,EACA,OAAAC,EACA,OAAQO,EAAS,MACnB,EAAGA,EAAS,MAAM,EACXA,CACT,OAASC,EAAU,CACjB,MAAAN,EAAK,IAAI,CACP,IAAAH,EACA,OAAAC,EACA,MAAOQ,EAAI,OACb,EAAG,GAAG,EACAA,CACR,CACF,EAEAhB,EAAiB,EACnB,ECrEO,IAAMiB,EAAN,KAAmB,CAAnB,cACL,KAAQ,UAA8B,KACtC,KAAQ,QAAgC,KAEjC,KAAKC,EAAwB,CAClC,GAAI,CAACA,EAAQ,OAAQ,CACnB,QAAQ,KAAK,yCAAyC,EACtD,MACF,CACA,KAAK,QAAUA,EACf,KAAK,UAAY,IAAIC,EAAUD,CAAO,EAGtC,GAAI,CACFE,EAA2B,CAC7B,OAAS,EAAG,CACNF,EAAQ,OAAO,QAAQ,KAAK,uCAAwC,CAAC,CAC3E,CAEIA,EAAQ,OAAO,QAAQ,IAAI,qCAAqC,CACtE,CAKO,YAAYG,EAAsI,CACvJ,GAAI,CAAC,KAAK,UAER,MAAO,CACL,WAAY,CACV,UAAW,KAAO,CAAE,IAAK,IAAM,CAAE,CAAE,GACnC,iBAAkB,IAAM,CAAE,EAC1B,QAAS,kCACX,EACA,IAAK,IAAM,CAAE,EACb,MAAO,SAAY,CAAE,CACvB,EAGF,IAAMC,EAAU,OAAO,WAAW,EAAE,QAAQ,KAAM,EAAE,EAC9CC,EAAY,YAAY,IAAI,EAC5BC,EAAgB,CAAC,EAuDvB,MAAO,CAAE,WApC2B,CAClC,QAAAF,EACA,UAnBgB,CAACG,EAAcC,EAA8C,WAAa,CAC1F,IAAMC,EAAe,YAAY,IAAI,EAC/BC,EAAWD,EAAeJ,EAChC,MAAO,CACL,IAAK,CAACM,EAAYC,IAAoB,CACpCN,EAAM,KAAK,CACT,KAAAC,EACA,KAAAC,EACA,UAAWE,EACX,SAAU,YAAY,IAAI,EAAID,EAC9B,OAAAG,EACA,KAAAD,CACF,CAAC,CACH,CACF,CACF,EAKE,iBAAmBE,GAAa,CAC9BP,EAAM,KAAK,CACT,KAAM,YACN,KAAM,SACN,UAAW,YAAY,IAAI,EAAID,EAC/B,SAAU,EACV,OAAQ,IACR,KAAM,CAAE,MAAOQ,EAAI,SAAW,OAAOA,CAAG,EAAG,MAAOA,EAAI,KAAM,CAC9D,CAAC,CACH,CACF,EAuBqB,IArBT,CAACD,EAAgBE,EAAgB,YAAc,CACzD,IAAMC,EAAW,YAAY,IAAI,EAAIV,EAC/BW,EAAqB,CACzB,QAAAZ,EACA,OAAQD,EAAK,QAAU,MACvB,MAAAW,EACA,KAAMX,EAAK,MAAQ,IACnB,OAAAS,EACA,SAAAG,EACA,GAAIZ,EAAK,GACT,UAAWA,EAAK,UAChB,UAAW,IAAI,KAAK,EAAE,YAAY,EAClC,MAAAG,CACF,EACA,KAAK,WAAW,IAAIU,CAAO,CAC7B,EAM0B,MAJZ,SAAY,CACxB,MAAM,KAAK,WAAW,MAAM,CAC9B,CAEgC,CAClC,CAKO,MAAMb,EAAgF,CAC3F,GAAI,CAAC,KAAK,UAAW,OAErB,IAAMa,EAAqB,CACzB,QAAS,OAAO,WAAW,EAC3B,OAAQb,EAAK,QAAU,MACvB,MAAOA,EAAK,MACZ,KAAMA,EAAK,MAAQ,IACnB,OAAQA,EAAK,OACb,SAAUA,EAAK,SACf,GAAIA,EAAK,GACT,UAAWA,EAAK,UAChB,UAAW,IAAI,KAAK,EAAE,YAAY,EAClC,MAAOA,EAAK,OAAS,CAAC,CACxB,EAEA,KAAK,UAAU,IAAIa,CAAO,EAC1B,KAAK,UAAU,MAAM,EAAE,MAAM,IAAM,CAAE,CAAC,CACxC,CAGO,WAAcb,EAA0Bc,EAAsB,CAAE,OAAOA,EAAS,CAAG,CACnF,SAASL,EAAgBT,EAA2B,CAAE,CAC/D,EAEae,EAAS,IAAInB,EC9HnB,IAAMoB,EAAiBC,GACxB,CAACA,GAAQA,IAAS,IAAY,IAE3BA,EAEJ,QACC,+EACA,OACF,EAEC,QAAQ,mBAAoB,WAAW,EAEvC,QAAQ,mBAAoB,MAAM,EAElC,MAAM,GAAG,EAAE,CAAC,EAMJC,EAAW,CAACC,EAAUC,IAE7BD,EAAI,OAASA,EAAI,MAAM,MACjBA,EAAI,SAAW,IAAMA,EAAI,MAAM,KAIrCA,EAAI,SAAWA,EAAI,QAAQ,aACtBA,EAAI,QAAQ,aAAa,KAI9BA,EAAI,WACCA,EAAI,WAINH,EAAcI,CAAY,ECnC5B,IAAMC,EAAcC,GAClB,MAAOC,EAAkBC,EAAUC,IAAa,CAGrD,IAAMC,EADM,IAAI,IAAIH,EAAQ,GAAG,EACd,SAGXI,EAAUC,EAAO,YAAY,CACjC,OAAQL,EAAQ,OAChB,KAAMG,EACN,UAAWH,EAAQ,QAAQ,IAAI,YAAY,GAAK,OAChD,GAAIA,EAAQ,QAAQ,IAAI,kBAAkB,GAAKA,EAAQ,QAAQ,IAAI,iBAAiB,GAAK,MAC3F,CAAC,EAID,OAAOM,EAAQ,IAAIF,EAAQ,WAAY,SAAY,CACjD,IAAIG,EACAC,EAAS,IAEb,GAAI,CAEF,OAAAD,EAAW,MAAMR,EAAQC,EAASC,EAAKC,EAAKE,EAAQ,UAAU,EAC9DI,EAASD,EAAS,OACXA,CACT,OAASE,EAAU,CACjB,MAAAL,EAAQ,WAAW,iBAAiBK,CAAG,EACjCA,CACR,QAAE,CAEAL,EAAQ,IAAII,EAAQE,EAAcP,CAAI,CAAC,EAGnCD,GAAO,OAAOA,EAAI,WAAc,WAClCA,EAAI,UAAUE,EAAQ,MAAM,CAAC,EAE7B,MAAMA,EAAQ,MAAM,CAExB,CACF,CAAC,CACH,ECxCK,IAAMO,EAAUC,GACd,MAAOC,GAAe,CAC3B,IAAMC,EAAMD,EAAM,KAAK,IACjBE,EAAOD,EAAI,aAAeA,EAAI,KAAO,IAGrCE,EAAUC,EAAO,YAAY,CACjC,OAAQH,EAAI,QAAU,MACtB,KAAMC,EACN,GAAID,EAAI,QAAQ,iBAAiB,GAAKA,EAAI,QAAQ,eAAiBD,EAAM,SAAS,IAAI,aACtF,UAAWC,EAAI,QAAQ,YAAY,CACrC,CAAC,EAID,OAAOI,EAAQ,IAAIF,EAAQ,WAAY,SAAY,CACjD,IAAIG,EACAC,EAAS,IAEb,GAAI,CACF,OAAAD,EAAW,MAAMP,EAAQC,CAAK,EAG1BA,EAAM,KAAK,IAAI,aAAYO,EAASP,EAAM,KAAK,IAAI,YACnDM,GAAYA,EAAS,aAAYC,EAASD,EAAS,YAEhDA,CACT,OAASE,EAAU,CACjB,MAAAD,EAASC,EAAI,YAAcA,EAAI,QAAU,IACzCL,EAAQ,WAAW,iBAAiBK,CAAG,EACjCA,CACR,QAAE,CAEAL,EAAQ,IAAII,EAAQE,EAAST,EAAOE,CAAI,CAAC,EAMzC,IAAMQ,GAFQV,EAAM,SAAS,YAAY,SAAWA,EAAM,SAAS,KAE1C,WAAaA,EAAM,UAExCU,GAAa,OAAOA,GAAc,WACpCA,EAAUP,EAAQ,MAAM,CAAC,EAIzBA,EAAQ,MAAM,EAAE,MAAM,IAAM,CAAE,CAAC,CAEnC,CACF,CAAC,CACH,ECpDF,IAAMQ,EAAS,CAKb,KAAOC,GAA2BC,EAAO,KAAKD,CAAO,EAMrD,OAAQE,EAMR,MAAOC,CACT,EAEOC,EAAQL","names":["Transport","config","trace","batch","endpoint","err","import_node_async_hooks","storage","isInstrumented","enableFetchInstrumentation","originalFetch","input","init","controller","storage","url","method","spanName","span","headers","spanId","traceParent","newInit","response","err","SenzorClient","options","Transport","enableFetchInstrumentation","data","traceId","startTime","spans","name","type","spanStartAbs","startRel","meta","status","err","route","duration","payload","callback","client","normalizePath","path","getRoute","req","fallbackPath","wrapWorker","handler","request","env","ctx","path","session","client","storage","response","status","err","normalizePath","wrapH3","handler","event","req","path","session","client","storage","response","status","err","getRoute","waitUntil","Senzor","options","client","wrapWorker","wrapH3","index_default"]}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var g=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var O=(r,t)=>{for(var e in t)g(r,e,{get:t[e],enumerable:!0})},k=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of D(t))!P.call(r,o)&&o!==e&&g(r,o,{get:()=>t[o],enumerable:!(n=C(t,o))||n.enumerable});return r};var E=r=>k(g({},"__esModule",{value:!0}),r);var H={};O(H,{Senzor:()=>U,default:()=>F});module.exports=E(H);var m=class{constructor(t){this.config=t;this.queue=[]}add(t){this.queue.push(t)}async flush(){if(this.queue.length===0)return;let t=[...this.queue];this.queue=[];try{let e=this.config.endpoint||"https://api.senzor.dev/api/ingest/apm";await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","x-service-api-key":this.config.apiKey},body:JSON.stringify(t),keepalive:!0}),this.config.debug&&console.log(`[Senzor] Flushed ${t.length} traces`)}catch(e){this.config.debug&&console.error("[Senzor] Ingestion Error:",e)}}};var w=require("async_hooks"),f=new w.AsyncLocalStorage;var S=!1,b=()=>{if(S)return;let r=globalThis.fetch;globalThis.fetch=async(t,e)=>{let n=f.getStore();if(!n)return r(t,e);let o="",a="GET";typeof t=="string"?o=t:t instanceof URL?o=t.toString():t instanceof Request&&(o=t.url,a=t.method),e&&e.method&&(a=e.method);let s=`HTTP ${a}`,c=n.startSpan(s,"http"),l=new Headers(e?.headers||(t instanceof Request?t.headers:{})),i=crypto.randomUUID().replace(/-/g,"").substring(0,16),h=`00-${n.traceId}-${i}-01`;l.set("traceparent",h);let u={...e,headers:l};try{let p=await r(t,u);return c.end({url:o,method:a,status:p.status},p.status),p}catch(p){throw c.end({url:o,method:a,error:p.message},500),p}},S=!0};var y=class{constructor(){this.transport=null;this.options=null}init(t){if(!t.apiKey){console.warn("[Senzor] API Key missing. SDK disabled.");return}this.options=t,this.transport=new m(t);try{b()}catch(e){t.debug&&console.warn("[Senzor] Failed to instrument fetch:",e)}t.debug&&console.log("[Senzor] Initialized for Serverless")}createTrace(t){if(!this.transport)return{controller:{startSpan:()=>({end:()=>{}}),captureException:()=>{},traceId:"00000000000000000000000000000000"},end:()=>{},flush:async()=>{}};let e=crypto.randomUUID().replace(/-/g,""),n=performance.now(),o=[];return{controller:{traceId:e,startSpan:(i,h="custom")=>{let u=performance.now(),p=u-n;return{end:(z,A)=>{o.push({name:i,type:h,startTime:p,duration:performance.now()-u,status:A,meta:z})}}},captureException:i=>{o.push({name:"exception",type:"custom",startTime:performance.now()-n,duration:0,status:500,meta:{error:i.message||String(i),stack:i.stack}})}},end:(i,h="UNKNOWN")=>{let u=performance.now()-n,p={traceId:e,method:t.method||"GET",route:h,path:t.path||"/",status:i,duration:u,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:o};this.transport?.add(p)},flush:async()=>{await this.transport?.flush()}}}track(t){if(!this.transport)return;let e={traceId:crypto.randomUUID(),method:t.method||"GET",route:t.route,path:t.path||"/",status:t.status,duration:t.duration,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:t.spans||[]};this.transport.add(e),this.transport.flush().catch(()=>{})}startTrace(t,e){return e()}endTrace(t,e){}},d=new y;var T=r=>!r||r==="/"?"/":r.replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,":uuid").replace(/[0-9a-fA-F]{24}/g,":objectId").replace(/\/(\d+)(?=\/|$)/g,"/:id").split("?")[0],x=(r,t)=>r.route&&r.route.path?(r.baseUrl||"")+r.route.path:r.context&&r.context.matchedRoute?r.context.matchedRoute.path:r.routerPath?r.routerPath:T(t);var I=r=>async(t,e,n)=>{let a=new URL(t.url).pathname,s=d.createTrace({method:t.method,path:a,userAgent:t.headers.get("user-agent")||void 0,ip:t.headers.get("cf-connecting-ip")||t.headers.get("x-forwarded-for")||void 0});return f.run(s.controller,async()=>{let c,l=500;try{return c=await r(t,e,n,s.controller),l=c.status,c}catch(i){throw s.controller.captureException(i),i}finally{s.end(l,T(a)),n&&typeof n.waitUntil=="function"?n.waitUntil(s.flush()):await s.flush()}})};var R=r=>async t=>{let e=t.node.req,n=e.originalUrl||e.url||"/",o=d.createTrace({method:e.method||"GET",path:n,ip:e.headers["x-forwarded-for"]||e.socket?.remoteAddress||t.context?.cf?.connectingIp,userAgent:e.headers["user-agent"]});return f.run(o.controller,async()=>{let a,s=200;try{return a=await r(t),t.node.res.statusCode&&(s=t.node.res.statusCode),a&&a.statusCode&&(s=a.statusCode),a}catch(c){throw s=c.statusCode||c.status||500,o.controller.captureException(c),c}finally{o.end(s,x(t,n));let l=(t.context?.cloudflare?.context||t.context?.cf)?.waitUntil||t.waitUntil;l&&typeof l=="function"?l(o.flush()):o.flush().catch(()=>{})}})};var U={init:r=>d.init(r),worker:I,nitro:R},F=U;0&&(module.exports={Senzor});
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/core/transport.ts","../src/core/context.ts","../src/instrumentation/fetch.ts","../src/core/client.ts","../src/core/normalizer.ts","../src/wrappers/worker.ts","../src/wrappers/h3.ts"],"sourcesContent":["import { client } from './core/client';\r\nimport { wrapWorker } from './wrappers/worker';\r\nimport { wrapH3 } from './wrappers/h3';\r\nimport { SenzorOptions } from './core/types';\r\n\r\nconst Senzor = {\r\n /**\r\n * Initialize the SDK.\r\n * Call this in the global scope of your Worker or Plugin.\r\n */\r\n init: (options: SenzorOptions) => client.init(options),\r\n\r\n /**\r\n * Wrap your Cloudflare Worker 'fetch' handler.\r\n * Inject trace controller as the 4th argument.\r\n */\r\n worker: wrapWorker,\r\n\r\n /**\r\n * Wrap a Nitro/H3 event handler.\r\n * Use this for Nuxt or pure Nitro server routes.\r\n */\r\n nitro: wrapH3\r\n};\r\n\r\nexport default Senzor;\r\nexport { Senzor };\r\nexport type { TraceController } from './core/types';\r\n","import { SenzorOptions, TraceData } from './types';\r\n\r\nexport class Transport {\r\n private queue: TraceData[] = [];\r\n\r\n constructor(private config: SenzorOptions) { }\r\n\r\n public add(trace: TraceData) {\r\n this.queue.push(trace);\r\n }\r\n\r\n /**\r\n * Flushes the queue to the API. \r\n * Returns a promise that should be passed to ctx.waitUntil()\r\n */\r\n public async flush(): Promise<void> {\r\n if (this.queue.length === 0) return;\r\n\r\n const batch = [...this.queue];\r\n this.queue = []; // Clear immediately\r\n\r\n try {\r\n const endpoint = this.config.endpoint || 'https://api.senzor.dev/api/ingest/apm';\r\n\r\n await fetch(endpoint, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'x-service-api-key': this.config.apiKey,\r\n },\r\n body: JSON.stringify(batch),\r\n // keepalive is not strictly necessary in workers if awaited/waitUntil'd, but good practice\r\n keepalive: true,\r\n });\r\n\r\n if (this.config.debug) console.log(`[Senzor] Flushed ${batch.length} traces`);\r\n } catch (err) {\r\n if (this.config.debug) console.error('[Senzor] Ingestion Error:', err);\r\n }\r\n }\r\n}\r\n","import { AsyncLocalStorage } from 'node:async_hooks';\r\nimport { TraceController } from './types';\r\n\r\n// This relies on the 'nodejs_compat' compatibility flag in Cloudflare Workers.\r\nexport const storage = new AsyncLocalStorage<TraceController>();\r\n","import { storage } from '../core/context';\r\n\r\nlet isInstrumented = false;\r\n\r\nexport const enableFetchInstrumentation = () => {\r\n if (isInstrumented) return;\r\n\r\n const originalFetch = globalThis.fetch;\r\n\r\n // Monkey-patch global fetch\r\n globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {\r\n // 1. Check for active trace context\r\n const controller = storage.getStore();\r\n\r\n // If no trace is active, just pass through\r\n if (!controller) {\r\n return originalFetch(input, init);\r\n }\r\n\r\n // 2. Resolve URL and Method\r\n let url = '';\r\n let method = 'GET';\r\n\r\n if (typeof input === 'string') {\r\n url = input;\r\n } else if (input instanceof URL) {\r\n url = input.toString();\r\n } else if (input instanceof Request) {\r\n url = input.url;\r\n method = input.method;\r\n }\r\n\r\n if (init && init.method) {\r\n method = init.method;\r\n }\r\n\r\n // 3. Start Span\r\n const spanName = `HTTP ${method}`;\r\n const span = controller.startSpan(spanName, 'http');\r\n\r\n // 4. Inject Trace Headers (W3C Trace Context)\r\n // We clone headers to avoid side effects on the input object\r\n const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : {}));\r\n\r\n // Generate a span ID for the outgoing request\r\n const spanId = crypto.randomUUID().replace(/-/g, '').substring(0, 16);\r\n const traceParent = `00-${controller.traceId}-${spanId}-01`;\r\n headers.set('traceparent', traceParent);\r\n\r\n const newInit: RequestInit = {\r\n ...init,\r\n headers\r\n };\r\n\r\n try {\r\n const response = await originalFetch(input, newInit);\r\n span.end({\r\n url,\r\n method,\r\n status: response.status\r\n }, response.status);\r\n return response;\r\n } catch (err: any) {\r\n span.end({\r\n url,\r\n method,\r\n error: err.message\r\n }, 500);\r\n throw err;\r\n }\r\n };\r\n\r\n isInstrumented = true;\r\n};\r\n","import { Transport } from './transport';\r\nimport { SenzorOptions, TraceData, Span, TraceController } from './types';\r\nimport { enableFetchInstrumentation } from '../instrumentation/fetch';\r\n\r\nexport class SenzorClient {\r\n private transport: Transport | null = null;\r\n private options: SenzorOptions | null = null;\r\n\r\n public init(options: SenzorOptions) {\r\n if (!options.apiKey) {\r\n console.warn('[Senzor] API Key missing. SDK disabled.');\r\n return;\r\n }\r\n this.options = options;\r\n this.transport = new Transport(options);\r\n\r\n // Auto-instrument global fetch\r\n try {\r\n enableFetchInstrumentation();\r\n } catch (e) {\r\n if (options.debug) console.warn('[Senzor] Failed to instrument fetch:', e);\r\n }\r\n\r\n if (options.debug) console.log('[Senzor] Initialized for Serverless');\r\n }\r\n\r\n /**\r\n * Creates a detached trace session.\r\n */\r\n public createTrace(data: Partial<TraceData>): { controller: TraceController, end: (status: number, route?: string) => void, flush: () => Promise<void> } {\r\n if (!this.transport) {\r\n // Return dummy if not initialized\r\n return {\r\n controller: {\r\n startSpan: () => ({ end: () => { } }),\r\n captureException: () => { },\r\n traceId: '00000000000000000000000000000000'\r\n },\r\n end: () => { },\r\n flush: async () => { }\r\n };\r\n }\r\n\r\n const traceId = crypto.randomUUID().replace(/-/g, ''); // 32 hex chars usually\r\n const startTime = performance.now();\r\n const spans: Span[] = [];\r\n\r\n const startSpan = (name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') => {\r\n const spanStartAbs = performance.now();\r\n const startRel = spanStartAbs - startTime;\r\n return {\r\n end: (meta?: any, status?: number) => {\r\n spans.push({\r\n name,\r\n type,\r\n startTime: startRel, // Relative to trace start\r\n duration: performance.now() - spanStartAbs,\r\n status,\r\n meta\r\n });\r\n }\r\n };\r\n };\r\n\r\n const controller: TraceController = {\r\n traceId,\r\n startSpan,\r\n captureException: (err: any) => {\r\n spans.push({\r\n name: 'exception',\r\n type: 'custom',\r\n startTime: performance.now() - startTime,\r\n duration: 0,\r\n status: 500,\r\n meta: { error: err.message || String(err), stack: err.stack }\r\n });\r\n }\r\n };\r\n\r\n const end = (status: number, route: string = 'UNKNOWN') => {\r\n const duration = performance.now() - startTime;\r\n const payload: TraceData = {\r\n traceId,\r\n method: data.method || 'GET',\r\n route,\r\n path: data.path || '/',\r\n status,\r\n duration,\r\n ip: data.ip,\r\n userAgent: data.userAgent,\r\n timestamp: new Date().toISOString(),\r\n spans\r\n };\r\n this.transport?.add(payload);\r\n };\r\n\r\n const flush = async () => {\r\n await this.transport?.flush();\r\n };\r\n\r\n return { controller, end, flush };\r\n }\r\n\r\n /**\r\n * Track a single request trace immediately.\r\n */\r\n public track(data: Partial<TraceData> & { status: number, duration: number, route: string }) {\r\n if (!this.transport) return;\r\n\r\n const payload: TraceData = {\r\n traceId: crypto.randomUUID(),\r\n method: data.method || 'GET',\r\n route: data.route,\r\n path: data.path || '/',\r\n status: data.status,\r\n duration: data.duration,\r\n ip: data.ip,\r\n userAgent: data.userAgent,\r\n timestamp: new Date().toISOString(),\r\n spans: data.spans || []\r\n };\r\n\r\n this.transport.add(payload);\r\n this.transport.flush().catch(() => { });\r\n }\r\n\r\n // Stubs for legacy Node support\r\n public startTrace<T>(data: Partial<TraceData>, callback: () => T): T { return callback(); }\r\n public endTrace(status: number, data?: { route?: string }) { }\r\n}\r\n\r\nexport const client = new SenzorClient();\r\n","/**\r\n * Heuristic URL Normalizer\r\n * Converts raw paths with IDs into generic patterns to prevent high cardinality.\r\n * Example: /users/123/orders/abc-def -> /users/:id/orders/:uuid\r\n */\r\nexport const normalizePath = (path: string): string => {\r\n if (!path || path === '/') return '/';\r\n\r\n return path\r\n // Replace UUIDs (long alphanumeric strings)\r\n .replace(\r\n /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,\r\n ':uuid'\r\n )\r\n // Replace MongoDB ObjectIds (24 hex chars)\r\n .replace(/[0-9a-fA-F]{24}/g, ':objectId')\r\n // Replace pure numeric IDs (e.g., /123)\r\n .replace(/\\/(\\d+)(?=\\/|$)/g, '/:id')\r\n // Remove query strings\r\n .split('?')[0];\r\n};\r\n\r\n/**\r\n * Tries to extract route from Framework internals, falls back to heuristic\r\n */\r\nexport const getRoute = (req: any, fallbackPath: string): string => {\r\n // Express / Connect\r\n if (req.route && req.route.path) {\r\n return (req.baseUrl || '') + req.route.path;\r\n }\r\n\r\n // H3 / Nitro (Nuxt)\r\n if (req.context && req.context.matchedRoute) {\r\n return req.context.matchedRoute.path;\r\n }\r\n\r\n // Fastify\r\n if (req.routerPath) {\r\n return req.routerPath;\r\n }\r\n\r\n // Fallback: Heuristic Normalization\r\n return normalizePath(fallbackPath);\r\n};","import { client } from '../core/client';\r\nimport { normalizePath } from '../core/normalizer';\r\nimport { TraceController } from '../core/types';\r\nimport { storage } from '../core/context';\r\n\r\ntype WorkerHandler = (request: Request, env: any, ctx: any, trace: TraceController) => Promise<Response>;\r\n\r\nexport const wrapWorker = (handler: WorkerHandler) => {\r\n return async (request: Request, env: any, ctx: any) => {\r\n // 2. Extract Request Info\r\n const url = new URL(request.url);\r\n const path = url.pathname;\r\n\r\n // 3. Start Trace\r\n const session = client.createTrace({\r\n method: request.method,\r\n path: path,\r\n userAgent: request.headers.get('user-agent') || undefined,\r\n ip: request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || undefined\r\n });\r\n\r\n // 4. Run Handler within Context (AsyncLocalStorage)\r\n // This enables the global fetch instrumentation to find the current trace controller\r\n return storage.run(session.controller, async () => {\r\n let response: Response;\r\n let status = 500;\r\n\r\n try {\r\n // Inject trace controller as 4th arg for manual usage\r\n response = await handler(request, env, ctx, session.controller);\r\n status = response.status;\r\n return response;\r\n } catch (err: any) {\r\n session.controller.captureException(err);\r\n throw err;\r\n } finally {\r\n // 5. End Trace\r\n session.end(status, normalizePath(path));\r\n\r\n // 6. Flush (Async WaitUntil)\r\n if (ctx && typeof ctx.waitUntil === 'function') {\r\n ctx.waitUntil(session.flush());\r\n } else {\r\n await session.flush();\r\n }\r\n }\r\n });\r\n };\r\n};\r\n","import { client } from '../core/client';\r\nimport { getRoute } from '../core/normalizer';\r\nimport { storage } from '../core/context';\r\n\r\n// Minimal types for H3 to avoid peer-deps\r\ntype EventHandler = (event: any) => any;\r\n\r\nexport const wrapH3 = (handler: EventHandler) => {\r\n return async (event: any) => {\r\n const req = event.node.req;\r\n const path = req.originalUrl || req.url || '/';\r\n\r\n // 1. Start Trace Session\r\n const session = client.createTrace({\r\n method: req.method || 'GET',\r\n path: path,\r\n ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress || event.context?.cf?.connectingIp,\r\n userAgent: req.headers['user-agent'],\r\n });\r\n\r\n // 2. Run Handler within AsyncLocalStorage Context\r\n // This ensures global fetch() auto-instrumentation works inside Nitro handlers\r\n return storage.run(session.controller, async () => {\r\n let response: any;\r\n let status = 200;\r\n\r\n try {\r\n response = await handler(event);\r\n\r\n // H3/Nitro response status handling\r\n if (event.node.res.statusCode) status = event.node.res.statusCode;\r\n if (response && response.statusCode) status = response.statusCode;\r\n\r\n return response;\r\n } catch (err: any) {\r\n status = err.statusCode || err.status || 500;\r\n session.controller.captureException(err);\r\n throw err;\r\n } finally {\r\n // 3. End Trace\r\n session.end(status, getRoute(event, path));\r\n\r\n // 4. Flush Data (Non-blocking for Cloudflare)\r\n // Nitro exposes Cloudflare context in event.context.cloudflare\r\n const cfCtx = event.context?.cloudflare?.context || event.context?.cf;\r\n // Or sometimes directly on event in newer H3 versions if adapter binds it\r\n const waitUntil = cfCtx?.waitUntil || event.waitUntil;\r\n\r\n if (waitUntil && typeof waitUntil === 'function') {\r\n waitUntil(session.flush());\r\n } else {\r\n // If not in a worker environment or waitUntil missing, flush async but don't block response significantly\r\n // (Note: without waitUntil, the runtime might kill the process before flush completes)\r\n session.flush().catch(() => { });\r\n }\r\n }\r\n });\r\n };\r\n};\r\n"],"mappings":"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","names":["index_exports","__export","Senzor","index_default","__toCommonJS","Transport","config","trace","batch","endpoint","err","import_node_async_hooks","storage","isInstrumented","enableFetchInstrumentation","originalFetch","input","init","controller","storage","url","method","spanName","span","headers","spanId","traceParent","newInit","response","err","SenzorClient","options","Transport","enableFetchInstrumentation","data","traceId","startTime","spans","name","type","spanStartAbs","startRel","meta","status","err","route","duration","payload","callback","client","normalizePath","path","getRoute","req","fallbackPath","wrapWorker","handler","request","env","ctx","path","session","client","storage","response","status","err","normalizePath","wrapH3","handler","event","req","path","session","client","storage","response","status","err","getRoute","waitUntil","Senzor","options","client","wrapWorker","wrapH3","index_default"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var m=class{constructor(t){this.config=t;this.queue=[]}add(t){this.queue.push(t)}async flush(){if(this.queue.length===0)return;let t=[...this.queue];this.queue=[];try{let e=this.config.endpoint||"https://api.senzor.dev/api/ingest/apm";await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","x-service-api-key":this.config.apiKey},body:JSON.stringify(t),keepalive:!0}),this.config.debug&&console.log(`[Senzor] Flushed ${t.length} traces`)}catch(e){this.config.debug&&console.error("[Senzor] Ingestion Error:",e)}}};import{AsyncLocalStorage as U}from"async_hooks";var f=new U;var T=!1,w=()=>{if(T)return;let r=globalThis.fetch;globalThis.fetch=async(t,e)=>{let n=f.getStore();if(!n)return r(t,e);let o="",a="GET";typeof t=="string"?o=t:t instanceof URL?o=t.toString():t instanceof Request&&(o=t.url,a=t.method),e&&e.method&&(a=e.method);let s=`HTTP ${a}`,c=n.startSpan(s,"http"),l=new Headers(e?.headers||(t instanceof Request?t.headers:{})),i=crypto.randomUUID().replace(/-/g,"").substring(0,16),h=`00-${n.traceId}-${i}-01`;l.set("traceparent",h);let u={...e,headers:l};try{let p=await r(t,u);return c.end({url:o,method:a,status:p.status},p.status),p}catch(p){throw c.end({url:o,method:a,error:p.message},500),p}},T=!0};var g=class{constructor(){this.transport=null;this.options=null}init(t){if(!t.apiKey){console.warn("[Senzor] API Key missing. SDK disabled.");return}this.options=t,this.transport=new m(t);try{w()}catch(e){t.debug&&console.warn("[Senzor] Failed to instrument fetch:",e)}t.debug&&console.log("[Senzor] Initialized for Serverless")}createTrace(t){if(!this.transport)return{controller:{startSpan:()=>({end:()=>{}}),captureException:()=>{},traceId:"00000000000000000000000000000000"},end:()=>{},flush:async()=>{}};let e=crypto.randomUUID().replace(/-/g,""),n=performance.now(),o=[];return{controller:{traceId:e,startSpan:(i,h="custom")=>{let u=performance.now(),p=u-n;return{end:(I,R)=>{o.push({name:i,type:h,startTime:p,duration:performance.now()-u,status:R,meta:I})}}},captureException:i=>{o.push({name:"exception",type:"custom",startTime:performance.now()-n,duration:0,status:500,meta:{error:i.message||String(i),stack:i.stack}})}},end:(i,h="UNKNOWN")=>{let u=performance.now()-n,p={traceId:e,method:t.method||"GET",route:h,path:t.path||"/",status:i,duration:u,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:o};this.transport?.add(p)},flush:async()=>{await this.transport?.flush()}}}track(t){if(!this.transport)return;let e={traceId:crypto.randomUUID(),method:t.method||"GET",route:t.route,path:t.path||"/",status:t.status,duration:t.duration,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:t.spans||[]};this.transport.add(e),this.transport.flush().catch(()=>{})}startTrace(t,e){return e()}endTrace(t,e){}},d=new g;var y=r=>!r||r==="/"?"/":r.replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,":uuid").replace(/[0-9a-fA-F]{24}/g,":objectId").replace(/\/(\d+)(?=\/|$)/g,"/:id").split("?")[0],S=(r,t)=>r.route&&r.route.path?(r.baseUrl||"")+r.route.path:r.context&&r.context.matchedRoute?r.context.matchedRoute.path:r.routerPath?r.routerPath:y(t);var b=r=>async(t,e,n)=>{let a=new URL(t.url).pathname,s=d.createTrace({method:t.method,path:a,userAgent:t.headers.get("user-agent")||void 0,ip:t.headers.get("cf-connecting-ip")||t.headers.get("x-forwarded-for")||void 0});return f.run(s.controller,async()=>{let c,l=500;try{return c=await r(t,e,n,s.controller),l=c.status,c}catch(i){throw s.controller.captureException(i),i}finally{s.end(l,y(a)),n&&typeof n.waitUntil=="function"?n.waitUntil(s.flush()):await s.flush()}})};var x=r=>async t=>{let e=t.node.req,n=e.originalUrl||e.url||"/",o=d.createTrace({method:e.method||"GET",path:n,ip:e.headers["x-forwarded-for"]||e.socket?.remoteAddress||t.context?.cf?.connectingIp,userAgent:e.headers["user-agent"]});return f.run(o.controller,async()=>{let a,s=200;try{return a=await r(t),t.node.res.statusCode&&(s=t.node.res.statusCode),a&&a.statusCode&&(s=a.statusCode),a}catch(c){throw s=c.statusCode||c.status||500,o.controller.captureException(c),c}finally{o.end(s,S(t,n));let l=(t.context?.cloudflare?.context||t.context?.cf)?.waitUntil||t.waitUntil;l&&typeof l=="function"?l(o.flush()):o.flush().catch(()=>{})}})};var z={init:r=>d.init(r),worker:b,nitro:x},M=z;export{z as Senzor,M as default};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/transport.ts","../src/core/context.ts","../src/instrumentation/fetch.ts","../src/core/client.ts","../src/core/normalizer.ts","../src/wrappers/worker.ts","../src/wrappers/h3.ts","../src/index.ts"],"sourcesContent":["import { SenzorOptions, TraceData } from './types';\r\n\r\nexport class Transport {\r\n private queue: TraceData[] = [];\r\n\r\n constructor(private config: SenzorOptions) { }\r\n\r\n public add(trace: TraceData) {\r\n this.queue.push(trace);\r\n }\r\n\r\n /**\r\n * Flushes the queue to the API. \r\n * Returns a promise that should be passed to ctx.waitUntil()\r\n */\r\n public async flush(): Promise<void> {\r\n if (this.queue.length === 0) return;\r\n\r\n const batch = [...this.queue];\r\n this.queue = []; // Clear immediately\r\n\r\n try {\r\n const endpoint = this.config.endpoint || 'https://api.senzor.dev/api/ingest/apm';\r\n\r\n await fetch(endpoint, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'x-service-api-key': this.config.apiKey,\r\n },\r\n body: JSON.stringify(batch),\r\n // keepalive is not strictly necessary in workers if awaited/waitUntil'd, but good practice\r\n keepalive: true,\r\n });\r\n\r\n if (this.config.debug) console.log(`[Senzor] Flushed ${batch.length} traces`);\r\n } catch (err) {\r\n if (this.config.debug) console.error('[Senzor] Ingestion Error:', err);\r\n }\r\n }\r\n}\r\n","import { AsyncLocalStorage } from 'node:async_hooks';\r\nimport { TraceController } from './types';\r\n\r\n// This relies on the 'nodejs_compat' compatibility flag in Cloudflare Workers.\r\nexport const storage = new AsyncLocalStorage<TraceController>();\r\n","import { storage } from '../core/context';\r\n\r\nlet isInstrumented = false;\r\n\r\nexport const enableFetchInstrumentation = () => {\r\n if (isInstrumented) return;\r\n\r\n const originalFetch = globalThis.fetch;\r\n\r\n // Monkey-patch global fetch\r\n globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {\r\n // 1. Check for active trace context\r\n const controller = storage.getStore();\r\n\r\n // If no trace is active, just pass through\r\n if (!controller) {\r\n return originalFetch(input, init);\r\n }\r\n\r\n // 2. Resolve URL and Method\r\n let url = '';\r\n let method = 'GET';\r\n\r\n if (typeof input === 'string') {\r\n url = input;\r\n } else if (input instanceof URL) {\r\n url = input.toString();\r\n } else if (input instanceof Request) {\r\n url = input.url;\r\n method = input.method;\r\n }\r\n\r\n if (init && init.method) {\r\n method = init.method;\r\n }\r\n\r\n // 3. Start Span\r\n const spanName = `HTTP ${method}`;\r\n const span = controller.startSpan(spanName, 'http');\r\n\r\n // 4. Inject Trace Headers (W3C Trace Context)\r\n // We clone headers to avoid side effects on the input object\r\n const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : {}));\r\n\r\n // Generate a span ID for the outgoing request\r\n const spanId = crypto.randomUUID().replace(/-/g, '').substring(0, 16);\r\n const traceParent = `00-${controller.traceId}-${spanId}-01`;\r\n headers.set('traceparent', traceParent);\r\n\r\n const newInit: RequestInit = {\r\n ...init,\r\n headers\r\n };\r\n\r\n try {\r\n const response = await originalFetch(input, newInit);\r\n span.end({\r\n url,\r\n method,\r\n status: response.status\r\n }, response.status);\r\n return response;\r\n } catch (err: any) {\r\n span.end({\r\n url,\r\n method,\r\n error: err.message\r\n }, 500);\r\n throw err;\r\n }\r\n };\r\n\r\n isInstrumented = true;\r\n};\r\n","import { Transport } from './transport';\r\nimport { SenzorOptions, TraceData, Span, TraceController } from './types';\r\nimport { enableFetchInstrumentation } from '../instrumentation/fetch';\r\n\r\nexport class SenzorClient {\r\n private transport: Transport | null = null;\r\n private options: SenzorOptions | null = null;\r\n\r\n public init(options: SenzorOptions) {\r\n if (!options.apiKey) {\r\n console.warn('[Senzor] API Key missing. SDK disabled.');\r\n return;\r\n }\r\n this.options = options;\r\n this.transport = new Transport(options);\r\n\r\n // Auto-instrument global fetch\r\n try {\r\n enableFetchInstrumentation();\r\n } catch (e) {\r\n if (options.debug) console.warn('[Senzor] Failed to instrument fetch:', e);\r\n }\r\n\r\n if (options.debug) console.log('[Senzor] Initialized for Serverless');\r\n }\r\n\r\n /**\r\n * Creates a detached trace session.\r\n */\r\n public createTrace(data: Partial<TraceData>): { controller: TraceController, end: (status: number, route?: string) => void, flush: () => Promise<void> } {\r\n if (!this.transport) {\r\n // Return dummy if not initialized\r\n return {\r\n controller: {\r\n startSpan: () => ({ end: () => { } }),\r\n captureException: () => { },\r\n traceId: '00000000000000000000000000000000'\r\n },\r\n end: () => { },\r\n flush: async () => { }\r\n };\r\n }\r\n\r\n const traceId = crypto.randomUUID().replace(/-/g, ''); // 32 hex chars usually\r\n const startTime = performance.now();\r\n const spans: Span[] = [];\r\n\r\n const startSpan = (name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') => {\r\n const spanStartAbs = performance.now();\r\n const startRel = spanStartAbs - startTime;\r\n return {\r\n end: (meta?: any, status?: number) => {\r\n spans.push({\r\n name,\r\n type,\r\n startTime: startRel, // Relative to trace start\r\n duration: performance.now() - spanStartAbs,\r\n status,\r\n meta\r\n });\r\n }\r\n };\r\n };\r\n\r\n const controller: TraceController = {\r\n traceId,\r\n startSpan,\r\n captureException: (err: any) => {\r\n spans.push({\r\n name: 'exception',\r\n type: 'custom',\r\n startTime: performance.now() - startTime,\r\n duration: 0,\r\n status: 500,\r\n meta: { error: err.message || String(err), stack: err.stack }\r\n });\r\n }\r\n };\r\n\r\n const end = (status: number, route: string = 'UNKNOWN') => {\r\n const duration = performance.now() - startTime;\r\n const payload: TraceData = {\r\n traceId,\r\n method: data.method || 'GET',\r\n route,\r\n path: data.path || '/',\r\n status,\r\n duration,\r\n ip: data.ip,\r\n userAgent: data.userAgent,\r\n timestamp: new Date().toISOString(),\r\n spans\r\n };\r\n this.transport?.add(payload);\r\n };\r\n\r\n const flush = async () => {\r\n await this.transport?.flush();\r\n };\r\n\r\n return { controller, end, flush };\r\n }\r\n\r\n /**\r\n * Track a single request trace immediately.\r\n */\r\n public track(data: Partial<TraceData> & { status: number, duration: number, route: string }) {\r\n if (!this.transport) return;\r\n\r\n const payload: TraceData = {\r\n traceId: crypto.randomUUID(),\r\n method: data.method || 'GET',\r\n route: data.route,\r\n path: data.path || '/',\r\n status: data.status,\r\n duration: data.duration,\r\n ip: data.ip,\r\n userAgent: data.userAgent,\r\n timestamp: new Date().toISOString(),\r\n spans: data.spans || []\r\n };\r\n\r\n this.transport.add(payload);\r\n this.transport.flush().catch(() => { });\r\n }\r\n\r\n // Stubs for legacy Node support\r\n public startTrace<T>(data: Partial<TraceData>, callback: () => T): T { return callback(); }\r\n public endTrace(status: number, data?: { route?: string }) { }\r\n}\r\n\r\nexport const client = new SenzorClient();\r\n","/**\r\n * Heuristic URL Normalizer\r\n * Converts raw paths with IDs into generic patterns to prevent high cardinality.\r\n * Example: /users/123/orders/abc-def -> /users/:id/orders/:uuid\r\n */\r\nexport const normalizePath = (path: string): string => {\r\n if (!path || path === '/') return '/';\r\n\r\n return path\r\n // Replace UUIDs (long alphanumeric strings)\r\n .replace(\r\n /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,\r\n ':uuid'\r\n )\r\n // Replace MongoDB ObjectIds (24 hex chars)\r\n .replace(/[0-9a-fA-F]{24}/g, ':objectId')\r\n // Replace pure numeric IDs (e.g., /123)\r\n .replace(/\\/(\\d+)(?=\\/|$)/g, '/:id')\r\n // Remove query strings\r\n .split('?')[0];\r\n};\r\n\r\n/**\r\n * Tries to extract route from Framework internals, falls back to heuristic\r\n */\r\nexport const getRoute = (req: any, fallbackPath: string): string => {\r\n // Express / Connect\r\n if (req.route && req.route.path) {\r\n return (req.baseUrl || '') + req.route.path;\r\n }\r\n\r\n // H3 / Nitro (Nuxt)\r\n if (req.context && req.context.matchedRoute) {\r\n return req.context.matchedRoute.path;\r\n }\r\n\r\n // Fastify\r\n if (req.routerPath) {\r\n return req.routerPath;\r\n }\r\n\r\n // Fallback: Heuristic Normalization\r\n return normalizePath(fallbackPath);\r\n};","import { client } from '../core/client';\r\nimport { normalizePath } from '../core/normalizer';\r\nimport { TraceController } from '../core/types';\r\nimport { storage } from '../core/context';\r\n\r\ntype WorkerHandler = (request: Request, env: any, ctx: any, trace: TraceController) => Promise<Response>;\r\n\r\nexport const wrapWorker = (handler: WorkerHandler) => {\r\n return async (request: Request, env: any, ctx: any) => {\r\n // 2. Extract Request Info\r\n const url = new URL(request.url);\r\n const path = url.pathname;\r\n\r\n // 3. Start Trace\r\n const session = client.createTrace({\r\n method: request.method,\r\n path: path,\r\n userAgent: request.headers.get('user-agent') || undefined,\r\n ip: request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || undefined\r\n });\r\n\r\n // 4. Run Handler within Context (AsyncLocalStorage)\r\n // This enables the global fetch instrumentation to find the current trace controller\r\n return storage.run(session.controller, async () => {\r\n let response: Response;\r\n let status = 500;\r\n\r\n try {\r\n // Inject trace controller as 4th arg for manual usage\r\n response = await handler(request, env, ctx, session.controller);\r\n status = response.status;\r\n return response;\r\n } catch (err: any) {\r\n session.controller.captureException(err);\r\n throw err;\r\n } finally {\r\n // 5. End Trace\r\n session.end(status, normalizePath(path));\r\n\r\n // 6. Flush (Async WaitUntil)\r\n if (ctx && typeof ctx.waitUntil === 'function') {\r\n ctx.waitUntil(session.flush());\r\n } else {\r\n await session.flush();\r\n }\r\n }\r\n });\r\n };\r\n};\r\n","import { client } from '../core/client';\r\nimport { getRoute } from '../core/normalizer';\r\nimport { storage } from '../core/context';\r\n\r\n// Minimal types for H3 to avoid peer-deps\r\ntype EventHandler = (event: any) => any;\r\n\r\nexport const wrapH3 = (handler: EventHandler) => {\r\n return async (event: any) => {\r\n const req = event.node.req;\r\n const path = req.originalUrl || req.url || '/';\r\n\r\n // 1. Start Trace Session\r\n const session = client.createTrace({\r\n method: req.method || 'GET',\r\n path: path,\r\n ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress || event.context?.cf?.connectingIp,\r\n userAgent: req.headers['user-agent'],\r\n });\r\n\r\n // 2. Run Handler within AsyncLocalStorage Context\r\n // This ensures global fetch() auto-instrumentation works inside Nitro handlers\r\n return storage.run(session.controller, async () => {\r\n let response: any;\r\n let status = 200;\r\n\r\n try {\r\n response = await handler(event);\r\n\r\n // H3/Nitro response status handling\r\n if (event.node.res.statusCode) status = event.node.res.statusCode;\r\n if (response && response.statusCode) status = response.statusCode;\r\n\r\n return response;\r\n } catch (err: any) {\r\n status = err.statusCode || err.status || 500;\r\n session.controller.captureException(err);\r\n throw err;\r\n } finally {\r\n // 3. End Trace\r\n session.end(status, getRoute(event, path));\r\n\r\n // 4. Flush Data (Non-blocking for Cloudflare)\r\n // Nitro exposes Cloudflare context in event.context.cloudflare\r\n const cfCtx = event.context?.cloudflare?.context || event.context?.cf;\r\n // Or sometimes directly on event in newer H3 versions if adapter binds it\r\n const waitUntil = cfCtx?.waitUntil || event.waitUntil;\r\n\r\n if (waitUntil && typeof waitUntil === 'function') {\r\n waitUntil(session.flush());\r\n } else {\r\n // If not in a worker environment or waitUntil missing, flush async but don't block response significantly\r\n // (Note: without waitUntil, the runtime might kill the process before flush completes)\r\n session.flush().catch(() => { });\r\n }\r\n }\r\n });\r\n };\r\n};\r\n","import { client } from './core/client';\r\nimport { wrapWorker } from './wrappers/worker';\r\nimport { wrapH3 } from './wrappers/h3';\r\nimport { SenzorOptions } from './core/types';\r\n\r\nconst Senzor = {\r\n /**\r\n * Initialize the SDK.\r\n * Call this in the global scope of your Worker or Plugin.\r\n */\r\n init: (options: SenzorOptions) => client.init(options),\r\n\r\n /**\r\n * Wrap your Cloudflare Worker 'fetch' handler.\r\n * Inject trace controller as the 4th argument.\r\n */\r\n worker: wrapWorker,\r\n\r\n /**\r\n * Wrap a Nitro/H3 event handler.\r\n * Use this for Nuxt or pure Nitro server routes.\r\n */\r\n nitro: wrapH3\r\n};\r\n\r\nexport default Senzor;\r\nexport { Senzor };\r\nexport type { TraceController } from './core/types';\r\n"],"mappings":"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","names":["Transport","config","trace","batch","endpoint","err","AsyncLocalStorage","storage","isInstrumented","enableFetchInstrumentation","originalFetch","input","init","controller","storage","url","method","spanName","span","headers","spanId","traceParent","newInit","response","err","SenzorClient","options","Transport","enableFetchInstrumentation","data","traceId","startTime","spans","name","type","spanStartAbs","startRel","meta","status","err","route","duration","payload","callback","client","normalizePath","path","getRoute","req","fallbackPath","wrapWorker","handler","request","env","ctx","path","session","client","storage","response","status","err","normalizePath","wrapH3","handler","event","req","path","session","client","storage","response","status","err","getRoute","waitUntil","Senzor","options","client","wrapWorker","wrapH3","index_default"]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@senzops/apm-worker",
3
+ "version": "1.0.0",
4
+ "description": "Cloudflare Worker / Serverless APM SDK for Senzor",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsup src/index.ts --format cjs,esm,iife --dts --clean --minify",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "dependencies": {},
12
+ "devDependencies": {
13
+ "@cloudflare/workers-types": "^4.20240101.0",
14
+ "@types/node": "^20.0.0",
15
+ "tsup": "^8.0.0",
16
+ "typescript": "^5.0.0"
17
+ },
18
+ "engines": {
19
+ "node": ">=16.0.0"
20
+ },
21
+ "keywords": [
22
+ "apm",
23
+ "monitoring",
24
+ "senzor",
25
+ "cloudflare",
26
+ "worker",
27
+ "serverless",
28
+ "observability"
29
+ ],
30
+ "author": "Senzops",
31
+ "license": "MIT"
32
+ }
@@ -0,0 +1,132 @@
1
+ import { Transport } from './transport';
2
+ import { SenzorOptions, TraceData, Span, TraceController } from './types';
3
+ import { enableFetchInstrumentation } from '../instrumentation/fetch';
4
+
5
+ export class SenzorClient {
6
+ private transport: Transport | null = null;
7
+ private options: SenzorOptions | null = null;
8
+
9
+ public init(options: SenzorOptions) {
10
+ if (!options.apiKey) {
11
+ console.warn('[Senzor] API Key missing. SDK disabled.');
12
+ return;
13
+ }
14
+ this.options = options;
15
+ this.transport = new Transport(options);
16
+
17
+ // Auto-instrument global fetch
18
+ try {
19
+ enableFetchInstrumentation();
20
+ } catch (e) {
21
+ if (options.debug) console.warn('[Senzor] Failed to instrument fetch:', e);
22
+ }
23
+
24
+ if (options.debug) console.log('[Senzor] Initialized for Serverless');
25
+ }
26
+
27
+ /**
28
+ * Creates a detached trace session.
29
+ */
30
+ public createTrace(data: Partial<TraceData>): { controller: TraceController, end: (status: number, route?: string) => void, flush: () => Promise<void> } {
31
+ if (!this.transport) {
32
+ // Return dummy if not initialized
33
+ return {
34
+ controller: {
35
+ startSpan: () => ({ end: () => { } }),
36
+ captureException: () => { },
37
+ traceId: '00000000000000000000000000000000'
38
+ },
39
+ end: () => { },
40
+ flush: async () => { }
41
+ };
42
+ }
43
+
44
+ const traceId = crypto.randomUUID().replace(/-/g, ''); // 32 hex chars usually
45
+ const startTime = performance.now();
46
+ const spans: Span[] = [];
47
+
48
+ const startSpan = (name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') => {
49
+ const spanStartAbs = performance.now();
50
+ const startRel = spanStartAbs - startTime;
51
+ return {
52
+ end: (meta?: any, status?: number) => {
53
+ spans.push({
54
+ name,
55
+ type,
56
+ startTime: startRel, // Relative to trace start
57
+ duration: performance.now() - spanStartAbs,
58
+ status,
59
+ meta
60
+ });
61
+ }
62
+ };
63
+ };
64
+
65
+ const controller: TraceController = {
66
+ traceId,
67
+ startSpan,
68
+ captureException: (err: any) => {
69
+ spans.push({
70
+ name: 'exception',
71
+ type: 'custom',
72
+ startTime: performance.now() - startTime,
73
+ duration: 0,
74
+ status: 500,
75
+ meta: { error: err.message || String(err), stack: err.stack }
76
+ });
77
+ }
78
+ };
79
+
80
+ const end = (status: number, route: string = 'UNKNOWN') => {
81
+ const duration = performance.now() - startTime;
82
+ const payload: TraceData = {
83
+ traceId,
84
+ method: data.method || 'GET',
85
+ route,
86
+ path: data.path || '/',
87
+ status,
88
+ duration,
89
+ ip: data.ip,
90
+ userAgent: data.userAgent,
91
+ timestamp: new Date().toISOString(),
92
+ spans
93
+ };
94
+ this.transport?.add(payload);
95
+ };
96
+
97
+ const flush = async () => {
98
+ await this.transport?.flush();
99
+ };
100
+
101
+ return { controller, end, flush };
102
+ }
103
+
104
+ /**
105
+ * Track a single request trace immediately.
106
+ */
107
+ public track(data: Partial<TraceData> & { status: number, duration: number, route: string }) {
108
+ if (!this.transport) return;
109
+
110
+ const payload: TraceData = {
111
+ traceId: crypto.randomUUID(),
112
+ method: data.method || 'GET',
113
+ route: data.route,
114
+ path: data.path || '/',
115
+ status: data.status,
116
+ duration: data.duration,
117
+ ip: data.ip,
118
+ userAgent: data.userAgent,
119
+ timestamp: new Date().toISOString(),
120
+ spans: data.spans || []
121
+ };
122
+
123
+ this.transport.add(payload);
124
+ this.transport.flush().catch(() => { });
125
+ }
126
+
127
+ // Stubs for legacy Node support
128
+ public startTrace<T>(data: Partial<TraceData>, callback: () => T): T { return callback(); }
129
+ public endTrace(status: number, data?: { route?: string }) { }
130
+ }
131
+
132
+ export const client = new SenzorClient();
@@ -0,0 +1,5 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { TraceController } from './types';
3
+
4
+ // This relies on the 'nodejs_compat' compatibility flag in Cloudflare Workers.
5
+ export const storage = new AsyncLocalStorage<TraceController>();
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Heuristic URL Normalizer
3
+ * Converts raw paths with IDs into generic patterns to prevent high cardinality.
4
+ * Example: /users/123/orders/abc-def -> /users/:id/orders/:uuid
5
+ */
6
+ export const normalizePath = (path: string): string => {
7
+ if (!path || path === '/') return '/';
8
+
9
+ return path
10
+ // Replace UUIDs (long alphanumeric strings)
11
+ .replace(
12
+ /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,
13
+ ':uuid'
14
+ )
15
+ // Replace MongoDB ObjectIds (24 hex chars)
16
+ .replace(/[0-9a-fA-F]{24}/g, ':objectId')
17
+ // Replace pure numeric IDs (e.g., /123)
18
+ .replace(/\/(\d+)(?=\/|$)/g, '/:id')
19
+ // Remove query strings
20
+ .split('?')[0];
21
+ };
22
+
23
+ /**
24
+ * Tries to extract route from Framework internals, falls back to heuristic
25
+ */
26
+ export const getRoute = (req: any, fallbackPath: string): string => {
27
+ // Express / Connect
28
+ if (req.route && req.route.path) {
29
+ return (req.baseUrl || '') + req.route.path;
30
+ }
31
+
32
+ // H3 / Nitro (Nuxt)
33
+ if (req.context && req.context.matchedRoute) {
34
+ return req.context.matchedRoute.path;
35
+ }
36
+
37
+ // Fastify
38
+ if (req.routerPath) {
39
+ return req.routerPath;
40
+ }
41
+
42
+ // Fallback: Heuristic Normalization
43
+ return normalizePath(fallbackPath);
44
+ };
@@ -0,0 +1,41 @@
1
+ import { SenzorOptions, TraceData } from './types';
2
+
3
+ export class Transport {
4
+ private queue: TraceData[] = [];
5
+
6
+ constructor(private config: SenzorOptions) { }
7
+
8
+ public add(trace: TraceData) {
9
+ this.queue.push(trace);
10
+ }
11
+
12
+ /**
13
+ * Flushes the queue to the API.
14
+ * Returns a promise that should be passed to ctx.waitUntil()
15
+ */
16
+ public async flush(): Promise<void> {
17
+ if (this.queue.length === 0) return;
18
+
19
+ const batch = [...this.queue];
20
+ this.queue = []; // Clear immediately
21
+
22
+ try {
23
+ const endpoint = this.config.endpoint || 'https://api.senzor.dev/api/ingest/apm';
24
+
25
+ await fetch(endpoint, {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ 'x-service-api-key': this.config.apiKey,
30
+ },
31
+ body: JSON.stringify(batch),
32
+ // keepalive is not strictly necessary in workers if awaited/waitUntil'd, but good practice
33
+ keepalive: true,
34
+ });
35
+
36
+ if (this.config.debug) console.log(`[Senzor] Flushed ${batch.length} traces`);
37
+ } catch (err) {
38
+ if (this.config.debug) console.error('[Senzor] Ingestion Error:', err);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,44 @@
1
+ export interface SenzorOptions {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ debug?: boolean;
5
+ }
6
+
7
+ export interface Span {
8
+ name: string;
9
+ type: 'db' | 'http' | 'function' | 'custom';
10
+ startTime: number; // Relative to trace start
11
+ duration: number;
12
+ status?: number;
13
+ meta?: Record<string, any>;
14
+ }
15
+
16
+ export interface TraceData {
17
+ traceId: string;
18
+ method: string;
19
+ route: string; // Normalized
20
+ path: string; // Raw
21
+ status: number;
22
+ duration: number;
23
+ ip?: string;
24
+ userAgent?: string;
25
+ timestamp: string;
26
+ spans: Span[];
27
+ }
28
+
29
+ export interface TraceController {
30
+ /**
31
+ * Starts a new span.
32
+ */
33
+ startSpan(name: string, type?: 'db' | 'http' | 'function' | 'custom'): { end: (meta?: any, status?: number) => void };
34
+
35
+ /**
36
+ * Adds an error to the trace.
37
+ */
38
+ captureException(error: Error | any): void;
39
+
40
+ /**
41
+ * The trace ID for the current session
42
+ */
43
+ readonly traceId: string;
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { client } from './core/client';
2
+ import { wrapWorker } from './wrappers/worker';
3
+ import { wrapH3 } from './wrappers/h3';
4
+ import { SenzorOptions } from './core/types';
5
+
6
+ const Senzor = {
7
+ /**
8
+ * Initialize the SDK.
9
+ * Call this in the global scope of your Worker or Plugin.
10
+ */
11
+ init: (options: SenzorOptions) => client.init(options),
12
+
13
+ /**
14
+ * Wrap your Cloudflare Worker 'fetch' handler.
15
+ * Inject trace controller as the 4th argument.
16
+ */
17
+ worker: wrapWorker,
18
+
19
+ /**
20
+ * Wrap a Nitro/H3 event handler.
21
+ * Use this for Nuxt or pure Nitro server routes.
22
+ */
23
+ nitro: wrapH3
24
+ };
25
+
26
+ export default Senzor;
27
+ export { Senzor };
28
+ export type { TraceController } from './core/types';
@@ -0,0 +1,74 @@
1
+ import { storage } from '../core/context';
2
+
3
+ let isInstrumented = false;
4
+
5
+ export const enableFetchInstrumentation = () => {
6
+ if (isInstrumented) return;
7
+
8
+ const originalFetch = globalThis.fetch;
9
+
10
+ // Monkey-patch global fetch
11
+ globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
12
+ // 1. Check for active trace context
13
+ const controller = storage.getStore();
14
+
15
+ // If no trace is active, just pass through
16
+ if (!controller) {
17
+ return originalFetch(input, init);
18
+ }
19
+
20
+ // 2. Resolve URL and Method
21
+ let url = '';
22
+ let method = 'GET';
23
+
24
+ if (typeof input === 'string') {
25
+ url = input;
26
+ } else if (input instanceof URL) {
27
+ url = input.toString();
28
+ } else if (input instanceof Request) {
29
+ url = input.url;
30
+ method = input.method;
31
+ }
32
+
33
+ if (init && init.method) {
34
+ method = init.method;
35
+ }
36
+
37
+ // 3. Start Span
38
+ const spanName = `HTTP ${method}`;
39
+ const span = controller.startSpan(spanName, 'http');
40
+
41
+ // 4. Inject Trace Headers (W3C Trace Context)
42
+ // We clone headers to avoid side effects on the input object
43
+ const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : {}));
44
+
45
+ // Generate a span ID for the outgoing request
46
+ const spanId = crypto.randomUUID().replace(/-/g, '').substring(0, 16);
47
+ const traceParent = `00-${controller.traceId}-${spanId}-01`;
48
+ headers.set('traceparent', traceParent);
49
+
50
+ const newInit: RequestInit = {
51
+ ...init,
52
+ headers
53
+ };
54
+
55
+ try {
56
+ const response = await originalFetch(input, newInit);
57
+ span.end({
58
+ url,
59
+ method,
60
+ status: response.status
61
+ }, response.status);
62
+ return response;
63
+ } catch (err: any) {
64
+ span.end({
65
+ url,
66
+ method,
67
+ error: err.message
68
+ }, 500);
69
+ throw err;
70
+ }
71
+ };
72
+
73
+ isInstrumented = true;
74
+ };
@@ -0,0 +1,59 @@
1
+ import { client } from '../core/client';
2
+ import { getRoute } from '../core/normalizer';
3
+ import { storage } from '../core/context';
4
+
5
+ // Minimal types for H3 to avoid peer-deps
6
+ type EventHandler = (event: any) => any;
7
+
8
+ export const wrapH3 = (handler: EventHandler) => {
9
+ return async (event: any) => {
10
+ const req = event.node.req;
11
+ const path = req.originalUrl || req.url || '/';
12
+
13
+ // 1. Start Trace Session
14
+ const session = client.createTrace({
15
+ method: req.method || 'GET',
16
+ path: path,
17
+ ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress || event.context?.cf?.connectingIp,
18
+ userAgent: req.headers['user-agent'],
19
+ });
20
+
21
+ // 2. Run Handler within AsyncLocalStorage Context
22
+ // This ensures global fetch() auto-instrumentation works inside Nitro handlers
23
+ return storage.run(session.controller, async () => {
24
+ let response: any;
25
+ let status = 200;
26
+
27
+ try {
28
+ response = await handler(event);
29
+
30
+ // H3/Nitro response status handling
31
+ if (event.node.res.statusCode) status = event.node.res.statusCode;
32
+ if (response && response.statusCode) status = response.statusCode;
33
+
34
+ return response;
35
+ } catch (err: any) {
36
+ status = err.statusCode || err.status || 500;
37
+ session.controller.captureException(err);
38
+ throw err;
39
+ } finally {
40
+ // 3. End Trace
41
+ session.end(status, getRoute(event, path));
42
+
43
+ // 4. Flush Data (Non-blocking for Cloudflare)
44
+ // Nitro exposes Cloudflare context in event.context.cloudflare
45
+ const cfCtx = event.context?.cloudflare?.context || event.context?.cf;
46
+ // Or sometimes directly on event in newer H3 versions if adapter binds it
47
+ const waitUntil = cfCtx?.waitUntil || event.waitUntil;
48
+
49
+ if (waitUntil && typeof waitUntil === 'function') {
50
+ waitUntil(session.flush());
51
+ } else {
52
+ // If not in a worker environment or waitUntil missing, flush async but don't block response significantly
53
+ // (Note: without waitUntil, the runtime might kill the process before flush completes)
54
+ session.flush().catch(() => { });
55
+ }
56
+ }
57
+ });
58
+ };
59
+ };
@@ -0,0 +1,49 @@
1
+ import { client } from '../core/client';
2
+ import { normalizePath } from '../core/normalizer';
3
+ import { TraceController } from '../core/types';
4
+ import { storage } from '../core/context';
5
+
6
+ type WorkerHandler = (request: Request, env: any, ctx: any, trace: TraceController) => Promise<Response>;
7
+
8
+ export const wrapWorker = (handler: WorkerHandler) => {
9
+ return async (request: Request, env: any, ctx: any) => {
10
+ // 2. Extract Request Info
11
+ const url = new URL(request.url);
12
+ const path = url.pathname;
13
+
14
+ // 3. Start Trace
15
+ const session = client.createTrace({
16
+ method: request.method,
17
+ path: path,
18
+ userAgent: request.headers.get('user-agent') || undefined,
19
+ ip: request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || undefined
20
+ });
21
+
22
+ // 4. Run Handler within Context (AsyncLocalStorage)
23
+ // This enables the global fetch instrumentation to find the current trace controller
24
+ return storage.run(session.controller, async () => {
25
+ let response: Response;
26
+ let status = 500;
27
+
28
+ try {
29
+ // Inject trace controller as 4th arg for manual usage
30
+ response = await handler(request, env, ctx, session.controller);
31
+ status = response.status;
32
+ return response;
33
+ } catch (err: any) {
34
+ session.controller.captureException(err);
35
+ throw err;
36
+ } finally {
37
+ // 5. End Trace
38
+ session.end(status, normalizePath(path));
39
+
40
+ // 6. Flush (Async WaitUntil)
41
+ if (ctx && typeof ctx.waitUntil === 'function') {
42
+ ctx.waitUntil(session.flush());
43
+ } else {
44
+ await session.flush();
45
+ }
46
+ }
47
+ });
48
+ };
49
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "node",
6
+ "declaration": true,
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["node_modules", "dist"]
14
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ minify: true,
9
+ sourcemap: true,
10
+ splitting: false,
11
+ });