@iann29/rastro 0.3.0 → 0.4.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.
Files changed (74) hide show
  1. package/README.md +80 -36
  2. package/agent/manifest.json +2 -2
  3. package/dist/component/_generated/component.d.ts +1 -0
  4. package/dist/component/_generated/component.d.ts.map +1 -1
  5. package/dist/component/_generated/server.d.ts +1 -0
  6. package/dist/component/_generated/server.d.ts.map +1 -1
  7. package/dist/component/_generated/server.js.map +1 -1
  8. package/dist/component/constants.d.ts +7 -2
  9. package/dist/component/constants.d.ts.map +1 -1
  10. package/dist/component/constants.js +24 -3
  11. package/dist/component/constants.js.map +1 -1
  12. package/dist/component/convex.config.d.ts +1 -0
  13. package/dist/component/convex.config.js +3 -0
  14. package/dist/component/convex.config.js.map +1 -1
  15. package/dist/component/geo.d.ts +1 -0
  16. package/dist/component/geo.d.ts.map +1 -1
  17. package/dist/component/geo.js +1 -1
  18. package/dist/component/geo.js.map +1 -1
  19. package/dist/component/http.d.ts.map +1 -1
  20. package/dist/component/http.js +14 -1
  21. package/dist/component/http.js.map +1 -1
  22. package/dist/component/identity.d.ts +12 -0
  23. package/dist/component/identity.d.ts.map +1 -1
  24. package/dist/component/identity.js +30 -2
  25. package/dist/component/identity.js.map +1 -1
  26. package/dist/component/ingest.d.ts +34 -0
  27. package/dist/component/ingest.d.ts.map +1 -1
  28. package/dist/component/ingest.js +69 -17
  29. package/dist/component/ingest.js.map +1 -1
  30. package/dist/component/live.d.ts.map +1 -1
  31. package/dist/component/live.js +34 -4
  32. package/dist/component/live.js.map +1 -1
  33. package/dist/component/reports.d.ts.map +1 -1
  34. package/dist/component/reports.js +21 -6
  35. package/dist/component/reports.js.map +1 -1
  36. package/dist/component/rollupStore.d.ts +7 -4
  37. package/dist/component/rollupStore.d.ts.map +1 -1
  38. package/dist/component/rollupStore.js +37 -17
  39. package/dist/component/rollupStore.js.map +1 -1
  40. package/dist/component/rollups.d.ts +27 -8
  41. package/dist/component/rollups.d.ts.map +1 -1
  42. package/dist/component/rollups.js +110 -32
  43. package/dist/component/rollups.js.map +1 -1
  44. package/dist/component/schema.d.ts +6 -2
  45. package/dist/component/schema.js +5 -2
  46. package/dist/component/schema.js.map +1 -1
  47. package/dist/tracker/generated.d.ts +11 -11
  48. package/dist/tracker/generated.d.ts.map +1 -1
  49. package/dist/tracker/generated.js +11 -11
  50. package/dist/tracker/generated.js.map +1 -1
  51. package/dist/tracker/tracker.js +88 -43
  52. package/dist/tracker/tracker.js.map +1 -1
  53. package/dist/tracker/vitals.d.ts.map +1 -1
  54. package/dist/tracker/vitals.js +22 -4
  55. package/dist/tracker/vitals.js.map +1 -1
  56. package/dist/tracker.min.js +1 -1
  57. package/dist/vitals.min.js +1 -1
  58. package/docs/upgrading.md +84 -28
  59. package/llms.txt +3 -3
  60. package/package.json +1 -1
  61. package/src/component/_generated/component.ts +1 -0
  62. package/src/component/_generated/server.ts +1 -0
  63. package/src/component/constants.ts +24 -3
  64. package/src/component/convex.config.ts +3 -0
  65. package/src/component/geo.ts +1 -1
  66. package/src/component/http.ts +15 -1
  67. package/src/component/identity.ts +41 -2
  68. package/src/component/ingest.ts +92 -32
  69. package/src/component/live.ts +51 -4
  70. package/src/component/reports.ts +24 -8
  71. package/src/component/rollupStore.ts +51 -20
  72. package/src/component/rollups.ts +141 -32
  73. package/src/component/schema.ts +6 -3
  74. package/src/tracker/generated.ts +11 -11
@@ -1,9 +1,14 @@
1
- import { v } from "convex/values";
1
+ import { v, type Infer } from "convex/values";
2
2
  import { internal } from "./_generated/api.js";
3
+ import type { Id } from "./_generated/dataModel.js";
4
+ import type { MutationCtx } from "./_generated/server.js";
3
5
  import { internalMutation } from "./_generated/server.js";
4
6
  import {
7
+ ROLLUP_DEAD_LETTER_RETENTION_MS,
5
8
  ROLLUP_FOLD_BATCH_DOCS,
9
+ ROLLUP_FOLD_DEBOUNCE_MS,
6
10
  ROLLUP_FOLD_LEASE_MS,
11
+ ROLLUP_FOLD_MAX_FAILURES,
7
12
  ROLLUP_FOLD_MAX_ROWS,
8
13
  } from "./constants.js";
