@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.
- package/agent/manifest.json +1 -1
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +3 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +5 -2
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +56 -31
- package/dist/component/reports.js.map +1 -1
- package/dist/component/sanitize.d.ts +8 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +36 -0
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +3 -1
- package/dist/component/sites.d.ts +3 -0
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +7 -1
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +8 -1
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +6 -0
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +1 -1
- package/dist/tracker/generated.js +1 -1
- package/docs/upgrading.md +27 -0
- package/package.json +6 -6
- package/src/component/_generated/component.ts +3 -0
- package/src/component/ingest.ts +5 -1
- package/src/component/reports.ts +84 -40
- package/src/component/sanitize.ts +45 -0
- package/src/component/sites.ts +8 -0
- package/src/component/validators.ts +6 -0
- package/src/tracker/generated.ts +1 -1
package/src/component/reports.ts
CHANGED
|
@@ -397,7 +397,17 @@ export const overview = query({
|
|
|
397
397
|
? ("overviewDay" as const)
|
|
398
398
|
: ("overviewLocalDay" as const);
|
|
399
399
|
|
|
400
|
-
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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
|
|
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:
|
|
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
|
|
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",
|
|
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
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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",
|
|
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
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/src/component/sites.ts
CHANGED
|
@@ -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
|
});
|
package/src/tracker/generated.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/build-tracker.mjs. Do not edit.
|
|
2
|
-
export const RASTRO_VERSION = "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;
|