Features

Analytics

You can't grow what you can't measure — but most analytics SDKs make you choose between insight and privacy. ShipThatApp ships with TelemetryDeck, which gives you both. It's a privacy-first product-analytics service that collects no personally identifiable information, no IP addresses, and no IDFA — so there's no ATT prompt to tank your opt-in rate and nothing to declare beyond the basics. It's already initialized at launch and already firing events at the moments that matter most: your purchase funnel and your review prompt.

Ship without a consent gate

Because TelemetryDeck never collects PII or device identifiers, you don't need an App Tracking Transparency prompt to use it. Still: describe your data practices in your privacy policy and App Store privacy nutrition label, and comply with local law. Privacy-friendly is not a substitute for being upfront.

How It's Wired

The integration lives in two spots:

  • InitializationShipThatAppApp.configureTelemetry(), called from the app's init() before any view loads.
  • Event sitesTelemetryManager.send(...) calls inside PurchaseManager, Paywall3View, and LandingView.

The SDK is the TelemetryClient package; you import it as import TelemetryClient and call the static TelemetryManager.

Configuration

The TelemetryDeck App ID is read from Info.plist — populated from a gitignored Config.xcconfig (TD_APP_ID) — so your key never lands in source control:

private func configureTelemetry() {
    guard let telemetryDeckAppID = Bundle.main.infoDictionary?["TD_APP_ID"] as? String else { return }
    let configuration = TelemetryManagerConfiguration(appID: telemetryDeckAppID)
    TelemetryManager.initialize(with: configuration)
}

This is invoked from the app entry point so analytics is live from the very first frame:

@main
struct ShipThatAppApp: App {
    init() {
        configureTelemetry()
        configureRevenueCat()
    }
}

If TD_APP_ID is missing the guard simply returns and the app runs without analytics — no crash, no noise. Grab your App ID from the TelemetryDeck dashboard.

Sending Events

Send a signal with TelemetryManager.send(_:). Attach structured properties with the with: parameter to slice the data later:

// A plain signal
TelemetryManager.send("succesfullySubscribe")

// A signal with attached payload
TelemetryManager.send("purchaseError", with: ["error": error.localizedDescription])

That's the entire surface area you need for product analytics — name the moment, optionally attach context, and TelemetryDeck handles batching, retry, and delivery.

Events Already Instrumented

ShipThatApp doesn't leave the instrumentation as an exercise — the high-value funnel is wired up out of the box. These are the exact signals the app emits today:

Purchase funnel
  clickedSubscribe          // user tapped Subscribe          (Paywall3View)
  succesfullySubscribe      // verified purchase completed     (PurchaseManager)
  cancelledSubscribe        // user cancelled the StoreKit sheet (PurchaseManager)
  purchaseError             // purchase threw                  (Paywall3View) + { error }
  loadProductsError         // products failed to load         (Paywall3View) + { error }

Restore
  clickedRestorePurchase    // user tapped Restore             (Paywall3View)
  restoreError              // AppStore.sync() threw           (Paywall3View) + { error }

Engagement
  reviewRequested           // review prompt shown             (LandingView)   + { version }

Out of the box this is enough to build a conversion funnel — clickedSubscribe → succesfullySubscribe is your paywall conversion rate, cancelledSubscribe plus the error signals tell you where buyers drop off, and reviewRequested confirms the rating prompt is firing for the right cohort. See In-App Purchases for where each purchase signal lives and Review Requests for the prompt logic behind reviewRequested.

Mind the existing event names

A couple of shipped event names contain typos (succesfullySubscribe, cancelledSubscribe). Event names are matched verbatim in the dashboard, so don't "fix" them in one place and leave the dashboard insight pointing at the old spelling — rename in both, or leave them as-is. Consistency beats correctness here.

Adding Your Own Events

Drop a send at any moment you want to understand — a feature first-use, an empty state, a key completion. Keep names stable and lowerCamelCase so they group cleanly:

import TelemetryClient

// Feature engagement
TelemetryManager.send("openedDexScanner")

// Outcome with context
TelemetryManager.send("scanCompleted", with: [
    "category": result.category,
    "offline": String(usedOfflineFallback)
])

Good places to instrument in this codebase:

  • AI features — a send("ranChatPrompt") in the chat flow, or send("scanCompleted") after a Dex Scanner identification, tells you which AI feature actually drives retention.
  • Onboarding — fire a signal as each onboarding step is completed to find your drop-off step.
  • Lifecyclesend("appLaunched") near configureTelemetry() if you want raw session counts.

Never put PII in a signal

TelemetryDeck's privacy guarantee only holds if you uphold it. Don't pass emails, names, raw user input, or anything personally identifying in the with: payload — send categories, counts, booleans, and enums instead. The whole point is analytics you can run without a consent gate.

Visualizing the Data

Every signal shows up in the TelemetryDeck dashboard, where you turn raw events into insights — funnels, retention curves, and breakdowns by the properties you attached (app version, category, error string). Build a funnel on clickedSubscribe → succesfullySubscribe to watch paywall conversion, or chart purchaseError grouped by its error payload to spot a broken product configuration before reviews do.

Customize / Extend It

  • Test vs. production data. Use a separate TelemetryDeck App ID for Debug builds (set a different TD_APP_ID per configuration in your .xcconfig) so test runs never pollute production insights.
  • Standardize event names. Centralize signal names in a small enum or constants file and call TelemetryManager.send(MyEvents.subscribed) — typos like the shipped ones disappear and the dashboard stays clean.
  • Swap the provider. Every call site funnels through TelemetryManager.send. To move to another analytics backend, wrap that single API in your own Analytics.track(_:_:) helper and rewrite the call sites once.
Previous
In-App Purchases