One panel for your app and its website, live.

See who is in your app and on your site right now, where visitors come from, how they use features and whether the new release helped. No analyst, no query building.

Free up to 100,000 events a month. No credit card.

tapll.com/panel/acme/fitflow/today
Overview in the Tapll panelLive in the Tapll panelPeople in the Tapll panelRevenue in the Tapll panelTraffic in the Tapll panelCrashes in the Tapll panel

The app, its revenue and the App Store in three bands, each saying how fresh its numbers are.

One Swift package and one script tag, plus RevenueCat, Stripe, Paddle, App Store Connect, Apple Search Ads and Survicate next to them.

  • Swift Package
  • iOS 16+
  • SwiftUI and UIKit
  • One script tag, under 5 KB
  • Cookieless by default
  • Optional consent banner
  • No IDFA, no ad id

The answers you check every day without a query builder.

See

Every session, as it happens — and the person behind it.

tapll.com/panel/acme/fitflow/live
Live in the Tapll panelSessions in the Tapll panelPeople in the Tapll panel
  1. 01

    Screens tracked for you

    App lifecycle and screen views without a line of code; name the events you care about.

  2. 02

    Offline-safe batches

    Events queue on the device and ship in batches. IP addresses are truncated before anything is stored.

  3. 03

    Server events

    Your backend can report for an install too — kept apart from what the app measured.

Understand

What people use, whether they come back, and what a release changed.

tapll.com/panel/acme/fitflow/features
Features in the Tapll panelRetention in the Tapll panelReleases in the Tapll panelCrashes in the Tapll panel
  1. 01

    Declared, not guessed

    A feature declared in the SDK is a definition; suggestions from names wait for your approval.

  2. 02

    Every number has a definition

    Each metric says what it counts and what it is compared with.

  3. 03

    Correlation, never cause

    Anything that moves with retention or revenue is labelled as a correlation.

Earn

Money and the people behind it — including the ones who leave.

tapll.com/panel/acme/fitflow/revenue
Revenue in the Tapll panelMRR in the Tapll panelChurn report in the Tapll panel
  1. 01

    Counted once

    RevenueCat and Apple describe the same purchases; Tapll counts one of them and says which.

  2. 02

    Gaps are gaps

    A day Apple has not reported yet shows as a gap, never a zero.

  3. 03

    NPS next to it

    Promoter, passive or detractor from Survicate — the score, never the comment.

One person, start to finish, and what to ask them.

tapll.com/panel/acme/fitflow/people/7f3a
A person’s profile with the churn report in the Tapll panel
  1. 1

    One person, every device

    Signed-in and anonymous sessions joined into one account, with the country, device and build.

  2. 2

    Their history in counts

    First seen, kept sessions and the sessions with a purchase.

  3. 3

    Churn report

    When they turned off renewal, how they used the app before it, and a survey link with a message you send from your own mailbox.

  4. 4

    Every session in order

    The screens and events of each session, newest first, with the device it came from.

From sign-up to first session. About five minutes.

  1. 1

    Add the Swift package

    Paste one write key into Tapll.configure(). Screens and app lifecycle are tracked for you; declare the features you care about. A website joins the same project with one script tag.

    import Tapll
    
    Tapll.configure(writeKey: "av_…")
    let checkout = Tapll.feature("checkout")
    checkout.start("checkout_started")
    checkout.success("order_placed")
  2. 2

    Connect revenue and the App Store

    RevenueCat or Apple’s notifications for the app, Stripe or Paddle for the site, and an App Store Connect key. All optional.

  3. 3

    Watch the first session land

    Run the app or load the site. The first session shows up in Live within seconds, with the environment it came from.

For Claude Code, Cursor or Codex. See the prompt
# Set up Tapll in your iOS app

