@senzops/web 1.2.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 -24
- package/dist/index.d.ts +63 -24
- 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 +423 -140
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,24 +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
|
-
private
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,24 +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
|
-
private
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 d(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,a=>{let e=Math.random()*16|0;return(a==="x"?e:e&3|8).toString(16)})}var o=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(e){if(this.initialized){console.warn("[Senzor] Agent already initialized.");return}if(this.initialized=!0,this.config={...this.config,...e},this.endpoint=this.config.endpoint||"https://api.senzor.dev/api/ingest/web",!this.config.webId){console.error("[Senzor] WebId is required.");return}this.manageSession(),this.trackPageView(),this.setupListeners()}normalizeUrl(e){return e?e.replace(/^https?:\/\//,""):""}manageSession(){let e=Date.now(),t=parseInt(localStorage.getItem("senzor_last_activity")||"0",10),r=1800*1e3;localStorage.getItem("senzor_vid")||localStorage.setItem("senzor_vid",d());let i=sessionStorage.getItem("senzor_sid"),n=e-t>r;!i||n?(i=d(),sessionStorage.setItem("senzor_sid",i),this.determineReferrer(!0)):this.determineReferrer(!1),localStorage.setItem("senzor_last_activity",e.toString())}determineReferrer(e){let t=document.referrer,r=window.location.hostname,i=sessionStorage.getItem("senzor_ref"),n=!1;if(t)try{new URL(t).hostname!==r&&(n=!0)}catch{n=!0}if(n){let s=this.normalizeUrl(t);s!==i&&sessionStorage.setItem("senzor_ref",s)}else e&&!i&&sessionStorage.setItem("senzor_ref","Direct")}getIds(){return localStorage.setItem("senzor_last_activity",Date.now().toString()),{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:sessionStorage.getItem("senzor_sid")||"unknown",referrer:sessionStorage.getItem("senzor_ref")||"Direct"}}trackPageView(){this.manageSession(),this.startTime=Date.now();let e={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};this.send(e)}trackPing(){let e=Math.floor((Date.now()-this.startTime)/1e3);if(e<1)return;let t={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:e};this.send(t)}send(e){if(navigator.sendBeacon){let t=new Blob([JSON.stringify(e)],{type:"application/json"});navigator.sendBeacon(this.endpoint,t)||this.fallbackSend(e)}else this.fallbackSend(e)}fallbackSend(e){fetch(this.endpoint,{method:"POST",body:JSON.stringify(e),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(t=>console.error("[Senzor] Telemetry Error:",t))}setupListeners(){let e=history.pushState;history.pushState=(...t)=>{this.trackPing(),e.apply(history,t),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()})}},c=new o;typeof window<"u"&&(window.Senzor=c);})();
|
|
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 a=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var f=(n,e)=>{for(var t in e)a(n,t,{get:e[t],enumerable:!0})},w=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of h(e))!p.call(n,i)&&i!==t&&a(n,i,{get:()=>e[i],enumerable:!(r=g(e,i))||r.enumerable});return n};var m=n=>w(a({},"__esModule",{value:!0}),n);var u={};f(u,{Senzor:()=>l});module.exports=m(u);function c(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{let e=Math.random()*16|0;return(n==="x"?e:e&3|8).toString(16)})}var d=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(e){if(this.initialized){console.warn("[Senzor] Agent already initialized.");return}if(this.initialized=!0,this.config={...this.config,...e},this.endpoint=this.config.endpoint||"https://api.senzor.dev/api/ingest/web",!this.config.webId){console.error("[Senzor] WebId is required.");return}this.manageSession(),this.trackPageView(),this.setupListeners()}normalizeUrl(e){return e?e.replace(/^https?:\/\//,""):""}manageSession(){let e=Date.now(),t=parseInt(localStorage.getItem("senzor_last_activity")||"0",10),r=1800*1e3;localStorage.getItem("senzor_vid")||localStorage.setItem("senzor_vid",c());let i=sessionStorage.getItem("senzor_sid"),s=e-t>r;!i||s?(i=c(),sessionStorage.setItem("senzor_sid",i),this.determineReferrer(!0)):this.determineReferrer(!1),localStorage.setItem("senzor_last_activity",e.toString())}determineReferrer(e){let t=document.referrer,r=window.location.hostname,i=sessionStorage.getItem("senzor_ref"),s=!1;if(t)try{new URL(t).hostname!==r&&(s=!0)}catch{s=!0}if(s){let o=this.normalizeUrl(t);o!==i&&sessionStorage.setItem("senzor_ref",o)}else e&&!i&&sessionStorage.setItem("senzor_ref","Direct")}getIds(){return localStorage.setItem("senzor_last_activity",Date.now().toString()),{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:sessionStorage.getItem("senzor_sid")||"unknown",referrer:sessionStorage.getItem("senzor_ref")||"Direct"}}trackPageView(){this.manageSession(),this.startTime=Date.now();let e={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};this.send(e)}trackPing(){let e=Math.floor((Date.now()-this.startTime)/1e3);if(e<1)return;let t={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:e};this.send(t)}send(e){if(navigator.sendBeacon){let t=new Blob([JSON.stringify(e)],{type:"application/json"});navigator.sendBeacon(this.endpoint,t)||this.fallbackSend(e)}else this.fallbackSend(e)}fallbackSend(e){fetch(this.endpoint,{method:"POST",body:JSON.stringify(e),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(t=>console.error("[Senzor] Telemetry Error:",t))}setupListeners(){let e=history.pushState;history.pushState=(...t)=>{this.trackPing(),e.apply(history,t),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()})}},l=new d;typeof window<"u"&&(window.Senzor=l);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 d(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,a=>{let e=Math.random()*16|0;return(a==="x"?e:e&3|8).toString(16)})}var o=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(e){if(this.initialized){console.warn("[Senzor] Agent already initialized.");return}if(this.initialized=!0,this.config={...this.config,...e},this.endpoint=this.config.endpoint||"https://api.senzor.dev/api/ingest/web",!this.config.webId){console.error("[Senzor] WebId is required.");return}this.manageSession(),this.trackPageView(),this.setupListeners()}normalizeUrl(e){return e?e.replace(/^https?:\/\//,""):""}manageSession(){let e=Date.now(),t=parseInt(localStorage.getItem("senzor_last_activity")||"0",10),r=1800*1e3;localStorage.getItem("senzor_vid")||localStorage.setItem("senzor_vid",d());let i=sessionStorage.getItem("senzor_sid"),n=e-t>r;!i||n?(i=d(),sessionStorage.setItem("senzor_sid",i),this.determineReferrer(!0)):this.determineReferrer(!1),localStorage.setItem("senzor_last_activity",e.toString())}determineReferrer(e){let t=document.referrer,r=window.location.hostname,i=sessionStorage.getItem("senzor_ref"),n=!1;if(t)try{new URL(t).hostname!==r&&(n=!0)}catch{n=!0}if(n){let s=this.normalizeUrl(t);s!==i&&sessionStorage.setItem("senzor_ref",s)}else e&&!i&&sessionStorage.setItem("senzor_ref","Direct")}getIds(){return localStorage.setItem("senzor_last_activity",Date.now().toString()),{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:sessionStorage.getItem("senzor_sid")||"unknown",referrer:sessionStorage.getItem("senzor_ref")||"Direct"}}trackPageView(){this.manageSession(),this.startTime=Date.now();let e={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};this.send(e)}trackPing(){let e=Math.floor((Date.now()-this.startTime)/1e3);if(e<1)return;let t={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:e};this.send(t)}send(e){if(navigator.sendBeacon){let t=new Blob([JSON.stringify(e)],{type:"application/json"});navigator.sendBeacon(this.endpoint,t)||this.fallbackSend(e)}else this.fallbackSend(e)}fallbackSend(e){fetch(this.endpoint,{method:"POST",body:JSON.stringify(e),keepalive:!0,headers:{"Content-Type":"application/json"}}).catch(t=>console.error("[Senzor] Telemetry Error:",t))}setupListeners(){let e=history.pushState;history.pushState=(...t)=>{this.trackPing(),e.apply(history,t),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()})}},c=new o;typeof window<"u"&&(window.Senzor=c);export{c 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,23 +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
|
-
title: string;
|
|
14
|
-
referrer: string;
|
|
15
|
-
width: number;
|
|
16
|
-
timezone: string;
|
|
17
|
-
duration?: number;
|
|
18
|
-
}
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// --- SHARED UTILITIES ---
|
|
3
|
+
// ============================================================================
|
|
19
4
|
|
|
20
|
-
//
|
|
5
|
+
// Native UUID Generator (No dependencies)
|
|
21
6
|
function generateUUID(): string {
|
|
22
7
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
23
8
|
return crypto.randomUUID();
|
|
@@ -29,31 +14,47 @@ function generateUUID(): string {
|
|
|
29
14
|
});
|
|
30
15
|
}
|
|
31
16
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
constructor() {
|
|
39
|
-
this.config = { webId: '', endpoint: 'https://api.senzor.dev/api/ingest/web' };
|
|
40
|
-
this.startTime = Date.now();
|
|
41
|
-
this.endpoint = '';
|
|
42
|
-
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);
|
|
43
22
|
}
|
|
23
|
+
return result.slice(0, length);
|
|
24
|
+
}
|
|
44
25
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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;
|
|
51
49
|
|
|
50
|
+
public init(config: AnalyticsConfig) {
|
|
51
|
+
if (this.initialized) return;
|
|
52
|
+
this.initialized = true;
|
|
52
53
|
this.config = { ...this.config, ...config };
|
|
53
|
-
this.endpoint =
|
|
54
|
+
if (config.endpoint) this.endpoint = config.endpoint;
|
|
54
55
|
|
|
55
56
|
if (!this.config.webId) {
|
|
56
|
-
console.error('[Senzor]
|
|
57
|
+
console.error('[Senzor] webId is required for Analytics.');
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -62,169 +63,451 @@ class SenzorWebAgent {
|
|
|
62
63
|
this.setupListeners();
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
// Helper to normalize referrer (strip protocol)
|
|
66
66
|
private normalizeUrl(url: string): string {
|
|
67
|
-
|
|
68
|
-
return url.replace(/^https?:\/\//, '');
|
|
67
|
+
return url ? url.replace(/^https?:\/\//, '') : '';
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
private manageSession() {
|
|
72
71
|
const now = Date.now();
|
|
73
|
-
const lastActivity = parseInt(localStorage.getItem('
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Visitor ID (Persistent 1 Year)
|
|
77
|
-
if (!localStorage.getItem('senzor_vid')) {
|
|
78
|
-
localStorage.setItem('senzor_vid', generateUUID());
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Session ID
|
|
82
|
-
let sessionId = sessionStorage.getItem('senzor_sid');
|
|
83
|
-
const isExpired = (now - lastActivity > sessionTimeout);
|
|
72
|
+
const lastActivity = parseInt(localStorage.getItem('sz_wa_last') || '0', 10);
|
|
73
|
+
if (!localStorage.getItem('sz_wa_vid')) localStorage.setItem('sz_wa_vid', generateUUID());
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
if (!sessionId ||
|
|
75
|
+
let sessionId = sessionStorage.getItem('sz_wa_sid');
|
|
76
|
+
if (!sessionId || (now - lastActivity > 30 * 60 * 1000)) {
|
|
87
77
|
sessionId = generateUUID();
|
|
88
|
-
sessionStorage.setItem('
|
|
78
|
+
sessionStorage.setItem('sz_wa_sid', sessionId);
|
|
89
79
|
this.determineReferrer(true);
|
|
90
80
|
} else {
|
|
91
|
-
// Ongoing session: Check if external source changed
|
|
92
81
|
this.determineReferrer(false);
|
|
93
82
|
}
|
|
94
|
-
|
|
95
|
-
localStorage.setItem('senzor_last_activity', now.toString());
|
|
83
|
+
localStorage.setItem('sz_wa_last', now.toString());
|
|
96
84
|
}
|
|
97
85
|
|
|
98
86
|
private determineReferrer(isNewSession: boolean) {
|
|
99
87
|
const rawReferrer = document.referrer;
|
|
100
|
-
const currentHost = window.location.hostname;
|
|
101
|
-
let storedReferrer = sessionStorage.getItem('senzor_ref');
|
|
102
|
-
|
|
103
88
|
let isExternal = false;
|
|
104
89
|
if (rawReferrer) {
|
|
105
|
-
try {
|
|
106
|
-
const refUrl = new URL(rawReferrer);
|
|
107
|
-
// Compare hosts
|
|
108
|
-
if (refUrl.hostname !== currentHost) {
|
|
109
|
-
isExternal = true;
|
|
110
|
-
}
|
|
111
|
-
} catch (e) {
|
|
112
|
-
isExternal = true;
|
|
113
|
-
}
|
|
90
|
+
try { isExternal = new URL(rawReferrer).hostname !== window.location.hostname; } catch (e) { isExternal = true; }
|
|
114
91
|
}
|
|
115
92
|
|
|
116
93
|
if (isExternal) {
|
|
117
|
-
// Always overwrite if it's a new external source
|
|
118
94
|
const cleanRef = this.normalizeUrl(rawReferrer);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
} else if (isNewSession && !storedReferrer) {
|
|
124
|
-
// New session with internal/no referrer = Direct
|
|
125
|
-
sessionStorage.setItem('senzor_ref', 'Direct');
|
|
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');
|
|
126
98
|
}
|
|
127
99
|
}
|
|
128
100
|
|
|
129
101
|
private getIds() {
|
|
130
|
-
localStorage.setItem('
|
|
102
|
+
localStorage.setItem('sz_wa_last', Date.now().toString());
|
|
131
103
|
return {
|
|
132
|
-
visitorId: localStorage.getItem('
|
|
133
|
-
sessionId: sessionStorage.getItem('
|
|
134
|
-
referrer: sessionStorage.getItem('
|
|
104
|
+
visitorId: localStorage.getItem('sz_wa_vid') || 'unknown',
|
|
105
|
+
sessionId: sessionStorage.getItem('sz_wa_sid') || 'unknown',
|
|
106
|
+
referrer: sessionStorage.getItem('sz_wa_ref') || 'Direct'
|
|
135
107
|
};
|
|
136
108
|
}
|
|
137
109
|
|
|
138
110
|
private trackPageView() {
|
|
139
111
|
this.manageSession();
|
|
140
112
|
this.startTime = Date.now();
|
|
141
|
-
|
|
142
|
-
const payload: Payload = {
|
|
143
|
-
type: 'pageview',
|
|
144
|
-
webId: this.config.webId,
|
|
145
|
-
...this.getIds(),
|
|
146
|
-
url: window.location.href,
|
|
147
|
-
path: window.location.pathname,
|
|
148
|
-
title: document.title,
|
|
149
|
-
width: window.innerWidth,
|
|
150
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
151
|
-
referrer: this.getIds().referrer
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
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 });
|
|
155
114
|
}
|
|
156
115
|
|
|
157
116
|
private trackPing() {
|
|
158
117
|
const duration = Math.floor((Date.now() - this.startTime) / 1000);
|
|
159
|
-
if (duration
|
|
160
|
-
|
|
161
|
-
const payload: Payload = {
|
|
162
|
-
type: 'ping',
|
|
163
|
-
webId: this.config.webId,
|
|
164
|
-
...this.getIds(),
|
|
165
|
-
url: window.location.href,
|
|
166
|
-
path: window.location.pathname,
|
|
167
|
-
title: document.title,
|
|
168
|
-
width: window.innerWidth,
|
|
169
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
170
|
-
referrer: this.getIds().referrer,
|
|
171
|
-
duration: duration
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
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 });
|
|
175
119
|
}
|
|
176
120
|
|
|
177
|
-
private send(data:
|
|
121
|
+
private send(data: any) {
|
|
178
122
|
if (navigator.sendBeacon) {
|
|
179
|
-
|
|
180
|
-
const success = navigator.sendBeacon(this.endpoint, blob);
|
|
181
|
-
if (!success) this.fallbackSend(data);
|
|
123
|
+
if (!navigator.sendBeacon(this.endpoint, new Blob([JSON.stringify(data)], { type: 'application/json' }))) this.fallbackSend(data);
|
|
182
124
|
} else {
|
|
183
125
|
this.fallbackSend(data);
|
|
184
126
|
}
|
|
185
127
|
}
|
|
186
128
|
|
|
187
|
-
private fallbackSend(data:
|
|
188
|
-
fetch(this.endpoint, {
|
|
189
|
-
method: 'POST',
|
|
190
|
-
body: JSON.stringify(data),
|
|
191
|
-
keepalive: true,
|
|
192
|
-
headers: { 'Content-Type': 'application/json' }
|
|
193
|
-
}).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(() => { });
|
|
194
131
|
}
|
|
195
132
|
|
|
196
133
|
private setupListeners() {
|
|
197
|
-
|
|
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() {
|
|
198
441
|
const originalPushState = history.pushState;
|
|
199
442
|
history.pushState = (...args) => {
|
|
200
|
-
this.
|
|
443
|
+
this.flush(); // Flush previous page view
|
|
201
444
|
originalPushState.apply(history, args);
|
|
202
|
-
this.
|
|
445
|
+
this.startNewTrace(false);
|
|
446
|
+
this.addBreadcrumb('navigation', window.location.pathname);
|
|
203
447
|
};
|
|
204
448
|
|
|
205
449
|
window.addEventListener('popstate', () => {
|
|
206
|
-
this.
|
|
207
|
-
this.
|
|
450
|
+
this.flush();
|
|
451
|
+
this.startNewTrace(false);
|
|
452
|
+
this.addBreadcrumb('navigation', window.location.pathname);
|
|
208
453
|
});
|
|
209
454
|
|
|
210
|
-
// Visibility & Unload
|
|
211
455
|
document.addEventListener('visibilitychange', () => {
|
|
212
|
-
if (document.visibilityState === 'hidden')
|
|
213
|
-
this.trackPing();
|
|
214
|
-
} else {
|
|
215
|
-
// User returned, restart timer (don't count background time)
|
|
216
|
-
this.startTime = Date.now();
|
|
217
|
-
this.manageSession();
|
|
218
|
-
}
|
|
456
|
+
if (document.visibilityState === 'hidden') this.flush();
|
|
219
457
|
});
|
|
220
458
|
|
|
221
|
-
window.addEventListener('
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|
|
224
496
|
}
|
|
225
497
|
}
|
|
226
498
|
|
|
227
|
-
|
|
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
|
+
};
|
|
228
511
|
|
|
229
512
|
if (typeof window !== 'undefined') {
|
|
230
513
|
(window as any).Senzor = Senzor;
|