@convex-internal/web-analytics
v2.2.2
Published
Shared consent state, consent banner, and analytics setup for Convex websites.
Readme
@convex-internal/web-analytics
Shared consent state, consent banner, and analytics setup for Convex websites.
Core
Mount a framework entry at the root (React or Svelte). Analytics start in production, except on localhost. Consent is synced to PostHog. The banner shows when the visitor has not chosen yet.
PostHog already copies standard campaign params (UTMs, gclid, rdt_cid,
fbclid, and other common click IDs) from the landing URL onto events. This
package also captures dub_id. After consent, PostHog creates an anonymous
person profile. Campaign params still available at that point can be stored as
$initial_*, supporting person-level attribution before login.
capture
Record an event from client code through the framework-neutral root entry. The optional third argument is PostHog's capture options.
import { capture } from "@convex-internal/web-analytics";
capture("www-login", undefined, {
transport: "sendBeacon",
send_instantly: true,
});getDistinctId
PostHog's identifier for the current visitor. After consent is granted it is
stored in a cookie on .convex.dev, so every Convex site in the browser uses
the same ID.
When this helper returns a value, consent has been granted. Pass that ID to a third-party tool so you can match that tool's events or sessions, such as a support chat, back to the same visitor in PostHog.
It's expected that a call on first load is always undefined, so call again
after consent is granted.
It returns one of:
undefined. PostHog is not initialized (localhost, non-production, or still loading), or consent has not been granted. Before consent, PostHog still has a cookieless ID, but it is not stable across sites or even page reloads, so returning it would create noise rather than being useful.- Anonymous UUID, such as
"019a7d8f-9190-7844-a5c0-73d461a3a5d1". Consent has been granted but this browser has not logged into the dashboard where the user is identified. - Convex profile ID, such as
"4281". Consent has been granted and this browser has logged into the dashboard.
Consent banner
The banner is visible when consent is "undecided". It is hidden while
consent is "unknown", "consented", or "declined".
Theme with --wa-banner-* variables on .wa-banner:
.wa-banner {
--wa-banner-background-color: var(--color-seashell);
--wa-banner-font-family: var(--font-primary);
--wa-banner-accept-background-color: var(--color-plum-p4);
}- Background, text, and border:
--wa-banner-background-color,--wa-banner-text-color,--wa-banner-border-color,--wa-banner-border-radius,--wa-banner-box-shadow,--wa-banner-backdrop-filter - Type and layout:
--wa-banner-font-family,--wa-banner-font-size,--wa-banner-padding,--wa-banner-max-width,--wa-banner-action-gap,--wa-banner-z-index - Link and focus:
--wa-banner-link-color,--wa-banner-focus-color - Buttons:
--wa-banner-button-border-radius,--wa-banner-button-padding,--wa-banner-button-min-height,--wa-banner-button-font-size,--wa-banner-button-font-weight - Accept button:
--wa-banner-accept-background-color,--wa-banner-accept-text-color - Decline button:
--wa-banner-decline-background-color,--wa-banner-decline-text-color
Buttons default to 80% opacity and go to 100% on hover or keyboard focus.
Consent events
cookie_consent_shown is recorded when the banner appears.
cookie_consent_decided is recorded with
{ decision: "accepted" | "declined", source: "banner" } just before the
choice is saved, and with { decision: "declined", source: "gpc" } when
the GPC preference is applied. Historic events without source are banner
decisions.
Cookie behavior
Once the cookie has been read, consent is resolved in this order:
- Global Privacy Control. If
navigator.globalPrivacyControl === true,consentis"declined". GPC is not stored: the cookie is not written or changed. Turn GPC off and the stored cookie (if set) applies again. - Cookie.
trueis"consented",falseis"declined", and a missing or unrecognized value is"undecided".
The cookie is stored with path=/ and a max-age of one year. On convex.dev
and *.convex.dev it is also set with domain=.convex.dev so the website,
Docs, Stack, and other subdomains share one choice. The cookie is marked
Secure everywhere except localhost.
Changes made through setConsent update immediately. Changes made
elsewhere, such as in another tab or DevTools, are synchronized through cookie
change events when available, and when the window regains focus or its
visibility changes as a fallback.
React
Import from @convex-internal/web-analytics/react.
WebAnalyticsProvider is a client component. useConsent throws if called
outside that provider.
import { WebAnalyticsProvider } from "@convex-internal/web-analytics/react";
<WebAnalyticsProvider>{children}</WebAnalyticsProvider>;Consented
Renders children only when consent is "consented". It renders nothing during
SSR, while consent is still "unknown" or "undecided", if the visitor
declined, or while Global Privacy Control is on.
import { Consented } from "@convex-internal/web-analytics/react";
<Consented>
<YourAnalyticsScripts />
</Consented>;useConsent
consent(ConsentStatus)"unknown"until cookie has been read (SSR and first client render)"undecided"after that if there is no recognized choice yet"consented"if the cookie istrueand GPC is not set"declined"if the cookie isfalseor GPC is set
setConsent("consented" | "declined")- Saves the choice to the
allowsCookiescookie and updates context - Do not write the cookie yourself
- Saves the choice to the
Use the hook when you need consent or setConsent directly. Enable tracking
only when consent === "consented".
Svelte
The Svelte entry exports only WebAnalytics. Render it once in the root
layout. It has no props and no snippet: it starts PostHog and shows the banner.
There is no Consented wrapper or consent store. Those can be added later;
nothing using the Svelte entry has needed them yet.
<script>
import { WebAnalytics } from "@convex-internal/web-analytics/svelte";
</script>
<WebAnalytics />Test in-development changes
- In the
web-analyticsrepo, build and pack the package:
cd /path/to/web-analytics
npm run build
npm packThis creates a tarball such as convex-internal-web-analytics-1.0.0.tgz.
- Install that tarball in the consuming app:
cd /path/to/consuming-app
npm install /path/to/web-analytics/convex-internal-web-analytics-1.0.0.tgz --force--force ensures npm refreshes the package when the filename and version have
not changed.
Publish a version to npm
- Confirm that you are signed in to the npm account with access to the
@convex-internalorganization:
npm whoamiRun npm login first if needed.
- Commit any README or code changes, then bump the version.
npm versionrequires a clean Git worktree and creates a version commit and tag.
npm version major # breaking, or 0.x → 1.0.0
npm version minor # new backwards-compatible functionality
npm version patch # bug fixes- Run tests, inspect the package contents, then publish.
.npmrcsetsignore-scripts=true, soprepackwill not run — build first:
npm test
npm run build
npm pack --dry-run
npm publishConsume a published version
Convex repos typically set min-release-age=7 in .npmrc, so a version
published in the last 7 days will fail to install. Override the window for that
one install:
npm install @convex-internal/web-analytics@<version> --min-release-age=0Use the local tarball workflow whenever you need to test changes that have not been published yet.
