Skip to content
Back to skills

Analytics And Consent

ASecurity

Gates analytics on opt-in consent: no SDK before a yes, typed events without personal data, ATT. Use when adding analytics, events, screen views or a usage-data toggle. Not for crashes (crash-reporting-and-observability).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentjavascriptrustgojavareactnodetestingapiperformance

Works with

  • cli
  • api

Security analysis

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

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

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill analytics-and-consent --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Analytics And Consent?

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

Security grade badge for Analytics And Consent
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-analytics-and-consent/badge)](https://www.skillsdirectory.com/skills/zakariaf-analytics-and-consent)

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: analytics-and-consent
description: "Gates analytics on opt-in consent: no SDK before a yes, typed events without personal data, ATT. Use when adding analytics, events, screen views or a usage-data toggle. Not for crashes (crash-reporting-and-observability)."
---

# Analytics and consent

Makes product analytics in an Expo app something a reviewer can trust: nothing leaves the device
until the user said yes, the vendor SDK exists only after that yes, every event comes from a typed
catalog that cannot carry personal data, analytics never tracks unless the app deliberately opts into
Apple's ATT, and the App Store and Google Play answers are generated from the same contract the code
is checked against. Two zero-dependency checkers prove it on the real code.

## Rules that must hold

1. **Opt-in before any analytics SDK exists.** The consent state starts `'unknown'`; `createAnalytics`
   drops every event until `setConsent('granted')` answers the current question, and calls `createSdk`
   (the vendor adapter) only then, in a build whose mode is not `off`. *Why:* Apple 5.1.1(ii) requires
   consent for usage data even when anonymous, ePrivacy requires it for an install id, and some SDKs
   contact their servers when constructed, opted out or not (PostHog 4.79.1 loads remote config).
2. **Withdrawing is as easy as giving, and stops and forgets.** A settings row calls `setConsent` at any
   time; a no runs `reset()` (anonymous and device ids and the unsent queue dropped) then `optOut()`, in
   that order, or `clearSdkStorage` when no SDK runs in the session, and `forget()` also clears the
   saved answer (account deletion). *Why:* 5.1.1(ii) and the
   GDPR require an easy withdrawal; PostHog's `reset()` clears a saved opt-out (the reverse order opts
   the user back in) and by default keeps the device id and the queue, which a flush still sends; an
   earlier session's ids stay on the device until something deletes them.
3. **A changed question asks again.** Raise `ANALYTICS_CONSENT_VERSION` whenever `collects`, `sdk` or
   `endpoints` in `contracts/analytics.json` change; an older answer reads as `'unknown'`. The contract's
   `consent: { version, covers }` records the fingerprint of what that version covers (`consent-covers`).
   *Why:* consent covers the purposes the user was shown, not the ones added later.
4. **Ads consent is not analytics consent, and analytics does not track by default.** Never derive the
   analytics answer from UMP's `canRequestAds`; the default contract says `tracking: false`, the adapter
   reads no advertising id, and `createAnalytics` gets no `tracking` option. The tracking profile passes
   the canonical `createExpoTrackingConsentAdapter` (never a second ATT import) and the adapter learns
   `isTrackingAllowed` only from an `'authorized'` answer. *Why:* UMP asks for Google's ad vendors, not
   the app's analytics vendor; tracking without ATT breaks 5.1.2(i).
5. **One vendor, one importer, created lazily in the root-kit registry.** `contracts/analytics.json`
   `sdk` is the only analytics package installed; only `src/services/analytics/<vendor>-analytics-adapter.ts`
   imports it; the adapter is created only inside `createAnalytics({ createSdk })` in
   `src/services/create-root-kits.ts` (app-startup-and-bootstrap's registry, part of the composition
   root), which registers `createAnalyticsKit`: features reach the port through `useAnalytics()`, never
   through an edited root file; the client is constructed inside the adapter's factory,
   never at module level; tests use the fakes. Auto-start SDKs (expo-insights; expo-observe unless the
   app writes `dispatchingEnabled: false`) and session replay are not allowed. *Why:* one seam to audit and upgrade, and a structural guarantee
   that nothing runs before consent.
