@tokenoftrust/storefront-runner 2.0.3 → 2.0.5
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/apps/storefront/.astro/content.d.ts +159 -0
- package/apps/storefront/.astro/integrations/_astrojs_cloudflare/cloudflare.d.ts +1 -0
- package/apps/storefront/.astro/types.d.ts +3 -0
- package/apps/storefront/migrations-apps/0011_dt_tracking_tenant_databases.sql +23 -0
- package/apps/storefront/node_modules/.bin/acorn +43 -0
- package/apps/storefront/node_modules/.bin/astro +43 -0
- package/apps/storefront/node_modules/.bin/astro-check +43 -0
- package/apps/storefront/node_modules/.bin/browserslist +43 -0
- package/apps/storefront/node_modules/.bin/cf-wrangler +43 -0
- package/apps/storefront/node_modules/.bin/depcruise +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-baseline +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-fmt +43 -0
- package/apps/storefront/node_modules/.bin/depcruise-wrap-stream-in-html +43 -0
- package/apps/storefront/node_modules/.bin/dependency-cruise +43 -0
- package/apps/storefront/node_modules/.bin/dependency-cruiser +43 -0
- package/apps/storefront/node_modules/.bin/drizzle-kit +43 -0
- package/apps/storefront/node_modules/.bin/esbuild +43 -0
- package/apps/storefront/node_modules/.bin/jiti +43 -0
- package/apps/storefront/node_modules/.bin/playwright +43 -0
- package/apps/storefront/node_modules/.bin/playwright-core +43 -0
- package/apps/storefront/node_modules/.bin/prettier +43 -0
- package/apps/storefront/node_modules/.bin/rollup +43 -0
- package/apps/storefront/node_modules/.bin/tsc +43 -0
- package/apps/storefront/node_modules/.bin/tsserver +43 -0
- package/apps/storefront/node_modules/.bin/tsx +43 -0
- package/apps/storefront/node_modules/.bin/vite +43 -0
- package/apps/storefront/node_modules/.bin/vitest +43 -0
- package/apps/storefront/node_modules/.bin/workerd +43 -0
- package/apps/storefront/node_modules/.bin/wrangler +43 -0
- package/apps/storefront/node_modules/.bin/wrangler2 +43 -0
- package/apps/storefront/node_modules/.bin/yaml +43 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js +58 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js +741 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js +2549 -0
- package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/_metadata.json +150 -0
- package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js +434 -0
- package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js +1535 -0
- package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js +48 -0
- package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js +42 -0
- package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/package.json +3 -0
- package/apps/storefront/node_modules/.vite/deps/preact.js +310 -0
- package/apps/storefront/node_modules/.vite/deps/preact.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat.js +420 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_compat_jsx-runtime.js +4 -0
- package/apps/storefront/node_modules/.vite/deps/preact_debug.js +236 -0
- package/apps/storefront/node_modules/.vite/deps/preact_debug.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_devtools.js +12 -0
- package/apps/storefront/node_modules/.vite/deps/preact_devtools.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_hooks.js +192 -0
- package/apps/storefront/node_modules/.vite/deps/preact_hooks.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-dev-runtime.js +3 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js +92 -0
- package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js +59 -0
- package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/rolldown-runtime-BPOCksWG.js +24 -0
- package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js +189 -0
- package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js +13 -0
- package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js +458 -0
- package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js +1016 -0
- package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js +32 -0
- package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js +113 -0
- package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js.map +1 -0
- package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +8 -0
- package/apps/storefront/node_modules/.vite/deps_ssr/package.json +3 -0
- package/apps/storefront/src/components/chrome/MegaMenu.astro +11 -1
- package/apps/storefront/src/components/chrome/NavDropdown.astro +49 -7
- package/apps/storefront/src/components/chrome/SiteFooter.astro +29 -4
- package/apps/storefront/src/components/chrome/SiteHeader.astro +39 -6
- package/apps/storefront/src/components/chrome/UtilityNav.astro +13 -1
- package/apps/storefront/src/components/islands/MobileNav.tsx +42 -2
- package/apps/storefront/src/layouts/Layout.astro +65 -7
- package/apps/storefront/src/lib/apps/registryAccess.ts +45 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +4 -4
- package/apps/storefront/src/lib/chrome/ctaIdentity.ts +21 -0
- package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +22 -22
- package/apps/storefront/src/lib/chrome/trackingPlacements.ts +36 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +57 -0
- package/apps/storefront/src/lib/rawChrome.ts +73 -14
- package/apps/storefront/src/lib/rawMarketingHtml.ts +30 -0
- package/apps/storefront/src/lib/shared/capability.ts +68 -0
- package/apps/storefront/src/lib/shared/developerInvites.ts +1250 -0
- package/apps/storefront/src/lib/shared/invitePreflight.ts +123 -0
- package/apps/storefront/src/lib/storyblok/provider.ts +21 -5
- package/apps/storefront/src/lib/tracking/collectorAllowlist.ts +40 -0
- package/apps/storefront/src/lib/tracking/collectorIngest.ts +188 -0
- package/apps/storefront/src/lib/tracking/d1Control.ts +180 -0
- package/apps/storefront/src/lib/tracking/interactionCollector.ts +202 -0
- package/apps/storefront/src/lib/tracking/interactionHealth.ts +252 -0
- package/apps/storefront/src/lib/tracking/interactionSchema.ts +107 -0
- package/apps/storefront/src/lib/tracking/interactionStore.ts +0 -0
- package/apps/storefront/src/lib/tracking/pruneRollup.ts +260 -0
- package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +162 -0
- package/apps/storefront/src/lib/tracking/tenantD1Registry.ts +214 -0
- package/apps/storefront/src/lib/tracking/tenantReadAccess.ts +138 -0
- package/apps/storefront/src/lib/tracking-writeback/rewriteTrackingIds.ts +175 -0
- package/apps/storefront/src/lib/tracking-writeback/writebackCandidate.ts +110 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +38 -4
- package/apps/storefront/src/lib/webhooks/emit.ts +1 -1
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +1 -1
- package/apps/storefront/src/pages/api/admin/interaction-insights.ts +101 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -1
- package/apps/storefront/src/pages/api/tracking/interaction.ts +123 -0
- package/apps/storefront/src/pages/index.astro +1 -1
- package/apps/storefront/src/pages/products/[handle].astro +1 -1
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +33 -14
- package/apps/storefront/src/workers/pruneRollupWorker.ts +120 -0
- package/package.json +1 -1
- package/packages/public-runtime/node_modules/.bin/tsc +43 -0
- package/packages/public-runtime/node_modules/.bin/tsserver +43 -0
- package/packages/public-runtime/node_modules/.bin/vitest +43 -0
- package/packages/public-runtime/src/chrome.ts +18 -7
- package/packages/public-runtime/src/index.ts +10 -0
- package/packages/public-runtime/src/integration/index.ts +1 -0
- package/packages/public-runtime/src/integration/interaction-tracking-gate.ts +142 -0
- package/packages/public-runtime/src/interaction-tracking.ts +364 -0
- package/packages/public-runtime/src/tracking-id-derivation.ts +1008 -0
- package/pnpm-workspace.yaml +3 -0
- package/scripts/tenant/validate.mjs +306 -1
- package/scripts/tot-dev.mjs +59 -3
- package/tenants/home/public/js/storefront.js +6 -87
- package/apps/storefront/src/lib/apps/adminService.ts +0 -116
- package/apps/storefront/src/lib/apps/adminSession.ts +0 -214
- package/apps/storefront/src/lib/apps/apiAuth.ts +0 -91
- package/apps/storefront/src/lib/apps/apiRoute.ts +0 -35
- package/apps/storefront/src/lib/apps/catalogMapper.ts +0 -39
- package/apps/storefront/src/lib/apps/consentConfigStore.ts +0 -228
- package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +0 -447
- package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +0 -367
- package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +0 -315
- package/apps/storefront/src/lib/apps/healthAggregate.ts +0 -66
- package/apps/storefront/src/lib/apps/integrationLifecycle.ts +0 -614
- package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +0 -125
- package/apps/storefront/src/lib/apps/integrationReadModel.ts +0 -563
- package/apps/storefront/src/lib/apps/integrationsTab.ts +0 -547
- package/apps/storefront/src/lib/apps/integrationsView.ts +0 -106
- package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +0 -599
- package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +0 -170
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +0 -121
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +0 -148
- package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +0 -16
- package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +0 -105
- package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +0 -177
- package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +0 -251
- package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +0 -241
- package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +0 -211
- package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +0 -409
- package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +0 -368
- package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +0 -188
- package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +0 -291
- package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +0 -95
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +0 -91
- package/apps/storefront/src/lib/content-edit/client.ts +0 -286
- package/apps/storefront/src/lib/content-edit/index.ts +0 -14
- package/apps/storefront/src/lib/content-edit/route.ts +0 -25
- package/apps/storefront/src/lib/content-edit/types.ts +0 -19
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +0 -164
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dt-collector-v1 — the browser-side delivery destination for
|
|
3
|
+
* `TotInteraction` (`@tot/public-runtime`'s declarative interaction-tracking
|
|
4
|
+
* contract). This module OWNS batching + transport; it does not install the
|
|
5
|
+
* click listener and does not gate on consent/environment — those are
|
|
6
|
+
* `interaction-tracking.ts` and `dt-consent-environment-gate` respectively.
|
|
7
|
+
*
|
|
8
|
+
* Wiring (owned by whichever module composes the gate in front of delivery,
|
|
9
|
+
* e.g. `dt-consent-environment-gate`):
|
|
10
|
+
*
|
|
11
|
+
* import { onInteraction } from "@tot/public-runtime";
|
|
12
|
+
* import { collectInteraction } from "@/lib/tracking/interactionCollector";
|
|
13
|
+
* onInteraction((interaction) => {
|
|
14
|
+
* if (consentAndEnvironmentAllow()) collectInteraction(interaction);
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* or, where the caller controls install directly:
|
|
18
|
+
*
|
|
19
|
+
* installInteractionTracking({ deliver: collectInteraction });
|
|
20
|
+
*
|
|
21
|
+
* PAYLOAD GUARDRAIL: this module reads only the five `TotInteraction` fields
|
|
22
|
+
* handed to it, plus what it synthesizes itself (`ts`, `correlationId`,
|
|
23
|
+
* `seq`). No DOM access, no `textContent`/`href`/form reads — same prohibition
|
|
24
|
+
* as `interaction-tracking.ts`'s listener; see that module's guardrail test
|
|
25
|
+
* (`tests/interaction-tracking.test.ts`) for the pattern this module must not
|
|
26
|
+
* violate either.
|
|
27
|
+
*/
|
|
28
|
+
import type { TotInteraction } from "@tot/public-runtime";
|
|
29
|
+
|
|
30
|
+
/** Where the browser beacon POSTs interaction batches. */
|
|
31
|
+
export const INTERACTION_BEACON_PATH = "/api/tracking/interaction";
|
|
32
|
+
|
|
33
|
+
/** Max events held in memory before a forced flush — bounds one beacon's size
|
|
34
|
+
* and the worst case if flush triggers never fire (e.g. a test/SSR context). */
|
|
35
|
+
export const MAX_QUEUE_SIZE = 20;
|
|
36
|
+
|
|
37
|
+
/** The wire shape POSTed to `INTERACTION_BEACON_PATH` — `TotInteraction`'s five
|
|
38
|
+
* fields plus this module's own synthesized correlation fields. Must stay in
|
|
39
|
+
* lockstep with `INTERACTION_WIRE_ALLOWLIST`
|
|
40
|
+
* (`apps/storefront/src/lib/tracking/collectorAllowlist.ts`). */
|
|
41
|
+
export interface InteractionWireEvent {
|
|
42
|
+
type: TotInteraction["type"];
|
|
43
|
+
el: string;
|
|
44
|
+
action: string | null;
|
|
45
|
+
placement: string | null;
|
|
46
|
+
scope: string | null;
|
|
47
|
+
index: number | null;
|
|
48
|
+
/** Client emit clock, ISO-8601. */
|
|
49
|
+
ts: string;
|
|
50
|
+
/** Groups every event from one collector lifetime (one page load). */
|
|
51
|
+
correlationId: string;
|
|
52
|
+
/** Monotonic within `correlationId`, starting at 0. */
|
|
53
|
+
seq: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build the wire event for one interaction. Pure — no DOM, no clock side
|
|
57
|
+
* effects beyond the `now` parameter, so it is unit-testable without faking
|
|
58
|
+
* globals. */
|
|
59
|
+
export function toWireEvent(
|
|
60
|
+
interaction: TotInteraction,
|
|
61
|
+
correlationId: string,
|
|
62
|
+
seq: number,
|
|
63
|
+
now: () => string = () => new Date().toISOString(),
|
|
64
|
+
): InteractionWireEvent {
|
|
65
|
+
return {
|
|
66
|
+
type: interaction.type,
|
|
67
|
+
el: interaction.el,
|
|
68
|
+
action: interaction.action,
|
|
69
|
+
placement: interaction.placement,
|
|
70
|
+
scope: interaction.scope,
|
|
71
|
+
index: interaction.index,
|
|
72
|
+
ts: now(),
|
|
73
|
+
correlationId,
|
|
74
|
+
seq,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A sink for a finished batch. The default posts one JSON beacon. */
|
|
79
|
+
export type InteractionBatchSender = (events: InteractionWireEvent[]) => void;
|
|
80
|
+
|
|
81
|
+
/** Default sender: one `sendBeacon` per flushed batch, falling back to a
|
|
82
|
+
* keepalive `fetch` where `sendBeacon` is absent (mirrors
|
|
83
|
+
* `analytics/webVitals.ts`'s `beaconReporter`). Never throws into the page. */
|
|
84
|
+
export function beaconBatchSender(endpoint: string = INTERACTION_BEACON_PATH): InteractionBatchSender {
|
|
85
|
+
return (events) => {
|
|
86
|
+
if (events.length === 0 || typeof navigator === "undefined") return;
|
|
87
|
+
const body = JSON.stringify({ events });
|
|
88
|
+
try {
|
|
89
|
+
if (typeof navigator.sendBeacon === "function") {
|
|
90
|
+
navigator.sendBeacon(endpoint, body);
|
|
91
|
+
} else if (typeof fetch === "function") {
|
|
92
|
+
void fetch(endpoint, { method: "POST", body, keepalive: true });
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Never let telemetry throw into the page.
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function randomCorrelationId(): string {
|
|
101
|
+
const c: Crypto | undefined = (globalThis as { crypto?: Crypto }).crypto;
|
|
102
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
103
|
+
return `ic-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A collector instance: batches interactions in memory and flushes them to
|
|
108
|
+
* `send` on demand, on queue overflow, or when the caller wires
|
|
109
|
+
* `flush`-on-`visibilitychange`/`pagehide` (see {@link installFlushTriggers}).
|
|
110
|
+
* Exposed as a factory (rather than one module-level singleton) so tests can
|
|
111
|
+
* construct an isolated instance with a fake sender and clock.
|
|
112
|
+
*/
|
|
113
|
+
export interface InteractionCollector {
|
|
114
|
+
/** Enqueue one interaction; synthesizes `ts`/`correlationId`/`seq`. Flushes
|
|
115
|
+
* immediately once the queue reaches `MAX_QUEUE_SIZE`. */
|
|
116
|
+
collect(interaction: TotInteraction): void;
|
|
117
|
+
/** Send whatever is queued right now (no-op if empty). */
|
|
118
|
+
flush(): void;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createInteractionCollector(
|
|
122
|
+
send: InteractionBatchSender = beaconBatchSender(),
|
|
123
|
+
correlationId: string = randomCorrelationId(),
|
|
124
|
+
now?: () => string,
|
|
125
|
+
): InteractionCollector {
|
|
126
|
+
let queue: InteractionWireEvent[] = [];
|
|
127
|
+
let seq = 0;
|
|
128
|
+
|
|
129
|
+
const flush = (): void => {
|
|
130
|
+
if (queue.length === 0) return;
|
|
131
|
+
const batch = queue;
|
|
132
|
+
queue = [];
|
|
133
|
+
send(batch);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const collect = (interaction: TotInteraction): void => {
|
|
137
|
+
queue.push(toWireEvent(interaction, correlationId, seq, now));
|
|
138
|
+
seq += 1;
|
|
139
|
+
if (queue.length >= MAX_QUEUE_SIZE) flush();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return { collect, flush };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Browser wiring (thin — everything above is pure + unit-tested).
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Wire `collector.flush` to the two reliable end-of-visibility moments (same
|
|
151
|
+
* pair `analytics/webVitals.ts#startWebVitals` uses): the page first going
|
|
152
|
+
* hidden, and `pagehide` — the last point mobile Safari guarantees a beacon
|
|
153
|
+
* can still be sent. No-op teardown when there is no `document`/`window`
|
|
154
|
+
* (SSR, tests).
|
|
155
|
+
*/
|
|
156
|
+
export function installFlushTriggers(collector: InteractionCollector): () => void {
|
|
157
|
+
if (typeof document === "undefined" || typeof window === "undefined") return () => {};
|
|
158
|
+
const onVisibility = (): void => {
|
|
159
|
+
if (document.visibilityState === "hidden") collector.flush();
|
|
160
|
+
};
|
|
161
|
+
document.addEventListener("visibilitychange", onVisibility, { capture: true });
|
|
162
|
+
window.addEventListener("pagehide", collector.flush, { capture: true });
|
|
163
|
+
return () => {
|
|
164
|
+
document.removeEventListener("visibilitychange", onVisibility, { capture: true });
|
|
165
|
+
window.removeEventListener("pagehide", collector.flush, { capture: true });
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let defaultCollector: InteractionCollector | null = null;
|
|
170
|
+
let defaultTeardown: (() => void) | null = null;
|
|
171
|
+
|
|
172
|
+
function getDefaultCollector(): InteractionCollector {
|
|
173
|
+
if (!defaultCollector) {
|
|
174
|
+
defaultCollector = createInteractionCollector();
|
|
175
|
+
defaultTeardown = installFlushTriggers(defaultCollector);
|
|
176
|
+
}
|
|
177
|
+
return defaultCollector;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The default delivery destination: `installInteractionTracking({ deliver:
|
|
182
|
+
* collectInteraction })` or forward into it from an `onInteraction` subscriber
|
|
183
|
+
* (that is what the consent/environment gate does). Lazily creates the
|
|
184
|
+
* module-level collector and its flush wiring on first call, so importing
|
|
185
|
+
* this module has no side effects until an interaction actually arrives.
|
|
186
|
+
*/
|
|
187
|
+
export function collectInteraction(interaction: TotInteraction): void {
|
|
188
|
+
getDefaultCollector().collect(interaction);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Force-flush the default collector (e.g. from a manual "send now" hook). */
|
|
192
|
+
export function flushInteractions(): void {
|
|
193
|
+
defaultCollector?.flush();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Test-only: drop the module-level singleton so a fresh collector/wiring is
|
|
197
|
+
* created on the next {@link collectInteraction} call. */
|
|
198
|
+
export function resetDefaultCollectorForTests(): void {
|
|
199
|
+
defaultTeardown?.();
|
|
200
|
+
defaultCollector = null;
|
|
201
|
+
defaultTeardown = null;
|
|
202
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interaction-tracking chain's HEALTH ASSERTION — the thing that makes a
|
|
3
|
+
* broken collection chain loud instead of silent.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists. Every other part of this system is correctly fail-quiet at
|
|
6
|
+
* runtime: the delegated listener returns without an event when there is no
|
|
7
|
+
* `data-tot-el` up the ancestor chain (that is the `tracking: "none"` opt-out,
|
|
8
|
+
* where ABSENCE is the declaration), and the consent/environment gate drops a
|
|
9
|
+
* disallowed interaction with no error. Both are right. Together they mean a
|
|
10
|
+
* surface that was never stamped at all produces exactly the same observable
|
|
11
|
+
* result as a surface nobody clicked — which is the failure mode that made the
|
|
12
|
+
* legacy mixin useless for years: no way to tell "collection is broken" from
|
|
13
|
+
* "legitimately idle".
|
|
14
|
+
*
|
|
15
|
+
* This module is the one place that CAN tell them apart, because it looks at
|
|
16
|
+
* the rendered markup rather than at the event stream:
|
|
17
|
+
*
|
|
18
|
+
* - `mount_missing` — the document carries actionable elements but not the
|
|
19
|
+
* serialized listener, so no click on it can ever be seen. FATAL.
|
|
20
|
+
* - `chrome_unstamped` — the surface carries actionable elements and NOT ONE
|
|
21
|
+
* of them carries `data-tot-el`. FATAL. Zero-of-N is not an opt-out
|
|
22
|
+
* declaration; a real opt-out is per element, and an author opting out of
|
|
23
|
+
* every single element still leaves the OTHER elements stamped. This is
|
|
24
|
+
* the finding that covers the **sharedChrome bypass path**: a tenant that
|
|
25
|
+
* ships a hand-authored `content/<id>/chrome.html` bypasses governed
|
|
26
|
+
* chrome entirely, so `validateChromeConfig` never sees it,
|
|
27
|
+
* `trackingAttributes()` never stamps it, and `rewriteTrackingIds`
|
|
28
|
+
* (dt-id-writeback) explicitly refuses to touch chrome. Nothing else in
|
|
29
|
+
* the chain would ever say a word about it.
|
|
30
|
+
* - `stamp_incomplete` — elements carry `data-tot-el` but no
|
|
31
|
+
* `data-tot-action`. NOT fatal: the publish-time safety net
|
|
32
|
+
* (`publishSafetyNet.ts`) stamps identity only, by design, and there is no
|
|
33
|
+
* derivation path from identity to intent. Reported because those rows
|
|
34
|
+
* land in the store un-analyzable, which is worth seeing.
|
|
35
|
+
*
|
|
36
|
+
* NO DOM-TEXT READS: only `tagName` and the parsed attribute list are ever
|
|
37
|
+
* read, matching the payload guardrail the listener and every consumer are
|
|
38
|
+
* held to. Actionability is decided by `@tot/public-runtime`'s shared
|
|
39
|
+
* `isTrackableNode` — the same oracle the derivation engine and the writeback
|
|
40
|
+
* rewriter use, so "actionable" cannot mean three different things.
|
|
41
|
+
*/
|
|
42
|
+
import { parse } from "parse5";
|
|
43
|
+
import {
|
|
44
|
+
INTERACTION_TRACKING_BOOT_JS,
|
|
45
|
+
TRACKING_ATTR_ACTION,
|
|
46
|
+
TRACKING_ATTR_EL,
|
|
47
|
+
isTrackableNode,
|
|
48
|
+
type StructuralNode,
|
|
49
|
+
} from "@tot/public-runtime";
|
|
50
|
+
|
|
51
|
+
/** The finding codes, in the order this module reports them. */
|
|
52
|
+
export type TrackingHealthCode = "mount_missing" | "chrome_unstamped" | "stamp_incomplete";
|
|
53
|
+
|
|
54
|
+
/** The codes that mean the chain is BROKEN rather than merely imperfect. */
|
|
55
|
+
const FATAL_CODES: ReadonlySet<TrackingHealthCode> = new Set<TrackingHealthCode>([
|
|
56
|
+
"mount_missing",
|
|
57
|
+
"chrome_unstamped",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
export interface TrackingHealthFinding {
|
|
61
|
+
code: TrackingHealthCode;
|
|
62
|
+
/** Human-actionable one-liner; carries counts and tags, never content. */
|
|
63
|
+
detail: string;
|
|
64
|
+
fatal: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TrackingHealthReport {
|
|
68
|
+
/** True iff there is no FATAL finding — i.e. a click here can be collected. */
|
|
69
|
+
ok: boolean;
|
|
70
|
+
/** Elements an interaction can originate from (`isTrackableNode`). */
|
|
71
|
+
actionable: number;
|
|
72
|
+
/** Of those, how many carry `data-tot-el`. */
|
|
73
|
+
stamped: number;
|
|
74
|
+
/** Of the stamped ones, how many also carry `data-tot-action`. */
|
|
75
|
+
attributed: number;
|
|
76
|
+
findings: TrackingHealthFinding[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TrackingHealthOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Require the serialized listener to be present. True for a whole document,
|
|
82
|
+
* false for a chrome WRAPPER inspected before `rawHtmlResponse()` has
|
|
83
|
+
* injected the listener into it.
|
|
84
|
+
*/
|
|
85
|
+
requireMount?: boolean;
|
|
86
|
+
/** A label for the log/error line — e.g. `chrome.html (tenant acme)`. */
|
|
87
|
+
surface?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Thrown by {@link assertTrackingChainHealthy} on a FATAL finding. */
|
|
91
|
+
export class TrackingChainBrokenError extends Error {
|
|
92
|
+
readonly report: TrackingHealthReport;
|
|
93
|
+
|
|
94
|
+
constructor(report: TrackingHealthReport, surface: string) {
|
|
95
|
+
const codes = report.findings.filter((f) => f.fatal).map((f) => f.code);
|
|
96
|
+
super(
|
|
97
|
+
`interaction tracking is BROKEN on ${surface} [${codes.join(", ")}] — ` +
|
|
98
|
+
report.findings
|
|
99
|
+
.filter((f) => f.fatal)
|
|
100
|
+
.map((f) => f.detail)
|
|
101
|
+
.join("; ") +
|
|
102
|
+
` (actionable=${report.actionable}, stamped=${report.stamped}, ` +
|
|
103
|
+
`attributed=${report.attributed}). A click on this surface produces NO ` +
|
|
104
|
+
`retrievable event, and nothing else in the chain reports it: the listener ` +
|
|
105
|
+
`returns silently when no ${TRACKING_ATTR_EL} is present, which is also how a ` +
|
|
106
|
+
`legitimate \`tracking: "none"\` opt-out looks.`,
|
|
107
|
+
);
|
|
108
|
+
this.name = "TrackingChainBrokenError";
|
|
109
|
+
this.report = report;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface P5Attr {
|
|
114
|
+
name: string;
|
|
115
|
+
value: string;
|
|
116
|
+
}
|
|
117
|
+
interface P5Node {
|
|
118
|
+
nodeName: string;
|
|
119
|
+
tagName?: string;
|
|
120
|
+
attrs?: P5Attr[];
|
|
121
|
+
childNodes?: P5Node[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function nonEmpty(v: string | undefined): boolean {
|
|
125
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Inspect one rendered surface (a full document, a chrome wrapper, or a body
|
|
130
|
+
* fragment). PURE — no I/O, no logging, no throwing. `html` that parses to
|
|
131
|
+
* nothing actionable is healthy by definition: a surface with no actionable
|
|
132
|
+
* element has nothing to collect, and saying otherwise would make every
|
|
133
|
+
* text-only page a false alarm.
|
|
134
|
+
*/
|
|
135
|
+
export function inspectTrackingChain(
|
|
136
|
+
html: string,
|
|
137
|
+
options: TrackingHealthOptions = {},
|
|
138
|
+
): TrackingHealthReport {
|
|
139
|
+
let actionable = 0;
|
|
140
|
+
let stamped = 0;
|
|
141
|
+
let attributed = 0;
|
|
142
|
+
const unstampedTags = new Set<string>();
|
|
143
|
+
|
|
144
|
+
const walk = (node: P5Node): void => {
|
|
145
|
+
if (node.tagName) {
|
|
146
|
+
const attrs: Record<string, string> = {};
|
|
147
|
+
for (const a of node.attrs ?? []) attrs[a.name] = a.value;
|
|
148
|
+
const structural: StructuralNode = { tag: node.tagName.toLowerCase(), attrs };
|
|
149
|
+
const hasEl = nonEmpty(attrs[TRACKING_ATTR_EL]);
|
|
150
|
+
if (isTrackableNode(structural)) {
|
|
151
|
+
actionable += 1;
|
|
152
|
+
if (hasEl) {
|
|
153
|
+
stamped += 1;
|
|
154
|
+
if (nonEmpty(attrs[TRACKING_ATTR_ACTION])) attributed += 1;
|
|
155
|
+
} else {
|
|
156
|
+
unstampedTags.add(structural.tag);
|
|
157
|
+
}
|
|
158
|
+
} else if (hasEl) {
|
|
159
|
+
// Stamped but not actionable by the shared oracle (e.g. a stamped
|
|
160
|
+
// wrapper). Counted so `stamped` never understates the surface.
|
|
161
|
+
stamped += 1;
|
|
162
|
+
if (nonEmpty(attrs[TRACKING_ATTR_ACTION])) attributed += 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const child of node.childNodes ?? []) walk(child);
|
|
166
|
+
};
|
|
167
|
+
walk(parse(html) as P5Node);
|
|
168
|
+
|
|
169
|
+
const findings: TrackingHealthFinding[] = [];
|
|
170
|
+
|
|
171
|
+
if (options.requireMount && actionable > 0 && !html.includes(INTERACTION_TRACKING_BOOT_JS)) {
|
|
172
|
+
findings.push({
|
|
173
|
+
code: "mount_missing",
|
|
174
|
+
detail: `${actionable} actionable element(s) but the serialized interaction-tracking listener is not in this document`,
|
|
175
|
+
fatal: true,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (actionable > 0 && stamped === 0) {
|
|
180
|
+
findings.push({
|
|
181
|
+
code: "chrome_unstamped",
|
|
182
|
+
detail:
|
|
183
|
+
`${actionable} actionable element(s) (${[...unstampedTags].sort().join(", ")}) and NOT ONE ` +
|
|
184
|
+
`carries ${TRACKING_ATTR_EL} — an unstamped surface, not a per-element opt-out`,
|
|
185
|
+
fatal: true,
|
|
186
|
+
});
|
|
187
|
+
} else if (stamped > attributed) {
|
|
188
|
+
findings.push({
|
|
189
|
+
code: "stamp_incomplete",
|
|
190
|
+
detail: `${stamped - attributed} of ${stamped} stamped element(s) carry no ${TRACKING_ATTR_ACTION}, so their rows land un-analyzable`,
|
|
191
|
+
fatal: false,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
ok: !findings.some((f) => f.fatal),
|
|
197
|
+
actionable,
|
|
198
|
+
stamped,
|
|
199
|
+
attributed,
|
|
200
|
+
findings,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* {@link inspectTrackingChain}, but a FATAL finding THROWS. For build/publish
|
|
206
|
+
* gates and tests — anywhere a broken chain should stop the line rather than
|
|
207
|
+
* be noted.
|
|
208
|
+
*/
|
|
209
|
+
export function assertTrackingChainHealthy(
|
|
210
|
+
html: string,
|
|
211
|
+
options: TrackingHealthOptions = {},
|
|
212
|
+
): TrackingHealthReport {
|
|
213
|
+
const report = inspectTrackingChain(html, options);
|
|
214
|
+
if (!report.ok) throw new TrackingChainBrokenError(report, options.surface ?? "this surface");
|
|
215
|
+
return report;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The render-path form: NEVER throws, emits ONE distinct structured line per
|
|
220
|
+
* distinct outcome so a broken surface is visible in Workers Logs. Returns the
|
|
221
|
+
* report so a caller can act on it.
|
|
222
|
+
*
|
|
223
|
+
* Non-throwing on purpose — a tenant whose hand-authored chrome was never
|
|
224
|
+
* stamped should get a loud log line, not a blank page.
|
|
225
|
+
*/
|
|
226
|
+
const reported = new Set<string>();
|
|
227
|
+
/** Bound the dedupe set so a long-lived isolate cannot grow it without limit. */
|
|
228
|
+
const REPORTED_MAX = 64;
|
|
229
|
+
|
|
230
|
+
export function reportTrackingChainHealth(
|
|
231
|
+
html: string,
|
|
232
|
+
options: TrackingHealthOptions = {},
|
|
233
|
+
): TrackingHealthReport {
|
|
234
|
+
const report = inspectTrackingChain(html, options);
|
|
235
|
+
if (report.findings.length === 0) return report;
|
|
236
|
+
const surface = options.surface ?? "unknown-surface";
|
|
237
|
+
const key = `${surface}|${report.findings.map((f) => f.code).join(",")}|${report.actionable}/${report.stamped}`;
|
|
238
|
+
if (reported.has(key)) return report;
|
|
239
|
+
if (reported.size >= REPORTED_MAX) reported.clear();
|
|
240
|
+
reported.add(key);
|
|
241
|
+
for (const finding of report.findings) {
|
|
242
|
+
const line = `[tracking:health] ${finding.fatal ? "BROKEN" : "degraded"} ${finding.code} on ${surface}: ${finding.detail}`;
|
|
243
|
+
if (finding.fatal) console.error(line);
|
|
244
|
+
else console.warn(line);
|
|
245
|
+
}
|
|
246
|
+
return report;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Test-only: forget which lines have already been emitted. */
|
|
250
|
+
export function resetTrackingHealthReportingForTests(): void {
|
|
251
|
+
reported.clear();
|
|
252
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RAW interaction-event table — the schema of a TENANT'S OWN D1 database.
|
|
3
|
+
*
|
|
4
|
+
* PER-TENANT, NOT PER-COLUMN. Every other D1 table in this repo lives in a
|
|
5
|
+
* shared database and carries a `tenant_id` column as its isolation boundary
|
|
6
|
+
* (see `d1/schema-apps.ts`). This one does NOT work that way: each tenant gets
|
|
7
|
+
* its OWN D1 database holding only its own rows, so cross-tenant leakage is
|
|
8
|
+
* impossible BY CONSTRUCTION rather than by a correct `WHERE tenant_id = ?` in
|
|
9
|
+
* every query. A missing predicate here cannot reach another tenant's data —
|
|
10
|
+
* there is no other tenant's data in the file.
|
|
11
|
+
*
|
|
12
|
+
* `tenant_id` is still stored on the row (redundantly, one constant value per
|
|
13
|
+
* database) deliberately: it makes an exported/backed-up/rolled-up row
|
|
14
|
+
* self-describing, and it lets a mis-routed insert be DETECTED (a row whose
|
|
15
|
+
* `tenant_id` disagrees with the database's owner is a routing bug, and
|
|
16
|
+
* `dt-tenant-isolation` can assert on exactly that).
|
|
17
|
+
*
|
|
18
|
+
* WHY D1 AND NOT A SHARED STORE: D1 is not billed per database — thousands of
|
|
19
|
+
* databases cost the same as one — so per-tenant databases are the endorsed
|
|
20
|
+
* shape, not a workaround. The hard limit is 10GB PER DATABASE (~30M rows at a
|
|
21
|
+
* 90-day window for a busy tenant), which is a per-tenant ceiling rather than a
|
|
22
|
+
* global one; `interaction_store_meta` below carries the counters
|
|
23
|
+
* `dt-prune-and-rollup` needs to surface a tenant approaching it.
|
|
24
|
+
*
|
|
25
|
+
* RETENTION is a 90-day window, enforced by a DELETE-based prune in a later
|
|
26
|
+
* unit (`dt-prune-and-rollup`) — D1 has no TTL. This schema only has to make
|
|
27
|
+
* that prune cheap, which is what `idx_interaction_events_ingested_at` is for:
|
|
28
|
+
* `DELETE FROM interaction_events WHERE ingested_at < ?` is an index range
|
|
29
|
+
* scan, not a table scan. `ingested_at` (the SERVER receive clock) is the prune
|
|
30
|
+
* key, never `ts` (a CLIENT clock a beacon can skew arbitrarily).
|
|
31
|
+
*
|
|
32
|
+
* SCHEMA IS APPLIED, NOT MIGRATED. A per-tenant database is created on demand
|
|
33
|
+
* at runtime, so it cannot ride the numbered `migrations-apps/` lineage (which
|
|
34
|
+
* `wrangler d1 migrations apply` runs against ONE named binding). Instead this
|
|
35
|
+
* DDL is idempotent (`IF NOT EXISTS` throughout) and applied at provision time;
|
|
36
|
+
* `INTERACTION_SCHEMA_VERSION` is recorded in `interaction_store_meta` so a
|
|
37
|
+
* future version bump can be detected and re-applied per tenant.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Bumped whenever `INTERACTION_D1_SCHEMA` changes; recorded per database. */
|
|
41
|
+
export const INTERACTION_SCHEMA_VERSION = 1;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Idempotent DDL for one tenant's raw-interaction database. Multi-statement:
|
|
45
|
+
* applied via `Queryable.exec`, which both the D1 binding and the D1 HTTP API
|
|
46
|
+
* accept as a semicolon-separated script.
|
|
47
|
+
*/
|
|
48
|
+
export const INTERACTION_D1_SCHEMA = `
|
|
49
|
+
CREATE TABLE IF NOT EXISTS interaction_events (
|
|
50
|
+
-- Owning tenant. One constant value per database (see the module note): the
|
|
51
|
+
-- isolation boundary is the DATABASE, this column is provenance + a
|
|
52
|
+
-- mis-routing tripwire.
|
|
53
|
+
tenant_id TEXT NOT NULL,
|
|
54
|
+
-- Stream discriminator, always "interaction" today. Present from row one so a
|
|
55
|
+
-- second stream can land in the same table without a migration.
|
|
56
|
+
stream TEXT NOT NULL,
|
|
57
|
+
-- The six TotInteraction fields, flattened for SQL aggregation.
|
|
58
|
+
type TEXT NOT NULL,
|
|
59
|
+
el TEXT NOT NULL,
|
|
60
|
+
action TEXT,
|
|
61
|
+
placement TEXT,
|
|
62
|
+
scope TEXT,
|
|
63
|
+
idx INTEGER,
|
|
64
|
+
-- Groups the events of one collector lifetime (~one page load).
|
|
65
|
+
correlation_id TEXT NOT NULL,
|
|
66
|
+
-- Monotonic within correlation_id, assigned by the client collector.
|
|
67
|
+
seq INTEGER NOT NULL,
|
|
68
|
+
-- Client emit clock (ISO-8601). Informational: never the prune or ordering key.
|
|
69
|
+
ts TEXT NOT NULL,
|
|
70
|
+
-- Server receive clock (ISO-8601). THE retention/prune key.
|
|
71
|
+
ingested_at TEXT NOT NULL,
|
|
72
|
+
-- (correlation_id, seq) is the collector's own natural identity, so a
|
|
73
|
+
-- retried/duplicated beacon re-inserting the same event is a no-op under
|
|
74
|
+
-- INSERT OR IGNORE rather than a double-count.
|
|
75
|
+
PRIMARY KEY (correlation_id, seq)
|
|
76
|
+
);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_interaction_events_ingested_at
|
|
78
|
+
ON interaction_events (ingested_at);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_interaction_events_stream_ingested_at
|
|
80
|
+
ON interaction_events (stream, ingested_at);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_interaction_events_el
|
|
82
|
+
ON interaction_events (el, ingested_at);
|
|
83
|
+
CREATE TABLE IF NOT EXISTS interaction_store_meta (
|
|
84
|
+
-- Single-row table. The constant primary key makes that a schema invariant.
|
|
85
|
+
-- (Keep every inline comment free of semicolons: this DDL is executed as one
|
|
86
|
+
-- semicolon-separated script.)
|
|
87
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
88
|
+
tenant_id TEXT NOT NULL,
|
|
89
|
+
schema_version INTEGER NOT NULL,
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
updated_at TEXT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
/** Statements that stamp/refresh the single `interaction_store_meta` row. */
|
|
96
|
+
export function interactionMetaUpsert(tenantId: string, at: string): {
|
|
97
|
+
sql: string;
|
|
98
|
+
params: readonly unknown[];
|
|
99
|
+
} {
|
|
100
|
+
return {
|
|
101
|
+
sql: `INSERT INTO interaction_store_meta (id, tenant_id, schema_version, created_at, updated_at)
|
|
102
|
+
VALUES (1, ?, ?, ?, ?)
|
|
103
|
+
ON CONFLICT(id) DO UPDATE SET schema_version = excluded.schema_version,
|
|
104
|
+
updated_at = excluded.updated_at`,
|
|
105
|
+
params: [tenantId, INTERACTION_SCHEMA_VERSION, at, at],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
Binary file
|