Skip to content

feat(apps-website): Stats, the first-party analytics collector - #501

Open
nicacioliveira wants to merge 1 commit into
mainfrom
feat/deco-analytics-stats
Open

feat(apps-website): Stats, the first-party analytics collector#501
nicacioliveira wants to merge 1 commit into
mainfrom
feat/deco-analytics-stats

Conversation

@nicacioliveira

@nicacioliveira nicacioliveira commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

The React counterpart of the Fresh <Stats /> in deco-cx/apps#1660, so the TanStack fleet has a path off OneDollarStats. Sites import it exactly where they import its predecessor:

import Stats from "@decocms/apps-website/components/Stats";
// …
<DecoRootLayout  >
  <Stats />
</DecoRootLayout>

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_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 own 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, deco_segment into experiment assignments, and the window.DECO.events subscription 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 global

dev and debug are 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_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.

Optional DECO_ANALYTICS_ORIGIN and DECO_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 the Host header, which a visitor cannot forge, and a tag-sourced identity must never reach an invoice.

Tests

Five, covering what this file actually decides: the gate (including that =1 does not enable it), same-origin default and the preconnect only when cross-origin, the data- attributes and their absence when off, the site key only when configured, and async unless defer is 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/54 in the package, tsc --noEmit clean.

🤖 Generated with Claude Code


Summary by cubic

Introduces Stats in @decocms/apps-website, a first-party analytics loader that can replace or run alongside OneDollarStats. Previously the component handled routing and event plumbing; now the collector bundle owns behavior, reducing app code and drift.

Review and rollout

  • New component: mount once in the root layout where OneDollarStats was mounted; it renders nothing unless DECO_ANALYTICS_ENABLED="true".
  • Behavior: defaults to same-origin /_dq/a.js; adds preconnect only when DECO_ANALYTICS_ORIGIN (or origin prop) is cross-origin.
  • Config: optional DECO_ANALYTICS_SITE_KEY (or siteKey prop) for sites not behind our edge; emits no data-site unless set.
  • Debugging: dev and debug are data-* attributes on the tag (not globals) to avoid TanStack hoisting issues.
  • Loading: async by default; use the defer prop to switch.
  • Gates are independent of ONEDOLLAR_ENABLED, allowing shadow runs. Tests cover gating, origin/preconnect, attributes, site key, and async/defer.

Migration

  • Add <Stats /> to the root layout (can coexist with <OneDollarStats />).
  • Set DECO_ANALYTICS_ENABLED="true" where enabled.
  • If hosting the collector elsewhere, set DECO_ANALYTICS_ORIGIN; if not on our edge, set DECO_ANALYTICS_SITE_KEY.
  • Optional: pass dev and/or debug props during local development or troubleshooting.

Written for commit 3c3180c. Summary will update on new commits.

Review in cubic

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>
@nicacioliveira
nicacioliveira requested a review from a team August 23, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant