@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
package/pnpm-workspace.yaml
CHANGED
|
@@ -168,6 +168,25 @@ function validateRawHtmlBody(html) {
|
|
|
168
168
|
// step with packages/cli/src/validate.mjs's identical copy.
|
|
169
169
|
const CHROME_HEADER_VARIANTS = new Set(["primary", "minimal"]);
|
|
170
170
|
const CHROME_FOOTER_VARIANTS = new Set(["default"]);
|
|
171
|
+
/**
|
|
172
|
+
* The declarative-interaction-tracking rule, mirrored: every actionable item —
|
|
173
|
+
* an object carrying both `id` and `href` — declares EXACTLY ONE of a non-empty
|
|
174
|
+
* `action` (its stable intent) or `tracking: "none"` (an explicit opt-out).
|
|
175
|
+
* Declaring neither or both is a violation, and nothing is derived from `id`.
|
|
176
|
+
* Contract: docs/architecture/interaction-tracking-attribute-contract.md
|
|
177
|
+
*/
|
|
178
|
+
function chromeTrackingDeclared(value) {
|
|
179
|
+
if (Array.isArray(value)) return value.every(chromeTrackingDeclared);
|
|
180
|
+
if (value == null || typeof value !== "object") return true;
|
|
181
|
+
if (typeof value.id === "string" && typeof value.href === "string") {
|
|
182
|
+
const hasAction = typeof value.action === "string" && value.action.trim() !== "";
|
|
183
|
+
const optedOut = value.tracking === "none";
|
|
184
|
+
if (hasAction === optedOut) return false;
|
|
185
|
+
if (value.tracking !== undefined && !optedOut) return false;
|
|
186
|
+
}
|
|
187
|
+
return Object.values(value).every(chromeTrackingDeclared);
|
|
188
|
+
}
|
|
189
|
+
|
|
171
190
|
export function looksLikeChromeConfig(value) {
|
|
172
191
|
if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
173
192
|
const header = value.header;
|
|
@@ -180,6 +199,7 @@ export function looksLikeChromeConfig(value) {
|
|
|
180
199
|
if (footer == null || typeof footer !== "object" || Array.isArray(footer)) return false;
|
|
181
200
|
if (!CHROME_FOOTER_VARIANTS.has(footer.variant)) return false;
|
|
182
201
|
if (!Array.isArray(footer.columns)) return false;
|
|
202
|
+
if (!chromeTrackingDeclared(value)) return false;
|
|
183
203
|
return true;
|
|
184
204
|
}
|
|
185
205
|
|
|
@@ -410,6 +430,278 @@ function validateHomeDoc(doc, file) {
|
|
|
410
430
|
return out;
|
|
411
431
|
}
|
|
412
432
|
|
|
433
|
+
// --- CTA / actionable-identity advisory rules (unit dt-validator-cta-rules) -
|
|
434
|
+
// Three ADVISORY warnings (never block) that a schema pass alone doesn't
|
|
435
|
+
// reach. `validateChromeConfig` (@tot/public-runtime chrome.ts) already
|
|
436
|
+
// HARD-FAILS the governed chrome.json path on the either/or action/
|
|
437
|
+
// tracking:"none" rule (interaction-tracking.ts validateTracking) — this
|
|
438
|
+
// validator does not re-run that schema (deep per-item errors are the
|
|
439
|
+
// write-path's job, per the looksLikeChromeConfig comment above). These three
|
|
440
|
+
// catch what the schema pass doesn't:
|
|
441
|
+
// - `duplicate-cta` — a copy-paste mistake that still PASSES schema (two
|
|
442
|
+
// actionable items sharing one governed identity).
|
|
443
|
+
// - `cta-missing-id` — tenant-authored raw HTML fragments carry NO
|
|
444
|
+
// `data-tot-*` at all pre-derivation (see docs/architecture/interaction-
|
|
445
|
+
// tracking-attribute-contract.md "Deriving data-tot-el") — there is no
|
|
446
|
+
// schema here for validateChromeConfig to fail.
|
|
447
|
+
// - `cta-id-drift` — `data-tot-el` is a PERMANENT identity per
|
|
448
|
+
// dt-contract-v1's decision ledger ("a change here is DRIFT and is a
|
|
449
|
+
// defect"); this one needs a PRIOR version to compare against, which a
|
|
450
|
+
// single-snapshot validateTenant() run doesn't have on its own — see
|
|
451
|
+
// `opts.previousChromeJson`, below.
|
|
452
|
+
|
|
453
|
+
// --- "assign stable tracking ids" — the one named, actionable step (unit
|
|
454
|
+
// dt-assign-ids-step). The three rules above are DETECTION; this is the single
|
|
455
|
+
// place every touchpoint (`tot validate`, `tot submit`/`tot preview`, and the
|
|
456
|
+
// `tot dev` save-loop nudge — scripts/dev/checkout-watch.mjs) points an author
|
|
457
|
+
// at the SAME fix in the SAME words, so it reads as one coherent nudge no
|
|
458
|
+
// matter which command surfaced it. The mechanism already exists (derivation:
|
|
459
|
+
// dt-id-derivation; writeback: dt-id-writeback,
|
|
460
|
+
// apps/storefront/src/pages/api/admin/tracking-writeback.ts) — this names the
|
|
461
|
+
// step and points at it, it invents nothing. Kept in step with
|
|
462
|
+
// packages/cli/src/validate.mjs's identical copy of this block.
|
|
463
|
+
export const TRACKING_ID_STEP_NAME = "assign stable tracking ids";
|
|
464
|
+
export const CTA_MISSING_ID_FIX =
|
|
465
|
+
"assign stable tracking ids: GET/POST /api/admin/tracking-writeback proposes a data-tot-el for every element that's missing one, as a reviewable candidate PR (GET first for a dry-run preview) — or run `tot validate` to see the full list before you submit.";
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The advisory rules LOUD enough to call out by NAME at `tot validate` / `tot
|
|
469
|
+
* submit` / `tot preview` — none of them block, but each ships a page that
|
|
470
|
+
* looks perfectly healthy (clean build, every other check green) while
|
|
471
|
+
* quietly losing something. `[rule, headline, what it costs if ignored]`.
|
|
472
|
+
* ONE place so every surface prints identical wording; `printLoudAdvisories`
|
|
473
|
+
* is the shared renderer.
|
|
474
|
+
*/
|
|
475
|
+
export const LOUD_ADVISORY_RULES = [
|
|
476
|
+
[
|
|
477
|
+
"git-conflict-markers",
|
|
478
|
+
"git conflict markers in submitted content — an unfinished merge/rebase?",
|
|
479
|
+
"The preview will still build, but it will serve the broken markers. Resolve before shipping.",
|
|
480
|
+
],
|
|
481
|
+
[
|
|
482
|
+
"duplicate-skip-link",
|
|
483
|
+
"duplicate skip link — your chrome already supplies one",
|
|
484
|
+
"The page renders fine and every automated check passes; a screen-reader user hears it twice.",
|
|
485
|
+
],
|
|
486
|
+
[
|
|
487
|
+
"duplicate-cta",
|
|
488
|
+
"duplicate CTA identity — two actionable items share one data-tot-el",
|
|
489
|
+
"Both elements still work, but the click listener and every downstream analytics query treat them as ONE interaction — you lose the ability to tell them apart.",
|
|
490
|
+
],
|
|
491
|
+
[
|
|
492
|
+
"cta-missing-id",
|
|
493
|
+
`actionable element with no governed identity (data-tot-el) — needs to ${TRACKING_ID_STEP_NAME}`,
|
|
494
|
+
`It renders and works fine, but it's invisible to interaction tracking — nothing about it is ever recorded. ${CTA_MISSING_ID_FIX}`,
|
|
495
|
+
],
|
|
496
|
+
[
|
|
497
|
+
"cta-id-drift",
|
|
498
|
+
"a previously-declared data-tot-el changed value",
|
|
499
|
+
"The page still works, but everything already recorded under the old id is now orphaned — this identity is supposed to be permanent.",
|
|
500
|
+
],
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Print the LOUD-but-advisory findings as named, grouped callouts — see
|
|
505
|
+
* packages/cli/src/validate.mjs's identical copy for the full rationale.
|
|
506
|
+
* @param {Finding[]} findings
|
|
507
|
+
* @param {{ log?: (s: string) => void }} [opts]
|
|
508
|
+
*/
|
|
509
|
+
export function printLoudAdvisories(findings, { log = (s) => console.error(s) } = {}) {
|
|
510
|
+
for (const [rule, headline, consequence] of LOUD_ADVISORY_RULES) {
|
|
511
|
+
const hits = findings.filter((f) => f.rule === rule);
|
|
512
|
+
if (!hits.length) continue;
|
|
513
|
+
log(`\n⚠ ${headline} (${hits.length} finding(s))`);
|
|
514
|
+
for (const f of hits) log(` ⚠ ${f.file} — ${f.message}`);
|
|
515
|
+
log(` ${consequence}\n`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Every declared actionable `id` in a ChromeConfig, with a path for
|
|
520
|
+
* messages — the SAME field rawChrome.ts / SiteHeader.astro stamp verbatim as
|
|
521
|
+
* `data-tot-el` (ctaIdentityAttr for header.ctas; the direct `id` field on
|
|
522
|
+
* every other actionable shape — see @tot/public-runtime chrome.ts's
|
|
523
|
+
* ChromeLink/ChromeNavItem/ChromeCta/ChromeActionLink), so a collision here
|
|
524
|
+
* is a collision of GOVERNED IDENTITY, not just of an authoring label. Walks
|
|
525
|
+
* the same shapes validateChromeConfig itself walks. Pure.
|
|
526
|
+
*/
|
|
527
|
+
function collectChromeActionableIds(config) {
|
|
528
|
+
const out = [];
|
|
529
|
+
const push = (id, path) => {
|
|
530
|
+
if (typeof id === "string" && id.trim()) out.push({ id: id.trim(), path });
|
|
531
|
+
};
|
|
532
|
+
const header = config?.header;
|
|
533
|
+
if (header && typeof header === "object" && !Array.isArray(header)) {
|
|
534
|
+
for (const [i, n] of (Array.isArray(header.nav) ? header.nav : []).entries()) {
|
|
535
|
+
if (n == null || typeof n !== "object") continue;
|
|
536
|
+
push(n.id, `header.nav[${i}]`);
|
|
537
|
+
for (const [j, c] of (Array.isArray(n.children) ? n.children : []).entries()) {
|
|
538
|
+
if (c && typeof c === "object") push(c.id, `header.nav[${i}].children[${j}]`);
|
|
539
|
+
}
|
|
540
|
+
for (const [j, col] of (Array.isArray(n.columns) ? n.columns : []).entries()) {
|
|
541
|
+
if (col == null || typeof col !== "object") continue;
|
|
542
|
+
for (const [k, l] of (Array.isArray(col.links) ? col.links : []).entries()) {
|
|
543
|
+
if (l && typeof l === "object") push(l.id, `header.nav[${i}].columns[${j}].links[${k}]`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
for (const [i, c] of (Array.isArray(header.ctas) ? header.ctas : []).entries()) {
|
|
548
|
+
if (c && typeof c === "object") push(c.id, `header.ctas[${i}]`);
|
|
549
|
+
}
|
|
550
|
+
if (header.memberCue && typeof header.memberCue === "object") push(header.memberCue.id, "header.memberCue");
|
|
551
|
+
const u = header.utilityNav;
|
|
552
|
+
if (u && typeof u === "object" && !Array.isArray(u)) {
|
|
553
|
+
for (const [i, l] of (Array.isArray(u.links) ? u.links : []).entries()) {
|
|
554
|
+
if (l && typeof l === "object") push(l.id, `header.utilityNav.links[${i}]`);
|
|
555
|
+
}
|
|
556
|
+
if (u.memberCue && typeof u.memberCue === "object") push(u.memberCue.id, "header.utilityNav.memberCue");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const footer = config?.footer;
|
|
560
|
+
if (footer && typeof footer === "object" && !Array.isArray(footer)) {
|
|
561
|
+
for (const [i, col] of (Array.isArray(footer.columns) ? footer.columns : []).entries()) {
|
|
562
|
+
if (col == null || typeof col !== "object") continue;
|
|
563
|
+
for (const [j, l] of (Array.isArray(col.links) ? col.links : []).entries()) {
|
|
564
|
+
if (l && typeof l === "object") push(l.id, `footer.columns[${i}].links[${j}]`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** `duplicate-cta`: two or more actionable items in one ChromeConfig resolve
|
|
572
|
+
* to the SAME governed identity. Almost always a copy-paste authoring
|
|
573
|
+
* mistake — the click listener's `closest("[data-tot-el]")` match and every
|
|
574
|
+
* downstream analytics query assume one element per id, so a duplicate
|
|
575
|
+
* silently merges two distinct interactions' data. WARN, never blocks: the
|
|
576
|
+
* page still renders and both elements still work.
|
|
577
|
+
*/
|
|
578
|
+
function findDuplicateCtaIds(config, file) {
|
|
579
|
+
const byId = new Map();
|
|
580
|
+
for (const { id, path } of collectChromeActionableIds(config)) {
|
|
581
|
+
if (!byId.has(id)) byId.set(id, []);
|
|
582
|
+
byId.get(id).push(path);
|
|
583
|
+
}
|
|
584
|
+
const out = [];
|
|
585
|
+
for (const [id, paths] of byId) {
|
|
586
|
+
if (paths.length > 1) {
|
|
587
|
+
out.push(
|
|
588
|
+
mk(WARN, "duplicate-cta", file,
|
|
589
|
+
`id "${id}" is declared on ${paths.length} actionable items (${paths.join(", ")}) — data-tot-el must be unique per element; the click listener's closest() match and downstream analytics both assume one`,
|
|
590
|
+
"give every occurrence but one a distinct id"),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* `cta-id-drift`: a governed actionable item's `data-tot-el` changed value
|
|
599
|
+
* between two versions of the SAME chrome.json, matched by structural
|
|
600
|
+
* position (e.g. "header.ctas[0]") — the same slot in both versions. Per
|
|
601
|
+
* dt-contract-v1's ledger `data-tot-el` is a PERMANENT identity ("a change
|
|
602
|
+
* here is DRIFT and is a defect"), unlike `data-tot-placement` which is
|
|
603
|
+
* expected to move on redesign.
|
|
604
|
+
*
|
|
605
|
+
* `previous` is an OPTIONAL caller-supplied prior chrome.json (already
|
|
606
|
+
* parsed) — this static, single-snapshot validator has no version history of
|
|
607
|
+
* its own, and no existing publish/preview utility in this repo diffs the
|
|
608
|
+
* CTA IDENTITY LIST across versions (apps/storefront/src/lib/publish and
|
|
609
|
+
* .../preview diff release ARTIFACT BYTES for the release panel, never this)
|
|
610
|
+
* to hook into instead. A version-aware caller (a future publish-time check,
|
|
611
|
+
* `tot validate --against <ref>`) supplies `previous`; this run alone never
|
|
612
|
+
* invents one, so omitting it is a silent no-op, not a missing check.
|
|
613
|
+
*
|
|
614
|
+
* A reorder/insert that shifts array positions can produce a false
|
|
615
|
+
* positive/negative here (position, not the item's own identity, is the
|
|
616
|
+
* match key) — a known limitation of matching without a stronger anchor.
|
|
617
|
+
*/
|
|
618
|
+
function findCtaIdDrift(previous, current, file) {
|
|
619
|
+
if (previous == null || typeof previous !== "object") return [];
|
|
620
|
+
const before = new Map(collectChromeActionableIds(previous).map((e) => [e.path, e.id]));
|
|
621
|
+
const out = [];
|
|
622
|
+
for (const { id, path } of collectChromeActionableIds(current)) {
|
|
623
|
+
const priorId = before.get(path);
|
|
624
|
+
if (priorId !== undefined && priorId !== id) {
|
|
625
|
+
out.push(
|
|
626
|
+
mk(WARN, "cta-id-drift", file,
|
|
627
|
+
`${path}.id changed from "${priorId}" to "${id}" — data-tot-el is a permanent identity; a change here is drift, not a redesign, and breaks continuity with everything already recorded under the old id`,
|
|
628
|
+
"keep the original id (a deliberate identity migration is a separate, tracked decision, not an incidental edit)"),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return out;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Compact mirror of @tot/public-runtime tracking-id-derivation.ts's trackable-
|
|
636
|
+
// tag set and `isWellFormedTrackingId` shape check — this script can't import
|
|
637
|
+
// that TS package at runtime (same published-CLI/no-build-step constraint as
|
|
638
|
+
// the chrome-shape mirrors above). Deliberately simplified: full structural
|
|
639
|
+
// derivation (region path, confidence) needs a real parser and an ancestor
|
|
640
|
+
// stack; this is a flat regex tag scan (the same style as HREF_RE/SRC_RE
|
|
641
|
+
// above), so it only answers "does this element carry a well-formed
|
|
642
|
+
// data-tot-el at all", not "what would a good one look like".
|
|
643
|
+
const ACTIONABLE_TAG_RE = /<([a-z][a-z0-9-]*)\b([^>]*)>/gi;
|
|
644
|
+
const TRACKABLE_HTML_TAGS = new Set(["a", "area", "button", "details", "summary", "form", "input", "select", "textarea", "label"]);
|
|
645
|
+
const TRACKABLE_HTML_ROLES = new Set(["button", "link", "tab", "menuitem", "switch", "checkbox"]);
|
|
646
|
+
|
|
647
|
+
function htmlAttr(attrs, name) {
|
|
648
|
+
const m = new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`, "i").exec(attrs);
|
|
649
|
+
return m ? m[1] : undefined;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Mirror of tracking-id-derivation.ts's `isTrackableNode`: an `<a>`/`<area>`
|
|
653
|
+
* needs a real `href`; the fixed tag set is always a candidate; anything
|
|
654
|
+
* else needs an interactive ARIA role. */
|
|
655
|
+
function isTrackableHtmlTag(tag, attrs) {
|
|
656
|
+
if (tag === "a" || tag === "area") {
|
|
657
|
+
const href = htmlAttr(attrs, "href");
|
|
658
|
+
return typeof href === "string" && href.trim().length > 0;
|
|
659
|
+
}
|
|
660
|
+
if (TRACKABLE_HTML_TAGS.has(tag)) return true;
|
|
661
|
+
const role = htmlAttr(attrs, "role");
|
|
662
|
+
return typeof role === "string" && TRACKABLE_HTML_ROLES.has(role.trim().toLowerCase());
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Mirror of tracking-id-derivation.ts's `isWellFormedTrackingId`: lowercase
|
|
666
|
+
* kebab tokens, non-empty, within the same 64-char budget. */
|
|
667
|
+
function isWellFormedCtaId(el) {
|
|
668
|
+
return typeof el === "string" && el.length > 0 && el.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(el);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* `cta-missing-id`: a tenant-authored HTML fragment's actionable element
|
|
673
|
+
* carries no well-formed `data-tot-el`. Tenant fragments ship NO `data-tot-*`
|
|
674
|
+
* at all pre-derivation (docs/architecture/interaction-tracking-attribute-
|
|
675
|
+
* contract.md "Deriving data-tot-el") — `validateChromeConfig`'s hard
|
|
676
|
+
* either/or failure only reaches the SCHEMA-GOVERNED chrome.json path, never
|
|
677
|
+
* a raw fragment, so an un-identified CTA here would otherwise ship silently
|
|
678
|
+
* unmeasurable: the page renders fine, every other check passes, and the
|
|
679
|
+
* element is simply invisible to interaction tracking. WARN, never blocks.
|
|
680
|
+
*
|
|
681
|
+
* Exported so the `tot dev` save-loop nudge (scripts/dev/checkout-watch.mjs)
|
|
682
|
+
* can reuse the SAME detection on a single saved file, in-process, rather
|
|
683
|
+
* than re-implementing it — dt-assign-ids-step is a discoverability/wiring
|
|
684
|
+
* unit, not a new detector.
|
|
685
|
+
*/
|
|
686
|
+
export function findMissingCtaIds(html, file) {
|
|
687
|
+
const out = [];
|
|
688
|
+
ACTIONABLE_TAG_RE.lastIndex = 0;
|
|
689
|
+
let m;
|
|
690
|
+
while ((m = ACTIONABLE_TAG_RE.exec(html))) {
|
|
691
|
+
const tag = m[1].toLowerCase();
|
|
692
|
+
const attrs = m[2] || "";
|
|
693
|
+
if (!isTrackableHtmlTag(tag, attrs)) continue;
|
|
694
|
+
const totEl = htmlAttr(attrs, "data-tot-el");
|
|
695
|
+
if (isWellFormedCtaId(totEl)) continue;
|
|
696
|
+
out.push(
|
|
697
|
+
mk(WARN, "cta-missing-id", file,
|
|
698
|
+
`a <${tag}> actionable element has ${totEl ? `a malformed data-tot-el (${JSON.stringify(totEl)})` : "no data-tot-el"} — it renders and functions but is invisible to interaction tracking`,
|
|
699
|
+
CTA_MISSING_ID_FIX),
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
|
|
413
705
|
// --- filesystem helpers ------------------------------------------------------
|
|
414
706
|
function readJsonSafe(path) {
|
|
415
707
|
try {
|
|
@@ -513,10 +805,13 @@ function resolvePlatformRouteOwnership(config, hostIsPlatform) {
|
|
|
513
805
|
/**
|
|
514
806
|
* Full static validation of a tenant directory.
|
|
515
807
|
* @param {string} tenantDir absolute path to the tenant dir (content/ public/ theme.json [.tot/])
|
|
516
|
-
* @param {{tenantId?:string, scope?:string, mode?:"monorepo"|"workspace"}} [opts]
|
|
808
|
+
* @param {{tenantId?:string, scope?:string, mode?:"monorepo"|"workspace", previousChromeJson?:any}} [opts]
|
|
517
809
|
* `mode` — "monorepo" (default): served by this platform, so commerce tenants own
|
|
518
810
|
* the framework routes. "workspace": a standalone checkout, conservative about
|
|
519
811
|
* platform-route ownership unless the config resolves the host.
|
|
812
|
+
* `previousChromeJson` — an already-parsed PRIOR version of content/chrome.json,
|
|
813
|
+
* supplied by a version-aware caller, so the `cta-id-drift` check has something
|
|
814
|
+
* to compare against (see findCtaIdDrift above). Omitted ⇒ that check is a no-op.
|
|
520
815
|
* @returns {{ok:boolean, findings:Finding[]}}
|
|
521
816
|
*/
|
|
522
817
|
export function validateTenant(tenantDir, opts = {}) {
|
|
@@ -615,6 +910,11 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
615
910
|
findings.push(mk(ERROR, "content-json-parse", `content/${name}`, `invalid JSON: ${error} (fails the build)`));
|
|
616
911
|
} else if (name === "home.json") {
|
|
617
912
|
findings.push(...validateHomeDoc(value, "content/home.json"));
|
|
913
|
+
} else if (name === "chrome.json") {
|
|
914
|
+
findings.push(...findDuplicateCtaIds(value, "content/chrome.json"));
|
|
915
|
+
if (opts.previousChromeJson != null) {
|
|
916
|
+
findings.push(...findCtaIdDrift(opts.previousChromeJson, value, "content/chrome.json"));
|
|
917
|
+
}
|
|
618
918
|
}
|
|
619
919
|
}
|
|
620
920
|
}
|
|
@@ -667,6 +967,9 @@ export function validateTenant(tenantDir, opts = {}) {
|
|
|
667
967
|
"use a bare <style> tag"),
|
|
668
968
|
);
|
|
669
969
|
}
|
|
970
|
+
// actionable elements with no (or malformed) governed identity — advisory,
|
|
971
|
+
// never blocking (see findMissingCtaIds above).
|
|
972
|
+
findings.push(...findMissingCtaIds(html, r));
|
|
670
973
|
|
|
671
974
|
// link portability + resolution
|
|
672
975
|
for (const m of html.matchAll(HREF_RE)) {
|
|
@@ -826,6 +1129,8 @@ if (isMain) {
|
|
|
826
1129
|
const errors = findings.filter((f) => f.level === ERROR);
|
|
827
1130
|
const warns = findings.filter((f) => f.level === WARN);
|
|
828
1131
|
console.log(`\ntot validate — ${tenantId ?? dir}\n`);
|
|
1132
|
+
// Named, grouped callouts FIRST — see printLoudAdvisories above.
|
|
1133
|
+
printLoudAdvisories(findings, { log: (s) => console.log(s) });
|
|
829
1134
|
for (const f of findings) {
|
|
830
1135
|
const tag = f.level === ERROR ? "✗" : "⚠";
|
|
831
1136
|
console.log(` ${tag} [${f.rule}] ${f.file}\n ${f.message}${f.fix ? `\n → ${f.fix}` : ""}`);
|
package/scripts/tot-dev.mjs
CHANGED
|
@@ -65,6 +65,7 @@ import { resolveDevSourceRoot } from "./dev/tenant-source-root.mjs";
|
|
|
65
65
|
import { tenantDirRelative, tenantDirSegments } from "./lib/tenant-dirs.mjs";
|
|
66
66
|
import { firstFreePort } from "./dev/port-check.mjs";
|
|
67
67
|
import { resync, startWatch } from "./dev/checkout-watch.mjs";
|
|
68
|
+
import { findMissingCtaIds } from "./tenant/validate.mjs";
|
|
68
69
|
|
|
69
70
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
70
71
|
const repoRoot = resolve(here, ".."); // scripts/ -> repo root
|
|
@@ -169,6 +170,42 @@ const grafts = [];
|
|
|
169
170
|
const watchers = [];
|
|
170
171
|
let cleanedUp = false;
|
|
171
172
|
|
|
173
|
+
// --- dev-loop nudge: "assign stable tracking ids" (unit dt-assign-ids-step) ---
|
|
174
|
+
// The save→reload loop is a point the developer is ALREADY paying attention to
|
|
175
|
+
// (they just edited and are watching for the reload) — the same named,
|
|
176
|
+
// actionable step surfaced at `tot validate` / `tot submit` / `tot preview`
|
|
177
|
+
// (scripts/tenant/validate.mjs's CTA_MISSING_ID_FIX / LOUD_ADVISORY_RULES)
|
|
178
|
+
// belongs here too, in the SAME words, or a developer sees a different warning
|
|
179
|
+
// depending on which command happened to notice first. Reuses that module's
|
|
180
|
+
// `findMissingCtaIds` detector directly — this unit wires an existing detector
|
|
181
|
+
// into a new touchpoint, it does not invent a new one. Advisory only: a
|
|
182
|
+
// scan failure (unreadable file, mid-write) never blocks the reload.
|
|
183
|
+
const trackingIdNudged = new Set();
|
|
184
|
+
function nudgeTrackingIds(absPath, label) {
|
|
185
|
+
if (!/\.html?$/i.test(absPath)) return;
|
|
186
|
+
let html;
|
|
187
|
+
try {
|
|
188
|
+
html = readFileSync(absPath, "utf8");
|
|
189
|
+
} catch {
|
|
190
|
+
return; // deleted mid-scan, or a transient editor write — never block the reload over this
|
|
191
|
+
}
|
|
192
|
+
const findings = findMissingCtaIds(html, label);
|
|
193
|
+
if (!findings.length) {
|
|
194
|
+
trackingIdNudged.delete(label); // fixed — nudge again if it regresses later
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// One nudge per file per session while it stays unfixed, not once per keystroke-driven save.
|
|
198
|
+
if (trackingIdNudged.has(label)) return;
|
|
199
|
+
trackingIdNudged.add(label);
|
|
200
|
+
console.log(
|
|
201
|
+
`\n ⚠ assign stable tracking ids — ${findings.length} actionable element(s) in ${label} have no data-tot-el`,
|
|
202
|
+
);
|
|
203
|
+
console.log(` They render and work fine, but stay invisible to interaction tracking until identified.`);
|
|
204
|
+
console.log(
|
|
205
|
+
` Assign them: GET/POST /api/admin/tracking-writeback proposes ids for review as a candidate PR, or run \`tot validate\` for the full list.\n`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
172
209
|
function isSymlink(p) {
|
|
173
210
|
try {
|
|
174
211
|
return lstatSync(p).isSymbolicLink();
|
|
@@ -275,7 +312,10 @@ if (workspacePath) {
|
|
|
275
312
|
// `startWatch` polls by default (reliable — native fs.watch dropped editor
|
|
276
313
|
// atomic-saves on macOS); TOT_DEV_NATIVE_WATCH=1 opts into event-driven watch.
|
|
277
314
|
try {
|
|
278
|
-
const w = startWatch(source, isDir, (rel) =>
|
|
315
|
+
const w = startWatch(source, isDir, (rel) => {
|
|
316
|
+
resync(source, target, rel);
|
|
317
|
+
nudgeTrackingIds(isDir ? join(source, rel) : source, rel ? `${wsRel}/${rel}` : wsRel);
|
|
318
|
+
});
|
|
279
319
|
watchers.push(w);
|
|
280
320
|
} catch (e) {
|
|
281
321
|
console.warn(` ⚠ could not watch ${wsRel} (edits won't hot-reload): ${e?.message}`);
|
|
@@ -289,8 +329,24 @@ if (workspacePath) {
|
|
|
289
329
|
cleanupGrafts();
|
|
290
330
|
process.exit(1);
|
|
291
331
|
}
|
|
292
|
-
} else
|
|
293
|
-
|
|
332
|
+
} else {
|
|
333
|
+
// MONOREPO mode: content/ is already in place (nothing grafted), so nothing
|
|
334
|
+
// else here watches it at the JS level — astro's own Vite HMR reloads on save
|
|
335
|
+
// silently. Add a lightweight watch for the SAME "assign stable tracking ids"
|
|
336
|
+
// nudge the standalone graft loop gets above (see nudgeTrackingIds).
|
|
337
|
+
const monorepoContentDir = resolve(repoRoot, ...tenantDirSegments(tenant), "content");
|
|
338
|
+
if (!existsSync(monorepoContentDir)) {
|
|
339
|
+
console.warn(`⚠ no tenants/${tenant}/content/ found — the tenant may not be registered yet.`);
|
|
340
|
+
} else {
|
|
341
|
+
try {
|
|
342
|
+
const w = startWatch(monorepoContentDir, true, (rel) => {
|
|
343
|
+
if (rel) nudgeTrackingIds(join(monorepoContentDir, rel), `content/${rel}`);
|
|
344
|
+
});
|
|
345
|
+
watchers.push(w);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
console.warn(` ⚠ could not watch tenants/${tenant}/content/ for the tracking-id nudge: ${e?.message}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
294
350
|
}
|
|
295
351
|
|
|
296
352
|
// Resolve the port BEFORE printing a URL or booting astro. Vite's `strictPort`
|
|
@@ -1,90 +1,17 @@
|
|
|
1
|
-
/* Storefront early-access landing — interactions
|
|
1
|
+
/* Storefront early-access landing — interactions.
|
|
2
2
|
* Served at /tenants/home/js/storefront.js (mirrored from tenants/home/public/).
|
|
3
|
-
|
|
4
|
-
* ---- tot20 analytics: public reportAnalytics endpoint --------------------
|
|
5
|
-
* GET /reportAnalytics/{origin}/{pluginVersion}/{subject}/{action}?...
|
|
6
|
-
* origin=totcom => anonymous marketing path (user_id null, device-id based).
|
|
7
|
-
* apiKeyNotRequired + allowCORs, so we fire a fire-and-forget GET beacon.
|
|
8
|
-
*
|
|
9
|
-
* Beacon transport is an Image() GET on purpose: the storefront CSP is
|
|
10
|
-
* `img-src https:` (allowed) but `connect-src 'self'` (a cross-origin fetch to
|
|
11
|
-
* qa.tokenoftrust.com would be BLOCKED). An <img> GET sidesteps connect-src.
|
|
12
|
-
*
|
|
13
|
-
* Detail rides in utm_content/utm_campaign (the endpoint's eventProperties).
|
|
14
|
-
* Events use subject=cust and are shaped onto two EXISTING registered wildcards
|
|
15
|
-
* in tot20's analytics schema (analyticEvents.json), so they validate (HTTP 200)
|
|
16
|
-
* with no schema change:
|
|
17
|
-
* - views → cust_viewed_totcom_page_* (the anonymous tokenoftrust.com convention)
|
|
18
|
-
* - clicks → cust_clicked_btn_*
|
|
19
|
-
* ------------------------------------------------------------------------- */
|
|
3
|
+
*/
|
|
20
4
|
(function () {
|
|
21
|
-
|
|
22
|
-
const ORIGIN = "totcom", PLUGIN = "1.0", SUBJECT = "cust", APP_DOMAIN = "tokenoftrust.com";
|
|
23
|
-
|
|
24
|
-
function deviceId() {
|
|
25
|
-
try {
|
|
26
|
-
const k = "ampDeviceId";
|
|
27
|
-
let v = localStorage.getItem(k);
|
|
28
|
-
if (!v) { v = (crypto.randomUUID ? crypto.randomUUID() : String(Date.now()) + Math.random()); localStorage.setItem(k, v); }
|
|
29
|
-
return v;
|
|
30
|
-
} catch (e) { return "anon"; }
|
|
31
|
-
}
|
|
32
|
-
function track(action, detail, campaign) {
|
|
33
|
-
try {
|
|
34
|
-
const qs = new URLSearchParams({
|
|
35
|
-
appDomain: APP_DOMAIN, ampDeviceId: deviceId(),
|
|
36
|
-
title: document.title, referrer: document.referrer || "",
|
|
37
|
-
lang: navigator.language || "", screen_w: String(screen.width), screen_h: String(screen.height)
|
|
38
|
-
});
|
|
39
|
-
if (detail) qs.set("utm_content", detail);
|
|
40
|
-
qs.set("utm_campaign", campaign || "storefront_early_access");
|
|
41
|
-
qs.set("utm_source", "storefront-landing"); qs.set("utm_medium", "web");
|
|
42
|
-
const url = BASE + "/reportAnalytics/" + ORIGIN + "/" + PLUGIN + "/" + SUBJECT + "/" + encodeURIComponent(action) + "?" + qs.toString();
|
|
43
|
-
// Image() GET beacon — allowed by img-src https:; a fetch would hit connect-src 'self'.
|
|
44
|
-
new Image().src = url;
|
|
45
|
-
} catch (e) { /* fire-and-forget */ }
|
|
46
|
-
}
|
|
47
|
-
window.__totTrack = track;
|
|
48
|
-
|
|
49
|
-
// 1) page view
|
|
50
|
-
track("viewed_totcom_page_storefront_early_access");
|
|
51
|
-
|
|
52
|
-
// 2) every click on a labeled control (captures all CTAs, nav, deck links, login)
|
|
53
|
-
document.addEventListener("click", function (e) {
|
|
54
|
-
const el = e.target.closest("[data-ev]");
|
|
55
|
-
if (el) { const l = el.getAttribute("data-ev"); track("clicked_btn_" + l, l); }
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// 3) section visibility (which parts of the page people actually reach)
|
|
59
|
-
if ("IntersectionObserver" in window) {
|
|
60
|
-
const seen = {};
|
|
61
|
-
const io = new IntersectionObserver(function (entries) {
|
|
62
|
-
entries.forEach(function (en) {
|
|
63
|
-
const name = en.target.getAttribute("data-section");
|
|
64
|
-
if (en.isIntersecting && !seen[name]) { seen[name] = 1; track("viewed_totcom_page_section_" + name, name); }
|
|
65
|
-
});
|
|
66
|
-
}, { threshold: .4 });
|
|
67
|
-
document.querySelectorAll("[data-section]").forEach(function (s) { io.observe(s); });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 4) scroll depth
|
|
71
|
-
const marks = [25, 50, 75, 100], hit = {};
|
|
72
|
-
window.addEventListener("scroll", function () {
|
|
73
|
-
const sc = document.documentElement, pct = Math.round((sc.scrollTop + window.innerHeight) / sc.scrollHeight * 100);
|
|
74
|
-
marks.forEach(function (m) { if (pct >= m && !hit[m]) { hit[m] = 1; track("viewed_totcom_page_scroll_" + m, String(m)); } });
|
|
75
|
-
}, { passive: true });
|
|
76
|
-
|
|
77
|
-
// 5) comparison expand/collapse
|
|
5
|
+
// 1) comparison expand/collapse
|
|
78
6
|
document.querySelectorAll(".cmp .top").forEach(function (btn) {
|
|
79
7
|
btn.addEventListener("click", function () {
|
|
80
8
|
const card = btn.closest(".cmp"), open = card.getAttribute("aria-expanded") === "true";
|
|
81
9
|
card.setAttribute("aria-expanded", open ? "false" : "true");
|
|
82
10
|
btn.setAttribute("aria-expanded", open ? "false" : "true");
|
|
83
|
-
// (analytics for the open already fired via data-ev on the button)
|
|
84
11
|
});
|
|
85
12
|
});
|
|
86
13
|
|
|
87
|
-
//
|
|
14
|
+
// 2) persona tabs
|
|
88
15
|
const tabs = document.querySelectorAll(".seg [data-persona]");
|
|
89
16
|
const panels = document.querySelectorAll(".ppanel");
|
|
90
17
|
tabs.forEach(function (tab) {
|
|
@@ -95,12 +22,6 @@
|
|
|
95
22
|
});
|
|
96
23
|
});
|
|
97
24
|
|
|
98
|
-
// 7) time on page (sent on leave)
|
|
99
|
-
const start = Date.now();
|
|
100
|
-
window.addEventListener("pagehide", function () {
|
|
101
|
-
track("viewed_totcom_page_exit", String(Math.round((Date.now() - start) / 1000)));
|
|
102
|
-
});
|
|
103
|
-
|
|
104
25
|
// ---- request-access modal --------------------------------------------------
|
|
105
26
|
// Board address the mailto: fallback targets (mirrors the server route's
|
|
106
27
|
// recipient). Kept in sync with apps/storefront/src/pages/api/request-access.ts.
|
|
@@ -108,13 +29,12 @@
|
|
|
108
29
|
const modal = document.getElementById("ra-modal");
|
|
109
30
|
const form = modal && modal.querySelector("form.ra");
|
|
110
31
|
|
|
111
|
-
function openModal(
|
|
32
|
+
function openModal() {
|
|
112
33
|
if (!modal) return;
|
|
113
34
|
modal.classList.remove("done");
|
|
114
35
|
if (form) form.classList.remove("has-error");
|
|
115
36
|
modal.classList.add("on");
|
|
116
37
|
document.body.style.overflow = "hidden";
|
|
117
|
-
track("clicked_btn_request_access_open", source || "unknown");
|
|
118
38
|
const first = modal.querySelector("input[name=email]");
|
|
119
39
|
if (first) setTimeout(function () { first.focus(); }, 40);
|
|
120
40
|
}
|
|
@@ -128,7 +48,7 @@
|
|
|
128
48
|
document.querySelectorAll('a[href="#request"]').forEach(function (a) {
|
|
129
49
|
a.addEventListener("click", function (e) {
|
|
130
50
|
e.preventDefault();
|
|
131
|
-
openModal(
|
|
51
|
+
openModal();
|
|
132
52
|
});
|
|
133
53
|
});
|
|
134
54
|
if (modal) {
|
|
@@ -164,7 +84,6 @@
|
|
|
164
84
|
phone: String(fd.get("phone") || "").trim(),
|
|
165
85
|
details: String(fd.get("details") || "").trim(),
|
|
166
86
|
};
|
|
167
|
-
track("clicked_btn_request_access_submit", fields.email ? "with_email" : "no_email");
|
|
168
87
|
|
|
169
88
|
const btn = form.querySelector("button[type=submit]");
|
|
170
89
|
const label = btn ? btn.textContent : "";
|