@iann29/rastro 0.6.0 → 0.8.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 +90 -0
- package/agent/integration.md +19 -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 +365 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +98 -6
- 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 +163 -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/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +7 -5
- package/dist/component/ingest.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 +3 -0
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +16 -5
- 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 +122 -1
- package/dist/component/schema.js +39 -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 +696 -1
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +130 -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 +57 -0
- package/llms.txt +2 -0
- package/package.json +1 -1
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +186 -0
- package/src/component/constants.ts +8 -0
- package/src/component/ingest.ts +8 -3
- package/src/component/leads.ts +823 -0
- package/src/component/reports.ts +23 -5
- package/src/component/sanitize.ts +46 -1
- package/src/component/schema.ts +45 -0
- package/src/component/sites.ts +14 -1
- package/src/component/validators.ts +165 -0
- package/src/tracker/generated.ts +1 -1
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,
|
|
@@ -711,6 +711,7 @@ export const siteMap = query({
|
|
|
711
711
|
window: v.union(v.object({ from: v.number(), to: v.number() }), v.null()),
|
|
712
712
|
routes: v.array(siteMapRouteValidator),
|
|
713
713
|
transitions: v.array(siteMapTransitionValidator),
|
|
714
|
+
routePatterns: v.optional(v.record(v.string(), v.array(v.string()))),
|
|
714
715
|
truncated: v.object({ routes: v.boolean(), transitions: v.boolean() }),
|
|
715
716
|
metadata: reportMetadataValidator.extend({
|
|
716
717
|
visitors: v.object({
|
|
@@ -728,6 +729,17 @@ export const siteMap = query({
|
|
|
728
729
|
const siteIds = validateSiteIds(args.siteIds);
|
|
729
730
|
validateRange(args.from, args.to, MAX_FEATURE_ROLLUP_DAYS);
|
|
730
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
|
+
);
|
|
731
743
|
const controls = await Promise.all(
|
|
732
744
|
siteIds.map((siteId) =>
|
|
733
745
|
ctx.db
|
|
@@ -775,6 +787,7 @@ export const siteMap = query({
|
|
|
775
787
|
window: null,
|
|
776
788
|
routes: [],
|
|
777
789
|
transitions: [],
|
|
790
|
+
routePatterns,
|
|
778
791
|
truncated: { routes: false, transitions: false },
|
|
779
792
|
metadata: metadata("unavailable"),
|
|
780
793
|
};
|
|
@@ -842,7 +855,10 @@ export const siteMap = query({
|
|
|
842
855
|
>();
|
|
843
856
|
for (const row of rows) {
|
|
844
857
|
if (row.bucketStart < completeFrom) continue;
|
|
845
|
-
|
|
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);
|
|
846
862
|
current.pageviews += row.pageviews;
|
|
847
863
|
current.entries += row.entries;
|
|
848
864
|
current.continued += row.continued;
|
|
@@ -854,10 +870,11 @@ export const siteMap = query({
|
|
|
854
870
|
(current.daily.get(row.bucketStart) ?? 0) + row.pageviews,
|
|
855
871
|
);
|
|
856
872
|
for (const target of row.outgoing) {
|
|
857
|
-
const
|
|
873
|
+
const to = normalizeRoute(target.route, patterns);
|
|
874
|
+
const key = `${route}\u0000${to}`;
|
|
858
875
|
const edge = edges.get(key) ?? {
|
|
859
|
-
from:
|
|
860
|
-
to
|
|
876
|
+
from: route,
|
|
877
|
+
to,
|
|
861
878
|
count: 0,
|
|
862
879
|
};
|
|
863
880
|
edge.count += target.count;
|
|
@@ -913,6 +930,7 @@ export const siteMap = query({
|
|
|
913
930
|
from: args.from,
|
|
914
931
|
to: args.to,
|
|
915
932
|
window: completeFrom > args.to ? null : { from: windowFrom, to: args.to },
|
|
933
|
+
routePatterns,
|
|
916
934
|
routes: routeRows,
|
|
917
935
|
transitions: transitionRows,
|
|
918
936
|
truncated: {
|
|
@@ -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
|
@@ -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(),
|
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",
|
|
@@ -161,6 +161,7 @@ export const siteFieldsValidator = v.object({
|
|
|
161
161
|
timezone: v.optional(v.string()),
|
|
162
162
|
currency: v.optional(v.string()),
|
|
163
163
|
cookieless: v.boolean(),
|
|
164
|
+
routePatterns: v.optional(v.array(v.string())),
|
|
164
165
|
createdAt: v.number(),
|
|
165
166
|
updatedAt: v.number(),
|
|
166
167
|
});
|
|
@@ -372,6 +373,79 @@ export const trackedLinkFieldsValidator = v.object({
|
|
|
372
373
|
disabledAt: v.optional(v.number()),
|
|
373
374
|
});
|
|
374
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
|
+
|
|
375
449
|
export const ingestResultValidator = v.object({
|
|
376
450
|
accepted: v.number(),
|
|
377
451
|
duplicates: v.number(),
|
|
@@ -398,3 +472,94 @@ export type FunnelStep = Infer<typeof funnelStepValidator>;
|
|
|
398
472
|
export type EventType = Infer<typeof eventTypeValidator>;
|
|
399
473
|
export type Channel = Infer<typeof channelValidator>;
|
|
400
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.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;
|