Instrumentation · working document · August 2026
What to count,
and what never
to touch

The standard App Store playbook assumes a subscription business with accounts and an ad budget. You have neither: v1 is a one-time unlock, there are no accounts, sync is the user's own iCloud, and "your photos stay on your phone" is a selling point. So most of that playbook's machinery — grace periods, win-back offers, save rates, attribution windows — is not applicable yet, and pretending otherwise would have you building plumbing for a business you don't run.

One north star, and it isn't downloads or revenue: median objects catalogued per user at day 30. Every other number on this page exists to explain a movement in that one.

The funnel order from the playbook holds, translated: depth → conversion → discovery → spend. Fix why people stop cataloguing before you optimise a screenshot, and don't spend a euro on ads until the first two are healthy.

01 · The constraint
Privacy-first analytics
is a design decision

Your app's whole trust argument is that it doesn't ship people's collections anywhere. An analytics SDK that phones home with object names would make that a lie, and it's the one lie a collector community would never forgive. So the rule is absolute and cheap to hold: you log that a thing happened and its shape, never what the thing was.

ALLOWED
Counts, kinds, buckets
Event name, type slug (vinyl, coins), a bucketed count, method used, storefront, app version, a rotating install id. Enough to answer every question on this page.
NEVER — WRITE THIS INTO THE PRIVACY PAGE
Object names, photos, prices, people, locations
No titles, artists, barcodes or catalogue numbers. No image, ever. No price paid, no valuation, no total. No lender names or contacts. No precise location. No third-party ad or analytics SDK. No device fingerprint.
HOW
Two sources, no SDK
App Analytics for everything store-side (impressions, page views, conversion, downloads, proceeds, D1/D7/D28, benchmarks) — free, already there, no code. Plus one first-party endpoint of your own for the in-app events below: a JSON POST, batched, retried, droppable. A day of work, no vendor, nothing to disclose beyond "anonymous usage counts".

Bucket every count before it leaves the device — 1–9 / 10–49 / 50–99 / 100–249 / 250–999 / 1000+. A raw "4,187 objects, type: synths, FR" is close to identifying one person; a bucket answers the same question and can't. Same for durations and prices: buckets only, and prices only as ranges, only in v2.

02 · The event taxonomy
Thirty-six events,
eight properties

Naming is noun_verb_past, always. One event per real user action — never one per screen, never one per tap. If you can't name the question an event answers, don't ship it: dead events cost you schema churn forever and answer nothing.

