diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47bbfcb4..92de99f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
## Next
+## 3.11.3 - 2025-02-25
+
+- feat: support quota limiting for feature flags ([#228](https://github.com/PostHog/posthog-android/pull/228))
+
## 3.11.2 - 2025-02-04
- fix: touches fall back to single touches if out of bounds ([#221](https://github.com/PostHog/posthog-android/pull/221))
diff --git a/posthog-android/lint-baseline.xml b/posthog-android/lint-baseline.xml
index 3b6ffb53..1abed766 100644
--- a/posthog-android/lint-baseline.xml
+++ b/posthog-android/lint-baseline.xml
@@ -36,7 +36,7 @@
-
-
-
-
diff --git a/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt b/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt
index c664981b..83a770e2 100644
--- a/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt
+++ b/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt
@@ -7,6 +7,7 @@ import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
* @property errorsWhileComputingFlags if there were errors computing feature flags
* @property featureFlags the feature flags
* @property featureFlagPayloads the feature flag payloads
+ * @property quotaLimited array of quota limited features
*/
@IgnoreJRERequirement
internal data class PostHogDecideResponse(
@@ -16,4 +17,5 @@ internal data class PostHogDecideResponse(
val featureFlagPayloads: Map?,
// its either a boolean or a map, see https://github.com/PostHog/posthog-js/blob/10fd7f4fa083f997d31a4a4c7be7d311d0a95e74/src/types.ts#L235-L243
val sessionRecording: Any? = false,
+ val quotaLimited: List? = null,
)
diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt
index b7022fdf..20dfe83a 100644
--- a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt
+++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt
@@ -89,6 +89,17 @@ internal class PostHogFeatureFlags(
response?.let {
synchronized(featureFlagsLock) {
+ if (response.quotaLimited?.contains("feature_flags") == true) {
+ config.logger.log("Feature flags are quota limited, clearing existing flags")
+ this.featureFlags = null
+ this.featureFlagPayloads = null
+ config.cachePreferences?.let { preferences ->
+ preferences.remove(FEATURE_FLAGS)
+ preferences.remove(FEATURE_FLAGS_PAYLOAD)
+ }
+ return@let
+ }
+
if (response.errorsWhileComputingFlags) {
// if not all flags were computed, we upsert flags instead of replacing them
this.featureFlags =
diff --git a/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt
index 12e5341f..7f33791f 100644
--- a/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt
+++ b/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt
@@ -434,4 +434,49 @@ internal class PostHogFeatureFlagsTest {
sut.clear()
}
+
+ @Test
+ fun `clear flags when quota limited`() {
+ val http =
+ mockHttp(
+ response =
+ MockResponse().setBody(
+ """
+ {
+ "featureFlags": {"flag1": true},
+ "featureFlagPayloads": {"flag1": "payload1"}
+ }
+ """.trimIndent(),
+ ),
+ )
+ val url = http.url("/")
+ val sut = getSut(host = url.toString())
+
+ // Load initial flags
+ sut.loadFeatureFlags("test_id", null, null, null)
+ executor.awaitExecution()
+
+ // Verify flags are loaded
+ assertNotNull(sut.getFeatureFlags())
+ assertNotNull(preferences.getValue(FEATURE_FLAGS))
+
+ // Send quota limited response
+ http.enqueue(
+ MockResponse().setBody(
+ """
+ {
+ "quotaLimited": ["feature_flags"]
+ }
+ """.trimIndent(),
+ ),
+ )
+
+ // Reload flags
+ sut.loadFeatureFlags("test_id", null, null, null)
+ executor.awaitExecution()
+
+ // Verify flags are cleared
+ assertNull(sut.getFeatureFlags())
+ assertNull(preferences.getValue(FEATURE_FLAGS))
+ }
}