@iann29/rastro 0.8.0 → 0.9.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 (35) hide show
  1. package/agent/manifest.json +1 -1
  2. package/dist/client/index.d.ts +4 -0
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/component/_generated/component.d.ts +3 -0
  5. package/dist/component/_generated/component.d.ts.map +1 -1
  6. package/dist/component/ingest.d.ts.map +1 -1
  7. package/dist/component/ingest.js +5 -2
  8. package/dist/component/ingest.js.map +1 -1
  9. package/dist/component/reports.d.ts.map +1 -1
  10. package/dist/component/reports.js +56 -31
  11. package/dist/component/reports.js.map +1 -1
  12. package/dist/component/sanitize.d.ts +8 -0
  13. package/dist/component/sanitize.d.ts.map +1 -1
  14. package/dist/component/sanitize.js +36 -0
  15. package/dist/component/sanitize.js.map +1 -1
  16. package/dist/component/schema.d.ts +3 -1
  17. package/dist/component/sites.d.ts +3 -0
  18. package/dist/component/sites.d.ts.map +1 -1
  19. package/dist/component/sites.js +7 -1
  20. package/dist/component/sites.js.map +1 -1
  21. package/dist/component/validators.d.ts +8 -1
  22. package/dist/component/validators.d.ts.map +1 -1
  23. package/dist/component/validators.js +6 -0
  24. package/dist/component/validators.js.map +1 -1
  25. package/dist/tracker/generated.d.ts +1 -1
  26. package/dist/tracker/generated.js +1 -1
  27. package/docs/upgrading.md +27 -0
  28. package/package.json +6 -6
  29. package/src/component/_generated/component.ts +3 -0
  30. package/src/component/ingest.ts +5 -1
  31. package/src/component/reports.ts +84 -40
  32. package/src/component/sanitize.ts +45 -0
  33. package/src/component/sites.ts +8 -0
  34. package/src/component/validators.ts +6 -0
  35. package/src/tracker/generated.ts +1 -1