EVENT
FIRES WHEN
PROPERTIES
THE QUESTION IT ANSWERS
A
ACTIVATION — the first ten minutes
first_open
First launch, once ever
storefront, app_version, source_page
Denominator for everything. source_page carries which custom product page they came through.
detail_bar_set
Onboarding question answered
level, skipped
Which of the three audiences actually installs — and how many skip the question entirely.
collection_created
A collection is made
type, is_mixed, is_custom
Which of the fourteen types are real demand, and how often none of them fit.
first_object_added
First object ever, once
method, minutes_since_first_open
The single most important activation number: how long until one object exists.
B
CATALOGUING — the core loop
object_added
Any object saved
method, type, seconds_bucket, from_review
Everything. method = photo / barcode / manual / import settles the photo-first bet.
lookup_requested
A photo or barcode is sent out
kind, type
Your variable cost, per user, per type. Sizes the lookup packs.
lookup_resolved
Match returned
kind, type, outcome, confidence_bucket
Match quality by type — and which types have no usable provider.
lookup_rejected
User refuses the match
kind, type
Whether the catalogue is trustworthy. A high rate here is a product emergency.
import_started
A file is picked
source, rows_bucket
Which export formats to build next — ranked by real usage, not guesses.
import_completed
Import lands
source, rows_bucket, matched_pct_bucket
The highest-leverage feature you have. Completion rate versus started is the whole verdict.
review_item_resolved
A queued guess is confirmed or edited
action, type
Whether the review queue gets worked or abandoned.
field_filled
A type-specific field gets a value
field_kind, type, control
Which fields people actually fill — the evidence for trimming fourteen schemas.
capture_3d_completed
A 3D capture finishes
type, abandoned
Whether the 3D object is a real feature or a demo. Watch the abandon rate hard.
C
DEPTH & RETURN — is it a tool or a toy
session_started
Foreground after 30 min idle
objects_bucket, days_since_first_open
Retention with collection size attached — the pairing that predicts payment.
object_opened
Detail screen opened
type, tab
Is anyone reading History? If not, your differentiator isn't landing.
search_performed
A query runs
token_count, result_bucket, zero_results
Whether search earns the Find tab. Zero-result rate is your query-language bug list.
sleeve_rotated
3D object dragged
type
Is the thing you spent weeks on being touched? Honest answer welcome.
tab_bar_edited
Slots reordered in settings
slots_after
Which five tabs people actually choose — the real answer to Q5 in the brief.
D
LENDING — the reason non-collectors open the app
lend_created
A loan handshake starts
type, has_due_date, reminder_on
Whether lending is a headline feature or a footnote.
lend_returned
Return confirmed
days_out_bucket, condition_changed
Loop completion — the proof the handshake works socially, not just technically.
lend_overdue_seen
Overdue state displayed
days_over_bucket
How much of the app's emotional value is "where is my stuff".
borrowed_added
Something not yours is logged
type
Whether the dashed-edge model gets used in both directions.
E
THE v2 GATES — measurable in v1, no marketplace needed
object_flagged_sellable
"Might let this go" or duplicate marked
type, reason
LATENT SUPPLY. Gate: ≥8% of users flag at least one. The seller side pre-registering.
wish_added
A wanted object is added
type, has_cap, priority
LATENT DEMAND. Gate: ≥30% of actives reach a 5-object wishlist. Your market's query volume before the market exists.
export_completed
A collection export finishes
kind, objects_bucket
Insurance appetite — the evidence you take into the insurer conversation.
F
THE GROWTH LOOP — every share is a store impression
share_link_created
A link is generated
kind, type, channel
Which artefact travels: one object, a wishlist, a loan receipt, a shelf.
share_link_opened
Server-side, recipient opens it
kind, is_new_visitor
Opens per link sent — the loop's multiplier. Under 1.0 and it isn't a loop.
share_link_converted
Install attributed to a link
kind
Your only honest acquisition attribution, and it needs no SDK or fingerprinting.
G
MONEY — one-time unlock, no subscription
cap_reached
The 100-object wall is hit
days_since_first_open, type
Is the cap in the right place? If almost nobody reaches it, it's too high to earn and too low to be generous.
unlock_shown
Purchase sheet displayed
trigger, objects_bucket
Which trigger converts: the cap, export, sharing, or a settings visit.
unlock_purchased
Unlock completes
trigger, objects_bucket, days_since_first_open
Conversion by depth. Expect a step change above ~60 objects; that's your paywall thesis confirmed or dead.
lookup_pack_purchased
Consumable bought
pack_size, allowance_hit_count
Whether metered lookups cover their own cost.
offer_code_redeemed
A code is used
campaign
Per-creator and per-community attribution, cleanly, with no tracking.
H
HEALTH — the quality floor under everything
entitlement_check_failed
Paid user sees a paywall
context
The fastest route to a one-star review. Should be zero; alert on any.
sync_conflict_resolved
iCloud conflict handled
resolution, objects_bucket
Trust in the data layer. A collector who loses one edit tells forty people.
import_failed
Import errors out
source, reason
Each distinct reason is a parser to fix, ranked by frequency.
The eight shared properties — send these on everything relevant, nothing else

type (one of the fourteen slugs, or custom / mixed) · method (photo / barcode / manual / import) · objects_bucket · days_since_first_open · storefront · app_version · source_page · is_unlocked. Nothing free-text, ever — a free-text property is where PII leaks in six months, from a well-meaning line of code.

