@iann29/rastro 0.9.0 → 0.10.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 +9 -1
- package/agent/integration.md +23 -1
- package/agent/manifest.json +4 -4
- package/dist/client/federation.d.ts +114 -9
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +54 -2
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +642 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +391 -9
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +14 -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 +278 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +4 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.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 +10 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +4 -0
- package/dist/component/convex.config.js +9 -0
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/errors.d.ts +1 -1
- package/dist/component/errors.d.ts.map +1 -1
- package/dist/component/errors.js.map +1 -1
- package/dist/component/leads.d.ts +69 -3
- package/dist/component/leads.d.ts.map +1 -1
- package/dist/component/leads.js +419 -16
- package/dist/component/leads.js.map +1 -1
- package/dist/component/meta.d.ts +358 -0
- package/dist/component/meta.d.ts.map +1 -0
- package/dist/component/meta.js +1295 -0
- package/dist/component/meta.js.map +1 -0
- package/dist/component/metaCapi.d.ts +162 -0
- package/dist/component/metaCapi.d.ts.map +1 -0
- package/dist/component/metaCapi.js +754 -0
- package/dist/component/metaCapi.js.map +1 -0
- package/dist/component/metaFake.d.ts +5 -0
- package/dist/component/metaFake.d.ts.map +1 -0
- package/dist/component/metaFake.js +225 -0
- package/dist/component/metaFake.js.map +1 -0
- package/dist/component/metaGraph.d.ts +165 -0
- package/dist/component/metaGraph.d.ts.map +1 -0
- package/dist/component/metaGraph.js +507 -0
- package/dist/component/metaGraph.js.map +1 -0
- package/dist/component/metaSync.d.ts +186 -0
- package/dist/component/metaSync.d.ts.map +1 -0
- package/dist/component/metaSync.js +961 -0
- package/dist/component/metaSync.js.map +1 -0
- package/dist/component/pipeline.d.ts +99 -0
- package/dist/component/pipeline.d.ts.map +1 -0
- package/dist/component/pipeline.js +337 -0
- package/dist/component/pipeline.js.map +1 -0
- package/dist/component/schema.d.ts +404 -2
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +168 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/component/secrets.d.ts +15 -0
- package/dist/component/secrets.d.ts.map +1 -0
- package/dist/component/secrets.js +67 -0
- package/dist/component/secrets.js.map +1 -0
- package/dist/component/validators.d.ts +1052 -15
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +307 -3
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +1 -1
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +1 -1
- package/dist/tracker/generated.js.map +1 -1
- package/dist/ui/campaigns/CampaignsView.d.ts +48 -0
- package/dist/ui/campaigns/CampaignsView.d.ts.map +1 -0
- package/dist/ui/campaigns/CampaignsView.js +47 -0
- package/dist/ui/campaigns/CampaignsView.js.map +1 -0
- package/dist/ui/campaigns/Conversions.d.ts +16 -0
- package/dist/ui/campaigns/Conversions.d.ts.map +1 -0
- package/dist/ui/campaigns/Conversions.js +40 -0
- package/dist/ui/campaigns/Conversions.js.map +1 -0
- package/dist/ui/campaigns/Drawer.d.ts +24 -0
- package/dist/ui/campaigns/Drawer.d.ts.map +1 -0
- package/dist/ui/campaigns/Drawer.js +120 -0
- package/dist/ui/campaigns/Drawer.js.map +1 -0
- package/dist/ui/campaigns/Icons.d.ts +35 -0
- package/dist/ui/campaigns/Icons.d.ts.map +1 -0
- package/dist/ui/campaigns/Icons.js +41 -0
- package/dist/ui/campaigns/Icons.js.map +1 -0
- package/dist/ui/campaigns/Summary.d.ts +21 -0
- package/dist/ui/campaigns/Summary.d.ts.map +1 -0
- package/dist/ui/campaigns/Summary.js +56 -0
- package/dist/ui/campaigns/Summary.js.map +1 -0
- package/dist/ui/campaigns/Thumbnail.d.ts +11 -0
- package/dist/ui/campaigns/Thumbnail.d.ts.map +1 -0
- package/dist/ui/campaigns/Thumbnail.js +23 -0
- package/dist/ui/campaigns/Thumbnail.js.map +1 -0
- package/dist/ui/campaigns/Tree.d.ts +12 -0
- package/dist/ui/campaigns/Tree.d.ts.map +1 -0
- package/dist/ui/campaigns/Tree.js +105 -0
- package/dist/ui/campaigns/Tree.js.map +1 -0
- package/dist/ui/campaigns/glyphs.d.ts +20 -0
- package/dist/ui/campaigns/glyphs.d.ts.map +1 -0
- package/dist/ui/campaigns/glyphs.js +19 -0
- package/dist/ui/campaigns/glyphs.js.map +1 -0
- package/dist/ui/campaigns/index.d.ts +4 -0
- package/dist/ui/campaigns/index.d.ts.map +1 -0
- package/dist/ui/campaigns/index.js +3 -0
- package/dist/ui/campaigns/index.js.map +1 -0
- package/dist/ui/campaigns/labels.d.ts +53 -0
- package/dist/ui/campaigns/labels.d.ts.map +1 -0
- package/dist/ui/campaigns/labels.js +169 -0
- package/dist/ui/campaigns/labels.js.map +1 -0
- package/dist/ui/campaigns/model.d.ts +99 -0
- package/dist/ui/campaigns/model.d.ts.map +1 -0
- package/dist/ui/campaigns/model.js +230 -0
- package/dist/ui/campaigns/model.js.map +1 -0
- package/dist/ui/campaigns/modes.d.ts +18 -0
- package/dist/ui/campaigns/modes.d.ts.map +1 -0
- package/dist/ui/campaigns/modes.js +7 -0
- package/dist/ui/campaigns/modes.js.map +1 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +20 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/integrations/AccountPicker.d.ts +26 -0
- package/dist/ui/integrations/AccountPicker.d.ts.map +1 -0
- package/dist/ui/integrations/AccountPicker.js +101 -0
- package/dist/ui/integrations/AccountPicker.js.map +1 -0
- package/dist/ui/integrations/ClientCards.d.ts +17 -0
- package/dist/ui/integrations/ClientCards.d.ts.map +1 -0
- package/dist/ui/integrations/ClientCards.js +42 -0
- package/dist/ui/integrations/ClientCards.js.map +1 -0
- package/dist/ui/integrations/ClientsTable.d.ts +36 -0
- package/dist/ui/integrations/ClientsTable.d.ts.map +1 -0
- package/dist/ui/integrations/ClientsTable.js +143 -0
- package/dist/ui/integrations/ClientsTable.js.map +1 -0
- package/dist/ui/integrations/Connect.d.ts +35 -0
- package/dist/ui/integrations/Connect.d.ts.map +1 -0
- package/dist/ui/integrations/Connect.js +62 -0
- package/dist/ui/integrations/Connect.js.map +1 -0
- package/dist/ui/integrations/Connection.d.ts +37 -0
- package/dist/ui/integrations/Connection.d.ts.map +1 -0
- package/dist/ui/integrations/Connection.js +108 -0
- package/dist/ui/integrations/Connection.js.map +1 -0
- package/dist/ui/integrations/Dialogs.d.ts +21 -0
- package/dist/ui/integrations/Dialogs.d.ts.map +1 -0
- package/dist/ui/integrations/Dialogs.js +34 -0
- package/dist/ui/integrations/Dialogs.js.map +1 -0
- package/dist/ui/integrations/EventsFeed.d.ts +15 -0
- package/dist/ui/integrations/EventsFeed.d.ts.map +1 -0
- package/dist/ui/integrations/EventsFeed.js +61 -0
- package/dist/ui/integrations/EventsFeed.js.map +1 -0
- package/dist/ui/integrations/EventsSheet.d.ts +34 -0
- package/dist/ui/integrations/EventsSheet.d.ts.map +1 -0
- package/dist/ui/integrations/EventsSheet.js +136 -0
- package/dist/ui/integrations/EventsSheet.js.map +1 -0
- package/dist/ui/integrations/Icons.d.ts +38 -0
- package/dist/ui/integrations/Icons.d.ts.map +1 -0
- package/dist/ui/integrations/Icons.js +40 -0
- package/dist/ui/integrations/Icons.js.map +1 -0
- package/dist/ui/integrations/IntegrationsView.d.ts +15 -0
- package/dist/ui/integrations/IntegrationsView.d.ts.map +1 -0
- package/dist/ui/integrations/IntegrationsView.js +141 -0
- package/dist/ui/integrations/IntegrationsView.js.map +1 -0
- package/dist/ui/integrations/Overlay.d.ts +18 -0
- package/dist/ui/integrations/Overlay.d.ts.map +1 -0
- package/dist/ui/integrations/Overlay.js +36 -0
- package/dist/ui/integrations/Overlay.js.map +1 -0
- package/dist/ui/integrations/Partner.d.ts +14 -0
- package/dist/ui/integrations/Partner.d.ts.map +1 -0
- package/dist/ui/integrations/Partner.js +38 -0
- package/dist/ui/integrations/Partner.js.map +1 -0
- package/dist/ui/integrations/RowMenu.d.ts +15 -0
- package/dist/ui/integrations/RowMenu.d.ts.map +1 -0
- package/dist/ui/integrations/RowMenu.js +47 -0
- package/dist/ui/integrations/RowMenu.js.map +1 -0
- package/dist/ui/integrations/UnlinkDialog.d.ts +18 -0
- package/dist/ui/integrations/UnlinkDialog.d.ts.map +1 -0
- package/dist/ui/integrations/UnlinkDialog.js +29 -0
- package/dist/ui/integrations/UnlinkDialog.js.map +1 -0
- package/dist/ui/integrations/audience.d.ts +12 -0
- package/dist/ui/integrations/audience.d.ts.map +1 -0
- package/dist/ui/integrations/audience.js +12 -0
- package/dist/ui/integrations/audience.js.map +1 -0
- package/dist/ui/integrations/format.d.ts +28 -0
- package/dist/ui/integrations/format.d.ts.map +1 -0
- package/dist/ui/integrations/format.js +101 -0
- package/dist/ui/integrations/format.js.map +1 -0
- package/dist/ui/integrations/index.d.ts +5 -0
- package/dist/ui/integrations/index.d.ts.map +1 -0
- package/dist/ui/integrations/index.js +3 -0
- package/dist/ui/integrations/index.js.map +1 -0
- package/dist/ui/integrations/modal.d.ts +10 -0
- package/dist/ui/integrations/modal.d.ts.map +1 -0
- package/dist/ui/integrations/modal.js +77 -0
- package/dist/ui/integrations/modal.js.map +1 -0
- package/dist/ui/integrations/model.d.ts +99 -0
- package/dist/ui/integrations/model.d.ts.map +1 -0
- package/dist/ui/integrations/model.js +164 -0
- package/dist/ui/integrations/model.js.map +1 -0
- package/dist/ui/integrations/parts.d.ts +36 -0
- package/dist/ui/integrations/parts.d.ts.map +1 -0
- package/dist/ui/integrations/parts.js +85 -0
- package/dist/ui/integrations/parts.js.map +1 -0
- package/dist/ui/integrations/types.d.ts +146 -0
- package/dist/ui/integrations/types.d.ts.map +1 -0
- package/dist/ui/integrations/types.js +2 -0
- package/dist/ui/integrations/types.js.map +1 -0
- package/dist/ui/metrics.d.ts +106 -0
- package/dist/ui/metrics.d.ts.map +1 -0
- package/dist/ui/metrics.js +166 -0
- package/dist/ui/metrics.js.map +1 -0
- package/dist/ui/portfolio/PortfolioView.d.ts +26 -0
- package/dist/ui/portfolio/PortfolioView.d.ts.map +1 -0
- package/dist/ui/portfolio/PortfolioView.js +216 -0
- package/dist/ui/portfolio/PortfolioView.js.map +1 -0
- package/dist/ui/portfolio/SpendChart.d.ts +10 -0
- package/dist/ui/portfolio/SpendChart.d.ts.map +1 -0
- package/dist/ui/portfolio/SpendChart.js +196 -0
- package/dist/ui/portfolio/SpendChart.js.map +1 -0
- package/dist/ui/portfolio/alerts.d.ts +74 -0
- package/dist/ui/portfolio/alerts.d.ts.map +1 -0
- package/dist/ui/portfolio/alerts.js +291 -0
- package/dist/ui/portfolio/alerts.js.map +1 -0
- package/dist/ui/portfolio/figures.d.ts +79 -0
- package/dist/ui/portfolio/figures.d.ts.map +1 -0
- package/dist/ui/portfolio/figures.js +103 -0
- package/dist/ui/portfolio/figures.js.map +1 -0
- package/dist/ui/portfolio/format.d.ts +8 -0
- package/dist/ui/portfolio/format.d.ts.map +1 -0
- package/dist/ui/portfolio/format.js +28 -0
- package/dist/ui/portfolio/format.js.map +1 -0
- package/dist/ui/sample.d.ts +536 -0
- package/dist/ui/sample.d.ts.map +1 -0
- package/dist/ui/sample.js +574 -0
- package/dist/ui/sample.js.map +1 -0
- package/dist/ui/seed.d.ts +188 -0
- package/dist/ui/seed.d.ts.map +1 -0
- package/dist/ui/seed.js +563 -0
- package/dist/ui/seed.js.map +1 -0
- package/dist/ui/ui.css +3424 -0
- package/docs/federation.md +46 -1
- package/docs/meta-ads.md +795 -0
- package/docs/upgrading.md +116 -0
- package/llms.txt +3 -0
- package/package.json +15 -3
- package/src/component/_generated/api.ts +14 -0
- package/src/component/_generated/component.ts +324 -1
- package/src/component/_generated/server.ts +4 -0
- package/src/component/constants.ts +10 -0
- package/src/component/convex.config.ts +9 -0
- package/src/component/errors.ts +5 -1
- package/src/component/leads.ts +517 -17
- package/src/component/meta.ts +1525 -0
- package/src/component/metaCapi.ts +921 -0
- package/src/component/metaFake.ts +277 -0
- package/src/component/metaGraph.ts +722 -0
- package/src/component/metaSync.ts +1164 -0
- package/src/component/pipeline.ts +410 -0
- package/src/component/schema.ts +197 -1
- package/src/component/secrets.ts +98 -0
- package/src/component/validators.ts +376 -3
- package/src/tracker/generated.ts +1 -1
- package/src/ui/seed.ts +755 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { v, type Infer } from "convex/values";
|
|
2
|
+
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
3
|
+
import {
|
|
4
|
+
mutation,
|
|
5
|
+
query,
|
|
6
|
+
type MutationCtx,
|
|
7
|
+
type QueryCtx,
|
|
8
|
+
} from "./_generated/server.js";
|
|
9
|
+
import { fail } from "./errors.js";
|
|
10
|
+
import { ownerKeyOf } from "./meta.js";
|
|
11
|
+
import { enqueueColumnEvent, type MetaUserData } from "./metaCapi.js";
|
|
12
|
+
import { cleanString } from "./sanitize.js";
|
|
13
|
+
import {
|
|
14
|
+
metaColumnEventValidator,
|
|
15
|
+
metaEventMapValidator,
|
|
16
|
+
pipelineColumnValidator,
|
|
17
|
+
} from "./validators.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The host's board for a site and the Meta event each column sends. The host
|
|
21
|
+
* declares its columns (`setPipeline`), moves leads between them (`column` on
|
|
22
|
+
* `leads.record` / `leads.setStage`), and an owner maps columns to events
|
|
23
|
+
* (`setEventMap`). Rastro counts the entries per column per day for the map's
|
|
24
|
+
* monthly figures. `Purchase` never comes from a column: it is sent when the
|
|
25
|
+
* host records the sale (`won` with a value). See docs/meta-ads.md.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type MetaColumnEvent = Infer<typeof metaColumnEventValidator>;
|
|
29
|
+
|
|
30
|
+
export const MAX_PIPELINE_COLUMNS = 40;
|
|
31
|
+
const COLUMN_KEY = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
|
|
32
|
+
const COLOR = /^[#A-Za-z0-9(),.% -]{1,32}$/;
|
|
33
|
+
const MAX_COLUMN_NAME = 80;
|
|
34
|
+
const MAX_GROUP_NAME = 60;
|
|
35
|
+
const DAY_MS = 86_400_000;
|
|
36
|
+
const MONTH_DAYS = 30;
|
|
37
|
+
/** The column moves `leads.remove` clears per lead in one transaction. */
|
|
38
|
+
const MAX_COLUMN_MOVES_PER_LEAD = 1_000;
|
|
39
|
+
|
|
40
|
+
const dayOf = (at: number) => Math.floor(at / DAY_MS) * DAY_MS;
|
|
41
|
+
|
|
42
|
+
function columnKeyOf(value: string): string {
|
|
43
|
+
const key = value.trim();
|
|
44
|
+
if (!COLUMN_KEY.test(key)) {
|
|
45
|
+
fail(
|
|
46
|
+
"INVALID_ARGUMENT",
|
|
47
|
+
"a column key is 1-64 letters, digits, '_', '.', ':' or '-'",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return key;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function pipelineOf(ctx: MutationCtx, siteId: Id<"sites">) {
|
|
54
|
+
return await ctx.db
|
|
55
|
+
.query("sitePipelines")
|
|
56
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
57
|
+
.unique();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function requireSite(ctx: QueryCtx, siteId: Id<"sites">): Promise<void> {
|
|
61
|
+
if (!(await ctx.db.get("sites", siteId))) fail("NOT_FOUND", "site not found");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Declares the host's board columns, in order. Declaring again replaces names,
|
|
66
|
+
* colors, groups and order; a column that is gone leaves the event map, and
|
|
67
|
+
* one that stays keeps its event.
|
|
68
|
+
*/
|
|
69
|
+
export const setPipeline = mutation({
|
|
70
|
+
args: { siteId: v.id("sites"), columns: v.array(pipelineColumnValidator) },
|
|
71
|
+
returns: v.null(),
|
|
72
|
+
handler: async (ctx, args) => {
|
|
73
|
+
await requireSite(ctx, args.siteId);
|
|
74
|
+
if (args.columns.length > MAX_PIPELINE_COLUMNS) {
|
|
75
|
+
fail(
|
|
76
|
+
"LIMIT_EXCEEDED",
|
|
77
|
+
`a pipeline has at most ${MAX_PIPELINE_COLUMNS} columns`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const existing = await pipelineOf(ctx, args.siteId);
|
|
81
|
+
const events = new Map(
|
|
82
|
+
(existing?.columns ?? []).map((column) => [column.key, column.event]),
|
|
83
|
+
);
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
const columns = args.columns.map((column) => {
|
|
86
|
+
const key = columnKeyOf(column.key);
|
|
87
|
+
if (seen.has(key)) fail("INVALID_ARGUMENT", `column ${key} is repeated`);
|
|
88
|
+
seen.add(key);
|
|
89
|
+
const name = cleanString(column.name, MAX_COLUMN_NAME);
|
|
90
|
+
if (!name) fail("INVALID_ARGUMENT", `column ${key} needs a name`);
|
|
91
|
+
const color = column.color?.trim();
|
|
92
|
+
if (color !== undefined && color !== "" && !COLOR.test(color)) {
|
|
93
|
+
fail("INVALID_ARGUMENT", `column ${key} has an invalid color`);
|
|
94
|
+
}
|
|
95
|
+
const group = column.group
|
|
96
|
+
? cleanString(column.group, MAX_GROUP_NAME)
|
|
97
|
+
: "";
|
|
98
|
+
const event = events.get(key);
|
|
99
|
+
return {
|
|
100
|
+
key,
|
|
101
|
+
name,
|
|
102
|
+
...(color ? { color } : {}),
|
|
103
|
+
...(group ? { group } : {}),
|
|
104
|
+
...(event ? { event } : {}),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (existing) {
|
|
109
|
+
await ctx.db.patch("sitePipelines", existing._id, {
|
|
110
|
+
columns,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
await ctx.db.insert("sitePipelines", {
|
|
115
|
+
siteId: args.siteId,
|
|
116
|
+
columns,
|
|
117
|
+
sendLeadEvent: await pixelSendsLead(ctx, args.siteId),
|
|
118
|
+
updatedAt: now,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
async function pixelSendsLead(ctx: MutationCtx, siteId: Id<"sites">) {
|
|
126
|
+
const pixel = await ctx.db
|
|
127
|
+
.query("sitePixels")
|
|
128
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", siteId))
|
|
129
|
+
.unique();
|
|
130
|
+
return pixel?.sendLeadEvent ?? false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Which event each declared column sends, replacing the whole map: a column
|
|
135
|
+
* left out, or mapped to null, sends nothing. `sendLeadEvent` turns `Lead` on
|
|
136
|
+
* for every new lead (kept on the pixel too, where the sender reads it).
|
|
137
|
+
*/
|
|
138
|
+
export const setEventMap = mutation({
|
|
139
|
+
args: {
|
|
140
|
+
siteId: v.id("sites"),
|
|
141
|
+
columns: v.record(v.string(), v.union(metaColumnEventValidator, v.null())),
|
|
142
|
+
sendLeadEvent: v.boolean(),
|
|
143
|
+
/**
|
|
144
|
+
* The owner saving the map. The map feeds the site's pixel, so whoever
|
|
145
|
+
* set the pixel decides it: with `ownerKey`, a pixel another owner (or
|
|
146
|
+
* the host) set is a `CONFLICT`. A site without a pixel takes anyone's.
|
|
147
|
+
*/
|
|
148
|
+
ownerKey: v.optional(v.string()),
|
|
149
|
+
},
|
|
150
|
+
returns: v.null(),
|
|
151
|
+
handler: async (ctx, args) => {
|
|
152
|
+
await requireSite(ctx, args.siteId);
|
|
153
|
+
const pixel = await ctx.db
|
|
154
|
+
.query("sitePixels")
|
|
155
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
|
|
156
|
+
.unique();
|
|
157
|
+
if (
|
|
158
|
+
args.ownerKey !== undefined &&
|
|
159
|
+
pixel &&
|
|
160
|
+
pixel.setBy !== ownerKeyOf(args.ownerKey)
|
|
161
|
+
) {
|
|
162
|
+
fail(
|
|
163
|
+
"CONFLICT",
|
|
164
|
+
"the site's pixel was set by another owner on this host; change its events there",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const existing = await pipelineOf(ctx, args.siteId);
|
|
168
|
+
const declared = new Set((existing?.columns ?? []).map((row) => row.key));
|
|
169
|
+
for (const key of Object.keys(args.columns)) {
|
|
170
|
+
if (!declared.has(key)) {
|
|
171
|
+
fail(
|
|
172
|
+
"INVALID_ARGUMENT",
|
|
173
|
+
`column ${key.slice(0, 64)} is not on the site's pipeline`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const columns = (existing?.columns ?? []).map(
|
|
178
|
+
({ event: _event, ...column }) => {
|
|
179
|
+
const event = args.columns[column.key];
|
|
180
|
+
return event ? { ...column, event } : column;
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
if (existing) {
|
|
185
|
+
await ctx.db.patch("sitePipelines", existing._id, {
|
|
186
|
+
columns,
|
|
187
|
+
sendLeadEvent: args.sendLeadEvent,
|
|
188
|
+
updatedAt: now,
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
await ctx.db.insert("sitePipelines", {
|
|
192
|
+
siteId: args.siteId,
|
|
193
|
+
columns,
|
|
194
|
+
sendLeadEvent: args.sendLeadEvent,
|
|
195
|
+
updatedAt: now,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// `updatedAt` stays: it says when the pixel itself was set, which is what
|
|
199
|
+
// removing its owner compares against.
|
|
200
|
+
if (pixel && pixel.sendLeadEvent !== args.sendLeadEvent) {
|
|
201
|
+
await ctx.db.patch("sitePixels", pixel._id, {
|
|
202
|
+
sendLeadEvent: args.sendLeadEvent,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The site's columns with their events and how many leads entered each in the
|
|
211
|
+
* 30 UTC days up to `now` (the caller's clock, so the query stays cacheable;
|
|
212
|
+
* without it, or before the host ever sent a column, the counts are null).
|
|
213
|
+
*/
|
|
214
|
+
export const eventMap = query({
|
|
215
|
+
args: { siteId: v.id("sites"), now: v.optional(v.number()) },
|
|
216
|
+
returns: metaEventMapValidator,
|
|
217
|
+
handler: async (ctx, args) => {
|
|
218
|
+
await requireSite(ctx, args.siteId);
|
|
219
|
+
if (
|
|
220
|
+
args.now !== undefined &&
|
|
221
|
+
(!Number.isSafeInteger(args.now) || args.now < 0)
|
|
222
|
+
) {
|
|
223
|
+
fail("INVALID_ARGUMENT", "now must be a Unix time in milliseconds");
|
|
224
|
+
}
|
|
225
|
+
const pipeline = await ctx.db
|
|
226
|
+
.query("sitePipelines")
|
|
227
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
|
|
228
|
+
.unique();
|
|
229
|
+
const pixel = await ctx.db
|
|
230
|
+
.query("sitePixels")
|
|
231
|
+
.withIndex("by_siteId", (range) => range.eq("siteId", args.siteId))
|
|
232
|
+
.unique();
|
|
233
|
+
const counted =
|
|
234
|
+
args.now !== undefined && pipeline?.columnsSince !== undefined;
|
|
235
|
+
const to = dayOf(args.now ?? 0);
|
|
236
|
+
const from = to - (MONTH_DAYS - 1) * DAY_MS;
|
|
237
|
+
const columns = [];
|
|
238
|
+
for (const column of pipeline?.columns ?? []) {
|
|
239
|
+
let monthCount: number | null = null;
|
|
240
|
+
if (counted) {
|
|
241
|
+
// At most 30 rows: one per day.
|
|
242
|
+
const rows = await ctx.db
|
|
243
|
+
.query("leadColumnDaily")
|
|
244
|
+
.withIndex("by_siteId_and_column_and_dayStart", (range) =>
|
|
245
|
+
range
|
|
246
|
+
.eq("siteId", args.siteId)
|
|
247
|
+
.eq("column", column.key)
|
|
248
|
+
.gte("dayStart", from)
|
|
249
|
+
.lte("dayStart", to),
|
|
250
|
+
)
|
|
251
|
+
.take(MONTH_DAYS);
|
|
252
|
+
monthCount = rows.reduce((sum, row) => sum + row.entries, 0);
|
|
253
|
+
}
|
|
254
|
+
columns.push({
|
|
255
|
+
key: column.key,
|
|
256
|
+
name: column.name,
|
|
257
|
+
color: column.color ?? null,
|
|
258
|
+
group: column.group ?? null,
|
|
259
|
+
event: column.event ?? null,
|
|
260
|
+
monthCount,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
columns,
|
|
265
|
+
sendLeadEvent: pipeline?.sendLeadEvent ?? pixel?.sendLeadEvent ?? false,
|
|
266
|
+
purchase: { enabled: pixel?.enabled ?? false },
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
async function bumpDay(
|
|
272
|
+
ctx: MutationCtx,
|
|
273
|
+
siteId: Id<"sites">,
|
|
274
|
+
column: string,
|
|
275
|
+
dayStart: number,
|
|
276
|
+
delta: 1 | -1,
|
|
277
|
+
) {
|
|
278
|
+
const row = await ctx.db
|
|
279
|
+
.query("leadColumnDaily")
|
|
280
|
+
.withIndex("by_siteId_and_column_and_dayStart", (range) =>
|
|
281
|
+
range.eq("siteId", siteId).eq("column", column).eq("dayStart", dayStart),
|
|
282
|
+
)
|
|
283
|
+
.unique();
|
|
284
|
+
if (!row) {
|
|
285
|
+
if (delta > 0) {
|
|
286
|
+
await ctx.db.insert("leadColumnDaily", {
|
|
287
|
+
siteId,
|
|
288
|
+
column,
|
|
289
|
+
dayStart,
|
|
290
|
+
entries: 1,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const entries = row.entries + delta;
|
|
296
|
+
if (entries <= 0) await ctx.db.delete("leadColumnDaily", row._id);
|
|
297
|
+
else await ctx.db.patch("leadColumnDaily", row._id, { entries });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Moves a lead into the host's column `key`, from `leads.record` or
|
|
302
|
+
* `leads.setStage`. A key the site's pipeline does not declare is ignored,
|
|
303
|
+
* never an error, and so is a move into the column the lead is already in or
|
|
304
|
+
* one older than the lead's last move (`columnAt`): a late retry never moves
|
|
305
|
+
* a lead back. Only an accepted move counts and reaches the event map's seam.
|
|
306
|
+
* Returns the fields to patch on the lead, or null when nothing moved.
|
|
307
|
+
*
|
|
308
|
+
* The monthly counts are distinct leads: each lead keeps one mark per column
|
|
309
|
+
* on the day it last entered it, and the day's count follows the mark, so a
|
|
310
|
+
* lead that leaves and comes back is counted once, on its latest entry.
|
|
311
|
+
*/
|
|
312
|
+
export async function enterColumn(
|
|
313
|
+
ctx: MutationCtx,
|
|
314
|
+
lead: Pick<Doc<"leads">, "_id" | "siteId" | "column" | "columnAt">,
|
|
315
|
+
key: string | undefined,
|
|
316
|
+
at: number,
|
|
317
|
+
meta?: MetaUserData,
|
|
318
|
+
): Promise<{ column: string; columnAt: number } | null> {
|
|
319
|
+
const column = key?.trim();
|
|
320
|
+
if (!column || column === lead.column) return null;
|
|
321
|
+
if (lead.columnAt !== undefined && at < lead.columnAt) return null;
|
|
322
|
+
const pipeline = await pipelineOf(ctx, lead.siteId);
|
|
323
|
+
const declared = pipeline?.columns.find((row) => row.key === column);
|
|
324
|
+
if (!pipeline || !declared) return null;
|
|
325
|
+
const dayStart = dayOf(at);
|
|
326
|
+
const mark = await ctx.db
|
|
327
|
+
.query("leadColumnMarks")
|
|
328
|
+
.withIndex("by_leadId_and_column", (range) =>
|
|
329
|
+
range.eq("leadId", lead._id).eq("column", column),
|
|
330
|
+
)
|
|
331
|
+
.unique();
|
|
332
|
+
if (!mark) {
|
|
333
|
+
await ctx.db.insert("leadColumnMarks", {
|
|
334
|
+
siteId: lead.siteId,
|
|
335
|
+
leadId: lead._id,
|
|
336
|
+
column,
|
|
337
|
+
dayStart,
|
|
338
|
+
});
|
|
339
|
+
await bumpDay(ctx, lead.siteId, column, dayStart, 1);
|
|
340
|
+
} else if (mark.dayStart < dayStart) {
|
|
341
|
+
await bumpDay(ctx, lead.siteId, column, mark.dayStart, -1);
|
|
342
|
+
await bumpDay(ctx, lead.siteId, column, dayStart, 1);
|
|
343
|
+
await ctx.db.patch("leadColumnMarks", mark._id, { dayStart });
|
|
344
|
+
}
|
|
345
|
+
if (pipeline.columnsSince === undefined) {
|
|
346
|
+
await ctx.db.patch("sitePipelines", pipeline._id, { columnsSince: at });
|
|
347
|
+
}
|
|
348
|
+
if (declared.event) {
|
|
349
|
+
await enqueueColumnEvent(ctx, {
|
|
350
|
+
siteId: lead.siteId,
|
|
351
|
+
leadId: lead._id,
|
|
352
|
+
eventName: declared.event,
|
|
353
|
+
at,
|
|
354
|
+
meta,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return { column, columnAt: at };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* A column move the host sent alone (`setLeadStage` with a column and no
|
|
362
|
+
* stage), idempotent by its `eventId` like a stage change. Returns whether
|
|
363
|
+
* the eventId was new, and the lead it was first recorded for.
|
|
364
|
+
*/
|
|
365
|
+
export async function recordColumnMove(
|
|
366
|
+
ctx: MutationCtx,
|
|
367
|
+
move: {
|
|
368
|
+
siteId: Id<"sites">;
|
|
369
|
+
leadId: Id<"leads">;
|
|
370
|
+
eventId: string;
|
|
371
|
+
column: string;
|
|
372
|
+
at: number;
|
|
373
|
+
},
|
|
374
|
+
): Promise<{ fresh: boolean; leadId: Id<"leads"> }> {
|
|
375
|
+
const seen = await ctx.db
|
|
376
|
+
.query("leadColumnMoves")
|
|
377
|
+
.withIndex("by_siteId_and_eventId", (range) =>
|
|
378
|
+
range.eq("siteId", move.siteId).eq("eventId", move.eventId),
|
|
379
|
+
)
|
|
380
|
+
.unique();
|
|
381
|
+
if (seen) return { fresh: false, leadId: seen.leadId };
|
|
382
|
+
await ctx.db.insert("leadColumnMoves", {
|
|
383
|
+
...move,
|
|
384
|
+
column: move.column.slice(0, 64),
|
|
385
|
+
});
|
|
386
|
+
return { fresh: true, leadId: move.leadId };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Takes a removed lead out of the monthly counts and forgets its column
|
|
391
|
+
* marks and moves (both bounded per lead).
|
|
392
|
+
*/
|
|
393
|
+
export async function forgetLeadColumns(
|
|
394
|
+
ctx: MutationCtx,
|
|
395
|
+
lead: Pick<Doc<"leads">, "_id" | "siteId">,
|
|
396
|
+
): Promise<void> {
|
|
397
|
+
const marks = await ctx.db
|
|
398
|
+
.query("leadColumnMarks")
|
|
399
|
+
.withIndex("by_leadId_and_column", (range) => range.eq("leadId", lead._id))
|
|
400
|
+
.take(MAX_PIPELINE_COLUMNS * 2);
|
|
401
|
+
for (const mark of marks) {
|
|
402
|
+
await bumpDay(ctx, lead.siteId, mark.column, mark.dayStart, -1);
|
|
403
|
+
await ctx.db.delete("leadColumnMarks", mark._id);
|
|
404
|
+
}
|
|
405
|
+
const moves = await ctx.db
|
|
406
|
+
.query("leadColumnMoves")
|
|
407
|
+
.withIndex("by_leadId", (range) => range.eq("leadId", lead._id))
|
|
408
|
+
.take(MAX_COLUMN_MOVES_PER_LEAD);
|
|
409
|
+
for (const move of moves) await ctx.db.delete("leadColumnMoves", move._id);
|
|
410
|
+
}
|
package/src/component/schema.ts
CHANGED
|
@@ -6,6 +6,13 @@ import {
|
|
|
6
6
|
aggregateFieldsValidator,
|
|
7
7
|
leadEventTypeValidator,
|
|
8
8
|
leadFieldsValidator,
|
|
9
|
+
metaColumnEventValidator,
|
|
10
|
+
metaConnectionFieldsValidator,
|
|
11
|
+
metaConversionEventNameValidator,
|
|
12
|
+
metaUserDataValidator,
|
|
13
|
+
pipelineColumnValidator,
|
|
14
|
+
siteAdAccountFieldsValidator,
|
|
15
|
+
sitePixelFieldsValidator,
|
|
9
16
|
batchedEventValidator,
|
|
10
17
|
funnelFieldsValidator,
|
|
11
18
|
goalFieldsValidator,
|
|
@@ -430,12 +437,132 @@ export default defineSchema({
|
|
|
430
437
|
"key",
|
|
431
438
|
]),
|
|
432
439
|
|
|
433
|
-
// Names the host fetches from the
|
|
440
|
+
// Names the Meta sync (or the host) fetches from the Marketing API.
|
|
434
441
|
adCatalog: defineTable(adCatalogFieldsValidator).index("by_siteId_and_adId", [
|
|
435
442
|
"siteId",
|
|
436
443
|
"adId",
|
|
437
444
|
]),
|
|
438
445
|
|
|
446
|
+
// A Meta token the host pasted for one of its owners (see meta.ts),
|
|
447
|
+
// encrypted with RASTRO_SECRETS_KEY. No query returns the cipher.
|
|
448
|
+
metaConnections: defineTable(metaConnectionFieldsValidator).index(
|
|
449
|
+
"by_ownerKey",
|
|
450
|
+
["ownerKey"],
|
|
451
|
+
),
|
|
452
|
+
|
|
453
|
+
// A site's spend source: one Meta ad account, synced by the chain in
|
|
454
|
+
// metaSync.ts. `nextSyncAt` is when the chain takes it next.
|
|
455
|
+
siteAdAccounts: defineTable(siteAdAccountFieldsValidator)
|
|
456
|
+
.index("by_siteId", ["siteId"])
|
|
457
|
+
.index("by_connectionId", ["connectionId"])
|
|
458
|
+
.index("by_nextSyncAt", ["nextSyncAt"])
|
|
459
|
+
.index("by_siteId_and_adAccountId", ["siteId", "adAccountId"])
|
|
460
|
+
// An ad account feeds one site: linking checks it here.
|
|
461
|
+
.index("by_adAccountId", ["adAccountId"]),
|
|
462
|
+
|
|
463
|
+
// What one ad spent on one account-local day (`dayStart` is that date's
|
|
464
|
+
// UTC midnight). Meta's figures replace the row on every resync.
|
|
465
|
+
adSpendDaily: defineTable({
|
|
466
|
+
siteId: v.id("sites"),
|
|
467
|
+
adAccountId: v.string(),
|
|
468
|
+
adId: v.string(),
|
|
469
|
+
dayStart: v.number(),
|
|
470
|
+
spendCents: v.number(),
|
|
471
|
+
impressions: v.number(),
|
|
472
|
+
clicks: v.number(),
|
|
473
|
+
metaConversations: v.optional(v.number()),
|
|
474
|
+
updatedAt: v.number(),
|
|
475
|
+
})
|
|
476
|
+
.index("by_siteId_and_dayStart", ["siteId", "dayStart"])
|
|
477
|
+
.index("by_siteId_and_adId_and_dayStart", ["siteId", "adId", "dayStart"])
|
|
478
|
+
.index("by_siteId_and_adAccountId_and_dayStart", [
|
|
479
|
+
"siteId",
|
|
480
|
+
"adAccountId",
|
|
481
|
+
"dayStart",
|
|
482
|
+
]),
|
|
483
|
+
|
|
484
|
+
// `adSpendDaily` summed per site, ad account and day, kept by every write
|
|
485
|
+
// to it: the site totals read these few rows, so linking Meta never eats the
|
|
486
|
+
// lead report's row budget.
|
|
487
|
+
siteSpendDaily: defineTable({
|
|
488
|
+
siteId: v.id("sites"),
|
|
489
|
+
adAccountId: v.string(),
|
|
490
|
+
dayStart: v.number(),
|
|
491
|
+
spendCents: v.number(),
|
|
492
|
+
impressions: v.number(),
|
|
493
|
+
clicks: v.number(),
|
|
494
|
+
})
|
|
495
|
+
.index("by_siteId_and_dayStart", ["siteId", "dayStart"])
|
|
496
|
+
.index("by_siteId_and_adAccountId_and_dayStart", [
|
|
497
|
+
"siteId",
|
|
498
|
+
"adAccountId",
|
|
499
|
+
"dayStart",
|
|
500
|
+
]),
|
|
501
|
+
|
|
502
|
+
// The site's Meta pixel (dataset) for the Conversions API.
|
|
503
|
+
sitePixels: defineTable(sitePixelFieldsValidator)
|
|
504
|
+
.index("by_siteId", ["siteId"])
|
|
505
|
+
// A connection's pixels go with it when it is removed.
|
|
506
|
+
.index("by_connectionId", ["connectionId"])
|
|
507
|
+
// The pixels an owner set, for the federated scope check.
|
|
508
|
+
.index("by_setBy", ["setBy"]),
|
|
509
|
+
|
|
510
|
+
// The host's board for a site (its columns in order) and which Meta event
|
|
511
|
+
// each column sends. One row per site; see pipeline.ts.
|
|
512
|
+
sitePipelines: defineTable({
|
|
513
|
+
siteId: v.id("sites"),
|
|
514
|
+
columns: v.array(
|
|
515
|
+
pipelineColumnValidator.extend({
|
|
516
|
+
event: v.optional(metaColumnEventValidator),
|
|
517
|
+
}),
|
|
518
|
+
),
|
|
519
|
+
/** Whether a new lead is sent as `Lead` (mirrored on the pixel). */
|
|
520
|
+
sendLeadEvent: v.boolean(),
|
|
521
|
+
/** When the host first sent a lead's column: counts start there. */
|
|
522
|
+
columnsSince: v.optional(v.number()),
|
|
523
|
+
updatedAt: v.number(),
|
|
524
|
+
}).index("by_siteId", ["siteId"]),
|
|
525
|
+
|
|
526
|
+
// Leads whose latest entry into a column fell on each UTC day, for the
|
|
527
|
+
// event map's monthly counts of distinct leads. Lead volume is human
|
|
528
|
+
// scale, so a row is never a hot spot.
|
|
529
|
+
leadColumnDaily: defineTable({
|
|
530
|
+
siteId: v.id("sites"),
|
|
531
|
+
column: v.string(),
|
|
532
|
+
dayStart: v.number(),
|
|
533
|
+
entries: v.number(),
|
|
534
|
+
}).index("by_siteId_and_column_and_dayStart", [
|
|
535
|
+
"siteId",
|
|
536
|
+
"column",
|
|
537
|
+
"dayStart",
|
|
538
|
+
]),
|
|
539
|
+
|
|
540
|
+
// One per lead and column: the UTC day the lead last entered it.
|
|
541
|
+
leadColumnMarks: defineTable({
|
|
542
|
+
siteId: v.id("sites"),
|
|
543
|
+
leadId: v.id("leads"),
|
|
544
|
+
column: v.string(),
|
|
545
|
+
dayStart: v.number(),
|
|
546
|
+
}).index("by_leadId_and_column", ["leadId", "column"]),
|
|
547
|
+
|
|
548
|
+
// Column moves the host sent alone, so a retried `eventId` is a no-op.
|
|
549
|
+
leadColumnMoves: defineTable({
|
|
550
|
+
siteId: v.id("sites"),
|
|
551
|
+
leadId: v.id("leads"),
|
|
552
|
+
eventId: v.string(),
|
|
553
|
+
column: v.string(),
|
|
554
|
+
at: v.number(),
|
|
555
|
+
})
|
|
556
|
+
.index("by_siteId_and_eventId", ["siteId", "eventId"])
|
|
557
|
+
.index("by_leadId", ["leadId"]),
|
|
558
|
+
|
|
559
|
+
// The single scheduler chain that syncs every linked ad account.
|
|
560
|
+
metaSyncLoop: defineTable({
|
|
561
|
+
key: v.literal("loop"),
|
|
562
|
+
scheduledFor: v.number(),
|
|
563
|
+
jobId: v.id("_scheduled_functions"),
|
|
564
|
+
}).index("by_key", ["key"]),
|
|
565
|
+
|
|
439
566
|
visitorSecrets: defineTable({
|
|
440
567
|
key: v.literal("current"),
|
|
441
568
|
value: v.string(),
|
|
@@ -569,4 +696,73 @@ export default defineSchema({
|
|
|
569
696
|
"windowStart",
|
|
570
697
|
])
|
|
571
698
|
.index("by_siteId_and_updatedAt", ["siteId", "updatedAt"]),
|
|
699
|
+
|
|
700
|
+
// Conversions waiting for (or done with) the site's Meta pixel, sent by
|
|
701
|
+
// metaCapi.ts. `userData` holds the host's hashes only until the final
|
|
702
|
+
// attempt, then it is erased.
|
|
703
|
+
metaConversionOutbox: defineTable({
|
|
704
|
+
siteId: v.id("sites"),
|
|
705
|
+
leadId: v.id("leads"),
|
|
706
|
+
eventId: v.string(),
|
|
707
|
+
eventName: metaConversionEventNameValidator,
|
|
708
|
+
eventTime: v.number(),
|
|
709
|
+
valueCents: v.optional(v.number()),
|
|
710
|
+
currency: v.optional(v.string()),
|
|
711
|
+
userData: v.optional(metaUserDataValidator),
|
|
712
|
+
status: v.union(
|
|
713
|
+
v.literal("pending"),
|
|
714
|
+
v.literal("sent"),
|
|
715
|
+
v.literal("failed"),
|
|
716
|
+
),
|
|
717
|
+
attempts: v.number(),
|
|
718
|
+
/** When the current attempt is due; the sweep reads it to find stalls. */
|
|
719
|
+
nextAttemptAt: v.number(),
|
|
720
|
+
/** When a send took the current attempt: its lease, for the sweep. */
|
|
721
|
+
attemptStartedAt: v.optional(v.number()),
|
|
722
|
+
lastError: v.optional(v.string()),
|
|
723
|
+
fbtraceId: v.optional(v.string()),
|
|
724
|
+
sentAt: v.optional(v.number()),
|
|
725
|
+
createdAt: v.number(),
|
|
726
|
+
})
|
|
727
|
+
.index("by_siteId_and_eventId", ["siteId", "eventId"])
|
|
728
|
+
.index("by_siteId_and_createdAt", ["siteId", "createdAt"])
|
|
729
|
+
.index("by_siteId_and_status_and_createdAt", [
|
|
730
|
+
"siteId",
|
|
731
|
+
"status",
|
|
732
|
+
"createdAt",
|
|
733
|
+
])
|
|
734
|
+
.index("by_leadId", ["leadId"])
|
|
735
|
+
.index("by_status_and_nextAttemptAt", ["status", "nextAttemptAt"]),
|
|
736
|
+
|
|
737
|
+
// The single scheduler chain that re-sends stalled outbox rows; it stops
|
|
738
|
+
// by itself when nothing is pending.
|
|
739
|
+
metaCapiSweep: defineTable({
|
|
740
|
+
key: v.literal("sweep"),
|
|
741
|
+
scheduledFor: v.number(),
|
|
742
|
+
jobId: v.id("_scheduled_functions"),
|
|
743
|
+
}).index("by_key", ["key"]),
|
|
744
|
+
|
|
745
|
+
// The outbox counted per site and status, kept by every write to it, so
|
|
746
|
+
// `meta.status` never scans the history. Split in shards by event (see
|
|
747
|
+
// metaCapi.ts) so concurrent sends rarely write the same document.
|
|
748
|
+
metaConversionCounts: defineTable({
|
|
749
|
+
siteId: v.id("sites"),
|
|
750
|
+
shard: v.number(),
|
|
751
|
+
pending: v.number(),
|
|
752
|
+
sent: v.number(),
|
|
753
|
+
failed: v.number(),
|
|
754
|
+
}).index("by_siteId_and_shard", ["siteId", "shard"]),
|
|
755
|
+
|
|
756
|
+
// Conversions Meta accepted, per site, UTC day and event, in the same
|
|
757
|
+
// shards as the counts: `meta.status` reads the last 30 days through the
|
|
758
|
+
// index, at most 31 days times the shards.
|
|
759
|
+
metaConversionDaily: defineTable({
|
|
760
|
+
siteId: v.id("sites"),
|
|
761
|
+
dayStart: v.number(),
|
|
762
|
+
shard: v.number(),
|
|
763
|
+
Purchase: v.number(),
|
|
764
|
+
QualifiedLead: v.number(),
|
|
765
|
+
Lead: v.number(),
|
|
766
|
+
Schedule: v.number(),
|
|
767
|
+
}).index("by_siteId_and_dayStart_and_shard", ["siteId", "dayStart", "shard"]),
|
|
572
768
|
});
|