FirstMRRHelp Center
Developers

Mobile apps (iOS and Android)

A one-file module, in Swift or Kotlin, that sends opens and conversions without any advertising id.

1 min read

The request

POSThttps://firstmrr.app/app/api/collectPublic key in the body (k)
FieldTypeRequiredDescription
kstringRequiredYour public key.
estringRequiredThe event: open or screen (counted as a visit), signup, activation, trial, payment, cancel, source, ab:exposure, or a name of your own.
astringRequiredThe install id: a random UUID created at the first launch.
pstringOptionalThe screen shown (/home).
wnumberOptionalThe screen width in points (tablet from 600).
v / c / inumber / string / stringOptionalPayment: amount, currency, transaction id.
s / dstring / stringOptionalOrigin answer and account named.
abobject[]OptionalA/B assignments, [{ "test": "pricing_page", "variant": "b" }], 4 at most.

The answer is always 204, without a body: don't read it and don't retry. Send without blocking the UI, 5 seconds at most.

The module

import UIKit

enum FirstMRR {
    private static let endpoint = URL(string: "https://firstmrr.app/app/api/collect")!
    private static let key = "pk_0123456789abcdef01234567" // your public key
    private static let defaults = UserDefaults.standard

    /// A random id created at the first launch: never the IDFA or identifierForVendor.
    static var installId: String {
        if let id = defaults.string(forKey: "firstmrr.installId") { return id }
        let id = UUID().uuidString
        defaults.set(id, forKey: "firstmrr.installId")
        return id
    }

    /// The "Audience measurement" setting of your app.
    static var enabled: Bool {
        get { !defaults.bool(forKey: "firstmrr.optOut") }
        set { defaults.set(!newValue, forKey: "firstmrr.optOut") }
    }

    static func track(_ event: String, screen: String = "/", extra: [String: Any] = [:]) {
        guard enabled else { return }
        var body: [String: Any] = ["k": key, "e": event, "a": installId, "p": screen, "w": Int(UIScreen.main.bounds.width)]
        body.merge(extra) { _, new in new }
        guard let data = try? JSONSerialization.data(withJSONObject: body) else { return }
        var request = URLRequest(url: endpoint, timeoutInterval: 5)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = data
        URLSession.shared.dataTask(with: request).resume() // fire and forget: no retry, no queue
    }
}

// FirstMRR.track("open", screen: "/home")
// FirstMRR.track("payment", extra: ["v": 9.99, "c": "USD", "i": transaction.id])

Call track("open") on every open and return to the foreground, signup, activation, trial, then payment in the success callback of StoreKit, Google Play Billing, RevenueCat or Superwall.

Apple and Google review

  • No IDFA, AAID, identifierForVendor or device fingerprint: no App Tracking Transparency prompt.
  • An "Audience measurement" setting, on by default, that stops every send.
  • PrivacyInfo.xcprivacy: Product Interaction and Device ID, not linked to identity, no tracking, Analytics purpose; Purchase History if you send payments; UserDefaults with reason CA92.1.

The assisted installation in Distribution → Tracking writes this module for you and tells you what to declare in App Store Connect and Google Play.