03 · The scorecard
Nine numbers
IF YOU TRACK NOTHING ELSE
1
Median objects per user at day 30 — the north star. Segment by acquisition source and by type; a channel's value is its median, not its volume.
WEEKLY
2
Time to first object — minutes from first open. If this exceeds ten minutes, nothing downstream can be fixed by pricing or marketing.
WEEKLY
3
Import completion rate — completed ÷ started, by source format. Your highest-leverage feature, and the cheapest thing to fix.
WEEKLY
4
Store conversion rate + benchmark percentile — downloads ÷ unique impressions, split by source type and by custom product page.
WEEKLY
5
Download-to-paid at day 30, plus unlock rate among users who reached the cap. Two different numbers; the second is the actionable one.
WEEKLY
6
D7 and D28 usage retention against peer percentile. The leading indicator — it moves weeks before revenue does.
WEEKLY
7
Latent supply and latent demand — sellable-flag rate (gate 8%) and 5-object wishlist rate (gate 30%). These two decide v2. Nothing else does.
MONTHLY
8
Share links: opens per link sent, installs per hundred opens — the growth loop's actual multiplier.
MONTHLY
9
Lookup cost per active user against average proceeds per download. The moment the first exceeds the second, your pricing is broken.
MONTHLY

Review rule, borrowed and worth keeping: each cycle, find the funnel stage where you sit at your lowest benchmark percentile and fix that one. Don't optimise a stage you're already good at — the headroom is where you're worst. Benchmarks tell you where to look; your own cohorts tell you what to fix.

04 · Reading it
Four diagnoses
you'll actually face
DEPTH LOW · RETENTION OK
An adding problem
People come back but their collections stay small. Look at seconds_bucket on object_added and at the import funnel. This is friction, not desire — the most fixable state on this list.
DEPTH OK · RETENTION LOW
A finished-job problem
They catalogued everything and left, which is arguably success. Then lending, wishlists and share links are your only reasons to return — check whether people who lend or keep wishlists retain differently. If they do, that's your v1 roadmap.
CAP REACHED · NO UNLOCK
A pricing or moment problem
Compare unlock_shown triggers. If the cap converts worse than export or sharing, the wall is in the wrong place — move it to the moment of value, not the moment of volume.
STORE VIEWS OK · DOWNLOADS LOW
A creative problem, per page
Conversion by source_page tells you which hobby's door is weak. Fix that page's screenshots before touching the default page, and keep product page optimization off community traffic.
What to deliberately not build yet

Attribution SDKs, conversion-value schemas, cohort dashboards, a data warehouse. With one-time purchases and no ad spend there's nothing to attribute and no budget to misallocate — App Analytics plus a weekly query against your own endpoint answers all nine numbers above. Revisit the moment you either run paid media or ship the v2 seller subscription; then the retention machinery — grace periods, save rates, win-back timing, the 85% proceeds tier past twelve months — becomes real, and worth a page of its own.

05 · Order of work
Ship the tagging
in three passes
PASS 1 — BEFORE TESTFLIGHT
Nine events
first_open · collection_created · first_object_added · object_added · import_started · import_completed · import_failed · session_started · cap_reached

That's the whole activation and depth picture — and it's what tells you whether fifty invested experts reach sixty objects each.
PASS 2 — BEFORE LAUNCH
Money, gates and the loop
All of groups E, F and G, plus lookup_* so your variable cost is visible from day one. The v2 gates must exist at launch — they need months of data before the decision, and you can't backfill them.
PASS 3 — MONTH TWO ONWARDS
The refinements
Groups C, D and H — field usage, 3D engagement, tab choices, sync health. These change the product rather than the business, so they can wait until there are enough real users to read.

Two hygiene rules that save you a rewrite. Version the schema from event one (v: 1 on every payload) and never rename an event — add a new one and let the old die. And sample nothing: at your volumes a 10% sample means one usable number a month, and full-fidelity anonymous counts are cheaper to store than the meeting about whether the sample is representative.