@anested/analytics 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @anested/analytics
2
+
3
+ Lightweight analytics & SEO intelligence tracking for websites. Under **4KB** gzipped.
4
+
5
+ ## Quick Start (Script Tag)
6
+
7
+ Add this to your HTML — no build step needed:
8
+
9
+ ```html
10
+ <script defer src="https://api.analytics.anested.com/a.js" data-client-id="YOUR_CLIENT_ID"></script>
11
+ ```
12
+
13
+ That's it. Pageviews, sessions, scroll depth, click tracking, SEO audits, and Web Vitals are all automatic.
14
+
15
+ ## NPM Installation
16
+
17
+ ```bash
18
+ npm install @anested/analytics
19
+ ```
20
+
21
+ ### React / Next.js
22
+
23
+ ```tsx
24
+ // app/layout.tsx or pages/_app.tsx
25
+ import { init } from '@anested/analytics';
26
+ import { useEffect } from 'react';
27
+
28
+ export default function RootLayout({ children }) {
29
+ useEffect(() => {
30
+ init({ clientId: 'YOUR_CLIENT_ID' });
31
+ }, []);
32
+
33
+ return <html><body>{children}</body></html>;
34
+ }
35
+ ```
36
+
37
+ ### Vue / Nuxt
38
+
39
+ ```js
40
+ // main.js or plugin
41
+ import { init } from '@anested/analytics';
42
+ init({ clientId: 'YOUR_CLIENT_ID' });
43
+ ```
44
+
45
+ ### Vanilla JS
46
+
47
+ ```js
48
+ import { init, track } from '@anested/analytics';
49
+
50
+ init({ clientId: 'YOUR_CLIENT_ID' });
51
+
52
+ // Track custom events
53
+ document.querySelector('#signup-btn').addEventListener('click', () => {
54
+ track('signup_click', { plan: 'pro' });
55
+ });
56
+ ```
57
+
58
+ ## Configuration
59
+
60
+ | Option | Type | Default | Description |
61
+ |--------|------|---------|-------------|
62
+ | `clientId` | string | *required* | Your site ID from the dashboard |
63
+ | `host` | string | `https://api.analytics.anested.com` | API endpoint |
64
+ | `clicks` | boolean | `true` | Track clicks, dead clicks, rage clicks |
65
+ | `scroll` | boolean | `true` | Track scroll depth |
66
+ | `seo` | boolean | `true` | Client-side SEO audit |
67
+ | `performance` | boolean | `true` | Web Vitals (LCP, FID, CLS) |
68
+ | `location` | boolean | `false` | Request browser geolocation (needs user permission) |
69
+ | `debug` | boolean | `false` | Console logging |
70
+
71
+ ## Script Tag Attributes
72
+
73
+ | Attribute | Description |
74
+ |-----------|-------------|
75
+ | `data-client-id` | Your client/site ID |
76
+ | `data-host` | Custom API host |
77
+ | `data-clicks` | `"false"` to disable click tracking |
78
+ | `data-scroll` | `"false"` to disable scroll tracking |
79
+ | `data-seo` | `"false"` to disable SEO audit |
80
+ | `data-performance` | `"false"` to disable Web Vitals |
81
+ | `data-location` | `"true"` to request browser geolocation |
82
+ | `data-debug` | `"true"` to enable debug logging |
83
+
84
+ ## Custom Events
85
+
86
+ ```js
87
+ // Script tag usage
88
+ window.analytica.track('purchase', { product: 'T-Shirt', price: 29.99 });
89
+
90
+ // NPM usage
91
+ import { track } from '@anested/analytics';
92
+ track('purchase', { product: 'T-Shirt', price: 29.99 });
93
+ ```
94
+
95
+ ## Privacy
96
+
97
+ - Respects `Do Not Track` and `Global Privacy Control`
98
+ - Opt-out: set `window.__analytica_optout = true` before script loads
99
+ - IP addresses are hashed server-side (never stored raw)
100
+ - Browser geolocation is opt-in only (requires `data-location="true"`)
101
+ - No cookies used — localStorage for visitor ID, sessionStorage for session
102
+
103
+ ## What's Tracked Automatically
104
+
105
+ - **Pageviews** — URL, path, title, referrer, UTM params
106
+ - **Sessions** — Duration, bounce detection, scroll depth
107
+ - **Devices** — Browser, OS, screen size, device type (server-side UA parsing)
108
+ - **Location** — Country, region, city (from IP, no permission needed)
109
+ - **Clicks** — All clicks with element info, rage clicks, dead clicks
110
+ - **Forms** — Submit events with field count
111
+ - **SEO** — Title, meta, H1, images, OG tags, structured data, score
112
+ - **Web Vitals** — LCP, FID, CLS
113
+ - **SPA** — Automatic navigation detection (pushState/replaceState/popstate)
114
+
115
+ ## License
116
+
117
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ export interface AnalyticsOptions {
2
+ /** Your site/client ID from the Anested dashboard */
3
+ clientId: string;
4
+ /** API endpoint (default: https://api.analytics.anested.com) */
5
+ host?: string;
6
+ /** Custom script source URL (overrides host for script loading) */
7
+ scriptSrc?: string;
8
+ /** Track clicks, dead clicks, rage clicks (default: true) */
9
+ clicks?: boolean;
10
+ /** Track scroll depth (default: true) */
11
+ scroll?: boolean;
12
+ /** Run client-side SEO audit (default: true) */
13
+ seo?: boolean;
14
+ /** Track Web Vitals — LCP, FID, CLS (default: true) */
15
+ performance?: boolean;
16
+ /** Request browser geolocation permission (default: false) */
17
+ location?: boolean;
18
+ /** Enable console debug logging (default: false) */
19
+ debug?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Initialize Anested Analytics tracking.
24
+ * Must be called in a browser environment (no-op in SSR/Node).
25
+ */
26
+ export function init(options: AnalyticsOptions): void;
27
+
28
+ /**
29
+ * Track a custom event.
30
+ */
31
+ export function track(name: string, properties?: Record<string, any>): void;
32
+
33
+ /**
34
+ * Opt out of all tracking (call before init to prevent any data collection).
35
+ */
36
+ export function optOut(): void;
37
+
38
+ declare const _default: {
39
+ init: typeof init;
40
+ track: typeof track;
41
+ optOut: typeof optOut;
42
+ };
43
+
44
+ export default _default;
package/index.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @anested/analytics — CommonJS wrapper
3
+ */
4
+
5
+ "use strict";
6
+
7
+ const DEFAULT_HOST = "https://api.analytics.anested.com";
8
+
9
+ /**
10
+ * Initialize Anested Analytics tracking.
11
+ * Must be called in a browser environment.
12
+ */
13
+ function init(options) {
14
+ if (typeof window === "undefined") return;
15
+ options = options || {};
16
+
17
+ window.__analytica_config = {
18
+ clientId: options.clientId || options.siteId || "",
19
+ host: options.host || DEFAULT_HOST,
20
+ clicks: options.clicks !== false,
21
+ scroll: options.scroll !== false,
22
+ seo: options.seo !== false,
23
+ performance: options.performance !== false,
24
+ location: options.location === true,
25
+ debug: options.debug === true,
26
+ };
27
+
28
+ var script = document.createElement("script");
29
+ script.src = (options.scriptSrc || options.host || DEFAULT_HOST) + "/a.js";
30
+ script.defer = true;
31
+ script.dataset.clientId = window.__analytica_config.clientId;
32
+ script.dataset.host = window.__analytica_config.host;
33
+ if (options.clicks === false) script.dataset.clicks = "false";
34
+ if (options.scroll === false) script.dataset.scroll = "false";
35
+ if (options.seo === false) script.dataset.seo = "false";
36
+ if (options.performance === false) script.dataset.performance = "false";
37
+ if (options.location === true) script.dataset.location = "true";
38
+ if (options.debug === true) script.dataset.debug = "true";
39
+ document.head.appendChild(script);
40
+ }
41
+
42
+ /**
43
+ * Track a custom event.
44
+ */
45
+ function track(name, properties) {
46
+ if (typeof window !== "undefined" && window.analytica) {
47
+ window.analytica.track(name, properties);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Opt out of tracking.
53
+ */
54
+ function optOut() {
55
+ if (typeof window !== "undefined") {
56
+ window.__analytica_optout = true;
57
+ }
58
+ }
59
+
60
+ module.exports = { init, track, optOut };
package/index.mjs ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @anested/analytics — ES Module wrapper
3
+ *
4
+ * Usage:
5
+ * import { init } from '@anested/analytics';
6
+ * init({ clientId: 'YOUR_CLIENT_ID' });
7
+ *
8
+ * Or with options:
9
+ * init({
10
+ * clientId: 'abc123',
11
+ * host: 'https://api.analytics.anested.com',
12
+ * clicks: true,
13
+ * scroll: true,
14
+ * seo: true,
15
+ * performance: true,
16
+ * location: false,
17
+ * debug: false,
18
+ * });
19
+ */
20
+
21
+ const DEFAULT_HOST = "https://api.analytics.anested.com";
22
+
23
+ /**
24
+ * Initialize Anested Analytics tracking.
25
+ * Must be called in a browser environment.
26
+ */
27
+ export function init(options = {}) {
28
+ if (typeof window === "undefined") return;
29
+
30
+ window.__analytica_config = {
31
+ clientId: options.clientId || options.siteId || "",
32
+ host: options.host || DEFAULT_HOST,
33
+ clicks: options.clicks !== false,
34
+ scroll: options.scroll !== false,
35
+ seo: options.seo !== false,
36
+ performance: options.performance !== false,
37
+ location: options.location === true,
38
+ debug: options.debug === true,
39
+ };
40
+
41
+ // Load the tracking script
42
+ const script = document.createElement("script");
43
+ script.src = (options.scriptSrc || options.host || DEFAULT_HOST) + "/a.js";
44
+ script.defer = true;
45
+ script.dataset.clientId = window.__analytica_config.clientId;
46
+ script.dataset.host = window.__analytica_config.host;
47
+ if (options.clicks === false) script.dataset.clicks = "false";
48
+ if (options.scroll === false) script.dataset.scroll = "false";
49
+ if (options.seo === false) script.dataset.seo = "false";
50
+ if (options.performance === false) script.dataset.performance = "false";
51
+ if (options.location === true) script.dataset.location = "true";
52
+ if (options.debug === true) script.dataset.debug = "true";
53
+ document.head.appendChild(script);
54
+ }
55
+
56
+ /**
57
+ * Track a custom event.
58
+ */
59
+ export function track(name, properties) {
60
+ if (typeof window !== "undefined" && window.analytica) {
61
+ window.analytica.track(name, properties);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Opt out of tracking (sets flag before script loads).
67
+ */
68
+ export function optOut() {
69
+ if (typeof window !== "undefined") {
70
+ window.__analytica_optout = true;
71
+ }
72
+ }
73
+
74
+ export default { init, track, optOut };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@anested/analytics",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight analytics & SEO intelligence tracking for websites",
5
+ "main": "index.js",
6
+ "module": "index.mjs",
7
+ "types": "index.d.ts",
8
+ "files": [
9
+ "index.js",
10
+ "index.mjs",
11
+ "index.d.ts",
12
+ "script.js",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "node build.js"
17
+ },
18
+ "keywords": [
19
+ "analytics",
20
+ "tracking",
21
+ "seo",
22
+ "web-vitals",
23
+ "pageviews",
24
+ "anested"
25
+ ],
26
+ "author": "Anested",
27
+ "license": "MIT",
28
+ "homepage": "https://analytics.anested.com",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/anested/analytics"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
package/script.js ADDED
@@ -0,0 +1,490 @@
1
+ /**
2
+ * @anested/analytics — Lightweight tracking script
3
+ * Default endpoint: https://api.analytics.anested.com
4
+ *
5
+ * USAGE (Script Tag — recommended):
6
+ * <script defer src="https://cdn.anested.com/a.js" data-client-id="YOUR_CLIENT_ID"></script>
7
+ *
8
+ * USAGE (NPM):
9
+ * import { init } from '@anested/analytics';
10
+ * init({ clientId: 'YOUR_CLIENT_ID' });
11
+ */
12
+ (function(){
13
+ "use strict";
14
+ var d=document,w=window,n=navigator,ls=w.localStorage,ss=w.sessionStorage;
15
+
16
+ /* ── Opt-out & DNT check ── */
17
+ if(w.__analytica_optout)return;
18
+ if(n.doNotTrack==="1"||n.globalPrivacyControl)return;
19
+
20
+ /* ── Config ── */
21
+ var config=w.__analytica_config||{};
22
+ var el=d.currentScript;
23
+ var siteId,host,trackClicks,trackScroll,trackSEO,trackPerf,trackLocation,debug;
24
+
25
+ if(el){
26
+ siteId=el.getAttribute("data-client-id")||el.getAttribute("data-site")||el.getAttribute("data-site-id")||config.clientId;
27
+ host=el.getAttribute("data-host")||config.host||"https://api.analytics.anested.com";
28
+ trackClicks=el.getAttribute("data-clicks")!=="false";
29
+ trackScroll=el.getAttribute("data-scroll")!=="false";
30
+ trackSEO=el.getAttribute("data-seo")!=="false";
31
+ trackPerf=el.getAttribute("data-performance")!=="false";
32
+ trackLocation=el.getAttribute("data-location")==="true";
33
+ debug=el.getAttribute("data-debug")==="true";
34
+ }else{
35
+ siteId=config.clientId||"";
36
+ host=config.host||"https://api.analytics.anested.com";
37
+ trackClicks=config.clicks!==false;
38
+ trackScroll=config.scroll!==false;
39
+ trackSEO=config.seo!==false;
40
+ trackPerf=config.performance!==false;
41
+ trackLocation=config.location===true;
42
+ debug=config.debug===true;
43
+ }
44
+
45
+ if(!siteId)return;
46
+
47
+ /* ── Helpers ── */
48
+ function uid(){return Math.random().toString(36).slice(2)+Date.now().toString(36)}
49
+ function log(){if(debug)console.log.apply(console,["[analytica]"].concat(Array.prototype.slice.call(arguments)))}
50
+
51
+ /* ── Visitor & Session ── */
52
+ var vid=ls.getItem("_av")||uid();ls.setItem("_av",vid);
53
+ var sid=ss.getItem("_as")||uid();ss.setItem("_as",sid);
54
+
55
+ /* ── Device Detection ── */
56
+ var ua=n.userAgent||"";
57
+ var mob=/Mobi|Android/i.test(ua);
58
+ var tab=/Tablet|iPad/i.test(ua);
59
+ var dev=tab?"tablet":mob?"mobile":"desktop";
60
+
61
+ /* ── Connection type ── */
62
+ var conn="";
63
+ if(n.connection){conn=n.connection.effectiveType||n.connection.type||""}
64
+
65
+ /* ── UTM parsing ── */
66
+ function qp(name){try{return new URL(w.location.href).searchParams.get(name)||""}catch(e){return""}}
67
+ var utm={source:qp("utm_source"),medium:qp("utm_medium"),campaign:qp("utm_campaign")};
68
+
69
+ /* ── Browser Geolocation (opt-in via data-location="true") ── */
70
+ var clientGeo={lat:null,lng:null,accuracy:null};
71
+ if(trackLocation&&n.geolocation){
72
+ n.geolocation.getCurrentPosition(function(pos){
73
+ clientGeo.lat=pos.coords.latitude;
74
+ clientGeo.lng=pos.coords.longitude;
75
+ clientGeo.accuracy=Math.round(pos.coords.accuracy);
76
+ log("browser geolocation acquired",clientGeo);
77
+ if(pvSent){
78
+ send("/collect/location",{
79
+ path:w.location.pathname,
80
+ client_lat:clientGeo.lat,
81
+ client_lng:clientGeo.lng,
82
+ client_accuracy:clientGeo.accuracy,
83
+ });
84
+ }
85
+ },function(err){
86
+ log("geolocation denied/unavailable:",err.message);
87
+ },{timeout:10000,maximumAge:300000,enableHighAccuracy:false});
88
+ }
89
+
90
+ /* ── Event Queue & Batch Sending ── */
91
+ var queue=[];
92
+ var FLUSH_INTERVAL=5000;
93
+ var BATCH_SIZE=10;
94
+
95
+ function send(endpoint,data){
96
+ data.site_id=siteId;data.visitor_id=vid;data.session_id=sid;
97
+ var url=host+endpoint;
98
+ var payload=JSON.stringify(data);
99
+ if(n.sendBeacon){
100
+ n.sendBeacon(url,payload);
101
+ }else{
102
+ var x=new XMLHttpRequest();x.open("POST",url);
103
+ x.setRequestHeader("Content-Type","application/json");
104
+ x.send(payload);
105
+ }
106
+ log("sent",endpoint,data);
107
+ }
108
+
109
+ function enqueue(type,data){
110
+ data._type=type;
111
+ data.site_id=siteId;data.visitor_id=vid;data.session_id=sid;
112
+ queue.push(data);
113
+ if(queue.length>=BATCH_SIZE)flush();
114
+ }
115
+
116
+ function flush(){
117
+ if(queue.length===0)return;
118
+ var batch=queue.splice(0,50);
119
+ var url=host+"/collect/batch";
120
+ var payload=JSON.stringify({events:batch});
121
+ if(n.sendBeacon){
122
+ n.sendBeacon(url,payload);
123
+ }else{
124
+ var x=new XMLHttpRequest();x.open("POST",url);
125
+ x.setRequestHeader("Content-Type","application/json");
126
+ x.send(payload);
127
+ }
128
+ log("flushed",batch.length,"events");
129
+ }
130
+
131
+ var flushTimer=setInterval(flush,FLUSH_INTERVAL);
132
+
133
+ /* ── Scroll Depth Tracking ── */
134
+ var maxScroll=0;
135
+ function onScroll(){
136
+ var st=w.pageYOffset||d.documentElement.scrollTop;
137
+ var sh=d.documentElement.scrollHeight-d.documentElement.clientHeight;
138
+ if(sh>0){var p=Math.round(st/sh*100);if(p>maxScroll)maxScroll=p}
139
+ }
140
+ if(trackScroll)w.addEventListener("scroll",onScroll,{passive:true});
141
+
142
+ /* ── Pageview Tracking ── */
143
+ var startTime=Date.now();
144
+ var pvSent=false;
145
+
146
+ function trackPageview(){
147
+ startTime=Date.now();maxScroll=0;pvSent=true;
148
+ var pvData={
149
+ url:w.location.href,
150
+ path:w.location.pathname,
151
+ title:d.title,
152
+ referrer:d.referrer,
153
+ utm:utm,
154
+ device:dev,
155
+ screen_w:w.screen.width,
156
+ screen_h:w.screen.height,
157
+ viewport_w:w.innerWidth,
158
+ viewport_h:w.innerHeight,
159
+ language:n.language||"",
160
+ connection:conn,
161
+ timezone:Intl&&Intl.DateTimeFormat?Intl.DateTimeFormat().resolvedOptions().timeZone:"",
162
+ };
163
+ if(clientGeo.lat!==null){
164
+ pvData.client_lat=clientGeo.lat;
165
+ pvData.client_lng=clientGeo.lng;
166
+ pvData.client_accuracy=clientGeo.accuracy;
167
+ }
168
+ send("/collect/pageview",pvData);
169
+ }
170
+ trackPageview();
171
+
172
+ /* ── Heartbeat (every 30s) ── */
173
+ var heartbeatInterval=setInterval(function(){
174
+ if(!pvSent)return;
175
+ var dur=Math.round((Date.now()-startTime)/1000);
176
+ send("/collect/heartbeat",{
177
+ path:w.location.pathname,
178
+ duration:dur,
179
+ });
180
+ },30000);
181
+
182
+ /* ── Engagement on leave ── */
183
+ function onLeave(){
184
+ if(!pvSent)return;
185
+ var dur=Math.round((Date.now()-startTime)/1000);
186
+ send("/collect/engagement",{
187
+ path:w.location.pathname,
188
+ duration:dur,
189
+ scroll_depth:maxScroll,
190
+ });
191
+ flush();
192
+ }
193
+ w.addEventListener("visibilitychange",function(){if(d.visibilityState==="hidden")onLeave()});
194
+ w.addEventListener("pagehide",onLeave);
195
+
196
+ /* ── Click Tracking ── */
197
+ if(trackClicks){
198
+ var lastClickTime=0;
199
+ var lastClickEl=null;
200
+ var clickCount=0;
201
+
202
+ d.addEventListener("click",function(e){
203
+ var t=e.target;if(!t)return;
204
+ var now=Date.now();
205
+
206
+ if(t===lastClickEl&&now-lastClickTime<1000){
207
+ clickCount++;
208
+ if(clickCount===3){
209
+ enqueue("event",{
210
+ url:w.location.href,path:w.location.pathname,
211
+ type:"rage_click",category:"interaction",
212
+ label:(t.innerText||"").slice(0,80),
213
+ selector:cssPath(t),
214
+ element:{tag:t.tagName,id:t.id,class:t.className,text:(t.innerText||"").slice(0,80),href:t.getAttribute("href")||""},
215
+ coordinates:{x:e.clientX,y:e.clientY},
216
+ });
217
+ }
218
+ }else{
219
+ clickCount=1;
220
+ }
221
+ lastClickTime=now;lastClickEl=t;
222
+
223
+ var el2=t.closest("a,button,[role=button],input,select,textarea,[data-track]");
224
+
225
+ if(!el2){
226
+ enqueue("event",{
227
+ url:w.location.href,path:w.location.pathname,
228
+ type:"dead_click",category:"interaction",
229
+ label:(t.innerText||"").slice(0,80),
230
+ selector:cssPath(t),
231
+ element:{tag:t.tagName,id:t.id,class:t.className||"",text:(t.innerText||"").slice(0,80),href:""},
232
+ coordinates:{x:e.clientX,y:e.clientY},
233
+ });
234
+ return;
235
+ }
236
+
237
+ var tag=(el2.tagName||"").toLowerCase();
238
+ var text=(el2.innerText||el2.value||"").slice(0,80).trim();
239
+ var href=el2.getAttribute("href")||"";
240
+
241
+ var category=tag;
242
+ if(el2.closest("[data-card],.card,[class*=card]"))category="card";
243
+ if(tag==="a"&&href&&href.indexOf(w.location.hostname)===-1&&href.startsWith("http"))category="outbound_link";
244
+
245
+ enqueue("event",{
246
+ url:w.location.href,path:w.location.pathname,
247
+ type:"click",
248
+ category:category,
249
+ label:text||el2.id||href||"",
250
+ selector:cssPath(el2),
251
+ element:{tag:tag,id:el2.id||"",class:(el2.className||"").toString().slice(0,200),text:text,href:href},
252
+ coordinates:{x:e.clientX,y:e.clientY},
253
+ });
254
+ },true);
255
+ }
256
+
257
+ /* ── Form Submit Tracking ── */
258
+ d.addEventListener("submit",function(e){
259
+ var form=e.target;if(!form||form.tagName!=="FORM")return;
260
+ enqueue("event",{
261
+ url:w.location.href,path:w.location.pathname,
262
+ type:"form_submit",
263
+ category:"form",
264
+ label:form.id||form.getAttribute("name")||form.action||"",
265
+ element:{tag:"form",id:form.id||"",class:(form.className||"").toString().slice(0,200),text:"",href:form.action||""},
266
+ properties:{field_count:form.elements?form.elements.length:0},
267
+ });
268
+ },true);
269
+
270
+ /* ── CSS Path Helper ── */
271
+ function cssPath(el){
272
+ var path=[];
273
+ while(el&&el.nodeType===1){
274
+ var s=el.tagName.toLowerCase();
275
+ if(el.id){s+="#"+el.id;path.unshift(s);break}
276
+ var c=el.className;
277
+ if(c&&typeof c==="string")s+="."+c.trim().split(/\s+/).slice(0,2).join(".");
278
+ path.unshift(s);el=el.parentElement;
279
+ if(path.length>4)break;
280
+ }
281
+ return path.join(" > ");
282
+ }
283
+
284
+ /* ── SPA Support ── */
285
+ var lastPath=w.location.pathname;
286
+ function checkNav(){
287
+ if(w.location.pathname!==lastPath){
288
+ onLeave();
289
+ lastPath=w.location.pathname;
290
+ pvSent=false;
291
+ trackPageview();
292
+ if(trackSEO)setTimeout(runSEOAudit,1000);
293
+ }
294
+ }
295
+ var origPush=history.pushState;var origReplace=history.replaceState;
296
+ history.pushState=function(){origPush.apply(this,arguments);checkNav()};
297
+ history.replaceState=function(){origReplace.apply(this,arguments);checkNav()};
298
+ w.addEventListener("popstate",checkNav);
299
+
300
+ /* ══════════════════════════════════════════════════════════════
301
+ SEO AUDIT (Client-Side)
302
+ ══════════════════════════════════════════════════════════════ */
303
+ function runSEOAudit(){
304
+ if(!trackSEO)return;
305
+ var issues=[];
306
+
307
+ var title=d.title||"";
308
+ if(!title)issues.push({type:"missing_title",severity:"error",element:"<title>",message:"Page has no title tag"});
309
+ else if(title.length<30)issues.push({type:"title_too_short",severity:"warning",element:"<title>",message:"Title is "+title.length+" chars (recommended: 30-60)"});
310
+ else if(title.length>60)issues.push({type:"title_too_long",severity:"warning",element:"<title>",message:"Title is "+title.length+" chars (recommended: 30-60)"});
311
+
312
+ var metaDesc=d.querySelector('meta[name="description"]');
313
+ var descContent=metaDesc?metaDesc.getAttribute("content")||"":"";
314
+ if(!metaDesc||!descContent)issues.push({type:"missing_description",severity:"error",element:'meta[name="description"]',message:"No meta description found"});
315
+ else if(descContent.length<70)issues.push({type:"description_too_short",severity:"warning",element:'meta[name="description"]',message:"Description is "+descContent.length+" chars (recommended: 70-160)"});
316
+ else if(descContent.length>160)issues.push({type:"description_too_long",severity:"warning",element:'meta[name="description"]',message:"Description is "+descContent.length+" chars (recommended: 70-160)"});
317
+
318
+ var h1s=d.querySelectorAll("h1");
319
+ if(h1s.length===0)issues.push({type:"missing_h1",severity:"error",element:"h1",message:"No H1 heading found"});
320
+ else if(h1s.length>1)issues.push({type:"multiple_h1",severity:"warning",element:"h1",message:h1s.length+" H1 tags found (recommended: 1)"});
321
+
322
+ var imgs=d.querySelectorAll("img");
323
+ var noAlt=0;
324
+ for(var i=0;i<imgs.length;i++){if(!imgs[i].getAttribute("alt"))noAlt++}
325
+ if(noAlt>0)issues.push({type:"missing_alt",severity:"error",element:"img",message:noAlt+" image(s) missing alt attribute"});
326
+
327
+ if(!d.querySelector('meta[property="og:title"]'))issues.push({type:"missing_og_title",severity:"warning",element:'meta[property="og:title"]',message:"No Open Graph title"});
328
+ if(!d.querySelector('meta[property="og:description"]'))issues.push({type:"missing_og_description",severity:"warning",element:'meta[property="og:description"]',message:"No Open Graph description"});
329
+ if(!d.querySelector('meta[property="og:image"]'))issues.push({type:"missing_og_image",severity:"warning",element:'meta[property="og:image"]',message:"No Open Graph image"});
330
+
331
+ if(!d.querySelector('meta[name="twitter:card"]')&&!d.querySelector('meta[property="twitter:card"]'))
332
+ issues.push({type:"missing_twitter_card",severity:"info",element:'meta[name="twitter:card"]',message:"No Twitter Card meta tag"});
333
+
334
+ var canonical=d.querySelector('link[rel="canonical"]');
335
+ if(!canonical)issues.push({type:"missing_canonical",severity:"warning",element:'link[rel="canonical"]',message:"No canonical URL specified"});
336
+
337
+ var htmlLang=d.documentElement.getAttribute("lang");
338
+ if(!htmlLang)issues.push({type:"missing_lang",severity:"warning",element:"<html>",message:"No lang attribute on html element"});
339
+
340
+ var viewport=d.querySelector('meta[name="viewport"]');
341
+ if(!viewport)issues.push({type:"missing_viewport",severity:"error",element:'meta[name="viewport"]',message:"No viewport meta tag"});
342
+
343
+ var favicon=d.querySelector('link[rel="icon"],link[rel="shortcut icon"]');
344
+ if(!favicon)issues.push({type:"missing_favicon",severity:"info",element:'link[rel="icon"]',message:"No favicon detected"});
345
+
346
+ var jsonLd=d.querySelectorAll('script[type="application/ld+json"]');
347
+ var hasMicrodata=d.querySelector("[itemscope]");
348
+ var hasStructuredData=jsonLd.length>0||!!hasMicrodata;
349
+ if(!hasStructuredData)issues.push({type:"missing_structured_data",severity:"info",element:"script[type=application/ld+json]",message:"No structured data (JSON-LD or Microdata) found"});
350
+
351
+ var bodyText=(d.body.innerText||"").replace(/\s+/g," ").trim();
352
+ var wordCount=bodyText?bodyText.split(" ").length:0;
353
+ if(wordCount<300)issues.push({type:"low_word_count",severity:"info",element:"body",message:"Page has "+wordCount+" words (recommended: 300+)"});
354
+
355
+ var links=d.querySelectorAll("a[href]");
356
+ var internalLinks=0,externalLinks=0;
357
+ for(var j=0;j<links.length;j++){
358
+ var href=links[j].getAttribute("href")||"";
359
+ if(href.startsWith("http")&&href.indexOf(w.location.hostname)===-1)externalLinks++;
360
+ else if(href&&!href.startsWith("#")&&!href.startsWith("javascript"))internalLinks++;
361
+ }
362
+
363
+ var score=100;
364
+ for(var k=0;k<issues.length;k++){
365
+ if(issues[k].severity==="error")score-=10;
366
+ else if(issues[k].severity==="warning")score-=5;
367
+ else score-=2;
368
+ }
369
+ score=Math.max(0,Math.min(100,score));
370
+
371
+ send("/collect/seo",{
372
+ url:w.location.href,
373
+ pathname:w.location.pathname,
374
+ score:score,
375
+ issues:issues,
376
+ meta:{
377
+ title:title,
378
+ description:descContent,
379
+ canonical:canonical?canonical.getAttribute("href")||"":"",
380
+ ogTags:{
381
+ title:(d.querySelector('meta[property="og:title"]')||{}).content||"",
382
+ description:(d.querySelector('meta[property="og:description"]')||{}).content||"",
383
+ image:(d.querySelector('meta[property="og:image"]')||{}).content||"",
384
+ },
385
+ twitterTags:{
386
+ card:(d.querySelector('meta[name="twitter:card"]')||{}).content||"",
387
+ title:(d.querySelector('meta[name="twitter:title"]')||{}).content||"",
388
+ },
389
+ h1Count:h1s.length,
390
+ h2Count:d.querySelectorAll("h2").length,
391
+ wordCount:wordCount,
392
+ imageCount:imgs.length,
393
+ imagesWithoutAlt:noAlt,
394
+ internalLinks:internalLinks,
395
+ externalLinks:externalLinks,
396
+ hasStructuredData:hasStructuredData,
397
+ hasViewport:!!viewport,
398
+ hasLang:!!htmlLang,
399
+ hasFavicon:!!favicon,
400
+ },
401
+ });
402
+
403
+ log("SEO audit complete. Score:",score,"Issues:",issues.length);
404
+ }
405
+
406
+ if(trackSEO){
407
+ if(d.readyState==="complete")setTimeout(runSEOAudit,2000);
408
+ else w.addEventListener("load",function(){setTimeout(runSEOAudit,2000)});
409
+ }
410
+
411
+ /* ══════════════════════════════════════════════════════════════
412
+ PERFORMANCE (Web Vitals)
413
+ ══════════════════════════════════════════════════════════════ */
414
+ if(trackPerf&&w.PerformanceObserver){
415
+ try{
416
+ var lcpObs=new PerformanceObserver(function(list){
417
+ var entries=list.getEntries();
418
+ var last=entries[entries.length-1];
419
+ if(last){
420
+ enqueue("event",{
421
+ url:w.location.href,path:w.location.pathname,
422
+ type:"web_vital",category:"performance",
423
+ label:"LCP",
424
+ value:Math.round(last.startTime),
425
+ properties:{metric:"LCP",value:Math.round(last.startTime)},
426
+ });
427
+ }
428
+ });
429
+ lcpObs.observe({type:"largest-contentful-paint",buffered:true});
430
+
431
+ var fidObs=new PerformanceObserver(function(list){
432
+ var entries=list.getEntries();
433
+ if(entries.length>0){
434
+ var first=entries[0];
435
+ enqueue("event",{
436
+ url:w.location.href,path:w.location.pathname,
437
+ type:"web_vital",category:"performance",
438
+ label:"FID",
439
+ value:Math.round(first.processingStart-first.startTime),
440
+ properties:{metric:"FID",value:Math.round(first.processingStart-first.startTime)},
441
+ });
442
+ }
443
+ });
444
+ fidObs.observe({type:"first-input",buffered:true});
445
+
446
+ var clsValue=0;
447
+ var clsObs=new PerformanceObserver(function(list){
448
+ for(var i=0;i<list.getEntries().length;i++){
449
+ var entry=list.getEntries()[i];
450
+ if(!entry.hadRecentInput)clsValue+=entry.value;
451
+ }
452
+ });
453
+ clsObs.observe({type:"layout-shift",buffered:true});
454
+
455
+ w.addEventListener("visibilitychange",function(){
456
+ if(d.visibilityState==="hidden"&&clsValue>0){
457
+ enqueue("event",{
458
+ url:w.location.href,path:w.location.pathname,
459
+ type:"web_vital",category:"performance",
460
+ label:"CLS",
461
+ value:Math.round(clsValue*1000)/1000,
462
+ properties:{metric:"CLS",value:Math.round(clsValue*1000)/1000},
463
+ });
464
+ }
465
+ });
466
+ }catch(e){log("Performance tracking error:",e.message)}
467
+ }
468
+
469
+ /* ── Public API ── */
470
+ w.analytica={
471
+ track:function(name,props){
472
+ enqueue("event",{
473
+ url:w.location.href,path:w.location.pathname,
474
+ type:"custom",category:"custom",
475
+ label:name,
476
+ properties:props||{},
477
+ });
478
+ },
479
+ identify:function(){/* reserved */},
480
+ };
481
+
482
+ /* ── Cleanup ── */
483
+ w.addEventListener("beforeunload",function(){
484
+ clearInterval(heartbeatInterval);
485
+ clearInterval(flushTimer);
486
+ flush();
487
+ });
488
+
489
+ log("initialized for site:",siteId,"→",host);
490
+ })();