Quickstart
From an empty project to seeing yourself on the dashboard in about five minutes. Seven steps, each with something you can check. Already integrated and looking for a parameter? The reference is at /docs.
On this page
🤖Using Claude Code, Cursor or Copilot? Paste this prompt and skip to step 4.Shortcut
The prompt does steps 1-3 for you, then interviews you about your screens, paywall and
core actions and implements the events, a funnel and, only if you want, user identification. You still
need a write key: create your app in the dashboard and replace
glance_live_… in the prompt, or let the agent ask you for it.
- Adds the Swift packageGradle dependency and the setup linesline, gated so your test runs never touch your live numbers
- Interviews you about your screens, paywall and core actions, then implements the events, and runs the app to prove they arrive
- Hands back a funnel link that saves itself into your dashboard, and your App Store privacyPlay Data safety answers
You are integrating AppGlance (privacy-first, live analytics for iOS and Android apps) into this Xcode project. Do the whole setup, interview me about what to track, implement it, and prove it works before you call it done. Work in small, reviewable steps and show me the diff before committing anything.
SWIFT PACKAGE: https://github.com/AppGlance/appglance-apple (module: AppGlance, version 1.2.4 or later, "Up to Next Major")
WRITE KEY (write-only, safe to ship in the binary): glance_live_… (if this still says glance_live_…, ask me for the key; it is in the dashboard under the app's Setup tab: https://appglance.app/app/)
DASHBOARD: https://appglance.app/app/
1. Add the Swift package to the app target (File → Add Package Dependencies…, or the project.yml / Package.swift if this project generates its Xcode project). Import it where needed with `import AppGlance`.
2. In the @main App's init(), as early as possible:
#if DEBUG
AppGlance.configure(apiKey: "glance_live_…", debug: true)
#else
AppGlance.configure(apiKey: "glance_live_…")
#endif
Attach `.trackAppLifecycle()` to the root view inside WindowGroup. This gives sessions, "active right now", installs and retention automatically. If the app is UIKit-based, call `AppGlance.setActive(true/false)` from the scene's foreground/background delegates instead.
Why the #if: Debug and Simulator builds send nothing by default (TestFlight and App Store do), which is the right default for shipping but makes a fresh integration look broken. `debug: true` makes this build send: events tagged simulator/debug, visible under the dashboard's dedicated "Debug" scope (and under "All"), never in Live, and log every event and send to the console. Gating it on #if DEBUG means there is nothing for me to remember to remove before I ship.
3. Interview me (ask, don't guess; a few short questions, one message):
a. What are the 3-6 key screens or steps a user moves through? (I'll get `.trackScreen("name")` on each, e.g. onboarding, paywall, the main screen.)
b. Is there a paywall / purchase (StoreKit 2, RevenueCat…)? Where does a purchase or trial succeed?
c. What is the core "job done" action (workout completed, entry saved, export finished…)? Any small, non-personal detail worth attaching (type, count, plan)?
d. Do users sign in / have accounts? If so, do I want to put a name or email on their install with identify (it changes the App Store privacy answers), or keep everyone anonymous?
e. Anything else worth counting (a feature toggled on, a share, an error the user sees)?
4. Implement the tracking plan from my answers, using ONLY these APIs:
a. `AppGlance.track("event.name", metadata: ["key": "value"])`. Event names lowercase, dot.separated, stable, ≤80 chars (e.g. paywall.viewed, purchase, trial.started, workout.completed, checkin.logged). Metadata is [String: String], values under 200 characters, at most 20 keys. Never put names, emails, ids or free text a user typed into event names or metadata.
b. Names are a small fixed set, never built from a value: `purchase` with metadata ["product": "pro.yearly"], never `purchase.pro.yearly` or `purchase.9.99`. A name that carries a value turns the Events tab into a list of one-offs that no chart, funnel or alert can use.
c. `.trackScreen("paywall")` on the SwiftUI views for the key screens (records screen.paywall on appear).
d. If there is a paywall, track `paywall.viewed`, and `purchase` / `trial.started` when they complete (with the product id as metadata), and tell me, because that changes the App Store privacy answers (Purchases → Purchase History).
e. Do NOT call AppGlance.identify unless I said yes in 3d; users are anonymous by default and that is the point. If I said yes: on sign-in call `AppGlance.identify(id: <account id>, email: <email>, name: <name>)` with only the fields I approved, and `AppGlance.reset()` on sign-out. Nothing the person didn't give the app.
f. You may propose user properties (`AppGlance.setUserProperties(["plan": "pro"])`) for things I would want to filter users by (plan, cohort, goal…), but ask before adding them. Values are short strings; they show up as filterable pills in the dashboard's Users tab.
g. Call the API directly where the thing happens: no wrapper protocol, no AnalyticsService layer, no event enum, and don't route it through any analytics SDK the project already has (leave that one exactly as it is). Analytics calls must never block the UI, throw, or run in unit/UI tests. Don't add a consent banner or an ATT prompt, because nothing here is tracking in Apple's sense.
5. Prove it works before you tell me it's done. Build and run on the Simulator and read the Xcode console:
- `[AppGlance] ✓ sent …` → it works. Tell me to open the dashboard with the scope (top right) set to All and watch "Active right now" go to 1.
- a line starting `not sending:` → the #if DEBUG branch above isn't the one running.
- `✕ HTTP 401` → wrong or rotated write key; ask me for it again.
- nothing from AppGlance at all → configure isn't running at launch, or the package isn't linked to this target.
Fix the cause. Never paper over it with a retry, a second configure call, a manual flush or a Task.sleep.
6. When done, reply with:
- the list of events you added, one line each: name, where it fires, metadata keys;
- a suggested funnel of 2-6 of those events in order, as this link (replace <app id> with the id from my dashboard URL, and the names): https://appglance.app/app/#app/<app id>/events?funnel=paywall.viewed,purchase (opening it saves the funnel in my dashboard);
- what my App Store privacy answers should be, based on what you implemented. Default: Identifiers → User ID and Usage Data → Product Interaction, not linked to the user, tracking No, no ATT prompt; add Purchases → Purchase History if you tracked purchases; if identify is used, add Contact Info and everything becomes "linked to the user". The dashboard's Setup tab generates the exact ticks.
Documentation: https://appglance.app/docs · this quickstart: https://appglance.app/quickstart. Keep it minimal: two lines of setup and a track call where the thing actually happens is the whole design.You are integrating AppGlance (privacy-first, live analytics for iOS and Android apps) into this Android project. Do the whole setup, interview me about what to track, implement it, and prove it works before you call it done. Work in small, reviewable steps and show me the diff before committing anything.
GRADLE DEPENDENCY: implementation("app.appglance:appglance:1.2.4") (Kotlin, minSdk 21, package app.appglance). It resolves from Maven Central, which a default Android project already has in its repositories, so no extra setup. If it does not resolve, tell me rather than guessing at a different coordinate or adding mavenLocal(); the coordinate above is correct.
WRITE KEY (write-only, safe to ship in the binary): glance_live_… (if this still says glance_live_…, ask me for the key; it is in the dashboard under the app's Setup tab: https://appglance.app/app/)
DASHBOARD: https://appglance.app/app/
1. Add the dependency to the app module. Nothing to add to the manifest (INTERNET comes with the library). Import with `import app.appglance.AppGlance`.
2. In the Application subclass's onCreate() (create one and register it in the manifest if the app doesn't have one), as early as possible:
AppGlance.configure(this, "glance_live_…", debug = BuildConfig.DEBUG)
Sessions, "active right now", installs and retention are automatic (the SDK watches ProcessLifecycleOwner), so there is nothing to attach to activities.
`debug = BuildConfig.DEBUG` is there so I can verify the integration from Android Studio right away: debug builds send (events tagged emulator/debug, visible under the dashboard's dedicated "Debug" scope and under "All", never in Live) and the SDK narrates to logcat under the tag AppGlance. Release builds behave normally: emulator and debuggable builds never send anything, production and beta builds do, and that default is the right one for shipping.
3. Interview me (ask, don't guess; a few short questions, one message):
a. What are the 3-6 key screens or steps a user moves through? (I'll get `AppGlance.trackScreen("name")` on each, via a Compose LaunchedEffect or onResume, e.g. onboarding, paywall, the main screen.)
b. Is there a paywall / purchase (Play Billing, RevenueCat…)? Where does a purchase or trial succeed?
c. What is the core "job done" action (workout completed, entry saved, export finished…)? Any small, non-personal detail worth attaching (type, count, plan)?
d. Do users sign in / have accounts? If so, do I want to put a name or email on their install with identify (it changes the Play Data safety answers), or keep everyone anonymous?
e. Anything else worth counting (a feature toggled on, a share, an error the user sees)?
4. Implement the tracking plan from my answers, using ONLY these APIs:
a. `AppGlance.track("event.name", mapOf("key" to "value"))`. Event names lowercase, dot.separated, stable, ≤80 chars (e.g. paywall.viewed, purchase, trial.started, workout.completed, checkin.logged). Metadata is Map<String, String>, values under 200 characters, at most 20 keys. Never put names, emails, ids or free text a user typed into event names or metadata.
b. Names are a small fixed set, never built from a value: `purchase` with metadata mapOf("product" to "pro.yearly"), never `purchase.pro.yearly` or `purchase.9.99`. A name that carries a value turns the Events tab into a list of one-offs that no chart, funnel or alert can use.
c. `AppGlance.trackScreen("paywall")` where the key screens appear (records screen.paywall). In Compose put it in a `LaunchedEffect(Unit)` so a recomposition doesn't record it twice.
d. If there is a paywall, track `paywall.viewed`, and `purchase` / `trial.started` when they complete (with the product id as metadata), and tell me, because that changes the Play Data safety answers (Purchase history).
e. Do NOT call AppGlance.identify unless I said yes in 3d; users are anonymous by default and that is the point. If I said yes: on sign-in call `AppGlance.identify(id = <account id>, email = <email>, name = <name>)` with only the fields I approved, and `AppGlance.reset()` on sign-out. Nothing the person didn't give the app.
f. You may propose user properties (`AppGlance.setUserProperties(mapOf("plan" to "pro"))`) for things I would want to filter users by (plan, cohort, goal…), but ask before adding them. Values are short strings; they show up as filterable pills in the dashboard's Users tab.
g. Call the API directly where the thing happens: no wrapper interface, no AnalyticsService layer, no event sealed class, and don't route it through any analytics SDK the project already has (leave that one exactly as it is). Analytics calls must never block the UI, throw, or run in unit/instrumented tests. Don't add a consent banner, because nothing here uses the advertising id, IP or location.
5. Prove it works before you tell me it's done. Run the app on an emulator or a device and read logcat, filtered on the tag AppGlance:
- `✓ sent …` → it works. Tell me to open the dashboard with the scope (top right) set to All and watch "Active right now" go to 1.
- a line starting `not sending:` → configure isn't getting `debug = BuildConfig.DEBUG`, or it's a release build.
- `✕ HTTP 401` → wrong or rotated write key; ask me for it again.
- nothing from AppGlance at all → configure isn't running (is the Application subclass registered with android:name in the manifest?), or the dependency isn't on this module.
Fix the cause. Never paper over it with a retry, a second configure call, a manual flush or a delay.
6. When done, reply with:
- the list of events you added, one line each: name, where it fires, metadata keys;
- a suggested funnel of 2-6 of those events in order, as this link (replace <app id> with the id from my dashboard URL, and the names): https://appglance.app/app/#app/<app id>/events?funnel=paywall.viewed,purchase (opening it saves the funnel in my dashboard);
- what my Google Play Data safety answers should be, based on what you implemented. Default: Device or other IDs (the random install id) and App interactions: collected, not shared, encrypted in transit, deletable on request, purpose Analytics; add Purchase history if you tracked purchases; if identify is used, add Personal info → Name / Email address.
Documentation: https://appglance.app/docs · this quickstart: https://appglance.app/quickstart?platform=android. Keep it minimal: one line of setup and a track call where the thing actually happens is the whole design.
1Create your app, copy the key 30 seconds
Sign up (free, no card) → New app → its name and
bundle idpackage name → open the app's
Setup tab → copy the write key. It looks like glance_live_….
glance_live_.Why is it safe to put this key in my app?
It's a write key: it can only append events to this one app's stream, never read anything, which is why it's fine to ship inside your binary, and why there's nothing to keep out of a public repo besides good taste. Rotate it any time from the same tab; the old one stops working within a minute.
One app in the dashboard = one thing you ship. If you have an iOS and an Android version of the same product, use one app and one key for both; the dashboard splits by OS where it matters.
2Add the package 1 minute
Xcode → File → Add Package Dependencies… → paste the URL → Dependency Rule
Up to Next Major Version from 1.2.4 → Add Package → tick the
AppGlance library for your app target.
https://github.com/AppGlance/appglance-appleimport AppGlance compiles.What's in the package?
One Swift package, no dependencies. iOS 16+, watchOS 9+, macOS 13+, tvOS 16+, visionOS 1+. It ships its own
PrivacyInfo.xcprivacy, so Xcode's privacy report already knows what it collects. Using
Package.swift or XcodeGen instead of the Xcode UI? Add
.package(url: "https://github.com/AppGlance/appglance-apple", from: "1.2.4") and the product AppGlance.
In your app module's build.gradle.kts:
dependencies {
implementation("app.appglance:appglance:1.2.4")
}import app.appglance.AppGlance resolves.What's in the library?
A Kotlin library, minSdk 21, one dependency (androidx.lifecycle:lifecycle-process). That is how
sessions and "active right now" work without you touching an Activity. The INTERNET permission comes with the
library's manifest; nothing else to declare. No advertising id, no location, no Play Services.
3Configure, the whole integration 1 minute
Once, as early as possible, in your App's init, plus one modifier on the root view:
import AppGlance
@main
struct MyApp: App {
init() {
AppGlance.configure(apiKey: "glance_live_…") // the write key from step 1
}
var body: some Scene {
WindowGroup {
RootView()
.trackAppLifecycle() // sessions · active right now · installs · retention
}
}
}What do these two lines do? (and UIKit)
configure is the entire hosted setup. No plist keys, no build phase, no delegate.
.trackAppLifecycle() turns launches and returns from the background into sessions, keeps the quiet
"still here" presence ping going while the app is open (that's what Active right now is made of),
records install once, and flushes on the way out.
UIKit app? Skip the modifier and call AppGlance.setActive(true) /
setActive(false) from your scene delegate's foreground / background callbacks.
Calls before configure are not lost: the 200 most recent are held and replayed in order, which is more than any launch makes.
Once, in your Application's onCreate:
import app.appglance.AppGlance
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
AppGlance.configure(this, "glance_live_…") // the write key from step 1
}
}No Application class yet? And what does the line do?
Create one and register it: <application android:name=".MyApp" …> in the manifest.
configure is the entire hosted setup. Sessions are automatic, because the SDK watches the app's foreground/background
state through ProcessLifecycleOwner: session.start when the app comes to the front after more than
five minutes away, a quiet "still here" presence ping at your plan's cadence while it's in front (that's what Active right
now is made of), install once, a flush when it leaves. Nothing to attach to activities. Prefer to drive it
yourself? trackAppLifecycle = false in the configuration and call AppGlance.setActive(true/false).
Calls before configure are not lost: the 200 most recent are held and replayed in order, which is more than any launch makes.
4Run it, the checkpoint 2 minutes
⚠️ Before you hit Run: Simulator and Debug buildsemulator and debuggable builds send nothing by default.
That is deliberate. enabledEnvironments defaults to App Store + TestFlightproduction + beta,
so a hundred XcodeAndroid Studio runs never touch your numbers. It also means that if you hit
Run right now, the dashboard stays empty and it looks broken. So for this first run, turn on debug mode:
#if DEBUG
AppGlance.configure(apiKey: "glance_live_…", debug: true)
#else
AppGlance.configure(apiKey: "glance_live_…")
#endif
AppGlance.configure(this, "glance_live_…", debug = BuildConfig.DEBUG)
This build now sends, and its events are tagged simulator / debug, so they show under
the dashboard's dedicated Debug scope (and under All), never in Live, and the SDK narrates to the Xcode console. The
#if is why there is nothing to remember to remove before you ship: a Release build compiles the
plain call and behaves exactly as before. Testing on TestFlight
instead? It sends by default; flip the dashboard scope to TestFlight.Debug builds
now send, and their events are tagged emulator / debug, so they show under the dashboard's dedicated Debug
scope (and under All), never in Live, and the SDK narrates to logcat (tag AppGlance). Release builds are untouched: BuildConfig.DEBUG
is false there, so nothing changes when you ship. Testing an internal-track build instead? Say environment = AppEnvironment.BETA
in that flavor and flip the dashboard scope to Beta.
Now hit Run. In the Xcode consoleIn logcat, filtered on AppGlance, you'll see, in order:
[AppGlance] debug mode on · environment: simulator · sending to api.appglance.app · install id 9A0C…
[AppGlance] events from this build are tagged "simulator": they show under All in the dashboard, never in Live
[AppGlance] ▸ install
[AppGlance] ▸ session.start
[AppGlance] ↑ sending 2 events…
[AppGlance] ✓ sent 2 events · on the dashboard now (scope: All)
I/AppGlance: debug mode on · environment: emulator · sending to api.appglance.app · install id 9A0C…
I/AppGlance: events from this build are tagged "emulator": they show under All in the dashboard, never in Live
I/AppGlance: ▸ install
I/AppGlance: ▸ session.start
I/AppGlance: ↑ sending 2…
I/AppGlance: ✓ sent 2 · they are on the dashboard now (scope: All shows every environment)
Open the dashboard, set the scope (top right) to All, and watch Active right now go to 1. That's you. The app's Setup tab flips its badge to green at the same moment.
✓ Active right now = 1 · Setup badge green · console says ✓ sent.
That's the moment. Everything past here is optional.
Nothing after fifteen seconds?
- Console says
not sending: this is a Simulator runan emulator…→ you skipped the callout above; adddebug: truedebug = BuildConfig.DEBUG. - Console says
✕ HTTP 401→ wrong or rotated key; copy it again from Setup. - Nothing from
AppGlanceat all →configureisn't running (is it ininit()Application.onCreate(), and is the Application class registered in the manifest?), or the package isn't linked to this targetmodule. - Console says
✓ sentbut the tile is 0 → the dashboard scope is still on Live; switch to All. - Emulator with no network? Events wait on disk and go out on the next try.
⟳ couldn't send … keeping 2 for the next tryis the SDK doing the right thing.
5Track your first event 1 minute
Anything you name, from anywhere in the app:
AppGlance.track("paywall.viewed")
AppGlance.track("workout.completed", metadata: ["type": "run"])
AppGlance.track("paywall.viewed")
AppGlance.track("workout.completed", mapOf("type" to "run"))
▸ paywall.viewed then ✓ sent, and the event is in the
app's Events tab (scope All) within about ten seconds.Naming rules, metadata, screens
New names appear in your app's Events tab on their own, with no schema and no setup, and any of them can be
charted from the Overview, used as a funnel step, or wired to a push alert. Names: lowercase, dot.separated, stable.
Metadata is small string context (at most 20 keys). Never put personal data in a name or metadata;
that's what identify is for (see the reference).
Screens are the cheapest funnel: .trackScreen("paywall") on a SwiftUI viewAppGlance.trackScreen("paywall") in a Compose LaunchedEffect or onResume
records screen.paywall each time it appears; two or three of those and the Funnel panel has something to work with.
6Get alerts on your phone 1 minute
Dashboard → your app → Alerts → pick what deserves a push: new install, purchase, or any signal you named in step 5. Then choose where it goes:
- ntfy or Discord. Paste a topic or a webhook URL; works on any phone, and it's the quickest way to get a buzz today.
- Your own server. A signed JSON webhook. How to verify it →
- The AppGlance app for iPhone. Native push, the live feed and your users in your pocket. Free on every plan. Get it on the App Store →
7Ship it and you're done
Remove debug: true (or gate it on #if DEBUG). App Store and TestFlight builds send by
default; nothing else to flip.Nothing to remove, because debug = BuildConfig.DEBUG is false in a
release build. Production builds send by default; for Play internal/closed testing set
environment = AppEnvironment.BETA in that flavor so testers land under Beta, not Live.
Your App Store privacy answersGoogle Play Data safety answers with the default setup: Identifiers → User ID and Usage Data → Product Interaction, not linked to the user, tracking No, no ATT prompt, no consent banner. The Setup tab lists the exact ticks.Device or other IDs (the random install id) and App interactions: collected, not shared, encrypted in transit, deletable on request, purpose Analytics. No advertising id, no location, no consent banner. Details, and what changes with identify →