@@ -397,7 +397,17 @@ export const overview = query({
397
397
  ? ("overviewDay" as const)
398
398
  : ("overviewLocalDay" as const);
399
399
 
400
- await assertSourceAvailable(ctx, siteIds, [overviewDataset], from);
400
+ // Buckets before a retention boundary are gone: read from it, and the
401
+ // coverage below reports the range as partial.
402
+ const readFrom = await readableFrom(ctx, siteIds, [overviewDataset], from);
403
+ // Cleanup keeps whole the bucket that holds the boundary (a local day
404
+ // around a UTC cutoff), so reading starts at that bucket, not the instant.
405
+ const firstReadBucket = Math.max(
406
+ firstBucket,
407
+ localDays
408
+ ? localDayStart(readFrom, siteTimezone!)
409
+ : Math.floor(readFrom / intervalMs) * intervalMs,
410
+ );
401
411
 
402
412
  for (const siteId of siteIds) {
403
413
  // Iterated rather than taken whole: within the row budget a busy range
@@ -412,7 +422,7 @@ export const overview = query({
412
422
  range
413
423
  .eq("siteId", siteId)
414
424
  .eq("granularity", granularity)
415
- .gte("bucketStart", firstBucket)
425
+ .gte("bucketStart", firstReadBucket)
416
426
  .lte("bucketStart", lastBucket),
417
427
  )) {
418
428
  buckets.push(bucket);
@@ -1116,7 +1126,12 @@ export const listSessions = query({
1116
1126
  returns: paginationResultValidator(sessionDocumentValidator),
1117
1127
  handler: async (ctx, args) => {
1118
1128
  validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
1119
- await assertSourceAvailable(ctx, [args.siteId], ["sessions"], args.from);
1129
+ const readFrom = await readableFrom(
1130
+ ctx,
1131
+ [args.siteId],
1132
+ ["sessions"],
1133
+ args.from,
1134
+ );
1120
1135
  const pagination = parseKeysetPagination(args.paginationOpts, {
1121
1136
  kind: "sessions",
1122
1137
  siteId: String(args.siteId),
@@ -1145,7 +1160,7 @@ export const listSessions = query({
1145
1160
  .withIndex("by_siteId_and_lastSeenAt", (range) =>
1146
1161
  range
1147
1162
  .eq("siteId", args.siteId)
1148
- .gte("lastSeenAt", args.from)
1163
+ .gte("lastSeenAt", readFrom)
1149
1164
  .lt("lastSeenAt", pagination.cursor!.timestamp),
1150
1165
  )
1151
1166
  .order("desc")
@@ -1159,7 +1174,7 @@ export const listSessions = query({
1159
1174
  .withIndex("by_siteId_and_lastSeenAt", (range) =>
1160
1175
  range
1161
1176
  .eq("siteId", args.siteId)
1162
- .gte("lastSeenAt", args.from)
1177
+ .gte("lastSeenAt", readFrom)
1163
1178
  .lte("lastSeenAt", args.to),
1164
1179
  )
1165
1180
  .order("desc")
@@ -1193,7 +1208,12 @@ export const sessionJourney = query({
1193
1208
  returns: paginationResultValidator(eventDocumentValidator),
1194
1209
  handler: async (ctx, args) => {
1195
1210
  validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
1196
- await assertSourceAvailable(ctx, [args.siteId], ["events"], args.from);
1211
+ const readFrom = await readableFrom(
1212
+ ctx,
1213
+ [args.siteId],
1214
+ ["events"],
1215
+ args.from,
1216
+ );
1197
1217
  const pagination = parseJourneyPagination(args.paginationOpts, {
1198
1218
  kind: "events",
1199
1219
  siteId: String(args.siteId),
@@ -1204,7 +1224,7 @@ export const sessionJourney = query({
1204
1224
  const rows = await loadSessionJourneyEvents(ctx, {
1205
1225
  siteId: args.siteId,
1206
1226
  sessionId: args.sessionId,
1207
- from: args.from,
1227
+ from: readFrom,
1208
1228
  to: args.to,
1209
1229
  cursor: pagination.cursor,
1210
1230
  resultLimit: pagination.numItems + 1,
@@ -1224,7 +1244,12 @@ export const listConversions = query({
1224
1244
  returns: paginationResultValidator(conversionDocumentValidator),
1225
1245
  handler: async (ctx, args) => {
1226
1246
  validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
1227
- await assertSourceAvailable(ctx, [args.siteId], ["conversions"], args.from);
1247
+ const readFrom = await readableFrom(
1248
+ ctx,
1249
+ [args.siteId],
1250
+ ["conversions"],
1251
+ args.from,
1252
+ );
1228
1253
  const pagination = parseKeysetPagination(args.paginationOpts, {
1229
1254
  kind: "conversions",
1230
1255
  siteId: String(args.siteId),
@@ -1253,7 +1278,7 @@ export const listConversions = query({
1253
1278
  .withIndex("by_siteId_and_timestamp", (range) =>
1254
1279
  range
1255
1280
  .eq("siteId", args.siteId)
1256
- .gte("timestamp", args.from)
1281
+ .gte("timestamp", readFrom)
1257
1282
  .lt("timestamp", pagination.cursor!.timestamp),
1258
1283
  )
1259
1284
  .order("desc")
@@ -1267,7 +1292,7 @@ export const listConversions = query({
1267
1292
  .withIndex("by_siteId_and_timestamp", (range) =>
1268
1293
  range
1269
1294
  .eq("siteId", args.siteId)
1270
- .gte("timestamp", args.from)
1295
+ .gte("timestamp", readFrom)
1271
1296
  .lte("timestamp", args.to),
1272
1297
  )
1273
1298
  .order("desc")
@@ -1293,7 +1318,7 @@ export const visitorJourney = query({
1293
1318
  handler: async (ctx, args) => {
1294
1319
  const siteIds = validateSiteIds(args.siteIds);
1295
1320
  validateRange(args.from, args.to, MAX_REPORT_RANGE_DAYS);
1296
- await assertSourceAvailable(ctx, siteIds, ["events"], args.from);
1321
+ const readFrom = await readableFrom(ctx, siteIds, ["events"], args.from);
1297
1322
  const limit = args.limit ?? 200;
1298
1323
  if (
1299
1324
  !Number.isSafeInteger(limit) ||
@@ -1318,7 +1343,7 @@ export const visitorJourney = query({
1318
1343
  ...(await loadVisitorJourneyEvents(ctx, {
1319
1344
  siteId,
1320
1345
  visitorId,
1321
- from: args.from,
1346
+ from: readFrom,
1322
1347
  to: cutoff,
1323
1348
  resultLimit: limit,
1324
1349
  })),
@@ -1382,7 +1407,7 @@ export const goalsReport = query({
1382
1407
  totals.set(key, total);
1383
1408
  }
1384
1409
  } else {
1385
- await assertSourceAvailable(
1410
+ const readFrom = await readableFrom(
1386
1411
  ctx,
1387
1412
  [args.siteId],
1388
1413
  ["goalCompletions"],
@@ -1393,7 +1418,7 @@ export const goalsReport = query({
1393
1418
  .withIndex("by_siteId_and_timestamp", (range) =>
1394
1419
  range
1395
1420
  .eq("siteId", args.siteId)
1396
- .gte("timestamp", args.from)
1421
+ .gte("timestamp", readFrom)
1397
1422
  .lte("timestamp", args.to),
1398
1423
  )
1399
1424
  .take(MAX_REPORT_BUCKETS + 1);
@@ -1474,14 +1499,9 @@ export const funnelsReport = query({
1474
1499
  args.to,
1475
1500
  )
1476
1501
  : null;
1477
- if (!rollups) {
1478
- await assertSourceAvailable(
1479
- ctx,
1480
- [args.siteId],
1481
- ["funnelSteps"],
1482
- args.from,
1483
- );
1484
- }
1502
+ const stepsFrom = rollups
1503
+ ? args.from
1504
+ : await readableFrom(ctx, [args.siteId], ["funnelSteps"], args.from);
1485
1505
  const reportCoverage = await coverageForRange(
1486
1506
  ctx,
1487
1507
  [args.siteId],
@@ -1511,7 +1531,7 @@ export const funnelsReport = query({
1511
1531
  range
1512
1532
  .eq("siteId", args.siteId)
1513
1533
  .eq("funnelId", funnel._id)
1514
- .gte("bucketStart", Math.floor(args.from / DAY_MS) * DAY_MS)
1534
+ .gte("bucketStart", Math.floor(stepsFrom / DAY_MS) * DAY_MS)
1515
1535
  .lte("bucketStart", Math.floor(args.to / DAY_MS) * DAY_MS),
1516
1536
  )
1517
1537
  .take(MAX_REPORT_BUCKETS + 1);
@@ -1635,7 +1655,7 @@ export const affiliatesReport = query({
1635
1655
  totals.set(key, total);
1636
1656
  }
1637
1657
  } else {
1638
- await assertSourceAvailable(
1658
+ const readFrom = await readableFrom(
1639
1659
  ctx,
1640
1660
  [args.siteId],
1641
1661
  ["affiliateStats", "sessions"],
@@ -1646,7 +1666,7 @@ export const affiliatesReport = query({
1646
1666
  .withIndex("by_siteId_and_bucketStart", (range) =>
1647
1667
  range
1648
1668
  .eq("siteId", args.siteId)
1649
- .gte("bucketStart", Math.floor(args.from / DAY_MS) * DAY_MS)
1669
+ .gte("bucketStart", Math.floor(readFrom / DAY_MS) * DAY_MS)
1650
1670
  .lte("bucketStart", Math.floor(args.to / DAY_MS) * DAY_MS),
1651
1671
  )
1652
1672
  .take(MAX_REPORT_BUCKETS + 1);
@@ -1655,7 +1675,7 @@ export const affiliatesReport = query({
1655
1675
  .withIndex("by_siteId_and_startedAt", (range) =>
1656
1676
  range
1657
1677
  .eq("siteId", args.siteId)
1658
- .gte("startedAt", args.from)
1678
+ .gte("startedAt", readFrom)
1659
1679
  .lte("startedAt", args.to),
1660
1680
  )
1661
1681
  .take(MAX_REPORT_BUCKETS + 1);
@@ -1763,7 +1783,7 @@ export const vitalsReport = query({
1763
1783
  handler: async (ctx, args) => {
1764
1784
  validateRange(args.from, args.to, MAX_FEATURE_ROLLUP_DAYS);
1765
1785
  requireAlignedRange(args.from, args.to, DAY_MS, "vitals report");
1766
- await assertSourceAvailable(
1786
+ const readFrom = await readableFrom(
1767
1787
  ctx,
1768
1788
  [args.siteId],
1769
1789
  ["vitalRollups"],
@@ -1774,7 +1794,7 @@ export const vitalsReport = query({
1774
1794
  .withIndex("by_siteId_and_bucketStart", (range) =>
1775
1795
  range
1776
1796
  .eq("siteId", args.siteId)
1777
- .gte("bucketStart", args.from)
1797
+ .gte("bucketStart", readFrom)
1778
1798
  .lte("bucketStart", args.to),
1779
1799
  )
1780
1800
  .take(MAX_VITALS_REPORT_ROWS + 1);
@@ -2188,12 +2208,44 @@ async function coverageForRange(
2188
2208
  };
2189
2209
  }
2190
2210
 
2211
+ /**
2212
+ * Where a report may start reading `sources` for these sites: `from`, or the
2213
+ * latest retention boundary when a cleanup or a policy already purged the data
2214
+ * before it. Reports read from here and let `coverageForRange` say `partial`
2215
+ * (or `unavailable` when the whole range is gone) instead of failing: a range
2216
+ * that begins before the retained data is a question with a partial answer,
2217
+ * not an error. Rows before the boundary are deleted anyway; clamping keeps a
2218
+ * cleanup still in progress from leaking a half-purged slice into the answer.
2219
+ */
2220
+ /**
2221
+ * Money is the exception. The commission summary has no coverage to say
2222
+ * "partial", and a retained period summed as zero would read as money that
2223
+ * never came in: it refuses instead.
2224
+ */
2191
2225
  async function assertSourceAvailable(
2192
2226
  ctx: QueryCtx,
2193
2227
  siteIds: Id<"sites">[],
2194
2228
  sources: CoverageSource[],
2195
2229
  from: number,
2196
2230
  ) {
2231
+ const readFrom = await readableFrom(ctx, siteIds, sources, from);
2232
+ if (readFrom > from) {
2233
+ fail(
2234
+ "REPORT_INCOMPLETE",
2235
+ "requested report range intersects retained data",
2236
+ {
2237
+ retainedBefore: readFrom,
2238
+ },
2239
+ );
2240
+ }
2241
+ }
2242
+
2243
+ async function readableFrom(
2244
+ ctx: QueryCtx,
2245
+ siteIds: Id<"sites">[],
2246
+ sources: CoverageSource[],
2247
+ from: number,
2248
+ ): Promise<number> {
2197
2249
  const rows = await Promise.all(
2198
2250
  siteIds.map((siteId) =>
2199
2251
  ctx.db
@@ -2202,23 +2254,15 @@ async function assertSourceAvailable(
2202
2254
  .unique(),
2203
2255
  ),
2204
2256
  );
2205
- const cutoff = rows.reduce<number | null>((latest, row) => {
2257
+ let cutoff = from;
2258
+ for (const row of rows) {
2206
2259
  for (const retained of row?.retained ?? []) {
2207
2260
  if (sources.some((source) => source === retained.dataset)) {
2208
- latest = Math.max(latest ?? 0, retained.before);
2261
+ cutoff = Math.max(cutoff, retained.before);
2209
2262
  }
2210
2263
  }
2211
- return latest;
2212
- }, null);
2213
- if (cutoff !== null && from < cutoff) {
2214
- fail(
2215
- "REPORT_INCOMPLETE",
2216
- "requested report range intersects retained data",
2217
- {
2218
- retainedBefore: cutoff,
2219
- },
2220
- );
2221
2264
  }
2265
+ return cutoff;
2222
2266
  }
2223
2267
 
2224
2268
  async function datasetCoverageForRange(
@@ -85,6 +85,51 @@ const HEX_SEGMENT = /^[0-9a-f]{16,}$/i;
85
85
  const ROUTE_PARAMETER = /^:[A-Za-z][A-Za-z0-9_]*$/;
86
86
 
87
87
  /** Ordered, full-path patterns; no regex or wildcard execution from settings. */
88
+ /**
89
+ * The paths a site records: an exact path (`/`, `/login`) or a section with a
90
+ * trailing `/*` (`/blog/*` covers `/blog` and everything under it). No query,
91
+ * fragment or other wildcard: the tracker sends the pathname only.
92
+ */
93
+ export function validateTrackedPaths(paths: readonly string[]): string[] {
94
+ if (paths.length > 32)
95
+ throw new Error("At most 32 tracked paths are allowed");
96
+ return [
97
+ ...new Set(
98
+ paths.map((value) => {
99
+ const raw = value.trim();
100
+ const section = raw.endsWith("/*");
101
+ const base = section ? raw.slice(0, -2) || "/" : raw;
102
+ const path = base.length > 1 ? base.replace(/\/+$/, "") : base;
103
+ if (
104
+ !path.startsWith("/") ||
105
+ path.length > 256 ||
106
+ /[\s?#*\\]/.test(path) ||
107
+ path.includes("//")
108
+ ) {
109
+ throw new Error("Use paths such as / , /login or /blog/*");
110
+ }
111
+ return section ? (path === "/" ? "/*" : `${path}/*`) : path;
112
+ }),
113
+ ),
114
+ ];
115
+ }
116
+
117
+ /** Whether `path` is one this site records (absent or empty: every path). */
118
+ export function pathTracked(
119
+ path: string,
120
+ trackedPaths: readonly string[] | undefined,
121
+ ): boolean {
122
+ if (!trackedPaths || trackedPaths.length === 0) return true;
123
+ const normalized = path.length > 1 ? path.replace(/\/+$/, "") : path;
124
+ return trackedPaths.some((tracked) => {
125
+ if (!tracked.endsWith("/*")) return normalized === tracked;
126
+ const base = tracked.slice(0, -2);
127
+ return (
128
+ base === "" || normalized === base || normalized.startsWith(`${base}/`)
129
+ );
130
+ });
131
+ }
132
+
88
133
  export function validateRoutePatterns(patterns: readonly string[]): string[] {
89
134
  if (patterns.length > 32)
90
135
  throw new Error("At most 32 route patterns are allowed");
@@ -13,6 +13,7 @@ import {
13
13
  sanitizeOpaqueId,
14
14
  validateTimezone,
15
15
  validateRoutePatterns,
16
+ validateTrackedPaths,
16
17
  } from "./sanitize.js";
17
18
  import { siteFieldsValidator } from "./validators.js";
18
19
 
@@ -125,6 +126,8 @@ export const update = mutation({
125
126
  currency: v.optional(v.string()),
126
127
  cookieless: v.optional(v.boolean()),
127
128
  routePatterns: v.optional(v.array(v.string())),
129
+ /** The only paths the site records; `[]` records every path again. */
130
+ trackedPaths: v.optional(v.array(v.string())),
128
131
  },
129
132
  returns: v.null(),
130
133
  handler: async (ctx, args) => {
@@ -139,6 +142,7 @@ export const update = mutation({
139
142
  currency?: string;
140
143
  cookieless?: boolean;
141
144
  routePatterns?: string[];
145
+ trackedPaths?: string[];
142
146
  updatedAt: number;
143
147
  } = { updatedAt: Date.now() };
144
148
  if (args.name !== undefined) {
@@ -174,6 +178,10 @@ export const update = mutation({
174
178
  if (args.routePatterns !== undefined) {
175
179
  patch.routePatterns = validateRoutePatterns(args.routePatterns);
176
180
  }
181
+ if (args.trackedPaths !== undefined) {
182
+ const trackedPaths = validateTrackedPaths(args.trackedPaths);
183
+ patch.trackedPaths = trackedPaths.length ? trackedPaths : undefined;
184
+ }
177
185
  } catch (error) {
178
186
  fail(
179
187
  "INVALID_SITE",
@@ -162,6 +162,12 @@ export const siteFieldsValidator = v.object({
162
162
  currency: v.optional(v.string()),
163
163
  cookieless: v.boolean(),
164
164
  routePatterns: v.optional(v.array(v.string())),
165
+ /**
166
+ * Since 0.9.0: the only paths this site records. Absent or empty records
167
+ * every path. Enforced at ingestion, so a stale tab or a script loaded where
168
+ * it should not be cannot put another page's activity in the site.
169
+ */
170
+ trackedPaths: v.optional(v.array(v.string())),
165
171
  createdAt: v.number(),
166
172
  updatedAt: v.number(),
167
173
  });
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/build-tracker.mjs. Do not edit.
2
- export const RASTRO_VERSION = "0.8.0";
2
+ export const RASTRO_VERSION = "0.9.0";
3
3
  export const TRACKER_SOURCE = "(()=>{const t=document.currentScript,e=t?.dataset.site,i=t?.dataset.endpoint||t?.src&&new URL(\"events\",t.src).href;if(!e||!i)return;const n=()=>crypto.randomUUID?.()||Math.random()+\"\",r=new URLSearchParams(location.search),s=r.get(\"ref\")?.slice(0,64);let o;for(const t of[\"source\",\"medium\",\"campaign\",\"term\",\"content\"]){const e=r.get(\"utm_\"+t)?.slice(0,64);e&&((o??={})[\"utm_\"+t]=e)}const a=\"gclid gbraid wbraid msclkid ttclid twclid li_fat_id ScCid rdt_cid dclid epik srsltid mc_cid mc_eid _hsenc igsh igshid fbclid\".split(\" \").filter(t=>r.has(t)).slice(0,8);let c,d,l,h,f=t.dataset.visitor||\"\",p=Boolean(f);try{f||=localStorage._rv||=n()}catch{}try{d=sessionStorage,l=d._r,c=d._ri==f&&d._rt,h=s||d._a,h&&(d._a=h),o||=d._u&&JSON.parse(d._u),o&&(d._u=JSON.stringify(o))}catch{h=s}let v,g=0;const u=[];let m=document.referrer.split(\"/\",3).join(\"/\")||void 0;const _=t=>{const n=u.splice(0,50);if(!n.length)return;const r=()=>new Blob([JSON.stringify({siteId:e,events:n,sentAt:Date.now()})]);let s=r();for(;s.size>64e3&&n.length>1;)u.unshift(n.pop()),s=r();t&&navigator.sendBeacon(i,s)||fetch(i,{method:\"POST\",body:s,keepalive:1}).catch(()=>{}),u.length&&_(t)},y=(t,e,i)=>{const r=Date.now(),s=r-c;c=r,s<18e5||(l=n(),\"pageview\"!=t&&(S=\"\",w()));try{d._r=l,d._ri=f,d._rt=r}catch{}u.push({eventId:`${l}.${r.toString(36)}.${g}`,sessionId:l,visitorId:f||l,type:t,name:e,path:location.pathname.slice(0,256),referrer:m,timestamp:r,sequence:g++,affiliateSlug:h,...i}),clearTimeout(v),v=setTimeout(_,1200)};let S;const w=()=>{const t=location.pathname;t!=S&&(S=t,y(\"pageview\",void 0,{properties:o,clid:a.length?a:void 0}))};window.rastro=(e,i,r)=>{if(\"context\"==e)return{sessionId:l,visitorId:f||l,identified:p};if(\"identify\"==e||\"reset\"==e){if(\"identify\"==e&&(!i||i.length>128||!/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(i)))return;if(\"identify\"==e&&i==f)return;_();const r=\"reset\"==e||p;if(p=\"identify\"==e,f=\"identify\"==e?i:n(),t.dataset.visitor=\"identify\"==e?f:\"\",r){h=void 0,o=void 0,m=void 0,a.length=0;try{\"reset\"==e&&(localStorage._rv=f),d._a=\"\",d._u=\"\"}catch{}}c=0,g=0,S=\"\",w()}\"event\"==e&&y(\"custom\",`${i||\"event\"}`.slice(0,80),r&&JSON.stringify(r).length>2e4?void 0:{properties:r})},document.addEventListener(\"click\",t=>{const e=t.target?.closest(\"a,button,[data-rastro-event]\");if(!e)return;const i=e.dataset.rastroEvent,n=e.href,r=n&&new URL(n),s=/^https?:$/.test(r?.protocol),o=s&&r.origin!=location.origin;y(i?\"custom\":o?\"outbound\":\"click\",i,{target:(e.dataset.rastroLabel||e.innerText||e.tagName).slice(0,64),href:s?((o?r.origin:\"\")+r.pathname).slice(0,256):void 0})},1);for(const t of[\"pushState\",\"replaceState\"]){const e=history[t];history[t]=(...t)=>{e.apply(history,t),w()}}addEventListener(\"popstate\",w);let b=0;const I=()=>{y(\"heartbeat\"),_()},L=()=>{b&&(clearInterval(b),b=0,y(\"leave\"),_(1))},E=()=>{document.hidden?L():b||(I(),b=setInterval(I,2e4))};addEventListener(\"pagehide\",L),addEventListener(\"pageshow\",E),document.addEventListener(\"visibilitychange\",E),b=setInterval(I,2e4),w(),E()})();";
4
4
  export const TRACKER_GZIP_BASE64 = "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";
5
5
  export const TRACKER_RAW_BYTES = 2999;