Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/stop-replay-on-mobile-quota.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'posthog': patch
'posthog-android': patch
---

Stop session replay when the remote config reports the mobile recordings quota as exceeded (`quotaLimited` contains `mobile_recordings`). Replay resumes once a later remote config no longer reports it.
21 changes: 10 additions & 11 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -903,26 +903,24 @@ public final class com/posthog/internal/PostHogFeatureFlagsInterface$DefaultImpl
}

public final class com/posthog/internal/PostHogFlagsResponse : com/posthog/internal/PostHogRemoteConfigResponse {
public fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;)V
public synthetic fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;)V
public synthetic fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Z
public final fun component2 ()Ljava/util/Map;
public final fun component3 ()Ljava/util/Map;
public final fun component4 ()Ljava/util/Map;
public final fun component5 ()Ljava/util/List;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Ljava/lang/Long;
public final fun component8 ()Ljava/lang/Boolean;
public final fun copy (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;)Lcom/posthog/internal/PostHogFlagsResponse;
public static synthetic fun copy$default (Lcom/posthog/internal/PostHogFlagsResponse;ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/posthog/internal/PostHogFlagsResponse;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/lang/Long;
public final fun component7 ()Ljava/lang/Boolean;
public final fun copy (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;)Lcom/posthog/internal/PostHogFlagsResponse;
public static synthetic fun copy$default (Lcom/posthog/internal/PostHogFlagsResponse;ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/posthog/internal/PostHogFlagsResponse;
public fun equals (Ljava/lang/Object;)Z
public final fun getErrorsWhileComputingFlags ()Z
public final fun getEvaluatedAt ()Ljava/lang/Long;
public final fun getFeatureFlagPayloads ()Ljava/util/Map;
public final fun getFeatureFlags ()Ljava/util/Map;
public final fun getFlags ()Ljava/util/Map;
public final fun getMinimalFlagCalledEvents ()Ljava/lang/Boolean;
public final fun getQuotaLimited ()Ljava/util/List;
public final fun getRequestId ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand Down Expand Up @@ -1099,12 +1097,13 @@ public final class com/posthog/internal/PostHogRemoteConfig$SessionReplayLinkedF

public class com/posthog/internal/PostHogRemoteConfigResponse {
public fun <init> ()V
public fun <init> (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Boolean;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Boolean;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Boolean;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Boolean;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getCapturePerformance ()Ljava/lang/Object;
public final fun getErrorTracking ()Ljava/lang/Object;
public final fun getHasFeatureFlags ()Ljava/lang/Boolean;
public final fun getPush ()Ljava/lang/Object;
public final fun getQuotaLimited ()Ljava/util/List;
public final fun getSessionRecording ()Ljava/lang/Object;
public final fun getSurveys ()Ljava/lang/Object;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
* @property featureFlags the feature flags
* @property featureFlagPayloads the feature flag payloads
* @property flags the feature flags.
* @property quotaLimited array of quota limited features
* @property requestId the request id generated by the flags server on evaluation
* @property evaluatedAt the evaluated at timestamp generated by the flags server on evaluation
* @property minimalFlagCalledEvents whether the server enabled minimal `$feature_flag_called`
Expand All @@ -23,7 +22,6 @@ public data class PostHogFlagsResponse(
val featureFlags: Map<String, Any>?,
val featureFlagPayloads: Map<String, Any?>?,
val flags: Map<String, FeatureFlag>? = null,
val quotaLimited: List<String>? = null,
val requestId: String?,
val evaluatedAt: Long?,
val minimalFlagCalledEvents: Boolean? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,16 @@ public class PostHogRemoteConfig(

response?.let {
synchronized(remoteConfigLock) {
processSessionRecordingConfig(it.sessionRecording)
// The server keeps sessionRecording enabled here because web recording is still allowed.
if (it.quotaLimited?.contains("mobile_recordings") == true) {
config.logger.log(
"""Session replay is quota limited, recordings are disabled.
Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts""",
)
processSessionRecordingConfig(false)
} else {
processSessionRecordingConfig(it.sessionRecording)
}
processSurveys(it.surveys)
processErrorTrackingConfig(it.errorTracking)
processCapturePerformanceConfig(it.capturePerformance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public open class PostHogRemoteConfigResponse(
// same as a project with none configured. See docs/internal/push-subscription-registration.md
// in PostHog/posthog.
public val push: Any? = null,
// quota limited products, e.g. "mobile_recordings" or "feature_flags"
public val quotaLimited: List<String>? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,39 @@ internal class PostHogRemoteConfigTest {
http.shutdown()
}

@Test
fun `mobile_recordings quota limit disables replay until a later remote config drops it`() {
val enabled = File("src/test/resources/json/basic-remote-config-no-flags.json").readText()
val quotaLimited =
enabled.replace(
"\"hasFeatureFlags\": false,",
"\"hasFeatureFlags\": false, \"quotaLimited\": [\"mobile_recordings\"],",
)
val http = mockHttp(response = MockResponse().setBody(quotaLimited))
http.enqueue(MockResponse().setBody(enabled))
val sut = getSut(host = http.url("/").toString())

val latch = CountDownLatch(1)
sut.loadRemoteConfig(
"my_identify",
anonymousId = "anonId",
emptyMap(),
onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() },
)
assertTrue(latch.await(5, TimeUnit.SECONDS), "quota limited remote config load should complete")

assertFalse(sut.isSessionReplayFlagActive())
assertNull(preferences.getValue(SESSION_REPLAY))

sut.loadRemoteConfig("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()

assertTrue(sut.isSessionReplayFlagActive())

sut.clear()
http.shutdown()
}

@Test
fun `explicit errorTracking false from remote config caches the disabled stance`() {
// Stale cache from when the project had autocapture enabled.
Expand Down
Loading