@iann29/rastro 0.5.0 → 0.7.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 +53 -9
- package/agent/integration.md +48 -19
- package/agent/manifest.json +13 -7
- package/agent/manifest.schema.json +18 -8
- package/dist/client/federation.d.ts +18 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +7 -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 +257 -7
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +110 -4
- 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 +92 -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 +37 -24
- package/dist/component/ingest.js.map +1 -1
- 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/reports.d.ts +13 -4
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +25 -10
- package/dist/component/reports.js.map +1 -1
- package/dist/component/sanitize.d.ts +3 -1
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +30 -1
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +42 -1
- package/dist/component/schema.js +30 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sites.d.ts +4 -0
- package/dist/component/sites.d.ts.map +1 -1
- package/dist/component/sites.js +13 -2
- package/dist/component/sites.js.map +1 -1
- package/dist/component/validators.d.ts +53 -1
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +28 -0
- 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 +2 -1
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +41 -2
- 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 +85 -20
- package/llms.txt +6 -2
- package/package.json +5 -3
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +104 -0
- package/src/component/ingest.ts +54 -30
- package/src/component/people.ts +321 -0
- package/src/component/reports.ts +34 -9
- package/src/component/sanitize.ts +46 -1
- package/src/component/schema.ts +35 -0
- package/src/component/sites.ts +14 -1
- package/src/component/validators.ts +50 -0
- package/src/component/visitors.ts +232 -55
- package/src/tracker/generated.ts +6 -6
package/src/component/reports.ts
CHANGED
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
} from "./eventStore.js";
|
|
57
57
|
import { isPlainRecord } from "./guards.js";
|
|
58
58
|
import { classifyOrigin } from "./origin.js";
|
|
59
|
-
import { externalReferrer } from "./sanitize.js";
|
|
59
|
+
import { externalReferrer, normalizeRoute } from "./sanitize.js";
|
|
60
60
|
import {
|
|
61
61
|
affiliateFieldsValidator,
|
|
62
62
|
eventFieldsValidator,
|
|
@@ -64,8 +64,9 @@ import {
|
|
|
64
64
|
goalFieldsValidator,
|
|
65
65
|
sessionFieldsValidator,
|
|
66
66
|
vitalMetricValidator,
|
|
67
|
+
visitorProfileValidator,
|
|
67
68
|
} from "./validators.js";
|
|
68
|
-
import { resolveVisitorIdentities } from "./visitors.js";
|
|
69
|
+
import { resolveVisitorIdentities, withVisitorProfiles } from "./visitors.js";
|
|
69
70
|
import {
|
|
70
71
|
mergeVitalHistograms,
|
|
71
72
|
VITAL_ALL,
|
|
@@ -76,6 +77,7 @@ import {
|
|
|
76
77
|
} from "./vitals.js";
|
|
77
78
|
|
|
78
79
|
const sessionDocumentValidator = sessionFieldsValidator.extend({
|
|
80
|
+
profile: v.optional(visitorProfileValidator),
|
|
79
81
|
_id: v.id("sessions"),
|
|
80
82
|
_creationTime: v.number(),
|
|
81
83
|
});
|
|
@@ -84,6 +86,7 @@ const eventDocumentValidator = eventFieldsValidator.extend({
|
|
|
84
86
|
_creationTime: v.number(),
|
|
85
87
|
});
|
|
86
88
|
const liveSessionDocumentValidator = v.object({
|
|
89
|
+
profile: v.optional(visitorProfileValidator),
|
|
87
90
|
_id: v.id("liveSessions"),
|
|
88
91
|
_creationTime: v.number(),
|
|
89
92
|
siteId: v.id("sites"),
|
|
@@ -628,7 +631,7 @@ export const liveVisitors = query({
|
|
|
628
631
|
.take(limit)),
|
|
629
632
|
);
|
|
630
633
|
}
|
|
631
|
-
|
|
634
|
+
const visible = rows
|
|
632
635
|
.sort((left, right) => right.lastSeenAt - left.lastSeenAt)
|
|
633
636
|
.slice(0, limit)
|
|
634
637
|
.map((row) => ({
|
|
@@ -662,6 +665,7 @@ export const liveVisitors = query({
|
|
|
662
665
|
: {}),
|
|
663
666
|
...(row.funnel ? { funnel: row.funnel } : {}),
|
|
664
667
|
}));
|
|
668
|
+
return withVisitorProfiles(ctx, visible);
|
|
665
669
|
},
|
|
666
670
|
});
|
|
667
671
|
|
|
@@ -707,6 +711,7 @@ export const siteMap = query({
|
|
|
707
711
|
window: v.union(v.object({ from: v.number(), to: v.number() }), v.null()),
|
|
708
712
|
routes: v.array(siteMapRouteValidator),
|
|
709
713
|
transitions: v.array(siteMapTransitionValidator),
|
|
714
|
+
routePatterns: v.optional(v.record(v.string(), v.array(v.string()))),
|
|
710
715
|
truncated: v.object({ routes: v.boolean(), transitions: v.boolean() }),
|
|
711
716
|
metadata: reportMetadataValidator.extend({
|
|
712
717
|
visitors: v.object({
|
|
@@ -724,6 +729,17 @@ export const siteMap = query({
|
|
|
724
729
|
const siteIds = validateSiteIds(args.siteIds);
|
|
725
730
|
validateRange(args.from, args.to, MAX_FEATURE_ROLLUP_DAYS);
|
|
726
731
|
requireAlignedRange(args.from, args.to, DAY_MS, "site map");
|
|
732
|
+
const routePatterns = Object.fromEntries(
|
|
733
|
+
await Promise.all(
|
|
734
|
+
siteIds.map(
|
|
735
|
+
async (siteId) =>
|
|
736
|
+
[
|
|
737
|
+
siteId,
|
|
738
|
+
(await ctx.db.get("sites", siteId))?.routePatterns ?? [],
|
|
739
|
+
] as const,
|
|
740
|
+
),
|
|
741
|
+
),
|
|
742
|
+
);
|
|
727
743
|
const controls = await Promise.all(
|
|
728
744
|
siteIds.map((siteId) =>
|
|
729
745
|
ctx.db
|
|
@@ -771,6 +787,7 @@ export const siteMap = query({
|
|
|
771
787
|
window: null,
|
|
772
788
|
routes: [],
|
|
773
789
|
transitions: [],
|
|
790
|
+
routePatterns,
|
|
774
791
|
truncated: { routes: false, transitions: false },
|
|
775
792
|
metadata: metadata("unavailable"),
|
|
776
793
|
};
|
|
@@ -838,7 +855,10 @@ export const siteMap = query({
|
|
|
838
855
|
>();
|
|
839
856
|
for (const row of rows) {
|
|
840
857
|
if (row.bucketStart < completeFrom) continue;
|
|
841
|
-
|
|
858
|
+
// Apply current patterns to retained rollups too, before merging sketches.
|
|
859
|
+
const patterns = routePatterns[row.siteId];
|
|
860
|
+
const route = normalizeRoute(row.route, patterns);
|
|
861
|
+
const current = totalsFor(route);
|
|
842
862
|
current.pageviews += row.pageviews;
|
|
843
863
|
current.entries += row.entries;
|
|
844
864
|
current.continued += row.continued;
|
|
@@ -850,10 +870,11 @@ export const siteMap = query({
|
|
|
850
870
|
(current.daily.get(row.bucketStart) ?? 0) + row.pageviews,
|
|
851
871
|
);
|
|
852
872
|
for (const target of row.outgoing) {
|
|
853
|
-
const
|
|
873
|
+
const to = normalizeRoute(target.route, patterns);
|
|
874
|
+
const key = `${route}\u0000${to}`;
|
|
854
875
|
const edge = edges.get(key) ?? {
|
|
855
|
-
from:
|
|
856
|
-
to
|
|
876
|
+
from: route,
|
|
877
|
+
to,
|
|
857
878
|
count: 0,
|
|
858
879
|
};
|
|
859
880
|
edge.count += target.count;
|
|
@@ -909,6 +930,7 @@ export const siteMap = query({
|
|
|
909
930
|
from: args.from,
|
|
910
931
|
to: args.to,
|
|
911
932
|
window: completeFrom > args.to ? null : { from: windowFrom, to: args.to },
|
|
933
|
+
routePatterns,
|
|
912
934
|
routes: routeRows,
|
|
913
935
|
transitions: transitionRows,
|
|
914
936
|
truncated: {
|
|
@@ -1003,7 +1025,9 @@ export const getSession = query({
|
|
|
1003
1025
|
...result
|
|
1004
1026
|
} = session;
|
|
1005
1027
|
const site = await ctx.db.get("sites", args.siteId);
|
|
1006
|
-
return
|
|
1028
|
+
return (
|
|
1029
|
+
await withVisitorProfiles(ctx, [withOrigin(result, site?.domains ?? [])])
|
|
1030
|
+
)[0];
|
|
1007
1031
|
},
|
|
1008
1032
|
});
|
|
1009
1033
|
|
|
@@ -1150,10 +1174,11 @@ export const listSessions = query({
|
|
|
1150
1174
|
...row
|
|
1151
1175
|
}) => withOrigin(row, site?.domains ?? []),
|
|
1152
1176
|
);
|
|
1153
|
-
|
|
1177
|
+
const result = keysetPaginationResult(publicRows, pagination, (row) => ({
|
|
1154
1178
|
timestamp: row.lastSeenAt,
|
|
1155
1179
|
creationTime: row._creationTime,
|
|
1156
1180
|
}));
|
|
1181
|
+
return { ...result, page: await withVisitorProfiles(ctx, result.page) };
|
|
1157
1182
|
},
|
|
1158
1183
|
});
|
|
1159
1184
|
|
|
@@ -82,6 +82,36 @@ const NUMERIC_SEGMENT = /^\d+$/;
|
|
|
82
82
|
const UUID_SEGMENT =
|
|
83
83
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
84
84
|
const HEX_SEGMENT = /^[0-9a-f]{16,}$/i;
|
|
85
|
+
const ROUTE_PARAMETER = /^:[A-Za-z][A-Za-z0-9_]*$/;
|
|
86
|
+
|
|
87
|
+
/** Ordered, full-path patterns; no regex or wildcard execution from settings. */
|
|
88
|
+
export function validateRoutePatterns(patterns: readonly string[]): string[] {
|
|
89
|
+
if (patterns.length > 32)
|
|
90
|
+
throw new Error("At most 32 route patterns are allowed");
|
|
91
|
+
return [
|
|
92
|
+
...new Set(
|
|
93
|
+
patterns.map((value) => {
|
|
94
|
+
const pattern = value.trim().replace(/\/$/, "");
|
|
95
|
+
const segments = pattern.slice(1).split("/");
|
|
96
|
+
if (
|
|
97
|
+
!pattern.startsWith("/") ||
|
|
98
|
+
pattern.length > 256 ||
|
|
99
|
+
/[\s?#*\\]/.test(pattern) ||
|
|
100
|
+
!segments.some((segment) => ROUTE_PARAMETER.test(segment)) ||
|
|
101
|
+
segments.some(
|
|
102
|
+
(segment) =>
|
|
103
|
+
!segment ||
|
|
104
|
+
segment === "." ||
|
|
105
|
+
segment === ".." ||
|
|
106
|
+
(segment.includes(":") && !ROUTE_PARAMETER.test(segment)),
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
throw new Error("Use full route patterns such as /pedido/:codigo");
|
|
110
|
+
return pattern;
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
85
115
|
|
|
86
116
|
/**
|
|
87
117
|
* Collapses the identifiers that unmistakably vary per record — numbers,
|
|
@@ -91,10 +121,25 @@ const HEX_SEGMENT = /^[0-9a-f]{16,}$/i;
|
|
|
91
121
|
* spellings serve the same page. Only the map uses this; every other report
|
|
92
122
|
* keeps the sanitized path exactly as the tracker sent it.
|
|
93
123
|
*/
|
|
94
|
-
export function normalizeRoute(
|
|
124
|
+
export function normalizeRoute(
|
|
125
|
+
path: string,
|
|
126
|
+
patterns: readonly string[] = [],
|
|
127
|
+
): string {
|
|
95
128
|
const trimmed =
|
|
96
129
|
path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
97
130
|
const segments = trimmed.split("/");
|
|
131
|
+
for (const pattern of patterns) {
|
|
132
|
+
const parts = pattern.split("/");
|
|
133
|
+
if (
|
|
134
|
+
parts.length === segments.length &&
|
|
135
|
+
parts.every(
|
|
136
|
+
(part, index) =>
|
|
137
|
+
part === segments[index] ||
|
|
138
|
+
(ROUTE_PARAMETER.test(part) && Boolean(segments[index])),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return pattern;
|
|
142
|
+
}
|
|
98
143
|
for (let index = 1; index < segments.length; index += 1) {
|
|
99
144
|
const segment = segments[index] ?? "";
|
|
100
145
|
if (
|
package/src/component/schema.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
siteFieldsValidator,
|
|
11
11
|
trackedLinkFieldsValidator,
|
|
12
12
|
visitorSketchValidator,
|
|
13
|
+
visitorProfileValidator,
|
|
14
|
+
personAttributeValueValidator,
|
|
13
15
|
vitalMetricValidator,
|
|
14
16
|
} from "./validators.js";
|
|
15
17
|
import {
|
|
@@ -395,6 +397,39 @@ export default defineSchema({
|
|
|
395
397
|
createdAt: v.number(),
|
|
396
398
|
}).index("by_key", ["key"]),
|
|
397
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
|
+
|
|
398
433
|
visitorAliases: defineTable({
|
|
399
434
|
siteId: v.id("sites"),
|
|
400
435
|
visitorId: v.string(),
|
package/src/component/sites.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
sanitizeCurrency,
|
|
13
13
|
sanitizeOpaqueId,
|
|
14
14
|
validateTimezone,
|
|
15
|
+
validateRoutePatterns,
|
|
15
16
|
} from "./sanitize.js";
|
|
16
17
|
import { siteFieldsValidator } from "./validators.js";
|
|
17
18
|
|
|
@@ -29,6 +30,7 @@ export const create = mutation({
|
|
|
29
30
|
timezone: v.optional(v.string()),
|
|
30
31
|
currency: v.optional(v.string()),
|
|
31
32
|
cookieless: v.optional(v.boolean()),
|
|
33
|
+
routePatterns: v.optional(v.array(v.string())),
|
|
32
34
|
},
|
|
33
35
|
returns: v.id("sites"),
|
|
34
36
|
handler: async (ctx, args) => {
|
|
@@ -41,6 +43,7 @@ export const create = mutation({
|
|
|
41
43
|
let timezone: string | undefined;
|
|
42
44
|
let networkId: string | undefined;
|
|
43
45
|
let currency: string;
|
|
46
|
+
let routePatterns: string[];
|
|
44
47
|
try {
|
|
45
48
|
domains = normalizeDomains(args.domains);
|
|
46
49
|
timezone = validateTimezone(args.timezone);
|
|
@@ -48,6 +51,7 @@ export const create = mutation({
|
|
|
48
51
|
? sanitizeOpaqueId(args.networkId, "networkId")
|
|
49
52
|
: undefined;
|
|
50
53
|
currency = sanitizeCurrency(args.currency ?? "USD");
|
|
54
|
+
routePatterns = validateRoutePatterns(args.routePatterns ?? []);
|
|
51
55
|
} catch (error) {
|
|
52
56
|
fail(
|
|
53
57
|
"INVALID_SITE",
|
|
@@ -69,7 +73,10 @@ export const create = mutation({
|
|
|
69
73
|
existing.networkId !== networkId ||
|
|
70
74
|
existing.timezone !== timezone ||
|
|
71
75
|
(existing.currency ?? "USD") !== currency ||
|
|
72
|
-
existing.cookieless !== cookieless
|
|
76
|
+
existing.cookieless !== cookieless ||
|
|
77
|
+
(args.routePatterns !== undefined &&
|
|
78
|
+
JSON.stringify(existing.routePatterns ?? []) !==
|
|
79
|
+
JSON.stringify(routePatterns))
|
|
73
80
|
) {
|
|
74
81
|
fail(
|
|
75
82
|
"CONFLICT",
|
|
@@ -99,6 +106,7 @@ export const create = mutation({
|
|
|
99
106
|
timezone,
|
|
100
107
|
currency,
|
|
101
108
|
cookieless,
|
|
109
|
+
routePatterns,
|
|
102
110
|
createdAt: now,
|
|
103
111
|
updatedAt: now,
|
|
104
112
|
});
|
|
@@ -116,6 +124,7 @@ export const update = mutation({
|
|
|
116
124
|
timezone: v.optional(v.union(v.string(), v.null())),
|
|
117
125
|
currency: v.optional(v.string()),
|
|
118
126
|
cookieless: v.optional(v.boolean()),
|
|
127
|
+
routePatterns: v.optional(v.array(v.string())),
|
|
119
128
|
},
|
|
120
129
|
returns: v.null(),
|
|
121
130
|
handler: async (ctx, args) => {
|
|
@@ -129,6 +138,7 @@ export const update = mutation({
|
|
|
129
138
|
timezone?: string | undefined;
|
|
130
139
|
currency?: string;
|
|
131
140
|
cookieless?: boolean;
|
|
141
|
+
routePatterns?: string[];
|
|
132
142
|
updatedAt: number;
|
|
133
143
|
} = { updatedAt: Date.now() };
|
|
134
144
|
if (args.name !== undefined) {
|
|
@@ -161,6 +171,9 @@ export const update = mutation({
|
|
|
161
171
|
if (args.currency !== undefined) {
|
|
162
172
|
patch.currency = sanitizeCurrency(args.currency);
|
|
163
173
|
}
|
|
174
|
+
if (args.routePatterns !== undefined) {
|
|
175
|
+
patch.routePatterns = validateRoutePatterns(args.routePatterns);
|
|
176
|
+
}
|
|
164
177
|
} catch (error) {
|
|
165
178
|
fail(
|
|
166
179
|
"INVALID_SITE",
|
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
import { v, type Infer } from "convex/values";
|
|
2
2
|
|
|
3
|
+
export const personAttributeValueValidator = v.union(
|
|
4
|
+
v.string(),
|
|
5
|
+
v.number(),
|
|
6
|
+
v.boolean(),
|
|
7
|
+
);
|
|
8
|
+
export const personAttributesValidator = v.record(
|
|
9
|
+
v.string(),
|
|
10
|
+
personAttributeValueValidator,
|
|
11
|
+
);
|
|
12
|
+
export type PersonAttributes = Infer<typeof personAttributesValidator>;
|
|
13
|
+
export const personFilterValidator = v.union(
|
|
14
|
+
v.object({
|
|
15
|
+
field: v.union(
|
|
16
|
+
v.literal("name"),
|
|
17
|
+
v.literal("email"),
|
|
18
|
+
v.literal("visitorId"),
|
|
19
|
+
),
|
|
20
|
+
prefix: v.string(),
|
|
21
|
+
}),
|
|
22
|
+
v.object({
|
|
23
|
+
field: v.literal("attribute"),
|
|
24
|
+
key: v.string(),
|
|
25
|
+
value: personAttributeValueValidator,
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
export type PersonFilter = Infer<typeof personFilterValidator>;
|
|
29
|
+
|
|
30
|
+
/** A profile supplied by the product's trusted backend, shared by its visits. */
|
|
31
|
+
export const visitorProfileValidator = v.object({
|
|
32
|
+
visitorId: v.string(),
|
|
33
|
+
name: v.optional(v.string()),
|
|
34
|
+
email: v.optional(v.string()),
|
|
35
|
+
attributes: v.optional(personAttributesValidator),
|
|
36
|
+
identifiedAt: v.number(),
|
|
37
|
+
updatedAt: v.number(),
|
|
38
|
+
});
|
|
39
|
+
export type VisitorProfile = Infer<typeof visitorProfileValidator>;
|
|
40
|
+
|
|
41
|
+
export const identifyVisitorFields = {
|
|
42
|
+
visitorId: v.string(),
|
|
43
|
+
previousVisitorId: v.optional(v.string()),
|
|
44
|
+
// Omission preserves the value; null explicitly removes it.
|
|
45
|
+
name: v.optional(v.union(v.string(), v.null())),
|
|
46
|
+
email: v.optional(v.union(v.string(), v.null())),
|
|
47
|
+
attributes: v.optional(
|
|
48
|
+
v.record(v.string(), v.union(personAttributeValueValidator, v.null())),
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
|
|
3
52
|
export const eventTypeValidator = v.union(
|
|
4
53
|
v.literal("pageview"),
|
|
5
54
|
v.literal("click"),
|
|
@@ -112,6 +161,7 @@ export const siteFieldsValidator = v.object({
|
|
|
112
161
|
timezone: v.optional(v.string()),
|
|
113
162
|
currency: v.optional(v.string()),
|
|
114
163
|
cookieless: v.boolean(),
|
|
164
|
+
routePatterns: v.optional(v.array(v.string())),
|
|
115
165
|
createdAt: v.number(),
|
|
116
166
|
updatedAt: v.number(),
|
|
117
167
|
});
|