feat(gnoweb): add SimpleAnalytics metadata and custom events#5554
feat(gnoweb): add SimpleAnalytics metadata and custom events#5554gfanton wants to merge 10 commits intognolang:masterfrom
Conversation
🛠 PR Checks Summary🔴 Changes related to gnoweb must be reviewed by its codeowners Manual Checks (for Reviewers):
Read More🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers. ✅ Automated Checks (for Contributors):🟢 Maintainers must be able to edit this pull request (more info) ☑️ Contributor Actions:
☑️ Reviewer Actions:
📚 Resources:Debug
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Nice work on the base. A few broader thoughts, gathered by theme.
Missing builder signals we might want to check (never the input or output but the intention) → to discuss:
- DISCOVER:
- searchbar submit / click
- network popup open click
- breadcrumb navigation (clicks vs URL typed vs back button)
- TOC open/close click
- Settings:
- theme toggle (retention signal) click
- devmode toggle (explicit "I'm a builder" signal) click
- BUILD on the action page — highest-signal events for builder intent:
- safe/fast/url mode change click
- send-mode toggle click
- qeval preview success/failure click
address_filledandparams_filledas booleans only (NEVER the values)
- Package listing (user page):
- sort order change click
- display mode change click
- list filter search (count only, NEVER the query) click
- Read engagement:
- scroll depth thresholds (50/75/100%) on source and action pages
- plus the time-on-page already captured by SA
These are cheap to instrument and would give a lot more visibility on the journey.
SimpleAnalytics features we may have left on the table:
inline-events.jsis not loaded. It's the privacy-first way to tag outbound links in HTML (<a data-event="outbound_docs_quickstart">) without writing JS delegation. Simpler than the TS click handler for the outbound use case, and it lets us split the outbound bucket into meaningful segments (docs vs Discord vs GitHub vs faucet vs playground) in the SA dashboard.- No priority tagging on outbound links yet. Right now everything goes to the generic "outbound" bucket via
auto-events.js. Without named events, DISCOVER-stage attribution collapses. - Goals and funnels not configured in the SA dashboard. Custom events are raw data; SA will only chart funnels if someone defines them in the SA UI. Worth confirming who owns that step.
- UTM parameter convention not defined. SA captures
utm_source/medium/campaignautomatically, but only if marketing puts them on Twitter/Discord/blog/email links. Without a documented convention (e.g.utm_source=discord&utm_campaign=grants_march), we lose most of the "where did this builder come from" signal. - SA has a data export API. If the DX dashboard from #5467 will consume SA later (alongside the indexer), it's worth shaping the taxonomy today with that consumer in mind: flat event names, stable dimension keys, bounded cardinality. 👍
Outbound-link strategy: a short list of "priority outbounds" worth named events would be something like: docs, GitHub repo, Discord, faucet, playground, Twitter. Everything else stays in the generic bucket from auto-events.js.
Taxonomy doc → to discuss: a short SIMPLEANALYTICS.md or ADR would help consolidate (a) what the events carry, (b) what they deliberately don't (IMPORTANT since gno.land is and will remain privacy-first, including at the SimpleAnalytics layer.: no raw query, no function args, no wallet, no IP beyond SA defaults, no cookies, no fingerprinting), (c) how to add a new event, (d) cardinality caps per dimension.
| if (btn.hasAttribute("data-copy-text-value")) kind = "link"; | ||
| else if (btn.hasAttribute("data-copy-remote-value")) kind = "snippet"; | ||
| fire("copy_action", { kind }); |
There was a problem hiding this comment.
Small analytics nit: snippet groups source-code copy (reading intent) with gnokey command copy (about-to-execute intent), two very different stages of the builder funnel. The data-copy-remote-value prefix already carries the info:
| if (btn.hasAttribute("data-copy-text-value")) kind = "link"; | |
| else if (btn.hasAttribute("data-copy-remote-value")) kind = "snippet"; | |
| fire("copy_action", { kind }); | |
| const remote = btn.getAttribute("data-copy-remote-value") ?? ""; | |
| let kind = "unknown"; | |
| if (btn.hasAttribute("data-copy-text-value")) { | |
| kind = "link"; | |
| } else if (remote === "source-code") { | |
| kind = "source"; | |
| } else if (remote.startsWith("func-")) { | |
| kind = "func_signature"; | |
| } else if (remote.startsWith("action-function-")) { | |
| kind = "gnokey_command"; | |
| } | |
| fire("copy_action", { kind }); |
| const raw = article.getAttribute("data-action-function-name-value") ?? ""; | ||
| fire("submit_action", { func: raw.slice(0, MAX_FUNC_NAME) }); |
There was a problem hiding this comment.
The event carries func but not the pkgpath, so we can tell which function was submitted but not which realm it's on. The <div> already exposes data-action-function-pkgpath-value, and pkgpaths are public so no privacy concern:
| const raw = article.getAttribute("data-action-function-name-value") ?? ""; | |
| fire("submit_action", { func: raw.slice(0, MAX_FUNC_NAME) }); | |
| const name = article.getAttribute("data-action-function-name-value") ?? ""; | |
| const pkg = article.getAttribute("data-action-function-pkgpath-value") ?? ""; | |
| const func = name.slice(0, MAX_FUNC_NAME); | |
| const pkgpath = pkg.slice(0, 128); | |
| fire("submit_action", { func, pkgpath }); |
| <script> | ||
| window.sa_metadata = { | ||
| page_type: {{ .PageType }}, | ||
| chain_id: {{ .ChainId }} | ||
| }; | ||
| </script> |
There was a problem hiding this comment.
A follow-up to add CSP (in the dedicated place - not sure it was done) with nonces would be worth opening?
| return "home" | ||
| case ViewModeUser: | ||
| return "user" | ||
| case ViewModePackage: |
There was a problem hiding this comment.
actually i need to change ViewModePure as well
Adds the full v1 SimpleAnalytics taxonomy: copy/submit refinements, DISCOVER (search, breadcrumb, network popup, back, toc), BUILD (mode, send, qeval, address/params), Settings (theme, devmode), package listing (filter, sort, display), scroll depth, and named outbound events for priority destinations. - gnodev: --web-analytics and --web-analytics-hostname flags - gnoweb: AnalyticsHostname plumbed through StaticMetadata for data-hostname override (required when SA cannot identify the host, e.g. local dev on a non-default port) - analytics.ts: 14 listeners covering 18 event names; onClick helper; one-shot listeners self-detach - theme/mode: typed CustomEvent dispatch via DocumentEventMap - qeval: placeholder text and error class moved to data-* attributes so the contract is shared between controller and analytics - footer/header: data-outbound on tagged anchors (docs, github, discord, faucet, status, twitter, youtube) - new SIMPLEANALYTICS.md: privacy posture, taxonomy, cardinality caps, procedure for adding events
… feat/gnoweb-sa-events-ux9
|
Thanks for the thorough review. I've addressed all of them, and also added Existing-event refinements
Events
About other comments
Yes, this needs to be configured on the dashboard side — probably once we have enough events flowing to define the funnels meaningfully.
Good idea went with
Hmm probably in a followup, this need to be configured infra side anyway
I went with
Out of scope for this PR I think
That's exactly the shape
Tagged 7 targets in this PR: |
Replace the inline window.sa_metadata script with a synchronous classic sa-bootstrap.js that reads its own data-* attributes and assigns the metadata. A classic <script src=...> blocks HTML parsing until it finishes, so window.sa_metadata is always set before SA's async latest.js can start loading — deterministic ordering, no race. Works under a strict CSP (script-src 'self' https://sa.gno.services) without needing 'unsafe-inline' or per-request nonces.
- Remove dead HeadData.Analytics bool (never read) - Drop redundant handler write of FooterData.Analytics.Hostname; single source of truth is IndexLayout deriving analytics fields from HeadData - Centralize redirect-view AnalyticsData construction in a new StaticMetadata.RedirectAnalytics() helper (was duplicated between faucet handler and redirect middleware) - Replace `as` type casts with instanceof narrowing in analytics.ts and sa-bootstrap.ts for runtime-safe DOM access - Trim redundant WHAT comments in controller-form-exec.ts
Summary
Adds the full v1 SimpleAnalytics event surface to gnoweb. Pageviews carry stable metadata; custom events cover the DISCOVER, BUILD, package listing, read engagement, and outbound funnels. All event payloads are derived from server-side context or low-cardinality DOM signals — no raw user input is forwarded.
Refs #5467.
Pageview metadata
Set on every pageview via
window.sa_metadatafrom the inline script inlayouts/analytics.html, before SA's asynclatest.jsloads.page_typecomponents.ClassifyPageType(mode, view)home,user,pure,realm,source,help,directory,status,redirect,explorer,otherchain_idcfg.ChainIDView type takes precedence over mode: a Source view inside a Realm
mode is
source, notrealm.Custom events
copy_actionbutton[data-controller~="copy"]{kind: link | source | func_signature | gnokey_command | unknown}submit_action{func, pkgpath}(capped 64 / 128)search_actionnetwork_popup_togglechangeon#searchbar-server-popup-toggle{open: bool}breadcrumb_clickback_navigationpopstatetoc_toggle<details>toggle{open: bool}mode_changemode:changedCustomEvent{mode}send_mode_toggle{active: bool}qeval_preview{success: bool}address_filled#action-user-addressparams_filledtheme_toggletheme:changedCustomEvent{theme: light | dark | system}devmode_togglechangeon#header-input-devmode{enabled: bool}list_filter_search#packages-searchlist_sort_changechangeoninput[name="order-mode"]{order}list_display_changechangeoninput[name="display-mode"]{mode}scroll_depth{threshold: 50 | 75 | 100, surface: source | action}outbound_<target>a[data-outbound]target∈docs,faucet,status,github,twitter,discord,youtube. SA's built-inoutbound/download/emaileventscontinue to fire for everything else.
Architecture
frontend/js/analytics.ts(loaded as a module). Listeners use the capture phase where a controller callsstopPropagationon the bubble (copy, submit). One-shot listeners self-detach after firing.controller-theme.tsandcontroller-action-header.tsdispatch typedCustomEvents on document; payload types are augmented viafrontend/js/events.d.tsso analytics consumes them without runtime assertions.data-qeval-*attributes. The action controller writes these constants and the analytics observer reads them, sharing the contract via the DOM rather than duplicating string literals.data-outboundis set in Go on theFooterLink/HeaderLinkstructs and rendered by the templates.Hostname override
AppConfig.AnalyticsHostname(gnoweb) and--web-analytics-hostname(gnodev) renderdata-hostnameon the SA script tag. SA's library defaults tolocation.host(which includes the port); the override lets a non-default-port deployment (typically local dev) report underthe bare hostname registered in the dashboard.
Documentation
SIMPLEANALYTICS.mddocuments the full taxonomy, privacy posture, cardinality caps, and the procedure for adding new events.Generated by Claude.