@iann29/rastro 0.1.0-alpha.8 → 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.
Files changed (105) hide show
  1. package/README.md +274 -75
  2. package/agent/integration.md +223 -65
  3. package/agent/manifest.json +34 -13
  4. package/agent/manifest.schema.json +53 -11
  5. package/dist/client/federation.d.ts +196 -11
  6. package/dist/client/federation.d.ts.map +1 -1
  7. package/dist/client/federation.js +160 -6
  8. package/dist/client/federation.js.map +1 -1
  9. package/dist/client/heat.d.ts +37 -0
  10. package/dist/client/heat.d.ts.map +1 -0
  11. package/dist/client/heat.js +43 -0
  12. package/dist/client/heat.js.map +1 -0
  13. package/dist/client/index.d.ts +448 -105
  14. package/dist/client/index.d.ts.map +1 -1
  15. package/dist/client/index.js +207 -17
  16. package/dist/client/index.js.map +1 -1
  17. package/dist/component/_generated/api.d.ts +6 -0
  18. package/dist/component/_generated/api.d.ts.map +1 -1
  19. package/dist/component/_generated/api.js.map +1 -1
  20. package/dist/component/_generated/component.d.ts +53 -16
  21. package/dist/component/_generated/component.d.ts.map +1 -1
  22. package/dist/component/constants.d.ts +5 -1
  23. package/dist/component/constants.d.ts.map +1 -1
  24. package/dist/component/constants.js +8 -1
  25. package/dist/component/constants.js.map +1 -1
  26. package/dist/component/coverage.d.ts +9 -0
  27. package/dist/component/coverage.d.ts.map +1 -1
  28. package/dist/component/coverage.js +36 -6
  29. package/dist/component/coverage.js.map +1 -1
  30. package/dist/component/eventStore.d.ts +1 -1
  31. package/dist/component/geo.d.ts +2 -0
  32. package/dist/component/geo.d.ts.map +1 -1
  33. package/dist/component/geo.js +56 -19
  34. package/dist/component/geo.js.map +1 -1
  35. package/dist/component/http.d.ts.map +1 -1
  36. package/dist/component/http.js +49 -27
  37. package/dist/component/http.js.map +1 -1
  38. package/dist/component/ingest.d.ts +1 -1
  39. package/dist/component/ingest.d.ts.map +1 -1
  40. package/dist/component/ingest.js +252 -305
  41. package/dist/component/ingest.js.map +1 -1
  42. package/dist/component/localTime.d.ts +25 -0
  43. package/dist/component/localTime.d.ts.map +1 -0
  44. package/dist/component/localTime.js +126 -0
  45. package/dist/component/localTime.js.map +1 -0
  46. package/dist/component/reports.d.ts +46 -15
  47. package/dist/component/reports.d.ts.map +1 -1
  48. package/dist/component/reports.js +193 -38
  49. package/dist/component/reports.js.map +1 -1
  50. package/dist/component/retention.js +26 -8
  51. package/dist/component/retention.js.map +1 -1
  52. package/dist/component/rollupStore.d.ts +320 -0
  53. package/dist/component/rollupStore.d.ts.map +1 -0
  54. package/dist/component/rollupStore.js +596 -0
  55. package/dist/component/rollupStore.js.map +1 -0
  56. package/dist/component/rollups.d.ts +20 -0
  57. package/dist/component/rollups.d.ts.map +1 -0
  58. package/dist/component/rollups.js +73 -0
  59. package/dist/component/rollups.js.map +1 -0
  60. package/dist/component/sanitize.d.ts +5 -0
  61. package/dist/component/sanitize.d.ts.map +1 -1
  62. package/dist/component/sanitize.js +15 -0
  63. package/dist/component/sanitize.js.map +1 -1
  64. package/dist/component/schema.d.ts +349 -15
  65. package/dist/component/schema.js +25 -0
  66. package/dist/component/schema.js.map +1 -1
  67. package/dist/component/sites.d.ts +12 -0
  68. package/dist/component/sites.d.ts.map +1 -1
  69. package/dist/component/sites.js +30 -0
  70. package/dist/component/sites.js.map +1 -1
  71. package/dist/component/validators.d.ts +41 -20
  72. package/dist/component/validators.d.ts.map +1 -1
  73. package/dist/component/validators.js +24 -3
  74. package/dist/component/validators.js.map +1 -1
  75. package/dist/tracker/generated.d.ts +7 -4
  76. package/dist/tracker/generated.d.ts.map +1 -1
  77. package/dist/tracker/generated.js +7 -4
  78. package/dist/tracker/generated.js.map +1 -1
  79. package/dist/tracker/tracker.d.ts +1 -1
  80. package/dist/tracker/tracker.d.ts.map +1 -1
  81. package/dist/tracker/tracker.js +24 -2
  82. package/dist/tracker/tracker.js.map +1 -1
  83. package/dist/tracker.min.js +1 -1
  84. package/docs/federation-setup.md +104 -27
  85. package/docs/federation.md +168 -31
  86. package/docs/upgrading.md +191 -16
  87. package/llms.txt +9 -7
  88. package/package.json +2 -1
  89. package/src/component/_generated/api.ts +6 -0
  90. package/src/component/_generated/component.ts +50 -12
  91. package/src/component/constants.ts +8 -1
  92. package/src/component/coverage.ts +46 -6
  93. package/src/component/geo.ts +82 -28
  94. package/src/component/http.ts +62 -26
  95. package/src/component/ingest.ts +378 -440
  96. package/src/component/localTime.ts +167 -0
  97. package/src/component/reports.ts +261 -39
  98. package/src/component/retention.ts +25 -7
  99. package/src/component/rollupStore.ts +799 -0
  100. package/src/component/rollups.ts +82 -0
  101. package/src/component/sanitize.ts +14 -0
  102. package/src/component/schema.ts +35 -0
  103. package/src/component/sites.ts +32 -0
  104. package/src/component/validators.ts +28 -1
  105. package/src/tracker/generated.ts +12 -0