6. **The SDK's privacy options are written out.** PostHog: `defaultOptIn: false`, `personProfiles:
   'never'` (anonymous), `disableGeoip: true` or a declared coarse location, no lifecycle events, no
   autocapture, no session replay, no `PostHogProvider`, error autocapture off, an explicit `host`,
   `disableRemoteFeatureFlags: true`, and a reset that is `reset([])` plus clearing the queue.
   *Why:* every default above either collects more than the catalog or survives a withdrawal.
7. **Every event is in the typed catalog, with a purpose, sent by a literal name.** `ANALYTICS_EVENTS`
   is plain data; names are snake_case, at most 40 characters, never vendor-reserved; `track()` is called
   from a model hook's handler as `track({ name: '<event>', props })`. *Why:* tsc and a text search then
   show every event and where it is sent; an event without a decision behind it is collection without a
   purpose.
8. **Events carry no personal data.** Properties are `enum`, `boolean`, bucketed `count` or `route`,
   never free text; names that mean ids, text, contact or location fail; `sanitizeEvent` drops anything
   that does not match at run time. *Why:* free strings are where titles, search terms and emails leak,
   usually by accident.
9. **Screens are route patterns.** `useScreenViews` sends `screenNameFromSegments(useSegments())`
   (`tasks/[id]`); nothing sends `usePathname()`, search params or URLs. *Why:* a concrete path holds
   record ids and whatever a deep link carried.
10. **Anonymous by default.** No identify, alias or person properties unless the contract says
    `identifies: true`, and then a linked `user-id` is declared. *Why:* the declarations and the consent
    text must match what is sent.
11. **Builds decide the project.** Development and preview are `off` (or `test`), production is `live`
    (or `off`); key and host come from the build's environment through `analyticsExtra()` into
    `expo.extra`, read back with `readAnalyticsExtra(extra, applicationId)`; never a key in code, never
    `EXPO_PUBLIC_ANALYTICS_*`. *Why:* testing builds must not pollute production data, E2E and audit
    builds must send nothing, and an EAS Update made for another variant must not redirect events.
12. **Store declarations come from the contract.** `collects` covers what the catalog, the SDK and the
    identity setting imply; `app.config.ts` writes `privacyManifests: { ...PRIVACY_MANIFESTS,
    NSPrivacyCollectedDataTypes: [...analyticsCollectedDataTypes(projectRoot)] }`; `contracts/privacy.json`
    `sdks["<sdk>"].collects` holds the same rows; the developer enters them in App Store Connect and
    Play's Data safety form. *Why:* a JavaScript SDK ships no privacy manifest, replacing the manifest
    drops its required-reason APIs (ITMS-91053), and answers that differ from the code get apps rejected.
13. **Apps for children ship no third-party analytics.** `audience: 'child-directed'` means `sdk:
    'none'`, no tracking, no identify; stop and ask before any analytics there. *Why:* App Review 1.3
    and 5.1.4 and Play Families forbid device information to third parties.
14. **Analytics is silent and never breaks a feature.** No port method throws or rejects; failures go
    to the error log; at most 100 events wait while the SDK starts. *Why:* analytics is never what the
    user came for, and an escaped rejection is lost in release builds.

## Workflow

1. **Decide with the developer** (stop and ask; never invent): the vendor (default PostHog), the region
   (`endpoints`), the audience, whether the app identifies users, and whether any SDK tracks. Read
   [references/vendors-and-sdk-facts.md](references/vendors-and-sdk-facts.md) and
   [references/att-and-tracking.md](references/att-and-tracking.md). The vendor project, its data
   retention and its data processing agreement are the developer's to set up.
2. **Install**: `npm install -E posthog-react-native@4.79.1` (outside the Expo map, pinned exactly) and
   `npx expo install expo-application` (`readAnalyticsExtra` compares the binary's application id);
   classify it in `contracts/architecture.json` (`"vendorSdks": { "posthog-react-native": "AnalyticsPort" }`,
   plus `vendor('posthog-react-native', 'AnalyticsPort')` in the ESLint config) and in the dependency policy.
3. **Copy the templates** at the paths their first lines name: `templates/src/services/analytics/`,
   `templates/src/config/analytics-config.ts` (+ test), `templates/src/hooks/use-screen-views.ts` (+ test),
   `templates/scripts/analytics/` (+ test), `templates/contracts/analytics.json` (+ schema; fill it). The
   synced canonical files (`src/services/consent/*`, `src/config/app-variant.ts`, `src/core/result.ts`, the
   tracking mock) are copied only when the app lacks them.
4. **Write the catalog** from the app's real questions: read
   [references/event-catalog.md](references/event-catalog.md); one event per decision, closed property kinds.
5. **Write the vendor adapter** after [examples/posthog-analytics-adapter.ts](examples/posthog-analytics-adapter.ts)
   and its test (mock the package; the only test that imports it).
6. **Register the kit** in app-startup-and-bootstrap's registry `src/services/create-root-kits.ts`
   ([examples/create-root-kits.ts](examples/create-root-kits.ts)): `createAnalytics` with the adapter call
   inside `createSdk`, then `createAnalyticsKit({ analytics, errorLog: data.errorLog })` in the list. The
   kit provides the port (`useAnalytics()`), sends screen views beside the root Stack and flushes on the
   way to the background; the root files stay unchanged. Wire `app.config.ts` ([examples/app-config-analytics.ts](examples/app-config-analytics.ts))
   and the EAS environments ([references/modes-keys-and-testing.md](references/modes-keys-and-testing.md)).
7. **Ask and let the user change the answer**: read [references/consent-gate.md](references/consent-gate.md);
   the prompt after onboarding and the settings row ([examples/use-usage-data-model.ts](examples/use-usage-data-model.ts),
   [examples/usage-data-prompt.tsx](examples/usage-data-prompt.tsx)), its keys merged into every catalog
   (`templates/src/i18n/catalogs/`); `forget()` from account deletion.
8. **Send events** from model handlers through `useAnalytics()` ([examples/use-tasks-model.ts](examples/use-tasks-model.ts));
   screen views and the background flush come with the kit (`analytics-root-kit.tsx`).
9. **Declare**: read [references/store-declarations.md](references/store-declarations.md); fill `collects`
   (the one source), copy it into `contracts/privacy.json` `sdks["<sdk>"].collects` when that contract
   exists, give the developer the App Privacy and Data safety rows; hand the manifest and network audit
   to privacy-and-store-declarations.
10. **Run the tests**: `npx jest src/services/analytics src/config/analytics-config.test.ts src/hooks/use-screen-views.test.tsx scripts/analytics --ci --maxWorkers=2`.
    An app with the port and catalog but no vendor yet sets `sdk: "none"` (every build off, nothing
    collected): check-analytics then notes that `manifest-wiring` and `consent-ui` wait for the vendor.
11. **Run the checkers**: `node ${CLAUDE_SKILL_DIR}/scripts/check-analytics.mjs .` and
    `node ${CLAUDE_SKILL_DIR}/scripts/check-analytics-behaviour.mjs .`; fix every `FAIL` line and rerun until
    both print `RESULT: PASS`.
12. **Tell the developer** what is theirs (vendor-console and store steps the agent cannot do): the
    vendor project and region, its "Discard client IP data" setting turned on (`disableGeoip` does not
    stop the server storing the IP; otherwise declare and justify keeping it), retention, the agreement,
    the store forms, the privacy policy text, and the device walkthrough (references/modes-keys-and-testing.md).

## Definition of done

- [ ] One analytics SDK, declared in `contracts/analytics.json`, pinned, imported only by its adapter,
      created only inside `createSdk` in the root-kit registry, which registers `createAnalyticsKit`;
      no auto-start or replay SDK; tests use the fakes.
- [ ] Nothing is sent and no SDK exists before a yes; a no resets then opts out (or `clearSdkStorage`
      deletes the stored SDK state when no SDK runs); `forget()` is wired to
      account deletion; the settings row changes the answer; `ANALYTICS_CONSENT_VERSION` equals
      `consent.version` and `consent.covers` is the fingerprint of the current `sdk`, `endpoints` and
      `collects` (check-analytics `consent-covers`).
- [ ] Every event is in `ANALYTICS_EVENTS` with a purpose and closed property kinds; screen views are
      route patterns; no identify unless declared.
- [ ] Default profile: no ATT from analytics, no advertising id, nothing declared as tracking (or the
      tracking profile done as references/att-and-tracking.md says); child-directed apps have no vendor.
- [ ] `ANALYTICS_MODE` per variant set in the EAS environments; no key in code; off builds for E2E,
      screenshots and the network audit.
- [ ] `collects` drives the privacy manifest; the developer has the App Privacy and Data safety rows; the
      device walkthrough is done or reported as owed.
- [ ] The Jest suites of step 10 pass.
- [ ] Expected only: `SKIP app.config.ts [manifest-wiring] not yet created (expo-project-bootstrap creates it)`
      before the app has a config, `SKIP contracts/privacy.json [privacy-sync] not yet created
      (privacy-and-store-declarations creates it)` before the privacy contract exists, and
      `NOT APPLICABLE` in an app with no analytics folder, contract or SDK.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-analytics.mjs .` prints `RESULT: PASS`
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-analytics-behaviour.mjs .` prints `RESULT: PASS`

## Anti-patterns

- **Constructing the SDK at startup "opted out until consent".** Some SDKs still call home on
  construction; create it inside `createSdk` (check-analytics `sdk-lazy`).
- **`optOut()` then `reset()`.** PostHog's reset clears the saved opt-out; reset first, then opt out.
- **Treating UMP's `canRequestAds` as analytics consent**, or putting analytics in Google's form
  (check-analytics `consent-source`).
- **`setConsent('granted')` in an effect, a render, a service or at startup.** Only the user's tap
  answers (`consent-source`).
- **Constructing the SDK only to reset it after a no.** Pass `clearSdkStorage` (`sdk-clear`).
- **`const client = new PostHog(...)` at the top of the adapter**, as vendor guides show: importing
  the module builds the client at launch (check-analytics `sdk-lazy`).
- **`privacyManifests: { NSPrivacyCollectedDataTypes: [...] }` without `...PRIVACY_MANIFESTS`.** It
  drops the required-reason APIs and the upload is rejected (`manifest-wiring`).
- **A pre-checked box, a bigger "Share" button, or a feature unlocked by sharing.** Two equal choices;
  nothing depends on the answer (5.1.1(ii)).
- **`track({ name, props: { title: task.title } })` or any free-text property.** Use enums, booleans and
  bucketed counts (`event-pii`, `event-props`).
- **Screen tracking with `usePathname()` and params**, as generic guides show. Send route patterns.
- **`identify(user.email)`.** Anonymous by default; an account id only with `identifies: true` and a
  linked `user-id` declaration.
- **`PostHogProvider` with autocapture, session replay, or the SDK's automatic screen capture.** They
  collect what the catalog does not allow.
- **Listing the analytics host in `NSPrivacyTrackingDomains`.** iOS then blocks it for everyone who
  declined ATT; that list is for tracking hosts only.
- **`EXPO_PUBLIC_ANALYTICS_KEY` or a key typed in `src/`.** Use the build environment and `expo.extra`.
- **A second `expo-tracking-transparency` import for analytics.** Use the canonical tracking adapter.
- **Asking for consent at first launch, over a task, or more than once per question version.**
- **Fixing a failing checker by loosening the catalog or the fixture.** Fix the event or the module.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/consent-gate.md](references/consent-gate.md) | Opt-in default and its sources, ads vs analytics consent, the record and version, the gate's life cycle, lazy creation, withdrawal order, forget, when to ask, modes, failures, tests | Workflow step 7; before changing the gate |
| [references/event-catalog.md](references/event-catalog.md) | Catalog shape, property kinds, names and limits, personal data, screen views with Expo Router, where to track, changing events, tests | Workflow steps 4 and 8 |
| [references/att-and-tracking.md](references/att-and-tracking.md) | Apple's tracking definition, the default and tracking profiles, the canonical ATT adapter, Android's advertising ID, manifest tracking keys, apps for children | Workflow step 1; before any attribution SDK |
| [references/store-declarations.md](references/store-declarations.md) | App Privacy answers, the manifest of a JavaScript SDK, Data safety, the mapping table, what the checker requires, when answers change, the privacy policy | Workflow step 9 |
| [references/vendors-and-sdk-facts.md](references/vendors-and-sdk-facts.md) | Why PostHog, install, every option with its default, source-verified pitfalls, repo wiring, other vendors, re-verify commands | Workflow steps 1, 2 and 5; on an SDK upgrade |
| [references/modes-keys-and-testing.md](references/modes-keys-and-testing.md) | Keys and hosts, modes per variant and EAS environments, the binary guard, off builds, the device walkthrough, test recipes, troubleshooting | Workflow steps 6 and 12; when events do not arrive |
| `templates/src/services/analytics/` | Port and SDK seam, consent-store port, event catalog, sanitizer, consent record, the gate (`createAnalytics`), kv-store consent adapter, fakes, screen names, the root kit (`analytics-root-kit.tsx`: provider, screen views, flusher) and `analytics-context.tsx` (`useAnalytics`), each with its test | Workflow steps 3 and 6 |
| `templates/src/config/` | `analytics-config.ts` (+ test): modes per variant, `analyticsExtra`, `readAnalyticsExtra`; `app-variant.ts` (+ test), synced (app-variants-and-environment owns it) | Workflow steps 3 and 6 |
| `templates/src/hooks/` | `use-screen-views.ts` (+ test): screen views by route pattern | Workflow step 8 |
| `templates/scripts/analytics/` | `analytics-manifest.ts` (+ test): `collects` to privacy-manifest entries and Data safety rows | Workflow steps 6 and 9 |
| `templates/src/i18n/catalogs/` | `en.json`, `ar.json`: the prompt's and the settings row's keys (merge into every catalog of the app) | Workflow step 7 |
| `templates/contracts/` | `analytics.json` (the app's analytics contract) and `analytics.schema.json` (every key documented) | Workflow step 3 |
| `templates/src/services/consent/` | The canonical ConsentPort, tracking adapter (+ iOS and Android tests) and fake (+ test), synced; admob-ads-and-consent owns them | Workflow step 3 (tracking profile) |
| `templates/src/core/` | `result.ts` (+ test), synced (error-handling-typed-results owns it) | Workflow step 3, only when missing |
| `templates/__mocks__/` | The `expo-tracking-transparency` root mock, synced (do not edit here) | Workflow step 3, tracking profile |
| [examples/posthog-analytics-adapter.ts](examples/posthog-analytics-adapter.ts) | The PostHog vendor adapter with every privacy option | Workflow step 5 |
| [examples/create-root-kits.ts](examples/create-root-kits.ts) | The analytics lines of the root-kit registry: the gate with the vendor inside `createSdk`, and the kit | Workflow step 6 |
| [examples/app-config-analytics.ts](examples/app-config-analytics.ts) | The analytics lines of `app.config.ts` | Workflow step 6 |
| [examples/use-usage-data-model.ts](examples/use-usage-data-model.ts) | The prompt and settings-row model | Workflow step 7 |
| [examples/usage-data-prompt.tsx](examples/usage-data-prompt.tsx) | The opt-in prompt, a pure view | Workflow step 7 |
| [examples/use-tasks-model.ts](examples/use-tasks-model.ts) | Tracking from a model handler | Workflow step 8 |
| `scripts/check-analytics.mjs` | Static checks: contract, files, inventory, imports, lazy creation, the root kit, stored-state clear, options, catalog, track calls, screens, identity, tracking, audience, declarations, wiring, privacy-contract sync, keys, test isolation, consent UI and its source | Workflow step 11 |
| `scripts/check-analytics-behaviour.mjs` | Runs the app's gate, consent codec, sanitizer, screen names, config and declaration mapping against stand-ins | Workflow step 11 |
| `scripts/lib/` | `analytics-repo.mjs` (facts, layout, contract, sources), `analytics-rules.mjs` (check-analytics' rules), `analytics-scenarios.mjs` (the behaviour scenarios); `app-modules.mjs`, `source-scan.mjs`, `ts-literal.mjs` synced (do not edit here) | Only to change a checker |
| `scripts/check-lib.mjs` | Shared script helper (synced; do not edit here) | Never by hand |
| `scripts/selftest.mjs` | Proves both checkers: templates and examples pass, every planted bug fails for its rule | After changing a checker, template or fixture |
| `assets/analytics-facts.json` | SDK facts (options, auto-start, kinds), advertising-id names, key patterns, catalog limits and personal-data words, Apple identifiers | Read by the checkers; update on an SDK upgrade |
| `assets/shared.json` | The shared files copied into this skill | When adding a shared file |
| `tests/` | The base app (a trimmed privacy contract and privacy-manifest module, silent planted findings under `.agents/skills/`, `skills/` and a vendored folder) and the overlay cases per checker | When adding a rule |

## Related skills

- `admob-ads-and-consent` - Google's consent form and the ATT prompt for ads; owns the ConsentPort.
- `privacy-and-store-declarations` - the whole privacy manifest, SDK inventory, network audit and release audit.
- `crash-reporting-and-observability` - crash and performance data, its own port and consent decision.
- `architecture-and-boundaries` - ports, adapters, the composition root, SDK classification.
- `app-variants-and-environment` - variants and environment variables the modes build on.
- `settings-and-preferences` - the look of the usage-data row.
- `auth-and-secure-storage` - account deletion, which calls `forget()`.
- `app-startup-and-bootstrap` - the root-kit registry and slots the kit plugs into (provider, effects, flushers).
- `dependency-management` - pinning the vendor SDK.
- `ota-updates` - publishing updates with the channel's environment.
- `e2e-maestro` - flows on `ANALYTICS_MODE=off` builds.
- `i18n-strings-and-catalogs` - the prompt's text in every language.

Files in this skill

  • SKILL.md22 KB
  • assets/analytics-facts.json11.3 KB
  • assets/shared.json1.7 KB
  • examples/app-config-analytics.ts1.8 KB
  • examples/create-root-kits.ts2.9 KB
  • examples/posthog-analytics-adapter.ts4.1 KB
  • examples/usage-data-prompt.tsx2.7 KB
  • examples/use-tasks-model.ts1 KB
  • examples/use-usage-data-model.ts1.9 KB
  • references/att-and-tracking.md7.5 KB
  • references/consent-gate.md13.3 KB
  • references/event-catalog.md8.1 KB
  • references/modes-keys-and-testing.md7 KB
  • references/store-declarations.md11.2 KB
  • references/vendors-and-sdk-facts.md13.2 KB
  • scripts/check-analytics-behaviour.mjs4.7 KB
  • scripts/check-analytics.mjs4.3 KB
  • scripts/lib/analytics-repo.mjs5.6 KB
  • scripts/lib/analytics-rules.mjs47.8 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.