Capacitor and Ionic
One package, apparent-capacitor, for Capacitor apps: Ionic, Vue, React, Angular, Svelte or plain. Capacitor 5 or later. No native code of its own: it reads three official Capacitor plugins.
Paste this into your AI
The fastest way. Open the app's folder in Claude Code, Cursor or Codex and paste this. Put in your app id and key from Add your app (that page has a prompt with them filled in).
It installs and starts the SDK, then wires what fills every card: signed-in people, purchases, the events behind Where new people stop (onboarding, sign-up, your main action, the upgrade screen, buying), problems, a few properties, links, notification taps and caught errors. Then it checks that the first event arrived. Prefer to do it yourself? Everything below is the same, by hand.
Add Apparent analytics to this app (your app, on iPhone and Android). Do it all in one go: don't ask me anything, don't wait for a yes.
App id: <app id>
Key: <write key>
Intake: https://in.apparent.fyi
1. In the app's folder (each app's folder, if the repo has more than one), run:
npx -y apparent-cli@latest init --app <app id> --key <write key> --yes
It finds the framework, installs the SDK with the platform's own tool, starts it in the entry file and tracks screens. If it lists steps "left for you", or can't tell the framework, do those steps from the SDK notes below. Never write your own SDK.
2. People and purchases (Apparent counts people by account and ties every sale to the person who made it). Use the SDK notes below for the exact lines:
- Where sign-in succeeds: identify with the app's own user id (never an email). On sign-out: reset (logout on Swift, Kotlin, Flutter). Skip if the app has no accounts.
- If the app uses RevenueCat (react-native-purchases, purchases-ios, purchases-android, purchases_flutter, @revenuecat/purchases-capacitor): add the RevenueCat link right after Purchases.configure. That's what fills "Spend per source" and the sales in each visit.
3. Add these events where they happen, with these exact names. They fill Apparent's cards and its "Where new people stop" funnel, which follows them in this order. Skip any that don't fit the app. Name the thing first and the verb last, lowercase_snake. Small properties only, never emails, names or other personal data:
- onboarding_started: the first onboarding or setup screen shows (skip if there is none). First step of "Where new people stop"
- onboarding_completed: the last onboarding or setup step is done. Shows how many get through it
- sign_up: someone makes an account (skip if the app has no accounts). The "Signed in" step
- <main_action>: the one thing people come for, named after it, like scan_completed or habit_completed. Most important: "Of every 10 people", the visit stories and the last step of "Where new people stop" are made of it
- paywall_view: the upgrade screen shows, with { source } (what opened it: onboarding, a locked feature, settings). "Saw the upgrade screen" step, "From store to sale", and visits that end there read "Left at the upgrade screen"
- purchase: a purchase goes through, with { product, price, currency }. The "Bought" step; marks the visit "Bought"
- <thing>_failed: something the person tried didn't work, with { reason } in a few words, like upload_failed or purchase_failed. Shows as "Hit a problem" in visits
Pick one main action: the thing that means someone got what they came for. Screens, visits, coming back, countries and crashes are tracked by themselves: don't add events for them.
4. Get the most out of it, only where the app already has the code (don't build anything new):
- Properties: after sign-in or when they change, set 1 to 3 facts about the person that rarely change, like plan (free/pro) or the language they chose. Every number can then be split by them. Never personal data.
- Links and notification taps: where the app already handles deep links or notification taps, add the SDK's line from the notes (Links and notification taps). Campaign links (utm_) then count as where people came from, and notification campaigns show who they brought back.
- Caught errors: in the existing catch blocks of the main flows (sign-in, purchase, the main action), report unexpected errors with captureError. At most 5 places.
5. Check it still compiles, once: npx tsc --noEmit (Expo, React Native, Capacitor; errors that were there before aren't yours), xcodebuild -destination 'generic/platform=iOS Simulator' build or swift build (Swift), ./gradlew assembleDebug (Android), flutter analyze (Flutter). Fix only what you broke. Don't run linters or anything that installs tools, don't run the app, and don't start or use a simulator or emulator.
6. Check the key works: send one test event (it counts as a test phone, never as a person), then ask Apparent what arrived:
curl -s -X POST https://in.apparent.fyi/i -H 'content-type: application/json' -d '{"a":"<app id>","k":"<write key>","v":"setup","d":{"platform":"ios","anon":"setup-check","simulator":true},"e":[{"n":"setup_check"}]}'
curl -s "https://in.apparent.fyi/i/check?a=%3Capp%20id%3E&k=%3Cwrite%20key%3E"
{"received":1 or more} means it works. 401 means the app id or key is wrong. (A website: visits show in Apparent within 10 minutes, skip this.)
7. Stop anything you started (Metro, dev servers). Tell me what you did in 3 short lines: what was installed, what went in where (events, sign-in, RevenueCat, properties, links; files), and what the check said. Then these two lines as they are:
Run the app once and open a screen: Apparent shows who's in it, live.
Optional: add reviews and sales from App Store Connect, Google Play and RevenueCat at https://app.apparent.fyi/connect
Keep the change small. Don't touch anything outside this project (global caches, other repos); if something there is in the way, say so in your 3 lines.
## SDK notes (for steps 2 and 4, and any step the command left to you)
### Capacitor
Looks like: @capacitor/core in package.json (Ionic too)
Install: npm install apparent-capacitor @capacitor/app @capacitor/device @capacitor/preferences && npx cap sync
Start it in the web app's entry file (src/main.ts or src/main.tsx), before the app mounts:
```
import { Apparent } from "apparent-capacitor";
Apparent.init({ apiUrl: "https://in.apparent.fyi", app: "<app id>", key: "<write key>" });
```
Screens: // Any router, next to init: import { trackScreens } from "apparent-capacitor"; trackScreens();
Events: Apparent.track("sign_up");
Signed-in people:
```
Apparent.identify(user.id); // after sign-in, your own id
Apparent.reset(); // on sign-out
```
Purchases:
```
// RevenueCat (@revenuecat/purchases-capacitor): right after Purchases.configure
await Apparent.linkRevenueCat((attributes) => Purchases.setAttributes(attributes));
// Where a purchase goes through
Apparent.track("purchase", { product: "pro_yearly", price: 29.99, currency: "USD" });
```
Properties:
```
Apparent.setProperties({ plan: "free", language: "de" }); // on every later event; null removes one
```
Links and notification taps:
```
// The launch link is sent by itself. Add one line to the app's own listeners (the SDK never adds its own):
App.addListener("appUrlOpen", (e) => { Apparent.trackDeepLink(e.url); /* the app's routing */ });
PushNotifications.addListener("pushNotificationActionPerformed", (a) => Apparent.trackPushOpened(a.notification.data, a.notification.id));
```
Caught errors:
```
Apparent.captureError(error); // in a catch block
```Install
npm install apparent-capacitor @capacitor/app @capacitor/device @capacitor/preferences && npx cap sync@capacitor/app tells it when the app opens and closes, @capacitor/device gives the phone's model, system and language, and @capacitor/preferences keeps the saved events in the phone's own storage, so a WebView that clears its storage loses nothing.
Start it
In the web app's entry file (src/main.ts or src/main.tsx), before the app mounts, once:
import { Apparent } from "apparent-capacitor";
Apparent.init({ apiUrl: "https://in.apparent.fyi", app: "<app id>", key: "<write key>" });Add disabled: import.meta.env.DEV (Vite) if you don't want development builds counted at all. You usually don't need to: simulators and emulators are left out of the numbers by themselves.
Screens
Any router, next to init:
import { trackScreens } from "apparent-capacitor";
trackScreens();It watches the address. /orders/42 is sent as the screen orders/:id (numbers, uuids and long ids become :id) with the real path in path, and / is home.
Or name screens from your router yourself:
router.afterEach((to) => Apparent.screen(String(to.name ?? to.path)));router.events.pipe(filter((e) => e instanceof NavigationEnd))
.subscribe((e) => Apparent.screen(e.urlAfterRedirects));const location = useLocation();
useEffect(() => Apparent.screen(location.pathname), [location.pathname]);Anything else: Apparent.screen("settings").
Events and users
Apparent.track("paywall_view", { source: "onboarding" });
Apparent.setProperties({ plan: "free" }); // added to every later event
Apparent.identify(user.id); // after sign-in
Apparent.reset(); // on sign-outMore in Events and users.
The events Apparent reads
Add these where they happen, with these names. They fill the cards and Where new people stop, which follows new people through them in this order. Skip any that don't fit your app.
Apparent.track("onboarding_started"); // the first onboarding screen shows
Apparent.track("onboarding_completed"); // the last onboarding step is done
Apparent.track("sign_up"); // an account was made
Apparent.track("habit_completed"); // your main action, named after it
Apparent.track("paywall_view", { source: "onboarding" }); // the upgrade screen shows, and what opened it
Apparent.track("purchase", { product: "pro_yearly", price: 29.99, currency: "USD" });
Apparent.track("upload_failed", { reason: "no space" }); // something didn't workPick one main action: the thing that means someone got what they came for (scan_completed, lesson_completed). Keep properties small, never emails or names. What each one fills: Events and users.
Purchases
With RevenueCat (@revenuecat/purchases-capacitor), after Purchases.configure:
await Apparent.linkRevenueCat((attributes) => Purchases.setAttributes(attributes));Where a purchase goes through:
Apparent.track("purchase", { product: "pro_yearly", price: 29.99, currency: "USD" });See RevenueCat.
Errors and opting out
- Uncaught errors and unhandled promise rejections are caught by themselves.
- Errors you catch:
Apparent.captureError(error). - Someone says no:
Apparent.optOut().Apparent.optIn()starts again. Both are remembered. - Asking for consent first? Start with
optOutByDefault: true. Nothing is collected untilApparent.optIn(). - Nothing showing up? With
debug: trueit printsApparent: app id or key is wrongonce when they don't match. Copy both again from Connect. - Simulators and emulators are never counted, even in a release build.
Links, notifications and your own filter
Links. The link the app was opened with is sent as deep_link_opened by itself, with its utm_ fields. Secrets like token= are hidden. A campaign link on someone's first day counts as where they came from. Off: trackDeepLinks: false.
A link that brings the app back while it's running needs one line in your own listener:
App.addListener("appUrlOpen", (e) => {
Apparent.trackDeepLink(e.url);
// your routing
});Notifications. One line in your tap listener sends push_opened with the campaign and title from the notification's data, never its text:
PushNotifications.addListener("pushNotificationActionPerformed", (a) =>
Apparent.trackPushOpened(a.notification.data, a.notification.id));Why by hand: Capacitor gives an event that arrived before anyone listened (the link or the tap that started the app) only to the first listener. If the SDK listened first, your own code would miss it.
Your own filter. beforeSend sees every event before it is saved. Return it changed, or null to drop it:
Apparent.init({
apiUrl: "https://in.apparent.fyi", app: "<app id>", key: "<write key>",
beforeSend: (event) => {
if (event.name === "debug_tap") return null;
const { email, ...props } = event.props ?? {};
return { ...event, props };
},
});In a browser
The same build running as a website (Capacitor's web platform, a PWA) is sent as web, so it doesn't mix with your iPhone and Android numbers. For the website itself, the website script counts visitors without cookies.
What it promises
- Events are saved on the phone right away, so a crash or a closed app loses nothing.
- Up to 1,000 events wait offline for a week, and are never counted twice.
- One request at a time. A failed one waits longer before each retry.
- No advertising ids, so no tracking prompt (ATT) on iOS.
Automatic events: first_open, app_open, app_updated, app_background, screen_view (with trackScreens), error, deep_link_opened (the launch link). Not on Capacitor: install_source, since Capacitor has no official plugin for Google Play's install referrer. Clicks on your store links are still counted.