@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 +42 -0
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/index.d.mts +32 -0
- package/dist/index.mjs +1 -0
- package/package.json +73 -0
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.
|
package/dist/index.d.mts
ADDED
|
@@ -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&¤t;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&⪙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&⪙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
|
+
}
|