You are integrating the Tapll iOS SDK (https://github.com/tapll/tapll-ios, v0.5.0). This is the generic version of the prompt; the one in the Tapll panel (Settings → SDK setup) is filled in with the project's write key, privacy mode and integrations.

## Goal

Integrate the Tapll iOS SDK into this app. Tapll is product analytics and crash reporting for mobile apps: live sessions, screens, feature funnels, releases, retention, crashes and revenue. The SDK is a Swift Package with no third-party dependencies, no IDFA and no ATT prompt.

Work in four phases and do not skip ahead: **explore → install and configure → propose a feature plan and wait for approval → instrument and verify.** The rules below are enforced by the Tapll server. Most mistakes are not errors — the event is accepted and the numbers are quietly wrong — so follow them exactly rather than approximately.

## 1. Explore before you edit

Read the project and report back, before changing anything:
- The app entry point (SwiftUI `App` struct, or `AppDelegate` / `SceneDelegate`) and the deployment target. The SDK needs **iOS 16+**; if the target is lower, stop and say so.
- How screens are built and navigated (SwiftUI `NavigationStack`, tabs, sheets, UIKit view controllers).
- Sign-in and sign-out, and what stable user id the backend uses.
- Purchases: StoreKit 2 `product.purchase(…)`, StoreKit 1, RevenueCat, or none.
- Deep links (`onOpenURL`, `scene(_:openURLContexts:)`, universal links).
- Survicate, if present.
- Any existing analytics (Firebase, Mixpanel, Amplitude, PostHog, custom). Their call sites are the best map of the moments worth measuring — but do not copy their event names blindly; they must follow the naming rules below. Do not remove the other SDK unless asked.

## 2. Install

Add the Swift Package `https://github.com/tapll/tapll-ios` (from: "0.5.0") and link the `Tapll` library to the **app target only** (not extensions or test targets). With an `.xcodeproj`, add it through the project's package dependencies; with a `Package.swift`, add:
```swift
.package(url: "https://github.com/tapll/tapll-ios", from: "0.5.0")
```
Do not add CocoaPods or Carthage variants — there are none.

## 3. Configure

Call `configure` exactly once, as early as possible — in the SwiftUI `App` initializer or at the top of `application(_:didFinishLaunchingWithOptions:)`:
```swift
import Tapll

// privacyMode must match the project's setting in Tapll (Settings → Privacy):
// .productAnalytics or .strictAnonymous. It cannot be changed after the project is created.
Tapll.configure(writeKey: "av_…", privacyMode: .productAnalytics)
```
- Replace `av_…` with the project's client write key from Tapll → Settings → SDK write keys. It is meant to ship in the app binary.
- One key serves every build. The SDK detects the environment itself (DEBUG → `development`, TestFlight → `testflight`, App Store → `production`) and sends it with every batch. Do **not** set `Options.environment` unless the user asks for it.
- For local debugging, raise the log level in DEBUG builds only:
```swift
var options = Options()
#if DEBUG
options.logLevel = .debug
#endif
Tapll.configure(writeKey: "av_…", privacyMode: .productAnalytics, options: options)
```
- `configure` already sends installs, updates, sessions, foreground/background and device context. Do not send your own `app_opened`, `session_started` or similar — they duplicate what the panel already counts.
- Deep links: call `Tapll.handleDeepLink(url)` from every place the app receives a URL. It records scheme, host and path only, never the query or fragment.

## 4. Screens

- SwiftUI: add `.trackScreen("Name")` to the root view of each screen (not to reusable subviews, rows or cells). UIKit: set `options.automaticScreenTracking = true`, or call `Tapll.screen("Name")` in `viewDidAppear`.
- Names are stable and PascalCase, one per screen: `"Checkout"`, `"WorkoutDetail"`. **Never put data in a screen name** (`"Profile-\(userId)"`, a title, a date) — every distinct string becomes a separate screen.
- Sheets and full-screen covers that the user perceives as a screen get their own name. Alerts, menus and toasts do not.

## 5. Identity

**If the project is Product Analytics:** call `Tapll.identify(userId:)` right after sign-in (and at launch when a session is restored) with the backend's **stable, opaque user id** — never an email address, phone number or name as the id. Call `Tapll.reset()` on sign-out.
- Traits (`identify(userId:traits:)`, ≤ 8 keys, values ≤ 256 characters) are optional. If you add `email` or `name`, the app is collecting that data and it must appear in the App Store privacy answers — tell the user rather than deciding for them.
**If the project is Strict Anonymous:** Do **not** call `Tapll.identify` or `Tapll.setCrashKey`, and do not read `Tapll.installId` / `Tapll.installUUID` (they are `nil`). The server rejects identity in this mode with `privacy_violation`. Screens, events, features and crashes all still work.

## 6. Feature plan — propose it, then wait for approval

A feature in Tapll is a funnel built from events the code **declares**. A declaration from a TestFlight or App Store build creates the feature in the panel permanently (development builds only pre-fill the editor). So before instrumenting anything, write a plan and **stop until the user approves it**.

Present it as a table: `feature key | event name | role | where it fires (file / function) | properties`. Then implement it with the feature handle:
```swift
let checkout = Tapll.feature("checkout")
checkout.screen("Checkout")                                   // discovery
checkout.start("checkout_started")                            // start
checkout.use("shipping_selected")                             // use
checkout.success("order_placed", properties: ["items": 3])    // success
checkout.failure("payment_failed", properties: ["reason": "card_declined"]) // failure
```
Roles — use them precisely:
- `discovery` — saw that the feature exists, has not tried it yet (a screen view, opening a menu). `feature.screen(…)` sets it for you.
- `start` — made an attempt whose outcome is not known yet. **This is the funnel's denominator**; a feature without `start` has no funnel, only a counter.
- `use` — working inside the feature; may happen many times per attempt. It is **not** a funnel step and never the default bucket.
- `success` — the feature gave the user what they came for. Once per attempt, not once per tap.
- `failure` — the attempt failed **from the user's point of view**: permission denied, no results, a save that did not happen. Not every caught error — those go to `Tapll.recordError`.

Rules the server enforces:
- **One event name = one (feature, role) pair**, everywhere in the code. The same name as `success` in one place and `failure` in another is a conflict the server shows and never resolves.
- Giving up is its **own event name** (`onboarding_completed` and `onboarding_abandoned`), not one event with a `skipped` property.
- A feature is something a user **can fail to adopt**. Never name a feature after a data model or a core surface every user touches on day one (`items`, `home`, `users`) — it reports 100% adoption and says nothing. Send those events without a feature key.
- Every feature needs a real path to `start` and `success`. If the attempt can visibly fail, give that path a `failure` event, or the funnel reads as if nobody ever fails.
- A declared feature that nothing emits is worse than none: it reads as zero adoption. Declare only what you actually instrument.
- Aim for 3–8 features for a typical app. Fewer, well-bounded features beat many thin ones.
- Feature keys follow the event-name rules below. A typo creates a second feature, so define each key once (a constant or a static handle) and reuse it.
- Events that belong to no feature are fine: `Tapll.track("settings_changed", properties: ["setting": "units"])`.

## 7. Event names and properties

- Names: lowercase `snake_case`, matching `^[a-z][a-z0-9_]*$`, ≤ 64 characters, `object_action` in the past tense (`workout_finished`, `photo_shared`). The `$` prefix is reserved for the SDK — never use it.
- Properties: ≤ 32 keys per event, keys in the same `snake_case` form; values are a string (≤ 256 characters), a finite number or a bool. No nested objects, arrays or dates (send an ISO string or a number).
- Keep values **low-cardinality** (an enum-like `plan: "pro"`, a count, a duration in seconds). Ids, timestamps and free text in properties make every value unique and useless for breakdowns.
- An invalid event is logged and **dropped**, never truncated or partially sent. The SDK never throws or crashes the app.
- `Tapll.track(_:properties:screen:)` can say where an event happened (`screen: "Checkout"`) without counting a screen view.

## 8. What must never be sent

- Anything the user typed: search queries, messages, notes, form fields, names of things they created.
- Tokens, passwords, API keys, receipts, full URLs or query strings.
- Precise location, contacts, photos metadata, advertising identifiers.
- Survey answers, comments or scores.
- Free text in `Tapll.recordError(_:reason:)` — use a short code (`"card_declined"`), not an exception message that may contain user data.
If a moment is only interesting because of its content, send the fact and a category (`note_created`, `length_bucket: "long"`), never the content.

## Purchases

Tapll links purchases, renewals and refunds to a person. Do this whichever purchase stack the app uses:
With **StoreKit 2**, pass Tapll's install id as the `appAccountToken` on every purchase:
```swift
let result = try await product.purchase(
    options: Tapll.installUUID.map { [.appAccountToken($0)] } ?? []
)
```
- The token **must be a UUID**; the App Store silently accepts other values and links nothing. If the app already sets `appAccountToken` to something else, stop and ask — there can be only one.
- It is not retroactive: purchases made before it ships can never be linked. It must ship before the first release that sells anything.

With **RevenueCat**, set the subscriber attribute after `Purchases.configure` and whenever the user changes:
```swift
if let id = Tapll.installId { Purchases.shared.attribution.setAttributes(["tapll_install_id": id]) }
```
- Call `Tapll.identify(userId:)` with the same id the app passes to RevenueCat as `app_user_id` (`Purchases.logIn`).
- Revenue itself arrives through RevenueCat's webhook to Tapll — do not send purchase events from the app.

If the project has App Store Server Notifications connected in Tapll, those own the revenue numbers and RevenueCat is not counted; set the token either way.

## Survicate (NPS)

Only if the app uses Survicate:
```swift
if let id = Tapll.installId {
    SurvicateSdk.shared.setUserTrait(UserTrait(withName: "tapll_install_id", value: id))
}
```
- That trait puts each answer on the right person. Set it after `Tapll.configure`.
- From Survicate's survey listener, send exactly these two events — the server looks the names up literally, so a typo fails silently:
```swift
Tapll.track("survey_displayed", properties: ["survey_id": surveyId])
Tapll.track("survey_closed", properties: ["survey_id": surveyId])
```
- Never send the answer, score or comment to Tapll. Scores arrive from Survicate's webhook; answer text is never stored.

## Crashes, hangs and errors

Crash and hang reporting is on by default once `configure` runs — do not add another crash reporter's handlers on top without asking (if the app already has Crashlytics or Sentry, report it and let the user decide). A crash is uploaded on the **next launch**, so it appears later, not immediately.
- `Tapll.recordError("PaymentFailed", reason: "card_declined")` or `Tapll.recordError(error)` for caught errors that matter. Not for expected control flow.
- `Tapll.leaveBreadcrumb("retrying_upload")` before risky operations — a name only, never a value.
- `Tapll.setCrashKey("plan", "pro")` for up to 8 labels on every later report. Same rules as properties: no ids, no personal data.

Upload dSYMs so crash frames get function names. Use whichever CI the project has; the key is a **server key** stored as the secret `TAPLL_SERVER_KEY` (created in Tapll → Settings → Server keys) — never paste it into the project or the prompt. The script is pinned by SHA-256 45588f5ee591…, and must be downloaded and verified exactly as shown:
- **Xcode Cloud** (`ci_scripts/ci_post_xcodebuild.sh`): Add TAPLL_SERVER_KEY to the workflow as a secret environment variable. Runs after every archive action.
```sh
#!/bin/sh
set -e
if [ "$CI_XCODEBUILD_ACTION" = "archive" ]; then
  curl -fsSL https://tapll.com/dsyms/v3.sh -o "/tmp/tapll-dsyms.sh" \
    && echo "45588f5ee591290d86e5e82a02f56e47c7256a66f303580e13e2f4caa137af8d  /tmp/tapll-dsyms.sh" | shasum -a 256 -c - \
    && bash "/tmp/tapll-dsyms.sh" "$CI_ARCHIVE_PATH"
fi
```
- **fastlane** (`fastlane/Fastfile`): After build_app (gym), which leaves the zipped dSYMs at DSYM_OUTPUT_PATH. Set TAPLL_SERVER_KEY in your CI secrets.
```ruby
lane :release do
  build_app(scheme: "MyApp")
  sh(<<~SH)
    curl -fsSL https://tapll.com/dsyms/v3.sh -o "/tmp/tapll-dsyms.sh" \
      && echo "45588f5ee591290d86e5e82a02f56e47c7256a66f303580e13e2f4caa137af8d  /tmp/tapll-dsyms.sh" | shasum -a 256 -c - \
      && bash "/tmp/tapll-dsyms.sh" "#{lane_context[SharedValues::DSYM_OUTPUT_PATH]}"
  SH
  upload_to_testflight
end
```
- **Build phase** (Run Script phase, last in the target): Release builds only. Turn off User Script Sandboxing for the target (the script reads the dSYM and uses the network), and provide TAPLL_SERVER_KEY from the environment — never paste it into the project.
```sh
[ "$CONFIGURATION" = "Release" ] || exit 0
curl -fsSL https://tapll.com/dsyms/v3.sh -o "$DERIVED_FILE_DIR/tapll-dsyms.sh" \
  && echo "45588f5ee591290d86e5e82a02f56e47c7256a66f303580e13e2f4caa137af8d  $DERIVED_FILE_DIR/tapll-dsyms.sh" | shasum -a 256 -c - \
  && bash "$DERIVED_FILE_DIR/tapll-dsyms.sh" "$DWARF_DSYM_FOLDER_PATH"
```
Set up only the one that matches the project, and tell the user to add the secret.

## Server-side events

Events that happen on the app's backend (a subscription renewed server-side, an invoice paid) go to `POST https://in.tapll.com/v1/server` with a **server key** (`sk_…`) — from the backend only, never from the app and never committed. The SDK never calls that endpoint. If the backend needs to attribute an event to the device, the app sends `Tapll.installId` to its own backend. Only mention this to the user; do not implement backend code unless asked.

## App privacy

The SDK ships a `PrivacyInfo.xcprivacy`. Tell the user which App Store privacy answers apply — you cannot change them in code:
- Product Analytics: Product Interaction, Device ID and User ID — linked to the user, used for analytics, **not** tracking. Plus anything added as traits (email, name).
- Strict Anonymous: Product Interaction, not linked to the user. Remove the Device ID and User ID entries from the app's own privacy manifest answers.
- Crash data counts as Other Diagnostic Data.
- Tapll uses no IDFA and needs no App Tracking Transparency prompt. Do not describe the data as "fully anonymous" in the app's privacy copy unless the project is Strict Anonymous.

## Verify

1. The project builds (`xcodebuild` for the app scheme, or build in Xcode) with no new warnings from the integration.
2. Run a DEBUG build in the simulator. With `logLevel = .debug` the SDK logs to the unified log under subsystem `app.tapll.sdk` (visible in the Xcode console). Invalid events are logged there with the reason — fix every one.
3. Background the app (or wait 10 s) to flush. In the Tapll panel, switch the environment to **Sandbox** — a DEBUG build is `development`, which the Production view never shows — and open **Live**. The session should appear within seconds.
4. Walk through each planned feature once and confirm its events show up under **Data** with the declared feature and role.

## Final report

Finish with:
- The files you changed.
- The event catalog as a table: event | feature | role | properties | where it fires.
- What you deliberately did **not** instrument, and why.
- What is left for a human: App Store privacy answers, the `TAPLL_SERVER_KEY` CI secret for dSYM upload, connecting purchases in Tapll Settings, and anything in the plan they still need to decide.

And the website behind it. One script tag, no cookie by default.

Under 5 KB gzipped, no dependencies. Page views, outbound links, downloads and single-page navigation are tracked for you.

tapll.com/panel/acme/fitflow/traffic
Website traffic in the Tapll panel
Add it to the head
<script defer src="https://cdn.tapll.com/v0/u.js"
        data-key="av_…"></script>
With identity and the built-in banner
<script defer src="https://cdn.tapll.com/v0/u.js"
        data-key="av_…"
        data-mode="product_analytics"></script>

Until a visitor chooses, nothing is stored or sent, and Accept and Decline carry equal weight. Served from cdn.tapll.com/v0/u.js.

  1. 01

    Traffic and sources

    Visitors, visits, page views, bounce rate and visit duration against the previous period, with channels, referrers, UTM campaigns and entry and exit pages.

  2. 02

    One project for app + site

    The website joins your app’s project: same key, same privacy mode, and a platform switcher on Sessions, Screens, Retention, Crashes and Data.

  3. 03

    Cookieless by default

    Nothing is stored in the browser, so no consent banner is needed. For retention and identity, switch to product analytics and use the optional built-in banner.

Your SDK is not the only source. The website, revenue, the App Store and NPS land next to it.

Every source keeps its own definition and its own clock. A number from Apple says it is from Apple, and a day Apple has not reported yet shows as a gap, not a zero.

  • Revenue from RevenueCat or Apple’s own notifications, counted once — never both
  • Downloads, ratings and keyword positions from the App Store, on their own report days
  • Stripe and Paddle for the website, tied to the visit’s channel and kept apart from in-app money
  • NPS from Survicate: the score, never the comment

Integrations

Questions it answers without building a report.

Simple pricing. Pay for events.

Plans differ only by monthly events and how long data is kept. Every view and integration is in every plan.

Free

€0/ month

  • 100,000 events a month
  • Session detail kept 30 days
  • Daily aggregates kept 6 months
  • Every view, every integration
Start for free
SoloMost popular

€9/ month

or €90 a year

  • 1,000,000 events a month
  • Session detail kept 90 days
  • Daily aggregates kept 24 months
  • Every view, every integration
Choose Solo
Pro

€29/ month

or €290 a year

  • 5,000,000 events a month
  • Session detail kept 180 days
  • Daily aggregates kept 36 months
  • Every view, every integration
Choose Pro
Studio

€79/ month

or €790 a year

  • 20,000,000 events a month
  • Session detail kept 365 days
  • Daily aggregates kept 60 months
  • Every view, every integration
Choose Studio

What people ask before they install.

How is Tapll different from Firebase or Mixpanel?

You do not build reports. The panel is a fixed set of answers: who is in the app now, what their sessions looked like, whether a feature is used to the end, whether a release helped, and what goes with subscriptions. Every number has a written definition and a comparison basis.

What about privacy?

Two modes, set per project. Strict anonymous keeps no identifier across sessions and counts aggregates only; product analytics uses a pseudonymous id (an install id in the app). On a website, strict anonymous stores nothing in the browser, so no consent banner is needed, and product analytics stores an id and can show a built-in consent banner. IP addresses are truncated before they are stored, and the mode is enforced when events arrive, not in the panel.

How does Live work?

The SDK sends events in small batches; each one is stored and pushed to your project’s live view over a WebSocket. An event is usually on screen within a couple of seconds of leaving the phone.

Why does Tapll say “correlation” and never “impact”?

A before/after comparison around a release includes every version active in the window, seasonality and your marketing. Tapll shows the difference and how it was measured, and never calls it the cause.

Is there crash reporting? Session replay?

Crash reporting, yes: crashes are grouped into issues with a crash-free rate per release, and dSYMs uploaded from CI name the stack frames. Video session replay is not part of Tapll. Nor are heatmaps, Web Vitals or JavaScript error tracking on websites.

Can one project cover my app and my website?

Yes. An app and its website share one project, one write key and one privacy mode. Platform is a dimension of the data, and Sessions, Screens, Retention, Crashes and Data have a switcher per platform. Website money from Stripe or Paddle is never added to in-app money.

Do I need a cookie banner on my website?

Not in strict anonymous mode, the default: the script stores nothing in the browser, and visitors are recognised for one day by a server-side hash. In product analytics mode the script stores an id, so ask first; the optional banner (`data-consent="banner"`) gives Accept and Decline equal weight. This describes what the script does and is not legal advice.

Which platforms are supported?

iOS, through a Swift package. Your own backend can send events too, through a separate server endpoint. Websites use one script tag, cookieless by default, and share a project with their app. Not published yet: Android, Flutter and React Native.

Where is my data stored?

On Cloudflare (Workers, D1 and R2). Session detail is kept for your plan’s retention window and daily aggregates for longer; both windows are listed on the pricing page.

See who is on your app and site right now. No analyst, no query building.