@pantho075/trackima 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +45 -2
- package/dist/index.d.ts +74 -0
- package/dist/index.js +265 -0
- package/dist/t.js +1 -0
- package/package.json +49 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pantho Haque
|
|
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
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pantho075/trackima
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The browser tracker for [Trackima](https://trackima.vercel.app): pageviews, custom events and visitor history,
|
|
4
|
+
batched in the browser and sent at most every 10 minutes (or when the tab is hidden). No cookies,
|
|
5
|
+
about 2 KB gzipped, and it never throws into your page.
|
|
6
|
+
|
|
7
|
+
## Script tag
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<script defer src="https://trackima.pantho-haque-dev.workers.dev/t.js" data-site="YOUR_SITE_ID"></script>
|
|
11
|
+
<!-- optional: data-consent="required" keeps ids in memory until trackima.consent(true) -->
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
trackima.event("signup", { plan: "pro" }); // up to 10 props: text, numbers, true/false
|
|
16
|
+
trackima.identify("user_42", { plan: "pro" }); // after your login: links this browser to your user id
|
|
17
|
+
trackima.reset(); // on logout
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No-code events: `<button data-trackima-event="signup" data-trackima-plan="pro">`.
|
|
21
|
+
|
|
22
|
+
## npm
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm add @pantho075/trackima
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { init, event, identify, reset, consent } from "@pantho075/trackima";
|
|
30
|
+
|
|
31
|
+
init({ site: "YOUR_SITE_ID", host: "https://trackima.pantho-haque-dev.workers.dev" });
|
|
32
|
+
event("add_to_cart", { item: "mug", price: 12 });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Default | |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `site` | required | 12-character site id from the dashboard (Setup & settings) |
|
|
38
|
+
| `host` | required | the Trackima Worker, `https://trackima.pantho-haque-dev.workers.dev` |
|
|
39
|
+
| `consent` | `"granted"` | `"required"`: nothing in localStorage until `consent(true)` |
|
|
40
|
+
| `flushInterval` | `600000` | ms between timed sends |
|
|
41
|
+
| `maxBatch` | `50` | send as soon as this many events are queued |
|
|
42
|
+
| `spa` | `true` | count `history.pushState` / `popstate` route changes |
|
|
43
|
+
|
|
44
|
+
Delivery: batches carry ids, so retries never double-count. Failed sends retry after 1, 5 and 15
|
|
45
|
+
minutes; a 429 (daily limit) pauses until midnight UTC. Unsent batches from a closed or crashed tab are
|
|
46
|
+
picked up by the next tab after 15 minutes.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type Props = Record<string, string | number | boolean>;
|
|
2
|
+
export type Options = {
|
|
3
|
+
/** 12-character site id from the Trackima dashboard */
|
|
4
|
+
site: string;
|
|
5
|
+
/** Trackima server origin, e.g. "https://trackima.example.workers.dev" */
|
|
6
|
+
host: string;
|
|
7
|
+
/** "required": keep ids in memory only until consent(true). Default "granted". */
|
|
8
|
+
consent?: "granted" | "required";
|
|
9
|
+
/** ms between timed sends. Default 10 minutes. */
|
|
10
|
+
flushInterval?: number;
|
|
11
|
+
/** send as soon as this many events are queued. Default 50. */
|
|
12
|
+
maxBatch?: number;
|
|
13
|
+
/** count history.pushState / replaceState / popstate route changes. Default true. */
|
|
14
|
+
spa?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type Batch = {
|
|
17
|
+
s: string;
|
|
18
|
+
v: string;
|
|
19
|
+
x?: string;
|
|
20
|
+
t?: Props;
|
|
21
|
+
ss: string;
|
|
22
|
+
st: number;
|
|
23
|
+
b: string;
|
|
24
|
+
pv: [number, string][];
|
|
25
|
+
ev: ([number, string] | [number, string, Props])[];
|
|
26
|
+
r?: string;
|
|
27
|
+
u?: Record<string, string>;
|
|
28
|
+
};
|
|
29
|
+
type Pending = Omit<Batch, "s" | "b">;
|
|
30
|
+
export declare const randomId: (prefix: string, n: number) => string;
|
|
31
|
+
export declare function createTracker(opts: Options): {
|
|
32
|
+
event: (name: string, props?: Props) => void;
|
|
33
|
+
identify(id: string, t?: Props): void;
|
|
34
|
+
reset(): void;
|
|
35
|
+
consent(granted: boolean): void;
|
|
36
|
+
flush: () => Promise<void>;
|
|
37
|
+
/** for tests and hot reloads */
|
|
38
|
+
destroy(): void;
|
|
39
|
+
readonly state: {
|
|
40
|
+
outbox: Batch[];
|
|
41
|
+
queue: Pending | null;
|
|
42
|
+
pausedUntil: number;
|
|
43
|
+
fails: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export type Tracker = ReturnType<typeof createTracker>;
|
|
47
|
+
export declare function init(opts: Options): {
|
|
48
|
+
event: (name: string, props?: Props) => void;
|
|
49
|
+
identify(id: string, t?: Props): void;
|
|
50
|
+
reset(): void;
|
|
51
|
+
consent(granted: boolean): void;
|
|
52
|
+
flush: () => Promise<void>;
|
|
53
|
+
/** for tests and hot reloads */
|
|
54
|
+
destroy(): void;
|
|
55
|
+
readonly state: {
|
|
56
|
+
outbox: Batch[];
|
|
57
|
+
queue: Pending | null;
|
|
58
|
+
pausedUntil: number;
|
|
59
|
+
fails: number;
|
|
60
|
+
};
|
|
61
|
+
} | null;
|
|
62
|
+
export declare const event: (name: string, props?: Props) => void | undefined;
|
|
63
|
+
export declare const identify: (id: string, traits?: Props) => void | undefined;
|
|
64
|
+
export declare const reset: () => void | undefined;
|
|
65
|
+
export declare const consent: (granted: boolean) => void | undefined;
|
|
66
|
+
export declare const flush: () => Promise<void> | undefined;
|
|
67
|
+
/** what is queued right now (for the playground and debugging) */
|
|
68
|
+
export declare const state: () => {
|
|
69
|
+
outbox: Batch[];
|
|
70
|
+
queue: Pending | null;
|
|
71
|
+
pausedUntil: number;
|
|
72
|
+
fails: number;
|
|
73
|
+
} | undefined;
|
|
74
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var SESSION_IDLE_MS = 30 * 6e4;
|
|
3
|
+
var ORPHAN_MS = 15 * 6e4;
|
|
4
|
+
var MAX_EVENTS = 1e3;
|
|
5
|
+
var GIVE_UP = [400, 403, 404, 409, 413];
|
|
6
|
+
var randomId = (prefix, n) => {
|
|
7
|
+
const a = crypto.getRandomValues(new Uint8Array(n));
|
|
8
|
+
return prefix + Array.from(a, (x) => "abcdefghijklmnopqrstuvwxyz0123456789"[x % 36]).join("");
|
|
9
|
+
};
|
|
10
|
+
var nextUtcMidnight = () => {
|
|
11
|
+
const d = /* @__PURE__ */ new Date();
|
|
12
|
+
d.setUTCHours(24, 0, 0, 0);
|
|
13
|
+
return d.getTime();
|
|
14
|
+
};
|
|
15
|
+
var parse = (s) => {
|
|
16
|
+
try {
|
|
17
|
+
return s ? JSON.parse(s) : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function createTracker(opts) {
|
|
23
|
+
const site = opts.site, endpoint = opts.host.replace(/\/$/, "") + "/e";
|
|
24
|
+
const maxBatch = opts.maxBatch ?? 50, tab = randomId("", 8), myKey = `tq:${site}:${tab}`;
|
|
25
|
+
const mem = /* @__PURE__ */ new Map();
|
|
26
|
+
let persist = opts.consent !== "required";
|
|
27
|
+
const store = {
|
|
28
|
+
get: (k) => {
|
|
29
|
+
if (persist) try {
|
|
30
|
+
return localStorage.getItem(k);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
return mem.get(k) ?? null;
|
|
34
|
+
},
|
|
35
|
+
set: (k, v) => {
|
|
36
|
+
mem.set(k, v);
|
|
37
|
+
if (persist) try {
|
|
38
|
+
localStorage.setItem(k, v);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
del: (k) => {
|
|
43
|
+
mem.delete(k);
|
|
44
|
+
try {
|
|
45
|
+
localStorage.removeItem(k);
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
keys: () => {
|
|
50
|
+
try {
|
|
51
|
+
return Object.keys(localStorage);
|
|
52
|
+
} catch {
|
|
53
|
+
return [...mem.keys()];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
let outbox = [], q = null, traits;
|
|
58
|
+
let pausedUntil = 0, fails = 0, busy = false, lastPath = "";
|
|
59
|
+
const cleanups = [];
|
|
60
|
+
const save = () => store.set(myKey, JSON.stringify({ outbox, q, at: Date.now() }));
|
|
61
|
+
const visitor = () => {
|
|
62
|
+
let v = store.get("trackima:v");
|
|
63
|
+
if (!v) store.set("trackima:v", v = randomId("v_", 16));
|
|
64
|
+
return v;
|
|
65
|
+
};
|
|
66
|
+
function session(now) {
|
|
67
|
+
let s = parse(store.get("trackima:s"));
|
|
68
|
+
if (!s || now - s.last > SESSION_IDLE_MS) {
|
|
69
|
+
const ref = document.referrer ? new URL(document.referrer).hostname : "";
|
|
70
|
+
const u = {};
|
|
71
|
+
new URLSearchParams(location.search).forEach((val, key) => {
|
|
72
|
+
if (key.startsWith("utm_") && val) u[key] = val;
|
|
73
|
+
});
|
|
74
|
+
s = { id: randomId("s_", 10), start: Math.floor(now / 1e3), last: now };
|
|
75
|
+
if (ref && ref !== location.hostname) s.r = ref;
|
|
76
|
+
if (Object.keys(u).length) s.u = u;
|
|
77
|
+
}
|
|
78
|
+
s.last = now;
|
|
79
|
+
store.set("trackima:s", JSON.stringify(s));
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
function seal() {
|
|
83
|
+
if (!q || !q.pv.length && !q.ev.length) return;
|
|
84
|
+
const b = { s: site, b: randomId("", 10), ...q };
|
|
85
|
+
if (traits) {
|
|
86
|
+
b.t = traits;
|
|
87
|
+
traits = void 0;
|
|
88
|
+
}
|
|
89
|
+
outbox.push(b);
|
|
90
|
+
q = { ...q, pv: [], ev: [], r: void 0, u: void 0 };
|
|
91
|
+
let total = outbox.reduce((n, x) => n + x.pv.length + x.ev.length, 0);
|
|
92
|
+
while (total > MAX_EVENTS && outbox.length > 1) {
|
|
93
|
+
const dropped = outbox.shift();
|
|
94
|
+
total -= dropped.pv.length + dropped.ev.length;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function record(kind, name, props) {
|
|
98
|
+
const now = Date.now(), s = session(now), v = visitor(), x = store.get("trackima:x") ?? void 0;
|
|
99
|
+
if (!q || q.ss !== s.id || q.v !== v) {
|
|
100
|
+
seal();
|
|
101
|
+
q = { v, ss: s.id, st: s.start, pv: [], ev: [], ...x && { x }, ...s.r && { r: s.r }, ...s.u && { u: s.u } };
|
|
102
|
+
}
|
|
103
|
+
const off = Math.max(0, Math.floor(now / 1e3) - q.st);
|
|
104
|
+
if (kind === "pv") q.pv.push([off, name]);
|
|
105
|
+
else q.ev.push(props && Object.keys(props).length ? [off, name, props] : [off, name]);
|
|
106
|
+
save();
|
|
107
|
+
if (q.pv.length + q.ev.length >= maxBatch) void flush2(false);
|
|
108
|
+
}
|
|
109
|
+
async function deliver(beacon) {
|
|
110
|
+
if (busy || Date.now() < pausedUntil) return;
|
|
111
|
+
while (outbox.length) {
|
|
112
|
+
const batch = outbox[0], body = JSON.stringify(batch);
|
|
113
|
+
if (beacon && navigator.sendBeacon) {
|
|
114
|
+
if (!navigator.sendBeacon(endpoint, new Blob([body], { type: "text/plain" }))) break;
|
|
115
|
+
outbox.shift();
|
|
116
|
+
save();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
busy = true;
|
|
120
|
+
let status = 0;
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(endpoint, { method: "POST", body, keepalive: body.length < 6e4, credentials: "omit", headers: { "Content-Type": "text/plain" } });
|
|
123
|
+
status = res.status;
|
|
124
|
+
} catch {
|
|
125
|
+
} finally {
|
|
126
|
+
busy = false;
|
|
127
|
+
}
|
|
128
|
+
if (status >= 200 && status < 300 || GIVE_UP.includes(status)) {
|
|
129
|
+
outbox.shift();
|
|
130
|
+
fails = 0;
|
|
131
|
+
save();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (status === 429) {
|
|
135
|
+
pausedUntil = nextUtcMidnight();
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
fails++;
|
|
139
|
+
pausedUntil = fails > 3 ? nextUtcMidnight() : Date.now() + [6e4, 3e5, 9e5][fails - 1];
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function flush2(beacon = false) {
|
|
144
|
+
seal();
|
|
145
|
+
save();
|
|
146
|
+
await deliver(beacon);
|
|
147
|
+
}
|
|
148
|
+
function pageview() {
|
|
149
|
+
const path = location.pathname;
|
|
150
|
+
if (path === lastPath) return;
|
|
151
|
+
lastPath = path;
|
|
152
|
+
record("pv", path);
|
|
153
|
+
}
|
|
154
|
+
function adoptOrphans() {
|
|
155
|
+
for (const key of store.keys()) {
|
|
156
|
+
if (!key.startsWith(`tq:${site}:`) || key === myKey) continue;
|
|
157
|
+
const saved = parse(store.get(key));
|
|
158
|
+
if (!saved || Date.now() - saved.at < ORPHAN_MS) continue;
|
|
159
|
+
outbox.push(...saved.outbox);
|
|
160
|
+
if (saved.q && (saved.q.pv.length || saved.q.ev.length)) outbox.push({ s: site, b: randomId("", 10), ...saved.q });
|
|
161
|
+
store.del(key);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const on = (target, type, fn) => {
|
|
165
|
+
target.addEventListener(type, fn);
|
|
166
|
+
cleanups.push(() => target.removeEventListener(type, fn));
|
|
167
|
+
};
|
|
168
|
+
adoptOrphans();
|
|
169
|
+
pageview();
|
|
170
|
+
const timer = setInterval(() => void flush2(false), opts.flushInterval ?? 6e5);
|
|
171
|
+
const heartbeat = setInterval(save, 6e4);
|
|
172
|
+
cleanups.push(() => {
|
|
173
|
+
clearInterval(timer);
|
|
174
|
+
clearInterval(heartbeat);
|
|
175
|
+
});
|
|
176
|
+
on(document, "visibilitychange", () => {
|
|
177
|
+
if (document.visibilityState === "hidden") void flush2(true);
|
|
178
|
+
});
|
|
179
|
+
on(window, "pagehide", () => void flush2(true));
|
|
180
|
+
on(document, "click", (e) => {
|
|
181
|
+
const el = e.target?.closest?.("[data-trackima-event]");
|
|
182
|
+
if (!el) return;
|
|
183
|
+
const props = {};
|
|
184
|
+
for (const [k, v] of Object.entries(el.dataset)) if (k.startsWith("trackima") && k !== "trackimaEvent" && v) props[k.slice(8, 9).toLowerCase() + k.slice(9)] = v;
|
|
185
|
+
record("ev", el.dataset.trackimaEvent, props);
|
|
186
|
+
});
|
|
187
|
+
if (opts.spa !== false) {
|
|
188
|
+
for (const m of ["pushState", "replaceState"]) {
|
|
189
|
+
const orig = history[m];
|
|
190
|
+
history[m] = function(...args) {
|
|
191
|
+
const r = orig.apply(this, args);
|
|
192
|
+
pageview();
|
|
193
|
+
return r;
|
|
194
|
+
};
|
|
195
|
+
cleanups.push(() => {
|
|
196
|
+
history[m] = orig;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
on(window, "popstate", pageview);
|
|
200
|
+
}
|
|
201
|
+
if (outbox.length) void deliver(false);
|
|
202
|
+
return {
|
|
203
|
+
event: (name, props) => record("ev", name, props),
|
|
204
|
+
identify(id, t) {
|
|
205
|
+
store.set("trackima:x", String(id).slice(0, 128));
|
|
206
|
+
if (q) q.x = String(id).slice(0, 128);
|
|
207
|
+
if (t) traits = t;
|
|
208
|
+
},
|
|
209
|
+
reset() {
|
|
210
|
+
void flush2(false);
|
|
211
|
+
for (const k of ["trackima:v", "trackima:s", "trackima:x"]) store.del(k);
|
|
212
|
+
q = null;
|
|
213
|
+
},
|
|
214
|
+
consent(granted) {
|
|
215
|
+
if (granted && !persist) {
|
|
216
|
+
persist = true;
|
|
217
|
+
for (const [k, v] of mem) store.set(k, v);
|
|
218
|
+
}
|
|
219
|
+
if (!granted) {
|
|
220
|
+
persist = false;
|
|
221
|
+
for (const k of ["trackima:v", "trackima:s", "trackima:x", myKey]) try {
|
|
222
|
+
localStorage.removeItem(k);
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
flush: () => flush2(false),
|
|
228
|
+
/** for tests and hot reloads */
|
|
229
|
+
destroy() {
|
|
230
|
+
cleanups.forEach((f) => f());
|
|
231
|
+
},
|
|
232
|
+
get state() {
|
|
233
|
+
return { outbox, queue: q, pausedUntil, fails };
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
var current = null;
|
|
238
|
+
var safe = (fn) => {
|
|
239
|
+
try {
|
|
240
|
+
return fn();
|
|
241
|
+
} catch {
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var usable = () => typeof window !== "undefined" && !navigator.webdriver;
|
|
246
|
+
function init(opts) {
|
|
247
|
+
return safe(() => current ?? (current = usable() ? createTracker(opts) : null)) ?? null;
|
|
248
|
+
}
|
|
249
|
+
var event = (name, props) => safe(() => current?.event(name, props));
|
|
250
|
+
var identify = (id, traits) => safe(() => current?.identify(id, traits));
|
|
251
|
+
var reset = () => safe(() => current?.reset());
|
|
252
|
+
var consent = (granted) => safe(() => current?.consent(granted));
|
|
253
|
+
var flush = () => safe(() => current?.flush());
|
|
254
|
+
var state = () => safe(() => current?.state);
|
|
255
|
+
export {
|
|
256
|
+
consent,
|
|
257
|
+
createTracker,
|
|
258
|
+
event,
|
|
259
|
+
flush,
|
|
260
|
+
identify,
|
|
261
|
+
init,
|
|
262
|
+
randomId,
|
|
263
|
+
reset,
|
|
264
|
+
state
|
|
265
|
+
};
|
package/dist/t.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(()=>{var F=[400,403,404,409,413],y=(s,l)=>{let S=crypto.getRandomValues(new Uint8Array(l));return s+Array.from(S,P=>"abcdefghijklmnopqrstuvwxyz0123456789"[P%36]).join("")},R=()=>{let s=new Date;return s.setUTCHours(24,0,0,0),s.getTime()},D=s=>{try{return s?JSON.parse(s):null}catch{return null}};function Q(s){let l=s.site,S=s.host.replace(/\/$/,"")+"/e",P=s.maxBatch??50,C=y("",8),_=`tq:${l}:${C}`,h=new Map,g=s.consent!=="required",c={get:t=>{if(g)try{return localStorage.getItem(t)}catch{}return h.get(t)??null},set:(t,e)=>{if(h.set(t,e),g)try{localStorage.setItem(t,e)}catch{}},del:t=>{h.delete(t);try{localStorage.removeItem(t)}catch{}},keys:()=>{try{return Object.keys(localStorage)}catch{return[...h.keys()]}}},o=[],i=null,k,x=0,p=0,T=!1,M="",w=[],v=()=>c.set(_,JSON.stringify({outbox:o,q:i,at:Date.now()})),K=()=>{let t=c.get("trackima:v");return t||c.set("trackima:v",t=y("v_",16)),t};function W(t){let e=D(c.get("trackima:s"));if(!e||t-e.last>18e5){let n=document.referrer?new URL(document.referrer).hostname:"",r={};new URLSearchParams(location.search).forEach((a,m)=>{m.startsWith("utm_")&&a&&(r[m]=a)}),e={id:y("s_",10),start:Math.floor(t/1e3),last:t},n&&n!==location.hostname&&(e.r=n),Object.keys(r).length&&(e.u=r)}return e.last=t,c.set("trackima:s",JSON.stringify(e)),e}function B(){if(!i||!i.pv.length&&!i.ev.length)return;let t={s:l,b:y("",10),...i};k&&(t.t=k,k=void 0),o.push(t),i={...i,pv:[],ev:[],r:void 0,u:void 0};let e=o.reduce((n,r)=>n+r.pv.length+r.ev.length,0);for(;e>1e3&&o.length>1;){let n=o.shift();e-=n.pv.length+n.ev.length}}function O(t,e,n){let r=Date.now(),a=W(r),m=K(),N=c.get("trackima:x")??void 0;(!i||i.ss!==a.id||i.v!==m)&&(B(),i={v:m,ss:a.id,st:a.start,pv:[],ev:[],...N&&{x:N},...a.r&&{r:a.r},...a.u&&{u:a.u}});let q=Math.max(0,Math.floor(r/1e3)-i.st);t==="pv"?i.pv.push([q,e]):i.ev.push(n&&Object.keys(n).length?[q,e,n]:[q,e]),v(),i.pv.length+i.ev.length>=P&&d(!1)}async function L(t){if(!(T||Date.now()<x))for(;o.length;){let e=o[0],n=JSON.stringify(e);if(t&&navigator.sendBeacon){if(!navigator.sendBeacon(S,new Blob([n],{type:"text/plain"})))break;o.shift(),v();continue}T=!0;let r=0;try{r=(await fetch(S,{method:"POST",body:n,keepalive:n.length<6e4,credentials:"omit",headers:{"Content-Type":"text/plain"}})).status}catch{}finally{T=!1}if(r>=200&&r<300||F.includes(r)){o.shift(),p=0,v();continue}if(r===429){x=R();break}p++,x=p>3?R():Date.now()+[6e4,3e5,9e5][p-1];break}}async function d(t=!1){B(),v(),await L(t)}function I(){let t=location.pathname;t!==M&&(M=t,O("pv",t))}function X(){for(let t of c.keys()){if(!t.startsWith(`tq:${l}:`)||t===_)continue;let e=D(c.get(t));!e||Date.now()-e.at<9e5||(o.push(...e.outbox),e.q&&(e.q.pv.length||e.q.ev.length)&&o.push({s:l,b:y("",10),...e.q}),c.del(t))}}let E=(t,e,n)=>{t.addEventListener(e,n),w.push(()=>t.removeEventListener(e,n))};X(),I();let z=setInterval(()=>{d(!1)},s.flushInterval??6e5),G=setInterval(v,6e4);if(w.push(()=>{clearInterval(z),clearInterval(G)}),E(document,"visibilitychange",()=>{document.visibilityState==="hidden"&&d(!0)}),E(window,"pagehide",()=>{d(!0)}),E(document,"click",t=>{let e=t.target?.closest?.("[data-trackima-event]");if(!e)return;let n={};for(let[r,a]of Object.entries(e.dataset))r.startsWith("trackima")&&r!=="trackimaEvent"&&a&&(n[r.slice(8,9).toLowerCase()+r.slice(9)]=a);O("ev",e.dataset.trackimaEvent,n)}),s.spa!==!1){for(let t of["pushState","replaceState"]){let e=history[t];history[t]=function(...n){let r=e.apply(this,n);return I(),r},w.push(()=>{history[t]=e})}E(window,"popstate",I)}return o.length&&L(!1),{event:(t,e)=>O("ev",t,e),identify(t,e){c.set("trackima:x",String(t).slice(0,128)),i&&(i.x=String(t).slice(0,128)),e&&(k=e)},reset(){d(!1);for(let t of["trackima:v","trackima:s","trackima:x"])c.del(t);i=null},consent(t){if(t&&!g){g=!0;for(let[e,n]of h)c.set(e,n)}if(!t){g=!1;for(let e of["trackima:v","trackima:s","trackima:x",_])try{localStorage.removeItem(e)}catch{}}},flush:()=>d(!1),destroy(){w.forEach(t=>t())},get state(){return{outbox:o,queue:i,pausedUntil:x,fails:p}}}}var u=null,f=s=>{try{return s()}catch{return}},Y=()=>typeof window<"u"&&!navigator.webdriver;function U(s){return f(()=>u??(u=Y()?Q(s):null))??null}var j=(s,l)=>f(()=>u?.event(s,l)),H=(s,l)=>f(()=>u?.identify(s,l)),A=()=>f(()=>u?.reset()),J=s=>f(()=>u?.consent(s)),V=()=>f(()=>u?.flush()),$=()=>f(()=>u?.state);var b=document.currentScript;b?.dataset.site&&U({site:b.dataset.site,host:b.dataset.host??new URL(b.src).origin,consent:b.dataset.consent==="required"?"required":"granted"});window.trackima={event:j,identify:H,reset:A,consent:J,flush:V,state:$};})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantho075/trackima",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser tracker for Trackima: pageviews, events and visitor history, batched and cookie-free.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Pantho Haque",
|
|
7
|
+
"homepage": "https://trackima.vercel.app",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Pantho-Haque/trackima.git",
|
|
11
|
+
"directory": "packages/tracker"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/Pantho-Haque/trackima/issues",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./t.js": "./dist/t.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"happy-dom": "^21.0.3",
|
|
32
|
+
"tsup": "^9.0.0",
|
|
33
|
+
"typescript": "^7.0.2",
|
|
34
|
+
"vitest": "^5.0.5"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"analytics",
|
|
38
|
+
"tracker",
|
|
39
|
+
"pageviews",
|
|
40
|
+
"events",
|
|
41
|
+
"cloudflare",
|
|
42
|
+
"privacy"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
6
52
|
}
|