@@ -0,0 +1,82 @@
1
+ import { v } from "convex/values";
2
+ import { internal } from "./_generated/api.js";
3
+ import { internalMutation } from "./_generated/server.js";
4
+ import {
5
+ ROLLUP_FOLD_BATCH_DOCS,
6
+ ROLLUP_FOLD_LEASE_MS,
7
+ ROLLUP_FOLD_MAX_ROWS,
8
+ } from "./constants.js";
9
+ import {
10
+ countReportRollupRows,
11
+ createReportRollupDeltas,
12
+ flushReportRollups,
13
+ mergeQueuedRollups,
14
+ } from "./rollupStore.js";
15
+
16
+ /**
17
+ * 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.
26
+ */
27
+ 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
+ }),
34
+ handler: async (ctx, args) => {
35
+ const state = await ctx.db
36
+ .query("rollupFoldStates")
37
+ .withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
38
+ .unique();
39
+ if (!state || state.token !== args.token) {
40
+ return { folded: 0, remaining: false, stale: true };
41
+ }
42
+ const now = Date.now();
43
+ const queued = await ctx.db
44
+ .query("reportRollupQueue")
45
+ .withIndex("by_siteId_and_createdAt", (range) =>
46
+ range.eq("siteId", args.siteId),
47
+ )
48
+ .order("asc")
49
+ .take(ROLLUP_FOLD_BATCH_DOCS + 1);
50
+ if (queued.length === 0) {
51
+ if (state.leaseUntil > now) {
52
+ await ctx.db.patch("rollupFoldStates", state._id, { leaseUntil: now });
53
+ }
54
+ return { folded: 0, remaining: false, stale: false };
55
+ }
56
+ const deltas = createReportRollupDeltas();
57
+ let taken = 0;
58
+ for (const document of queued.slice(0, ROLLUP_FOLD_BATCH_DOCS)) {
59
+ mergeQueuedRollups(deltas, document);
60
+ taken += 1;
61
+ if (countReportRollupRows(deltas) >= ROLLUP_FOLD_MAX_ROWS) break;
62
+ }
63
+ await flushReportRollups(ctx, deltas);
64
+ for (const document of queued.slice(0, taken)) {
65
+ await ctx.db.delete("reportRollupQueue", document._id);
66
+ }
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 });
79
+ }
80
+ return { folded: taken, remaining, stale: false };
81
+ },
82
+ });
@@ -121,6 +121,20 @@ export function sanitizePublicUrl(value: string): string | undefined {
121
121
  }
122
122
  }
123
123
 
