Add Google Analytics with a consent banner
This continues the shop app from 330. You will add a cookie consent banner, load Google Analytics only after the analytics category is accepted and keep the measurement id in configuration so each deployment sets its own.
Before you start
Install from crates.io. You need no Node and no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
Add the banner library#
The banner is a library, so it arrives the way Chart.js did in 330:
The import map gains vanilla-cookieconsent and the package's own declarations land under app/types/. Its stylesheet is a file to serve like any other: copy dist/cookieconsent.css from the package into app/styles/ and the host links it.
Declare the id#
Google Analytics needs a measurement id and the id is different in production than on your machine. Declare it under [public] in config/app.toml with the value development uses:
[]
= ""Every key under [public] is readable in a loader as ctx.config.<key>, typed from the value written here. Development runs with the empty string, which turns analytics off there.
Seed the browser with it#
The browser needs the id and a loader is how data reaches a page. Add app/routes/layout.loader.ts so every route carries it:
;
;store seeds the store the islands share, from 020. Name the key once in app/src/store.ts:
;
;Write the analytics module#
Google's snippet is four lines of inline script. As a module it is a function that reads the id from the store, builds the queue the same way and appends the loader script:
;
;
declare global
Save it as app/src/analytics.ts. The guard and the shim work like this:
- The first guard returns when there is no id, so development never loads gtag.js. It also returns when
dataLayeralready exists, so a second call does nothing. - The shim pushes
arguments, as Google's snippet does. gtag.js checks each queue entry for that type and ignores an array, so a shim written with a rest parameter queues calls that never fire and no page view is sent.
Write the consent module#
One module runs the banner and decides what loads. It knows nothing about Google beyond the function it calls:
;
;
;Save it as app/src/consent.ts. onConsent runs when a visitor answers the banner and again on a later visit once the stored answer is read. onChange runs when they change the answer in the preferences dialog, so someone who accepts later is measured from then on.
The usual way to gate a tag is to write it as type="text/plain" with a category attribute and let the banner library rewrite it once the category is accepted. That approach exists for tags written in HTML. This app has no tag, so the banner's callback does the gating.
Import it from the entry#
app/src/main.ts is the one script the document loads. Import the consent module for its effect:
;
;
;
;
;
;Build it#
The generated Config interface has one field, analytics_id: string, because that is what [public] declared. Misspell the read in the loader and the build says so:
Misspell the declaration instead, so the loader reads a key no configuration sets and fsr doctor reports it:
Set it per deployment#
The configuration is a list of files chosen by environment variables and applied in order: app.toml, then <RELEASE_ENV>.toml, then <APP_ENV>.toml, then <APP_REGION>.toml. APP_ENV defaults to local, so a config/local.toml overrides app.toml on your machine:
[]
= "G-XXXXXXXXXX"Boot the host and the report lists both files and the value in force:
The page's store seed carries it, {"site/analytics":"G-XXXXXXXXXX"}. Accepting the banner loads gtag.js with that id. A production deployment writes its own value into its own file and nothing in the application changes.
Every
[public]value reaches the browser, which is why the section has that name. Don't put a key that must stay on the server under[public].
Next up: 350. Protect a route.