NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev
Apple Search Ads ROAS tracking for iOS Flutter apps — first-party AdServices attribution + StoreKit 2 revenue. No IDFA.
Last release 13 days ago
25 Sep 2026
Ships on a steady schedule
a new release about every 2 weeks
Most releases are documented
notes for 23 of 26 stable releases
Nothing withdrawn
no release was ever pulled
2 months old
26 releases · first in 2026
One column per month.
Nothing published for this version
Nothing published for this version
3.1.8 — the SDK no longer finishes StoreKit transactions; that is the…
3.1.8 — the SDK no longer finishes StoreKit transactions; that is the…
3.1.7 — install instructions for all four distributions, new reposito…
3.1.7 — install instructions for all four distributions, new reposito…
Verified on the device: an iPhone XS on iOS 16.3.1 died at currency: tx.currencyCode with EXC_BAD_ACCESS at 0x0, and the same build with this change l
Verified on the device: an iPhone XS on iOS 16.3.1 died at currency: tx.currencyCode with
EXC_BAD_ACCESS at 0x0, and the same build with this change launches clean.
Below iOS 17.2 neither property is stored — both parse the raw JWS by keypath, through different
transforms — and the two carry different back-deployment thresholds:
currencyCode @backDeployed(before: iOS 16.0) → on 16.3 dispatches to the OS symbol
currency @backDeployed(before: iOS 17.2) → on 16.3 runs the copy compiled into the app
That asymmetry also explains the crash log, which had frame 0 in a NULL image (base 0, size 0, zero
UUID) with StoreKit absent from the stack. A back-deployment dispatch is itself a call through a
resolved function pointer, so a failed resolution branches to address 0 having never entered StoreKit —
which is why "StoreKit is not on the stack" did not rule this accessor out.
From iOS 17.2 both are a stored property, so this costs nothing there. iOS 15 keeps the old call: it is
the one OS with no alternative, and it reaches a different accessor entirely.
Credit where due: the app-side engineer identified this line and this fix. I argued against the
mechanism twice — a Swift trap would indeed have been EXC_BREAKPOINT — while the remedy was right all
along, and the locking work in 3.1.4/3.1.5 fixed a real defect that was not what was crashing.
Identical behaviour to 3.1.4; this is a refactor.
Identical behaviour to 3.1.4; this is a refactor.
NSLock.lock() is unavailable from an asynchronous context — a warning today and an error in the
Swift 6 language mode. The objection is to holding a lock ACROSS a suspension point, which blocks a
cooperative thread and can deadlock the pool. sweepAll never did that, but it was true only by
inspection.
The two critical sections now live in synchronous helpers, claimSweep and finishSweepPass, with the
await strictly between them — so it is structurally impossible rather than merely currently correct.
Clean under -strict-concurrency=complete.
StoreKitObserver.environment was a plain var String . The Runtime writes it once await AppTransaction.shared returns — a storekitd round trip that tak
StoreKitObserver.environment was a plain var String. The Runtime writes it once
await AppTransaction.shared returns — a storekitd round trip that takes seconds on a cold
launch — while the transaction sweep is already reading it for every transaction it emits.
Reading a String retains a buffer; writing one releases the old buffer. 3.1.0 added rescan(),
so from then on two sweeps read it concurrently, the same buffer could be released by parties
that no longer agreed on who owned it, and the freed memory was reused. A Swift Task later
resumed into a null function pointer: EXC_BAD_ACCESS, pc = 0, ten seconds into launch. Seen on
an iPhone XS on iOS 16.3, where AppTransaction is slow enough to land mid-sweep.
Every mutable field on the observer is now behind a lock and read once into a local.
Also in this release:
Storage.markTransactionSent was a read-modify-write over three UserDefaults calls with noGADAdValue.value can betry? never saw it. Non-finite values are dropped and the encoder is checked.didBecomeActive is not a purchase signal — it fires on every return to the app. The launchiPhone11,2) alongside the OS version. Raw, never a friendlysetUserId reaches the dashboard instead of a variable nothing read. Pass your own user idNothing published for this version
Apple Search Ads attribution and revenue tracking for iOS. AdServices attribution with no IDFA and no ATT prompt, StoreKit 2 purchases and renewals ob
Apple Search Ads attribution and revenue tracking for iOS. AdServices attribution with
no IDFA and no ATT prompt, StoreKit 2 purchases and renewals observed automatically, and
impression-level ad revenue from AdMob.
Integration is two calls: configure once at launch, and report ad revenue if the app
shows ads. Purchases need no code.
Distributed for Swift Package Manager, CocoaPods, Flutter and Unity — one version across
all four.
The Flutter plugin's podspec globbed Classes//* , which was harmless for a year because Classes/ held nothing but Swift. 2.2.0 put PrivacyInfo.xcpriva
The Flutter plugin's podspec globbed Classes/**/*, which was harmless for a year
because Classes/ held nothing but Swift. 2.2.0 put PrivacyInfo.xcprivacy in there,
so from that release the glob swept a resource into the compile phase — visible in a
consumer's generated Pods project as "PrivacyInfo.xcprivacy in Sources", the only
pod doing it. It is also listed in resource_bundles, which is where it belongs and
where it stays.
Restricting the glob to *.swift is correct whether Xcode treats the stray file as an
error or merely warns; we were never able to reproduce a build failure cleanly, and
the fix costs nothing either way.
The root podspec was always correct (Sources/Asalyze/**/*.swift), so SPM, native
CocoaPods and Unity were never affected. Bumped across all four anyway rather than
leaving Flutter alone at 3.1.1 — a version that differs per distribution is the thing
that makes questions like this hard to answer later.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Copyright and package author fields named a placeholder "Codematics Inc." that
never appeared anywhere else in the project. The real registered entity —
Codematics Services Private Limited, shared with Apple as the vendor account —
is what LICENSE and every manifest name now.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Apple Search Ads attribution and revenue tracking for iOS. AdServices attribution with no IDFA and no ATT prompt, StoreKit 2 purchases and renewals ob
Apple Search Ads attribution and revenue tracking for iOS. AdServices attribution with
no IDFA and no ATT prompt, StoreKit 2 purchases and renewals observed automatically, and
impression-level ad revenue from AdMob.
Integration is two calls: configure once at launch, and report ad revenue if the app
shows ads. Purchases need no code.
Distributed for Swift Package Manager, CocoaPods, Flutter and Unity — one version across
all four.
2.2.0 — the SDK reports its own version, and Apple's reason for a mis…
2.2.0 — the SDK reports its own version, and Apple's reason for a mis…
2.1.0 — iOS 15 install dates, ad impression country, OS version
2.1.0 — iOS 15 install dates, ad impression country, OS version
Installs on iOS 15 now report an install date. AppTransaction is iOS 16+, and below it StoreKit 2
offers no app-level record at all — so those users arrived with no date and stayed permanently
unclassified. Measured 2026-08-21 in production: a flat ~1.8% of installs, 539 of the last 620 undated
ones, every single one on a current build. It was never a device failure; the SDK simply never asked in
a way an older OS could answer.
It now falls back to the legacy App Store receipt, which is what Apple's own documentation points to for this case: use the receipt when the app "needs the receipt to validate the app download because it can't use AppTransaction". The receipt is sent as Apple signed it and read on the server — a date the app could have retyped is worth less than one Apple issued. The fallback runs only below iOS 16, so its deprecation is irrelevant: those devices can never reach an OS where it might be removed.
Ad revenue now carries the country the ad was served in. eCPM is set by the viewer's country, so ad revenue without geography cannot really be read — $100 from Pakistan and $100 from the US are completely different performances. The region is read at the impression rather than taken from the install, because a user who has travelled since installing would otherwise have every impression priced against the country they signed up in.
Also sent at registration: the device OS version, and — on iOS 16+ — the whole signed AppTransaction
record rather than one field pulled out of it. That record carries originalApplicationVersion, which
separates a long-standing user from a new one without needing a usable date, and it exists nowhere but
on the device: no server API can be asked for it afterwards.
No API changes. Nothing to update in your integration.
Apple returns countryOrRegion only for installs it attributed. Measured 2026-08-20: 7,850 of 7,906 paid installs carried a country, against 14 of 66,2
Apple returns countryOrRegion only for installs it attributed. Measured 2026-08-20:
7,850 of 7,906 paid installs carried a country, against 14 of 66,200 organic — so
89% of users had no geography and any geo view was a paid-only view.
Locale.current.region on iOS 16+, regionCode below it, which also covers the older
devices that cannot provide an AppTransaction.
It is the device's own setting, not the App Store storefront, and disagrees for
someone travelling or living abroad. The backend keeps it in its own column rather
than merging it into Apple's country, so a weaker signal never overwrites a stronger.
No API change.
The SDK now reports the device's region. Apple returns a country only for installs it attributed, so organic users — the large majority — arrived with no geography at all: measured 2026-08-20, 7,850 of 7,906 paid installs carried a country against 14 of 66,200 organic.
Locale.current.region on iOS 16+, regionCode below it, so it also covers the older devices that cannot
provide an AppTransaction. It is the device's own setting, not the App Store storefront, and the backend
keeps it separate from Apple's answer rather than merging the two.
No API change. Nothing to do but update.
The C# still called AsalyzeTrackPurchase, whose ObjC bridge symbol was removed in this release, so the package would have failed to link. Both are gon
The C# still called AsalyzeTrackPurchase, whose ObjC bridge symbol was removed in
this release, so the package would have failed to link. Both are gone now, which
means a project still calling it fails to BUILD rather than silently sending data
that cannot be deduped.
Asalyze-2.0.0.unitypackage is rebuilt from the same GUIDs as 1.3.1, so importing
it upgrades the existing assets in place instead of duplicating every script.
TrackAdRevenue, TrackEvent, SetUserId and InstallId are unchanged.
Breaking: Asalyze.trackPurchase is removed (Swift, Dart and the Objective-C/Unity bridge).
Purchases are never reported by hand. The SDK requires iOS 15, so StoreKit 2 observation always runs and every App Store purchase already arrives with Apple's own transaction id, price, currency, offer type and dates — the authoritative values. A manual call could only ever disagree with them, and did:
offer defaulted to .none, so a free trial reported this way booked at full price.In production one app reported roughly double its real revenue, with 52 free trials counted as paid purchases. Removing the method removes the whole class of error.
Also breaking: offer is now required on trackSubscription (and on SubscriptionEvent.init).
It defaulted to .none, which is the same trap: forget the argument and a free trial is recorded as a
paid purchase. .none is still correct for events where no offer applies — expiry, cancellation — it
simply has to be stated rather than assumed. The Flutter channel now rejects an unrecognised offer
instead of quietly falling back to .none.
Migrating: delete your trackPurchase calls — nothing is lost, the SDK already has those purchases
with better data than the call supplied — and pass offer: explicitly to trackSubscription.
trackAdRevenue and trackEvent are unchanged; ad revenue genuinely does need reporting, since Apple
knows nothing about it.
1.3.0 — @objc AsalyzeBridge + Unity package (iOS)
1.3.0 — @objc AsalyzeBridge + Unity package (iOS)
AsalyzeBridge façade so non-Swift hosts can drive the SDK through a
C-callable surface. Powers the new Unity package (native iOS bridge). No change for Swift/Flutter apps.1.2.2 — re-publish of 1.2.1 (identical code) to repair CocoaPods trun…
1.2.2 — re-publish of 1.2.1 (identical code) to repair CocoaPods trun…
1.2.1 — reliable subscription reporting (mark-sent after 2xx) + Asaly…
1.2.1 — reliable subscription reporting (mark-sent after 2xx) + Asaly…
Transaction.all on the next launch — no purchase is
lost just because one network call failed. No integration changes required.Asalyze.purchase(product) — a drop-in for product.purchase(options:) that attaches
appAccountToken = installId automatically, so Apple echoes the install id in every server
notification. This attributes a purchase (and all its renewals) even when the buyer never reopens the
app — the one case on-launch retry can't recover. Optional one-line swap at your purchase call site.1.2.0: capture exact app version + build + true install date (AppTran…
1.2.0: capture exact app version + build + true install date (AppTran…
v1.1.4 — non-App-Store (ad-hoc/dev) builds classified sandbox
v1.1.4 — non-App-Store (ad-hoc/dev) builds classified sandbox
v1.1.3 — AsalyzeAdMob.report accepts NSDecimalNumber directly
v1.1.3 — AsalyzeAdMob.report accepts NSDecimalNumber directly
AsalyzeAdMob.report(_:currencyCode:precision:format:) now takes AdMob's
GADAdValue.value (an NSDecimalNumber, in currency units) directly — pass it straight through, no
micro-unit conversion. A labelled report(micros:…) overload remains for micro-unit callers.Redownload tracking: the SDK now detects a reinstall (the Keychain install id survived but the app's local first-run marker was wiped) and flags it, s
Environment detection now uses StoreKit 2's AppTransaction (and each transaction's own .environment) instead of the receipt-name heuristic — so TestFl
AppTransaction (and each transaction's own
.environment) instead of the receipt-name heuristic — so TestFlight / ad-hoc / Release-configured
test builds are reliably tagged sandbox and never leak into production reports.Every StoreKit transaction now reports Apple's transactionId — the backend deduplicates it against the App Store Server API, so purchases/renewals are
transactionId — the backend deduplicates it against
the App Store Server API, so purchases/renewals are never double-counted across sources.Transaction.all to backfill a user's full purchase history with real dates
(so subscribers who bought before the SDK shipped still show a complete timeline).Default backend endpoint is now https://asalyze.com (production).
https://asalyze.com (production).Apple Search Ads first-party attribution via AdServices (no IDFA, no ATT prompt).
Your coding agent can read these notes before it upgrades. Set up the MCP server →