AnalyticsCLI TypeScript SDK
Use This Skill When
- adding AnalyticsCLI analytics to a JS or TS app
- instrumenting onboarding, paywall, purchase, or survey events
- upgrading within the current
@analyticscli/sdk line
- validating SDK behavior together with
analyticscli
Supported Versions
- Skill pack:
1.6.11
- Target package:
@analyticscli/sdk
- Supported range:
>=0.1.1 <0.2.0
- If a future SDK major changes APIs or event contracts in incompatible ways, add a sibling skill such as
analyticscli-ts-sdk-v1
See Versioning Notes.
Error Recovery Order
When SDK setup, instrumentation, ingestion, docs, or validation behavior is broken or missing:
- Refetch this skill and upgrade
@analyticscli/sdk to the newest compatible release first.
- If CLI validation is involved, also update
@analyticscli/cli and verify analyticscli --help.
- Rerun the smallest host-app or CLI repro.
- If no newer version is available, the update cannot be applied, or the newest version still fails, submit sanitized AnalyticsCLI product feedback with
analyticscli feedback submit.
Feedback belongs to the AnalyticsCLI SaaS owner and must not be confused with tenant-owned end-user feedback collected through the SDK feedback feature. Include SDK version, skill version, CLI version if used, package-manager update attempt, failing code path or command, expected behavior, actual behavior, and workaround.
Example:
ANALYTICSCLI_CLI_ENABLE_WRITE_COMMANDS=true analyticscli feedback submit \
--category bug \
--message "SDK ingestion validation fails after latest compatible upgrade" \
--origin-name "analyticscli-ts-sdk skill" \
--location-id "analyticscli-ts-sdk/error-recovery" \
--context "sdk=@analyticscli/sdk skill=analyticscli-ts-sdk@latest cli=@analyticscli/cli flow=<sanitized_flow> workaround=<workaround>" \
--meta '{"expected":"<expected behavior>","actual":"<actual behavior>"}'
Core Rules
- Initialize exactly once near app bootstrap.
- For generated host-app code, prefer
init({ ... }) with explicit identity mode (identityTrackingMode: 'consent_gated').
init('<YOUR_APP_KEY>') shortform is acceptable for quick demos/tests or low-level client-only integrations.
initFromEnv(...), initBrowserFromEnv(...), and initReactNativeFromEnv(...) are available for setup tooling and small apps; pass an explicit env object when the framework does not expose values on process.env (for example env: import.meta.env in Vite/Astro).
- Keep setup options minimal:
apiKey is enough for ingest.
- In host apps, use client-safe publishable env names (for example
ANALYTICSCLI_PUBLISHABLE_API_KEY).
- Do not use
WRITE_KEY env names in generated host-app snippets (ANALYTICSCLI_WRITE_KEY, EXPO_PUBLIC_ANALYTICSCLI_WRITE_KEY, etc.).
runtimeEnv is auto-attached. Do not pass a mode string.
debug is only a boolean for SDK console logging.
- Do not pass
endpoint and do not add endpoint env vars in app templates. Use the SDK default collector endpoint.
- For
platform, do not use framework labels (react-native, expo).
- Use only canonical platform values (
web, ios, android, mac, windows) or omit the field.
- In React Native/Expo, pass
Platform.OS directly; the SDK normalizes values like macos -> mac and win32 -> windows.
- Treat
platform as runtime family only (web/ios/android/mac/windows), not as OS version/name.
- Treat
osName as operating-system label (for example iOS, Android, Windows, macOS, Web). Prefer always setting/populating osName; keep platform optional.
init(...)/new AnalyticsClient(...) auto-emits one session_start event per client instance on SDK mount (source: sdk_mount), so host apps do not need manual startup wiring.
- Browser clients automatically flush queued events on
pagehide, hidden visibilitychange, and beforeunload; host apps do not need custom unload handlers.
- Do not manually emit duplicate
session_start unless you intentionally also track a separate custom launch event (for example app_launch).
- In React Native/Expo, prefer
appVersion from expo-application (nativeApplicationVersion); nullable values can be passed directly.
- Do not specify
dedupeOnboardingStepViewsPerSession in generated host-app code by default; SDK default is true. Only set it explicitly when the user requests a different behavior or asks for explicit config.
- Do not specify
dedupeScreenViewsPerSession in generated host-app code by default; SDK default is true. Only set it explicitly when the user requests a different behavior or asks for explicit config.
- Set
screenViewDedupeWindowMs only when needed for a non-standard navigation stack; otherwise rely on SDK default (1200 ms).
- Prefer SDK trackers over host-side wrapper utilities. Keep integration code close to call sites.
- Keep event properties stable and query-relevant.
- Avoid direct PII.
- Set
identityTrackingMode explicitly in generated host-app bootstrap code; use 'consent_gated' as the default.
- For EU/EEA/UK user traffic, keep
identityTrackingMode: 'consent_gated' (or strict) unless legal counsel approves a different setup.
identify / setUser only work when full tracking is enabled (always_on, or after full-tracking consent in consent_gated).
- Do not force storage adapters in generated bootstrap code by default.
- Avoid top-level
Promise singletons in app utility files.
- Use neutral file names like
analytics.ts (not provider-specific names such as aptabase.ts).
- Avoid re-exporting
PAYWALL_EVENTS / PURCHASE_EVENTS from host app utility files. Import SDK constants directly when needed, or use createPaywallTracker(...).
- When using
createPaywallTracker(...), create one tracker per stable paywall context and reuse it across shown/skip/purchase calls. Recreate only when defaults change.
- If your paywall provider exposes an offering/paywall identifier, pass it as
offeringId in tracker defaults.
RevenueCat: offering identifier; Adapty: paywall/placement identifier; Superwall: placement/paywall identifier.
- In hosted paywall screens (RevenueCat UI / Adapty / Superwall or custom wrappers around them), do not use generic
track(...) / trackEvent(...) for paywall or purchase milestones.
Use one memoized createPaywallTracker(...) per screen/context and route lifecycle callbacks to tracker methods:
shown (visible), purchaseStarted, one terminal event (purchaseSuccess/purchaseFailed/purchaseCancel), and skip (dismiss/close/back).
- If multiple paywall screens exist, each screen/context must have its own stable tracker defaults (
source, paywallId, optional offeringId) so events are not mixed across screens.
- Prefer SDK identity helpers (
setUser, identify, clearUser) directly instead of wrapping identify logic in host-app boilerplate.
- Do not keep legacy analytics providers or event aliases active in generated host-app code.
- For touched paywall/purchase/onboarding flows, use canonical AnalyticsCLI event names only.
- For generated docs or README snippets, write from tenant developer perspective (
your app, your workspace) and avoid provider-centric phrasing such as our SaaS.
- Default to canonical SDK event names at call sites.
- Before generating host-app code, ensure
@analyticscli/sdk is upgraded to the newest release in that repo.
- For onboarding instrumentation, use dedicated SDK onboarding APIs instead of generic
track(...)/trackEvent(...):
createOnboardingTracker(...), trackOnboardingEvent(...), trackOnboardingSurveyResponse(...),
plus step helpers (step(...).view(), step(...).complete(), step(...).surveyResponse(...)).
- Use
onboarding:step_view as the default step progression signal. Treat onboarding:step_complete as optional and only emit it when a step has a meaningful completion boundary (for example explicit submit/continue confirmation or async success).
- For survey steps, default to
onboarding:step_view + onboarding:survey_response; avoid unconditional onboarding:step_complete unless completion semantics are explicit.
- For onboarding survey events, prefer
trackOnboardingSurveyResponse(...) (or tracker survey helpers) so SDK sanitization/normalization is preserved.
- To avoid repetitive payloads, create one onboarding tracker with shared flow defaults and use
step(...).surveyResponse(...) with only survey-specific fields at call sites.
- For React Native / Expo non-onboarding screens, track screen views on focus with
useFocusEffect(...) and analytics.screen(...).
- For RevenueCat correlation in host apps, keep AnalyticsCLI user identity in sync with the same stable user id used in
Purchases.logIn(...) (setUser on sign-in/session restore, clearUser on sign-out).
Developer Setup DX
When helping a developer set up the SDK, make the flow guided and verifiable.
- First detect the framework, package manager, app entrypoint, env naming convention, and existing analytics providers.
- Explain the minimal setup path before editing: install/upgrade SDK, add publishable API key env, initialize once, instrument core funnels, verify ingestion.
- Ask only for the missing dashboard value that cannot be inferred. For SDK setup this is normally the publishable ingest API key; CLI verification can also use a readonly token.
- Tell the developer where to find each value in the dashboard and what it is used for.
- Prefer direct repo edits and package installs when the environment allows it; do not hand back generic setup instructions when you can implement them.
- Keep host-app snippets small and idiomatic for the detected framework.
- End setup with a concrete verification path: app event to trigger, CLI/dashboard check to run, and the expected event names.
- If this SDK setup is part of AI Growth Engineer onboarding, explain that high-quality instrumentation plus connected GitHub code access lets analytics findings map back to actionable implementation areas.
Feedback Collection Rules
- If the app collects qualitative feedback, prefer the SDK
feedback config plus submitFeedback(...) instead of a separate ad-hoc client.
- Do not use legacy
analytics.feedback(message, rating, properties) for end-user feedback that must appear in the AnalyticsCLI dashboard feedback inbox. It only emits an analytics event through ingest (/v1/collect) and does not create a stored feedback message.
- If the installed SDK version does not expose
submitFeedback(...), use the official public feedback endpoint directly: POST <feedback.serviceUrl>/v1/feedback with x-feedback-key (or x-api-key) and a body containing feedback, location/locationId, appSurface/surface, originName, and optional metadata.
- Always include both a stable
locationId and a human-readable originName.
locationId should stay code-stable (settings/restore, onboarding/paywall).
originName should explain the exact product surface or UI origin (restore purchases footer, paywall dismiss modal).
- For AnalyticsCLI-backed feedback that should appear in the dashboard User Feedback view, SDK versions with the feedback default use the AnalyticsCLI API automatically. On older SDK versions, configure
feedback.serviceUrl explicitly. Override feedback.serviceUrl only for a tenant-owned proxy or external service. appId is optional unless the target endpoint requires it.
- Do not treat SDK
submitFeedback(...) returning delivery: 'analytics_only' as stored qualitative feedback. That mode only emits a lightweight analytics event and does not create a feedback-store row for the dashboard Feedback view.
- If host code already tracks a post-submit
feedback:submitted analytics event after a successful stored feedback submission, set feedback.trackEvents: false in SDK config to avoid duplicate feedback:submitted events.
- Do not put privileged feedback secrets into mobile binaries.
Host App Minimalism Guardrails
When this skill writes host-app code, optimize for low boilerplate by default.
- Do not generate a large event translation layer such as
mapEventToCanonical(...) with many switch branches.
- Do not create host-side wrappers around
identify/setUser unless required by an existing app contract.
- Do not add per-call
try/catch wrappers around every analytics helper unless the user asked for that policy.
- Do not duplicate SDK constants/events in host utility files.
- Prefer direct SDK calls in feature code (
trackPaywallEvent, tracker helpers, screen, track) instead of generic proxy helpers.
- Keep a single screen-tracking owner per route boundary (parent layout or screen component, not both).
- If a thin
analytics.ts is needed, keep it focused to bootstrap + a few shared helpers. Avoid becoming an event-translation layer.
Hard Fail Patterns
Do not generate these patterns:
- giant
switch/if trees that translate event names
- helpers like
mapEventToCanonical(...) spanning many event cases
- broad catch-all wrappers around every analytics call
- top-level
Promise<AnalyticsClient | null> bootstrap patterns
- host-side re-exports of SDK constants/events
- creating a new
createPaywallTracker(...) instance inside each paywall callback/event helper
- helper wrappers that create a fresh paywall tracker per call (for example
trackPaywallTrackerEvent(...))
- hosted paywall screens that only emit
screen(...) / trackScreenView(...) but never emit paywall:shown
- paywall/purchase milestones emitted via generic
track(...) / trackEvent(...) although stable paywall context is available
- onboarding step/survey milestones emitted via generic
track(...) / trackEvent(...) although dedicated onboarding APIs are available
- legacy/alias event names for onboarding/paywall/purchase milestones (for example
view_paywall, purchase_completed)
- dual-write analytics emission to preserve old event names/providers
apiKey fallback chains using *WRITE_KEY* env variables in host-app code
- duplicate screen tracking for the same route transition from both parent layout and child screen
If such a pattern already exists in the target codebase:
- do not expand it
- prefer reducing it while keeping behavior stable
Pre-Ship Self-Check
Before finishing, verify the generated integration code meets all checks:
- bootstrap uses
init({ ... }) (no initFromEnv(...))
- no explicit
endpoint env var in host app templates
- no large event translation layer added
- SDK APIs used directly at call sites for onboarding/paywall/purchase milestones
- identity uses SDK methods directly (
identify/setUser/clearUser) without extra wrappers
platform is web/ios/android/mac/windows or omitted (never framework labels)
- generated bootstrap sets
identityTrackingMode explicitly (default 'consent_gated')
- paywall flow reuses a tracker instance per stable paywall context (no per-event tracker re-creation)
- host-app snippets only use publishable API key env names (no
*WRITE_KEY* fallback)
- if provider exposes offering/paywall id,
createPaywallTracker(...) defaults include offeringId
- exactly one screen-tracking owner exists per route transition
- touched onboarding/paywall/purchase call sites emit canonical AnalyticsCLI events only (no legacy aliases, no dual-write)
- every touched hosted paywall screen emits
paywall:shown via tracker when shown becomes visible (not only screen-view events)
- every touched hosted paywall screen maps purchase lifecycle callbacks to tracker methods (
purchaseStarted + exactly one terminal outcome)
- every touched paywall dismissal path (close/back/skip) emits tracker
skip(...)
- touched onboarding step milestones use dedicated onboarding APIs (tracker step helpers or
trackOnboardingEvent(...)) instead of generic track(...)
- touched onboarding survey milestones use
trackOnboardingSurveyResponse(...) (or tracker survey helpers), not ad-hoc generic track(...) payloads
- touched React Native / Expo non-onboarding screens use
useFocusEffect(...) + analytics.screen(...) with one owner per route transition
- touched onboarding flows do not force
onboarding:step_complete on every step; default to onboarding:step_view and add step_complete only where completion semantics are explicit
- if touched feedback should appear in the dashboard Feedback view, SDK bootstrap or SDK defaults provide a real feedback endpoint and public feedback key, call sites pass stable
locationId/originName, and the flow does not rely on analytics_only delivery
Dashboard Credentials Checklist
Before SDK bootstrap, collect the required values from your dashboard:
- Open dash.analyticscli.com and select the target project.
- In API Keys, copy the publishable ingest API key for SDK init.
- If you will verify ingestion with CLI, create/copy a CLI
readonly_token in the same API Keys area.
- Optional for CLI verification: set a default project once with
analyticscli projects select (arrow-key picker), or pass --project <project_id> per command.
Minimal Web Setup
import { init } from '@analyticscli/sdk';
const analytics = init({
apiKey: process.env.NEXT_PUBLIC_ANALYTICSCLI_PUBLISHABLE_API_KEY ?? '',
platform: 'web',
projectSurface: 'app',
identityTrackingMode: 'consent_gated', // default
});
init(...) is preferred for host apps.
Resolve env values in app code and pass apiKey explicitly.
For Vite/Astro:
import { init } from '@analyticscli/sdk';
const analytics = init({
apiKey: import.meta.env.VITE_ANALYTICSCLI_PUBLISHABLE_API_KEY ?? '',
platform: 'web',
projectSurface: 'app',
identityTrackingMode: 'consent_gated', // default
});
React Native Setup
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Application from 'expo-application';
import { Platform } from 'react-native';
import { init } from '@analyticscli/sdk';
const analytics = init({
apiKey: process.env.EXPO_PUBLIC_ANALYTICSCLI_PUBLISHABLE_API_KEY,
debug: __DEV__,
platform: Platform.OS,
appVersion: Application.nativeApplicationVersion,
identityTrackingMode: 'consent_gated', // default
storage: AsyncStorage, // optional for RN if you want persistent IDs after consent
});
Consent gate for full tracking:
// user accepts full tracking
analytics.setFullTrackingConsent(true);
// user declines full tracking (strict analytics can continue)
analytics.setFullTrackingConsent(false);
There is no "do not start yet" init flag. Tracking starts on init(...); ready() (or initAsync(...)) is only for explicitly blocking first-flow logic until async storage hydration is done.
React Native Screen Tracking Pattern (Non-Onboarding)
Use useFocusEffect(...) for non-onboarding screens so screen views fire on route focus and not only on mount:
import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';
import { analytics } from '@/utils/analytics';
export function SettingsScreen() {
useFocusEffect(
useCallback(() => {
analytics.screen('settings', {
screen_class: 'SettingsScreen',
source: 'tabs',
});
}, []),
);
return null;
}
Notes:
- Keep exactly one screen-tracking owner per route transition.
- Do not emit duplicate screen events from both parent layout and child screen.
- For onboarding steps, do not replace onboarding milestone events with screen events.
Integration Depth Checklist
The integration should cover more than SDK bootstrap:
- onboarding flow boundaries and step progression
- paywall exposure, skip, purchase start, success, fail, cancel
- screen views for core routes/screens
- key product actions tied to user value (for example: first calibration complete, first result generated, export/share, restore purchases)
- stable context properties (
appVersion, platform, source, flow identifiers)
- if using RevenueCat, correlate client-side paywall/purchase intent with server-side subscription lifecycle updates
RevenueCat + Analytics Sync (Trials & Subscriptions)
You can include trial/purchase/cancel lifecycle data inside user flows, but "perfectly synced in real time"
is not realistic because app callbacks, store billing events, retries, and webhook delivery are eventually consistent.
Use this pattern for near-lossless correlation:
- Single identity key across both systems
- Use the same stable app user id for RevenueCat
appUserID and AnalyticsCLI analytics.setUser(...) (or setUser(...) on raw client).
- Do not rely on anonymous ids alone for subscription lifecycle analysis.
- Dual event streams
- Client stream (SDK): paywall and purchase journey intent (
paywall:shown, purchase:started, purchase:success/failed/cancel).
- Server stream (RevenueCat webhook): authoritative billing lifecycle changes (trial started, trial cancelled, renewal, subscription cancelled/expired, billing issue).
- Correlation keys on every relevant event
- Always include stable keys when available:
userId, offeringId, paywallId, packageId, entitlementKey.
- For webhook-derived events, also include RevenueCat identifiers from payload (
rcEventId, rcEventType, original transaction/subscription ids, environment/store).
- Webhook idempotency is mandatory
- Deduplicate webhook ingestion by RevenueCat event id before emitting analytics events.
- Replays/retries must not create duplicate cancellation or renewal events.
- Model cancellation reasons explicitly
- Persist store/webhook cancellation reason fields when provided (or
unknown).
- Cancellation can happen outside the app; only webhook ingestion gives reliable coverage.
Recommended custom lifecycle events (in addition to canonical purchase:* journey events):
billing:trial_started
billing:trial_cancelled
billing:trial_converted
billing:subscription_renewed
billing:subscription_cancelled
billing:subscription_expired
billing:billing_issue
This split lets funnels answer both questions:
- Journey intent: "what users did in-app before purchase/cancel"
- Billing truth: "what subscription state changed in the store/backend"
Instrumentation Rules
- Use
createOnboardingTracker(...) for onboarding flows.
- For onboarding steps in touched flows, prefer
createOnboardingTracker(...).step(...).view()/complete() over generic track(...).
- For low-noise step funnels, use
view() as baseline and call complete() only on steps with explicit completion semantics.
- For onboarding surveys in touched flows, prefer
trackOnboardingSurveyResponse(...) or tracker survey helpers over generic track(...).
- For survey steps, default to
view() + surveyResponse(...); emit complete() only when the host flow has a real completion boundary.
- Prefer tracker defaults for repeated fields in onboarding/survey flows; avoid re-sending unchanged flow metadata at every call site.
- For React Native / Expo non-onboarding screens, use
useFocusEffect(...) to call analytics.screen(...) on focus.
- Use
createPaywallTracker(...) when paywall context is stable in a flow (source, paywallId, experiment variant).
- Keep
createPaywallTracker(...) instance lifetime aligned to one stable paywall context (for example one screen flow); do not create a new tracker for every paywall event.
- Include
offeringId in paywall tracker defaults when available from provider metadata (RevenueCat/Adapty/Superwall). This is strongly recommended for reliable paywall funnel segmentation.
- Use
trackPaywallEvent(...) for one-off paywall and purchase milestones.
- Hosted paywall callback mapping is mandatory for touched flows:
- paywall visible callback ->
paywallTracker.shown(...)
- purchase started callback ->
paywallTracker.purchaseStarted(...)
- purchase success callback ->
paywallTracker.purchaseSuccess(...)
- purchase cancelled callback ->
paywallTracker.purchaseCancel(...)
- purchase error callback ->
paywallTracker.purchaseFailed(...)
- close/back/dismiss callback ->
paywallTracker.skip(...)
- Use canonical event names from
ONBOARDING_EVENTS, PAYWALL_EVENTS, and PURCHASE_EVENTS.
- Keep
onboardingFlowId, onboardingFlowVersion, paywallId, source, and appVersion stable.
- The SDK built-in dedupe covers
onboarding:step_view (dedupeOnboardingStepViewsPerSession: true, default), immediate duplicate screen(...) calls (dedupeScreenViewsPerSession: true, default; window screenViewDedupeWindowMs, default 1200 ms), and immediate overlap between onboarding screen:* and onboarding:step_view for the same step (dedupeOnboardingScreenStepViewOverlapsPerSession: true, default).
- Prevent duplicate tracking for the same user action across nested layouts/components.
- Use a single tracking owner per route or lifecycle boundary; if multiple hooks can fire, gate with a session-local idempotency key.
- For each paywall attempt, emit each milestone once (
paywall:shown, purchase:started, and one terminal event: purchase:cancel or purchase:failed or purchase:success).
No-Legacy Policy
For pre-production integrations, do not preserve legacy compatibility by default:
- Remove legacy analytics providers from touched flows instead of dual-writing.
- Replace legacy/alias milestone names with canonical AnalyticsCLI events in the same change.
- Prefer dedicated SDK helpers (
createOnboardingTracker(...), trackOnboardingSurveyResponse(...), createPaywallTracker(...)) over ad-hoc generic tracking wrappers.
Validation Loop
After integration or upgrade, verify ingestion with stable CLI checks:
analyticscli schema events
analyticscli goal-completion --start onboarding:start --complete onboarding:complete --last 30d
analyticscli get onboarding-journey --last 30d --format text
References
- Onboarding And Paywall Contract
- Minimal Host Template
- Storage Options
- Versioning Notes
1---2name: analyticscli-ts-sdk3description: Use when integrating or upgrading the AnalyticsCLI TypeScript SDK in web, TypeScript, React Native, or Expo apps.4license: MIT5---67# AnalyticsCLI TypeScript SDK89## Use This Skill When1011- adding AnalyticsCLI analytics to a JS or TS app12- instrumenting onboarding, paywall, purchase, or survey events13- upgrading within the current `@analyticscli/sdk` line14- validating SDK behavior together with `analyticscli`1516## Supported Versions1718- Skill pack: `1.6.11`19- Target package: `@analyticscli/sdk`20- Supported range: `>=0.1.1 <0.2.0`21- If a future SDK major changes APIs or event contracts in incompatible ways, add a sibling skill such as `analyticscli-ts-sdk-v1`2223See [Versioning Notes](references/versioning.md).2425## Error Recovery Order2627When SDK setup, instrumentation, ingestion, docs, or validation behavior is broken or missing:28291. Refetch this skill and upgrade `@analyticscli/sdk` to the newest compatible release first.302. If CLI validation is involved, also update `@analyticscli/cli` and verify `analyticscli --help`.313. Rerun the smallest host-app or CLI repro.324. If no newer version is available, the update cannot be applied, or the newest version still fails, submit sanitized AnalyticsCLI product feedback with `analyticscli feedback submit`.3334Feedback belongs to the AnalyticsCLI SaaS owner and must not be confused with tenant-owned end-user feedback collected through the SDK feedback feature. Include SDK version, skill version, CLI version if used, package-manager update attempt, failing code path or command, expected behavior, actual behavior, and workaround.3536Example:3738```bash39ANALYTICSCLI_CLI_ENABLE_WRITE_COMMANDS=true analyticscli feedback submit \40 --category bug \41 --message "SDK ingestion validation fails after latest compatible upgrade" \42 --origin-name "analyticscli-ts-sdk skill" \43 --location-id "analyticscli-ts-sdk/error-recovery" \44 --context "sdk=@analyticscli/sdk skill=analyticscli-ts-sdk@latest cli=@analyticscli/cli flow=<sanitized_flow> workaround=<workaround>" \45 --meta '{"expected":"<expected behavior>","actual":"<actual behavior>"}'46```4748## Core Rules4950- Initialize exactly once near app bootstrap.51- For generated host-app code, prefer `init({ ... })` with explicit identity mode (`identityTrackingMode: 'consent_gated'`).52- `init('<YOUR_APP_KEY>')` shortform is acceptable for quick demos/tests or low-level client-only integrations.53- `initFromEnv(...)`, `initBrowserFromEnv(...)`, and `initReactNativeFromEnv(...)` are available for setup tooling and small apps; pass an explicit `env` object when the framework does not expose values on `process.env` (for example `env: import.meta.env` in Vite/Astro).54- Keep setup options minimal: `apiKey` is enough for ingest.55- In host apps, use client-safe publishable env names (for example `ANALYTICSCLI_PUBLISHABLE_API_KEY`).56- Do not use `WRITE_KEY` env names in generated host-app snippets (`ANALYTICSCLI_WRITE_KEY`, `EXPO_PUBLIC_ANALYTICSCLI_WRITE_KEY`, etc.).57- `runtimeEnv` is auto-attached. Do not pass a `mode` string.58- `debug` is only a boolean for SDK console logging.59- Do not pass `endpoint` and do not add endpoint env vars in app templates. Use the SDK default collector endpoint.60- For `platform`, do not use framework labels (`react-native`, `expo`).61- Use only canonical platform values (`web`, `ios`, `android`, `mac`, `windows`) or omit the field.62- In React Native/Expo, pass `Platform.OS` directly; the SDK normalizes values like `macos -> mac` and `win32 -> windows`.63- Treat `platform` as runtime family only (`web`/`ios`/`android`/`mac`/`windows`), not as OS version/name.64- Treat `osName` as operating-system label (for example `iOS`, `Android`, `Windows`, `macOS`, `Web`). Prefer always setting/populating `osName`; keep `platform` optional.65- `init(...)`/`new AnalyticsClient(...)` auto-emits one `session_start` event per client instance on SDK mount (`source: sdk_mount`), so host apps do not need manual startup wiring.66- Browser clients automatically flush queued events on `pagehide`, hidden `visibilitychange`, and `beforeunload`; host apps do not need custom unload handlers.67- Do not manually emit duplicate `session_start` unless you intentionally also track a separate custom launch event (for example `app_launch`).68- In React Native/Expo, prefer `appVersion` from `expo-application` (`nativeApplicationVersion`); nullable values can be passed directly.69- Do not specify `dedupeOnboardingStepViewsPerSession` in generated host-app code by default; SDK default is `true`. Only set it explicitly when the user requests a different behavior or asks for explicit config.70- Do not specify `dedupeScreenViewsPerSession` in generated host-app code by default; SDK default is `true`. Only set it explicitly when the user requests a different behavior or asks for explicit config.71- Set `screenViewDedupeWindowMs` only when needed for a non-standard navigation stack; otherwise rely on SDK default (`1200` ms).72- Prefer SDK trackers over host-side wrapper utilities. Keep integration code close to call sites.73- Keep event properties stable and query-relevant.74- Avoid direct PII.75- Set `identityTrackingMode` explicitly in generated host-app bootstrap code; use `'consent_gated'` as the default.76- For EU/EEA/UK user traffic, keep `identityTrackingMode: 'consent_gated'` (or `strict`) unless legal counsel approves a different setup.77- `identify` / `setUser` only work when full tracking is enabled (`always_on`, or after full-tracking consent in `consent_gated`).78- Do not force storage adapters in generated bootstrap code by default.79- Avoid top-level `Promise` singletons in app utility files.80- Use neutral file names like `analytics.ts` (not provider-specific names such as `aptabase.ts`).81- Avoid re-exporting `PAYWALL_EVENTS` / `PURCHASE_EVENTS` from host app utility files. Import SDK constants directly when needed, or use `createPaywallTracker(...)`.82- When using `createPaywallTracker(...)`, create one tracker per stable paywall context and reuse it across `shown`/`skip`/purchase calls. Recreate only when defaults change.83- If your paywall provider exposes an offering/paywall identifier, pass it as `offeringId` in tracker defaults.84 RevenueCat: offering identifier; Adapty: paywall/placement identifier; Superwall: placement/paywall identifier.85- In hosted paywall screens (RevenueCat UI / Adapty / Superwall or custom wrappers around them), do not use generic `track(...)` / `trackEvent(...)` for paywall or purchase milestones.86 Use one memoized `createPaywallTracker(...)` per screen/context and route lifecycle callbacks to tracker methods:87 `shown` (visible), `purchaseStarted`, one terminal event (`purchaseSuccess`/`purchaseFailed`/`purchaseCancel`), and `skip` (dismiss/close/back).88- If multiple paywall screens exist, each screen/context must have its own stable tracker defaults (`source`, `paywallId`, optional `offeringId`) so events are not mixed across screens.89- Prefer SDK identity helpers (`setUser`, `identify`, `clearUser`) directly instead of wrapping identify logic in host-app boilerplate.90- Do not keep legacy analytics providers or event aliases active in generated host-app code.91- For touched paywall/purchase/onboarding flows, use canonical AnalyticsCLI event names only.92- For generated docs or README snippets, write from tenant developer perspective (`your app`, `your workspace`) and avoid provider-centric phrasing such as `our SaaS`.93- Default to canonical SDK event names at call sites.94- Before generating host-app code, ensure `@analyticscli/sdk` is upgraded to the newest release in that repo.95- For onboarding instrumentation, use dedicated SDK onboarding APIs instead of generic `track(...)`/`trackEvent(...)`:96 `createOnboardingTracker(...)`, `trackOnboardingEvent(...)`, `trackOnboardingSurveyResponse(...)`,97 plus step helpers (`step(...).view()`, `step(...).complete()`, `step(...).surveyResponse(...)`).98- Use `onboarding:step_view` as the default step progression signal. Treat `onboarding:step_complete` as optional and only emit it when a step has a meaningful completion boundary (for example explicit submit/continue confirmation or async success).99- For survey steps, default to `onboarding:step_view` + `onboarding:survey_response`; avoid unconditional `onboarding:step_complete` unless completion semantics are explicit.100- For onboarding survey events, prefer `trackOnboardingSurveyResponse(...)` (or tracker survey helpers) so SDK sanitization/normalization is preserved.101- To avoid repetitive payloads, create one onboarding tracker with shared flow defaults and use `step(...).surveyResponse(...)` with only survey-specific fields at call sites.102- For React Native / Expo non-onboarding screens, track screen views on focus with `useFocusEffect(...)` and `analytics.screen(...)`.103- For RevenueCat correlation in host apps, keep AnalyticsCLI user identity in sync with the same stable user id used in `Purchases.logIn(...)` (`setUser` on sign-in/session restore, `clearUser` on sign-out).104105## Developer Setup DX106107When helping a developer set up the SDK, make the flow guided and verifiable.108109- First detect the framework, package manager, app entrypoint, env naming convention, and existing analytics providers.110- Explain the minimal setup path before editing: install/upgrade SDK, add publishable API key env, initialize once, instrument core funnels, verify ingestion.111- Ask only for the missing dashboard value that cannot be inferred. For SDK setup this is normally the publishable ingest API key; CLI verification can also use a readonly token.112- Tell the developer where to find each value in the dashboard and what it is used for.113- Prefer direct repo edits and package installs when the environment allows it; do not hand back generic setup instructions when you can implement them.114- Keep host-app snippets small and idiomatic for the detected framework.115- End setup with a concrete verification path: app event to trigger, CLI/dashboard check to run, and the expected event names.116- If this SDK setup is part of AI Growth Engineer onboarding, explain that high-quality instrumentation plus connected GitHub code access lets analytics findings map back to actionable implementation areas.117118## Feedback Collection Rules119120- If the app collects qualitative feedback, prefer the SDK `feedback` config plus `submitFeedback(...)` instead of a separate ad-hoc client.121- Do not use legacy `analytics.feedback(message, rating, properties)` for end-user feedback that must appear in the AnalyticsCLI dashboard feedback inbox. It only emits an analytics event through ingest (`/v1/collect`) and does not create a stored feedback message.122- If the installed SDK version does not expose `submitFeedback(...)`, use the official public feedback endpoint directly: `POST <feedback.serviceUrl>/v1/feedback` with `x-feedback-key` (or `x-api-key`) and a body containing `feedback`, `location`/`locationId`, `appSurface`/`surface`, `originName`, and optional `metadata`.123- Always include both a stable `locationId` and a human-readable `originName`.124- `locationId` should stay code-stable (`settings/restore`, `onboarding/paywall`).125- `originName` should explain the exact product surface or UI origin (`restore purchases footer`, `paywall dismiss modal`).126- For AnalyticsCLI-backed feedback that should appear in the dashboard User Feedback view, SDK versions with the feedback default use the AnalyticsCLI API automatically. On older SDK versions, configure `feedback.serviceUrl` explicitly. Override `feedback.serviceUrl` only for a tenant-owned proxy or external service. `appId` is optional unless the target endpoint requires it.127- Do not treat SDK `submitFeedback(...)` returning `delivery: 'analytics_only'` as stored qualitative feedback. That mode only emits a lightweight analytics event and does not create a feedback-store row for the dashboard Feedback view.128- If host code already tracks a post-submit `feedback:submitted` analytics event after a successful stored feedback submission, set `feedback.trackEvents: false` in SDK config to avoid duplicate `feedback:submitted` events.129- Do not put privileged feedback secrets into mobile binaries.130131## Host App Minimalism Guardrails132133When this skill writes host-app code, optimize for low boilerplate by default.134135- Do not generate a large event translation layer such as `mapEventToCanonical(...)` with many `switch` branches.136- Do not create host-side wrappers around `identify`/`setUser` unless required by an existing app contract.137- Do not add per-call `try/catch` wrappers around every analytics helper unless the user asked for that policy.138- Do not duplicate SDK constants/events in host utility files.139- Prefer direct SDK calls in feature code (`trackPaywallEvent`, tracker helpers, `screen`, `track`) instead of generic proxy helpers.140- Keep a single screen-tracking owner per route boundary (parent layout or screen component, not both).141- If a thin `analytics.ts` is needed, keep it focused to bootstrap + a few shared helpers. Avoid becoming an event-translation layer.142143## Hard Fail Patterns144145Do not generate these patterns:146147- giant `switch`/`if` trees that translate event names148- helpers like `mapEventToCanonical(...)` spanning many event cases149- broad catch-all wrappers around every analytics call150- top-level `Promise<AnalyticsClient | null>` bootstrap patterns151- host-side re-exports of SDK constants/events152- creating a new `createPaywallTracker(...)` instance inside each paywall callback/event helper153- helper wrappers that create a fresh paywall tracker per call (for example `trackPaywallTrackerEvent(...)`)154- hosted paywall screens that only emit `screen(...)` / `trackScreenView(...)` but never emit `paywall:shown`155- paywall/purchase milestones emitted via generic `track(...)` / `trackEvent(...)` although stable paywall context is available156- onboarding step/survey milestones emitted via generic `track(...)` / `trackEvent(...)` although dedicated onboarding APIs are available157- legacy/alias event names for onboarding/paywall/purchase milestones (for example `view_paywall`, `purchase_completed`)158- dual-write analytics emission to preserve old event names/providers159- `apiKey` fallback chains using `*WRITE_KEY*` env variables in host-app code160- duplicate screen tracking for the same route transition from both parent layout and child screen161162If such a pattern already exists in the target codebase:163- do not expand it164- prefer reducing it while keeping behavior stable165166## Pre-Ship Self-Check167168Before finishing, verify the generated integration code meets all checks:1691701. bootstrap uses `init({ ... })` (no `initFromEnv(...)`)1712. no explicit `endpoint` env var in host app templates1723. no large event translation layer added1734. SDK APIs used directly at call sites for onboarding/paywall/purchase milestones1745. identity uses SDK methods directly (`identify`/`setUser`/`clearUser`) without extra wrappers1756. `platform` is `web`/`ios`/`android`/`mac`/`windows` or omitted (never framework labels)1767. generated bootstrap sets `identityTrackingMode` explicitly (default `'consent_gated'`)1778. paywall flow reuses a tracker instance per stable paywall context (no per-event tracker re-creation)1789. host-app snippets only use publishable API key env names (no `*WRITE_KEY*` fallback)17910. if provider exposes offering/paywall id, `createPaywallTracker(...)` defaults include `offeringId`18011. exactly one screen-tracking owner exists per route transition18112. touched onboarding/paywall/purchase call sites emit canonical AnalyticsCLI events only (no legacy aliases, no dual-write)18213. every touched hosted paywall screen emits `paywall:shown` via tracker when shown becomes visible (not only screen-view events)18314. every touched hosted paywall screen maps purchase lifecycle callbacks to tracker methods (`purchaseStarted` + exactly one terminal outcome)18415. every touched paywall dismissal path (close/back/skip) emits tracker `skip(...)`18516. touched onboarding step milestones use dedicated onboarding APIs (tracker step helpers or `trackOnboardingEvent(...)`) instead of generic `track(...)`18617. touched onboarding survey milestones use `trackOnboardingSurveyResponse(...)` (or tracker survey helpers), not ad-hoc generic `track(...)` payloads18718. touched React Native / Expo non-onboarding screens use `useFocusEffect(...)` + `analytics.screen(...)` with one owner per route transition18819. touched onboarding flows do not force `onboarding:step_complete` on every step; default to `onboarding:step_view` and add `step_complete` only where completion semantics are explicit18920. if touched feedback should appear in the dashboard Feedback view, SDK bootstrap or SDK defaults provide a real feedback endpoint and public feedback key, call sites pass stable `locationId`/`originName`, and the flow does not rely on `analytics_only` delivery190191## Dashboard Credentials Checklist192193Before SDK bootstrap, collect the required values from your dashboard:194195- Open [dash.analyticscli.com](https://dash.analyticscli.com) and select the target project.196- In **API Keys**, copy the publishable ingest API key for SDK init.197- If you will verify ingestion with CLI, create/copy a CLI `readonly_token` in the same **API Keys** area.198- Optional for CLI verification: set a default project once with `analyticscli projects select` (arrow-key picker), or pass `--project <project_id>` per command.199200## Minimal Web Setup201202```ts203import { init } from '@analyticscli/sdk';204205const analytics = init({206 apiKey: process.env.NEXT_PUBLIC_ANALYTICSCLI_PUBLISHABLE_API_KEY ?? '',207 platform: 'web',208 projectSurface: 'app',209 identityTrackingMode: 'consent_gated', // default210});211```212213`init(...)` is preferred for host apps.214Resolve env values in app code and pass `apiKey` explicitly.215216For Vite/Astro:217218```ts219import { init } from '@analyticscli/sdk';220221const analytics = init({222 apiKey: import.meta.env.VITE_ANALYTICSCLI_PUBLISHABLE_API_KEY ?? '',223 platform: 'web',224 projectSurface: 'app',225 identityTrackingMode: 'consent_gated', // default226});227```228229## React Native Setup230231```ts232import AsyncStorage from '@react-native-async-storage/async-storage';233import * as Application from 'expo-application';234import { Platform } from 'react-native';235import { init } from '@analyticscli/sdk';236237const analytics = init({238 apiKey: process.env.EXPO_PUBLIC_ANALYTICSCLI_PUBLISHABLE_API_KEY,239 debug: __DEV__,240 platform: Platform.OS,241 appVersion: Application.nativeApplicationVersion,242 identityTrackingMode: 'consent_gated', // default243 storage: AsyncStorage, // optional for RN if you want persistent IDs after consent244});245```246247Consent gate for full tracking:248249```ts250// user accepts full tracking251analytics.setFullTrackingConsent(true);252253// user declines full tracking (strict analytics can continue)254analytics.setFullTrackingConsent(false);255```256257There is no "do not start yet" init flag. Tracking starts on `init(...)`; `ready()` (or `initAsync(...)`) is only for explicitly blocking first-flow logic until async storage hydration is done.258259## React Native Screen Tracking Pattern (Non-Onboarding)260261Use `useFocusEffect(...)` for non-onboarding screens so screen views fire on route focus and not only on mount:262263```ts264import { useFocusEffect } from '@react-navigation/native';265import { useCallback } from 'react';266import { analytics } from '@/utils/analytics';267268export function SettingsScreen() {269 useFocusEffect(270 useCallback(() => {271 analytics.screen('settings', {272 screen_class: 'SettingsScreen',273 source: 'tabs',274 });275 }, []),276 );277278 return null;279}280```281282Notes:283- Keep exactly one screen-tracking owner per route transition.284- Do not emit duplicate screen events from both parent layout and child screen.285- For onboarding steps, do not replace onboarding milestone events with screen events.286287## Integration Depth Checklist288289The integration should cover more than SDK bootstrap:2902911. onboarding flow boundaries and step progression2922. paywall exposure, skip, purchase start, success, fail, cancel2933. screen views for core routes/screens2944. key product actions tied to user value (for example: first calibration complete, first result generated, export/share, restore purchases)2955. stable context properties (`appVersion`, `platform`, `source`, flow identifiers)2966. if using RevenueCat, correlate client-side paywall/purchase intent with server-side subscription lifecycle updates297298## RevenueCat + Analytics Sync (Trials & Subscriptions)299300You can include trial/purchase/cancel lifecycle data inside user flows, but "perfectly synced in real time"301is not realistic because app callbacks, store billing events, retries, and webhook delivery are eventually consistent.302303Use this pattern for near-lossless correlation:3043051. **Single identity key across both systems**306 - Use the same stable app user id for RevenueCat `appUserID` and AnalyticsCLI `analytics.setUser(...)` (or `setUser(...)` on raw client).307 - Do not rely on anonymous ids alone for subscription lifecycle analysis.3082. **Dual event streams**309 - Client stream (SDK): paywall and purchase journey intent (`paywall:shown`, `purchase:started`, `purchase:success`/`failed`/`cancel`).310 - Server stream (RevenueCat webhook): authoritative billing lifecycle changes (trial started, trial cancelled, renewal, subscription cancelled/expired, billing issue).3113. **Correlation keys on every relevant event**312 - Always include stable keys when available: `userId`, `offeringId`, `paywallId`, `packageId`, `entitlementKey`.313 - For webhook-derived events, also include RevenueCat identifiers from payload (`rcEventId`, `rcEventType`, original transaction/subscription ids, environment/store).3144. **Webhook idempotency is mandatory**315 - Deduplicate webhook ingestion by RevenueCat event id before emitting analytics events.316 - Replays/retries must not create duplicate cancellation or renewal events.3175. **Model cancellation reasons explicitly**318 - Persist store/webhook cancellation reason fields when provided (or `unknown`).319 - Cancellation can happen outside the app; only webhook ingestion gives reliable coverage.320321Recommended custom lifecycle events (in addition to canonical `purchase:*` journey events):322- `billing:trial_started`323- `billing:trial_cancelled`324- `billing:trial_converted`325- `billing:subscription_renewed`326- `billing:subscription_cancelled`327- `billing:subscription_expired`328- `billing:billing_issue`329330This split lets funnels answer both questions:331- **Journey intent:** "what users did in-app before purchase/cancel"332- **Billing truth:** "what subscription state changed in the store/backend"333334## Instrumentation Rules335336- Use `createOnboardingTracker(...)` for onboarding flows.337- For onboarding steps in touched flows, prefer `createOnboardingTracker(...).step(...).view()/complete()` over generic `track(...)`.338- For low-noise step funnels, use `view()` as baseline and call `complete()` only on steps with explicit completion semantics.339- For onboarding surveys in touched flows, prefer `trackOnboardingSurveyResponse(...)` or tracker survey helpers over generic `track(...)`.340- For survey steps, default to `view()` + `surveyResponse(...)`; emit `complete()` only when the host flow has a real completion boundary.341- Prefer tracker defaults for repeated fields in onboarding/survey flows; avoid re-sending unchanged flow metadata at every call site.342- For React Native / Expo non-onboarding screens, use `useFocusEffect(...)` to call `analytics.screen(...)` on focus.343- Use `createPaywallTracker(...)` when paywall context is stable in a flow (`source`, `paywallId`, experiment variant).344- Keep `createPaywallTracker(...)` instance lifetime aligned to one stable paywall context (for example one screen flow); do not create a new tracker for every paywall event.345- Include `offeringId` in paywall tracker defaults when available from provider metadata (RevenueCat/Adapty/Superwall). This is strongly recommended for reliable paywall funnel segmentation.346- Use `trackPaywallEvent(...)` for one-off paywall and purchase milestones.347- Hosted paywall callback mapping is mandatory for touched flows:348 - paywall visible callback -> `paywallTracker.shown(...)`349 - purchase started callback -> `paywallTracker.purchaseStarted(...)`350 - purchase success callback -> `paywallTracker.purchaseSuccess(...)`351 - purchase cancelled callback -> `paywallTracker.purchaseCancel(...)`352 - purchase error callback -> `paywallTracker.purchaseFailed(...)`353 - close/back/dismiss callback -> `paywallTracker.skip(...)`354- Use canonical event names from `ONBOARDING_EVENTS`, `PAYWALL_EVENTS`, and `PURCHASE_EVENTS`.355- Keep `onboardingFlowId`, `onboardingFlowVersion`, `paywallId`, `source`, and `appVersion` stable.356- The SDK built-in dedupe covers `onboarding:step_view` (`dedupeOnboardingStepViewsPerSession: true`, default), immediate duplicate `screen(...)` calls (`dedupeScreenViewsPerSession: true`, default; window `screenViewDedupeWindowMs`, default `1200` ms), and immediate overlap between onboarding `screen:*` and `onboarding:step_view` for the same step (`dedupeOnboardingScreenStepViewOverlapsPerSession: true`, default).357- Prevent duplicate tracking for the same user action across nested layouts/components.358- Use a single tracking owner per route or lifecycle boundary; if multiple hooks can fire, gate with a session-local idempotency key.359- For each paywall attempt, emit each milestone once (`paywall:shown`, `purchase:started`, and one terminal event: `purchase:cancel` or `purchase:failed` or `purchase:success`).360361## No-Legacy Policy362363For pre-production integrations, do not preserve legacy compatibility by default:3643651. Remove legacy analytics providers from touched flows instead of dual-writing.3662. Replace legacy/alias milestone names with canonical AnalyticsCLI events in the same change.3673. Prefer dedicated SDK helpers (`createOnboardingTracker(...)`, `trackOnboardingSurveyResponse(...)`, `createPaywallTracker(...)`) over ad-hoc generic tracking wrappers.368369## Validation Loop370371After integration or upgrade, verify ingestion with stable CLI checks:372373```bash374analyticscli schema events375analyticscli goal-completion --start onboarding:start --complete onboarding:complete --last 30d376analyticscli get onboarding-journey --last 30d --format text377```378379## References380381- [Onboarding And Paywall Contract](references/onboarding-paywall.md)382- [Minimal Host Template](references/minimal-host-template.md)383- [Storage Options](references/storage.md)384- [Versioning Notes](references/versioning.md)