@stacksjs/analytics 0.70.54 → 0.70.55
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/package.json +3 -2
- package/src/drivers/fathom.ts +1 -0
- package/src/drivers/index.ts +2 -0
- package/src/drivers/self-hosted.ts +148 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/analytics",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.55",
|
|
5
5
|
"description": "Stacks Analytics. Privacy-friendly.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"types": "dist/index.d.ts",
|
|
42
42
|
"files": [
|
|
43
43
|
"README.md",
|
|
44
|
-
"dist"
|
|
44
|
+
"dist",
|
|
45
|
+
"src"
|
|
45
46
|
],
|
|
46
47
|
"scripts": {
|
|
47
48
|
"build": "bun build.ts",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const fathomWip = 1
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Hosted Analytics Driver
|
|
3
|
+
*
|
|
4
|
+
* This driver generates a minimal, privacy-focused tracking script
|
|
5
|
+
* that sends analytics data to your own API endpoint (powered by dynamodb-tooling analytics).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - No cookies required
|
|
9
|
+
* - Privacy-focused (hashed visitor IDs)
|
|
10
|
+
* - Do Not Track support
|
|
11
|
+
* - Page view tracking
|
|
12
|
+
* - Custom event tracking
|
|
13
|
+
* - Outbound link tracking (optional)
|
|
14
|
+
* - Hash-based routing support (optional)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface SelfHostedConfig {
|
|
18
|
+
/** Site ID for tracking */
|
|
19
|
+
siteId: string
|
|
20
|
+
/** API endpoint URL for collecting analytics */
|
|
21
|
+
apiEndpoint: string
|
|
22
|
+
/** Honor Do Not Track browser setting */
|
|
23
|
+
honorDnt?: boolean
|
|
24
|
+
/** Track hash changes as page views */
|
|
25
|
+
trackHashChanges?: boolean
|
|
26
|
+
/** Track outbound link clicks */
|
|
27
|
+
trackOutboundLinks?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate the self-hosted analytics tracking script
|
|
32
|
+
*/
|
|
33
|
+
export function generateSelfHostedScript(config: SelfHostedConfig): string {
|
|
34
|
+
if (!config.siteId || !config.apiEndpoint) {
|
|
35
|
+
return ''
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const siteId = escapeAttr(config.siteId)
|
|
39
|
+
const apiEndpoint = escapeAttr(config.apiEndpoint)
|
|
40
|
+
const honorDnt = config.honorDnt ? `if(n.doNotTrack==="1")return;` : ''
|
|
41
|
+
const hashTracking = config.trackHashChanges ? `w.addEventListener('hashchange',pv);` : ''
|
|
42
|
+
const outboundTracking = config.trackOutboundLinks
|
|
43
|
+
? `
|
|
44
|
+
d.addEventListener('click',function(e){
|
|
45
|
+
var a=e.target.closest('a');
|
|
46
|
+
if(a&&a.hostname!==location.hostname){
|
|
47
|
+
t('outbound',{url:a.href});
|
|
48
|
+
}
|
|
49
|
+
});`
|
|
50
|
+
: ''
|
|
51
|
+
|
|
52
|
+
return `<!-- Stacks Self-Hosted Analytics -->
|
|
53
|
+
<script data-site="${siteId}" data-api="${apiEndpoint}" defer>
|
|
54
|
+
(function(){
|
|
55
|
+
'use strict';
|
|
56
|
+
var d=document,w=window,n=navigator,s=d.currentScript;
|
|
57
|
+
var site=s.dataset.site,api=s.dataset.api;
|
|
58
|
+
${honorDnt}
|
|
59
|
+
var q=[],sid=Math.random().toString(36).slice(2);
|
|
60
|
+
function t(e,p){
|
|
61
|
+
var x=new XMLHttpRequest();
|
|
62
|
+
x.open('POST',api+'/collect',true);
|
|
63
|
+
x.setRequestHeader('Content-Type','application/json');
|
|
64
|
+
// Strip the URL's query string before sending. location.href includes
|
|
65
|
+
// tokens / session IDs / search terms that shouldn't reach the
|
|
66
|
+
// analytics backend. Same goes for document.referrer when it points
|
|
67
|
+
// back to our own domain (path is fine, query is not).
|
|
68
|
+
var safeUrl=location.origin+location.pathname+location.hash;
|
|
69
|
+
var safeRef=d.referrer?d.referrer.split('?')[0]:'';
|
|
70
|
+
x.send(JSON.stringify({
|
|
71
|
+
s:site,sid:sid,e:e,p:p||{},
|
|
72
|
+
u:safeUrl,r:safeRef,t:d.title,
|
|
73
|
+
sw:screen.width,sh:screen.height
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
function pv(){t('pageview');}
|
|
77
|
+
${hashTracking}
|
|
78
|
+
${outboundTracking}
|
|
79
|
+
if(d.readyState==='complete')pv();
|
|
80
|
+
else w.addEventListener('load',pv);
|
|
81
|
+
w.stacksAnalytics={track:function(n,v){t('event',{name:n,value:v});}};
|
|
82
|
+
})();
|
|
83
|
+
</script>`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate the head tag for including self-hosted analytics
|
|
88
|
+
* Compatible with Stacks docs config.ts head array format
|
|
89
|
+
*/
|
|
90
|
+
export function getSelfHostedAnalyticsHead(config: SelfHostedConfig): [string, Record<string, string>][] {
|
|
91
|
+
if (!config.siteId || !config.apiEndpoint) {
|
|
92
|
+
return []
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Return as an inline script tag configuration
|
|
96
|
+
// Note: For VitePress/Stacks docs, inline scripts need special handling
|
|
97
|
+
return [
|
|
98
|
+
['script', {
|
|
99
|
+
'data-site': config.siteId,
|
|
100
|
+
'data-api': config.apiEndpoint,
|
|
101
|
+
'defer': '',
|
|
102
|
+
'innerHTML': generateInlineScript(config),
|
|
103
|
+
}],
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate just the inline script content (without script tags)
|
|
109
|
+
*/
|
|
110
|
+
function generateInlineScript(config: SelfHostedConfig): string {
|
|
111
|
+
const honorDnt = config.honorDnt ? `if(n.doNotTrack==="1")return;` : ''
|
|
112
|
+
const hashTracking = config.trackHashChanges ? `w.addEventListener('hashchange',pv);` : ''
|
|
113
|
+
const outboundTracking = config.trackOutboundLinks
|
|
114
|
+
? `d.addEventListener('click',function(e){var a=e.target.closest('a');if(a&&a.hostname!==location.hostname){t('outbound',{url:a.href});}});`
|
|
115
|
+
: ''
|
|
116
|
+
|
|
117
|
+
return `(function(){
|
|
118
|
+
'use strict';
|
|
119
|
+
var d=document,w=window,n=navigator,s=d.currentScript;
|
|
120
|
+
var site=s.dataset.site,api=s.dataset.api;
|
|
121
|
+
${honorDnt}
|
|
122
|
+
var q=[],sid=Math.random().toString(36).slice(2);
|
|
123
|
+
function t(e,p){
|
|
124
|
+
var x=new XMLHttpRequest();
|
|
125
|
+
x.open('POST',api+'/collect',true);
|
|
126
|
+
x.setRequestHeader('Content-Type','application/json');
|
|
127
|
+
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}));
|
|
128
|
+
}
|
|
129
|
+
function pv(){t('pageview');}
|
|
130
|
+
${hashTracking}
|
|
131
|
+
${outboundTracking}
|
|
132
|
+
if(d.readyState==='complete')pv();
|
|
133
|
+
else w.addEventListener('load',pv);
|
|
134
|
+
w.stacksAnalytics={track:function(n,v){t('event',{name:n,value:v});}};
|
|
135
|
+
})();`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Escape attribute value for safe HTML insertion
|
|
140
|
+
*/
|
|
141
|
+
function escapeAttr(str: string): string {
|
|
142
|
+
return str
|
|
143
|
+
.replace(/&/g, '&')
|
|
144
|
+
.replace(/"/g, '"')
|
|
145
|
+
.replace(/'/g, ''')
|
|
146
|
+
.replace(/</g, '<')
|
|
147
|
+
.replace(/>/g, '>')
|
|
148
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './drivers'
|