Skip to content
Back to skills

Configure Analytics

ASecurity

Use when adding analytics to a frontend — one provider-agnostic seam (trackEvent/trackPageView), Core Web Vitals as real-user data, consent-gated by default (GDPR/TDDDG), path-only page views, no PII, no-op without config.

  • 5 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 10, 2026
developmentgobashreactvueapifrontendbackendsecurityperformance

Works with

  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 10, 2026

npx -y skills add VelimirMueller/lab-claude-skills --skill configure-analytics --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Configure Analytics?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Configure Analytics
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/velimirmueller-configure-analytics-lab-claude-skills/badge)](https://www.skillsdirectory.com/skills/velimirmueller-configure-analytics-lab-claude-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: configure-analytics
description: Use when adding analytics to a frontend — one provider-agnostic seam (trackEvent/trackPageView), Core Web Vitals as real-user data, consent-gated by default (GDPR/TDDDG), path-only page views, no PII, no-op without config.
---

# Configure Analytics

## 1. Audit current state

```bash
cat .claude/stack-profile.md 2>/dev/null || cat ~/.claude/stack-profile.md 2>/dev/null   # frontend.framework, frontend.meta, observability, package_manager
grep -E '"(@plausible-analytics/tracker|plausible-tracker|posthog-js|@vercel/analytics|web-vitals)"' package.json 2>/dev/null
ls src/libs/analytics.ts 2>/dev/null
```

Read `frontend.framework` (router hook in step 7) and `package_manager` (commands use pnpm); `observability.backend` is for errors and traces, not product analytics — keep them separate. **Prerequisites:** `optimize-performance` (we forward its `web-vitals`) and `set-up-routing` (page views fire on route change).

## 2. Decide what to do
- No analytics → full setup (seam + vitals + page views).
- Analytics calls scattered in components → centralize behind the seam (step 5).

## 3. Choose a provider, and decide consent first
- **Consent decides the code, not the provider.** The seam sends nothing until the user opts in (`CONSENT_REQUIRED = true`). This is the default for any EU/German audience: GDPR needs a legal basis and transparency, and § 25 TDDDG requires consent for any read or write on the user's device. A cookieless tool can fall outside § 25 and still needs a documented basis; the npm Plausible tracker, for one, reads `localStorage.plausible_ignore`. Set `CONSENT_REQUIRED = false` only with a written legal/DPO decision for that product.
- **Cookieless** (Plausible, Fathom): no cookies, no personal profile, tiny script. **Default provider.**
- **Product analytics** (PostHog): funnels and session replay are richer and need consent plus a data-processing agreement; review its privacy settings.
- Avoid cookie-based GA unless a consent banner already exists.
Why: `analytics-patterns.md`.

## 4. Install
```bash
pnpm add @plausible-analytics/tracker   # or posthog-js, @vercel/analytics — the seam hides the choice
```
(`plausible-tracker`, the older package, was last released in 2022 and its API differs; use the `@plausible-analytics/tracker` package.)

## 5. The analytics seam (provider-agnostic, no-op without config or consent)

One module owns analytics — swapping providers is a one-file change, exactly like `captureError`. Checked against `@plausible-analytics/tracker` 0.4 (`init` runs once; `track(name, { props })` takes string props).

```ts
// src/libs/analytics.ts
import { init, track } from '@plausible-analytics/tracker';

type Props = Record<string, string | number | boolean>;

declare global {
  interface Navigator {
    globalPrivacyControl?: boolean; // not in lib.dom yet
  }
}

/** true = nothing is sent before an explicit opt-in. Flip only with a written legal/DPO decision. */
const CONSENT_REQUIRED = true;
const CONSENT_KEY = 'analytics-consent';
let started = false;

function consentGranted(): boolean {
  try {
    return localStorage.getItem(CONSENT_KEY) === 'granted';
  } catch {
    return false; // storage blocked: treat as no consent
  }
}

/** Evaluated on every request, so withdrawing consent stops sending at once. */
function mayTrack(): boolean {
  if (navigator.globalPrivacyControl || navigator.doNotTrack === '1') return false;
  return !CONSENT_REQUIRED || consentGranted();
}

export function initAnalytics(): void {
  const domain = import.meta.env.VITE_ANALYTICS_DOMAIN;
  if (!domain || started || !mayTrack()) return; // no config, already running, or no consent: no-op
  init({
    domain,
    autoCapturePageviews: false, // page views come from the router (step 7), the same for any provider
    transformRequest: (payload) => (mayTrack() ? payload : null),
  });
  started = true;
}

export function trackEvent(name: string, props?: Props): void {
  if (!started || !mayTrack()) return; // mayTrack here too: after withdrawal the seam stops preparing payloads, not just the request
  // Plausible custom properties are strings.
  const stringProps = props && Object.fromEntries(Object.entries(props).map(([k, v]) => [k, String(v)]));
  track(name, stringProps ? { props: stringProps } : {});
}

export function trackPageView(path: string): void {
  // Path only: query strings carry tokens, emails and search terms. The full URL keeps the origin — in a
  // local/preview run that is localhost:PORT, so do not set VITE_ANALYTICS_DOMAIN with consent granted there.
  if (started && mayTrack()) track('pageview', { url: `${location.origin}${path.split(/[?#]/)[0]}` });
}

/** Call from the consent banner (the banner itself is outside this skill). */
export function setConsent(granted: boolean): void {
  try {
    localStorage.setItem(CONSENT_KEY, granted ? 'granted' : 'denied');
  } catch {
    /* storage blocked: the choice lasts for this page view only */
  }
  if (granted) {
    const wasStarted = started;
    initAnalytics();
    // The router hook (step 7) fires on the first load too; only fire here when consent is what started the seam.
    if (!wasStarted) trackPageView(location.pathname);
  }
}
```

Call `initAnalytics()` once from the app entry. Gate any other third-party script the same way (Sentry replay, chat widgets): call it from `setConsent`, never at load. Add `VITE_ANALYTICS_DOMAIN` and the tracker's endpoint to the CSP `connect-src` (`set-up-security-headers`); proxy the endpoint through your own domain if ad blockers matter.

## 6. Wire Core Web Vitals as real-user monitoring

Feed the `reportWebVitals` from `optimize-performance` into the seam:
```ts
// app entry, prod only
import { reportWebVitals } from '@/libs/reportWebVitals';
import { trackEvent } from '@/libs/analytics';

if (import.meta.env.PROD) {
  reportWebVitals(({ name, value }) =>
    trackEvent('web-vital', { metric: name, value: Math.round(name === 'CLS' ? value * 1000 : value) }), // CLS ×1000: rounding a 0.0x score to an integer would erase it
  );
}
```
Now LCP/INP/CLS are measured on **real users**, not just your Lighthouse run. They go through the same seam, so they obey the same consent gate: no consent, no field data. (CLS reports only from Chromium.)

## 7. Page views on route change

```ts
// React (TanStack Router) — fires on the first load too
router.subscribe('onResolved', ({ toLocation }) => trackPageView(toLocation.pathname));
// Vue Router
router.afterEach((to) => trackPageView(to.path));
```
The seam passes the path only, so the router hook never leaks a query string. (A tracker's own SPA auto-capture would double count; step 5 turns it off.)

## 8. Track meaningful events, not everything

Define a small taxonomy (`signup_completed`, `todo_created`, `checkout_started`) and call `trackEvent` at those moments — typically inside the mutation `onSuccess`. Never put PII (email, name, IDs that identify a person) in props.

## 9. Verify
```bash
pnpm build && pnpm preview
```
With `VITE_ANALYTICS_DOMAIN` set **and consent granted**: navigating fires page views, a web-vital event arrives, and a sample `trackEvent` shows in the provider dashboard. Network tab before consent: no request to the provider. After `setConsent(false)`: no further requests. Without the env var: nothing loads, app unaffected.

## References
- ./analytics-patterns.md — the seam, consent (GDPR/TDDDG), no-PII, web-vitals RUM, event taxonomy, no-op-without-config.
- ../optimize-performance/SKILL.md — the `reportWebVitals` source.
- ../configure-error-tracking/SKILL.md — the sibling seam pattern.

Files in this skill

  • SKILL.md7.5 KB
  • analytics-patterns.md4.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.