@senzops/web 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -43
- package/dist/index.d.mts +63 -22
- package/dist/index.d.ts +63 -22
- package/dist/index.global.js +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/index.ts +441 -119
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# **@senzops/web**
|
|
2
2
|
|
|
3
|
-
The official, lightweight, and privacy-conscious
|
|
3
|
+
The official, lightweight, and privacy-conscious Web Analytics & Real User Monitoring (RUM) SDK for **Senzor**.
|
|
4
4
|
|
|
5
|
-
**Senzor Web** is a tiny (<
|
|
5
|
+
**Senzor Web** is a tiny (< 4KB gzipped) TypeScript agent designed to track page views, visitor sessions, Core Web Vitals, frontend exceptions, and distributed traces without impacting your website's performance. It works seamlessly with Single Page Applications (SPAs) like React, Next.js, and Vue.
|
|
6
6
|
|
|
7
7
|
## **🚀 Installation**
|
|
8
8
|
|
|
@@ -19,11 +19,19 @@ yarn add @senzops/web
|
|
|
19
19
|
Add this to the <head> of your website:
|
|
20
20
|
|
|
21
21
|
```html
|
|
22
|
-
<script src="https://cdn.jsdelivr.net/gh/senzops/web-agent/dist/index.global.js"></script>
|
|
22
|
+
<script src="[https://cdn.jsdelivr.net/gh/senzops/web-agent/dist/index.global.js](https://cdn.jsdelivr.net/gh/senzops/web-agent/dist/index.global.js)"></script>
|
|
23
23
|
<script>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Opt-in to Web Analytics (Marketing/Product)
|
|
25
|
+
window.Senzor.init({
|
|
26
|
+
webId: "YOUR_WEB_ID_HERE",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Opt-in to Web APM / RUM (Engineering)
|
|
30
|
+
window.Senzor.initRum({
|
|
31
|
+
apiKey: "YOUR_RUM_API_KEY_HERE",
|
|
32
|
+
sampleRate: 0.1, // 10% of sessions
|
|
33
|
+
allowedOrigins: ["https://api.yourdomain.com"] // For distributed tracing
|
|
34
|
+
});
|
|
27
35
|
</script>
|
|
28
36
|
```
|
|
29
37
|
|
|
@@ -31,21 +39,29 @@ Add this to the <head> of your website:
|
|
|
31
39
|
|
|
32
40
|
### **In React / Next.js**
|
|
33
41
|
|
|
34
|
-
Initialize the agent once in your root layout or main app component.
|
|
42
|
+
Initialize the agent once in your root layout or main app component. Senzor cleanly separates **Web Analytics** (100% sampling, privacy-focused) from **Web APM / RUM** (performance tracing, configurable sampling). You can use either, or both\!
|
|
35
43
|
|
|
36
44
|
```jsx
|
|
37
45
|
import { useEffect } from "react";
|
|
38
46
|
import { Senzor } from "@senzops/web";
|
|
39
47
|
|
|
40
48
|
export default function App({ Component, pageProps }) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
// 1. Web Analytics (Page views, Bounce rates, Sessions)
|
|
51
|
+
Senzor.init({
|
|
52
|
+
webId: "req_123456789", // Get this from your Senzor Dashboard
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 2. Web APM / RUM (Core Web Vitals, Network Spans, JS Errors)
|
|
56
|
+
Senzor.initRum({
|
|
57
|
+
apiKey: "rum\_987654321", // Get this from your Senzor Dashboard
|
|
58
|
+
sampleRate: 1.0, // Trace 100% of sessions (Adjust for high-traffic sites)
|
|
59
|
+
allowedOrigins: ["https://api.mycompany.com"] // Inject W3C trace headers here
|
|
45
60
|
});
|
|
46
|
-
}, []);
|
|
47
61
|
|
|
48
|
-
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return <Component {...pageProps} />;
|
|
49
65
|
}
|
|
50
66
|
```
|
|
51
67
|
|
|
@@ -53,42 +69,40 @@ export default function App({ Component, pageProps }) {
|
|
|
53
69
|
|
|
54
70
|
The Senzor Agent is designed to be **"Fire and Forget"**. It operates asynchronously to ensure it never blocks the main thread or slows down page loads.
|
|
55
71
|
|
|
56
|
-
### **1
|
|
57
|
-
|
|
58
|
-
- **Visitor ID:** When a user visits, we generate a random UUID and store it in localStorage. This allows us to track unique visitors over a 1-year period.
|
|
59
|
-
- **Session ID:** We generate a UUID in sessionStorage. This persists across tab reloads but clears when the browser/tab is closed, allowing us to calculate **Bounce Rates** and **Session Duration**.
|
|
60
|
-
- **Privacy:** We do **not** use cookies. All data is first-party.
|
|
72
|
+
### **Part 1: Web Analytics (Marketing & Product)**
|
|
61
73
|
|
|
62
|
-
|
|
74
|
+
- **Identity & Sessions:** We generate random UUIDs in localStorage and sessionStorage. This allows us to track unique visitors and calculate Bounce Rates without using cookies.
|
|
75
|
+
- **Event Tracking:** Detects SPA route changes via history.pushState and popstate.
|
|
76
|
+
- **Duration Pings:** Calculates precise time-on-page by listening to visibilitychange and beforeunload, sending a final duration "ping" when the user leaves.
|
|
63
77
|
|
|
64
|
-
|
|
78
|
+
### **Part 2: Web APM & RUM (Engineering & Performance)**
|
|
65
79
|
|
|
66
|
-
- **
|
|
67
|
-
- **
|
|
68
|
-
- **
|
|
80
|
+
- **Core Web Vitals (CWV):** Passively listens via PerformanceObserver to capture Google Web Vitals (LCP, INP, CLS, FCP) without stalling the main thread.
|
|
81
|
+
- **Distributed Tracing:** Safely intercepts window.fetch and XMLHttpRequest. If a request targets an allowedOrigins domain, it injects a W3C traceparent header, connecting frontend clicks directly to your Node.js backend database queries.
|
|
82
|
+
- **UX Frustrations:** Detects "Rage Clicks" (rapid clicking on one element) and "Dead Clicks" (clicking non-interactive elements) to highlight poor UX.
|
|
83
|
+
- **Universal Error Engine:** Captures uncaughtException and unhandledRejection, attaching a "Breadcrumb" trail of the user's last 15 actions (clicks, navigations) leading up to the crash.
|
|
69
84
|
|
|
70
|
-
### **
|
|
85
|
+
### **Data Transmission**
|
|
71
86
|
|
|
72
|
-
|
|
87
|
+
We prioritize data reliability using navigator.sendBeacon. It queues data to be sent by the browser even _after_ the tab is closed. If unavailable, it falls back to a standard fetch request with keepalive: true.
|
|
73
88
|
|
|
74
|
-
|
|
75
|
-
2. When the user navigates away (beforeunload) or hides the tab (visibilitychange), we calculate duration = Now - startTime.
|
|
76
|
-
3. We send a ping event with this duration.
|
|
77
|
-
4. **The Backend** receives this ping and updates the _previous_ pageview entry in the database, incrementing its duration.
|
|
89
|
+
## **⚙️ Configuration Options**
|
|
78
90
|
|
|
79
|
-
### **
|
|
91
|
+
### **Analytics Options (Senzor.init)**
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
| Option | Type | Default | Description |
|
|
94
|
+
| :------- | :----- | :---------------- | :------------------------------------------- |
|
|
95
|
+
| webId | string | **Required** | The unique Analytics ID of your website. |
|
|
96
|
+
| endpoint | string | api.senzor.dev... | Ingestion API URL. Use this if self-hosting. |
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
- **Fallback:** If sendBeacon is unavailable, we fall back to a standard fetch request with keepalive: true.
|
|
98
|
+
### **RUM Options (Senzor.initRum)**
|
|
85
99
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
| endpoint
|
|
100
|
+
| Option | Type | Default | Description |
|
|
101
|
+
| :------------- | :----- | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| apiKey | string | **Required** | The unique RUM API Key of your website. |
|
|
103
|
+
| sampleRate | number | 1.0 | Percentage of performance traces to capture (0.0 to 1.0). _Errors are ALWAYS 100% captured regardless of this setting._ |
|
|
104
|
+
| allowedOrigins | array | [] | Array of strings or RegEx. The agent will only inject W3C traceparent headers into requests targeting these origins (prevents CORS failures on 3rd-party APIs). |
|
|
105
|
+
| endpoint | string | api.senzor.dev... | Ingestion API URL. Use this if self-hosting. |
|
|
92
106
|
|
|
93
107
|
## **📦 Development**
|
|
94
108
|
|
|
@@ -102,12 +116,12 @@ To build the agent locally:
|
|
|
102
116
|
npm install
|
|
103
117
|
```
|
|
104
118
|
|
|
105
|
-
2. Build
|
|
119
|
+
2. **Build**
|
|
106
120
|
Uses tsup to bundle for ESM, CJS, and IIFE (Global variable).
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
```sh
|
|
123
|
+
npm run build
|
|
124
|
+
```
|
|
111
125
|
|
|
112
126
|
3. **Output**
|
|
113
127
|
- dist/index.js (CommonJS)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,22 +1,63 @@
|
|
|
1
|
-
interface
|
|
2
|
-
webId: string;
|
|
3
|
-
endpoint?: string;
|
|
4
|
-
}
|
|
5
|
-
declare class
|
|
6
|
-
private config;
|
|
7
|
-
private startTime;
|
|
8
|
-
private endpoint;
|
|
9
|
-
private initialized;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
interface AnalyticsConfig {
|
|
2
|
+
webId: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
}
|
|
5
|
+
declare class SenzorAnalyticsAgent {
|
|
6
|
+
private config;
|
|
7
|
+
private startTime;
|
|
8
|
+
private endpoint;
|
|
9
|
+
private initialized;
|
|
10
|
+
init(config: AnalyticsConfig): void;
|
|
11
|
+
private normalizeUrl;
|
|
12
|
+
private manageSession;
|
|
13
|
+
private determineReferrer;
|
|
14
|
+
private getIds;
|
|
15
|
+
private trackPageView;
|
|
16
|
+
private trackPing;
|
|
17
|
+
private send;
|
|
18
|
+
private fallbackSend;
|
|
19
|
+
private setupListeners;
|
|
20
|
+
}
|
|
21
|
+
interface RumConfig {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
sampleRate?: number;
|
|
25
|
+
allowedOrigins?: (string | RegExp)[];
|
|
26
|
+
}
|
|
27
|
+
declare class SenzorRumAgent {
|
|
28
|
+
private config;
|
|
29
|
+
private endpoint;
|
|
30
|
+
private initialized;
|
|
31
|
+
private isSampled;
|
|
32
|
+
private sessionId;
|
|
33
|
+
private traceId;
|
|
34
|
+
private traceStartTime;
|
|
35
|
+
private isInitialLoad;
|
|
36
|
+
private spans;
|
|
37
|
+
private errors;
|
|
38
|
+
private breadcrumbs;
|
|
39
|
+
private vitals;
|
|
40
|
+
private frustrations;
|
|
41
|
+
private clickHistory;
|
|
42
|
+
private flushInterval;
|
|
43
|
+
init(config: RumConfig): void;
|
|
44
|
+
private manageSession;
|
|
45
|
+
private startNewTrace;
|
|
46
|
+
private addBreadcrumb;
|
|
47
|
+
private setupUXListeners;
|
|
48
|
+
private setupPerformanceObservers;
|
|
49
|
+
private getNavigationTimings;
|
|
50
|
+
private shouldAttachTraceHeader;
|
|
51
|
+
private patchNetwork;
|
|
52
|
+
private setupErrorListeners;
|
|
53
|
+
private setupRoutingListeners;
|
|
54
|
+
private flush;
|
|
55
|
+
}
|
|
56
|
+
declare const Analytics: SenzorAnalyticsAgent;
|
|
57
|
+
declare const RUM: SenzorRumAgent;
|
|
58
|
+
declare const Senzor: {
|
|
59
|
+
init: (config: AnalyticsConfig) => void;
|
|
60
|
+
initRum: (config: RumConfig) => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export { Analytics, RUM, Senzor };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,63 @@
|
|
|
1
|
-
interface
|
|
2
|
-
webId: string;
|
|
3
|
-
endpoint?: string;
|
|
4
|
-
}
|
|
5
|
-
declare class
|
|
6
|
-
private config;
|
|
7
|
-
private startTime;
|
|
8
|
-
private endpoint;
|
|
9
|
-
private initialized;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
interface AnalyticsConfig {
|
|
2
|
+
webId: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
}
|
|
5
|
+
declare class SenzorAnalyticsAgent {
|
|
6
|
+
private config;
|
|
7
|
+
private startTime;
|
|
8
|
+
private endpoint;
|
|
9
|
+
private initialized;
|
|
10
|
+
init(config: AnalyticsConfig): void;
|
|
11
|
+
private normalizeUrl;
|
|
12
|
+
private manageSession;
|
|
13
|
+
private determineReferrer;
|
|
14
|
+
private getIds;
|
|
15
|
+
private trackPageView;
|
|
16
|
+
private trackPing;
|
|
17
|
+
private send;
|
|
18
|
+
private fallbackSend;
|
|
19
|
+
private setupListeners;
|
|
20
|
+
}
|
|
21
|
+
interface RumConfig {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
sampleRate?: number;
|
|
25
|
+
allowedOrigins?: (string | RegExp)[];
|
|
26
|
+
}
|
|
27
|
+
declare class SenzorRumAgent {
|
|
28
|
+
private config;
|
|
29
|
+
private endpoint;
|
|
30
|
+
private initialized;
|
|
31
|
+
private isSampled;
|
|
32
|
+
private sessionId;
|
|
33
|
+
private traceId;
|
|
34
|
+
private traceStartTime;
|
|
35
|
+
private isInitialLoad;
|
|
36
|
+
private spans;
|
|
37
|
+
private errors;
|
|
38
|
+
private breadcrumbs;
|
|
39
|
+
private vitals;
|
|
40
|
+
private frustrations;
|
|
41
|
+
private clickHistory;
|
|
42
|
+
private flushInterval;
|
|
43
|
+
init(config: RumConfig): void;
|
|
44
|
+
private manageSession;
|
|
45
|
+
private startNewTrace;
|
|
46
|
+
private addBreadcrumb;
|
|
47
|
+
private setupUXListeners;
|
|
48
|
+
private setupPerformanceObservers;
|
|
49
|
+
private getNavigationTimings;
|
|
50
|
+
private shouldAttachTraceHeader;
|
|
51
|
+
private patchNetwork;
|
|
52
|
+
private setupErrorListeners;
|
|
53
|
+
private setupRoutingListeners;
|
|
54
|
+
private flush;
|
|
55
|
+
}
|
|
56
|
+
declare const Analytics: SenzorAnalyticsAgent;
|
|
57
|
+
declare const RUM: SenzorRumAgent;
|
|
58
|
+
declare const Senzor: {
|
|
59
|
+
init: (config: AnalyticsConfig) => void;
|
|
60
|
+
initRum: (config: RumConfig) => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export { Analytics, RUM, Senzor };
|
package/dist/index.global.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{function r(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{let t=Math.random()*16|0;return(n==="x"?t:t&3|8).toString(16)})}var i=class{config;startTime;endpoint;initialized;constructor(){this.config={webId:"",endpoint:"https://api.senzor.dev/api/ingest/web"},this.startTime=Date.now(),this.endpoint="",this.initialized=!1}init(t){if(this.initialized){console.warn("[Senzor] Agent already initialized.");return}if(this.initialized=!0,this.config={...this.config,...t},this.endpoint=this.config.endpoint||"https://api.senzor.dev/api/ingest/web",!this.config.webId){console.error("[Senzor] WebId is required.");return}this.checkSession(),this.trackPageView(),this.setupListeners()}checkSession(){let t=Date.now(),e=parseInt(localStorage.getItem("senzor_last_activity")||"0",10),o=1800*1e3;localStorage.getItem("senzor_vid")||localStorage.setItem("senzor_vid",r()),(!localStorage.getItem("senzor_sid")||t-e>o)&&localStorage.setItem("senzor_sid",r()),localStorage.setItem("senzor_last_activity",t.toString())}getIds(){return localStorage.setItem("senzor_last_activity",Date.now().toString()),{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:localStorage.getItem("senzor_sid")||"unknown"}}trackPageView(){this.checkSession(),this.startTime=Date.now();let t={type:"pageview",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone};this.send(t)}trackPing(){let t=Math.floor((Date.now()-this.startTime)/1e3);if(t<1)return;let e={type:"ping",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,duration:t};this.send(e)}send(t){if(navigator.sendBeacon){let e=new Blob([JSON.stringify(t)],{type:"application/json"});navigator.sendBeacon(this.endpoint,e)||this.fallbackSend(t)}else this.fallbackSend(t)}fallbackSend(t){fetch(this.endpoint,{method:"POST",body:JSON.stringify(t),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(e=>console.error("[Senzor] Telemetry Error:",e))}setupListeners(){let t=history.pushState;history.pushState=(...e)=>{this.trackPing(),t.apply(history,e),this.trackPageView()},window.addEventListener("popstate",()=>{this.trackPing(),this.trackPageView()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?this.trackPing():(this.startTime=Date.now(),this.checkSession())}),window.addEventListener("beforeunload",()=>{this.trackPing()})}},s=new i;typeof window<"u"&&(window.Senzor=s);})();
|
|
1
|
+
(()=>{function f(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,a=>{let t=Math.random()*16|0;return(a==="x"?t:t&3|8).toString(16)})}function u(a){let t="";for(;t.length<a;)t+=Math.random().toString(16).slice(2);return t.slice(0,a)}var w=()=>{var a;return{userAgent:navigator.userAgent,url:window.location.href,deviceMemory:navigator.deviceMemory||void 0,connectionType:((a=navigator.connection)==null?void 0:a.effectiveType)||void 0}},m=class{config={webId:""};startTime=Date.now();endpoint="https://api.senzor.dev/api/ingest/web";initialized=!1;init(t){if(!this.initialized){if(this.initialized=!0,this.config={...this.config,...t},t.endpoint&&(this.endpoint=t.endpoint),!this.config.webId){console.error("[Senzor] webId is required for Analytics.");return}this.manageSession(),this.trackPageView(),this.setupListeners()}}normalizeUrl(t){return t?t.replace(/^https?:\/\//,""):""}manageSession(){let t=Date.now(),e=parseInt(localStorage.getItem("sz_wa_last")||"0",10);localStorage.getItem("sz_wa_vid")||localStorage.setItem("sz_wa_vid",f());let i=sessionStorage.getItem("sz_wa_sid");!i||t-e>1800*1e3?(i=f(),sessionStorage.setItem("sz_wa_sid",i),this.determineReferrer(!0)):this.determineReferrer(!1),localStorage.setItem("sz_wa_last",t.toString())}determineReferrer(t){let e=document.referrer,i=!1;if(e)try{i=new URL(e).hostname!==window.location.hostname}catch{i=!0}if(i){let n=this.normalizeUrl(e);n!==sessionStorage.getItem("sz_wa_ref")&&sessionStorage.setItem("sz_wa_ref",n)}else t&&!sessionStorage.getItem("sz_wa_ref")&&sessionStorage.setItem("sz_wa_ref","Direct")}getIds(){return localStorage.setItem("sz_wa_last",Date.now().toString()),{visitorId:localStorage.getItem("sz_wa_vid")||"unknown",sessionId:sessionStorage.getItem("sz_wa_sid")||"unknown",referrer:sessionStorage.getItem("sz_wa_ref")||"Direct"}}trackPageView(){this.manageSession(),this.startTime=Date.now(),this.send({type:"pageview",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,title:document.title,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,referrer:this.getIds().referrer})}trackPing(){let t=Math.floor((Date.now()-this.startTime)/1e3);t>=1&&this.send({type:"ping",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,title:document.title,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,referrer:this.getIds().referrer,duration:t})}send(t){navigator.sendBeacon?navigator.sendBeacon(this.endpoint,new Blob([JSON.stringify(t)],{type:"application/json"}))||this.fallbackSend(t):this.fallbackSend(t)}fallbackSend(t){fetch(this.endpoint,{method:"POST",body:JSON.stringify(t),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(()=>{})}setupListeners(){let t=history.pushState;history.pushState=(...e)=>{this.trackPing(),t.apply(history,e),this.trackPageView()},window.addEventListener("popstate",()=>{this.trackPing(),this.trackPageView()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?this.trackPing():(this.startTime=Date.now(),this.manageSession())}),window.addEventListener("beforeunload",()=>this.trackPing())}},g=class{config={apiKey:"",sampleRate:1,allowedOrigins:[]};endpoint="https://api.senzor.dev/api/ingest/rum";initialized=!1;isSampled=!0;sessionId="";traceId="";traceStartTime=0;isInitialLoad=!0;spans=[];errors=[];breadcrumbs=[];vitals={};frustrations={rageClicks:0,deadClicks:0,errorCount:0};clickHistory=[];flushInterval;init(t){if(!this.initialized){if(this.initialized=!0,this.config={...this.config,...t},t.endpoint&&(this.endpoint=t.endpoint),!this.config.apiKey){console.error("[Senzor RUM] apiKey is required.");return}this.isSampled=Math.random()<=(this.config.sampleRate??1),this.manageSession(),this.startNewTrace(!0),this.setupErrorListeners(),this.setupPerformanceObservers(),this.setupUXListeners(),this.isSampled&&this.patchNetwork(),this.flushInterval=setInterval(()=>this.flush(),1e4),this.setupRoutingListeners()}}manageSession(){sessionStorage.getItem("sz_rum_sid")||sessionStorage.setItem("sz_rum_sid",f()),this.sessionId=sessionStorage.getItem("sz_rum_sid")}startNewTrace(t){this.traceId=u(32),this.traceStartTime=Date.now(),this.isInitialLoad=t,this.spans=[],this.vitals={},this.frustrations={rageClicks:0,deadClicks:0,errorCount:0}}addBreadcrumb(t,e){this.breadcrumbs.push({type:t,message:e,time:Date.now()}),this.breadcrumbs.length>15&&this.breadcrumbs.shift()}setupUXListeners(){document.addEventListener("click",t=>{let e=t.target,i=e.tagName?e.tagName.toLowerCase():"";this.addBreadcrumb("click",`Clicked ${i}${e.id?"#"+e.id:""}${e.className?"."+e.className.split(" ")[0]:""}`),["a","button","input","select","textarea","label"].includes(i)||e.closest("button")||e.closest("a")||e.hasAttribute("role")||e.onclick||this.frustrations.deadClicks++;let r=Date.now();if(this.clickHistory.push({x:t.clientX,y:t.clientY,time:r}),this.clickHistory=this.clickHistory.filter(o=>r-o.time<1e3),this.clickHistory.length>=3){let o=this.clickHistory[0],c=!0;for(let d=1;d<this.clickHistory.length;d++){let l=Math.abs(this.clickHistory[d].x-o.x),p=Math.abs(this.clickHistory[d].y-o.y);(l>50||p>50)&&(c=!1)}c&&(this.frustrations.rageClicks++,this.clickHistory=[])}},{capture:!0,passive:!0})}setupPerformanceObservers(){if(!(!this.isSampled||typeof PerformanceObserver>"u"))try{new PerformanceObserver(e=>{for(let i of e.getEntriesByName("first-contentful-paint"))this.vitals.fcp=i.startTime}).observe({type:"paint",buffered:!0}),new PerformanceObserver(e=>{let i=e.getEntries(),n=i[i.length-1];n&&(this.vitals.lcp=n.startTime)}).observe({type:"largest-contentful-paint",buffered:!0});let t=0;new PerformanceObserver(e=>{for(let i of e.getEntries())i.hadRecentInput||(t+=i.value,this.vitals.cls=t)}).observe({type:"layout-shift",buffered:!0}),new PerformanceObserver(e=>{for(let i of e.getEntries()){let n=i,s=n.duration||(n.processingStart&&n.startTime?n.processingStart-n.startTime:0);(!this.vitals.inp||s>this.vitals.inp)&&(this.vitals.inp=s)}}).observe({type:"event",buffered:!0,durationThreshold:40})}catch{}}getNavigationTimings(){if(typeof performance>"u")return{};let t=performance.getEntriesByType("navigation")[0];return t?{dns:Math.max(0,t.domainLookupEnd-t.domainLookupStart),tcp:Math.max(0,t.connectEnd-t.connectStart),ssl:t.secureConnectionStart?Math.max(0,t.requestStart-t.secureConnectionStart):0,ttfb:Math.max(0,t.responseStart-t.requestStart),domInteractive:Math.max(0,t.domInteractive-t.startTime),domComplete:Math.max(0,t.domComplete-t.startTime)}:{}}shouldAttachTraceHeader(t){if(!this.config.allowedOrigins||this.config.allowedOrigins.length===0)return!1;try{let e=new URL(t,window.location.origin);return this.config.allowedOrigins.some(i=>typeof i=="string"?e.origin.includes(i):i instanceof RegExp?i.test(e.origin):!1)}catch{return!1}}patchNetwork(){let t=this,e=XMLHttpRequest.prototype.open,i=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(s,r,...o){return this.__szMethod=s,this.__szUrl=r,e.apply(this,[s,r,...o])},XMLHttpRequest.prototype.send=function(s){let r=this,o=u(16),c=Date.now()-t.traceStartTime;return t.shouldAttachTraceHeader(r.__szUrl)&&r.setRequestHeader("traceparent",`00-${t.traceId}-${o}-01`),r.addEventListener("loadend",()=>{t.spans.push({spanId:o,name:new URL(r.__szUrl,window.location.origin).pathname,type:"xhr",method:r.__szMethod,status:r.status,startTime:c,duration:Date.now()-t.traceStartTime-c})}),i.call(this,s)};let n=window.fetch;window.fetch=async function(...s){var l,p;let r=typeof s[0]=="string"?s[0]:s[0].url,o=(((l=s[1])==null?void 0:l.method)||s[0].method||"GET").toUpperCase(),c=u(16),d=Date.now()-t.traceStartTime;if(t.shouldAttachTraceHeader(r)){let h=new Headers(((p=s[1])==null?void 0:p.headers)||s[0].headers||{});h.set("traceparent",`00-${t.traceId}-${c}-01`),s[1]?s[1].headers=h:s[0]instanceof Request&&(s[0]=new Request(s[0],{headers:h}))}try{let h=await n.apply(this,s);return t.spans.push({spanId:c,name:new URL(r,window.location.origin).pathname,type:"fetch",method:o,status:h.status,startTime:d,duration:Date.now()-t.traceStartTime-d}),h}catch(h){throw t.spans.push({spanId:c,name:new URL(r,window.location.origin).pathname,type:"fetch",method:o,status:0,startTime:d,duration:Date.now()-t.traceStartTime-d}),h}}}setupErrorListeners(){let t=(e,i)=>{this.frustrations.errorCount++;let n=e.message||String(e);this.errors.push({errorClass:e.name||"Error",message:n,stackTrace:e.stack||"",traceId:this.isSampled?this.traceId:void 0,context:{type:i,...w(),breadcrumbs:[...this.breadcrumbs]},timestamp:new Date().toISOString()}),this.flush()};window.addEventListener("error",e=>{e.error&&t(e.error,"Uncaught Exception")}),window.addEventListener("unhandledrejection",e=>{t(e.reason instanceof Error?e.reason:new Error(String(e.reason)),"Unhandled Promise Rejection")})}setupRoutingListeners(){let t=history.pushState;history.pushState=(...e)=>{this.flush(),t.apply(history,e),this.startNewTrace(!1),this.addBreadcrumb("navigation",window.location.pathname)},window.addEventListener("popstate",()=>{this.flush(),this.startNewTrace(!1),this.addBreadcrumb("navigation",window.location.pathname)}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()}),window.addEventListener("pagehide",()=>this.flush())}flush(){if(this.spans.length===0&&this.errors.length===0&&!this.isInitialLoad)return;let t={traces:[],errors:this.errors};if(this.isSampled&&t.traces.push({traceId:this.traceId,sessionId:this.sessionId,traceType:this.isInitialLoad?"initial_load":"route_change",path:window.location.pathname,referrer:document.referrer||"",vitals:{...this.vitals},timings:this.isInitialLoad?this.getNavigationTimings():{},frustration:{...this.frustrations},...w(),spans:[...this.spans],duration:Date.now()-this.traceStartTime,timestamp:new Date(this.traceStartTime).toISOString()}),this.spans=[],this.errors=[],this.frustrations={rageClicks:0,deadClicks:0,errorCount:0},this.isInitialLoad=!1,t.traces.length>0||t.errors.length>0){let e=new Blob([JSON.stringify(t)],{type:"application/json"});navigator.sendBeacon?navigator.sendBeacon(this.endpoint,e):fetch(this.endpoint,{method:"POST",body:e,keepalive:!0}).catch(()=>{})}}},v=new m,y=new g,S={init:a=>v.init(a),initRum:a=>y.init(a)};typeof window<"u"&&(window.Senzor=S);})();
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var r=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var h=Object.prototype.hasOwnProperty;var g=(i,t)=>{for(var e in t)r(i,e,{get:t[e],enumerable:!0})},p=(i,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of l(t))!h.call(i,o)&&o!==e&&r(i,o,{get:()=>t[o],enumerable:!(n=c(t,o))||n.enumerable});return i};var w=i=>p(r({},"__esModule",{value:!0}),i);var f={};g(f,{Senzor:()=>d});module.exports=w(f);function a(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,i=>{let t=Math.random()*16|0;return(i==="x"?t:t&3|8).toString(16)})}var s=class{config;startTime;endpoint;initialized;constructor(){this.config={webId:"",endpoint:"https://api.senzor.dev/api/ingest/web"},this.startTime=Date.now(),this.endpoint="",this.initialized=!1}init(t){if(this.initialized){console.warn("[Senzor] Agent already initialized.");return}if(this.initialized=!0,this.config={...this.config,...t},this.endpoint=this.config.endpoint||"https://api.senzor.dev/api/ingest/web",!this.config.webId){console.error("[Senzor] WebId is required.");return}this.checkSession(),this.trackPageView(),this.setupListeners()}checkSession(){let t=Date.now(),e=parseInt(localStorage.getItem("senzor_last_activity")||"0",10),n=1800*1e3;localStorage.getItem("senzor_vid")||localStorage.setItem("senzor_vid",a()),(!localStorage.getItem("senzor_sid")||t-e>n)&&localStorage.setItem("senzor_sid",a()),localStorage.setItem("senzor_last_activity",t.toString())}getIds(){return localStorage.setItem("senzor_last_activity",Date.now().toString()),{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:localStorage.getItem("senzor_sid")||"unknown"}}trackPageView(){this.checkSession(),this.startTime=Date.now();let t={type:"pageview",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone};this.send(t)}trackPing(){let t=Math.floor((Date.now()-this.startTime)/1e3);if(t<1)return;let e={type:"ping",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,duration:t};this.send(e)}send(t){if(navigator.sendBeacon){let e=new Blob([JSON.stringify(t)],{type:"application/json"});navigator.sendBeacon(this.endpoint,e)||this.fallbackSend(t)}else this.fallbackSend(t)}fallbackSend(t){fetch(this.endpoint,{method:"POST",body:JSON.stringify(t),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(e=>console.error("[Senzor] Telemetry Error:",e))}setupListeners(){let t=history.pushState;history.pushState=(...e)=>{this.trackPing(),t.apply(history,e),this.trackPageView()},window.addEventListener("popstate",()=>{this.trackPing(),this.trackPageView()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?this.trackPing():(this.startTime=Date.now(),this.checkSession())}),window.addEventListener("beforeunload",()=>{this.trackPing()})}},d=new s;typeof window<"u"&&(window.Senzor=d);0&&(module.exports={Senzor});
|
|
1
|
+
var u=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var k=(r,t)=>{for(var e in t)u(r,e,{get:t[e],enumerable:!0})},T=(r,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of x(t))!_.call(r,s)&&s!==e&&u(r,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return r};var L=r=>T(u({},"__esModule",{value:!0}),r);var R={};k(R,{Analytics:()=>y,RUM:()=>S,Senzor:()=>b});module.exports=L(R);function m(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,r=>{let t=Math.random()*16|0;return(r==="x"?t:t&3|8).toString(16)})}function f(r){let t="";for(;t.length<r;)t+=Math.random().toString(16).slice(2);return t.slice(0,r)}var v=()=>{var r;return{userAgent:navigator.userAgent,url:window.location.href,deviceMemory:navigator.deviceMemory||void 0,connectionType:((r=navigator.connection)==null?void 0:r.effectiveType)||void 0}},g=class{config={webId:""};startTime=Date.now();endpoint="https://api.senzor.dev/api/ingest/web";initialized=!1;init(t){if(!this.initialized){if(this.initialized=!0,this.config={...this.config,...t},t.endpoint&&(this.endpoint=t.endpoint),!this.config.webId){console.error("[Senzor] webId is required for Analytics.");return}this.manageSession(),this.trackPageView(),this.setupListeners()}}normalizeUrl(t){return t?t.replace(/^https?:\/\//,""):""}manageSession(){let t=Date.now(),e=parseInt(localStorage.getItem("sz_wa_last")||"0",10);localStorage.getItem("sz_wa_vid")||localStorage.setItem("sz_wa_vid",m());let i=sessionStorage.getItem("sz_wa_sid");!i||t-e>1800*1e3?(i=m(),sessionStorage.setItem("sz_wa_sid",i),this.determineReferrer(!0)):this.determineReferrer(!1),localStorage.setItem("sz_wa_last",t.toString())}determineReferrer(t){let e=document.referrer,i=!1;if(e)try{i=new URL(e).hostname!==window.location.hostname}catch{i=!0}if(i){let s=this.normalizeUrl(e);s!==sessionStorage.getItem("sz_wa_ref")&&sessionStorage.setItem("sz_wa_ref",s)}else t&&!sessionStorage.getItem("sz_wa_ref")&&sessionStorage.setItem("sz_wa_ref","Direct")}getIds(){return localStorage.setItem("sz_wa_last",Date.now().toString()),{visitorId:localStorage.getItem("sz_wa_vid")||"unknown",sessionId:sessionStorage.getItem("sz_wa_sid")||"unknown",referrer:sessionStorage.getItem("sz_wa_ref")||"Direct"}}trackPageView(){this.manageSession(),this.startTime=Date.now(),this.send({type:"pageview",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,title:document.title,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,referrer:this.getIds().referrer})}trackPing(){let t=Math.floor((Date.now()-this.startTime)/1e3);t>=1&&this.send({type:"ping",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,title:document.title,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,referrer:this.getIds().referrer,duration:t})}send(t){navigator.sendBeacon?navigator.sendBeacon(this.endpoint,new Blob([JSON.stringify(t)],{type:"application/json"}))||this.fallbackSend(t):this.fallbackSend(t)}fallbackSend(t){fetch(this.endpoint,{method:"POST",body:JSON.stringify(t),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(()=>{})}setupListeners(){let t=history.pushState;history.pushState=(...e)=>{this.trackPing(),t.apply(history,e),this.trackPageView()},window.addEventListener("popstate",()=>{this.trackPing(),this.trackPageView()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?this.trackPing():(this.startTime=Date.now(),this.manageSession())}),window.addEventListener("beforeunload",()=>this.trackPing())}},w=class{config={apiKey:"",sampleRate:1,allowedOrigins:[]};endpoint="https://api.senzor.dev/api/ingest/rum";initialized=!1;isSampled=!0;sessionId="";traceId="";traceStartTime=0;isInitialLoad=!0;spans=[];errors=[];breadcrumbs=[];vitals={};frustrations={rageClicks:0,deadClicks:0,errorCount:0};clickHistory=[];flushInterval;init(t){if(!this.initialized){if(this.initialized=!0,this.config={...this.config,...t},t.endpoint&&(this.endpoint=t.endpoint),!this.config.apiKey){console.error("[Senzor RUM] apiKey is required.");return}this.isSampled=Math.random()<=(this.config.sampleRate??1),this.manageSession(),this.startNewTrace(!0),this.setupErrorListeners(),this.setupPerformanceObservers(),this.setupUXListeners(),this.isSampled&&this.patchNetwork(),this.flushInterval=setInterval(()=>this.flush(),1e4),this.setupRoutingListeners()}}manageSession(){sessionStorage.getItem("sz_rum_sid")||sessionStorage.setItem("sz_rum_sid",m()),this.sessionId=sessionStorage.getItem("sz_rum_sid")}startNewTrace(t){this.traceId=f(32),this.traceStartTime=Date.now(),this.isInitialLoad=t,this.spans=[],this.vitals={},this.frustrations={rageClicks:0,deadClicks:0,errorCount:0}}addBreadcrumb(t,e){this.breadcrumbs.push({type:t,message:e,time:Date.now()}),this.breadcrumbs.length>15&&this.breadcrumbs.shift()}setupUXListeners(){document.addEventListener("click",t=>{let e=t.target,i=e.tagName?e.tagName.toLowerCase():"";this.addBreadcrumb("click",`Clicked ${i}${e.id?"#"+e.id:""}${e.className?"."+e.className.split(" ")[0]:""}`),["a","button","input","select","textarea","label"].includes(i)||e.closest("button")||e.closest("a")||e.hasAttribute("role")||e.onclick||this.frustrations.deadClicks++;let a=Date.now();if(this.clickHistory.push({x:t.clientX,y:t.clientY,time:a}),this.clickHistory=this.clickHistory.filter(o=>a-o.time<1e3),this.clickHistory.length>=3){let o=this.clickHistory[0],c=!0;for(let d=1;d<this.clickHistory.length;d++){let l=Math.abs(this.clickHistory[d].x-o.x),p=Math.abs(this.clickHistory[d].y-o.y);(l>50||p>50)&&(c=!1)}c&&(this.frustrations.rageClicks++,this.clickHistory=[])}},{capture:!0,passive:!0})}setupPerformanceObservers(){if(!(!this.isSampled||typeof PerformanceObserver>"u"))try{new PerformanceObserver(e=>{for(let i of e.getEntriesByName("first-contentful-paint"))this.vitals.fcp=i.startTime}).observe({type:"paint",buffered:!0}),new PerformanceObserver(e=>{let i=e.getEntries(),s=i[i.length-1];s&&(this.vitals.lcp=s.startTime)}).observe({type:"largest-contentful-paint",buffered:!0});let t=0;new PerformanceObserver(e=>{for(let i of e.getEntries())i.hadRecentInput||(t+=i.value,this.vitals.cls=t)}).observe({type:"layout-shift",buffered:!0}),new PerformanceObserver(e=>{for(let i of e.getEntries()){let s=i,n=s.duration||(s.processingStart&&s.startTime?s.processingStart-s.startTime:0);(!this.vitals.inp||n>this.vitals.inp)&&(this.vitals.inp=n)}}).observe({type:"event",buffered:!0,durationThreshold:40})}catch{}}getNavigationTimings(){if(typeof performance>"u")return{};let t=performance.getEntriesByType("navigation")[0];return t?{dns:Math.max(0,t.domainLookupEnd-t.domainLookupStart),tcp:Math.max(0,t.connectEnd-t.connectStart),ssl:t.secureConnectionStart?Math.max(0,t.requestStart-t.secureConnectionStart):0,ttfb:Math.max(0,t.responseStart-t.requestStart),domInteractive:Math.max(0,t.domInteractive-t.startTime),domComplete:Math.max(0,t.domComplete-t.startTime)}:{}}shouldAttachTraceHeader(t){if(!this.config.allowedOrigins||this.config.allowedOrigins.length===0)return!1;try{let e=new URL(t,window.location.origin);return this.config.allowedOrigins.some(i=>typeof i=="string"?e.origin.includes(i):i instanceof RegExp?i.test(e.origin):!1)}catch{return!1}}patchNetwork(){let t=this,e=XMLHttpRequest.prototype.open,i=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(n,a,...o){return this.__szMethod=n,this.__szUrl=a,e.apply(this,[n,a,...o])},XMLHttpRequest.prototype.send=function(n){let a=this,o=f(16),c=Date.now()-t.traceStartTime;return t.shouldAttachTraceHeader(a.__szUrl)&&a.setRequestHeader("traceparent",`00-${t.traceId}-${o}-01`),a.addEventListener("loadend",()=>{t.spans.push({spanId:o,name:new URL(a.__szUrl,window.location.origin).pathname,type:"xhr",method:a.__szMethod,status:a.status,startTime:c,duration:Date.now()-t.traceStartTime-c})}),i.call(this,n)};let s=window.fetch;window.fetch=async function(...n){var l,p;let a=typeof n[0]=="string"?n[0]:n[0].url,o=(((l=n[1])==null?void 0:l.method)||n[0].method||"GET").toUpperCase(),c=f(16),d=Date.now()-t.traceStartTime;if(t.shouldAttachTraceHeader(a)){let h=new Headers(((p=n[1])==null?void 0:p.headers)||n[0].headers||{});h.set("traceparent",`00-${t.traceId}-${c}-01`),n[1]?n[1].headers=h:n[0]instanceof Request&&(n[0]=new Request(n[0],{headers:h}))}try{let h=await s.apply(this,n);return t.spans.push({spanId:c,name:new URL(a,window.location.origin).pathname,type:"fetch",method:o,status:h.status,startTime:d,duration:Date.now()-t.traceStartTime-d}),h}catch(h){throw t.spans.push({spanId:c,name:new URL(a,window.location.origin).pathname,type:"fetch",method:o,status:0,startTime:d,duration:Date.now()-t.traceStartTime-d}),h}}}setupErrorListeners(){let t=(e,i)=>{this.frustrations.errorCount++;let s=e.message||String(e);this.errors.push({errorClass:e.name||"Error",message:s,stackTrace:e.stack||"",traceId:this.isSampled?this.traceId:void 0,context:{type:i,...v(),breadcrumbs:[...this.breadcrumbs]},timestamp:new Date().toISOString()}),this.flush()};window.addEventListener("error",e=>{e.error&&t(e.error,"Uncaught Exception")}),window.addEventListener("unhandledrejection",e=>{t(e.reason instanceof Error?e.reason:new Error(String(e.reason)),"Unhandled Promise Rejection")})}setupRoutingListeners(){let t=history.pushState;history.pushState=(...e)=>{this.flush(),t.apply(history,e),this.startNewTrace(!1),this.addBreadcrumb("navigation",window.location.pathname)},window.addEventListener("popstate",()=>{this.flush(),this.startNewTrace(!1),this.addBreadcrumb("navigation",window.location.pathname)}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()}),window.addEventListener("pagehide",()=>this.flush())}flush(){if(this.spans.length===0&&this.errors.length===0&&!this.isInitialLoad)return;let t={traces:[],errors:this.errors};if(this.isSampled&&t.traces.push({traceId:this.traceId,sessionId:this.sessionId,traceType:this.isInitialLoad?"initial_load":"route_change",path:window.location.pathname,referrer:document.referrer||"",vitals:{...this.vitals},timings:this.isInitialLoad?this.getNavigationTimings():{},frustration:{...this.frustrations},...v(),spans:[...this.spans],duration:Date.now()-this.traceStartTime,timestamp:new Date(this.traceStartTime).toISOString()}),this.spans=[],this.errors=[],this.frustrations={rageClicks:0,deadClicks:0,errorCount:0},this.isInitialLoad=!1,t.traces.length>0||t.errors.length>0){let e=new Blob([JSON.stringify(t)],{type:"application/json"});navigator.sendBeacon?navigator.sendBeacon(this.endpoint,e):fetch(this.endpoint,{method:"POST",body:e,keepalive:!0}).catch(()=>{})}}},y=new g,S=new w,b={init:r=>y.init(r),initRum:r=>S.init(r)};typeof window<"u"&&(window.Senzor=b);0&&(module.exports={Analytics,RUM,Senzor});
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function r(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{let t=Math.random()*16|0;return(n==="x"?t:t&3|8).toString(16)})}var i=class{config;startTime;endpoint;initialized;constructor(){this.config={webId:"",endpoint:"https://api.senzor.dev/api/ingest/web"},this.startTime=Date.now(),this.endpoint="",this.initialized=!1}init(t){if(this.initialized){console.warn("[Senzor] Agent already initialized.");return}if(this.initialized=!0,this.config={...this.config,...t},this.endpoint=this.config.endpoint||"https://api.senzor.dev/api/ingest/web",!this.config.webId){console.error("[Senzor] WebId is required.");return}this.checkSession(),this.trackPageView(),this.setupListeners()}checkSession(){let t=Date.now(),e=parseInt(localStorage.getItem("senzor_last_activity")||"0",10),o=1800*1e3;localStorage.getItem("senzor_vid")||localStorage.setItem("senzor_vid",r()),(!localStorage.getItem("senzor_sid")||t-e>o)&&localStorage.setItem("senzor_sid",r()),localStorage.setItem("senzor_last_activity",t.toString())}getIds(){return localStorage.setItem("senzor_last_activity",Date.now().toString()),{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:localStorage.getItem("senzor_sid")||"unknown"}}trackPageView(){this.checkSession(),this.startTime=Date.now();let t={type:"pageview",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone};this.send(t)}trackPing(){let t=Math.floor((Date.now()-this.startTime)/1e3);if(t<1)return;let e={type:"ping",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,duration:t};this.send(e)}send(t){if(navigator.sendBeacon){let e=new Blob([JSON.stringify(t)],{type:"application/json"});navigator.sendBeacon(this.endpoint,e)||this.fallbackSend(t)}else this.fallbackSend(t)}fallbackSend(t){fetch(this.endpoint,{method:"POST",body:JSON.stringify(t),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(e=>console.error("[Senzor] Telemetry Error:",e))}setupListeners(){let t=history.pushState;history.pushState=(...e)=>{this.trackPing(),t.apply(history,e),this.trackPageView()},window.addEventListener("popstate",()=>{this.trackPing(),this.trackPageView()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?this.trackPing():(this.startTime=Date.now(),this.checkSession())}),window.addEventListener("beforeunload",()=>{this.trackPing()})}},s=new i;typeof window<"u"&&(window.Senzor=s);export{s as Senzor};
|
|
1
|
+
function f(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,a=>{let t=Math.random()*16|0;return(a==="x"?t:t&3|8).toString(16)})}function u(a){let t="";for(;t.length<a;)t+=Math.random().toString(16).slice(2);return t.slice(0,a)}var w=()=>{var a;return{userAgent:navigator.userAgent,url:window.location.href,deviceMemory:navigator.deviceMemory||void 0,connectionType:((a=navigator.connection)==null?void 0:a.effectiveType)||void 0}},m=class{config={webId:""};startTime=Date.now();endpoint="https://api.senzor.dev/api/ingest/web";initialized=!1;init(t){if(!this.initialized){if(this.initialized=!0,this.config={...this.config,...t},t.endpoint&&(this.endpoint=t.endpoint),!this.config.webId){console.error("[Senzor] webId is required for Analytics.");return}this.manageSession(),this.trackPageView(),this.setupListeners()}}normalizeUrl(t){return t?t.replace(/^https?:\/\//,""):""}manageSession(){let t=Date.now(),e=parseInt(localStorage.getItem("sz_wa_last")||"0",10);localStorage.getItem("sz_wa_vid")||localStorage.setItem("sz_wa_vid",f());let i=sessionStorage.getItem("sz_wa_sid");!i||t-e>1800*1e3?(i=f(),sessionStorage.setItem("sz_wa_sid",i),this.determineReferrer(!0)):this.determineReferrer(!1),localStorage.setItem("sz_wa_last",t.toString())}determineReferrer(t){let e=document.referrer,i=!1;if(e)try{i=new URL(e).hostname!==window.location.hostname}catch{i=!0}if(i){let n=this.normalizeUrl(e);n!==sessionStorage.getItem("sz_wa_ref")&&sessionStorage.setItem("sz_wa_ref",n)}else t&&!sessionStorage.getItem("sz_wa_ref")&&sessionStorage.setItem("sz_wa_ref","Direct")}getIds(){return localStorage.setItem("sz_wa_last",Date.now().toString()),{visitorId:localStorage.getItem("sz_wa_vid")||"unknown",sessionId:sessionStorage.getItem("sz_wa_sid")||"unknown",referrer:sessionStorage.getItem("sz_wa_ref")||"Direct"}}trackPageView(){this.manageSession(),this.startTime=Date.now(),this.send({type:"pageview",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,title:document.title,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,referrer:this.getIds().referrer})}trackPing(){let t=Math.floor((Date.now()-this.startTime)/1e3);t>=1&&this.send({type:"ping",webId:this.config.webId,...this.getIds(),url:window.location.href,path:window.location.pathname,title:document.title,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,referrer:this.getIds().referrer,duration:t})}send(t){navigator.sendBeacon?navigator.sendBeacon(this.endpoint,new Blob([JSON.stringify(t)],{type:"application/json"}))||this.fallbackSend(t):this.fallbackSend(t)}fallbackSend(t){fetch(this.endpoint,{method:"POST",body:JSON.stringify(t),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(()=>{})}setupListeners(){let t=history.pushState;history.pushState=(...e)=>{this.trackPing(),t.apply(history,e),this.trackPageView()},window.addEventListener("popstate",()=>{this.trackPing(),this.trackPageView()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?this.trackPing():(this.startTime=Date.now(),this.manageSession())}),window.addEventListener("beforeunload",()=>this.trackPing())}},g=class{config={apiKey:"",sampleRate:1,allowedOrigins:[]};endpoint="https://api.senzor.dev/api/ingest/rum";initialized=!1;isSampled=!0;sessionId="";traceId="";traceStartTime=0;isInitialLoad=!0;spans=[];errors=[];breadcrumbs=[];vitals={};frustrations={rageClicks:0,deadClicks:0,errorCount:0};clickHistory=[];flushInterval;init(t){if(!this.initialized){if(this.initialized=!0,this.config={...this.config,...t},t.endpoint&&(this.endpoint=t.endpoint),!this.config.apiKey){console.error("[Senzor RUM] apiKey is required.");return}this.isSampled=Math.random()<=(this.config.sampleRate??1),this.manageSession(),this.startNewTrace(!0),this.setupErrorListeners(),this.setupPerformanceObservers(),this.setupUXListeners(),this.isSampled&&this.patchNetwork(),this.flushInterval=setInterval(()=>this.flush(),1e4),this.setupRoutingListeners()}}manageSession(){sessionStorage.getItem("sz_rum_sid")||sessionStorage.setItem("sz_rum_sid",f()),this.sessionId=sessionStorage.getItem("sz_rum_sid")}startNewTrace(t){this.traceId=u(32),this.traceStartTime=Date.now(),this.isInitialLoad=t,this.spans=[],this.vitals={},this.frustrations={rageClicks:0,deadClicks:0,errorCount:0}}addBreadcrumb(t,e){this.breadcrumbs.push({type:t,message:e,time:Date.now()}),this.breadcrumbs.length>15&&this.breadcrumbs.shift()}setupUXListeners(){document.addEventListener("click",t=>{let e=t.target,i=e.tagName?e.tagName.toLowerCase():"";this.addBreadcrumb("click",`Clicked ${i}${e.id?"#"+e.id:""}${e.className?"."+e.className.split(" ")[0]:""}`),["a","button","input","select","textarea","label"].includes(i)||e.closest("button")||e.closest("a")||e.hasAttribute("role")||e.onclick||this.frustrations.deadClicks++;let r=Date.now();if(this.clickHistory.push({x:t.clientX,y:t.clientY,time:r}),this.clickHistory=this.clickHistory.filter(o=>r-o.time<1e3),this.clickHistory.length>=3){let o=this.clickHistory[0],c=!0;for(let d=1;d<this.clickHistory.length;d++){let l=Math.abs(this.clickHistory[d].x-o.x),p=Math.abs(this.clickHistory[d].y-o.y);(l>50||p>50)&&(c=!1)}c&&(this.frustrations.rageClicks++,this.clickHistory=[])}},{capture:!0,passive:!0})}setupPerformanceObservers(){if(!(!this.isSampled||typeof PerformanceObserver>"u"))try{new PerformanceObserver(e=>{for(let i of e.getEntriesByName("first-contentful-paint"))this.vitals.fcp=i.startTime}).observe({type:"paint",buffered:!0}),new PerformanceObserver(e=>{let i=e.getEntries(),n=i[i.length-1];n&&(this.vitals.lcp=n.startTime)}).observe({type:"largest-contentful-paint",buffered:!0});let t=0;new PerformanceObserver(e=>{for(let i of e.getEntries())i.hadRecentInput||(t+=i.value,this.vitals.cls=t)}).observe({type:"layout-shift",buffered:!0}),new PerformanceObserver(e=>{for(let i of e.getEntries()){let n=i,s=n.duration||(n.processingStart&&n.startTime?n.processingStart-n.startTime:0);(!this.vitals.inp||s>this.vitals.inp)&&(this.vitals.inp=s)}}).observe({type:"event",buffered:!0,durationThreshold:40})}catch{}}getNavigationTimings(){if(typeof performance>"u")return{};let t=performance.getEntriesByType("navigation")[0];return t?{dns:Math.max(0,t.domainLookupEnd-t.domainLookupStart),tcp:Math.max(0,t.connectEnd-t.connectStart),ssl:t.secureConnectionStart?Math.max(0,t.requestStart-t.secureConnectionStart):0,ttfb:Math.max(0,t.responseStart-t.requestStart),domInteractive:Math.max(0,t.domInteractive-t.startTime),domComplete:Math.max(0,t.domComplete-t.startTime)}:{}}shouldAttachTraceHeader(t){if(!this.config.allowedOrigins||this.config.allowedOrigins.length===0)return!1;try{let e=new URL(t,window.location.origin);return this.config.allowedOrigins.some(i=>typeof i=="string"?e.origin.includes(i):i instanceof RegExp?i.test(e.origin):!1)}catch{return!1}}patchNetwork(){let t=this,e=XMLHttpRequest.prototype.open,i=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(s,r,...o){return this.__szMethod=s,this.__szUrl=r,e.apply(this,[s,r,...o])},XMLHttpRequest.prototype.send=function(s){let r=this,o=u(16),c=Date.now()-t.traceStartTime;return t.shouldAttachTraceHeader(r.__szUrl)&&r.setRequestHeader("traceparent",`00-${t.traceId}-${o}-01`),r.addEventListener("loadend",()=>{t.spans.push({spanId:o,name:new URL(r.__szUrl,window.location.origin).pathname,type:"xhr",method:r.__szMethod,status:r.status,startTime:c,duration:Date.now()-t.traceStartTime-c})}),i.call(this,s)};let n=window.fetch;window.fetch=async function(...s){var l,p;let r=typeof s[0]=="string"?s[0]:s[0].url,o=(((l=s[1])==null?void 0:l.method)||s[0].method||"GET").toUpperCase(),c=u(16),d=Date.now()-t.traceStartTime;if(t.shouldAttachTraceHeader(r)){let h=new Headers(((p=s[1])==null?void 0:p.headers)||s[0].headers||{});h.set("traceparent",`00-${t.traceId}-${c}-01`),s[1]?s[1].headers=h:s[0]instanceof Request&&(s[0]=new Request(s[0],{headers:h}))}try{let h=await n.apply(this,s);return t.spans.push({spanId:c,name:new URL(r,window.location.origin).pathname,type:"fetch",method:o,status:h.status,startTime:d,duration:Date.now()-t.traceStartTime-d}),h}catch(h){throw t.spans.push({spanId:c,name:new URL(r,window.location.origin).pathname,type:"fetch",method:o,status:0,startTime:d,duration:Date.now()-t.traceStartTime-d}),h}}}setupErrorListeners(){let t=(e,i)=>{this.frustrations.errorCount++;let n=e.message||String(e);this.errors.push({errorClass:e.name||"Error",message:n,stackTrace:e.stack||"",traceId:this.isSampled?this.traceId:void 0,context:{type:i,...w(),breadcrumbs:[...this.breadcrumbs]},timestamp:new Date().toISOString()}),this.flush()};window.addEventListener("error",e=>{e.error&&t(e.error,"Uncaught Exception")}),window.addEventListener("unhandledrejection",e=>{t(e.reason instanceof Error?e.reason:new Error(String(e.reason)),"Unhandled Promise Rejection")})}setupRoutingListeners(){let t=history.pushState;history.pushState=(...e)=>{this.flush(),t.apply(history,e),this.startNewTrace(!1),this.addBreadcrumb("navigation",window.location.pathname)},window.addEventListener("popstate",()=>{this.flush(),this.startNewTrace(!1),this.addBreadcrumb("navigation",window.location.pathname)}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()}),window.addEventListener("pagehide",()=>this.flush())}flush(){if(this.spans.length===0&&this.errors.length===0&&!this.isInitialLoad)return;let t={traces:[],errors:this.errors};if(this.isSampled&&t.traces.push({traceId:this.traceId,sessionId:this.sessionId,traceType:this.isInitialLoad?"initial_load":"route_change",path:window.location.pathname,referrer:document.referrer||"",vitals:{...this.vitals},timings:this.isInitialLoad?this.getNavigationTimings():{},frustration:{...this.frustrations},...w(),spans:[...this.spans],duration:Date.now()-this.traceStartTime,timestamp:new Date(this.traceStartTime).toISOString()}),this.spans=[],this.errors=[],this.frustrations={rageClicks:0,deadClicks:0,errorCount:0},this.isInitialLoad=!1,t.traces.length>0||t.errors.length>0){let e=new Blob([JSON.stringify(t)],{type:"application/json"});navigator.sendBeacon?navigator.sendBeacon(this.endpoint,e):fetch(this.endpoint,{method:"POST",body:e,keepalive:!0}).catch(()=>{})}}},v=new m,y=new g,S={init:a=>v.init(a),initRum:a=>y.init(a)};typeof window<"u"&&(window.Senzor=S);export{v as Analytics,y as RUM,S as Senzor};
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
interface Payload {
|
|
7
|
-
type: 'pageview' | 'ping';
|
|
8
|
-
webId: string;
|
|
9
|
-
visitorId: string;
|
|
10
|
-
sessionId: string;
|
|
11
|
-
url: string;
|
|
12
|
-
path: string;
|
|
13
|
-
referrer: string;
|
|
14
|
-
width: number;
|
|
15
|
-
timezone: string;
|
|
16
|
-
duration?: number;
|
|
17
|
-
}
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// --- SHARED UTILITIES ---
|
|
3
|
+
// ============================================================================
|
|
18
4
|
|
|
19
|
-
//
|
|
5
|
+
// Native UUID Generator (No dependencies)
|
|
20
6
|
function generateUUID(): string {
|
|
21
7
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
22
8
|
return crypto.randomUUID();
|
|
@@ -28,164 +14,500 @@ function generateUUID(): string {
|
|
|
28
14
|
});
|
|
29
15
|
}
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
constructor() {
|
|
38
|
-
this.config = { webId: '', endpoint: 'https://api.senzor.dev/api/ingest/web' };
|
|
39
|
-
this.startTime = Date.now();
|
|
40
|
-
this.endpoint = '';
|
|
41
|
-
this.initialized = false;
|
|
17
|
+
// W3C Trace & Span ID Generators (Hex strings)
|
|
18
|
+
function generateHex(length: number): string {
|
|
19
|
+
let result = '';
|
|
20
|
+
while (result.length < length) {
|
|
21
|
+
result += Math.random().toString(16).slice(2);
|
|
42
22
|
}
|
|
23
|
+
return result.slice(0, length);
|
|
24
|
+
}
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
26
|
+
const getBrowserContext = () => {
|
|
27
|
+
return {
|
|
28
|
+
userAgent: navigator.userAgent,
|
|
29
|
+
url: window.location.href, // This provides the URL dynamically
|
|
30
|
+
deviceMemory: (navigator as any).deviceMemory || undefined,
|
|
31
|
+
connectionType: (navigator as any).connection?.effectiveType || undefined
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// --- WEB ANALYTICS (MARKETING) MODULE ---
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
interface AnalyticsConfig {
|
|
40
|
+
webId: string;
|
|
41
|
+
endpoint?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class SenzorAnalyticsAgent {
|
|
45
|
+
private config: AnalyticsConfig = { webId: '' };
|
|
46
|
+
private startTime: number = Date.now();
|
|
47
|
+
private endpoint: string = 'https://api.senzor.dev/api/ingest/web';
|
|
48
|
+
private initialized: boolean = false;
|
|
50
49
|
|
|
50
|
+
public init(config: AnalyticsConfig) {
|
|
51
|
+
if (this.initialized) return;
|
|
52
|
+
this.initialized = true;
|
|
51
53
|
this.config = { ...this.config, ...config };
|
|
52
|
-
this.endpoint =
|
|
54
|
+
if (config.endpoint) this.endpoint = config.endpoint;
|
|
53
55
|
|
|
54
56
|
if (!this.config.webId) {
|
|
55
|
-
console.error('[Senzor]
|
|
57
|
+
console.error('[Senzor] webId is required for Analytics.');
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
this.checkSession();
|
|
61
|
-
|
|
62
|
-
// 2. Track initial load
|
|
61
|
+
this.manageSession();
|
|
63
62
|
this.trackPageView();
|
|
64
|
-
|
|
65
|
-
// 3. Setup Listeners
|
|
66
63
|
this.setupListeners();
|
|
67
64
|
}
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
private normalizeUrl(url: string): string {
|
|
67
|
+
return url ? url.replace(/^https?:\/\//, '') : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private manageSession() {
|
|
72
71
|
const now = Date.now();
|
|
73
|
-
const lastActivity = parseInt(localStorage.getItem('
|
|
74
|
-
|
|
72
|
+
const lastActivity = parseInt(localStorage.getItem('sz_wa_last') || '0', 10);
|
|
73
|
+
if (!localStorage.getItem('sz_wa_vid')) localStorage.setItem('sz_wa_vid', generateUUID());
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
if (!
|
|
78
|
-
|
|
75
|
+
let sessionId = sessionStorage.getItem('sz_wa_sid');
|
|
76
|
+
if (!sessionId || (now - lastActivity > 30 * 60 * 1000)) {
|
|
77
|
+
sessionId = generateUUID();
|
|
78
|
+
sessionStorage.setItem('sz_wa_sid', sessionId);
|
|
79
|
+
this.determineReferrer(true);
|
|
80
|
+
} else {
|
|
81
|
+
this.determineReferrer(false);
|
|
79
82
|
}
|
|
83
|
+
localStorage.setItem('sz_wa_last', now.toString());
|
|
84
|
+
}
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
private determineReferrer(isNewSession: boolean) {
|
|
87
|
+
const rawReferrer = document.referrer;
|
|
88
|
+
let isExternal = false;
|
|
89
|
+
if (rawReferrer) {
|
|
90
|
+
try { isExternal = new URL(rawReferrer).hostname !== window.location.hostname; } catch (e) { isExternal = true; }
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
if (isExternal) {
|
|
94
|
+
const cleanRef = this.normalizeUrl(rawReferrer);
|
|
95
|
+
if (cleanRef !== sessionStorage.getItem('sz_wa_ref')) sessionStorage.setItem('sz_wa_ref', cleanRef);
|
|
96
|
+
} else if (isNewSession && !sessionStorage.getItem('sz_wa_ref')) {
|
|
97
|
+
sessionStorage.setItem('sz_wa_ref', 'Direct');
|
|
98
|
+
}
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
private getIds() {
|
|
92
|
-
|
|
93
|
-
localStorage.setItem('senzor_last_activity', Date.now().toString());
|
|
102
|
+
localStorage.setItem('sz_wa_last', Date.now().toString());
|
|
94
103
|
return {
|
|
95
|
-
visitorId: localStorage.getItem('
|
|
96
|
-
sessionId:
|
|
104
|
+
visitorId: localStorage.getItem('sz_wa_vid') || 'unknown',
|
|
105
|
+
sessionId: sessionStorage.getItem('sz_wa_sid') || 'unknown',
|
|
106
|
+
referrer: sessionStorage.getItem('sz_wa_ref') || 'Direct'
|
|
97
107
|
};
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
private trackPageView() {
|
|
101
|
-
|
|
102
|
-
this.checkSession();
|
|
111
|
+
this.manageSession();
|
|
103
112
|
this.startTime = Date.now();
|
|
104
|
-
|
|
105
|
-
const payload: Payload = {
|
|
106
|
-
type: 'pageview',
|
|
107
|
-
webId: this.config.webId,
|
|
108
|
-
...this.getIds(),
|
|
109
|
-
url: window.location.href,
|
|
110
|
-
path: window.location.pathname,
|
|
111
|
-
referrer: document.referrer,
|
|
112
|
-
width: window.innerWidth,
|
|
113
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
this.send(payload);
|
|
113
|
+
this.send({ type: 'pageview', webId: this.config.webId, ...this.getIds(), url: window.location.href, path: window.location.pathname, title: document.title, width: window.innerWidth, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, referrer: this.getIds().referrer });
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
private trackPing() {
|
|
120
117
|
const duration = Math.floor((Date.now() - this.startTime) / 1000);
|
|
121
|
-
if (duration
|
|
122
|
-
|
|
123
|
-
const payload: Payload = {
|
|
124
|
-
type: 'ping',
|
|
125
|
-
webId: this.config.webId,
|
|
126
|
-
...this.getIds(),
|
|
127
|
-
url: window.location.href,
|
|
128
|
-
path: window.location.pathname,
|
|
129
|
-
referrer: document.referrer,
|
|
130
|
-
width: window.innerWidth,
|
|
131
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
132
|
-
duration: duration
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
this.send(payload);
|
|
118
|
+
if (duration >= 1) this.send({ type: 'ping', webId: this.config.webId, ...this.getIds(), url: window.location.href, path: window.location.pathname, title: document.title, width: window.innerWidth, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, referrer: this.getIds().referrer, duration });
|
|
136
119
|
}
|
|
137
120
|
|
|
138
|
-
private send(data:
|
|
121
|
+
private send(data: any) {
|
|
139
122
|
if (navigator.sendBeacon) {
|
|
140
|
-
|
|
141
|
-
const success = navigator.sendBeacon(this.endpoint, blob);
|
|
142
|
-
if (!success) this.fallbackSend(data);
|
|
123
|
+
if (!navigator.sendBeacon(this.endpoint, new Blob([JSON.stringify(data)], { type: 'application/json' }))) this.fallbackSend(data);
|
|
143
124
|
} else {
|
|
144
125
|
this.fallbackSend(data);
|
|
145
126
|
}
|
|
146
127
|
}
|
|
147
128
|
|
|
148
|
-
private fallbackSend(data:
|
|
149
|
-
fetch(this.endpoint, {
|
|
150
|
-
method: 'POST',
|
|
151
|
-
body: JSON.stringify(data),
|
|
152
|
-
keepalive: true,
|
|
153
|
-
headers: { 'Content-Type': 'application/json' }
|
|
154
|
-
}).catch(err => console.error('[Senzor] Telemetry Error:', err));
|
|
129
|
+
private fallbackSend(data: any) {
|
|
130
|
+
fetch(this.endpoint, { method: 'POST', body: JSON.stringify(data), keepalive: true, headers: { 'Content-Type': 'application/json' } }).catch(() => { });
|
|
155
131
|
}
|
|
156
132
|
|
|
157
133
|
private setupListeners() {
|
|
158
|
-
|
|
134
|
+
const originalPushState = history.pushState;
|
|
135
|
+
history.pushState = (...args) => { this.trackPing(); originalPushState.apply(history, args); this.trackPageView(); };
|
|
136
|
+
window.addEventListener('popstate', () => { this.trackPing(); this.trackPageView(); });
|
|
137
|
+
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') this.trackPing(); else { this.startTime = Date.now(); this.manageSession(); } });
|
|
138
|
+
window.addEventListener('beforeunload', () => this.trackPing());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// --- RUM / WEB APM (ENGINEERING) MODULE ---
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
interface RumConfig {
|
|
148
|
+
apiKey: string;
|
|
149
|
+
endpoint?: string;
|
|
150
|
+
sampleRate?: number; // 0.0 to 1.0 (Defaults to 1.0)
|
|
151
|
+
allowedOrigins?: (string | RegExp)[]; // Origins allowed to receive W3C traceparent headers
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class SenzorRumAgent {
|
|
155
|
+
private config: RumConfig = { apiKey: '', sampleRate: 1.0, allowedOrigins: [] };
|
|
156
|
+
private endpoint: string = 'https://api.senzor.dev/api/ingest/rum';
|
|
157
|
+
private initialized: boolean = false;
|
|
158
|
+
private isSampled: boolean = true;
|
|
159
|
+
|
|
160
|
+
// State
|
|
161
|
+
private sessionId: string = '';
|
|
162
|
+
private traceId: string = '';
|
|
163
|
+
private traceStartTime: number = 0;
|
|
164
|
+
private isInitialLoad: boolean = true;
|
|
165
|
+
|
|
166
|
+
// Buffers
|
|
167
|
+
private spans: any[] = [];
|
|
168
|
+
private errors: any[] = [];
|
|
169
|
+
private breadcrumbs: any[] = [];
|
|
170
|
+
private vitals: any = {};
|
|
171
|
+
private frustrations = { rageClicks: 0, deadClicks: 0, errorCount: 0 };
|
|
172
|
+
private clickHistory: { x: number; y: number; time: number }[] = [];
|
|
173
|
+
|
|
174
|
+
// Intervals
|
|
175
|
+
private flushInterval: any;
|
|
176
|
+
|
|
177
|
+
public init(config: RumConfig) {
|
|
178
|
+
if (this.initialized) return;
|
|
179
|
+
this.initialized = true;
|
|
180
|
+
this.config = { ...this.config, ...config };
|
|
181
|
+
if (config.endpoint) this.endpoint = config.endpoint;
|
|
182
|
+
|
|
183
|
+
if (!this.config.apiKey) {
|
|
184
|
+
console.error('[Senzor RUM] apiKey is required.');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Determine Sampling (Errors are ALWAYS 100% sampled, only Traces drop)
|
|
189
|
+
this.isSampled = Math.random() <= (this.config.sampleRate ?? 1.0);
|
|
190
|
+
|
|
191
|
+
this.manageSession();
|
|
192
|
+
this.startNewTrace(true);
|
|
193
|
+
|
|
194
|
+
this.setupErrorListeners();
|
|
195
|
+
this.setupPerformanceObservers();
|
|
196
|
+
this.setupUXListeners();
|
|
197
|
+
if (this.isSampled) this.patchNetwork();
|
|
198
|
+
|
|
199
|
+
// Micro-batch flush every 10s
|
|
200
|
+
this.flushInterval = setInterval(() => this.flush(), 10000);
|
|
201
|
+
|
|
202
|
+
// SPA and Unload Listeners
|
|
203
|
+
this.setupRoutingListeners();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private manageSession() {
|
|
207
|
+
if (!sessionStorage.getItem('sz_rum_sid')) {
|
|
208
|
+
sessionStorage.setItem('sz_rum_sid', generateUUID());
|
|
209
|
+
}
|
|
210
|
+
this.sessionId = sessionStorage.getItem('sz_rum_sid') as string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private startNewTrace(isInitialLoad: boolean) {
|
|
214
|
+
this.traceId = generateHex(32); // W3C Standard Trace ID
|
|
215
|
+
this.traceStartTime = Date.now();
|
|
216
|
+
this.isInitialLoad = isInitialLoad;
|
|
217
|
+
this.spans = [];
|
|
218
|
+
this.vitals = {};
|
|
219
|
+
this.frustrations = { rageClicks: 0, deadClicks: 0, errorCount: 0 };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- Breadcrumbs (For Error Context) ---
|
|
223
|
+
private addBreadcrumb(type: string, message: string) {
|
|
224
|
+
this.breadcrumbs.push({ type, message, time: Date.now() });
|
|
225
|
+
if (this.breadcrumbs.length > 15) this.breadcrumbs.shift(); // Keep last 15 actions
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- 1. UX Frustration Detection ---
|
|
229
|
+
private setupUXListeners() {
|
|
230
|
+
document.addEventListener('click', (e) => {
|
|
231
|
+
const target = e.target as HTMLElement;
|
|
232
|
+
const tag = target.tagName ? target.tagName.toLowerCase() : '';
|
|
233
|
+
|
|
234
|
+
// Breadcrumb
|
|
235
|
+
this.addBreadcrumb('click', `Clicked ${tag}${target.id ? '#' + target.id : ''}${target.className ? '.' + target.className.split(' ')[0] : ''}`);
|
|
236
|
+
|
|
237
|
+
// Dead Click Heuristic (Clicked non-interactive element)
|
|
238
|
+
const interactiveElements = ['a', 'button', 'input', 'select', 'textarea', 'label'];
|
|
239
|
+
const isInteractive = interactiveElements.includes(tag) || target.closest('button') || target.closest('a') || target.hasAttribute('role') || target.onclick;
|
|
240
|
+
if (!isInteractive) {
|
|
241
|
+
this.frustrations.deadClicks++;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Rage Click Heuristic (>= 3 clicks within 50px radius in < 1 second)
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
this.clickHistory.push({ x: e.clientX, y: e.clientY, time: now });
|
|
247
|
+
|
|
248
|
+
// Clean old history
|
|
249
|
+
this.clickHistory = this.clickHistory.filter(c => now - c.time < 1000);
|
|
250
|
+
|
|
251
|
+
if (this.clickHistory.length >= 3) {
|
|
252
|
+
const first = this.clickHistory[0];
|
|
253
|
+
let isRage = true;
|
|
254
|
+
for (let i = 1; i < this.clickHistory.length; i++) {
|
|
255
|
+
const dx = Math.abs(this.clickHistory[i].x - first.x);
|
|
256
|
+
const dy = Math.abs(this.clickHistory[i].y - first.y);
|
|
257
|
+
if (dx > 50 || dy > 50) isRage = false;
|
|
258
|
+
}
|
|
259
|
+
if (isRage) {
|
|
260
|
+
this.frustrations.rageClicks++;
|
|
261
|
+
this.clickHistory = []; // Reset after registering rage click
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}, { capture: true, passive: true });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- 2. Google Core Web Vitals (Non-blocking) ---
|
|
268
|
+
private setupPerformanceObservers() {
|
|
269
|
+
if (!this.isSampled || typeof PerformanceObserver === 'undefined') return;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
// First Contentful Paint (FCP)
|
|
273
|
+
new PerformanceObserver((entryList) => {
|
|
274
|
+
for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
|
|
275
|
+
this.vitals.fcp = entry.startTime;
|
|
276
|
+
}
|
|
277
|
+
}).observe({ type: 'paint', buffered: true });
|
|
278
|
+
|
|
279
|
+
// Largest Contentful Paint (LCP)
|
|
280
|
+
new PerformanceObserver((entryList) => {
|
|
281
|
+
const entries = entryList.getEntries();
|
|
282
|
+
const lastEntry = entries[entries.length - 1];
|
|
283
|
+
if (lastEntry) this.vitals.lcp = lastEntry.startTime;
|
|
284
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
285
|
+
|
|
286
|
+
// Cumulative Layout Shift (CLS)
|
|
287
|
+
let clsScore = 0;
|
|
288
|
+
new PerformanceObserver((entryList) => {
|
|
289
|
+
for (const entry of entryList.getEntries()) {
|
|
290
|
+
if (!(entry as any).hadRecentInput) {
|
|
291
|
+
clsScore += (entry as any).value;
|
|
292
|
+
this.vitals.cls = clsScore;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
296
|
+
|
|
297
|
+
// Interaction to Next Paint (INP / FID fallback)
|
|
298
|
+
new PerformanceObserver((entryList) => {
|
|
299
|
+
for (const entry of entryList.getEntries()) {
|
|
300
|
+
const evt = entry as any; // Safely bypass TS base-class limits for PerformanceEventTiming
|
|
301
|
+
const delay = evt.duration || (evt.processingStart && evt.startTime ? evt.processingStart - evt.startTime : 0);
|
|
302
|
+
if (!this.vitals.inp || delay > this.vitals.inp) {
|
|
303
|
+
this.vitals.inp = delay;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}).observe({ type: 'event', buffered: true, durationThreshold: 40 } as any);
|
|
307
|
+
|
|
308
|
+
} catch (e) {
|
|
309
|
+
// Browser doesn't support specific observer type, degrade gracefully
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private getNavigationTimings() {
|
|
314
|
+
if (typeof performance === 'undefined') return {};
|
|
315
|
+
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
316
|
+
if (!nav) return {};
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
dns: Math.max(0, nav.domainLookupEnd - nav.domainLookupStart),
|
|
320
|
+
tcp: Math.max(0, nav.connectEnd - nav.connectStart),
|
|
321
|
+
ssl: nav.secureConnectionStart ? Math.max(0, nav.requestStart - nav.secureConnectionStart) : 0,
|
|
322
|
+
ttfb: Math.max(0, nav.responseStart - nav.requestStart),
|
|
323
|
+
domInteractive: Math.max(0, nav.domInteractive - nav.startTime),
|
|
324
|
+
domComplete: Math.max(0, nav.domComplete - nav.startTime),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// --- 3. Distributed Tracing (Patching) ---
|
|
329
|
+
private shouldAttachTraceHeader(url: string): boolean {
|
|
330
|
+
if (!this.config.allowedOrigins || this.config.allowedOrigins.length === 0) return false;
|
|
331
|
+
try {
|
|
332
|
+
const targetUrl = new URL(url, window.location.origin);
|
|
333
|
+
return this.config.allowedOrigins.some(allowed => {
|
|
334
|
+
if (typeof allowed === 'string') return targetUrl.origin.includes(allowed);
|
|
335
|
+
if (allowed instanceof RegExp) return allowed.test(targetUrl.origin);
|
|
336
|
+
return false;
|
|
337
|
+
});
|
|
338
|
+
} catch { return false; }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private patchNetwork() {
|
|
342
|
+
const self = this;
|
|
343
|
+
|
|
344
|
+
// Patch XHR
|
|
345
|
+
const originalXhrOpen = XMLHttpRequest.prototype.open;
|
|
346
|
+
const originalXhrSend = XMLHttpRequest.prototype.send;
|
|
347
|
+
|
|
348
|
+
XMLHttpRequest.prototype.open = function (method: string, url: string, ...rest: any[]) {
|
|
349
|
+
(this as any).__szMethod = method;
|
|
350
|
+
(this as any).__szUrl = url;
|
|
351
|
+
return originalXhrOpen.apply(this, [method, url, ...rest] as any);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {
|
|
355
|
+
const xhr = this as any;
|
|
356
|
+
const spanId = generateHex(16);
|
|
357
|
+
const startTime = Date.now() - self.traceStartTime;
|
|
358
|
+
|
|
359
|
+
if (self.shouldAttachTraceHeader(xhr.__szUrl)) {
|
|
360
|
+
xhr.setRequestHeader('traceparent', `00-${self.traceId}-${spanId}-01`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
xhr.addEventListener('loadend', () => {
|
|
364
|
+
self.spans.push({
|
|
365
|
+
spanId, name: new URL(xhr.__szUrl, window.location.origin).pathname,
|
|
366
|
+
type: 'xhr', method: xhr.__szMethod, status: xhr.status,
|
|
367
|
+
startTime, duration: (Date.now() - self.traceStartTime) - startTime
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return originalXhrSend.call(this, body);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Patch Fetch
|
|
375
|
+
const originalFetch = window.fetch;
|
|
376
|
+
window.fetch = async function (...args) {
|
|
377
|
+
const url = typeof args[0] === 'string' ? args[0] : (args[0] as Request).url;
|
|
378
|
+
const method = (args[1]?.method || (args[0] as Request).method || 'GET').toUpperCase();
|
|
379
|
+
|
|
380
|
+
const spanId = generateHex(16);
|
|
381
|
+
const startTime = Date.now() - self.traceStartTime;
|
|
382
|
+
|
|
383
|
+
if (self.shouldAttachTraceHeader(url)) {
|
|
384
|
+
const headers = new Headers(args[1]?.headers || (args[0] as Request).headers || {});
|
|
385
|
+
headers.set('traceparent', `00-${self.traceId}-${spanId}-01`);
|
|
386
|
+
if (args[1]) args[1].headers = headers;
|
|
387
|
+
else if (args[0] instanceof Request) args[0] = new Request(args[0], { headers });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const response = await originalFetch.apply(this, args);
|
|
392
|
+
self.spans.push({
|
|
393
|
+
spanId, name: new URL(url, window.location.origin).pathname,
|
|
394
|
+
type: 'fetch', method, status: response.status,
|
|
395
|
+
startTime, duration: (Date.now() - self.traceStartTime) - startTime
|
|
396
|
+
});
|
|
397
|
+
return response;
|
|
398
|
+
} catch (error) {
|
|
399
|
+
self.spans.push({
|
|
400
|
+
spanId, name: new URL(url, window.location.origin).pathname,
|
|
401
|
+
type: 'fetch', method, status: 0,
|
|
402
|
+
startTime, duration: (Date.now() - self.traceStartTime) - startTime
|
|
403
|
+
});
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// --- 4. Universal Error Engine Hooks ---
|
|
410
|
+
private setupErrorListeners() {
|
|
411
|
+
const handleGlobalError = (errorObj: Error, type: string) => {
|
|
412
|
+
this.frustrations.errorCount++;
|
|
413
|
+
const message = errorObj.message || String(errorObj);
|
|
414
|
+
|
|
415
|
+
this.errors.push({
|
|
416
|
+
errorClass: errorObj.name || 'Error',
|
|
417
|
+
message: message,
|
|
418
|
+
stackTrace: errorObj.stack || '',
|
|
419
|
+
traceId: this.isSampled ? this.traceId : undefined,
|
|
420
|
+
context: {
|
|
421
|
+
type,
|
|
422
|
+
...getBrowserContext(),
|
|
423
|
+
breadcrumbs: [...this.breadcrumbs] // Snapshot of actions leading up to crash
|
|
424
|
+
},
|
|
425
|
+
timestamp: new Date().toISOString()
|
|
426
|
+
});
|
|
427
|
+
this.flush(); // Flush immediately on error
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
window.addEventListener('error', (event) => {
|
|
431
|
+
if (event.error) handleGlobalError(event.error, 'Uncaught Exception');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
435
|
+
handleGlobalError(event.reason instanceof Error ? event.reason : new Error(String(event.reason)), 'Unhandled Promise Rejection');
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// --- 5. Lifecycle & Beaconing ---
|
|
440
|
+
private setupRoutingListeners() {
|
|
159
441
|
const originalPushState = history.pushState;
|
|
160
442
|
history.pushState = (...args) => {
|
|
161
|
-
this.
|
|
443
|
+
this.flush(); // Flush previous page view
|
|
162
444
|
originalPushState.apply(history, args);
|
|
163
|
-
this.
|
|
445
|
+
this.startNewTrace(false);
|
|
446
|
+
this.addBreadcrumb('navigation', window.location.pathname);
|
|
164
447
|
};
|
|
165
448
|
|
|
166
449
|
window.addEventListener('popstate', () => {
|
|
167
|
-
this.
|
|
168
|
-
this.
|
|
450
|
+
this.flush();
|
|
451
|
+
this.startNewTrace(false);
|
|
452
|
+
this.addBreadcrumb('navigation', window.location.pathname);
|
|
169
453
|
});
|
|
170
454
|
|
|
171
|
-
// Visibility & Unload
|
|
172
455
|
document.addEventListener('visibilitychange', () => {
|
|
173
|
-
if (document.visibilityState === 'hidden')
|
|
174
|
-
this.trackPing();
|
|
175
|
-
} else {
|
|
176
|
-
// User returned, restart timer (don't count background time)
|
|
177
|
-
this.startTime = Date.now();
|
|
178
|
-
this.checkSession(); // Verify session hasn't expired while tab was hidden
|
|
179
|
-
}
|
|
456
|
+
if (document.visibilityState === 'hidden') this.flush();
|
|
180
457
|
});
|
|
181
458
|
|
|
182
|
-
window.addEventListener('
|
|
183
|
-
|
|
184
|
-
|
|
459
|
+
window.addEventListener('pagehide', () => this.flush());
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private flush() {
|
|
463
|
+
if (this.spans.length === 0 && this.errors.length === 0 && !this.isInitialLoad) return;
|
|
464
|
+
|
|
465
|
+
const payload: any = { traces: [], errors: this.errors };
|
|
466
|
+
|
|
467
|
+
// Only send performance trace if sampled
|
|
468
|
+
if (this.isSampled) {
|
|
469
|
+
payload.traces.push({
|
|
470
|
+
traceId: this.traceId,
|
|
471
|
+
sessionId: this.sessionId,
|
|
472
|
+
traceType: this.isInitialLoad ? 'initial_load' : 'route_change',
|
|
473
|
+
path: window.location.pathname,
|
|
474
|
+
referrer: document.referrer || '',
|
|
475
|
+
vitals: { ...this.vitals },
|
|
476
|
+
timings: this.isInitialLoad ? this.getNavigationTimings() : {},
|
|
477
|
+
frustration: { ...this.frustrations },
|
|
478
|
+
...getBrowserContext(), // Injects URL, userAgent, connectionType, etc.
|
|
479
|
+
spans: [...this.spans],
|
|
480
|
+
duration: Date.now() - this.traceStartTime,
|
|
481
|
+
timestamp: new Date(this.traceStartTime).toISOString()
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Reset Buffers
|
|
486
|
+
this.spans = [];
|
|
487
|
+
this.errors = [];
|
|
488
|
+
this.frustrations = { rageClicks: 0, deadClicks: 0, errorCount: 0 };
|
|
489
|
+
this.isInitialLoad = false; // Next flush on same page is an update, not initial load
|
|
490
|
+
|
|
491
|
+
if (payload.traces.length > 0 || payload.errors.length > 0) {
|
|
492
|
+
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
|
493
|
+
if (navigator.sendBeacon) navigator.sendBeacon(this.endpoint, blob);
|
|
494
|
+
else fetch(this.endpoint, { method: 'POST', body: blob, keepalive: true }).catch(() => { });
|
|
495
|
+
}
|
|
185
496
|
}
|
|
186
497
|
}
|
|
187
498
|
|
|
188
|
-
|
|
499
|
+
// ============================================================================
|
|
500
|
+
// --- EXPORTS & INITIALIZATION ---
|
|
501
|
+
// ============================================================================
|
|
502
|
+
|
|
503
|
+
export const Analytics = new SenzorAnalyticsAgent();
|
|
504
|
+
export const RUM = new SenzorRumAgent();
|
|
505
|
+
|
|
506
|
+
// Maintain backwards compatibility for existing users
|
|
507
|
+
export const Senzor = {
|
|
508
|
+
init: (config: AnalyticsConfig) => Analytics.init(config),
|
|
509
|
+
initRum: (config: RumConfig) => RUM.init(config)
|
|
510
|
+
};
|
|
189
511
|
|
|
190
512
|
if (typeof window !== 'undefined') {
|
|
191
513
|
(window as any).Senzor = Senzor;
|