9
14
  import {
@@ -13,24 +18,65 @@ import {
13
18
  mergeQueuedRollups,
14
19
  } from "./rollupStore.js";
15
20
 
21
+ const foldArgs = { siteId: v.id("sites"), token: v.string() };
22
+ const foldResult = v.object({
23
+ folded: v.number(),
24
+ remaining: v.boolean(),
25
+ stale: v.boolean(),
26
+ });
27
+ type FoldResult = Infer<typeof foldResult>;
28
+
29
+ // A failed run's reads still count against the transaction that catches it,
30
+ // so a run stops short of the platform's read limits (16 MiB, 32,000
31
+ // documents, 4,096 queries) and leaves room to record the failure.
32
+ const RUN_READ_LIMITS = {
33
+ bytesRead: 15 * 1024 * 1024,
34
+ documentsRead: 31_000,
35
+ databaseQueries: 4_000,
36
+ };
37
+
16
38
  /**
17
39
  * Folds a site's queued report rollup deltas into the rollup rows. Exactly
18
- * one chain runs per site: ingestion starts one when no lease is held, every
19
- * run carries the token of the lease it belongs to, and a run whose token is
20
- * no longer current exits without touching anything. A run that finds more
21
- * queued documents than it could take continues at once; a run that drains
22
- * the queue releases the lease. A batch that lands while a run is draining
23
- * is never stranded: the run read the queue range the batch inserted into
24
- * and the batch read the lease the run released, so one of the two retries
25
- * and sees the other's work.
40
+ * one chain runs per site: a batch that finds the lease free takes it and
41
+ * schedules the first run a debounce away (see ensureRollupFold), every run
42
+ * carries the token of that lease, and a run whose token is no longer
43
+ * current exits without touching anything. Batches that land while the
44
+ * lease is held only append to the queue; they never write the lease or
45
+ * schedule anything.
46
+ *
47
+ * A run folds the batches created before it began and, if it folded any,
48
+ * schedules the next run a debounce later (at once while a backlog remains),
49
+ * so a steady stream costs one run per second and batches landing during a
50
+ * run never conflict with it. Only a run that finds nothing left releases
51
+ * the lease, and that run reads the whole queue range: a batch landing
52
+ * concurrently either commits first, and the run retries and folds it, or
53
+ * reads the released lease and starts a new chain.
54
+ *
55
+ * A run executes as a nested transaction, so one that throws is rolled back
56
+ * whole and its batches stay queued; the oldest is charged the failure and
57
+ * the next run retries it alone. After ROLLUP_FOLD_MAX_FAILURES runs in a
58
+ * row it is set aside as a dead letter and the queue moves on.
26
59
  */
27
60
  export const fold = internalMutation({
28
- args: { siteId: v.id("sites"), token: v.string() },
29
- returns: v.object({
30
- folded: v.number(),
31
- remaining: v.boolean(),
32
- stale: v.boolean(),
33
- }),
61
+ args: foldArgs,
62
+ returns: foldResult,
63
+ handler: async (ctx, args) => {
64
+ try {
65
+ const result: FoldResult = await ctx.runMutation(
66
+ internal.rollups.foldRun,
67
+ args,
68
+ { transactionLimits: RUN_READ_LIMITS },
69
+ );
70
+ return result;
71
+ } catch (error) {
72
+ return await setAsideFailedRun(ctx, args, error);
73
+ }
74
+ },
75
+ });
76
+
77
+ export const foldRun = internalMutation({
78
+ args: foldArgs,
79
+ returns: foldResult,
34
80
  handler: async (ctx, args) => {
35
81
  const state = await ctx.db
36
82
  .query("rollupFoldStates")
@@ -42,20 +88,31 @@ export const fold = internalMutation({
42
88
  const now = Date.now();
43
89
  const queued = await ctx.db
44
90
  .query("reportRollupQueue")
45
- .withIndex("by_siteId_and_createdAt", (range) =>
46
- range.eq("siteId", args.siteId),
91
+ .withIndex("by_siteId_and_deadLetteredAt_and_createdAt", (range) =>
92
+ range
93
+ .eq("siteId", args.siteId)
94
+ .eq("deadLetteredAt", undefined)
95
+ .lte("createdAt", now),
47
96
  )
48
- .order("asc")
49
97
  .take(ROLLUP_FOLD_BATCH_DOCS + 1);
50
98
  if (queued.length === 0) {
51
- if (state.leaseUntil > now) {
99
+ if (await oldestQueued(ctx, args.siteId)) {
100
+ await ctx.scheduler.runAfter(
101
+ ROLLUP_FOLD_DEBOUNCE_MS,
102
+ internal.rollups.fold,
103
+ args,
104
+ );
105
+ } else if (state.leaseUntil > now) {
52
106
  await ctx.db.patch("rollupFoldStates", state._id, { leaseUntil: now });
53
107
  }
54
108
  return { folded: 0, remaining: false, stale: false };
55
109
  }
110
+ // A batch that already failed a run is retried alone, so the next
111
+ // failure is charged to the batch that causes it.
112
+ const batchDocs = queued[0].foldFailures ? 1 : ROLLUP_FOLD_BATCH_DOCS;
56
113
  const deltas = createReportRollupDeltas();
57
114
  let taken = 0;
58
- for (const document of queued.slice(0, ROLLUP_FOLD_BATCH_DOCS)) {
115
+ for (const document of queued.slice(0, batchDocs)) {
59
116
  mergeQueuedRollups(deltas, document);
60
117
  taken += 1;
61
118
  if (countReportRollupRows(deltas) >= ROLLUP_FOLD_MAX_ROWS) break;
@@ -64,19 +121,71 @@ export const fold = internalMutation({
64
121
  for (const document of queued.slice(0, taken)) {
65
122
  await ctx.db.delete("reportRollupQueue", document._id);
66
123
  }
67
- const remaining = queued.length > taken;
68
- if (remaining) {
69
- // Renew only when the lease is halfway gone, so the state document is
70
- // rewritten about twice a minute under load rather than on every run.
71
- if (state.leaseUntil - now < ROLLUP_FOLD_LEASE_MS / 2) {
72
- await ctx.db.patch("rollupFoldStates", state._id, {
73
- leaseUntil: now + ROLLUP_FOLD_LEASE_MS,
74
- });
75
- }
76
- await ctx.scheduler.runAfter(0, internal.rollups.fold, args);
77
- } else {
78
- await ctx.db.patch("rollupFoldStates", state._id, { leaseUntil: now });
124
+ // Renew only when the lease is halfway gone, so the state document is
125
+ // rewritten about twice a minute while batches keep arriving.
126
+ if (state.leaseUntil - now < ROLLUP_FOLD_LEASE_MS / 2) {
127
+ await ctx.db.patch("rollupFoldStates", state._id, {
128
+ leaseUntil: now + ROLLUP_FOLD_LEASE_MS,
129
+ });
79
130
  }
131
+ const remaining = queued.length > taken;
132
+ await ctx.scheduler.runAfter(
133
+ remaining ? 0 : ROLLUP_FOLD_DEBOUNCE_MS,
134
+ internal.rollups.fold,
135
+ args,
136
+ );
80
137
  return { folded: taken, remaining, stale: false };
81
138
  },
82
139
  });
140
+
141
+ /** The oldest batch still waiting to fold; dead letters are skipped. */
142
+ function oldestQueued(ctx: MutationCtx, siteId: Id<"sites">) {
143
+ return ctx.db
144
+ .query("reportRollupQueue")
145
+ .withIndex("by_siteId_and_deadLetteredAt_and_createdAt", (range) =>
146
+ range.eq("siteId", siteId).eq("deadLetteredAt", undefined),
147
+ )
148
+ .first();
149
+ }
150
+
151
+ async function setAsideFailedRun(
152
+ ctx: MutationCtx,
153
+ args: { siteId: Id<"sites">; token: string },
154
+ error: unknown,
155
+ ): Promise<FoldResult> {
156
+ const state = await ctx.db
157
+ .query("rollupFoldStates")
158
+ .withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
159
+ .unique();
160
+ const head = await oldestQueued(ctx, args.siteId);
161
+ if (!head || state?.token !== args.token) throw error;
162
+ const now = Date.now();
163
+ const foldFailures = (head.foldFailures ?? 0) + 1;
164
+ if (foldFailures < ROLLUP_FOLD_MAX_FAILURES) {
165
+ await ctx.db.patch("reportRollupQueue", head._id, { foldFailures });
166
+ } else {
167
+ console.error(
168
+ `rollup fold set aside queued batch ${head._id} of site ${args.siteId} after ${foldFailures} failed runs`,
169
+ error,
170
+ );
171
+ await ctx.db.patch("reportRollupQueue", head._id, {
172
+ foldFailures,
173
+ deadLetteredAt: now,
174
+ });
175
+ // Dead letters stay for inspection; each new one retires expired ones.
176
+ const expired = await ctx.db
177
+ .query("reportRollupQueue")
178
+ .withIndex("by_siteId_and_deadLetteredAt_and_createdAt", (range) =>
179
+ range
180
+ .eq("siteId", args.siteId)
181
+ .gte("deadLetteredAt", 0)
182
+ .lt("deadLetteredAt", now - ROLLUP_DEAD_LETTER_RETENTION_MS),
183
+ )
184
+ .take(ROLLUP_FOLD_BATCH_DOCS);
185
+ for (const document of expired) {
186
+ await ctx.db.delete("reportRollupQueue", document._id);
187
+ }
188
+ }
189
+ await ctx.scheduler.runAfter(0, internal.rollups.fold, args);
190
+ return { folded: 0, remaining: true, stale: false };
191
+ }
@@ -248,10 +248,11 @@ export default defineSchema({
248
248
  .index("by_siteId_and_bucketStart", ["siteId", "bucketStart"]),
249
249
 
250
250
  // Rollup deltas wait here between ingestion and the fold job; see
251
- // rollupStore.ts. Rows are deleted as they are folded.
251
+ // rollups.ts. Rows are deleted as they are folded; a batch that keeps
252
+ // failing is set aside with `deadLetteredAt` and skipped.
252
253
  reportRollupQueue: defineTable(reportRollupQueueFields).index(
253
- "by_siteId_and_createdAt",
254
- ["siteId", "createdAt"],
254
+ "by_siteId_and_deadLetteredAt_and_createdAt",
255
+ ["siteId", "deadLetteredAt", "createdAt"],
255
256
  ),
256
257
 
257
258
  rollupFoldStates: defineTable(rollupFoldStateFields).index("by_siteId", [
@@ -441,6 +442,8 @@ export default defineSchema({
441
442
 
442
443
  ingestWindows: defineTable({
443
444
  siteId: v.id("sites"),
445
+ // A session ID, or `~client:` plus a salted daily client-network key for
446
+ // the per-client budget; no sanitized session ID starts with `~`.
444
447
  sessionId: v.string(),
445
448
  windowStart: v.number(),
446
449
  eventCount: v.number(),
@@ -1,12 +1,12 @@
1
1
  // Generated by scripts/build-tracker.mjs. Do not edit.
2
- export const RASTRO_VERSION = "0.3.0";
3
- export const TRACKER_SOURCE = "(()=>{const e=document.currentScript,t=e?.dataset.site,s=e?.dataset.endpoint||e?.src&&new URL(\"events\",e.src).href;if(!t||!s)return;const o=()=>crypto.randomUUID?.()||Math.random()+\"\",n=new URLSearchParams(location.search),r=n.get(\"ref\")?.slice(0,64);let a,i,c;for(const e of[\"source\",\"medium\",\"campaign\",\"term\",\"content\"]){const t=n.get(\"utm_\"+e)?.slice(0,64);t&&((a??={})[\"utm_\"+e]=t)}try{i=sessionStorage._r||o(),sessionStorage._r=i,c=r||sessionStorage._a,c&&(sessionStorage._a=c),a||=sessionStorage._u&&JSON.parse(sessionStorage._u),a&&(sessionStorage._u=JSON.stringify(a))}catch{i=o(),c=r}let d=e.dataset.visitor;try{d||=localStorage._rv||=o()}catch{}d||=i;let l,p=0;const g=[],h=document.referrer?new URL(document.referrer).origin:void 0,u=e=>{const o=g.splice(0,50);if(!o.length)return;const n=()=>new Blob([JSON.stringify({siteId:t,events:o,sentAt:Date.now()})]);let r=n();for(;r.size>64e3&&o.length>1;)g.unshift(o.pop()),r=n();e&&navigator.sendBeacon(s,r)||fetch(s,{method:\"POST\",body:r,keepalive:1}).catch(()=>{}),g.length&&u(e)},m=(e,t,s)=>{const o=Date.now();g.push({eventId:`${i}.${o.toString(36)}.${p}`,sessionId:i,visitorId:d,type:e,name:t,path:location.pathname.slice(0,256),referrer:h,timestamp:o,sequence:p++,affiliateSlug:c,...s}),clearTimeout(l),l=setTimeout(u,1200)};let v;const f=()=>{const e=location.pathname;e!==v&&(v=e,m(\"pageview\",void 0,a&&{properties:a}))};window.rastro=(e,t,s)=>{if(\"context\"===e)return{sessionId:i,visitorId:d};\"event\"===e&&m(\"custom\",`${t||\"event\"}`.slice(0,80),s&&JSON.stringify(s).length>2e4?void 0:{properties:s})},document.addEventListener(\"click\",e=>{const t=e.target?.closest(\"a,button,[data-rastro-event]\");if(!t)return;const s=t.dataset.rastroEvent,o=t.href,n=o&&new URL(o),r=/^https?:$/.test(n?.protocol),a=r&&n.origin!==location.origin;m(s?\"custom\":a?\"outbound\":\"click\",s,{target:(t.dataset.rastroLabel||t.innerText||t.tagName).slice(0,64),href:r?((a?n.origin:\"\")+n.pathname).slice(0,256):void 0})},1);const S=e=>{const t=history[e];history[e]=(...e)=>{t.apply(history,e),f()}};S(\"pushState\"),S(\"replaceState\"),addEventListener(\"popstate\",f),addEventListener(\"pagehide\",()=>{m(\"leave\"),u(1)}),addEventListener(\"pageshow\",e=>{e.persisted&&(m(\"heartbeat\"),u(1))}),setInterval(()=>{m(\"heartbeat\"),u()},2e4),f()})();";
4
- export const TRACKER_GZIP_BASE64 = "H4sIAAAAAAACA3VV227cNhD9FZswBBKeMLbjGIUEZtEgfXDhJkE3eTLcmKZGEhGJVMnROtuV/r2QVuv1JXmTZsi5nDlnyLlQ7zbGu0gHqHJvugYdSdOFgI6WJtiWgBQuZK5JRyQZLSHExxZ0eeuto77HhYzBJInD+4Ovf19xhit0FBngaBeyClhktuCH1PeHUQSkLrhsm92rsRQT1i15GbTLffP16+WHheSi7//SVM1GLo4ZA6fmHEvUwVSfddBN5LU3mqx3Mk5WAUE5WSJxFrBgYiFjbQ3yE7g4F1mNdKDBgskKH/gMwYEvrln0XTDIgDWY265hwIxuWm1Lx4ARhsniHaEjdiNm9GiXqqPmGzvGZ9koSTjXi4XaDOJ6d+ZGkRgorDdWRYzRerckH3SJ8lvoe88FvDArC0aFvn/u0GCShL+wKiNA9/2L8F2S/Ln89FG2OkR8ca0ToH8SrVPTnUjButIWa66FGIwmU22sGqs1KgwjrLnCB3asbLTkQza2mfe9GmdU7/tZ9f14dQ4zjCfsNJoaWnUyc6NU1zdQ7ekZsMAQMCx2RHvhEdIHW1qXrrzND06gU/jAc69KGdt5Nm9PxMRIL2t0JVVPSekmUo5Z3tf+jl8/638ziuEyTwm2RE89RHT0O6UfNKF0/p6LQdxsuRaU42LiWhZktP/hu4tzfJMku8zvTjNRys7FyhbEvWx9y8VEYS4yTBKnV7bU5IOM6PL3qI13PEIQfV8gmYpH2DRIlc9T9vnT8guDO5+v0wDfEVtd2xWmp4OQE9B8kv0goJyTJ0nHUQzQKI5AEMUjtPa9ZKVsu1jxzdTuZZ7eHm3sII82XpJfTrDwNxditLTD7Y68l3lqYabBZZ7mQOsWUwSnG0wJWk1V+qDc8W90PIjn7O2FgN1Y0wrINhhJN+0E9r8dOoNpe3wMuihsbTXhsu7K1ICUMg4CTI06fLEN+o54LaBWEWn338Hp2cmJGKYBreahF+rJUnxRWoaHSq2ShK8UQsNZq0tcWbxnMJNNJ8mmDb7FQBZjqgchhuzeutzfy6AjBf8IZVvw7TL5QUwphTMBN78Ab8i2S3U6myQNZ6aL5BsGt0cb6vvZO9w+APjbiYA4y31P3Sh2tDvD88W28PRx1XEQAzzoSuf5H2PgKxsJHQbOTG3NdwZ7WZFCSTqUSAtpah8xEmca7joi7+B63Aevtt2/mmq8YVvl0VPJRUUPu2N7fEoMXtH0foBTfv/C+FEgr/+piNq4SI9eSxrTuoVsgydvfC1Aq5Akbt4Hh2o/z60la3hc7EBM9YL5ju5853KW7nqMsNk2lvLntV3pO6z7nqR1DsMX/EHjD+nyo25QPH4BYCw+DYvxFdjlThkTx3tiiSekn1fXOIZTMYOzVI/xrmwkH9bXeJPtPxWXUuLILJK6bes1n32AAgouhiFbcjbKeEmakAlYjs9jW2uDO8vLabe+jZMTip+6dYmVzZHBJJ2Gsxr1agzV8VMx/OpKrPz9lkIoWwxxdOZJwhvOKtSB7lDTHGMMEpEuHWFY6Zrv8jw9JwY4w/Ntn4KL7H/zTKaM3wgAAA==";
5
- export const TRACKER_RAW_BYTES = 2271;
6
- export const TRACKER_GZIP_BYTES = 1177;
7
- export const TRACKER_HASH = "14dc90ee3a9b5ce4";
8
- export const VITALS_SOURCE = "(()=>{const t=document.currentScript,e=t?.dataset.site,n=t?.dataset.endpoint||t?.src&&new URL(\"events\",t.src).href;if(!e||!n)return;const r=()=>crypto.randomUUID?.()||Math.random()+\"\";let a;try{a=sessionStorage._r||r(),sessionStorage._r=a}catch{a=r()}const s=t.dataset.visitor||a,o=location.pathname.slice(0,256),i={},c=[],d=(t,e,n)=>{try{const r=new PerformanceObserver(t=>e(t.getEntries()));r.observe({type:t,buffered:1,...n}),c.push({observer:r,handler:e})}catch{}},h=performance.getEntriesByType?.(\"navigation\")[0];h&&h.responseStart>0&&(i.TTFB=h.responseStart),d(\"paint\",t=>{for(const e of t)\"first-contentful-paint\"===e.name&&(i.FCP=e.startTime)}),d(\"largest-contentful-paint\",t=>{const e=t[t.length-1];e&&(i.LCP=e.startTime)});let l=0,f=0,m=0;d(\"layout-shift\",t=>{for(const e of t)e.hadRecentInput||(l&&e.startTime-m<1e3&&e.startTime-f<5e3?l+=e.value:(l=e.value,f=e.startTime),m=e.startTime,i.CLS=Math.max(i.CLS||0,1e3*l))});const u=[],p=new Set,v=t=>{for(const e of t)e.interactionId&&(p.add(e.interactionId),u.push(e.duration));if(!u.length)return;u.sort((t,e)=>e-t),u.length=Math.min(u.length,10);const e=performance.interactionCount||p.size;i.INP=u[Math.min(u.length-1,Math.floor(e/50))]};d(\"event\",v,{durationThreshold:40}),d(\"first-input\",v);let g=0;const y=()=>{if(g)return;for(const{observer:t,handler:e}of c)e(t.takeRecords()),t.disconnect();const t=Date.now(),r=Object.keys(i).map((e,n)=>({eventId:`${a}.${t.toString(36)}.v${n}`,sessionId:a,visitorId:s,type:\"vital\",name:e,value:Math.round(i[e]),path:o,timestamp:t,sequence:n}));if(!r.length)return;g=1;const d=new Blob([JSON.stringify({siteId:e,events:r,sentAt:t})]),h=navigator.sendBeacon?.bind(navigator);h&&h(n,d)||fetch(n,{method:\"POST\",body:d,keepalive:1}).catch(()=>{})};document.addEventListener(\"visibilitychange\",()=>{\"hidden\"===document.visibilityState&&y()}),addEventListener(\"pagehide\",y)})();";
9
- export const VITALS_GZIP_BASE64 = "H4sIAAAAAAACA3VV72/bNhD9V1IhMMiV5uxl6Qe5TLCkLeAha4I4+RQEK02eLC4SqZEnd5qk/32gZOfn+k06kXfv3b17IoSKk1Y5G/AAhXaqLsEiV7X3YHGlvKmQgcBTriXKAMiDQWD2eQSsrpyx2HV4yoNXk4mF7we31xckgS1YDAnDGKc895AtTEbeQde9s9QD1t4uxupeRCjKNxU67qXVrry9XX465YR23R8S812Q0PdJsigAD+QCfdNKESAE4+wKnZcb4H/6rvOEsjdhIXslUeWtFJ7QfiwbBD4S2Zpg0Pmuk8yJwimJxlleScytLIGHwiggM/bL8QfKjGh7psTdPdOCIANmYyMjoD2d2IQr8JnzpbQKLtcB/BY8QXECBPkG8LNFbyAQSunCczceIC02FaTI1nWWgQedzhnn3PaUKV7VISft7qRPPcul1QX4FHq6I9f3LBfVU91nhc6am6aCU04SK7dmM9BL6N3sfpFPJjn3ECpnA6xQejyZTSbE8JubL2fi1SfKNEkqaSwmDMVJmzlPRtJw4LIDpElmfMCpchbBYlYX0/G0EAJ4bOWQ+sv5lQAeYsYbUwLth7yF9Bv4v8tDqV0ZgXfIC7AbzKfz+8WY7+JNvkElhZixTMxYKWaLIX/japyG3GQ/gg88l/oaFFhc2qrGriPFZPIs9bT8OIejl6Hs4zEcnRbvBfCtLGpISbF/ZNkLXKx8/soMP79YiUHgpfyHDK9dN2NzOPqpoJHFCK6OWqsGVa0A2Vb8ALuxCF6qONulnkxIxaXW5FWcsnrUEnBd+0EIlA6bWe/6ul/OmgfnkUSJU3ECU4xXxyM70MaSfYTNZ3u48EKDz4qfuzpaRcWD+RcWhi+/Xon67k2q6ZwNsaxwzhP4+XhG6X0fJzh4SsK2rN1Dv8k9hNwVOv11NqpoFKCJ00vYdhTCRsx22JrBalqTkc2e5mMjn5YLny2Xyw4UjVuL8gGuQTmv49oy5NoE5awFhWRPHcUnicCt+04o8+Jy/Rco5A/QBGIoL2VFyGgXpB24LHX67bCVPT9skaNboTd2Q44+0J5vD1vbf9tb2VKnku1MaqnTwAanSLYGZZGwuFgpsFF9o2G62mpi7uCesuhiqWNoSggoyypFFuDvGqyC1Pa74ftXw9+I+Y6THoR3Vrg1uft9dfmVhwGlyRrSxj/CUqfARrdPPQtg8TdMsaf3lOVi5zbO8wBWn4FUzp7ytbGaPH6igwcRyzTtugxQxee2BMydTpOry9VNwtZON6lmDwCVLMwW0nlP+eB6ZBhoT/vF4y9Mav054rkwAcGCJ0ns3NoUBhuVS7uBhA23ktxoDTa60+Plp6MrlAiTSUOiP73NWckN5EZDwhraU0IX/wGQzzziUQcAAA==";
10
- export const VITALS_RAW_BYTES = 1873;
11
- export const VITALS_GZIP_BYTES = 1042;
12
- export const VITALS_HASH = "ec87741c5dfdd0cb";
2
+ export const RASTRO_VERSION = "0.4.0";
3
+ export const TRACKER_SOURCE = "(()=>{const e=document.currentScript,t=e?.dataset.site,n=e?.dataset.endpoint||e?.src&&new URL(\"events\",e.src).href;if(!t||!n)return;const r=()=>crypto.randomUUID?.()||Math.random()+\"\",o=new URLSearchParams(location.search),s=o.get(\"ref\")?.slice(0,64);let a;for(const e of[\"source\",\"medium\",\"campaign\",\"term\",\"content\"]){const t=o.get(\"utm_\"+e)?.slice(0,64);t&&((a??={})[\"utm_\"+e]=t)}let i,c,d,l,h=e.dataset.visitor||\"\";try{h||=localStorage._rv||=r()}catch{}try{c=sessionStorage,d=c._r,i=c._ri==h&&c._rt,l=s||c._a,l&&(c._a=l),a||=c._u&&JSON.parse(c._u),a&&(c._u=JSON.stringify(a))}catch{l=s}let p,u=0;const v=[],g=document.referrer?new URL(document.referrer).origin:void 0,m=e=>{const r=v.splice(0,50);if(!r.length)return;const o=()=>new Blob([JSON.stringify({siteId:t,events:r,sentAt:Date.now()})]);let s=o();for(;s.size>64e3&&r.length>1;)v.unshift(r.pop()),s=o();e&&navigator.sendBeacon(n,s)||fetch(n,{method:\"POST\",body:s,keepalive:1}).catch(()=>{}),v.length&&m(e)},f=(e,t,n)=>{const o=Date.now(),s=o-i;i=o,s<18e5||(d=r(),\"pageview\"!=e&&(y=\"\",_()));try{c._r=d,c._ri=h,c._rt=o}catch{}v.push({eventId:`${d}.${o.toString(36)}.${u}`,sessionId:d,visitorId:h||d,type:e,name:t,path:location.pathname.slice(0,256),referrer:g,timestamp:o,sequence:u++,affiliateSlug:l,...n}),clearTimeout(p),p=setTimeout(m,1200)};let y;const _=()=>{const e=location.pathname;e!=y&&(y=e,f(\"pageview\",void 0,a&&{properties:a}))};window.rastro=(e,t,n)=>{if(\"context\"==e)return{sessionId:d,visitorId:h||d};\"event\"==e&&f(\"custom\",`${t||\"event\"}`.slice(0,80),n&&JSON.stringify(n).length>2e4?void 0:{properties:n})},document.addEventListener(\"click\",e=>{const t=e.target?.closest(\"a,button,[data-rastro-event]\");if(!t)return;const n=t.dataset.rastroEvent,r=t.href,o=r&&new URL(r),s=/^https?:$/.test(o?.protocol),a=s&&o.origin!=location.origin;f(n?\"custom\":a?\"outbound\":\"click\",n,{target:(t.dataset.rastroLabel||t.innerText||t.tagName).slice(0,64),href:s?((a?o.origin:\"\")+o.pathname).slice(0,256):void 0})},1);for(const e of[\"pushState\",\"replaceState\"]){const t=history[e];history[e]=(...e)=>{t.apply(history,e),_()}}addEventListener(\"popstate\",_);let S=0;const w=()=>{f(\"heartbeat\"),m()},I=()=>{S&&(clearInterval(S),S=0,f(\"leave\"),m(1))},L=()=>{document.hidden?I():S||(w(),S=setInterval(w,2e4))};addEventListener(\"pagehide\",I),addEventListener(\"pageshow\",L),document.addEventListener(\"visibilitychange\",L),S=setInterval(w,2e4),_(),L()})();";
4
+ export const TRACKER_GZIP_BASE64 = "H4sIAAAAAAACA32VUW/bNhDHv0pCBAKJXFmnTYNCHmus6B48ZG0xt09BljDS2SIqkRp5sutZ+u4DJdlOmm5PEnkn3fH4+99xLtS7XeZsoBNUucuaCi3JrPEeLS0yb2oCUjiTuSYdkGQwhGAf76DNa2cstS3OZPBZkljcnHz985ozXKOlwADjvpCFx+XULPkpte2pFR6p8XY6RPcqppL5bU1Oem1zV339Ov8wk1y07R+ainGTi3PGwKkxxgK1z4rP2usq8NJlmoyzMvS7AoJycoXEmcclEzMZSpMhn8DVpZiWSCd6unSej8c/ccsbFlzjM2TAKsxNUzFgma5qbVaWASP0/Y6zhJbYrRgrR/swDVV37Bx/iERJwrmezdSuEzd7n1tFoos5GMgghxIKhYeSrk0w5HzbMjYlv90Vbavi4coFOa9XKO/8um2V56LLNGXFrotemQoYgnF29IJcZfLOg+kfRqkiSeIbQalC22byTkOZJDy+qFKAbtvo2STJ74tPH2WtfcBobATowa1RvSWQN3ZllluuxT6DUoX+ODU0ajLe6Vrd3MLqiJXHJXqPfrYH5JlFSOfNyth07Ux+MoFK4YFPr9Yy1GNd30xET5KXJdoVFU9hcj1MMcr70j3wmx+y3kWI53lKMACaegho6VdKP2hCad2Gi07cDowE5bjoOZkGGcw/+O7qEl8nyT7yu4upWMvGhsIsiXtZu5qLHj0uppgkVq/NSpPzMqDN36POnOUWgmjbJVJWcAu7Cqlweco+f1p8YfDg8m0a4BtirUuzxvSiE7KvMu/l2glYj8GTpOIoOlgqjkBgj2p26niWmMwLMzXKQfjl4i2+aVueR3qA1XqFa4MbdqowSfhWMQZ3XAjRcxdpUTkM+BT9k5TbM7eWdRMKvuuLOM/T+7Nd3smznZPkFn2x+esrEXea7h5GNud5msPI9zxPi7bNgbY1pghWV5gS1JqK9CDluIqGg6JevbkSsOclXQGZCgPpqk4dBPy7QZth2pyfg14uTWk04aJsVmkJUkrbCchK1P6LqdA1xGsBtQpI+3UFF68mE9H1N78dabpTT7rks9SmeKq2ffEQlvxYUhgh1kmyq72r0ZPBkOpOiG66MTZ3G+l1IO8e3Z5Z8qHBfCemFI5c7/67et10aLPRO0ni100gVzG4P9tR247G7v5QwLcTAXbU+FETVux5foWXsyHz9HHathMdHASr8/y3+ONrEwgtes6y0mTfGBz1Sgolab9CmsmsdAEDcabhoSFyFm5ir3sxHP9Fn+MtGyRNT7VsFR364uDeBwavqB8o4JQ/jhwfYX/5V0FUh1l69lJSDOtmsvaOXOZim1MhSdzYaE6P1zlsTJfczvY1TPWMuYYeXGNzlu6PaGE3nCvlP6Z2rR+wbFuSxlr0X/A7xQXp1UddoXg8FSDmnoZZnAz7ZFLGxLk7cCWeMD+2xHgLF+LZ3IpKXJCmOLo81qXOcFg+GlKFCeT89gZvp8dXxaWUGMkjqeu63PLRBihiJ+i651dduzoMse6GHrk4tPzNoJUlZwVqTw+oiQmouOhgPpgWcZREDc4toV/rki8ELNQkSqdEvcbe/0KIDq6HLw7QFSbP0c7mXKSLtuWxsy2ieg9/2sArvIzq+knOeoWFyZHBXMDPzaFwGwbX4v8oj9p7MKWhbVZou8Le/2dJxNrBdZwjXEz/BQqEd9NnCQAA";
5
+ export const TRACKER_RAW_BYTES = 2407;
6
+ export const TRACKER_GZIP_BYTES = 1278;
7
+ export const TRACKER_HASH = "7fccc2fba6e092cb";
8
+ export const VITALS_SOURCE = "(()=>{const t=document.currentScript,e=t?.dataset.site,n=t?.dataset.endpoint||t?.src&&new URL(\"events\",t.src).href;if(!e||!n)return;const r=()=>crypto.randomUUID?.()||Math.random()+\"\";let s=t.dataset.visitor||\"\";try{s||=localStorage._rv||=r()}catch{}const o=Date.now();let a;try{a=sessionStorage._ri===s&&o-sessionStorage._rt<18e5&&sessionStorage._r||r(),sessionStorage._r=a,sessionStorage._ri=s,sessionStorage._rt=o}catch{a=r()}const i=s||a,c=location.pathname.slice(0,256),d={},h=[],l=(t,e,n)=>{try{const r=new PerformanceObserver(t=>e(t.getEntries()));r.observe({type:t,buffered:1,...n}),h.push({observer:r,handler:e})}catch{}},f=performance.getEntriesByType?.(\"navigation\")[0];f&&f.responseStart>0&&(d.TTFB=f.responseStart),l(\"paint\",t=>{for(const e of t)\"first-contentful-paint\"===e.name&&(d.FCP=e.startTime)}),l(\"largest-contentful-paint\",t=>{const e=t[t.length-1];e&&(d.LCP=e.startTime)});let m=0,u=0,v=0;l(\"layout-shift\",t=>{for(const e of t)e.hadRecentInput||(m&&e.startTime-v<1e3&&e.startTime-u<5e3?m+=e.value:(m=e.value,u=e.startTime),v=e.startTime,d.CLS=Math.max(d.CLS||0,1e3*m))});const g=[],p=new Set,S=t=>{for(const e of t)e.interactionId&&(p.add(e.interactionId),g.push(e.duration));if(!g.length)return;g.sort((t,e)=>e-t),g.length=Math.min(g.length,10);const e=performance.interactionCount||p.size;d.INP=g[Math.min(g.length-1,Math.floor(e/50))]};l(\"event\",S,{durationThreshold:40}),l(\"first-input\",S);let y=0;const b=()=>{if(y)return;for(const{observer:t,handler:e}of h)e(t.takeRecords()),t.disconnect();const t=Date.now(),r=Object.keys(d).map((e,n)=>({eventId:`${a}.${t.toString(36)}.v${n}`,sessionId:a,visitorId:i,type:\"vital\",name:e,value:Math.round(d[e]),path:c,timestamp:t,sequence:n}));if(!r.length)return;y=1;const s=new Blob([JSON.stringify({siteId:e,events:r,sentAt:t})]),o=navigator.sendBeacon?.bind(navigator);o&&o(n,s)||fetch(n,{method:\"POST\",body:s,keepalive:1}).catch(()=>{})};document.addEventListener(\"visibilitychange\",()=>{\"hidden\"===document.visibilityState&&b()}),addEventListener(\"pagehide\",b)})();";
9
+ export const VITALS_GZIP_BASE64 = "H4sIAAAAAAACA3VVbW/bNhD+K6lQCORKc/ayFINcJlj6AnjImqBOPwXBSosniatEauTJnSbpvw+i7MSJ22/SkXf33N1zDwmh4rxLrfF4gkLZtKnAIE8b58DgOnW6RgYCL7iSKD0g9xqBmUMLGFVbbbDv8YJ7l8axgW8nnz9dkQi2YNBHDEc75YWDbKkz8gL6/oWhDrBxZjlld2KEkrq2RsudNMpWnz+v3l1wQvv+T4nFzkjoqyhaloAnXuADhq32Gq3r+yhaoms73/eitKks12idzIH/5bZ9LxyhQyoxLbphymrFO4nAjf1GaAgqg7sUHrzX1jy6ayGEj2M7OzrBN4vf4CyOjw763hHKjsxCsu9E98dGFHaHVk7IA2QtfN9Llob6UFvDa4mFkRVwX+oUyJz9cvaaMiW6gRXi7p6VgiADZsZRj9XtGz6O6QZcZl0lTQrXGw9uC46gOAeCPAd8b9Bp8IRSunTcThdIh20NCbJNk2XgQCULxjk3A2UFrxtfkG530yWOFdKoElwCw0PrB5aJ+jHvQaLL9rat4YKTyMitzkN5Eb2b3y+zOM64A19b42GN0uH5PI6J4re3Hy7FsyPKShLVUhuMGIrzLrOOTEXDic1OkEaZdh5nqTUIBrOmnE23hRDAx1aG0B/e3gjgfox4qyugQ4hbSpfD95xDql0agXfISzA5FrPF/XKKd3UUL1CuEnPWiDnbivkyxG9tgzNf6OxH8IEXUn2CFAyuTN1g35Mqjg9Cz7ZvFnD61NS8OYPTi+qVAL6VZQMJqfafrHmCi20Pf5nib6/WIqxgJf8l4bfv52wBpz9VdKxiApePXKsDq9aAbC1+gF0bBCfTcbYrFcek5lIp8sxOWT5xCbhqXCACpUE78l1f9/KRc28dkpHiVJzDDEfX6coOtDZkb2GL+R4uPOHgQfK3thnFrOZe/wdLxVcfb0R+dxRqtmDBlpXWOgI/n80pvR/GCQbVi9iadXvot4UDX9hSJb/OJxZNBNTj9CK2nojQivkO2yaIYacz0u7LfGjk43LhwXLZ7KSg49ai/AqfILVOjWvLkCvtU2sMpEj2peOB6jEnrjd/Q4r8K7SeKMorWRMyyQXpQi0rlXx52cmBv+yQo12j0yYnp6/pwLcvOzN82WvXSiWS7bR4pRLNglJEW42yjNi4WAmwiX2TpNvGKKLu4J6yUcWSlKGuwKOs6gSZh38aMCkkZtgN3z0bfisWu5p8IN5laTfk7o/19UfuA0qdtaQb36yVSoBN71HimAeDv2OCA72nzIqd2ljHPRh1CTK15oJvtFHk4YgubRxbYpinfZ8BpgUxrKsAC6uS6OZ6fRuxjVVt4tlXgFqWegvJYqA8qB4JAx3osHx4ZKVS70c8V9ojGHAkGju30aXGNi2kySFiwSsqtFJgRnV6cH68ukaJEMcbMurTccxa5lBoBRHb0IESuvwfweV+3/MHAAA=";
10
+ export const VITALS_RAW_BYTES = 2035;
11
+ export const VITALS_GZIP_BYTES = 1097;
12
+ export const VITALS_HASH = "3dd7a68d5212c1f8";