> For the complete documentation index, see [llms.txt](https://docs.amply.tools/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amply.tools/changelog.md).

# Changelog

User-visible changes to the Amply SDKs and dashboard.

## Latest releases

* **iOS SDK** — 0.10.1
* **Android SDK** — 0.10.1 (published to Maven Central as `tools.amply:sdk-android:0.10.1`)
* **React Native SDK** — 0.10.1 (ships the 0.10.1 native SDKs)

## 0.9.1

All platforms (iOS · Android · React Native). **Recommended for every app on 0.8.0 or later** — no API change and nothing to migrate.

**Events are no longer sent twice.** When an upload succeeded but the response did not carry a server time the SDK could read, the SDK treated the whole upload as failed and sent the same events again on its next attempt. The server had already accepted and stored them, so this did not retry a delivery — it duplicated one.

That matters beyond tidiness, because duplicated events are counted. An audience rule on how many times something happened, and a frequency cap that limits how often a campaign may show, both read those counts — so an affected app could have seen a campaign trigger earlier than its rule intended, or a cap reached sooner.

Three situations reached it: a response with no timestamp at all, a response whose body omitted it, and — new in 0.8.0, which is why this is worth upgrading from — a network proxy that rewrites the timestamp to something implausible. The clock reading became more precise in 0.8.0 and, with it, stricter, which widened what counted as unreadable.

Reading the server time stays best-effort on both paths; delivery no longer depends on it.

## 0.9.0

All platforms (iOS · Android · React Native).

**Detaching a listener now works.** Previously, asking the SDK to stop calling a listener could silently have no effect on iOS: the listener stayed registered and the SDK kept calling it long after the code that owned it was gone. Three things followed from that, all fixed here.

* **React Native apps no longer crash on a bundle reload.** In development, reloading the JavaScript bundle while the app was running crashed it — reliably, every reload. Nothing to change in your app: the fix is entirely inside the native bridge and the **JavaScript API is unchanged**.
* **Deeplinks are delivered once, not once per reload.** Each reload used to leave the previous listener behind, so a single deeplink arrived several times over.
* **A gate registered by a screen that has gone away no longer hangs the next call.** It used to wait out the full timeout — up to a minute — showing nothing. It now resolves immediately, the way that gate was registered to resolve, and costs the campaign no impression.

**Native API change (iOS · Android · KMP).** Registering a listener now returns a token, and you pass that token back to detach:

* `registerDeepLinkListener` / `removeDeepLinkListener`
* `setSystemEventsListener` / `clearSystemEventsListener`
* `setLogListener` / `clearLogListener` — `setLogListener` no longer accepts `null`
* `registerGate` / `unregisterGate` — **`unregisterGate` is new**; there was previously no way to withdraw a gate at all

Keep the token wherever you keep the listener. Anything scoped to the whole process — a listener set up once at launch and never removed — needs no change beyond storing the return value. See [iOS](/developer-guide/ios-integration.md) and [Android](/developer-guide/android-integration.md) for the per-platform shape.

**React Native apps need no code changes** — the bridge holds the tokens for you.

## 0.8.0

All platforms (iOS · Android · React Native). A fix release: no new capabilities, several defects that could cost you data.

**A failing listener can no longer take the SDK down with it.** A log listener that threw could end the process. On Android and React Native a listener that throws is now contained. On native iOS, catch exceptions inside your own listener — an Objective-C exception raised in your code still ends the process, and the SDK cannot intercept that. React Native apps hit this without writing a log listener at all: setting `debug` or `logLevel` installs one for you.

**`CampaignShown` was announced twice.** If you mirror campaign impressions into your own analytics — which this guide recommends — an impression could be delivered to your listener a second time, so your funnel counted shows that happened once. Fixed. `SessionFinished` could also be missing from the stream entirely; it is now always emitted before the session ends.

**One copy of the SDK, however many times you start it.** If your app constructed the SDK more than once — a second initialization, or a React Native reload — two copies could run side by side: two sessions started where there should have been one, and one copy could stop the other's events from ever being sent, so a whole session went missing from your data. A second construction now attaches to the SDK that is already running, and any configuration passed the second time is ignored.

**Repeated sends of the same events are fixed.** Each background/foreground cycle used to leave the previous session's uploader running, and with events still waiting, several of those could send the same batch — so the same event could arrive more than once. Uploading now stops with the session it belongs to. Data already collected from apps on earlier versions may still contain those repeats, so counts from those versions can read higher than the real number.

**You can now withdraw a listener.** `removeDeepLinkListener` and `clearSystemEventsListener` join the register/set calls they mirror. This matters if you register from something that gets torn down and rebuilt while your app keeps running — registering a deep link listener **adds** one rather than replacing it, so without withdrawing, the old listeners stayed registered and every later deep link went to all of them. React Native apps need no code change for the fixes above; the JavaScript API is unchanged.

**Frequency caps.**

* Two audience conditions that differed only by the type of their value could share a counter, so a campaign with a frequency cap could show when it should not. Affects campaigns published before SDK 0.6.0, where the value type was not recorded.
* A campaign impression resolved while the app was being backgrounded could go uncounted. Each one lost allowed one extra show for the rest of that install's life, and they accumulate. Such impressions are now recorded; a small number can still be missed if the device is under pressure at that moment.

**Also fixed.** Event timestamps now use the server-corrected clock from the first event of every launch, instead of the device clock until the correction loaded. A gated call (`trackGated`) can no longer wait indefinitely behind other campaign work — it fails open on a deadline, like the other steps on that path.

**Breaking, native only.** Two declarations changed on the native surface, so a Swift/Kotlin app touching them needs an edit: `HttpClientInterface.extractServerTime` is removed (it only ever returned local time), and the session-tracker state stream now carries a `SessionEvent` enum instead of integer constants. Neither is reachable from React Native.

## 0.7.1

React Native only.

* Custom `configBaseUrl` / `backendBaseUrl` passed at initialization were being dropped instead of applied. If you point the SDK at your own endpoints, this release is required.
* The Expo config plugin no longer warns on every prebuild.

## 0.7.0

All platforms (iOS · Android · React Native).

**Frequency caps hold under load.** Two triggers arriving in quick succession could both pass a cap that allows only one show. Shows are now counted before the next campaign is evaluated.

**Sessions survive background/foreground churn.** Several defects where a session could be ended while the app was still using it, or where events fired during that window were rejected and lost. Events tracked around backgrounding now land in the session they belong to.

**Event-history targeting got much faster.** Answering a campaign's event conditions no longer rescans everything the app has ever recorded, so the cost no longer grows with how long the app has been installed.

**Fixed:** `getDataSetSnapshot` with `@triggeredEvent` counted every event sharing the name, including system events, and reported a type that disagreed with its own count. Campaign matching was never affected — this is the number you read while debugging.

## 0.6.1

All platforms (iOS · Android · React Native).

Internal improvements to how event conditions are evaluated on the device — no visible changes to authoring or behavior.

Event conditions now require apps built with SDK 0.6.1 or later. On older app builds (including 0.6.0), campaigns that use them simply never match — nothing breaks; they start matching once the app updates its SDK.

## 0.6.0

All platforms (iOS · Android · React Native).

**Event-history targeting** — campaign audience rules can now target what users have done, directly on their events. No extra code and no custom property to maintain: pick an event in the rule builder and the condition evaluates on the device, live and offline, like all targeting.

* **Counts** — "did `Purchase` at least 3 times", "`ExportClicked` has never happened". Operators: exactly, not exactly, more than, fewer than, at least, at most — plus has happened (ever) / has never happened. Counts cover the lifetime of the install.
* **First and last occurrence** — relative ("last `WorkoutCompleted` more than 14 days ago", with an "…or never" variant; "within the last 7 days") and absolute (before / after a date, whole calendar days in UTC).
* **Property filters** — narrow a condition to matching occurrences ("`Purchase` where `product` equals `pro`"); only matching occurrences count, for both counts and first/last dates.

Event conditions require apps built with SDK 0.6.0 or later. On older app builds, campaigns that use them simply never match — nothing breaks.

Fixes in this release:

* Campaigns limited to "no more than N impressions total" now reliably stop at N.
* Install-date conditions using "is" with a specific date now match the entire calendar day.
* A fresh install no longer briefly reports an incorrect install date.

See [Targeting and audiences](/concepts/targeting-and-audiences.md) for the model and [Targeting an audience](/user-guide/targeting-an-audience.md) for the dashboard walkthrough.

## 0.5.0

All platforms (iOS · Android · React Native).

**Mediated-gate contract** — campaigns can now be gated on an outcome your app controls.

* `track` — fire-and-forget event tracking, unchanged from earlier releases.
* `trackGated` — tracks an event and suspends the caller until the campaign action at that moment resolves; lets the app await a campaign's outcome (for example, a rewarded ad completing) before continuing its own flow.
* `registerGate` — registers a named gate your app exposes; Amply uses it to attach outcome-bearing actions to the moments you declare.

See [Gating an action](/concepts/gating-an-action.md) for the concept and [Gating actions](/developer-guide/gating-actions.md) for the SDK integration.

Install steps and compatibility notes are in the [Quickstart](/getting-started/quickstart.md) and [Developer Guide](/developer-guide/installation.md).

## See full history

Complete release notes, including older versions and bug-fix releases, live on GitHub:

* iOS SDK — [github.com/amply-tools/amply-sdk-ios/releases](https://github.com/amply-tools/amply-sdk-ios/releases)
* React Native SDK — [github.com/amply-tools/amply-react-native/releases](https://github.com/amply-tools/amply-react-native/releases)
* Kotlin / Android SDK — published to Maven Central under `tools.amply:sdk-android`; release notes are attached to the matching tags on GitHub.

Dashboard releases are rolled out continuously and are not versioned publicly.

## Related

* [Quickstart](/getting-started/quickstart.md) — install the current SDK
* [Developer Guide](/developer-guide/installation.md) — SDK reference
