@privatrak/tracker 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ This changelog covers the npm package **`@privatrak/tracker`** — the tracking
4
+ script that runs on customers' own sites. It follows the package's own semver
5
+ line, which is independent of the hosted Privatrak service — a change to the
6
+ service may leave no entry here, and the other way round. The Nuxt module,
7
+ [`@privatrak/nuxt`](https://www.npmjs.com/package/@privatrak/nuxt), is a
8
+ separate package and ships a third changelog of its own.
9
+
10
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
11
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
12
+
13
+ ## [Unreleased]
14
+
15
+ Nothing has been published to npm yet; entries accumulate here until the first
16
+ release. Until then these describe the shape of that release rather than a
17
+ change anyone has to migrate.
18
+
19
+ ### Changed
20
+
21
+ - The Nuxt module moved out of this package into
22
+ [`@privatrak/nuxt`](https://www.npmjs.com/package/@privatrak/nuxt). There is
23
+ no `@privatrak/tracker/nuxt` subpath; Nuxt apps install the module, which
24
+ depends on this package.
25
+ - The Element Picker is no longer linked into the npm bundle. It is fetched from
26
+ the configured `apiHost` on demand, the same way a `<script src>` page gets
27
+ it, which takes `dist/index.mjs` from 71.6 kB to 16.4 kB raw (15.9 kB to
28
+ 4.9 kB gzipped). Only an account holder who activates the picker ever loads
29
+ it, so visitors no longer download an editor they cannot open. The consequence
30
+ is that activating the picker now needs `apiHost` reachable from the page: if
31
+ it is not, the tracker logs `Could not load the Element Picker` and carries on
32
+ tracking.
33
+ - `sideEffects` is `false`. Importing the package does nothing on its own —
34
+ `init()` does — so a bundler may drop whatever a consumer does not import. The
35
+ array that was here before only existed to protect the picker's side-effect
36
+ import, which is gone.
37
+ - The published tarball is the ESM entry and its types, and nothing else: 33 kB
38
+ unpacked across 6 files, down from 368 kB across 31. `tracker.js` and
39
+ `tracker-picker.js` are for the `<script src>` route and are served by the
40
+ API, not by npm, and the declarations are flattened into one self-contained
41
+ `dist/index.d.mts` rather than a graph rooted in 207 kB of generated OpenAPI
42
+ types.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omexa LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # @privatrak/tracker
2
+
3
+ The tracking script behind [Privatrak](https://privatrak.com), a privacy-first
4
+ product analytics service. Add it to a site and it starts recording pageviews,
5
+ clicks and form submissions on its own — no manual instrumentation, no `track()`
6
+ calls sprinkled through your components, no tagging plan to maintain. It sets no
7
+ cookies, writes nothing to `localStorage` or `sessionStorage`, does no browser
8
+ fingerprinting, and never reads the values a visitor typed into a form. Visitors
9
+ are counted server-side from an irreversible daily hash of IP and User-Agent, so
10
+ there is no identifier to store on the device, nothing to ask consent for, and
11
+ no cookie banner to show. The browser bundle is a single ~3 KB IIFE.
12
+
13
+ Using Nuxt? Install [`@privatrak/nuxt`](https://www.npmjs.com/package/@privatrak/nuxt)
14
+ instead — it wraps this package in a Nuxt module with a `useTracker()` composable.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @privatrak/tracker
20
+ ```
21
+
22
+ Or skip the bundler entirely and load the hosted script, which Privatrak serves
23
+ for you:
24
+
25
+ ```html
26
+ <script src="https://api.privatrak.com/tracker.js" data-api-key="stk_your_project_key"></script>
27
+ ```
28
+
29
+ The script tag route needs nothing built and nothing redeployed when a setting
30
+ changes: the options below have `data-*` attributes too, and the API host is
31
+ read from the script's own `src`. The rest of this README uses the npm form.
32
+
33
+ ## Quick start
34
+
35
+ ```js
36
+ import { init, track, setTraits } from '@privatrak/tracker';
37
+
38
+ init({
39
+ apiKey: 'stk_your_project_key',
40
+ apiHost: 'https://api.privatrak.com',
41
+ });
42
+
43
+ // Everything below is optional — autocapture already records pageviews,
44
+ // clicks and form submissions, including single-page-app navigations.
45
+
46
+ track('checkout-completed', { plan: 'pro' });
47
+ setTraits(['paid', 'admin']);
48
+ ```
49
+
50
+ Call `init()` once, as early as you can — from your app's entry module, or a
51
+ client-side bootstrap file. It is a no-op during server-side rendering, so it is
52
+ safe to import from code that also runs on the server. Your project's API key is
53
+ in the Privatrak dashboard under **Settings → API Keys**; it is a public,
54
+ write-only key and is meant to ship in client code.
55
+
56
+ Elements can be labelled in markup instead of in code, which is how most
57
+ feature-level analytics end up being defined:
58
+
59
+ ```html
60
+ <button data-track="signup-cta" data-track-plan="pro" data-track-source="header">
61
+ Sign up
62
+ </button>
63
+ ```
64
+
65
+ A click on that button is recorded with `data_track: "signup-cta"` and
66
+ `data_track_attrs: { plan: "pro", source: "header" }`. If the click lands on
67
+ something inside a labelled element, the tracker walks up the DOM to find it.
68
+
69
+ ## Configuration
70
+
71
+ Every option is optional except `apiKey`. Pass them to `init()`, or set the
72
+ matching attribute on the hosted `<script>` tag. Anything you do not set follows
73
+ your project's settings from the dashboard, which the tracker fetches at
74
+ startup; values passed to `init()` always win over the server's.
75
+
76
+ | Option | Script attribute | Type | Default | Description |
77
+ |--------|------------------|------|---------|-------------|
78
+ | `apiKey` | `data-api-key` | `string` | — | Project API key. Required; without it nothing is sent. |
79
+ | `apiHost` | `data-api-host` | `string` | the page's own origin (the hosted script uses the origin of its `src`) | Base URL of the Privatrak API, e.g. `https://api.privatrak.com`. |
80
+ | `autocapture` | `data-autocapture` | `boolean` | `true` | Record clicks and form submissions automatically. |
81
+ | `autocapturePageviews` | `data-autocapture` | `boolean` | `true` | Record pageviews, including SPA navigations. |
82
+ | `autocaptureElements` | — | `string[]` | `a`, `button`, `input[type=submit]`, plus ARIA roles such as `[role=button]`, `[role=menuitem]`, `[role=tab]` | Selectors treated as interactive click targets. |
83
+ | `walkDepth` | `data-walk-depth` | `number` | `3` | How many ancestors to walk up when looking for an interactive element or a `data-track` label. |
84
+ | `maxTextLength` | `data-max-text-length` | `number` | `100` | Element text is truncated to this many characters. |
85
+ | `flushIntervalMs` | `data-flush-interval` | `number` | `5000` | How often the queue is sent. |
86
+ | `maxQueueSize` | `data-max-queue-size` | `number` | `10` | Queue length that triggers an immediate send. |
87
+ | `piiUrlParams` | `data-pii-params` | `string[]` | `['email', 'token', 'key', 'password', 'secret']` | Query parameters whose values are replaced with `:redacted` before the URL is sent. |
88
+ | `piiPathPatterns` | `data-pii-path-patterns` | `string[]` | `[]` | Regular expressions whose matching path segments are replaced with `:redacted`. |
89
+ | `normalizePathIds` | `data-normalize-path-ids` | `boolean` | `true` | Collapse ID-shaped path segments to `:id`, so `/orders/4711` and `/orders/4712` are one page. |
90
+ | `excludedUrls` | `data-excluded-urls` | `string[]` | `[]` | URL patterns that are not tracked at all. |
91
+ | `samplingRate` | `data-sampling-rate` | `number` | `1.0` | Fraction of visitors recorded, from `0` to `1`. |
92
+ | `traits` | `data-traits` | `string[]` | `[]` | Session labels attached to every event in the batch — see `setTraits()`. |
93
+
94
+ List options are comma-separated in an attribute: `data-pii-params="email,token"`.
95
+ The two autocapture switches share one attribute, which takes a list of what to
96
+ keep: `data-autocapture="pageviews,clicks"`, or `data-autocapture="none"` to
97
+ record nothing automatically. Add `data-manual-init` to stop the script
98
+ initialising itself, and call `window.tracker.init({ ... })` when you are ready.
99
+
100
+ ### The Element Picker
101
+
102
+ Privatrak's point-and-click element picker is a second file,
103
+ `tracker-picker.js`, and it is not part of this package — your visitors never
104
+ download it. It is fetched from `apiHost` the first time someone signed in to
105
+ the Privatrak dashboard activates the picker on your site, which is also the
106
+ only time it can run. If that fetch fails — no `apiHost` was configured, or the
107
+ API is not reachable from the page — the tracker logs a warning and carries on
108
+ tracking; nothing else is affected.
109
+
110
+ ## API
111
+
112
+ ### `init(config?): void`
113
+
114
+ Starts the tracker: applies the configuration, records the first pageview, and
115
+ attaches the delegated `click` and `submit` listeners plus the History API
116
+ patches that detect SPA navigation. Calling it twice logs a warning and does
117
+ nothing the second time. Returns immediately; the first batch leaves once the
118
+ project's settings have loaded.
119
+
120
+ ### `track(name, attrs?): void`
121
+
122
+ Records a custom event. `name` is the feature label you will see in the
123
+ dashboard, `attrs` an optional flat object of string values, equivalent to
124
+ `data-track-*` attributes in markup.
125
+
126
+ ```js
127
+ track('invite-sent', { role: 'editor' });
128
+ ```
129
+
130
+ ### `setTraits(traits): void`
131
+
132
+ Labels the current session with developer-chosen strings — `['paid', 'trial']`,
133
+ `['plan:pro']`, a feature-flag bucket — which every subsequent event carries and
134
+ which the dashboard can filter and break down by. Traits are your own labels,
135
+ not personal data: at most 50 per batch, 64 characters each. Call it whenever
136
+ what you know about the session changes.
137
+
138
+ ### `version: string`
139
+
140
+ The package version of the build you imported, baked in at build time. On the
141
+ hosted script the same value is at `window.tracker.version`. Quote it in a bug
142
+ report.
143
+
144
+ ### Types
145
+
146
+ `TrackerInitConfig` (the shape `init()` takes) and `DataTrackAttrs` are exported
147
+ as types.
148
+
149
+ ## What gets captured
150
+
151
+ **Pageview** — on load and on every SPA navigation (`pushState`,
152
+ `replaceState`, `popstate`). Navigations to the same URL are not recorded twice.
153
+ Fields: `page_url`, `referrer`, `timestamp`.
154
+
155
+ **Click** — on anything in `autocaptureElements`, or anything carrying
156
+ `data-track`, via a single delegated listener on `document`. A click on a
157
+ non-interactive child walks up to `walkDepth` ancestors to find the real target.
158
+ Fields: `element_tag`, `element_id`, `element_class`, `element_text` (truncated),
159
+ `data_track`, `data_track_attrs`, plus `page_url` and `timestamp`.
160
+
161
+ **Form submit** — on submit, with the form's own identity and labels. Field
162
+ values are never read. Fields: `element_tag`, `element_id`, `element_class`,
163
+ `data_track`, `data_track_attrs`, `page_url`, `timestamp`.
164
+
165
+ **Custom** — whatever you pass to `track()`.
166
+
167
+ Password fields, hidden fields and inputs whose `autocomplete` starts with `cc-`
168
+ are skipped outright, so they are never even click targets.
169
+
170
+ ### Delivery
171
+
172
+ Events are queued in memory and sent in batches to `POST /api/events` — every 5
173
+ seconds, or as soon as 10 are waiting, or immediately when the tab is hidden.
174
+ Normal sends use `fetch()` with `keepalive`; the tab-hidden send uses
175
+ `navigator.sendBeacon()` so it survives the page going away. A failed batch is
176
+ re-queued for the next cycle, and the queue is capped at 100 events so a broken
177
+ connection cannot grow it without bound.
178
+
179
+ ### URLs
180
+
181
+ The tracker sends one absolute URL per event and the API splits it into origin,
182
+ path, query and fragment. Origin plus path is the page's identity, so
183
+ `/search?q=analytics` and `/search?q=funnels` are the same page, counted once,
184
+ while the parameters stay individually filterable. Redaction happens here in the
185
+ browser, before anything is sent: `piiUrlParams`, `piiPathPatterns` and
186
+ `normalizePathIds` rewrite the URL first.
187
+
188
+ ## Privacy
189
+
190
+ This is the whole point of the package, so it is worth being specific.
191
+
192
+ - **No cookies and no storage.** Nothing is written to `document.cookie`,
193
+ `localStorage` or `sessionStorage` while tracking. There is no device
194
+ identifier, which is why no consent banner is required for it.
195
+ - **No fingerprinting.** No canvas, WebGL or AudioContext probing, no navigator
196
+ enumeration.
197
+ - **Visitors are counted server-side.** The API derives a session ID from
198
+ `HMAC(IP + User-Agent + project, daily key)` and stores only that hash — raw
199
+ IP and User-Agent are never written down. The key rotates at midnight in the
200
+ project's timezone, so yesterday's sessions cannot be linked to today's. It is
201
+ the model Plausible Analytics uses, which CNIL has found acceptable without
202
+ consent.
203
+ - **Form values are never captured.** Not opt-in, not redacted afterwards — the
204
+ tracker does not read them.
205
+ - **Element text is truncated** to `maxTextLength` (100) characters, and
206
+ sensitive inputs are excluded from tracking entirely.
207
+ - **URLs are cleaned in the browser**, before the event leaves the page.
208
+
209
+ More detail, and the data processing terms, are at
210
+ [privatrak.com/privacy](https://privatrak.com/privacy).
211
+
212
+ ## Links
213
+
214
+ - [Privatrak](https://privatrak.com) — the product this reports into
215
+ - [How it works](https://privatrak.com/how-it-works) · [Features](https://privatrak.com/features) · [Pricing](https://privatrak.com/pricing)
216
+ - [`@privatrak/nuxt`](https://www.npmjs.com/package/@privatrak/nuxt) — the Nuxt module built on this package
217
+ - [Contact](https://privatrak.com/contact) — questions and bug reports
218
+
219
+ ## Versioning and license
220
+
221
+ This package is versioned independently of the hosted product. Changes are
222
+ recorded in `CHANGELOG.md`, shipped in the package. Released under the MIT
223
+ license (`LICENSE`): it runs on customers' own sites, so it is licensed
224
+ separately from the rest of the service.
@@ -0,0 +1,32 @@
1
+ type DataTrackAttrs = Record<string, string>;
2
+ interface TrackerInitConfig {
3
+ apiKey?: string;
4
+ apiHost?: string;
5
+ flushIntervalMs?: number;
6
+ maxQueueSize?: number;
7
+ walkDepth?: number;
8
+ maxTextLength?: number;
9
+ autocapturePageviews?: boolean;
10
+ autocapture?: boolean;
11
+ autocaptureElements?: string[];
12
+ piiUrlParams?: string[];
13
+ normalizePathIds?: boolean;
14
+ piiPathPatterns?: string[];
15
+ excludedUrls?: string[];
16
+ samplingRate?: number;
17
+ traits?: string[];
18
+ }
19
+
20
+ /**
21
+ * Version of this tracker build, substituted from `package.json` at build time
22
+ * (see `src/globals.d.ts`). Exposed as `window.tracker.version` on the served
23
+ * script and exported from the npm entry point, so a bug report can name the
24
+ * exact build it came from.
25
+ */
26
+ declare const version: string;
27
+ declare function init(initConfig?: TrackerInitConfig): void;
28
+ declare function track(name: string, attrs?: DataTrackAttrs): void;
29
+ declare function setTraits(newTraits: string[]): void;
30
+
31
+ export { init, setTraits, track, version };
32
+ export type { DataTrackAttrs, TrackerInitConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ var config={flush_interval_ms:5e3,max_queue_size:10,walk_depth:3,max_text_length:100,autocapture_pageviews:!0,autocapture:!0,autocapture_elements:["a","button","input[type=submit]","[role=button]","[role=link]","[role=menuitem]","[role=menuitemcheckbox]","[role=menuitemradio]","[role=option]","[role=tab]","[role=switch]","[role=checkbox]","[role=radio]","[role=combobox]"],pii_url_params:["email","token","key","password","secret"],normalize_path_ids:!0,pii_path_patterns:[],excluded_urls:[],sampling_rate:1},apiBase="",apiKey="",configLoaded=!1,trackingDisabled=!1,traits=[];function setApiBase(value){apiBase=value}function setApiKey(value){apiKey=value}function setConfigLoaded(value){configLoaded=value}function setTrackingDisabled(value){trackingDisabled=value}function setTraits(value){traits=value}function applyServerConfig(server){if(typeof server!="object"||server===null)return;let s=server;typeof s.flush_interval_ms=="number"&&(config.flush_interval_ms=s.flush_interval_ms),typeof s.max_queue_size=="number"&&(config.max_queue_size=s.max_queue_size),typeof s.walk_depth=="number"&&(config.walk_depth=s.walk_depth),typeof s.max_text_length=="number"&&(config.max_text_length=s.max_text_length),typeof s.autocapture_pageviews=="boolean"&&(config.autocapture_pageviews=s.autocapture_pageviews),typeof s.autocapture=="boolean"&&(config.autocapture=s.autocapture),Array.isArray(s.autocapture_elements)&&(config.autocapture_elements=s.autocapture_elements),Array.isArray(s.pii_url_params)&&(config.pii_url_params=s.pii_url_params),typeof s.normalize_path_ids=="boolean"&&(config.normalize_path_ids=s.normalize_path_ids),Array.isArray(s.pii_path_patterns)&&(config.pii_path_patterns=s.pii_path_patterns),Array.isArray(s.excluded_urls)&&(config.excluded_urls=s.excluded_urls),typeof s.sampling_rate=="number"&&(config.sampling_rate=s.sampling_rate)}async function fetchConfig(){try{let response=await fetch(`${apiBase}/api/config`,{headers:{"X-API-Key":apiKey},credentials:"omit"});if(!response.ok)return;let data=await response.json();applyServerConfig(data)}catch{}}function applyInitConfig(initConfig){initConfig.apiKey!==void 0&&(apiKey=initConfig.apiKey),initConfig.apiHost!==void 0&&(apiBase=initConfig.apiHost),initConfig.flushIntervalMs!==void 0&&(config.flush_interval_ms=initConfig.flushIntervalMs),initConfig.maxQueueSize!==void 0&&(config.max_queue_size=initConfig.maxQueueSize),initConfig.walkDepth!==void 0&&(config.walk_depth=initConfig.walkDepth),initConfig.maxTextLength!==void 0&&(config.max_text_length=initConfig.maxTextLength),initConfig.autocapturePageviews!==void 0&&(config.autocapture_pageviews=initConfig.autocapturePageviews),initConfig.autocapture!==void 0&&(config.autocapture=initConfig.autocapture),initConfig.autocaptureElements!==void 0&&(config.autocapture_elements=initConfig.autocaptureElements),initConfig.piiUrlParams!==void 0&&(config.pii_url_params=initConfig.piiUrlParams),typeof initConfig.normalizePathIds=="boolean"&&(config.normalize_path_ids=initConfig.normalizePathIds),Array.isArray(initConfig.piiPathPatterns)&&(config.pii_path_patterns=initConfig.piiPathPatterns),initConfig.excludedUrls!==void 0&&(config.excluded_urls=initConfig.excludedUrls),initConfig.samplingRate!==void 0&&(config.sampling_rate=initConfig.samplingRate),initConfig.traits!==void 0&&(traits=initConfig.traits)}var ID_PATTERNS=[/^\d+$/,/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,/^[0-9a-f]{8,}$/i];function isIdSegment(segment){return ID_PATTERNS.some(p=>p.test(segment))}function buildPiiPattern(){if(config.pii_url_params.length===0)return null;let escaped=config.pii_url_params.map(p=>p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"));return new RegExp("("+escaped.join("|")+")","i")}function buildPiiPathPattern(){if(config.pii_path_patterns.length===0)return null;try{return new RegExp("^("+config.pii_path_patterns.join("|")+")$","i")}catch{return null}}function sanitizeUrl(url){try{let u=new URL(url),segments=u.pathname.split("/"),piiPathRegex=buildPiiPathPattern(),pathChanged=!1;for(let i=0;i<segments.length;i++){let seg=segments[i];if(seg){if(config.normalize_path_ids&&isIdSegment(seg)){segments[i]=":id",pathChanged=!0;continue}piiPathRegex?.test(seg)&&(segments[i]=":redacted",pathChanged=!0)}}pathChanged&&(u.pathname=segments.join("/"));let paramPattern=buildPiiPattern();if(paramPattern){let toRedact=[];u.searchParams.forEach((_v,k)=>{paramPattern.test(k)&&toRedact.push(k)});for(let k of toRedact)u.searchParams.set(k,":redacted")}return u.toString()}catch{return url}}function matchGlob(pattern,url){try{let path=new URL(url).pathname,escaped=pattern.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp("^"+escaped.replace(/\*/g,".*")+"$").test(path)}catch{return!1}}function isExcludedUrl(url){return config.excluded_urls.some(pattern=>matchGlob(pattern,url))}function disableTracking(queue2){setTrackingDisabled(!0),queue2.length=0,config.autocapture_pageviews=!1,config.autocapture=!1}function applySampling(queue2){if(!(config.sampling_rate>=1)){if(config.sampling_rate<=0){disableTracking(queue2);return}Math.random()>config.sampling_rate&&disableTracking(queue2)}}function extractText(el){let raw=el.textContent;if(!raw)return;let trimmed=raw.trim().replace(/\s+/g," ");if(trimmed.length!==0)return trimmed.length>config.max_text_length?trimmed.slice(0,config.max_text_length):trimmed}function extractDataTrack(el){let current=el;for(let i=0;i<=config.walk_depth&&current;i++){let trackValue=current.getAttribute("data-track");if(trackValue!==null){let attrs={};for(let attr of current.attributes)if(attr.name.startsWith("data-track-")){let key=attr.name.slice(11);attrs[key]=attr.value}return{dataTrack:trackValue||void 0,dataTrackAttrs:Object.keys(attrs).length>0?attrs:void 0}}current=current.parentElement}return{dataTrack:void 0,dataTrackAttrs:void 0}}function getSegment(el){let seg=el.tagName.toLowerCase();el instanceof HTMLInputElement&&el.type&&(seg+='[type="'+CSS.escape(el.type)+'"]');let role=el.getAttribute("role");role&&(seg+='[role="'+CSS.escape(role)+'"]');let parent=el.parentElement;if(parent){let tag=el.tagName,siblings=parent.children,sameTag=0,idx=0;for(let i=0;i<siblings.length;i++){let sib=siblings[i];sib&&sib.tagName===tag&&(sameTag++,sib===el&&(idx=sameTag))}sameTag>1&&(seg+=":nth-of-type("+String(idx)+")")}return seg}function computeSelector(target){let parts=[],el=target;for(;el&&el!==document.documentElement;)parts.unshift(getSegment(el)),el=el.parentElement;let result=parts.join(" > ");for(;result.length>200&&parts.length>1;)parts.shift(),result=parts.join(" > ");return result}function getGenericSegment(el){let seg=el.tagName.toLowerCase();el instanceof HTMLInputElement&&el.type&&(seg+='[type="'+CSS.escape(el.type)+'"]');let role=el.getAttribute("role");return role&&(seg+='[role="'+CSS.escape(role)+'"]'),seg}function computeGenericSelector(target){let parts=[],el=target;for(;el&&el!==document.documentElement;)parts.unshift(getGenericSegment(el)),el=el.parentElement;let result=parts.join(" > ");for(;result.length>200&&parts.length>1;)parts.shift(),result=parts.join(" > ");let specific=computeSelector(target);return result===specific?null:result}function createPageviewEvent(referrer){return{event_type:"pageview",page_url:sanitizeUrl(window.location.href),referrer:referrer?sanitizeUrl(referrer):void 0,timestamp:new Date().toISOString()}}function createClickEvent(el){let{dataTrack,dataTrackAttrs}=extractDataTrack(el);return{event_type:"click",page_url:sanitizeUrl(window.location.href),element_tag:el.tagName.toLowerCase(),element_selector:computeSelector(el),element_selector_generalized:computeGenericSelector(el)??void 0,element_text:extractText(el),data_track:dataTrack,data_track_attrs:dataTrackAttrs,timestamp:new Date().toISOString()}}function createFormSubmitEvent(form){let{dataTrack,dataTrackAttrs}=extractDataTrack(form);return{event_type:"form_submit",page_url:sanitizeUrl(window.location.href),element_tag:form.tagName.toLowerCase(),element_selector:computeSelector(form),element_selector_generalized:computeGenericSelector(form)??void 0,data_track:dataTrack,data_track_attrs:dataTrackAttrs,timestamp:new Date().toISOString()}}var pickerActive=!1,pickerDashboardOrigin=null;function setPickerActive(value){pickerActive=value}function setPickerDashboardOrigin(value){pickerDashboardOrigin=value}var REJECTION_REASONS={401:"the API key is missing or not valid",403:"this site's origin is not in the project's allowed domains",429:"the project is over its event limit"},queue=[],lastUrl="",isFlushing=!1;function setLastUrl(value){lastUrl=value}function filterEvents(events){return events.filter(event=>!(isExcludedUrl(event.page_url)||event.event_type==="pageview"&&!config.autocapture_pageviews||(event.event_type==="click"||event.event_type==="form_submit")&&!config.autocapture&&!event.data_track))}function enqueue(event){trackingDisabled||pickerActive||apiKey&&(queue.length>=100||(queue.push(event),configLoaded&&queue.length>=config.max_queue_size&&flush()))}async function flush(){if(trackingDisabled||pickerActive||isFlushing||queue.length===0)return;isFlushing=!0;let events=filterEvents(queue.splice(0));if(events.length===0){isFlushing=!1;return}let body={events,...traits.length>0&&{traits}},headers={"Content-Type":"application/json"};apiKey&&(headers["X-API-Key"]=apiKey);try{let response=await fetch(`${apiBase}/api/events`,{method:"POST",headers,body:JSON.stringify(body),keepalive:!0,credentials:"omit"});if(!response.ok){let status=response.status;status===401||status===403||status===429?(console.warn(`[tracker] The API rejected events with HTTP ${String(status)} \u2014 ${REJECTION_REASONS[status]??"the request was refused"}. Tracking is now off for this page.`),disableTracking(queue)):status>=500&&queue.unshift(...events)}}catch{queue.unshift(...events)}finally{isFlushing=!1}}function flushBeacon(){if(trackingDisabled||queue.length===0)return;let events=filterEvents(queue.splice(0));if(events.length===0)return;let body={events,...traits.length>0&&{traits}};apiKey&&(body.api_key=apiKey);let headers={"Content-Type":"application/json"};apiKey&&(headers["X-API-Key"]=apiKey),fetch(`${apiBase}/api/events`,{method:"POST",headers,body:JSON.stringify(body),keepalive:!0,credentials:"omit"}).catch(()=>{})}function isSensitive(el){if(el instanceof HTMLInputElement){let t=el.type.toLowerCase();if(t==="password"||t==="hidden")return!0}let ac=el.getAttribute("autocomplete");return!!(ac&&ac.toLowerCase().startsWith("cc-"))}function isInteractive(el){if(el.hasAttribute("data-track"))return!0;if(!config.autocapture)return!1;for(let selector of config.autocapture_elements)try{if(el.matches(selector))return!0}catch{}return!1}function findInteractiveAncestor(target){let el=target;for(let i=0;i<=config.walk_depth&&el;i++){if(isInteractive(el))return el;el=el.parentElement}return null}function isPickerTarget(el){return isInteractive(el)||el.matches("form")}function findPickerTarget(target){let el=target;for(let i=0;i<=config.walk_depth&&el;i++){if(isPickerTarget(el))return el;el=el.parentElement}return null}var PICKER_HANDOFF_KEY="__stPickerHandoff";var PICKER_FILE="tracker-picker.js",picker=null,loading=!1,failed=!1,pendingOptions=null,pickerHostApi={setActive:setPickerActive,setDashboardOrigin:setPickerDashboardOrigin,clearQueue:()=>{queue.length=0},computeSelector,computeGenericSelector,extractText,extractDataTrack,findPickerTarget};function pickerScriptUrl(){for(let script of document.querySelectorAll("script[src]")){let src=script.getAttribute("src");if(src?.includes("tracker.js"))try{return new URL(PICKER_FILE,new URL(src,document.baseURI)).toString()}catch{}}return apiBase?apiBase.replace(/\/+$/,"")+"/"+PICKER_FILE:"/"+PICKER_FILE}function registerPicker(module){if(picker=module,loading=!1,pendingOptions){let options=pendingOptions;pendingOptions=null,activate(module,options)}}function activate(module,options){module.activate({...options,dashboardOrigin:pickerDashboardOrigin})}function abandonLoad(url){loading=!1,failed=!0,pendingOptions=null,setPickerActive(!1),console.warn(`[tracker] Could not load the Element Picker from ${url}. Tracking is unaffected.`)}function loadPicker(){let url=pickerScriptUrl(),script=document.createElement("script");script.src=url,script.async=!0,script[PICKER_HANDOFF_KEY]={host:pickerHostApi,register:registerPicker},script.addEventListener("load",()=>{script.remove(),picker||abandonLoad(url)}),script.addEventListener("error",()=>{script.remove(),abandonLoad(url)}),document.head.appendChild(script)}function requestPicker(options){if(picker){activate(picker,options??{});return}failed||(setPickerActive(!0),queue.length=0,pendingOptions=options??{},!loading&&(loading=!0,loadPicker()))}function onPickerNavigation(){picker?.onNavigation()}function onPickerWindowMessage(e){if(picker){picker.handleMessage(e);return}typeof e.data!="object"||e.data===null||e.data.type!=="st-picker-activate"||pickerDashboardOrigin&&e.origin!==pickerDashboardOrigin||pickerActive||!window.opener||requestPicker()}function onClickCapture(e){if(pickerActive)return;let target=e.target;if(!(target instanceof Element))return;let interactive=findInteractiveAncestor(target);interactive&&(isSensitive(interactive)||enqueue(createClickEvent(interactive)))}function onSubmitCapture(e){if(pickerActive)return;let form=e.target;form instanceof HTMLFormElement&&(isSensitive(form)||!config.autocapture&&!extractDataTrack(form).dataTrack||enqueue(createFormSubmitEvent(form)))}function onSpaNavigation(){if(pickerActive){onPickerNavigation();return}if(!config.autocapture_pageviews)return;let currentUrl=window.location.href;if(currentUrl===lastUrl)return;let referrer=lastUrl;setLastUrl(currentUrl),enqueue(createPageviewEvent(referrer))}function patchHistory(){let origPushState=history.pushState.bind(history),origReplaceState=history.replaceState.bind(history);history.pushState=function(...args){origPushState(...args),onSpaNavigation()},history.replaceState=function(...args){origReplaceState(...args),onSpaNavigation()}}var version="0.1.0",initialized=!1,storedInitConfig={};function init(initConfig){if(typeof window>"u")return;if(initialized){console.warn("[tracker] Already initialized. Ignoring duplicate init() call.");return}if(initialized=!0,storedInitConfig=initConfig??{},storedInitConfig.apiKey!==void 0&&setApiKey(storedInitConfig.apiKey),storedInitConfig.apiHost!==void 0&&setApiBase(storedInitConfig.apiHost),!apiKey){console.error("[tracker] Missing apiKey. Events will not be sent.");return}applyInitConfig(storedInitConfig),setLastUrl(window.location.href),config.autocapture_pageviews&&enqueue(createPageviewEvent(document.referrer)),patchHistory(),window.addEventListener("popstate",onSpaNavigation),document.addEventListener("click",onClickCapture),document.addEventListener("submit",onSubmitCapture),window.addEventListener("message",onPickerWindowMessage);let hashMatch=window.location.hash.match(/^#st-picker=(.+)$/);if(hashMatch?.[1]){let origin=decodeURIComponent(hashMatch[1]);setPickerDashboardOrigin(origin);try{sessionStorage.setItem("st-picker",JSON.stringify({active:!0,origin}))}catch{}history.replaceState(null,"",window.location.pathname+window.location.search),window.opener&&requestPicker()}else try{let stored=sessionStorage.getItem("st-picker");if(stored&&window.opener){let parsed=JSON.parse(stored);if(typeof parsed=="object"&&parsed!==null&&"active"in parsed&&"origin"in parsed){let obj=parsed;if(obj.active&&typeof obj.origin=="string"){setPickerDashboardOrigin(obj.origin);let storedScan="scanEnabled"in obj&&obj.scanEnabled===!0,storedNav="navigateMode"in obj&&obj.navigateMode===!0,rawMode="selectorMode"in obj?obj.selectorMode:null;requestPicker({scanEnabled:storedScan,navigateMode:storedNav,selectorMode:rawMode==="generic"||rawMode==="exact"?rawMode:null})}}}}catch{}document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&flushBeacon()}),fetchConfig().then(()=>{applyInitConfig(storedInitConfig),applySampling(queue),!trackingDisabled&&(setConfigLoaded(!0),setInterval(()=>{flush()},config.flush_interval_ms),flush())})}function track(name,attrs){typeof window>"u"||trackingDisabled||name&&enqueue({event_type:"custom",page_url:sanitizeUrl(window.location.href),data_track:name,data_track_attrs:attrs,timestamp:new Date().toISOString()})}function setTraits2(newTraits){typeof window>"u"||setTraits(newTraits)}export{init,setTraits2 as setTraits,track,version};
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@privatrak/tracker",
3
+ "version": "0.1.0",
4
+ "description": "Cookieless, privacy-first analytics tracker: auto-captures pageviews, clicks and form submissions with no cookies, no localStorage, no PII and no consent banner.",
5
+ "keywords": [
6
+ "analytics",
7
+ "web-analytics",
8
+ "product-analytics",
9
+ "tracking",
10
+ "event-tracking",
11
+ "autocapture",
12
+ "pageview",
13
+ "privacy",
14
+ "privacy-friendly",
15
+ "cookieless",
16
+ "no-cookies",
17
+ "gdpr",
18
+ "privatrak"
19
+ ],
20
+ "author": "Omexa LLC (https://privatrak.com)",
21
+ "license": "MIT",
22
+ "homepage": "https://privatrak.com",
23
+ "bugs": {
24
+ "url": "https://privatrak.com/contact"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "main": "dist/index.mjs",
32
+ "types": "dist/index.d.mts",
33
+ "files": [
34
+ "dist/index.mjs",
35
+ "dist/index.d.mts",
36
+ "README.md",
37
+ "LICENSE",
38
+ "CHANGELOG.md"
39
+ ],
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.mts",
43
+ "import": "./dist/index.mjs"
44
+ }
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "scripts": {
50
+ "build:iife": "esbuild src/bootstrap.ts --bundle --minify --format=iife --define:__TRACKER_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/tracker.js",
51
+ "build:picker": "esbuild src/picker/entry.ts --bundle --minify --format=iife --define:__TRACKER_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/tracker-picker.js",
52
+ "build:esm": "esbuild src/index.ts --bundle --minify-whitespace --minify-syntax --format=esm --define:__TRACKER_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/index.mjs",
53
+ "build:types": "tsc -p tsconfig.build.json && rollup -c rollup.dts.config.mjs && rm -rf .types",
54
+ "build": "npm run build:iife && npm run build:picker && npm run build:esm && npm run build:types",
55
+ "prepack": "npm run build",
56
+ "dev": "esbuild src/bootstrap.ts --bundle --format=iife --define:__TRACKER_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/tracker.js --watch & esbuild src/picker/entry.ts --bundle --format=iife --define:__TRACKER_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/tracker-picker.js --watch & wait",
57
+ "lint": "eslint --max-warnings 0 .",
58
+ "typecheck": "tsc --noEmit",
59
+ "format": "prettier --write 'src/**/*.ts'",
60
+ "format:check": "prettier --check 'src/**/*.ts'",
61
+ "generate": "openapi-typescript ../openapi.yaml -o src/generated/api-types.ts"
62
+ },
63
+ "devDependencies": {
64
+ "esbuild": "^0.25",
65
+ "eslint": "^9",
66
+ "openapi-typescript": "^7",
67
+ "prettier": "^3",
68
+ "rollup": "^4.63.4",
69
+ "rollup-plugin-dts": "^6.5.1",
70
+ "typescript": "^5",
71
+ "typescript-eslint": "^8"
72
+ }
73
+ }