@senzops/web 1.0.0 → 1.1.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/dist/index.d.mts CHANGED
@@ -1,21 +1,22 @@
1
- interface Config {
2
- webId: string;
3
- endpoint?: string;
4
- }
5
- declare class SenzorWebAgent {
6
- private config;
7
- private startTime;
8
- private endpoint;
9
- constructor();
10
- init(config: Config): void;
11
- private initSession;
12
- private getIds;
13
- private trackPageView;
14
- private trackPing;
15
- private send;
16
- private fallbackSend;
17
- private setupListeners;
18
- }
19
- declare const Senzor: SenzorWebAgent;
20
-
21
- export { Senzor };
1
+ interface Config {
2
+ webId: string;
3
+ endpoint?: string;
4
+ }
5
+ declare class SenzorWebAgent {
6
+ private config;
7
+ private startTime;
8
+ private endpoint;
9
+ private initialized;
10
+ constructor();
11
+ init(config: Config): void;
12
+ private checkSession;
13
+ private getIds;
14
+ private trackPageView;
15
+ private trackPing;
16
+ private send;
17
+ private fallbackSend;
18
+ private setupListeners;
19
+ }
20
+ declare const Senzor: SenzorWebAgent;
21
+
22
+ export { Senzor };
package/dist/index.d.ts CHANGED
@@ -1,21 +1,22 @@
1
- interface Config {
2
- webId: string;
3
- endpoint?: string;
4
- }
5
- declare class SenzorWebAgent {
6
- private config;
7
- private startTime;
8
- private endpoint;
9
- constructor();
10
- init(config: Config): void;
11
- private initSession;
12
- private getIds;
13
- private trackPageView;
14
- private trackPing;
15
- private send;
16
- private fallbackSend;
17
- private setupListeners;
18
- }
19
- declare const Senzor: SenzorWebAgent;
20
-
21
- export { Senzor };
1
+ interface Config {
2
+ webId: string;
3
+ endpoint?: string;
4
+ }
5
+ declare class SenzorWebAgent {
6
+ private config;
7
+ private startTime;
8
+ private endpoint;
9
+ private initialized;
10
+ constructor();
11
+ init(config: Config): void;
12
+ private checkSession;
13
+ private getIds;
14
+ private trackPageView;
15
+ private trackPing;
16
+ private send;
17
+ private fallbackSend;
18
+ private setupListeners;
19
+ }
20
+ declare const Senzor: SenzorWebAgent;
21
+
22
+ export { Senzor };
@@ -1 +1 @@
1
- (()=>{function o(){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 i=class{config;startTime;endpoint;constructor(){this.config={webId:"",endpoint:"https://api.senzor.dev/api/ingest/web"},this.startTime=Date.now(),this.endpoint=""}init(e){if(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 to initialize analytics.");return}this.initSession(),this.trackPageView(),this.setupListeners()}initSession(){let e=localStorage.getItem("senzor_vid");e||(e=o(),localStorage.setItem("senzor_vid",e));let t=sessionStorage.getItem("senzor_sid");t||(t=o(),sessionStorage.setItem("senzor_sid",t))}getIds(){return{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:sessionStorage.getItem("senzor_sid")||"unknown"}}trackPageView(){this.startTime=Date.now();let e={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(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,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,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] Failed to send telemetry:",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()}),window.addEventListener("beforeunload",()=>{this.trackPing()})}},s=new i;typeof window<"u"&&(window.Senzor=s);})();
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);})();
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var s=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var h=(i,e)=>{for(var t in e)s(i,t,{get:e[t],enumerable:!0})},l=(i,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of p(e))!g.call(i,n)&&n!==t&&s(i,n,{get:()=>e[n],enumerable:!(o=c(e,n))||o.enumerable});return i};var w=i=>l(s({},"__esModule",{value:!0}),i);var f={};h(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 e=Math.random()*16|0;return(i==="x"?e:e&3|8).toString(16)})}var r=class{config;startTime;endpoint;constructor(){this.config={webId:"",endpoint:"https://api.senzor.dev/api/ingest/web"},this.startTime=Date.now(),this.endpoint=""}init(e){if(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 to initialize analytics.");return}this.initSession(),this.trackPageView(),this.setupListeners()}initSession(){let e=localStorage.getItem("senzor_vid");e||(e=a(),localStorage.setItem("senzor_vid",e));let t=sessionStorage.getItem("senzor_sid");t||(t=a(),sessionStorage.setItem("senzor_sid",t))}getIds(){return{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:sessionStorage.getItem("senzor_sid")||"unknown"}}trackPageView(){this.startTime=Date.now();let e={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(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,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,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] Failed to send telemetry:",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()}),window.addEventListener("beforeunload",()=>{this.trackPing()})}},d=new r;typeof window<"u"&&(window.Senzor=d);0&&(module.exports={Senzor});
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});
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- function o(){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 i=class{config;startTime;endpoint;constructor(){this.config={webId:"",endpoint:"https://api.senzor.dev/api/ingest/web"},this.startTime=Date.now(),this.endpoint=""}init(e){if(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 to initialize analytics.");return}this.initSession(),this.trackPageView(),this.setupListeners()}initSession(){let e=localStorage.getItem("senzor_vid");e||(e=o(),localStorage.setItem("senzor_vid",e));let t=sessionStorage.getItem("senzor_sid");t||(t=o(),sessionStorage.setItem("senzor_sid",t))}getIds(){return{visitorId:localStorage.getItem("senzor_vid")||"unknown",sessionId:sessionStorage.getItem("senzor_sid")||"unknown"}}trackPageView(){this.startTime=Date.now();let e={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(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,referrer:document.referrer,width:window.innerWidth,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,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] Failed to send telemetry:",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()}),window.addEventListener("beforeunload",()=>{this.trackPing()})}},s=new i;typeof window<"u"&&(window.Senzor=s);export{s as Senzor};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senzops/web",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Senzor Web Analytics SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,4 +16,4 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  }
19
- }
19
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,3 @@
1
- // Remove the external uuid import causing the crash
2
- // import { v4 as uuidv4 } from 'uuid';
3
-
4
1
  interface Config {
5
2
  webId: string;
6
3
  endpoint?: string;
@@ -16,16 +13,14 @@ interface Payload {
16
13
  referrer: string;
17
14
  width: number;
18
15
  timezone: string;
19
- duration?: number; // Only for pings/unload
16
+ duration?: number;
20
17
  }
21
18
 
22
- // --- Native UUID Helper (Fixes "require('crypto')" error) ---
19
+ // Browser-Native UUID (No Node.js dependencies)
23
20
  function generateUUID(): string {
24
- // Use native crypto API if available (Modern Browsers)
25
21
  if (typeof crypto !== 'undefined' && crypto.randomUUID) {
26
22
  return crypto.randomUUID();
27
23
  }
28
- // Fallback for older environments
29
24
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
30
25
  const r = (Math.random() * 16) | 0;
31
26
  const v = c === 'x' ? r : (r & 0x3) | 0x8;
@@ -37,58 +32,76 @@ class SenzorWebAgent {
37
32
  private config: Config;
38
33
  private startTime: number;
39
34
  private endpoint: string;
35
+ private initialized: boolean;
40
36
 
41
37
  constructor() {
42
38
  this.config = { webId: '', endpoint: 'https://api.senzor.dev/api/ingest/web' };
43
39
  this.startTime = Date.now();
44
40
  this.endpoint = '';
41
+ this.initialized = false;
45
42
  }
46
43
 
47
44
  public init(config: Config) {
45
+ if (this.initialized) {
46
+ console.warn('[Senzor] Agent already initialized.');
47
+ return;
48
+ }
49
+ this.initialized = true;
50
+
48
51
  this.config = { ...this.config, ...config };
49
- // Allow overriding endpoint for self-hosting or dev
50
52
  this.endpoint = this.config.endpoint || 'https://api.senzor.dev/api/ingest/web';
51
53
 
52
54
  if (!this.config.webId) {
53
- console.error('[Senzor] WebId is required to initialize analytics.');
55
+ console.error('[Senzor] WebId is required.');
54
56
  return;
55
57
  }
56
58
 
57
- // 1. Initialize Session
58
- this.initSession();
59
+ // 1. Manage Session State
60
+ this.checkSession();
59
61
 
60
- // 2. Track Initial Page View
62
+ // 2. Track initial load
61
63
  this.trackPageView();
62
64
 
63
65
  // 3. Setup Listeners
64
66
  this.setupListeners();
65
67
  }
66
68
 
67
- private initSession() {
68
- // Persistent Visitor ID (1 year)
69
- let vid = localStorage.getItem('senzor_vid');
70
- if (!vid) {
71
- vid = generateUUID();
72
- localStorage.setItem('senzor_vid', vid!);
69
+ // --- Standard Analytics Session Logic ---
70
+ // A session ends after 30 minutes of inactivity.
71
+ private checkSession() {
72
+ const now = Date.now();
73
+ const lastActivity = parseInt(localStorage.getItem('senzor_last_activity') || '0', 10);
74
+ const sessionTimeout = 30 * 60 * 1000; // 30 mins
75
+
76
+ // 1. Visitor ID (Persistent 1 Year)
77
+ if (!localStorage.getItem('senzor_vid')) {
78
+ localStorage.setItem('senzor_vid', generateUUID());
73
79
  }
74
80
 
75
- // Session ID (Expires when browser closes)
76
- let sid = sessionStorage.getItem('senzor_sid');
77
- if (!sid) {
78
- sid = generateUUID();
79
- sessionStorage.setItem('senzor_sid', sid!);
81
+ // 2. Session ID
82
+ // Create new if missing OR expired
83
+ if (!localStorage.getItem('senzor_sid') || (now - lastActivity > sessionTimeout)) {
84
+ localStorage.setItem('senzor_sid', generateUUID());
80
85
  }
86
+
87
+ // Update Activity
88
+ localStorage.setItem('senzor_last_activity', now.toString());
81
89
  }
82
90
 
83
91
  private getIds() {
92
+ // Refresh activity timestamp on every hit
93
+ localStorage.setItem('senzor_last_activity', Date.now().toString());
84
94
  return {
85
95
  visitorId: localStorage.getItem('senzor_vid') || 'unknown',
86
- sessionId: sessionStorage.getItem('senzor_sid') || 'unknown'
96
+ sessionId: localStorage.getItem('senzor_sid') || 'unknown'
87
97
  };
88
98
  }
89
99
 
90
100
  private trackPageView() {
91
- this.startTime = Date.now(); // Reset timer for new page
101
+ // Ensure session is valid before tracking
102
+ this.checkSession();
103
+ this.startTime = Date.now();
104
+
92
105
  const payload: Payload = {
93
106
  type: 'pageview',
94
107
  webId: this.config.webId,
@@ -103,10 +116,9 @@ class SenzorWebAgent {
103
116
  this.send(payload);
104
117
  }
105
118
 
106
- // Captures time spent on page when user leaves or hides tab
107
119
  private trackPing() {
108
120
  const duration = Math.floor((Date.now() - this.startTime) / 1000);
109
- if (duration < 1) return; // Ignore accidental bounces
121
+ if (duration < 1) return;
110
122
 
111
123
  const payload: Payload = {
112
124
  type: 'ping',
@@ -124,10 +136,8 @@ class SenzorWebAgent {
124
136
  }
125
137
 
126
138
  private send(data: Payload) {
127
- // Use sendBeacon for reliability during unload, fallback to fetch
128
139
  if (navigator.sendBeacon) {
129
140
  const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
130
- // sendBeacon returns false if it fails (e.g. payload too large)
131
141
  const success = navigator.sendBeacon(this.endpoint, blob);
132
142
  if (!success) this.fallbackSend(data);
133
143
  } else {
@@ -141,16 +151,16 @@ class SenzorWebAgent {
141
151
  body: JSON.stringify(data),
142
152
  keepalive: true,
143
153
  headers: { 'Content-Type': 'application/json' }
144
- }).catch(err => console.error('[Senzor] Failed to send telemetry:', err));
154
+ }).catch(err => console.error('[Senzor] Telemetry Error:', err));
145
155
  }
146
156
 
147
157
  private setupListeners() {
148
- // 1. History API Support (SPA - React/Next.js/Vue)
158
+ // SPA Support
149
159
  const originalPushState = history.pushState;
150
160
  history.pushState = (...args) => {
151
- this.trackPing(); // Send duration for previous page
161
+ this.trackPing(); // End previous page
152
162
  originalPushState.apply(history, args);
153
- this.trackPageView(); // Track new page
163
+ this.trackPageView(); // Start new page
154
164
  };
155
165
 
156
166
  window.addEventListener('popstate', () => {
@@ -158,27 +168,25 @@ class SenzorWebAgent {
158
168
  this.trackPageView();
159
169
  });
160
170
 
161
- // 2. Visibility Change (Tab switch / Minimize)
171
+ // Visibility & Unload
162
172
  document.addEventListener('visibilitychange', () => {
163
173
  if (document.visibilityState === 'hidden') {
164
174
  this.trackPing();
165
175
  } else {
166
- // User came back, reset timer so we don't count background time
176
+ // User returned, restart timer (don't count background time)
167
177
  this.startTime = Date.now();
178
+ this.checkSession(); // Verify session hasn't expired while tab was hidden
168
179
  }
169
180
  });
170
181
 
171
- // 3. Before Unload (Closing tab)
172
182
  window.addEventListener('beforeunload', () => {
173
183
  this.trackPing();
174
184
  });
175
185
  }
176
186
  }
177
187
 
178
- // Export Singleton
179
188
  export const Senzor = new SenzorWebAgent();
180
189
 
181
- // Allow window access for script tag usage
182
190
  if (typeof window !== 'undefined') {
183
191
  (window as any).Senzor = Senzor;
184
192
  }