@senzops/apm-worker 1.0.0 → 1.0.1
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 +10 -32
- package/dist/index.d.mts +17 -5
- package/dist/index.d.ts +17 -5
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/core/client.ts +2 -2
- package/src/index.ts +11 -4
- package/src/wrappers/nitro.ts +77 -0
package/README.md
CHANGED
|
@@ -37,15 +37,12 @@ Initialize Senzor in the global scope and wrap your `fetch` handler.
|
|
|
37
37
|
```typescript
|
|
38
38
|
import Senzor from "@senzops/apm-worker";
|
|
39
39
|
|
|
40
|
-
// 1. Initialize (Global Scope)
|
|
41
40
|
Senzor.init({
|
|
42
41
|
apiKey: "sz_apm_...",
|
|
43
42
|
});
|
|
44
43
|
|
|
45
44
|
export default {
|
|
46
|
-
// 2. Wrap the fetch handler
|
|
47
45
|
fetch: Senzor.worker(async (request, env, ctx) => {
|
|
48
|
-
// ... your code ...
|
|
49
46
|
return new Response("Hello World!");
|
|
50
47
|
}),
|
|
51
48
|
};
|
|
@@ -55,51 +52,30 @@ export default {
|
|
|
55
52
|
|
|
56
53
|
## **⚡ Usage with Nitro / Nuxt**
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
For **Nitro** (standalone) or **Nuxt**, use a server plugin to instrument the entire application globally without wrapping individual handlers.
|
|
59
56
|
|
|
60
|
-
### **1. Create a
|
|
57
|
+
### **1. Create a Plugin**
|
|
61
58
|
|
|
62
|
-
Create
|
|
59
|
+
Create `server/plugins/senzor.ts`:
|
|
63
60
|
|
|
64
61
|
```typescript
|
|
65
62
|
import Senzor from "@senzops/apm-worker";
|
|
66
63
|
|
|
67
64
|
export default defineNitroPlugin((nitroApp) => {
|
|
68
65
|
// 1. Initialize
|
|
69
|
-
//
|
|
66
|
+
// Use process.env or runtime config if available
|
|
70
67
|
Senzor.init({
|
|
71
68
|
apiKey: process.env.SENZOR_API_KEY || "sz_apm_...",
|
|
72
69
|
});
|
|
73
70
|
|
|
74
|
-
// 2.
|
|
75
|
-
|
|
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
|
-
});
|
|
71
|
+
// 2. Register Global Instrumentation
|
|
72
|
+
Senzor.nitroPlugin(nitroApp);
|
|
79
73
|
});
|
|
80
74
|
```
|
|
81
75
|
|
|
82
|
-
### **
|
|
76
|
+
### **2. Configuration**
|
|
83
77
|
|
|
84
|
-
|
|
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.
|
|
78
|
+
Ensure `nodejs_compat` is enabled in your `nitro.config.ts`:
|
|
103
79
|
|
|
104
80
|
```typescript
|
|
105
81
|
// nitro.config.ts
|
|
@@ -112,6 +88,8 @@ export default defineNitroConfig({
|
|
|
112
88
|
});
|
|
113
89
|
```
|
|
114
90
|
|
|
91
|
+
That's it! Every request to your Nitro app is now traced, and any `fetch` calls made within your API routes will automatically appear as spans in the trace.
|
|
92
|
+
|
|
115
93
|
---
|
|
116
94
|
|
|
117
95
|
## **📋 Production Checklist**
|
package/dist/index.d.mts
CHANGED
|
@@ -20,6 +20,15 @@ interface TraceController {
|
|
|
20
20
|
readonly traceId: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
interface NitroApp {
|
|
24
|
+
h3App: {
|
|
25
|
+
handler: (event: any) => Promise<any>;
|
|
26
|
+
stack: any[];
|
|
27
|
+
};
|
|
28
|
+
hooks: any;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
declare const Senzor: {
|
|
24
33
|
/**
|
|
25
34
|
* Initialize the SDK.
|
|
@@ -28,14 +37,17 @@ declare const Senzor: {
|
|
|
28
37
|
init: (options: SenzorOptions) => void;
|
|
29
38
|
/**
|
|
30
39
|
* Wrap your Cloudflare Worker 'fetch' handler.
|
|
31
|
-
* Inject trace controller as the 4th argument.
|
|
32
40
|
*/
|
|
33
41
|
worker: (handler: (request: Request, env: any, ctx: any, trace: TraceController) => Promise<Response>) => (request: Request, env: any, ctx: any) => Promise<Response>;
|
|
34
42
|
/**
|
|
35
|
-
* Wrap a
|
|
36
|
-
|
|
43
|
+
* Wrap a generic H3 event handler.
|
|
44
|
+
*/
|
|
45
|
+
wrapH3: (handler: (event: any) => any) => (event: any) => Promise<any>;
|
|
46
|
+
/**
|
|
47
|
+
* Nitro/Nuxt Plugin for global instrumentation.
|
|
48
|
+
* Usage: export default Senzor.nitroPlugin; in server/plugins/senzor.ts
|
|
37
49
|
*/
|
|
38
|
-
|
|
50
|
+
nitroPlugin: (nitroApp: NitroApp) => void;
|
|
39
51
|
};
|
|
40
52
|
|
|
41
|
-
export { Senzor, type TraceController, Senzor as default };
|
|
53
|
+
export { type NitroApp, Senzor, type TraceController, Senzor as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,15 @@ interface TraceController {
|
|
|
20
20
|
readonly traceId: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
interface NitroApp {
|
|
24
|
+
h3App: {
|
|
25
|
+
handler: (event: any) => Promise<any>;
|
|
26
|
+
stack: any[];
|
|
27
|
+
};
|
|
28
|
+
hooks: any;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
declare const Senzor: {
|
|
24
33
|
/**
|
|
25
34
|
* Initialize the SDK.
|
|
@@ -28,14 +37,17 @@ declare const Senzor: {
|
|
|
28
37
|
init: (options: SenzorOptions) => void;
|
|
29
38
|
/**
|
|
30
39
|
* Wrap your Cloudflare Worker 'fetch' handler.
|
|
31
|
-
* Inject trace controller as the 4th argument.
|
|
32
40
|
*/
|
|
33
41
|
worker: (handler: (request: Request, env: any, ctx: any, trace: TraceController) => Promise<Response>) => (request: Request, env: any, ctx: any) => Promise<Response>;
|
|
34
42
|
/**
|
|
35
|
-
* Wrap a
|
|
36
|
-
|
|
43
|
+
* Wrap a generic H3 event handler.
|
|
44
|
+
*/
|
|
45
|
+
wrapH3: (handler: (event: any) => any) => (event: any) => Promise<any>;
|
|
46
|
+
/**
|
|
47
|
+
* Nitro/Nuxt Plugin for global instrumentation.
|
|
48
|
+
* Usage: export default Senzor.nitroPlugin; in server/plugins/senzor.ts
|
|
37
49
|
*/
|
|
38
|
-
|
|
50
|
+
nitroPlugin: (nitroApp: NitroApp) => void;
|
|
39
51
|
};
|
|
40
52
|
|
|
41
|
-
export { Senzor, type TraceController, Senzor as default };
|
|
53
|
+
export { type NitroApp, Senzor, type TraceController, Senzor as default };
|
package/dist/index.global.js
CHANGED
|
@@ -1,2 +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"),
|
|
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"),d=new T.AsyncLocalStorage;var x=!1,S=()=>{if(x)return;let r=globalThis.fetch;globalThis.fetch=async(t,e)=>{let o=d.getStore();if(!o)return r(t,e);let a="",i="GET";typeof t=="string"?a=t:t instanceof URL?a=t.toString():t instanceof Request&&(a=t.url,i=t.method),e&&e.method&&(i=e.method);let n=`HTTP ${i}`,s=o.startSpan(n,"http"),p=new Headers(e?.headers||(t instanceof Request?t.headers:{})),c=crypto.randomUUID().replace(/-/g,"").substring(0,16),u=`00-${o.traceId}-${c}-01`;p.set("traceparent",u);let h={...e,headers:p};try{let l=await r(t,h);return s.end({url:a,method:i,status:l.status},l.status),l}catch(l){throw s.end({url:a,method:i,error:l.message},500),l}},x=!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{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,""),o=performance.now(),a=[];return{controller:{traceId:e,startSpan:(c,u="custom")=>{let h=performance.now(),l=h-o;return{end:(U,C)=>{a.push({name:c,type:u,startTime:l,duration:performance.now()-h,status:C,meta:U})}}},captureException:c=>{a.push({name:"exception",type:"custom",startTime:performance.now()-o,duration:0,status:500,meta:{error:c.message||String(c),stack:c.stack}})}},end:(c,u="UNKNOWN")=>{let h=performance.now()-o,l={traceId:e,method:t.method||"GET",route:u,path:t.path||"/",status:c,duration:h,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:a};this.transport?.add(l)},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){}},f=new y;var w=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],g=(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:w(t);var b=r=>async(t,e,o)=>{let i=new URL(t.url).pathname,n=f.createTrace({method:t.method,path:i,userAgent:t.headers.get("user-agent")||void 0,ip:t.headers.get("cf-connecting-ip")||t.headers.get("x-forwarded-for")||void 0});return d.run(n.controller,async()=>{let s,p=500;try{return s=await r(t,e,o,n.controller),p=s.status,s}catch(c){throw n.controller.captureException(c),c}finally{n.end(p,w(i)),o&&typeof o.waitUntil=="function"?o.waitUntil(n.flush()):await n.flush()}})};var A=r=>async t=>{let e=t.node.req,o=e.originalUrl||e.url||"/",a=f.createTrace({method:e.method||"GET",path:o,ip:e.headers["x-forwarded-for"]||e.socket?.remoteAddress||t.context?.cf?.connectingIp,userAgent:e.headers["user-agent"]});return d.run(a.controller,async()=>{let i,n=200;try{return i=await r(t),t.node.res.statusCode&&(n=t.node.res.statusCode),i&&i.statusCode&&(n=i.statusCode),i}catch(s){throw n=s.statusCode||s.status||500,a.controller.captureException(s),s}finally{a.end(n,g(t,o));let p=(t.context?.cloudflare?.context||t.context?.cf)?.waitUntil||t.waitUntil;p&&typeof p=="function"?p(a.flush()):a.flush().catch(()=>{})}})};var I=r=>{if(!r.h3App||!r.h3App.handler){f.options?.debug&&console.warn("[Senzor] NitroApp.h3App.handler not found. Skipping instrumentation.");return}let t=r.h3App.handler;r.h3App.handler=async e=>{let o=e.node?.req||e.req,a=o?.originalUrl||o?.url||e.path||"/",i=o?.method||e.method||"GET",n=f.createTrace({method:i,path:a,ip:o?.headers?.["x-forwarded-for"]||o?.socket?.remoteAddress||e.context?.cf?.connectingIp,userAgent:o?.headers?.["user-agent"]});return d.run(n.controller,async()=>{let s,p=200;try{return s=await t(e),e.node?.res?.statusCode&&(p=e.node.res.statusCode),s?.status&&(p=s.status),s}catch(c){throw p=c.statusCode||c.status||500,n.controller.captureException(c),c}finally{n.end(p,g(e,a));let u=(e.context?.cloudflare?.context||e.context?.cf||e.context)?.waitUntil||e.waitUntil;u&&typeof u=="function"?u(n.flush()):n.flush().catch(()=>{})}})}};var P={init:r=>f.init(r),worker:b,wrapH3:A,nitroPlugin:I},et=P;})();
|
|
2
2
|
//# sourceMappingURL=index.global.js.map
|
package/dist/index.global.js.map
CHANGED
|
@@ -1 +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","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"]}
|
|
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/wrappers/nitro.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 public 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 * 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 { getRoute } from '../core/normalizer';\r\nimport { storage } from '../core/context';\r\n\r\n// Type definitions for NitroApp to avoid heavy dependencies\r\nexport interface NitroApp {\r\n h3App: {\r\n handler: (event: any) => Promise<any>;\r\n stack: any[];\r\n };\r\n hooks: any;\r\n [key: string]: any;\r\n}\r\n\r\n/**\r\n * A Nitro Plugin to automatically instrument the entire application.\r\n * Usage: Create 'server/plugins/senzor.ts' and export default senzorPlugin;\r\n */\r\nexport const senzorPlugin = (nitroApp: NitroApp) => {\r\n // Capture the original handler\r\n if (!nitroApp.h3App || !nitroApp.h3App.handler) {\r\n if (client.options?.debug) console.warn('[Senzor] NitroApp.h3App.handler not found. Skipping instrumentation.');\r\n return;\r\n }\r\n\r\n const originalHandler = nitroApp.h3App.handler;\r\n\r\n // Replace the main H3 app handler with a wrapped version\r\n nitroApp.h3App.handler = async (event: any) => {\r\n const req = event.node?.req || event.req;\r\n // Fallback for H3 event structure\r\n const path = req?.originalUrl || req?.url || (event.path as string) || '/';\r\n const method = req?.method || (event.method as string) || 'GET';\r\n\r\n // 1. Start Trace\r\n const session = client.createTrace({\r\n method: method,\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 execution inside AsyncLocalStorage context\r\n return storage.run(session.controller, async () => {\r\n let response;\r\n let status = 200;\r\n\r\n try {\r\n response = await originalHandler(event);\r\n\r\n // Try to determine status from response or event\r\n if (event.node?.res?.statusCode) status = event.node.res.statusCode;\r\n if (response?.status) status = response.status;\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 (Non-blocking)\r\n // Check for Cloudflare context in various locations\r\n const cfCtx = event.context?.cloudflare?.context || event.context?.cf || event.context;\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 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 { senzorPlugin, NitroApp } from './wrappers/nitro';\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 */\r\n worker: wrapWorker,\r\n\r\n /**\r\n * Wrap a generic H3 event handler.\r\n */\r\n wrapH3: wrapH3,\r\n\r\n /**\r\n * Nitro/Nuxt Plugin for global instrumentation.\r\n * Usage: export default Senzor.nitroPlugin; in server/plugins/senzor.ts\r\n */\r\n nitroPlugin: senzorPlugin\r\n};\r\n\r\nexport default Senzor;\r\nexport { Senzor };\r\nexport type { TraceController } from './core/types';\r\n// Re-export types that are used in public API signatures\r\nexport type { NitroApp } from './wrappers/nitro';\r\n"],"mappings":"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","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","senzorPlugin","nitroApp","client","originalHandler","event","req","path","method","session","storage","response","status","err","getRoute","waitUntil","Senzor","options","client","wrapWorker","wrapH3","senzorPlugin","index_default"]}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var y=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var D=Object.prototype.hasOwnProperty;var E=(r,t)=>{for(var e in t)y(r,e,{get:t[e],enumerable:!0})},O=(r,t,e,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of k(t))!D.call(r,n)&&n!==e&&y(r,n,{get:()=>t[n],enumerable:!(o=R(t,n))||o.enumerable});return r};var F=r=>O(y({},"__esModule",{value:!0}),r);var N={};E(N,{Senzor:()=>C,default:()=>H});module.exports=F(N);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 x=require("async_hooks"),d=new x.AsyncLocalStorage;var S=!1,b=()=>{if(S)return;let r=globalThis.fetch;globalThis.fetch=async(t,e)=>{let o=d.getStore();if(!o)return r(t,e);let n="",i="GET";typeof t=="string"?n=t:t instanceof URL?n=t.toString():t instanceof Request&&(n=t.url,i=t.method),e&&e.method&&(i=e.method);let a=`HTTP ${i}`,s=o.startSpan(a,"http"),p=new Headers(e?.headers||(t instanceof Request?t.headers:{})),c=crypto.randomUUID().replace(/-/g,"").substring(0,16),u=`00-${o.traceId}-${c}-01`;p.set("traceparent",u);let h={...e,headers:p};try{let l=await r(t,h);return s.end({url:n,method:i,status:l.status},l.status),l}catch(l){throw s.end({url:n,method:i,error:l.message},500),l}},S=!0};var w=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,""),o=performance.now(),n=[];return{controller:{traceId:e,startSpan:(c,u="custom")=>{let h=performance.now(),l=h-o;return{end:(z,P)=>{n.push({name:c,type:u,startTime:l,duration:performance.now()-h,status:P,meta:z})}}},captureException:c=>{n.push({name:"exception",type:"custom",startTime:performance.now()-o,duration:0,status:500,meta:{error:c.message||String(c),stack:c.stack}})}},end:(c,u="UNKNOWN")=>{let h=performance.now()-o,l={traceId:e,method:t.method||"GET",route:u,path:t.path||"/",status:c,duration:h,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:n};this.transport?.add(l)},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){}},f=new w;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],g=(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 A=r=>async(t,e,o)=>{let i=new URL(t.url).pathname,a=f.createTrace({method:t.method,path:i,userAgent:t.headers.get("user-agent")||void 0,ip:t.headers.get("cf-connecting-ip")||t.headers.get("x-forwarded-for")||void 0});return d.run(a.controller,async()=>{let s,p=500;try{return s=await r(t,e,o,a.controller),p=s.status,s}catch(c){throw a.controller.captureException(c),c}finally{a.end(p,T(i)),o&&typeof o.waitUntil=="function"?o.waitUntil(a.flush()):await a.flush()}})};var I=r=>async t=>{let e=t.node.req,o=e.originalUrl||e.url||"/",n=f.createTrace({method:e.method||"GET",path:o,ip:e.headers["x-forwarded-for"]||e.socket?.remoteAddress||t.context?.cf?.connectingIp,userAgent:e.headers["user-agent"]});return d.run(n.controller,async()=>{let i,a=200;try{return i=await r(t),t.node.res.statusCode&&(a=t.node.res.statusCode),i&&i.statusCode&&(a=i.statusCode),i}catch(s){throw a=s.statusCode||s.status||500,n.controller.captureException(s),s}finally{n.end(a,g(t,o));let p=(t.context?.cloudflare?.context||t.context?.cf)?.waitUntil||t.waitUntil;p&&typeof p=="function"?p(n.flush()):n.flush().catch(()=>{})}})};var U=r=>{if(!r.h3App||!r.h3App.handler){f.options?.debug&&console.warn("[Senzor] NitroApp.h3App.handler not found. Skipping instrumentation.");return}let t=r.h3App.handler;r.h3App.handler=async e=>{let o=e.node?.req||e.req,n=o?.originalUrl||o?.url||e.path||"/",i=o?.method||e.method||"GET",a=f.createTrace({method:i,path:n,ip:o?.headers?.["x-forwarded-for"]||o?.socket?.remoteAddress||e.context?.cf?.connectingIp,userAgent:o?.headers?.["user-agent"]});return d.run(a.controller,async()=>{let s,p=200;try{return s=await t(e),e.node?.res?.statusCode&&(p=e.node.res.statusCode),s?.status&&(p=s.status),s}catch(c){throw p=c.statusCode||c.status||500,a.controller.captureException(c),c}finally{a.end(p,g(e,n));let u=(e.context?.cloudflare?.context||e.context?.cf||e.context)?.waitUntil||e.waitUntil;u&&typeof u=="function"?u(a.flush()):a.flush().catch(()=>{})}})}};var C={init:r=>f.init(r),worker:A,wrapH3:I,nitroPlugin:U},H=C;0&&(module.exports={Senzor});
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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","../src/wrappers/nitro.ts"],"sourcesContent":["import { client } from './core/client';\r\nimport { wrapWorker } from './wrappers/worker';\r\nimport { wrapH3 } from './wrappers/h3';\r\nimport { senzorPlugin, NitroApp } from './wrappers/nitro';\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 */\r\n worker: wrapWorker,\r\n\r\n /**\r\n * Wrap a generic H3 event handler.\r\n */\r\n wrapH3: wrapH3,\r\n\r\n /**\r\n * Nitro/Nuxt Plugin for global instrumentation.\r\n * Usage: export default Senzor.nitroPlugin; in server/plugins/senzor.ts\r\n */\r\n nitroPlugin: senzorPlugin\r\n};\r\n\r\nexport default Senzor;\r\nexport { Senzor };\r\nexport type { TraceController } from './core/types';\r\n// Re-export types that are used in public API signatures\r\nexport type { NitroApp } from './wrappers/nitro';\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 public 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 * 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 { getRoute } from '../core/normalizer';\r\nimport { storage } from '../core/context';\r\n\r\n// Type definitions for NitroApp to avoid heavy dependencies\r\nexport interface NitroApp {\r\n h3App: {\r\n handler: (event: any) => Promise<any>;\r\n stack: any[];\r\n };\r\n hooks: any;\r\n [key: string]: any;\r\n}\r\n\r\n/**\r\n * A Nitro Plugin to automatically instrument the entire application.\r\n * Usage: Create 'server/plugins/senzor.ts' and export default senzorPlugin;\r\n */\r\nexport const senzorPlugin = (nitroApp: NitroApp) => {\r\n // Capture the original handler\r\n if (!nitroApp.h3App || !nitroApp.h3App.handler) {\r\n if (client.options?.debug) console.warn('[Senzor] NitroApp.h3App.handler not found. Skipping instrumentation.');\r\n return;\r\n }\r\n\r\n const originalHandler = nitroApp.h3App.handler;\r\n\r\n // Replace the main H3 app handler with a wrapped version\r\n nitroApp.h3App.handler = async (event: any) => {\r\n const req = event.node?.req || event.req;\r\n // Fallback for H3 event structure\r\n const path = req?.originalUrl || req?.url || (event.path as string) || '/';\r\n const method = req?.method || (event.method as string) || 'GET';\r\n\r\n // 1. Start Trace\r\n const session = client.createTrace({\r\n method: method,\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 execution inside AsyncLocalStorage context\r\n return storage.run(session.controller, async () => {\r\n let response;\r\n let status = 200;\r\n\r\n try {\r\n response = await originalHandler(event);\r\n\r\n // Try to determine status from response or event\r\n if (event.node?.res?.statusCode) status = event.node.res.statusCode;\r\n if (response?.status) status = response.status;\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 (Non-blocking)\r\n // Check for Cloudflare context in various locations\r\n const cfCtx = event.context?.cloudflare?.context || event.context?.cf || event.context;\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 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","senzorPlugin","nitroApp","client","originalHandler","event","req","path","method","session","storage","response","status","err","getRoute","waitUntil","Senzor","options","client","wrapWorker","wrapH3","senzorPlugin","index_default"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +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
|
|
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 C}from"async_hooks";var d=new C;var T=!1,x=()=>{if(T)return;let r=globalThis.fetch;globalThis.fetch=async(t,e)=>{let o=d.getStore();if(!o)return r(t,e);let a="",i="GET";typeof t=="string"?a=t:t instanceof URL?a=t.toString():t instanceof Request&&(a=t.url,i=t.method),e&&e.method&&(i=e.method);let n=`HTTP ${i}`,s=o.startSpan(n,"http"),p=new Headers(e?.headers||(t instanceof Request?t.headers:{})),c=crypto.randomUUID().replace(/-/g,"").substring(0,16),u=`00-${o.traceId}-${c}-01`;p.set("traceparent",u);let h={...e,headers:p};try{let l=await r(t,h);return s.end({url:a,method:i,status:l.status},l.status),l}catch(l){throw s.end({url:a,method:i,error:l.message},500),l}},T=!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{x()}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,""),o=performance.now(),a=[];return{controller:{traceId:e,startSpan:(c,u="custom")=>{let h=performance.now(),l=h-o;return{end:(I,U)=>{a.push({name:c,type:u,startTime:l,duration:performance.now()-h,status:U,meta:I})}}},captureException:c=>{a.push({name:"exception",type:"custom",startTime:performance.now()-o,duration:0,status:500,meta:{error:c.message||String(c),stack:c.stack}})}},end:(c,u="UNKNOWN")=>{let h=performance.now()-o,l={traceId:e,method:t.method||"GET",route:u,path:t.path||"/",status:c,duration:h,ip:t.ip,userAgent:t.userAgent,timestamp:new Date().toISOString(),spans:a};this.transport?.add(l)},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){}},f=new y;var w=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],g=(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:w(t);var S=r=>async(t,e,o)=>{let i=new URL(t.url).pathname,n=f.createTrace({method:t.method,path:i,userAgent:t.headers.get("user-agent")||void 0,ip:t.headers.get("cf-connecting-ip")||t.headers.get("x-forwarded-for")||void 0});return d.run(n.controller,async()=>{let s,p=500;try{return s=await r(t,e,o,n.controller),p=s.status,s}catch(c){throw n.controller.captureException(c),c}finally{n.end(p,w(i)),o&&typeof o.waitUntil=="function"?o.waitUntil(n.flush()):await n.flush()}})};var b=r=>async t=>{let e=t.node.req,o=e.originalUrl||e.url||"/",a=f.createTrace({method:e.method||"GET",path:o,ip:e.headers["x-forwarded-for"]||e.socket?.remoteAddress||t.context?.cf?.connectingIp,userAgent:e.headers["user-agent"]});return d.run(a.controller,async()=>{let i,n=200;try{return i=await r(t),t.node.res.statusCode&&(n=t.node.res.statusCode),i&&i.statusCode&&(n=i.statusCode),i}catch(s){throw n=s.statusCode||s.status||500,a.controller.captureException(s),s}finally{a.end(n,g(t,o));let p=(t.context?.cloudflare?.context||t.context?.cf)?.waitUntil||t.waitUntil;p&&typeof p=="function"?p(a.flush()):a.flush().catch(()=>{})}})};var A=r=>{if(!r.h3App||!r.h3App.handler){f.options?.debug&&console.warn("[Senzor] NitroApp.h3App.handler not found. Skipping instrumentation.");return}let t=r.h3App.handler;r.h3App.handler=async e=>{let o=e.node?.req||e.req,a=o?.originalUrl||o?.url||e.path||"/",i=o?.method||e.method||"GET",n=f.createTrace({method:i,path:a,ip:o?.headers?.["x-forwarded-for"]||o?.socket?.remoteAddress||e.context?.cf?.connectingIp,userAgent:o?.headers?.["user-agent"]});return d.run(n.controller,async()=>{let s,p=200;try{return s=await t(e),e.node?.res?.statusCode&&(p=e.node.res.statusCode),s?.status&&(p=s.status),s}catch(c){throw p=c.statusCode||c.status||500,n.controller.captureException(c),c}finally{n.end(p,g(e,a));let u=(e.context?.cloudflare?.context||e.context?.cf||e.context)?.waitUntil||e.waitUntil;u&&typeof u=="function"?u(n.flush()):n.flush().catch(()=>{})}})}};var z={init:r=>f.init(r),worker:S,wrapH3:b,nitroPlugin:A},tt=z;export{z as Senzor,tt as default};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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"]}
|
|
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/wrappers/nitro.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 public 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 * 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 { getRoute } from '../core/normalizer';\r\nimport { storage } from '../core/context';\r\n\r\n// Type definitions for NitroApp to avoid heavy dependencies\r\nexport interface NitroApp {\r\n h3App: {\r\n handler: (event: any) => Promise<any>;\r\n stack: any[];\r\n };\r\n hooks: any;\r\n [key: string]: any;\r\n}\r\n\r\n/**\r\n * A Nitro Plugin to automatically instrument the entire application.\r\n * Usage: Create 'server/plugins/senzor.ts' and export default senzorPlugin;\r\n */\r\nexport const senzorPlugin = (nitroApp: NitroApp) => {\r\n // Capture the original handler\r\n if (!nitroApp.h3App || !nitroApp.h3App.handler) {\r\n if (client.options?.debug) console.warn('[Senzor] NitroApp.h3App.handler not found. Skipping instrumentation.');\r\n return;\r\n }\r\n\r\n const originalHandler = nitroApp.h3App.handler;\r\n\r\n // Replace the main H3 app handler with a wrapped version\r\n nitroApp.h3App.handler = async (event: any) => {\r\n const req = event.node?.req || event.req;\r\n // Fallback for H3 event structure\r\n const path = req?.originalUrl || req?.url || (event.path as string) || '/';\r\n const method = req?.method || (event.method as string) || 'GET';\r\n\r\n // 1. Start Trace\r\n const session = client.createTrace({\r\n method: method,\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 execution inside AsyncLocalStorage context\r\n return storage.run(session.controller, async () => {\r\n let response;\r\n let status = 200;\r\n\r\n try {\r\n response = await originalHandler(event);\r\n\r\n // Try to determine status from response or event\r\n if (event.node?.res?.statusCode) status = event.node.res.statusCode;\r\n if (response?.status) status = response.status;\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 (Non-blocking)\r\n // Check for Cloudflare context in various locations\r\n const cfCtx = event.context?.cloudflare?.context || event.context?.cf || event.context;\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 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 { senzorPlugin, NitroApp } from './wrappers/nitro';\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 */\r\n worker: wrapWorker,\r\n\r\n /**\r\n * Wrap a generic H3 event handler.\r\n */\r\n wrapH3: wrapH3,\r\n\r\n /**\r\n * Nitro/Nuxt Plugin for global instrumentation.\r\n * Usage: export default Senzor.nitroPlugin; in server/plugins/senzor.ts\r\n */\r\n nitroPlugin: senzorPlugin\r\n};\r\n\r\nexport default Senzor;\r\nexport { Senzor };\r\nexport type { TraceController } from './core/types';\r\n// Re-export types that are used in public API signatures\r\nexport type { NitroApp } from './wrappers/nitro';\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","senzorPlugin","nitroApp","client","originalHandler","event","req","path","method","session","storage","response","status","err","getRoute","waitUntil","Senzor","options","client","wrapWorker","wrapH3","senzorPlugin","index_default"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senzops/apm-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Cloudflare Worker / Serverless APM SDK for Senzor",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
],
|
|
30
30
|
"author": "Senzops",
|
|
31
31
|
"license": "MIT"
|
|
32
|
-
}
|
|
32
|
+
}
|
package/src/core/client.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { enableFetchInstrumentation } from '../instrumentation/fetch';
|
|
|
4
4
|
|
|
5
5
|
export class SenzorClient {
|
|
6
6
|
private transport: Transport | null = null;
|
|
7
|
-
|
|
7
|
+
public options: SenzorOptions | null = null;
|
|
8
8
|
|
|
9
9
|
public init(options: SenzorOptions) {
|
|
10
10
|
if (!options.apiKey) {
|
|
@@ -129,4 +129,4 @@ export class SenzorClient {
|
|
|
129
129
|
public endTrace(status: number, data?: { route?: string }) { }
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
export const client = new SenzorClient();
|
|
132
|
+
export const client = new SenzorClient();
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { client } from './core/client';
|
|
2
2
|
import { wrapWorker } from './wrappers/worker';
|
|
3
3
|
import { wrapH3 } from './wrappers/h3';
|
|
4
|
+
import { senzorPlugin, NitroApp } from './wrappers/nitro';
|
|
4
5
|
import { SenzorOptions } from './core/types';
|
|
5
6
|
|
|
6
7
|
const Senzor = {
|
|
@@ -12,17 +13,23 @@ const Senzor = {
|
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Wrap your Cloudflare Worker 'fetch' handler.
|
|
15
|
-
* Inject trace controller as the 4th argument.
|
|
16
16
|
*/
|
|
17
17
|
worker: wrapWorker,
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Wrap a
|
|
21
|
-
* Use this for Nuxt or pure Nitro server routes.
|
|
20
|
+
* Wrap a generic H3 event handler.
|
|
22
21
|
*/
|
|
23
|
-
|
|
22
|
+
wrapH3: wrapH3,
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Nitro/Nuxt Plugin for global instrumentation.
|
|
26
|
+
* Usage: export default Senzor.nitroPlugin; in server/plugins/senzor.ts
|
|
27
|
+
*/
|
|
28
|
+
nitroPlugin: senzorPlugin
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
export default Senzor;
|
|
27
32
|
export { Senzor };
|
|
28
33
|
export type { TraceController } from './core/types';
|
|
34
|
+
// Re-export types that are used in public API signatures
|
|
35
|
+
export type { NitroApp } from './wrappers/nitro';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { client } from '../core/client';
|
|
2
|
+
import { getRoute } from '../core/normalizer';
|
|
3
|
+
import { storage } from '../core/context';
|
|
4
|
+
|
|
5
|
+
// Type definitions for NitroApp to avoid heavy dependencies
|
|
6
|
+
export interface NitroApp {
|
|
7
|
+
h3App: {
|
|
8
|
+
handler: (event: any) => Promise<any>;
|
|
9
|
+
stack: any[];
|
|
10
|
+
};
|
|
11
|
+
hooks: any;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A Nitro Plugin to automatically instrument the entire application.
|
|
17
|
+
* Usage: Create 'server/plugins/senzor.ts' and export default senzorPlugin;
|
|
18
|
+
*/
|
|
19
|
+
export const senzorPlugin = (nitroApp: NitroApp) => {
|
|
20
|
+
// Capture the original handler
|
|
21
|
+
if (!nitroApp.h3App || !nitroApp.h3App.handler) {
|
|
22
|
+
if (client.options?.debug) console.warn('[Senzor] NitroApp.h3App.handler not found. Skipping instrumentation.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const originalHandler = nitroApp.h3App.handler;
|
|
27
|
+
|
|
28
|
+
// Replace the main H3 app handler with a wrapped version
|
|
29
|
+
nitroApp.h3App.handler = async (event: any) => {
|
|
30
|
+
const req = event.node?.req || event.req;
|
|
31
|
+
// Fallback for H3 event structure
|
|
32
|
+
const path = req?.originalUrl || req?.url || (event.path as string) || '/';
|
|
33
|
+
const method = req?.method || (event.method as string) || 'GET';
|
|
34
|
+
|
|
35
|
+
// 1. Start Trace
|
|
36
|
+
const session = client.createTrace({
|
|
37
|
+
method: method,
|
|
38
|
+
path: path,
|
|
39
|
+
ip: req?.headers?.['x-forwarded-for'] || req?.socket?.remoteAddress || event.context?.cf?.connectingIp,
|
|
40
|
+
userAgent: req?.headers?.['user-agent'],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 2. Run execution inside AsyncLocalStorage context
|
|
44
|
+
return storage.run(session.controller, async () => {
|
|
45
|
+
let response;
|
|
46
|
+
let status = 200;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
response = await originalHandler(event);
|
|
50
|
+
|
|
51
|
+
// Try to determine status from response or event
|
|
52
|
+
if (event.node?.res?.statusCode) status = event.node.res.statusCode;
|
|
53
|
+
if (response?.status) status = response.status;
|
|
54
|
+
|
|
55
|
+
return response;
|
|
56
|
+
} catch (err: any) {
|
|
57
|
+
status = err.statusCode || err.status || 500;
|
|
58
|
+
session.controller.captureException(err);
|
|
59
|
+
throw err;
|
|
60
|
+
} finally {
|
|
61
|
+
// 3. End Trace
|
|
62
|
+
session.end(status, getRoute(event, path));
|
|
63
|
+
|
|
64
|
+
// 4. Flush (Non-blocking)
|
|
65
|
+
// Check for Cloudflare context in various locations
|
|
66
|
+
const cfCtx = event.context?.cloudflare?.context || event.context?.cf || event.context;
|
|
67
|
+
const waitUntil = cfCtx?.waitUntil || event.waitUntil;
|
|
68
|
+
|
|
69
|
+
if (waitUntil && typeof waitUntil === 'function') {
|
|
70
|
+
waitUntil(session.flush());
|
|
71
|
+
} else {
|
|
72
|
+
session.flush().catch(() => { });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
};
|