feat(apps-website): Stats, the first-party analytics collector - #501
Merged
Conversation
The React counterpart of the Fresh <Stats /> in deco-cx/apps, so the TanStack fleet has a path off OneDollarStats. Sites import it exactly where they import its predecessor, from @decocms/apps-website/components/Stats. It is twenty lines where OneDollarStats is three hundred, and not because it does less. The lilstts SDK has no notion of how this app routes, no notion of the deco_segment cookie and no notion of window.DECO.events, so that component has to wrap history.pushState, poll for globals, decode the cookie and forward every commerce event by hand. The deco collector's bundle already does all of it and is tested doing it -- first pageview through the prerender guard, pushState/replaceState/popstate, flush on pagehide and visibilitychange, cookie into experiment assignments, and the DECO event subscription with the commerce mapping. Reimplementing any of that here would be a second version of it, drifting from the first. So there is no useEffect, no readiness polling and no module-level StrictMode guard, because there is no client state to guard. dev and debug are data- attributes rather than a global, and on this framework that is load-bearing: TanStack hoists <script async> into <head> ABOVE any inline configuration block -- measured at byte 190 against byte 1108 on a real site. A component that set a global would boot into silence, with the collector seeing a development host and skipping without an error. Off by default: DECO_ANALYTICS_ENABLED must be "true". The inverse of ONEDOLLAR_ENABLED, which defaults to on, because one is the incumbent and the other is being introduced. The gates are independent, so a site can run both during a shadow comparison and neither can turn the other off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The collector resolves the site server-side while RENDERING the bundle: it reads ?k= from the request and writes the resolved config into the script it returns. A key on the element arrives long after that decision, and is read by nothing -- the bundle only looks at data-dev and data-debug. As data-site this rendered perfectly, resolved nothing, served the s:"unknown" fallback bundle and collected exactly zero, with no error anywhere. The same shape as the bug that once made the entire self-serve tier silent, which is how this was noticed: a key-registered site is precisely the case a site not behind our CDN would be. The admin API's own snippet already does it this way, which is what the component should have matched from the start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hugo-ccabral
approved these changes
Aug 28, 2026
|
🎉 This PR is included in version 7.54.0 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The React counterpart of the Fresh
<Stats />in deco-cx/apps#1660, so the TanStack fleet has a path offOneDollarStats. Sites import it exactly where they import its predecessor:Why it is twenty lines and OneDollarStats is three hundred
Not because it does less — because the work is on the other side. The lilstts SDK has no notion of how this app routes, no notion of the
deco_segmentcookie and no notion ofwindow.DECO.events, so that component has to wraphistory.pushState, poll for globals, decode the cookie and forward every commerce event by hand.The deco collector's own bundle already does all of it, and is tested doing it: first pageview through the prerender guard,
pushState/replaceState/popstate, flush onpagehideandvisibilitychange,deco_segmentinto experiment assignments, and thewindow.DECO.eventssubscription with the commerce vocabulary mapped. Reimplementing any of it here would be a second version, drifting from the first.So there is no
useEffect, no readiness polling and no module-level StrictMode guard — there is no client state to guard.data-attributes, not a globaldevanddebugare read off the tag. On this framework that is load-bearing: TanStack hoists<script async>into<head>above any inline configuration block — measured at byte 190 against byte 1108 on a real site. A component that set a global and expected the collector to find it would boot into silence, with the collector seeing a development host and skipping without an error.Gating
DECO_ANALYTICS_ENABLEDmust be"true"— the inverse ofONEDOLLAR_ENABLED, which defaults to on, because one is the incumbent and the other is being introduced. The gates are independent, so a site can run both during a shadow comparison and neither can turn the other off.Optional
DECO_ANALYTICS_ORIGINandDECO_ANALYTICS_SITE_KEY. Same-origin by default, which is the intended deployment: script and beacon served from the site's own hostname, so no third-party request is involved. A site key is only for sites not behind our edge — sites that are get identified by theHostheader, which a visitor cannot forge, and atag-sourced identity must never reach an invoice.Tests
Five, covering what this file actually decides: the gate (including that
=1does not enable it), same-origin default and the preconnect only when cross-origin, thedata-attributes and their absence when off, the site key only when configured, andasyncunlessdeferis asked for. Each re-imports the module, because the gate is read at module load and a test that set the variable afterwards would pass or fail on file order.54/54in the package,tsc --noEmitclean.🤖 Generated with Claude Code
Summary by cubic
Introduces
Statsin@decocms/apps-website, a first-party analytics loader that can replace or run alongsideOneDollarStats. Previously the component handled routing and event plumbing; now the collector bundle owns behavior, reducing app code and drift.Review and rollout
OneDollarStatswas mounted; it renders nothing unlessDECO_ANALYTICS_ENABLED="true"./_dq/a.js; addspreconnectonly whenDECO_ANALYTICS_ORIGIN(ororiginprop) is cross-origin.DECO_ANALYTICS_SITE_KEY(orsiteKeyprop) for sites not behind our edge; the key goes in the URL query string (?k=), which is where the collector reads it while rendering the bundle.devanddebugaredata-*attributes on the tag (not globals) to avoid TanStack hoisting issues.asyncby default; use thedeferprop to switch.ONEDOLLAR_ENABLED, allowing shadow runs. Tests cover gating, origin/preconnect, attributes, site key, and async/defer.Migration
<Stats />to the root layout (can coexist with<OneDollarStats />).DECO_ANALYTICS_ENABLED="true"where enabled.DECO_ANALYTICS_ORIGIN; if not on our edge, setDECO_ANALYTICS_SITE_KEY.devand/ordebugprops during local development or troubleshooting.Written for commit 5d20a4c. Summary will update on new commits.