@iann29/rastro 0.7.0 → 0.8.1
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/README.md +63 -0
- package/agent/integration.md +11 -0
- package/agent/manifest.json +4 -4
- package/dist/client/federation.d.ts +34 -9
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +14 -2
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +350 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +90 -5
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +158 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +5 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +8 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/leads.d.ts +190 -0
- package/dist/component/leads.d.ts.map +1 -0
- package/dist/component/leads.js +689 -0
- package/dist/component/leads.js.map +1 -0
- 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/schema.d.ts +119 -0
- package/dist/component/schema.js +39 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators.d.ts +693 -0
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +129 -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/federation-setup.md +29 -0
- package/docs/upgrading.md +42 -0
- package/llms.txt +2 -0
- package/package.json +6 -6
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +181 -0
- package/src/component/constants.ts +8 -0
- package/src/component/leads.ts +823 -0
- package/src/component/reports.ts +84 -40
- package/src/component/schema.ts +45 -0
- package/src/component/validators.ts +164 -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(
|
package/src/component/schema.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
import {
|
|
4
|
+
adCatalogFieldsValidator,
|
|
4
5
|
affiliateFieldsValidator,
|
|
5
6
|
aggregateFieldsValidator,
|
|
7
|
+
leadEventTypeValidator,
|
|
8
|
+
leadFieldsValidator,
|
|
6
9
|
batchedEventValidator,
|
|
7
10
|
funnelFieldsValidator,
|
|
8
11
|
goalFieldsValidator,
|
|
@@ -391,6 +394,48 @@ export default defineSchema({
|
|
|
391
394
|
bots: v.number(),
|
|
392
395
|
}).index("by_linkId_and_dayStart_and_shard", ["linkId", "dayStart", "shard"]),
|
|
393
396
|
|
|
397
|
+
// Leads the host's server reports (see leads.ts), one per host conversation.
|
|
398
|
+
leads: defineTable(leadFieldsValidator)
|
|
399
|
+
.index("by_siteId_and_leadKey", ["siteId", "leadKey"])
|
|
400
|
+
.index("by_siteId_and_firstAt", ["siteId", "firstAt"])
|
|
401
|
+
.index("by_siteId_and_ad_and_firstAt", ["siteId", "ad.adId", "firstAt"]),
|
|
402
|
+
|
|
403
|
+
// What happened to a lead, append-only; `eventId` makes a retry a no-op.
|
|
404
|
+
leadEvents: defineTable({
|
|
405
|
+
siteId: v.id("sites"),
|
|
406
|
+
leadId: v.id("leads"),
|
|
407
|
+
eventId: v.string(),
|
|
408
|
+
type: leadEventTypeValidator,
|
|
409
|
+
at: v.number(),
|
|
410
|
+
revenueCents: v.optional(v.number()),
|
|
411
|
+
})
|
|
412
|
+
.index("by_siteId_and_eventId", ["siteId", "eventId"])
|
|
413
|
+
.index("by_leadId_and_at", ["leadId", "at"]),
|
|
414
|
+
|
|
415
|
+
// One row per site, UTC cohort day, dimension and key. Lead volume is human
|
|
416
|
+
// scale (a conversation each), so a row is never a write hot spot.
|
|
417
|
+
leadDailyRollups: defineTable({
|
|
418
|
+
siteId: v.id("sites"),
|
|
419
|
+
dayStart: v.number(),
|
|
420
|
+
dimension: v.union(v.literal("origin"), v.literal("ad")),
|
|
421
|
+
key: v.string(),
|
|
422
|
+
leads: v.number(),
|
|
423
|
+
qualified: v.number(),
|
|
424
|
+
won: v.number(),
|
|
425
|
+
revenueCents: v.number(),
|
|
426
|
+
}).index("by_siteId_and_dimension_and_dayStart_and_key", [
|
|
427
|
+
"siteId",
|
|
428
|
+
"dimension",
|
|
429
|
+
"dayStart",
|
|
430
|
+
"key",
|
|
431
|
+
]),
|
|
432
|
+
|
|
433
|
+
// Names the host fetches from the Meta Marketing API for the ads it saw.
|
|
434
|
+
adCatalog: defineTable(adCatalogFieldsValidator).index("by_siteId_and_adId", [
|
|
435
|
+
"siteId",
|
|
436
|
+
"adId",
|
|
437
|
+
]),
|
|
438
|
+
|
|
394
439
|
visitorSecrets: defineTable({
|
|
395
440
|
key: v.literal("current"),
|
|
396
441
|
value: v.string(),
|
|
@@ -373,6 +373,79 @@ export const trackedLinkFieldsValidator = v.object({
|
|
|
373
373
|
disabledAt: v.optional(v.number()),
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Leads (0.8.0): a conversation the host's server reports, with no web session
|
|
378
|
+
* behind it. Today the channel is WhatsApp; the stage follows the host's
|
|
379
|
+
* pipeline as `lead` → `qualified` → `won`, or `lost`.
|
|
380
|
+
*/
|
|
381
|
+
export const leadChannelValidator = v.literal("whatsapp");
|
|
382
|
+
export const leadStageValidator = v.union(
|
|
383
|
+
v.literal("lead"),
|
|
384
|
+
v.literal("qualified"),
|
|
385
|
+
v.literal("won"),
|
|
386
|
+
v.literal("lost"),
|
|
387
|
+
);
|
|
388
|
+
/** What the host may move a lead to; `open` undoes a sale or a loss. */
|
|
389
|
+
export const leadStageChangeValidator = v.union(
|
|
390
|
+
v.literal("qualified"),
|
|
391
|
+
v.literal("won"),
|
|
392
|
+
v.literal("lost"),
|
|
393
|
+
v.literal("open"),
|
|
394
|
+
);
|
|
395
|
+
export const leadEventTypeValidator = v.union(
|
|
396
|
+
v.literal("created"),
|
|
397
|
+
v.literal("qualified"),
|
|
398
|
+
v.literal("won"),
|
|
399
|
+
v.literal("lost"),
|
|
400
|
+
v.literal("open"),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
/** The Meta Click-to-WhatsApp ad a lead came from, as the message carried it. */
|
|
404
|
+
export const leadAdValidator = v.object({
|
|
405
|
+
adId: v.optional(v.string()),
|
|
406
|
+
sourceApp: v.optional(v.string()),
|
|
407
|
+
sourceUrl: v.optional(v.string()),
|
|
408
|
+
ctwaClid: v.optional(v.string()),
|
|
409
|
+
title: v.optional(v.string()),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
export const leadFieldsValidator = v.object({
|
|
413
|
+
siteId: v.id("sites"),
|
|
414
|
+
/** The host's opaque id for the lead (never a phone number). */
|
|
415
|
+
leadKey: v.string(),
|
|
416
|
+
channel: leadChannelValidator,
|
|
417
|
+
/** The channel's raw entry point, as the host received it. */
|
|
418
|
+
entryPoint: v.optional(v.string()),
|
|
419
|
+
/** The bucket reports group by: `ctwa_ad` for any ad, else the entry point or `none`. */
|
|
420
|
+
originKey: v.string(),
|
|
421
|
+
ad: v.optional(leadAdValidator),
|
|
422
|
+
/** A short display name the host chooses to share, if any. */
|
|
423
|
+
label: v.optional(v.string()),
|
|
424
|
+
/** The web visitor this lead came from, when a tracked link bridged them. */
|
|
425
|
+
visitorId: v.optional(v.string()),
|
|
426
|
+
stage: leadStageValidator,
|
|
427
|
+
revenueCents: v.number(),
|
|
428
|
+
firstAt: v.number(),
|
|
429
|
+
/** UTC day of `firstAt`: the cohort every later stage is counted in. */
|
|
430
|
+
dayStart: v.number(),
|
|
431
|
+
stageAt: v.number(),
|
|
432
|
+
qualifiedAt: v.optional(v.number()),
|
|
433
|
+
wonAt: v.optional(v.number()),
|
|
434
|
+
updatedAt: v.number(),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
export const adCatalogFieldsValidator = v.object({
|
|
438
|
+
siteId: v.id("sites"),
|
|
439
|
+
adId: v.string(),
|
|
440
|
+
adName: v.optional(v.string()),
|
|
441
|
+
adsetId: v.optional(v.string()),
|
|
442
|
+
adsetName: v.optional(v.string()),
|
|
443
|
+
campaignId: v.optional(v.string()),
|
|
444
|
+
campaignName: v.optional(v.string()),
|
|
445
|
+
thumbnailUrl: v.optional(v.string()),
|
|
446
|
+
updatedAt: v.number(),
|
|
447
|
+
});
|
|
448
|
+
|
|
376
449
|
export const ingestResultValidator = v.object({
|
|
377
450
|
accepted: v.number(),
|
|
378
451
|
duplicates: v.number(),
|
|
@@ -399,3 +472,94 @@ export type FunnelStep = Infer<typeof funnelStepValidator>;
|
|
|
399
472
|
export type EventType = Infer<typeof eventTypeValidator>;
|
|
400
473
|
export type Channel = Infer<typeof channelValidator>;
|
|
401
474
|
export type Evidence = Infer<typeof evidenceValidator>;
|
|
475
|
+
|
|
476
|
+
// Lead reports. Ids are plain strings so the host's own functions can return
|
|
477
|
+
// these shapes unchanged across the component boundary.
|
|
478
|
+
const leadCountersFields = {
|
|
479
|
+
leads: v.number(),
|
|
480
|
+
qualified: v.number(),
|
|
481
|
+
won: v.number(),
|
|
482
|
+
revenueCents: v.number(),
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export const leadAdRowValidator = v.object({
|
|
486
|
+
adId: v.string(),
|
|
487
|
+
adName: v.union(v.string(), v.null()),
|
|
488
|
+
sourceApp: v.union(v.string(), v.null()),
|
|
489
|
+
sourceUrl: v.union(v.string(), v.null()),
|
|
490
|
+
title: v.union(v.string(), v.null()),
|
|
491
|
+
thumbnailUrl: v.union(v.string(), v.null()),
|
|
492
|
+
...leadCountersFields,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
export const leadAdsetRowValidator = v.object({
|
|
496
|
+
adsetId: v.union(v.string(), v.null()),
|
|
497
|
+
adsetName: v.union(v.string(), v.null()),
|
|
498
|
+
...leadCountersFields,
|
|
499
|
+
ads: v.array(leadAdRowValidator),
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
export const leadCampaignRowValidator = v.object({
|
|
503
|
+
campaignId: v.union(v.string(), v.null()),
|
|
504
|
+
campaignName: v.union(v.string(), v.null()),
|
|
505
|
+
...leadCountersFields,
|
|
506
|
+
adsets: v.array(leadAdsetRowValidator),
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
export const leadsReportValidator = v.object({
|
|
510
|
+
from: v.number(),
|
|
511
|
+
to: v.number(),
|
|
512
|
+
/** The sites' currency, or null when they differ (revenue then mixes). */
|
|
513
|
+
currency: v.union(v.string(), v.null()),
|
|
514
|
+
totals: v.object(leadCountersFields),
|
|
515
|
+
origins: v.array(v.object({ key: v.string(), ...leadCountersFields })),
|
|
516
|
+
/**
|
|
517
|
+
* Campaign → ad set → ad, with ads the catalog does not name yet grouped
|
|
518
|
+
* under a null campaign and a null ad set.
|
|
519
|
+
*/
|
|
520
|
+
campaigns: v.array(leadCampaignRowValidator),
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
export const leadSummaryValidator = v.object({
|
|
524
|
+
leadId: v.string(),
|
|
525
|
+
siteId: v.string(),
|
|
526
|
+
label: v.union(v.string(), v.null()),
|
|
527
|
+
channel: leadChannelValidator,
|
|
528
|
+
originKey: v.string(),
|
|
529
|
+
entryPoint: v.union(v.string(), v.null()),
|
|
530
|
+
adId: v.union(v.string(), v.null()),
|
|
531
|
+
sourceApp: v.union(v.string(), v.null()),
|
|
532
|
+
stage: leadStageValidator,
|
|
533
|
+
revenueCents: v.number(),
|
|
534
|
+
firstAt: v.number(),
|
|
535
|
+
stageAt: v.number(),
|
|
536
|
+
/** Whether a web journey is linked, through a tracked link. */
|
|
537
|
+
hasVisitor: v.boolean(),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
export const leadJourneyValidator = v.union(
|
|
541
|
+
v.null(),
|
|
542
|
+
v.object({
|
|
543
|
+
lead: leadSummaryValidator,
|
|
544
|
+
ad: v.union(
|
|
545
|
+
v.null(),
|
|
546
|
+
v.object({
|
|
547
|
+
adId: v.union(v.string(), v.null()),
|
|
548
|
+
adName: v.union(v.string(), v.null()),
|
|
549
|
+
adsetName: v.union(v.string(), v.null()),
|
|
550
|
+
campaignName: v.union(v.string(), v.null()),
|
|
551
|
+
sourceApp: v.union(v.string(), v.null()),
|
|
552
|
+
sourceUrl: v.union(v.string(), v.null()),
|
|
553
|
+
title: v.union(v.string(), v.null()),
|
|
554
|
+
}),
|
|
555
|
+
),
|
|
556
|
+
visitorId: v.union(v.string(), v.null()),
|
|
557
|
+
events: v.array(
|
|
558
|
+
v.object({
|
|
559
|
+
type: leadEventTypeValidator,
|
|
560
|
+
at: v.number(),
|
|
561
|
+
revenueCents: v.union(v.number(), v.null()),
|
|
562
|
+
}),
|
|
563
|
+
),
|
|
564
|
+
}),
|
|
565
|
+
);
|
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.8.1";
|
|
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;
|