@stacksjs/analytics 0.71.1 → 0.71.10

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.
@@ -0,0 +1,12 @@
1
+ /** Should this request count as a pageview? Pure, testable. */
2
+ export declare function isPageviewRequest(method: string, pathname: string, headers: Headers): boolean;
3
+ /** Coarse classification - enough for the devices/browsers dashboards, nothing identifying. */
4
+ export declare function classifyAgent(userAgent: string): { device: string, browser: string };
5
+ /** The referrer's host, or null for direct/self traffic. */
6
+ export declare function referrerHost(referrer: string | null, ownHost: string): string | null;
7
+ /**
8
+ * Record one pageview. Fire-and-forget from the serving hot path - callers
9
+ * do `void recordPageview(...)`; a failed insert loses one statistic, never
10
+ * a page render.
11
+ */
12
+ export declare function recordPageview(req: Request): Promise<void>;
@@ -0,0 +1 @@
1
+ import{db,sqlDateTime}from"@stacksjs/database";const ASSET_EXT=/\.(?:css|js|mjs|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|txt|xml|json|webmanifest|mp[34]|webm|pdf|zip)$/i;export function isPageviewRequest(method,pathname,headers){if(method!=="GET")return!1;if(ASSET_EXT.test(pathname))return!1;if(pathname.startsWith("/api/")||pathname.startsWith("/__")||pathname.startsWith("/_"))return!1;const accept=headers.get("accept")??"";if(accept&&!accept.includes("text/html")&&!accept.includes("*/*"))return!1;const agent=(headers.get("user-agent")??"").toLowerCase();if(!agent)return!1;if(/bot|crawler|spider|preview|monitor|curl|wget|python-requests|headless/.test(agent))return!1;return!0}export function classifyAgent(userAgent){const agent=userAgent.toLowerCase(),device=/ipad|tablet/.test(agent)?"tablet":/mobi|iphone|android/.test(agent)?"mobile":"desktop",browser=agent.includes("edg/")?"edge":agent.includes("opr/")||agent.includes("opera")?"opera":agent.includes("firefox/")?"firefox":agent.includes("chrome/")||agent.includes("crios/")?"chrome":agent.includes("safari/")?"safari":"other";return{device,browser}}export function referrerHost(referrer,ownHost){if(!referrer)return null;try{const host=new URL(referrer).host.toLowerCase();return host&&host!==ownHost.toLowerCase()?host:null}catch{return null}}export async function recordPageview(req){try{const url=new URL(req.url);if(!isPageviewRequest(req.method,url.pathname,req.headers))return;const agent=req.headers.get("user-agent")??"",{device,browser}=classifyAgent(agent),now=sqlDateTime(new Date);await db.insertInto("analytics_events").values({name:"pageview",category:"web",path:url.pathname,properties:JSON.stringify({host:req.headers.get("x-forwarded-host")?.split(",")[0]?.trim()||url.host,referrer:referrerHost(req.headers.get("referer"),url.host),device,browser}),created_at:now,updated_at:now}).execute()}catch{}}
@@ -0,0 +1 @@
1
+ export declare const fathomWip: 1;
@@ -0,0 +1 @@
1
+ export const fathomWip=1;
@@ -0,0 +1,2 @@
1
+ export * from './fathom';
2
+ export * from './self-hosted';
@@ -0,0 +1 @@
1
+ export*from"./fathom";export*from"./self-hosted";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Generate the self-hosted analytics tracking script
3
+ */
4
+ export declare function generateSelfHostedScript(config: SelfHostedConfig): string;
5
+ /**
6
+ * Generate the head tag for including self-hosted analytics
7
+ * Compatible with Stacks docs config.ts head array format
8
+ */
9
+ export declare function getSelfHostedAnalyticsHead(config: SelfHostedConfig): [string, Record<string, string>][];
10
+ /**
11
+ * Self-Hosted Analytics Driver
12
+ *
13
+ * This driver generates a minimal, privacy-focused tracking script
14
+ * that sends analytics data to your own API endpoint (powered by dynamodb-tooling analytics).
15
+ *
16
+ * Features:
17
+ * - No cookies required
18
+ * - Privacy-focused (hashed visitor IDs)
19
+ * - Do Not Track support
20
+ * - Page view tracking
21
+ * - Custom event tracking
22
+ * - Outbound link tracking (optional)
23
+ * - Hash-based routing support (optional)
24
+ */
25
+ export declare interface SelfHostedConfig {
26
+ siteId: string
27
+ apiEndpoint: string
28
+ honorDnt?: boolean
29
+ trackHashChanges?: boolean
30
+ trackOutboundLinks?: boolean
31
+ }
@@ -0,0 +1,56 @@
1
+ export function generateSelfHostedScript(config){if(!config.siteId||!config.apiEndpoint)return"";const siteId=escapeAttr(config.siteId),apiEndpoint=escapeAttr(config.apiEndpoint),honorDnt=config.honorDnt?'if(n.doNotTrack==="1")return;':"",hashTracking=config.trackHashChanges?"w.addEventListener('hashchange',pv);":"",outboundTracking=config.trackOutboundLinks?`
2
+ d.addEventListener('click',function(e){
3
+ var a=e.target.closest('a');
4
+ if(a&&a.hostname!==location.hostname){
5
+ t('outbound',{url:a.href});
6
+ }
7
+ });`:"";return`<!-- Stacks Self-Hosted Analytics -->
8
+ <script data-site="${siteId}" data-api="${apiEndpoint}" defer>
9
+ (function(){
10
+ 'use strict';
11
+ var d=document,w=window,n=navigator,s=d.currentScript;
12
+ var site=s.dataset.site,api=s.dataset.api;
13
+ ${honorDnt}
14
+ var q=[],sid=Math.random().toString(36).slice(2);
15
+ function t(e,p){
16
+ var x=new XMLHttpRequest();
17
+ x.open('POST',api+'/collect',true);
18
+ x.setRequestHeader('Content-Type','application/json');
19
+ // Strip the URL's query string before sending. location.href includes
20
+ // tokens / session IDs / search terms that shouldn't reach the
21
+ // analytics backend. Same goes for document.referrer when it points
22
+ // back to our own domain (path is fine, query is not).
23
+ var safeUrl=location.origin+location.pathname+location.hash;
24
+ var safeRef=d.referrer?d.referrer.split('?')[0]:'';
25
+ x.send(JSON.stringify({
26
+ s:site,sid:sid,e:e,p:p||{},
27
+ u:safeUrl,r:safeRef,t:d.title,
28
+ sw:screen.width,sh:screen.height
29
+ }));
30
+ }
31
+ function pv(){t('pageview');}
32
+ ${hashTracking}
33
+ ${outboundTracking}
34
+ if(d.readyState==='complete')pv();
35
+ else w.addEventListener('load',pv);
36
+ w.stacksAnalytics={track:function(n,v){t('event',{name:n,value:v});}};
37
+ })();
38
+ </script>`}export function getSelfHostedAnalyticsHead(config){if(!config.siteId||!config.apiEndpoint)return[];return[["script",{"data-site":config.siteId,"data-api":config.apiEndpoint,defer:"",innerHTML:generateInlineScript(config)}]]}function generateInlineScript(config){const honorDnt=config.honorDnt?'if(n.doNotTrack==="1")return;':"",hashTracking=config.trackHashChanges?"w.addEventListener('hashchange',pv);":"",outboundTracking=config.trackOutboundLinks?"d.addEventListener('click',function(e){var a=e.target.closest('a');if(a&&a.hostname!==location.hostname){t('outbound',{url:a.href});}});":"";return`(function(){
39
+ 'use strict';
40
+ var d=document,w=window,n=navigator,s=d.currentScript;
41
+ var site=s.dataset.site,api=s.dataset.api;
42
+ ${honorDnt}
43
+ var q=[],sid=Math.random().toString(36).slice(2);
44
+ function t(e,p){
45
+ var x=new XMLHttpRequest();
46
+ x.open('POST',api+'/collect',true);
47
+ x.setRequestHeader('Content-Type','application/json');
48
+ var safeUrl=location.origin+location.pathname+location.hash;var safeRef=d.referrer?d.referrer.split('?')[0]:'';x.send(JSON.stringify({s:site,sid:sid,e:e,p:p||{},u:safeUrl,r:safeRef,t:d.title,sw:screen.width,sh:screen.height}));
49
+ }
50
+ function pv(){t('pageview');}
51
+ ${hashTracking}
52
+ ${outboundTracking}
53
+ if(d.readyState==='complete')pv();
54
+ else w.addEventListener('load',pv);
55
+ w.stacksAnalytics={track:function(n,v){t('event',{name:n,value:v});}};
56
+ })();`}function escapeAttr(str){return str.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/'/g,"&#39;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
@@ -0,0 +1,2 @@
1
+ export { classifyAgent, isPageviewRequest, recordPageview, referrerHost } from './capture';
2
+ export * from './drivers/index';
package/dist/index.js CHANGED
@@ -1,57 +1 @@
1
- // @bun
2
- var W=1;function j(m){if(!m.siteId||!m.apiEndpoint)return"";let x=q(m.siteId),w=q(m.apiEndpoint),P=m.honorDnt?'if(n.doNotTrack==="1")return;':"",v=m.trackHashChanges?"w.addEventListener('hashchange',pv);":"",y=m.trackOutboundLinks?`
3
- d.addEventListener('click',function(e){
4
- var a=e.target.closest('a');
5
- if(a&&a.hostname!==location.hostname){
6
- t('outbound',{url:a.href});
7
- }
8
- });`:"";return`<!-- Stacks Self-Hosted Analytics -->
9
- <script data-site="${x}" data-api="${w}" defer>
10
- (function(){
11
- 'use strict';
12
- var d=document,w=window,n=navigator,s=d.currentScript;
13
- var site=s.dataset.site,api=s.dataset.api;
14
- ${P}
15
- var q=[],sid=Math.random().toString(36).slice(2);
16
- function t(e,p){
17
- var x=new XMLHttpRequest();
18
- x.open('POST',api+'/collect',true);
19
- x.setRequestHeader('Content-Type','application/json');
20
- // Strip the URL's query string before sending. location.href includes
21
- // tokens / session IDs / search terms that shouldn't reach the
22
- // analytics backend. Same goes for document.referrer when it points
23
- // back to our own domain (path is fine, query is not).
24
- var safeUrl=location.origin+location.pathname+location.hash;
25
- var safeRef=d.referrer?d.referrer.split('?')[0]:'';
26
- x.send(JSON.stringify({
27
- s:site,sid:sid,e:e,p:p||{},
28
- u:safeUrl,r:safeRef,t:d.title,
29
- sw:screen.width,sh:screen.height
30
- }));
31
- }
32
- function pv(){t('pageview');}
33
- ${v}
34
- ${y}
35
- if(d.readyState==='complete')pv();
36
- else w.addEventListener('load',pv);
37
- w.stacksAnalytics={track:function(n,v){t('event',{name:n,value:v});}};
38
- })();
39
- </script>`}function z(m){if(!m.siteId||!m.apiEndpoint)return[];return[["script",{"data-site":m.siteId,"data-api":m.apiEndpoint,defer:"",innerHTML:R(m)}]]}function R(m){let x=m.honorDnt?'if(n.doNotTrack==="1")return;':"",w=m.trackHashChanges?"w.addEventListener('hashchange',pv);":"",P=m.trackOutboundLinks?"d.addEventListener('click',function(e){var a=e.target.closest('a');if(a&&a.hostname!==location.hostname){t('outbound',{url:a.href});}});":"";return`(function(){
40
- 'use strict';
41
- var d=document,w=window,n=navigator,s=d.currentScript;
42
- var site=s.dataset.site,api=s.dataset.api;
43
- ${x}
44
- var q=[],sid=Math.random().toString(36).slice(2);
45
- function t(e,p){
46
- var x=new XMLHttpRequest();
47
- x.open('POST',api+'/collect',true);
48
- x.setRequestHeader('Content-Type','application/json');
49
- var safeUrl=location.origin+location.pathname+location.hash;var safeRef=d.referrer?d.referrer.split('?')[0]:'';x.send(JSON.stringify({s:site,sid:sid,e:e,p:p||{},u:safeUrl,r:safeRef,t:d.title,sw:screen.width,sh:screen.height}));
50
- }
51
- function pv(){t('pageview');}
52
- ${w}
53
- ${P}
54
- if(d.readyState==='complete')pv();
55
- else w.addEventListener('load',pv);
56
- w.stacksAnalytics={track:function(n,v){t('event',{name:n,value:v});}};
57
- })();`}function q(m){return m.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/'/g,"&#39;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}export{K as referrerHost,J as recordPageview,H as isPageviewRequest,z as getSelfHostedAnalyticsHead,j as generateSelfHostedScript,W as fathomWip,G as classifyAgent};
1
+ export{classifyAgent,isPageviewRequest,recordPageview,referrerHost}from"./capture";export*from"./drivers";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/analytics",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.71.1",
5
+ "version": "0.71.10",
6
6
  "description": "Stacks Analytics. Privacy-friendly.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [