@iann29/rastro 0.4.0 → 0.6.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/README.md +146 -24
- package/agent/integration.md +66 -29
- package/agent/manifest.json +19 -8
- package/agent/manifest.schema.json +28 -9
- package/dist/client/federation.d.ts +38 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +18 -1
- package/dist/client/federation.js.map +1 -1
- package/dist/client/identity.d.ts +11 -0
- package/dist/client/identity.d.ts.map +1 -0
- package/dist/client/identity.js +123 -0
- package/dist/client/identity.js.map +1 -0
- package/dist/client/index.d.ts +501 -11
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +220 -4
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +8 -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 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/constants.d.ts +2 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +7 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/coverage.d.ts +1 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +6 -1
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/eventStore.d.ts +2 -0
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +51 -1
- package/dist/component/http.js.map +1 -1
- package/dist/component/ingest.d.ts +2 -0
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +72 -22
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/origin.d.ts +70 -0
- package/dist/component/origin.d.ts.map +1 -0
- package/dist/component/origin.js +230 -0
- package/dist/component/origin.js.map +1 -0
- package/dist/component/people.d.ts +79 -0
- package/dist/component/people.d.ts.map +1 -0
- package/dist/component/people.js +249 -0
- package/dist/component/people.js.map +1 -0
- package/dist/component/platforms.d.ts +33 -0
- package/dist/component/platforms.d.ts.map +1 -0
- package/dist/component/platforms.js +328 -0
- package/dist/component/platforms.js.map +1 -0
- package/dist/component/reports.d.ts +35 -74
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +59 -14
- package/dist/component/reports.js.map +1 -1
- package/dist/component/sanitize.d.ts +5 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +33 -1
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +101 -7
- package/dist/component/schema.js +45 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/trackedLinks.d.ts +91 -0
- package/dist/component/trackedLinks.d.ts.map +1 -0
- package/dist/component/trackedLinks.js +314 -0
- package/dist/component/trackedLinks.js.map +1 -0
- package/dist/component/useragent.d.ts +6 -0
- package/dist/component/useragent.d.ts.map +1 -1
- package/dist/component/useragent.js +9 -0
- package/dist/component/useragent.js.map +1 -1
- package/dist/component/validators.d.ts +101 -11
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +62 -1
- package/dist/component/validators.js.map +1 -1
- package/dist/component/visitors.d.ts +38 -2
- package/dist/component/visitors.d.ts.map +1 -1
- package/dist/component/visitors.js +162 -42
- package/dist/component/visitors.js.map +1 -1
- package/dist/react/index.d.ts +9 -5
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +36 -5
- package/dist/react/index.js.map +1 -1
- package/dist/tracker/generated.d.ts +6 -6
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +6 -6
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts +3 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +56 -5
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/docs/federation.md +24 -0
- package/docs/identity.md +307 -0
- package/docs/upgrading.md +190 -19
- package/llms.txt +11 -2
- package/package.json +5 -3
- package/src/component/_generated/api.ts +8 -0
- package/src/component/_generated/component.ts +251 -1
- package/src/component/constants.ts +7 -0
- package/src/component/coverage.ts +7 -1
- package/src/component/http.ts +56 -1
- package/src/component/ingest.ts +100 -28
- package/src/component/origin.ts +273 -0
- package/src/component/people.ts +321 -0
- package/src/component/platforms.ts +359 -0
- package/src/component/reports.ts +84 -13
- package/src/component/sanitize.ts +39 -1
- package/src/component/schema.ts +53 -0
- package/src/component/trackedLinks.ts +384 -0
- package/src/component/useragent.ts +11 -0
- package/src/component/validators.ts +120 -0
- package/src/component/visitors.ts +232 -55
- package/src/tracker/generated.ts +6 -6
package/src/component/reports.ts
CHANGED
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
type RawJourneyEvent,
|
|
56
56
|
} from "./eventStore.js";
|
|
57
57
|
import { isPlainRecord } from "./guards.js";
|
|
58
|
+
import { classifyOrigin } from "./origin.js";
|
|
59
|
+
import { externalReferrer } from "./sanitize.js";
|
|
58
60
|
import {
|
|
59
61
|
affiliateFieldsValidator,
|
|
60
62
|
eventFieldsValidator,
|
|
@@ -62,8 +64,9 @@ import {
|
|
|
62
64
|
goalFieldsValidator,
|
|
63
65
|
sessionFieldsValidator,
|
|
64
66
|
vitalMetricValidator,
|
|
67
|
+
visitorProfileValidator,
|
|
65
68
|
} from "./validators.js";
|
|
66
|
-
import { resolveVisitorIdentities } from "./visitors.js";
|
|
69
|
+
import { resolveVisitorIdentities, withVisitorProfiles } from "./visitors.js";
|
|
67
70
|
import {
|
|
68
71
|
mergeVitalHistograms,
|
|
69
72
|
VITAL_ALL,
|
|
@@ -74,6 +77,7 @@ import {
|
|
|
74
77
|
} from "./vitals.js";
|
|
75
78
|
|
|
76
79
|
const sessionDocumentValidator = sessionFieldsValidator.extend({
|
|
80
|
+
profile: v.optional(visitorProfileValidator),
|
|
77
81
|
_id: v.id("sessions"),
|
|
78
82
|
_creationTime: v.number(),
|
|
79
83
|
});
|
|
@@ -82,6 +86,7 @@ const eventDocumentValidator = eventFieldsValidator.extend({
|
|
|
82
86
|
_creationTime: v.number(),
|
|
83
87
|
});
|
|
84
88
|
const liveSessionDocumentValidator = v.object({
|
|
89
|
+
profile: v.optional(visitorProfileValidator),
|
|
85
90
|
_id: v.id("liveSessions"),
|
|
86
91
|
_creationTime: v.number(),
|
|
87
92
|
siteId: v.id("sites"),
|
|
@@ -220,6 +225,11 @@ export const overview = query({
|
|
|
220
225
|
topMediums: v.array(topItemValidator),
|
|
221
226
|
// Destination hosts of outbound clicks, ranked by clicks.
|
|
222
227
|
topOutbound: v.array(topItemValidator),
|
|
228
|
+
// The session's origin (origin.ts), ranked like sources: platform ids,
|
|
229
|
+
// channel ids, and the evidence that decided them.
|
|
230
|
+
topPlatforms: v.array(topItemValidator),
|
|
231
|
+
topChannels: v.array(topItemValidator),
|
|
232
|
+
topEvidence: v.array(topItemValidator),
|
|
223
233
|
metadata: v.object({
|
|
224
234
|
visitors: v.object({
|
|
225
235
|
basis: v.union(
|
|
@@ -242,6 +252,9 @@ export const overview = query({
|
|
|
242
252
|
topGoals: v.literal("goalCompletionVolume"),
|
|
243
253
|
topMediums: breakdownBasisValidator,
|
|
244
254
|
topOutbound: v.literal("eventVolume"),
|
|
255
|
+
topPlatforms: breakdownBasisValidator,
|
|
256
|
+
topChannels: breakdownBasisValidator,
|
|
257
|
+
topEvidence: breakdownBasisValidator,
|
|
245
258
|
}),
|
|
246
259
|
// Since when every site in the request records the medium and outbound
|
|
247
260
|
// slots; null while one of them has not stamped it. A range that starts
|
|
@@ -536,6 +549,9 @@ export const overview = query({
|
|
|
536
549
|
topGoals: top("goal", "eventVolume"),
|
|
537
550
|
topMediums: top("medium", sessionBasis),
|
|
538
551
|
topOutbound: top("outbound", "eventVolume"),
|
|
552
|
+
topPlatforms: top("platform", sessionBasis),
|
|
553
|
+
topChannels: top("channel", sessionBasis),
|
|
554
|
+
topEvidence: top("evidence", sessionBasis),
|
|
539
555
|
metadata: {
|
|
540
556
|
visitors: {
|
|
541
557
|
basis: visitorBasis,
|
|
@@ -555,6 +571,9 @@ export const overview = query({
|
|
|
555
571
|
topGoals: "goalCompletionVolume" as const,
|
|
556
572
|
topMediums: sessionBasis,
|
|
557
573
|
topOutbound: "eventVolume" as const,
|
|
574
|
+
topPlatforms: sessionBasis,
|
|
575
|
+
topChannels: sessionBasis,
|
|
576
|
+
topEvidence: sessionBasis,
|
|
558
577
|
},
|
|
559
578
|
dimensionsSince: controls.every(
|
|
560
579
|
(control) => control?.dimensionsSince !== undefined,
|
|
@@ -612,7 +631,7 @@ export const liveVisitors = query({
|
|
|
612
631
|
.take(limit)),
|
|
613
632
|
);
|
|
614
633
|
}
|
|
615
|
-
|
|
634
|
+
const visible = rows
|
|
616
635
|
.sort((left, right) => right.lastSeenAt - left.lastSeenAt)
|
|
617
636
|
.slice(0, limit)
|
|
618
637
|
.map((row) => ({
|
|
@@ -646,6 +665,7 @@ export const liveVisitors = query({
|
|
|
646
665
|
: {}),
|
|
647
666
|
...(row.funnel ? { funnel: row.funnel } : {}),
|
|
648
667
|
}));
|
|
668
|
+
return withVisitorProfiles(ctx, visible);
|
|
649
669
|
},
|
|
650
670
|
});
|
|
651
671
|
|
|
@@ -948,6 +968,27 @@ async function readRouteRollups(
|
|
|
948
968
|
return { rows, completeFrom: cutAt + DAY_MS };
|
|
949
969
|
}
|
|
950
970
|
|
|
971
|
+
type SessionRow = Omit<
|
|
972
|
+
Doc<"sessions">,
|
|
973
|
+
"geoLookupAttemptedAt" | "lastPageviewAt"
|
|
974
|
+
>;
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* A session recorded before origins existed, classified on read from what it
|
|
978
|
+
* kept (utm fields, referrer, affiliate) and marked `legacy`: it carries no
|
|
979
|
+
* click IDs or in-app signal, and it never enters the aggregates.
|
|
980
|
+
*/
|
|
981
|
+
function withOrigin(session: SessionRow, domains: string[]): SessionRow {
|
|
982
|
+
if (session.platform !== undefined) return session;
|
|
983
|
+
const origin = classifyOrigin({
|
|
984
|
+
utmSource: session.utmSource,
|
|
985
|
+
utmMedium: session.utmMedium,
|
|
986
|
+
referrer: externalReferrer(session.referrer, domains),
|
|
987
|
+
affiliate: session.affiliateSlug !== undefined,
|
|
988
|
+
});
|
|
989
|
+
return { ...session, ...origin, evidence: "legacy" };
|
|
990
|
+
}
|
|
991
|
+
|
|
951
992
|
/** Metadata for a selected session, independent of list pagination. */
|
|
952
993
|
export const getSession = query({
|
|
953
994
|
args: { siteId: v.id("sites"), sessionId: v.string() },
|
|
@@ -965,7 +1006,10 @@ export const getSession = query({
|
|
|
965
1006
|
lastPageviewAt: _pageview,
|
|
966
1007
|
...result
|
|
967
1008
|
} = session;
|
|
968
|
-
|
|
1009
|
+
const site = await ctx.db.get("sites", args.siteId);
|
|
1010
|
+
return (
|
|
1011
|
+
await withVisitorProfiles(ctx, [withOrigin(result, site?.domains ?? [])])
|
|
1012
|
+
)[0];
|
|
969
1013
|
},
|
|
970
1014
|
});
|
|
971
1015
|
|
|
@@ -1104,17 +1148,19 @@ export const listSessions = query({
|
|
|
1104
1148
|
.take(take)),
|
|
1105
1149
|
);
|
|
1106
1150
|
}
|
|
1151
|
+
const site = await ctx.db.get("sites", args.siteId);
|
|
1107
1152
|
const publicRows = rows.map(
|
|
1108
1153
|
({
|
|
1109
1154
|
geoLookupAttemptedAt: _attemptedAt,
|
|
1110
1155
|
lastPageviewAt: _lastPageviewAt,
|
|
1111
1156
|
...row
|
|
1112
|
-
}) => row,
|
|
1157
|
+
}) => withOrigin(row, site?.domains ?? []),
|
|
1113
1158
|
);
|
|
1114
|
-
|
|
1159
|
+
const result = keysetPaginationResult(publicRows, pagination, (row) => ({
|
|
1115
1160
|
timestamp: row.lastSeenAt,
|
|
1116
1161
|
creationTime: row._creationTime,
|
|
1117
1162
|
}));
|
|
1163
|
+
return { ...result, page: await withVisitorProfiles(ctx, result.page) };
|
|
1118
1164
|
},
|
|
1119
1165
|
});
|
|
1120
1166
|
|
|
@@ -1826,6 +1872,7 @@ const coverageDatasetValidator = v.union(
|
|
|
1826
1872
|
v.literal("affiliates"),
|
|
1827
1873
|
v.literal("vitals"),
|
|
1828
1874
|
v.literal("siteMap"),
|
|
1875
|
+
v.literal("origin"),
|
|
1829
1876
|
);
|
|
1830
1877
|
|
|
1831
1878
|
export const dataCoverage = query({
|
|
@@ -1859,6 +1906,7 @@ export const dataCoverage = query({
|
|
|
1859
1906
|
"affiliates",
|
|
1860
1907
|
"vitals",
|
|
1861
1908
|
"siteMap",
|
|
1909
|
+
"origin",
|
|
1862
1910
|
] as const;
|
|
1863
1911
|
return {
|
|
1864
1912
|
siteId: args.siteId,
|
|
@@ -2163,7 +2211,26 @@ async function datasetCoverageForRange(
|
|
|
2163
2211
|
to: number,
|
|
2164
2212
|
) {
|
|
2165
2213
|
if (dataset === "siteMap") {
|
|
2166
|
-
return
|
|
2214
|
+
return stampedDatasetCoverage(
|
|
2215
|
+
ctx,
|
|
2216
|
+
siteId,
|
|
2217
|
+
from,
|
|
2218
|
+
to,
|
|
2219
|
+
"siteMapSince",
|
|
2220
|
+
siteMapCoverageDataset,
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
// Origins exist in sessions and aggregate slots from the site's stamp, and
|
|
2224
|
+
// the overview reads them from the daily aggregates retention trims.
|
|
2225
|
+
if (dataset === "origin") {
|
|
2226
|
+
return stampedDatasetCoverage(
|
|
2227
|
+
ctx,
|
|
2228
|
+
siteId,
|
|
2229
|
+
from,
|
|
2230
|
+
to,
|
|
2231
|
+
"originSince",
|
|
2232
|
+
"overviewDay",
|
|
2233
|
+
);
|
|
2167
2234
|
}
|
|
2168
2235
|
// Vitals have no legacy fallback source; the rollup either covers the range
|
|
2169
2236
|
// or the coverage states say how much of it exists.
|
|
@@ -2205,23 +2272,26 @@ async function datasetCoverageForRange(
|
|
|
2205
2272
|
}
|
|
2206
2273
|
|
|
2207
2274
|
/**
|
|
2208
|
-
*
|
|
2209
|
-
* `siteMapSince` and ends where retention cut
|
|
2210
|
-
* narrow the window when the range exceeds
|
|
2275
|
+
* A stamped dataset's coverage without reading its rows: it starts at the
|
|
2276
|
+
* site's stamp (`siteMapSince`, `originSince`) and ends where retention cut
|
|
2277
|
+
* it. The site map report may still narrow the window when the range exceeds
|
|
2278
|
+
* its row budget.
|
|
2211
2279
|
*/
|
|
2212
|
-
async function
|
|
2280
|
+
async function stampedDatasetCoverage(
|
|
2213
2281
|
ctx: QueryCtx,
|
|
2214
2282
|
siteId: Id<"sites">,
|
|
2215
2283
|
from: number,
|
|
2216
2284
|
to: number,
|
|
2285
|
+
stamp: "siteMapSince" | "originSince",
|
|
2286
|
+
retainedDataset: string,
|
|
2217
2287
|
) {
|
|
2218
2288
|
const control = await ctx.db
|
|
2219
2289
|
.query("analyticsCoverage")
|
|
2220
2290
|
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
2221
2291
|
.unique();
|
|
2222
|
-
const since = control?.
|
|
2292
|
+
const since = control?.[stamp] ?? null;
|
|
2223
2293
|
const retainedBefore =
|
|
2224
|
-
control?.retained.find((item) => item.dataset ===
|
|
2294
|
+
control?.retained.find((item) => item.dataset === retainedDataset)
|
|
2225
2295
|
?.before ?? null;
|
|
2226
2296
|
const unavailable =
|
|
2227
2297
|
since === null ||
|
|
@@ -2252,7 +2322,8 @@ type PublicCoverageDataset =
|
|
|
2252
2322
|
| "funnels"
|
|
2253
2323
|
| "affiliates"
|
|
2254
2324
|
| "vitals"
|
|
2255
|
-
| "siteMap"
|
|
2325
|
+
| "siteMap"
|
|
2326
|
+
| "origin";
|
|
2256
2327
|
|
|
2257
2328
|
type CoverageSource =
|
|
2258
2329
|
| "overviewHour"
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
TrackerEvent,
|
|
11
11
|
} from "./validators.js";
|
|
12
12
|
import { isValidVitalValue, isVitalMetric } from "./vitals.js";
|
|
13
|
+
import { isPlatform, knownClickIds } from "./origin.js";
|
|
13
14
|
|
|
14
15
|
const HOST_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
15
16
|
const SAFE_KEY = /^[A-Za-z][A-Za-z0-9_.-]{0,39}$/;
|
|
@@ -121,6 +122,27 @@ export function sanitizePublicUrl(value: string): string | undefined {
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
const ANDROID_PACKAGE = /^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+)+$/;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A referrer as sanitizePublicUrl keeps it, or the Android app that opened
|
|
129
|
+
* the link: `android-app://<package>/…` keeps the package alone.
|
|
130
|
+
*/
|
|
131
|
+
function sanitizeReferrer(value: string): string | undefined {
|
|
132
|
+
const input = cleanString(value, 2048);
|
|
133
|
+
if (!input.toLowerCase().startsWith("android-app://")) {
|
|
134
|
+
return sanitizePublicUrl(input);
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const pkg = new URL(input).hostname.toLowerCase();
|
|
138
|
+
return pkg.length <= 120 && ANDROID_PACKAGE.test(pkg)
|
|
139
|
+
? `android-app://${pkg}`
|
|
140
|
+
: undefined;
|
|
141
|
+
} catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
124
146
|
/**
|
|
125
147
|
* The host of a public URL the way the outbound dimension names it: lowercase,
|
|
126
148
|
* without port, path, or query. A relative path has no host.
|
|
@@ -229,7 +251,7 @@ export function sanitizeEvent(event: TrackerEvent, now: number): TrackerEvent {
|
|
|
229
251
|
referrer:
|
|
230
252
|
event.referrer === undefined
|
|
231
253
|
? undefined
|
|
232
|
-
:
|
|
254
|
+
: sanitizeReferrer(event.referrer),
|
|
233
255
|
timestamp: Math.trunc(event.timestamp),
|
|
234
256
|
sequence: event.sequence,
|
|
235
257
|
target:
|
|
@@ -246,6 +268,7 @@ export function sanitizeEvent(event: TrackerEvent, now: number): TrackerEvent {
|
|
|
246
268
|
event.type === "vital" && event.value !== undefined
|
|
247
269
|
? Math.round(event.value)
|
|
248
270
|
: undefined,
|
|
271
|
+
clid: event.clid === undefined ? undefined : knownClickIds(event.clid),
|
|
249
272
|
};
|
|
250
273
|
}
|
|
251
274
|
|
|
@@ -353,6 +376,8 @@ export function sanitizeContext(
|
|
|
353
376
|
context?.visitorKey && SAFE_OPAQUE_ID.test(context.visitorKey)
|
|
354
377
|
? cleanString(context.visitorKey, 128) || undefined
|
|
355
378
|
: undefined,
|
|
379
|
+
inApp:
|
|
380
|
+
context?.inApp && isPlatform(context.inApp) ? context.inApp : undefined,
|
|
356
381
|
};
|
|
357
382
|
}
|
|
358
383
|
|
|
@@ -393,6 +418,19 @@ export function sourceFromReferrer(referrer: string | undefined): string {
|
|
|
393
418
|
}
|
|
394
419
|
}
|
|
395
420
|
|
|
421
|
+
/**
|
|
422
|
+
* The referrer unless it is internal: a relative path or one of the site's
|
|
423
|
+
* own domains never names where a session came from.
|
|
424
|
+
*/
|
|
425
|
+
export function externalReferrer(
|
|
426
|
+
referrer: string | undefined,
|
|
427
|
+
domains: string[],
|
|
428
|
+
): string | undefined {
|
|
429
|
+
if (!referrer || referrer.startsWith("/") || originAllowed(referrer, domains))
|
|
430
|
+
return undefined;
|
|
431
|
+
return referrer;
|
|
432
|
+
}
|
|
433
|
+
|
|
396
434
|
export function stableHash(value: string): number {
|
|
397
435
|
let hash = 2_166_136_261;
|
|
398
436
|
for (let index = 0; index < value.length; index += 1) {
|
package/src/component/schema.ts
CHANGED
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
goalFieldsValidator,
|
|
9
9
|
sessionFieldsValidator,
|
|
10
10
|
siteFieldsValidator,
|
|
11
|
+
trackedLinkFieldsValidator,
|
|
11
12
|
visitorSketchValidator,
|
|
13
|
+
visitorProfileValidator,
|
|
14
|
+
personAttributeValueValidator,
|
|
12
15
|
vitalMetricValidator,
|
|
13
16
|
} from "./validators.js";
|
|
14
17
|
import {
|
|
@@ -268,6 +271,8 @@ export default defineSchema({
|
|
|
268
271
|
localDaySince: v.optional(v.number()),
|
|
269
272
|
// Since when the medium and outbound dimension slots are recorded.
|
|
270
273
|
dimensionsSince: v.optional(v.number()),
|
|
274
|
+
// Since when sessions carry their origin and the origin slots exist.
|
|
275
|
+
originSince: v.optional(v.number()),
|
|
271
276
|
retained: v.array(v.object({ dataset: v.string(), before: v.number() })),
|
|
272
277
|
updatedAt: v.number(),
|
|
273
278
|
}).index("by_siteId", ["siteId"]),
|
|
@@ -371,12 +376,60 @@ export default defineSchema({
|
|
|
371
376
|
.index("by_siteId", ["siteId"])
|
|
372
377
|
.index("by_siteId_and_slug", ["siteId", "slug"]),
|
|
373
378
|
|
|
379
|
+
// Unique by slug across the deployment: the redirect route names no site.
|
|
380
|
+
trackedLinks: defineTable(trackedLinkFieldsValidator)
|
|
381
|
+
.index("by_siteId", ["siteId"])
|
|
382
|
+
.index("by_slug", ["slug"]),
|
|
383
|
+
|
|
384
|
+
// One row per link, UTC day and shard, plus each shard's all-time row
|
|
385
|
+
// under dayStart 0 (see trackedLinks.ts).
|
|
386
|
+
trackedLinkClicks: defineTable({
|
|
387
|
+
linkId: v.id("trackedLinks"),
|
|
388
|
+
dayStart: v.number(),
|
|
389
|
+
shard: v.number(),
|
|
390
|
+
clicks: v.number(),
|
|
391
|
+
bots: v.number(),
|
|
392
|
+
}).index("by_linkId_and_dayStart_and_shard", ["linkId", "dayStart", "shard"]),
|
|
393
|
+
|
|
374
394
|
visitorSecrets: defineTable({
|
|
375
395
|
key: v.literal("current"),
|
|
376
396
|
value: v.string(),
|
|
377
397
|
createdAt: v.number(),
|
|
378
398
|
}).index("by_key", ["key"]),
|
|
379
399
|
|
|
400
|
+
visitorProfiles: defineTable(
|
|
401
|
+
visitorProfileValidator.extend({
|
|
402
|
+
siteId: v.id("sites"),
|
|
403
|
+
searchName: v.optional(v.string()),
|
|
404
|
+
searchEmail: v.optional(v.string()),
|
|
405
|
+
}),
|
|
406
|
+
)
|
|
407
|
+
.index("by_siteId_and_visitorId", ["siteId", "visitorId"])
|
|
408
|
+
.index("by_siteId_and_searchName_and_visitorId", [
|
|
409
|
+
"siteId",
|
|
410
|
+
"searchName",
|
|
411
|
+
"visitorId",
|
|
412
|
+
])
|
|
413
|
+
.index("by_siteId_and_searchEmail_and_visitorId", [
|
|
414
|
+
"siteId",
|
|
415
|
+
"searchEmail",
|
|
416
|
+
"visitorId",
|
|
417
|
+
]),
|
|
418
|
+
|
|
419
|
+
visitorAttributes: defineTable({
|
|
420
|
+
siteId: v.id("sites"),
|
|
421
|
+
visitorId: v.string(),
|
|
422
|
+
key: v.string(),
|
|
423
|
+
value: personAttributeValueValidator,
|
|
424
|
+
})
|
|
425
|
+
.index("by_siteId_and_key_and_value_and_visitorId", [
|
|
426
|
+
"siteId",
|
|
427
|
+
"key",
|
|
428
|
+
"value",
|
|
429
|
+
"visitorId",
|
|
430
|
+
])
|
|
431
|
+
.index("by_siteId_and_visitorId", ["siteId", "visitorId"]),
|
|
432
|
+
|
|
380
433
|
visitorAliases: defineTable({
|
|
381
434
|
siteId: v.id("sites"),
|
|
382
435
|
visitorId: v.string(),
|