@pageviews/tracker 0.1.0 → 0.2.3

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,145 @@
1
+ # @pageviews/tracker
2
+
3
+ The official JavaScript SDK for [Pageviews.ai](https://pageviews.ai) — a lightweight, cookie-free analytics platform built for privacy.
4
+
5
+ Use this package to add analytics to React, Vue, Next.js, or any other JavaScript framework with full programmatic control over pageview and event tracking.
6
+
7
+ > For simple script tag installation (no build step), see the [Script Tag guide](https://pageviews.ai/docs) instead.
8
+
9
+ ## Features
10
+
11
+ - Cookie-free and GDPR-compliant — no consent banner required
12
+ - Automatic pageview tracking for SPAs (React Router, Vue Router, Next.js App Router)
13
+ - Custom event tracking with properties and revenue data
14
+ - Engagement tracking — active time and scroll depth per page
15
+ - Auto-tracking for outbound links, file downloads, and form submissions
16
+ - Realtime User Explorer via `identify()`
17
+ - Full TypeScript support
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @pageviews/tracker
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```js
28
+ import { init, track, identify } from '@pageviews/tracker'
29
+
30
+ // Initialize once at your app's entry point
31
+ await init({ siteId: 'your-site-id' })
32
+
33
+ // Track custom events
34
+ track('signup', { props: { plan: 'pro' } })
35
+
36
+ // Identify a logged-in user for the Realtime User Explorer
37
+ identify({ userId: 'user-123' })
38
+ ```
39
+
40
+ Your site ID is available in the Pageviews dashboard under **Settings → Tracking**.
41
+
42
+ ## Framework Examples
43
+
44
+ ### Next.js (App Router)
45
+
46
+ ```tsx
47
+ // app/providers.tsx
48
+ 'use client'
49
+
50
+ import { useEffect } from 'react'
51
+ import { init, destroy } from '@pageviews/tracker'
52
+
53
+ export function Providers({ children }: { children: React.ReactNode }) {
54
+ useEffect(() => {
55
+ init({ siteId: 'your-site-id' })
56
+ return () => destroy()
57
+ }, [])
58
+
59
+ return <>{children}</>
60
+ }
61
+ ```
62
+
63
+ ### React
64
+
65
+ ```jsx
66
+ // Entry point or layout component
67
+ import { useEffect } from 'react'
68
+ import { init, destroy } from '@pageviews/tracker'
69
+
70
+ export function AnalyticsProvider({ children, siteId }) {
71
+ useEffect(() => {
72
+ init({ siteId })
73
+ return () => destroy()
74
+ }, [siteId])
75
+
76
+ return children
77
+ }
78
+ ```
79
+
80
+ ### Vue
81
+
82
+ ```js
83
+ // main.js
84
+ import { init } from '@pageviews/tracker'
85
+
86
+ init({ siteId: 'your-site-id' })
87
+ ```
88
+
89
+ ## API
90
+
91
+ ### `init(options): Promise<void>`
92
+
93
+ Initializes the tracker. Call once at your app's entry point.
94
+
95
+ ```ts
96
+ await init({
97
+ siteId: 'your-site-id', // required — from your Pageviews dashboard
98
+ endpoint?: string, // optional — custom ingest URL (self-hosted / proxy)
99
+ configEndpoint?: string, // optional — custom config URL
100
+ })
101
+ ```
102
+
103
+ On init, site configuration (routing mode, auto-tracking rules, scroll goals) is fetched from the Pageviews API and cached in `localStorage` for 1 hour.
104
+
105
+ ### `track(eventName, options?): void`
106
+
107
+ Sends a custom event.
108
+
109
+ ```ts
110
+ track('purchase', {
111
+ props: { plan: 'pro', annual: true },
112
+ revenue: { amount: 49, currency: 'USD' },
113
+ callback: (result) => console.log(result),
114
+ })
115
+ ```
116
+
117
+ ### `identify(data): void`
118
+
119
+ Associates a user identifier with the current session. Appears in the Realtime User Explorer in the dashboard. **Not persisted to the database** — stored in memory only for the duration of the session.
120
+
121
+ ```ts
122
+ identify({ userId: 'user-123' })
123
+ ```
124
+
125
+ ### `destroy(): void`
126
+
127
+ Stops all tracking and resets state. Useful for SPA cleanup or testing. `init()` can be called again after `destroy()`.
128
+
129
+ ## Privacy
130
+
131
+ - No cookies or persistent browser identifiers set
132
+ - Query parameters containing potential PII are stripped from URLs before sending — only UTM parameters and `ref` are kept
133
+ - Referrer query strings are stripped entirely — only the domain and path are sent
134
+ - IP addresses are hashed server-side with a daily-rotating salt and never stored in raw form
135
+ - No cross-site tracking
136
+
137
+ ## Full Documentation
138
+
139
+ For configuration options, advanced usage, and the script tag installation method, see the full docs:
140
+
141
+ **[pageviews.ai/docs/npm-module-at-pageviewstracker](https://pageviews.ai/docs/npm-module-at-pageviewstracker)**
142
+
143
+ ## License
144
+
145
+ MIT
@@ -35,6 +35,35 @@ function sendBeaconEvent(endpoint, payload, siteId) {
35
35
  }
36
36
 
37
37
  // src/core/utils.ts
38
+ const ALLOWED_QUERY_PARAMS = new Set([
39
+ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'ref',
40
+ ]);
41
+ function sanitizeUrl(url) {
42
+ try {
43
+ const u = new URL(url);
44
+ const toDelete = [];
45
+ u.searchParams.forEach((_, key) => {
46
+ if (!ALLOWED_QUERY_PARAMS.has(key))
47
+ toDelete.push(key);
48
+ });
49
+ toDelete.forEach(key => u.searchParams.delete(key));
50
+ return u.toString();
51
+ }
52
+ catch (_a) {
53
+ return url;
54
+ }
55
+ }
56
+ function sanitizeReferrer(referrer) {
57
+ if (!referrer)
58
+ return referrer;
59
+ try {
60
+ const u = new URL(referrer);
61
+ return u.origin + u.pathname;
62
+ }
63
+ catch (_a) {
64
+ return referrer;
65
+ }
66
+ }
38
67
  function getScreenSize() {
39
68
  return `${window.innerWidth}x${window.innerHeight}`;
40
69
  }
@@ -75,8 +104,8 @@ function createTracker(config) {
75
104
  const payload = {
76
105
  n: 'pageview',
77
106
  d: config.site,
78
- u: url || location.href,
79
- r: document.referrer || null,
107
+ u: sanitizeUrl(url || location.href),
108
+ r: sanitizeReferrer(document.referrer) || null,
80
109
  s: getScreenSize(),
81
110
  };
82
111
  if (config.hashRouting)
@@ -87,8 +116,8 @@ function createTracker(config) {
87
116
  const payload = {
88
117
  n: eventName,
89
118
  d: config.site,
90
- u: (options === null || options === void 0 ? void 0 : options.url) || location.href,
91
- r: document.referrer || null,
119
+ u: sanitizeUrl((options === null || options === void 0 ? void 0 : options.url) || location.href),
120
+ r: sanitizeReferrer(document.referrer) || null,
92
121
  s: getScreenSize(),
93
122
  };
94
123
  if (options === null || options === void 0 ? void 0 : options.props) {
@@ -113,7 +142,7 @@ function createTracker(config) {
113
142
  const payload = {
114
143
  n: 'identify',
115
144
  d: config.site,
116
- u: location.href,
145
+ u: sanitizeUrl(location.href),
117
146
  r: null,
118
147
  s: getScreenSize(),
119
148
  uid: data.userId,
@@ -284,7 +313,7 @@ function startEventTracking(trackFn, config) {
284
313
  return;
285
314
  }
286
315
  if (config.outboundLinks && isOutbound(link)) {
287
- trackFn('Outbound Link: Click', { props: { url: href } });
316
+ trackFn('Outbound Link: Click', { props: { url: cleanUrl } });
288
317
  }
289
318
  };
290
319
  document.addEventListener('click', clickHandler);
@@ -352,7 +381,7 @@ function setupTracking(config, tracker) {
352
381
  stopEngagement();
353
382
  startEngagement();
354
383
  let isFirstPageview = true;
355
- let currentUrl = location.href;
384
+ let currentUrl = sanitizeUrl(location.href);
356
385
  startNavTracking(() => {
357
386
  // Send engagement for previous page (skip on first pageview)
358
387
  if (!isFirstPageview) {
@@ -371,7 +400,7 @@ function setupTracking(config, tracker) {
371
400
  resetEngagement();
372
401
  }
373
402
  isFirstPageview = false;
374
- currentUrl = location.href;
403
+ currentUrl = sanitizeUrl(location.href);
375
404
  tracker.trackPageview();
376
405
  stopScrollGoals();
377
406
  if (config.scrollTargets.length > 0) {
@@ -33,6 +33,35 @@ function sendBeaconEvent(endpoint, payload, siteId) {
33
33
  }
34
34
 
35
35
  // src/core/utils.ts
36
+ const ALLOWED_QUERY_PARAMS = new Set([
37
+ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'ref',
38
+ ]);
39
+ function sanitizeUrl(url) {
40
+ try {
41
+ const u = new URL(url);
42
+ const toDelete = [];
43
+ u.searchParams.forEach((_, key) => {
44
+ if (!ALLOWED_QUERY_PARAMS.has(key))
45
+ toDelete.push(key);
46
+ });
47
+ toDelete.forEach(key => u.searchParams.delete(key));
48
+ return u.toString();
49
+ }
50
+ catch (_a) {
51
+ return url;
52
+ }
53
+ }
54
+ function sanitizeReferrer(referrer) {
55
+ if (!referrer)
56
+ return referrer;
57
+ try {
58
+ const u = new URL(referrer);
59
+ return u.origin + u.pathname;
60
+ }
61
+ catch (_a) {
62
+ return referrer;
63
+ }
64
+ }
36
65
  function getScreenSize() {
37
66
  return `${window.innerWidth}x${window.innerHeight}`;
38
67
  }
@@ -73,8 +102,8 @@ function createTracker(config) {
73
102
  const payload = {
74
103
  n: 'pageview',
75
104
  d: config.site,
76
- u: url || location.href,
77
- r: document.referrer || null,
105
+ u: sanitizeUrl(url || location.href),
106
+ r: sanitizeReferrer(document.referrer) || null,
78
107
  s: getScreenSize(),
79
108
  };
80
109
  if (config.hashRouting)
@@ -85,8 +114,8 @@ function createTracker(config) {
85
114
  const payload = {
86
115
  n: eventName,
87
116
  d: config.site,
88
- u: (options === null || options === void 0 ? void 0 : options.url) || location.href,
89
- r: document.referrer || null,
117
+ u: sanitizeUrl((options === null || options === void 0 ? void 0 : options.url) || location.href),
118
+ r: sanitizeReferrer(document.referrer) || null,
90
119
  s: getScreenSize(),
91
120
  };
92
121
  if (options === null || options === void 0 ? void 0 : options.props) {
@@ -111,7 +140,7 @@ function createTracker(config) {
111
140
  const payload = {
112
141
  n: 'identify',
113
142
  d: config.site,
114
- u: location.href,
143
+ u: sanitizeUrl(location.href),
115
144
  r: null,
116
145
  s: getScreenSize(),
117
146
  uid: data.userId,
@@ -282,7 +311,7 @@ function startEventTracking(trackFn, config) {
282
311
  return;
283
312
  }
284
313
  if (config.outboundLinks && isOutbound(link)) {
285
- trackFn('Outbound Link: Click', { props: { url: href } });
314
+ trackFn('Outbound Link: Click', { props: { url: cleanUrl } });
286
315
  }
287
316
  };
288
317
  document.addEventListener('click', clickHandler);
@@ -350,7 +379,7 @@ function setupTracking(config, tracker) {
350
379
  stopEngagement();
351
380
  startEngagement();
352
381
  let isFirstPageview = true;
353
- let currentUrl = location.href;
382
+ let currentUrl = sanitizeUrl(location.href);
354
383
  startNavTracking(() => {
355
384
  // Send engagement for previous page (skip on first pageview)
356
385
  if (!isFirstPageview) {
@@ -369,7 +398,7 @@ function setupTracking(config, tracker) {
369
398
  resetEngagement();
370
399
  }
371
400
  isFirstPageview = false;
372
- currentUrl = location.href;
401
+ currentUrl = sanitizeUrl(location.href);
373
402
  tracker.trackPageview();
374
403
  stopScrollGoals();
375
404
  if (config.scrollTargets.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pageviews/tracker",
3
- "version": "0.1.0",
3
+ "version": "0.2.3",
4
4
  "description": "Lightweight, cookie-free analytics tracker for PageViews.ai",
5
5
  "author": "Pageviews <hello@pageviews.ai>",
6
6
  "homepage": "https://pageviews.ai",
@@ -28,7 +28,7 @@
28
28
  }
29
29
  },
30
30
  "files": [
31
- "dist"
31
+ "dist/module"
32
32
  ],
33
33
  "scripts": {
34
34
  "build": "rollup -c",
package/dist/tracker.js DELETED
@@ -1 +0,0 @@
1
- !function(){"use strict";function t(t,e,n,i){if("undefined"==typeof fetch)return;const o={"Content-Type":"text/plain"};i&&(o["X-Site-ID"]=i),fetch(t,{method:"POST",headers:o,keepalive:!0,body:JSON.stringify(e)}).then(t=>{n&&n({status:t.status})}).catch(t=>{n&&n({error:t})})}function e(){return`${window.innerWidth}x${window.innerHeight}`}let n=null,i=0,o=0,r=!1,s=0,c=!1,a=0;function l(){const t=document.body||{},e=document.documentElement||{};return Math.max(t.scrollHeight||0,t.offsetHeight||0,e.scrollHeight||0,e.offsetHeight||0)}function u(){a=l()}function d(){"hidden"===document.visibilityState?r&&(o+=Date.now()-i,r=!1):"visible"===document.visibilityState&&(i=Date.now(),r=!0)}function v(){c||(c=!0,requestAnimationFrame(()=>{const t=function(){if(0===a)return 0;const t=window.innerHeight||0;if(a<=t)return 100;const e=window.scrollY||document.documentElement.scrollTop||0;return Math.round((e+t)/a*100)}();t>s&&(s=t),c=!1}))}function m(){document.removeEventListener("visibilitychange",d),document.removeEventListener("scroll",v),window.removeEventListener("resize",u)}function f(){const t=r?Date.now()-i:0;return{activeTime:o+t,maxScrollDepth:s}}const h=["pdf","xlsx","docx","txt","rtf","csv","exe","key","pps","ppt","pptx","7z","pkg","rar","gz","zip","avi","mov","mp4","mpeg","wmv","midi","mp3","wav","wma","dmg"];let p=null,g=null;function w(){p&&(document.removeEventListener("click",p),p=null),g&&(document.removeEventListener("submit",g,!0),g=null)}let y=null;function b(){y&&(document.removeEventListener("scroll",y),y=null)}function E(c,E){m(),m(),i=Date.now(),o=0,s=0,r=!0,a=l(),document.addEventListener("visibilitychange",d),document.addEventListener("scroll",v,{passive:!0}),window.addEventListener("resize",u,{passive:!0});let L=!0,S=location.href;!function(t,e){let i=location.pathname;const o=()=>{(e||location.pathname!==i)&&(i=location.pathname,t())};if(t(),e)window.addEventListener("hashchange",o),n=()=>window.removeEventListener("hashchange",o);else{const e=history.pushState;history.pushState=function(...t){e.apply(this,t),o()};const r=()=>{i=location.pathname,t()};window.addEventListener("popstate",r),n=()=>{history.pushState=e,window.removeEventListener("popstate",r)}}const r=e=>{e.persisted&&t()};window.addEventListener("pageshow",r);const s=n;n=()=>{null==s||s(),window.removeEventListener("pageshow",r)}}(()=>{if(!L){const n=f();n.activeTime>=1e3&&t(c.endpoint,{n:"engagement",d:c.site,u:S,r:null,s:e(),sd:n.maxScrollDepth,e:n.activeTime},void 0,c.siteId),i=Date.now(),o=0,s=0,r=!0}L=!1,S=location.href,E.trackPageview(),b(),c.scrollTargets.length>0&&function(t,e,n){const i=e.filter(t=>t.path===n);if(0===i.length)return;const o=new Set;let r=!1;y=()=>{r||(r=!0,requestAnimationFrame(()=>{var e;const n=Math.max(document.documentElement.scrollHeight||0,(null===(e=document.body)||void 0===e?void 0:e.scrollHeight)||0),s=window.innerHeight||0,c=window.scrollY||0,a=n<=s?100:Math.round((c+s)/n*100);for(const e of i)!o.has(e.goalName)&&a>=e.threshold&&(o.add(e.goalName),t(e.goalName));r=!1}))},document.addEventListener("scroll",y,{passive:!0})}(t=>E.track(t),c.scrollTargets,location.pathname)},c.hashRouting),function(t,e){w(),p=n=>{const i=function(t){let e=t;for(;e&&"A"!==e.tagName;)e=e.parentElement;return e}(n.target);if(!i||!i.href)return;const o=i.href,r=o.split("?")[0];e.fileDownloads&&function(t){var e;const n=null===(e=t.split("?")[0].split(".").pop())||void 0===e?void 0:e.toLowerCase();return!!n&&h.includes(n)}(o)?t("File Download",{props:{url:r}}):e.outboundLinks&&function(t){return""!==t.host&&t.host!==location.host}(i)&&t("Outbound Link: Click",{props:{url:o}})},document.addEventListener("click",p),e.formSubmissions&&(g=e=>{const n=e.target;(n.hasAttribute("novalidate")||n.checkValidity())&&t("Form Submission")},document.addEventListener("submit",g,!0))}((t,e)=>E.track(t,e),{outboundLinks:c.outboundLinks,fileDownloads:c.fileDownloads,formSubmissions:c.formSubmissions});const k=()=>{if("hidden"===document.visibilityState){const n=f();n.activeTime>=1e3&&function(e,n,i){const o=JSON.stringify(n);if("undefined"!=typeof navigator&&navigator.sendBeacon){const t=new Blob([o],{type:"text/plain"});if(navigator.sendBeacon(e,t))return}t(e,n,void 0,i)}(c.endpoint,{n:"engagement",d:c.site,u:S,r:null,s:e(),sd:n.maxScrollDepth,e:n.activeTime},c.siteId)}};return document.addEventListener("visibilitychange",k),()=>{null==n||n(),n=null,m(),w(),b(),document.removeEventListener("visibilitychange",k)}}!function(){var n;const i=document.currentScript;if(!i)return;const o="undefined"!=typeof __PAGEVIEWS_CONFIG__?__PAGEVIEWS_CONFIG__:null;if(!o)return;const r=i.getAttribute("data-api")||void 0,s=function(t,e){return e||"https://data.pageviews.ai/api/event"}(i.src,r),c=Object.assign(Object.assign({},o),{endpoint:s}),{tracker:a}=function(n){const i=function(n){const i="localhost"===(o=location.hostname)||/^127(\.[0-9]+){0,2}\.[0-9]+$/.test(o)||"[::1]"===o||function(){try{return"true"===window.localStorage.getItem("pageviews_ignore")}catch(t){return!1}}()||function(){const t=window,e=navigator;return!!(t._phantom||t.__nightmare||e.webdriver||t.Cypress||t.__puppeteer_evaluation_script__||t.__playwright)}();var o;function r(e,o){i?null==o||o():t(n.endpoint,e,o,n.siteId)}return{trackPageview:function(t){const i={n:"pageview",d:n.site,u:t||location.href,r:document.referrer||null,s:e()};n.hashRouting&&(i.h=!0),r(i)},track:function(t,i){const o={n:t,d:n.site,u:(null==i?void 0:i.url)||location.href,r:document.referrer||null,s:e()};if(null==i?void 0:i.props){const t={};for(const[e,n]of Object.entries(i.props))t[e]=String(n);o.p=t}if(null==i?void 0:i.revenue){const{amount:t,currency:e}=i.revenue;t>0&&t<=1e9&&e&&(o.rv={a:t,c:e})}n.hashRouting&&(o.h=!0),r(o,null==i?void 0:i.callback)},identify:function(t){r({n:"identify",d:n.site,u:location.href,r:null,s:e(),uid:t.userId})}}}(n);let o=null;if("prerender"===document.visibilityState){const t=()=>{"visible"===document.visibilityState&&(document.removeEventListener("visibilitychange",t),o=E(n,i))};return document.addEventListener("visibilitychange",t),{tracker:i,destroy:()=>{document.removeEventListener("visibilitychange",t),null==o||o()}}}return o=E(n,i),{tracker:i,destroy:()=>{null==o||o()}}}(c),l=i.getAttribute("data-user-id");l&&a.identify({userId:l});const u=(t,...e)=>{"track"===t?a.track(e[0],e[1]):"identify"===t&&a.identify(e[0])},d=null===(n=window.pageviews)||void 0===n?void 0:n.q;Array.isArray(d)&&d.forEach(t=>u(t[0],...t.slice(1))),window.pageviews=u}()}();