DORA metrics were built for teams that own the deploy button. Push to main, the pipeline runs, the service is live. Deployment frequency, lead time, change failure rate, time to restore. Four numbers. The whole industry learned to speak them.
iOS does not work that way. You can merge, archive, and upload in an hour. Then Apple holds the build. Review queues. Manual release. Phased rollout. There is no instant rollback. If you copy-paste the four DORA definitions onto an App Store team, the numbers lie.
The events Apple already sends are enough to measure the same ideas, once you remap them onto the review pipeline. You do not need a new dashboard product. You need timestamped state transitions.
Deployment frequency: two cadences, not one
On the web, a deploy is one thing. On iOS it is two, and they move at different speeds.
TestFlight cadence is how often a build becomes available to testers. The useful events are BUILD_UPLOAD_STATE_UPDATED when the state moves from PROCESSING to COMPLETE, and BUILD_BETA_DETAIL_EXTERNAL_BUILD_STATE_UPDATED when an external group can actually install it. That is your internal ship rate. Count those completions per week and you have a number that matches how the team actually works.
App Store cadence is how often a version reaches customers. The event is APP_STORE_VERSION_APP_VERSION_STATE_UPDATED arriving at READY_FOR_DISTRIBUTION. That is the public ship rate. It is slower, noisier, and gated by review.
If you blend them into one "deploys per week" number, TestFlight volume swamps App Store releases and the metric stops meaning anything. Track both. Elite on TestFlight and stuck on the store is a real pattern, and a blended number hides it.
Lead time: split what you own from what Apple owns
Lead time for changes is the interval from "this is ready" to "this is live." On iOS that interval is a state machine, not a CI job:
PREPARE_FOR_SUBMISSION → READY_FOR_REVIEW → WAITING_FOR_REVIEW → IN_REVIEW → PENDING_DEVELOPER_RELEASE → READY_FOR_DISTRIBUTION
Every arrow is an APP_STORE_VERSION_APP_VERSION_STATE_UPDATED event with an oldValue, a newValue, and a timestamp. Pair consecutive events on the same version and you have durations.
The mistake is reporting one blended lead time. The queue and the review are not yours. The submit step and the manual release are.
Time you own
PREPARE_FOR_SUBMISSIONtoREADY_FOR_REVIEW. How long the version sits while metadata, screenshots, and the review notes get finished.PENDING_DEVELOPER_RELEASEtoREADY_FOR_DISTRIBUTION. If you use manual release, this delay is a product choice, not Apple latency.
Time Apple owns
WAITING_FOR_REVIEWtoIN_REVIEW. Queue time.IN_REVIEWto an outcome:PENDING_DEVELOPER_RELEASE,READY_FOR_DISTRIBUTION, orREJECTED. Review time.
The queue is often the larger and more variable slice. A single "median lead time" number will mostly describe Apple's backlog that week, not your team's speed. Split the two. Improve the parts you can touch. Treat Apple's parts as a forecast, not a performance review.
Change failure rate: three signals, not one
"Did this change fail?" is not a boolean on the App Store. Three different things get flattened into one bucket if you are not careful.
Apple rejection. IN_REVIEW to REJECTED. Then, often, REJECTED back to READY_FOR_REVIEW as the team resubmits. That loop is common enough that you should model resubmission as a normal pipeline path, not an exception. A version that ships after one rejection still shipped. Count the rejections. Do not pretend the first attempt is the only attempt.
Self-withdrawal. Transitions through DEVELOPER_REJECTED mean your team pulled the build. That is a process failure or a last-minute find, not an Apple reject. Mixing it with REJECTED hides whether guideline issues or internal quality gates are the problem.
Pre-release quality. BETA_FEEDBACK_CRASH_SUBMISSION_CREATED per build is the cheapest early warning you will get. A version that is "green" in review and noisy in TestFlight is a change that already failed, just not in the store yet. If you only count store rejections, you will ship the crash.
Keep the three rates separate. They answer different questions: did Apple say no, did we pull it, and did testers hit it first.
Time to restore: there is no rollback
This is the metric that breaks the DORA metaphor hardest. You cannot revert production to the previous binary. The previous version is gone the moment the new one is live, unless you still have it sitting in PENDING_DEVELOPER_RELEASE and never released it.
Measure restore time from the first signal that something is wrong to the next version reaching READY_FOR_DISTRIBUTION.
Useful start signals:
BETA_FEEDBACK_CRASH_SUBMISSION_CREATEDfor a crash that escaped TestFlight, or one that showed up right after release.- A revenue signal such as
DID_FAIL_TO_RENEWwhen the failure is billing, not a crash.
The only real levers once you are in this interval are expedited review and phased release. Phased release does not restore service. It limits blast radius while you prepare the next version. Expedited review shortens Apple-owned time. Everything else is the same pipeline as a normal release, just with worse stakes.
If your restore time is "however long a normal review takes," that is the answer. Write it down. Plan incidents around it. Do not import a web-team SLO that assumes a five-minute revert.
Why webhooks and not polling
These metrics are durations. Durations need both ends of an interval.
App Store Connect webhooks already carry the interval boundary: oldValue, newValue, and a timestamp. Polling the API gives you a snapshot. Any state that appears and disappears between two polls is gone. The transition you needed for lead time or restore time is the one you are most likely to miss if you check twice a day.
You can reconstruct a coarse history from version records after the fact. You cannot reconstruct the queue wait if you never saw WAITING_FOR_REVIEW. The webhook is the record. Everything else is interpolation.
See the transitions as they happen
Once you care about these intervals, waiting until Friday to reconstruct them is too late. The useful moment is the transition itself: a build finished processing, a version entered review, a rejection loop started, a tester filed a crash.
Yeethook receives those App Store Connect events (and the subscription events from App Store Server Notifications) and puts them in Slack as they fire, with the version and build context attached. You still compute the metrics however you want. You stop discovering the endpoints of each interval by logging into App Store Connect and hoping you remember when the last state changed.