124
+ /**
125
+ * The host of a public URL the way the outbound dimension names it: lowercase,
126
+ * without port, path, or query. A relative path has no host.
127
+ */
128
+ export function publicUrlHost(value: string | undefined): string | undefined {
129
+ if (!value || value.startsWith("/")) return undefined;
130
+ try {
131
+ const host = new URL(value).hostname.toLowerCase();
132
+ return host ? host.slice(0, 120) : undefined;
133
+ } catch {
134
+ return undefined;
135
+ }
136
+ }
137
+
124
138
  export function sanitizeProperties(
125
139
  properties: EventProperties | undefined,
126
140
  ): EventProperties | undefined {
@@ -11,6 +11,10 @@ import {
11
11
  visitorSketchValidator,
12
12
  vitalMetricValidator,
13
13
  } from "./validators.js";
14
+ import {
15
+ reportRollupQueueFields,
16
+ rollupFoldStateFields,
17
+ } from "./rollupStore.js";
14
18
 
15
19
  export default defineSchema({
16
20
  sites: defineTable(siteFieldsValidator)
@@ -56,6 +60,22 @@ export default defineSchema({
56
60
  eventCount: v.number(),
57
61
  previousPath: v.optional(v.string()),
58
62
  movedAt: v.optional(v.number()),
63
+ // What the live view needs to tell intent apart (since 0.1.0): where the
64
+ // session came in, whether the visitor had a history, the campaign and
65
+ // affiliate it carries, conversions so far, and the furthest funnel step
66
+ // the visitor has reached.
67
+ entryPath: v.optional(v.string()),
68
+ returning: v.optional(v.boolean()),
69
+ utmCampaign: v.optional(v.string()),
70
+ affiliateSlug: v.optional(v.string()),
71
+ conversionCount: v.optional(v.number()),
72
+ funnel: v.optional(
73
+ v.object({
74
+ funnelId: v.id("funnels"),
75
+ step: v.number(),
76
+ steps: v.number(),
77
+ }),
78
+ ),
59
79
  })
60
80
  .index("by_siteId_and_sessionId", ["siteId", "sessionId"])
61
81
  .index("by_siteId_and_expiresAt", ["siteId", "expiresAt"])
@@ -227,11 +247,26 @@ export default defineSchema({
227
247
  ])
228
248
  .index("by_siteId_and_bucketStart", ["siteId", "bucketStart"]),
229
249
 
250
+ // Rollup deltas wait here between ingestion and the fold job; see
251
+ // rollupStore.ts. Rows are deleted as they are folded.
252
+ reportRollupQueue: defineTable(reportRollupQueueFields).index(
253
+ "by_siteId_and_createdAt",
254
+ ["siteId", "createdAt"],
255
+ ),
256
+
257
+ rollupFoldStates: defineTable(rollupFoldStateFields).index("by_siteId", [
258
+ "siteId",
259
+ ]),
260
+
230
261
  analyticsCoverage: defineTable({
231
262
  siteId: v.id("sites"),
232
263
  generation: v.literal(1),
233
264
  rollupCompleteFrom: v.number(),
234
265
  siteMapSince: v.optional(v.number()),
266
+ countersSince: v.optional(v.number()),
267
+ localDaySince: v.optional(v.number()),
268
+ // Since when the medium and outbound dimension slots are recorded.
269
+ dimensionsSince: v.optional(v.number()),
235
270
  retained: v.array(v.object({ dataset: v.string(), before: v.number() })),
236
271
  updatedAt: v.number(),
237
272
  }).index("by_siteId", ["siteId"]),
@@ -3,6 +3,8 @@ import { internalQuery, mutation, query } from "./_generated/server.js";
3
3
  import { MAX_SITES_PER_OWNER } from "./constants.js";
4
4
  import { ensureAnalyticsControl } from "./coverage.js";
5
5
  import { fail } from "./errors.js";
6
+ import { resetLocalDayCoverage } from "./coverage.js";
7
+ import { localDayTimezone } from "./localTime.js";
6
8
  import {
7
9
  cleanString,
8
10
  normalizeDomains,
@@ -173,6 +175,9 @@ export const update = mutation({
173
175
  }
174
176
  if (args.cookieless !== undefined) patch.cookieless = args.cookieless;
175
177
  await ctx.db.patch("sites", args.siteId, patch);
178
+ if (args.timezone !== undefined && site.timezone !== patch.timezone) {
179
+ await resetLocalDayCoverage(ctx, args.siteId, patch.updatedAt);
180
+ }
176
181
  return null;
177
182
  },
178
183
  });
@@ -185,6 +190,33 @@ export const get = query({
185
190
  },
186
191
  });
187
192
 
193
+ /**
194
+ * The calendar days a site's daily overview can answer in: the timezone its
195
+ * local-day buckets follow and the instant from which they are complete, or
196
+ * null when the site keeps none (no timezone, or a zone that never leaves
197
+ * UTC, whose days are the UTC days already kept).
198
+ */
199
+ export const localDays = query({
200
+ args: { siteId: v.id("sites") },
201
+ returns: v.union(
202
+ v.object({ timezone: v.string(), since: v.number() }),
203
+ v.null(),
204
+ ),
205
+ handler: async (ctx, args) => {
206
+ const site = await ctx.db.get("sites", args.siteId);
207
+ const timezone = localDayTimezone(site?.timezone);
208
+ if (!timezone) return null;
209
+ const control = await ctx.db
210
+ .query("analyticsCoverage")
211
+ .withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
212
+ .unique();
213
+ // A site that has not ingested since the upgrade has no stamp yet; its
214
+ // days begin with its first batch, which is later than now.
215
+ if (control?.localDaySince === undefined) return null;
216
+ return { timezone, since: control.localDaySince };
217
+ },
218
+ });
219
+
188
220
  export const listByOwner = query({
189
221
  args: {
190
222
  ownerId: v.string(),
@@ -6,6 +6,7 @@ export const eventTypeValidator = v.union(
6
6
  v.literal("custom"),
7
7
  v.literal("conversion"),
8
8
  v.literal("heartbeat"),
9
+ v.literal("leave"),
9
10
  v.literal("outbound"),
10
11
  v.literal("vital"),
11
12
  );
@@ -84,6 +85,9 @@ export const sessionFieldsValidator = v.object({
84
85
  sessionId: v.string(),
85
86
  visitorId: v.string(),
86
87
  visitorKey: v.optional(v.string()),
88
+ // Since 0.1.0: whether nothing was recorded under the visitor before this
89
+ // session (a host id linked to anonymous history at sign-up is a return).
90
+ newVisitor: v.optional(v.boolean()),
87
91
  startedAt: v.number(),
88
92
  lastSeenAt: v.number(),
89
93
  entryPath: v.string(),
@@ -147,6 +151,10 @@ export const dimensionTypeValidator = v.union(
147
151
  v.literal("browser"),
148
152
  v.literal("event"),
149
153
  v.literal("goal"),
154
+ // Since 0.1.0: the lowercase utm_medium of the session, and the destination
155
+ // host of an outbound click (never its path).
156
+ v.literal("medium"),
157
+ v.literal("outbound"),
150
158
  );
151
159
 
152
160
  export const dimensionSlotValidator = v.object({
@@ -155,6 +163,11 @@ export const dimensionSlotValidator = v.object({
155
163
  value: v.string(),
156
164
  count: v.number(),
157
165
  revenueCents: v.number(),
166
+ // Slots written since alpha.11 also count the pageviews and the sessions
167
+ // (a session's first event) they saw, so pages rank by views and every
168
+ // other dimension by sessions instead of by raw event volume.
169
+ pageviews: v.optional(v.number()),
170
+ sessions: v.optional(v.number()),
158
171
  });
159
172
 
160
173
  export const visitorSketchValidator = v.object({
@@ -163,9 +176,17 @@ export const visitorSketchValidator = v.object({
163
176
  registers: v.bytes(),
164
177
  });
165
178
 
179
+ export const aggregateGranularityValidator = v.union(
180
+ v.literal("hour"),
181
+ v.literal("day"),
182
+ // One bucket per calendar day in the site's timezone, written alongside
183
+ // the UTC day for sites with a timezone (see localTime.ts).
184
+ v.literal("localDay"),
185
+ );
186
+
166
187
  export const aggregateFieldsValidator = v.object({
167
188
  siteId: v.id("sites"),
168
- granularity: v.union(v.literal("hour"), v.literal("day")),
189
+ granularity: aggregateGranularityValidator,
169
190
  bucketStart: v.number(),
170
191
  shard: v.number(),
171
192
  events: v.number(),
@@ -180,6 +201,12 @@ export const aggregateFieldsValidator = v.object({
180
201
  conversions: v.number(),
181
202
  revenueCents: v.number(),
182
203
  dimensions: v.array(dimensionSlotValidator),
204
+ // Engagement counters recorded since alpha.11: how much session duration
205
+ // grew from the activity that landed in this bucket, sessions that reached
206
+ // a second pageview here, and sessions of visitors never seen before.
207
+ durationMs: v.optional(v.number()),
208
+ continuedSessions: v.optional(v.number()),
209
+ newVisitors: v.optional(v.number()),
183
210
  });
184
211
 
185
212
  export const goalMatchTypeValidator = v.union(
@@ -0,0 +1,12 @@
1
+ // Generated by scripts/build-tracker.mjs. Do not edit.
2
+ export const RASTRO_VERSION = "0.1.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";