Repository navigation
feat(blocks): host the Deco Analytics client's source, and run it natively on v8 - #785
Open
thaisrocine wants to merge 4 commits into
Open
thaisrocine wants to merge 4 commits into
thaisrocine wants to merge 4 commits into
Conversation
The tag's code moves to blocks so it is written in one place for every consumer, while the standalone script stays a build artifact: decocms/analytics bundles these entries and its collector Worker keeps serving /_dq/a.js to v7, checkout and sites outside deco. Serving stays in the Worker because one purge fixes the whole fleet; a script inlined into each site's build would need a dependency bump per repo. The move is byte-for-byte: built from this folder (and from the packed tarball) with the esbuild the collector uses, all ten bundles hash identically to what production serves today, so no site receives a different script. Two things keep it that way. The folder's tsconfig.json sets strict and ships with src, because without it esbuild drops the "use strict" every production bundle opens with. Biome formats the folder but neither lints it nor organizes its imports: reordering imports reordered the core bundle, and the lint fixes rewrite size-tuned code. tag.test.ts bundles every entry and fails if anything outside the folder reaches the tag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A v8 site should not need the external tag to collect. startAnalytics(), on the new @decocms/blocks/analytics/client subpath, runs the same files the collector bundles into /_dq/a.js: it fills window.__dq with the fields the collector writes ahead of the tag, imports core, then each module in order. Identification, the queue, the transport and the automatic collection keep one implementation, and the standalone build is untouched. Order matters because every module registers on load through __dqRegister, and core replays the page's stub in a microtask that a module in its own chunk misses. So the client holds the stub back and replays it once every module is in. It does nothing on the server and never rejects, so a failure stops analytics and nothing else. Events go to /_dq/e on the page's own host, which keeps the site resolved from Host. commerce.ts and vitals.ts gain an empty export so they can be imported as modules; the ten bundles still hash to decocms/analytics' tag.lock.json. client.test.ts checks that a page view and a click reach the collector exactly as they do from the standalone bundle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
thaisrocine
marked this pull request as ready for review
November 10, 2026 13:53
decocms/analytics pins @decocms/blocks at an exact version, so a change to the client's source reaches /_dq/a.js only when someone bumps it there and its e2e passes. Agreed with @tlgimenes to keep that step manual for now; written here so an agent releasing a change to the client knows the second half. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tlgimenes
approved these changes
Nov 10, 2026
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.
Why
The Deco Analytics tag (clicks, page views, heatmap, commerce, errors, vitals) still has to reach v7 sites, checkout and sites outside deco through the standalone
/_dq/a.js. If v8 got its own copy of the collection logic, we would maintain the same code in two repos. As agreed with @tlgimenes, the code moves to blocks and the standalone script becomes a build artifact, the way a package's source lives in the repo and its published version lives on npm.This is the first of three steps:
@decocms/blocks(pinned version) instead of its ownruntime/, which is deleted. Its CI checks that the bundle hashes are unchanged, and its e2e (browser to Worker to ClickHouse) runs before deploy.EXECUTION_PLAN.mdandDECO_ANALYTICS.mdin decocms/context.What stays where
runtime/,shared/contract.ts,shared/anchor.ts)packages/blocks/src/v8/analytics//_dq/a.js, purgevisitor_idhashing, ingest, ClickHouse, dashboardAnalyticsScriptkeeps rendering<script async src="/_dq/a.js">and does not inline the tag. The Worker is in the delivery path so that a single purge fixes the whole fleet in seconds. If each site bundled the tag at build time, every fix would need a dependency bump in N repos.Nothing changes for existing sites
The move is byte-for-byte. Built with the esbuild version the collector uses (0.23.1), with the collector's flags, all ten bundles hash identically to what production serves today. That holds both from this folder and from the
npm packtarball. The collector's cache key (RUNTIME_VERSION) is a hash of those same bytes, so no browser is served a different script.sha256 of each bundle (identical before and after)
Three things were needed to keep it that way:
analytics/tsconfig.jsonsetsstrictand ships withsrc. Without it, esbuild drops the"use strict";every production bundle opens with, so each bundle changes by 13 bytes and runs in sloppy mode.== null, assignments in expressions, theexport {}) rewrite code that was tuned for size. Bringing it to blocks' lint rules can be a separate PR that has to keep the hashes.contract.tsnamed v7's invoke path literally.sc-12forbids that string insrc/v8, so I reworded the comment. Comments never reach the bundle.Tests
analytics/tag.test.tsbundles every entry the Worker composes:import,requireornode:);I checked that the guards fail when they should: an import from
experiments.tsin a module fails the second test, and removing the tsconfig fails all ten strict-mode tests.bun run checkandbun run testpass. The one warning is the existingcompat-v7suppression.Second commit: the client runs natively on v8
@decocms/blocks/analytics/clientexportsstartAnalytics(options), so a v8 site collects without the external tag. It runs the same files the collector bundles. Identification, the queue, the transport and the automatic collection keep one implementation.window.__dqwith the fields the collector writes ahead of/_dq/a.js(r,tier,hash,k,hr,er,e,dev). It then imports core and each module inoptions.modules, one chunk each. Bundled as a site would bundle it, the entry is about 1 KB. Core (about 6.6 KB) and each module load only when started.__dqRegister. Core replays the page's stub queue in a microtask, and a module in its own chunk would miss that replay. So the client holds the stub back and replays it once every module has registered./_dq/eon the page's own host, so the collector resolves the site fromHost. The collector ignores the body'ssiteand falls back to akey.commerce.tsandvitals.tsgain an emptyexport {}so they can be imported as modules. All ten bundles still match decocms/analytics'worker/tag.lock.json, checked with that repo'sops/build-tag.mjs --check --src.client.test.ts(jsdom) covers:/_dq/e;client.server.test.tschecks that it imports and does nothing on the server.Mutation checks: dropping the stub replay fails one test, and not loading the modules fails three.
What this commit does not do, deliberately:
{k:1}in a beacon's response kills it.DECO_ANALYTICS.mdnames, and it changes there once this is agreed.AnalyticsScriptchange. A site picks one of the two. LeavingdecoAnalyticsKeyanddecoAnalyticsFirstPartyunset renders no tag.EdgeIngestover a service binding).The two commits are independent. The first is the byte-identical move. The second changes no bundle and can be split into its own PR if preferred.
Until step 2 lands
decocms/analytics
runtime/is still what production serves. A change to the tag goes to both copies until the collector builds from this package.CLAUDE.mdsays so.🤖 Generated with Claude Code
Summary by cubic
Moves the Deco Analytics tag's source (
runtime/,shared/) intopackages/blocks/src/v8/analytics/so the collection logic lives in one place, and adds@decocms/blocks/analytics/clientso a v8 site can collect without loading the external tag./_dq/a.jsstill serves v7, checkout and sites outside deco unchanged.The move is byte-for-byte. Built with the collector's esbuild (0.23.1) and flags, all ten bundles hash identically to what production serves today, so the collector's
RUNTIME_VERSIONcache key is unchanged and decocms/analytics keeps serving from its Worker. Serving stays in the delivery path because a single purge fixes the whole fleet, whereas inlining into each site's build would need a dependency bump per repo.Keeping the bytes identical
analytics/tsconfig.jsonsetsstrictand shipssrc; without it esbuild drops the"use strict"every production bundle opens with.contract.tswas reworded to avoid a string thatsc-12forbids insrc/v8; comments never reach the bundle.startAnalytics()runs the same files the collector bundles: it fillswindow.__dqwith the fields the collector writes ahead of the tag, imports core, then each module in order. It holds the page's stub back until every module has registered, because core replays the stub in a microtask that a module in its own chunk misses. It does nothing on the server and never rejects. Events go to/_dq/eon the page's own host.commerce.tsandvitals.tsgained an empty export to be importable as modules; the ten bundles still hash to decocms/analytics'tag.lock.json.analytics/tag.test.tsbundles every entry the Worker composes and fails if anything outside the folder reaches the tag or if the runtime imports from it;client.test.tschecks that a page view and a click reach the collector exactly as they do from the standalone bundle. Until the collector builds from this package, decocms/analytics' ownruntime/is still what production serves, and since it pins@decocms/blocksat an exact version, a change to the client reaches/_dq/a.jsonly after someone bumps the pin there and its e2e passes —CLAUDE.mddocuments that step as deliberate.bun run checkandbun run testpass.Written for commit ef66c14. Summary will update on new commits.