From 1daa790988d58c23c17f4305e3052618da7064c6 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 28 Oct 2026 19:52:54 -0400 Subject: [PATCH 1/2] fix: keep the personless option when a legacy person processing property is set A passed or registered `$process_person_profile` property no longer stops the SDK from setting `process_person_profile: false` when person processing is off. The send hoists the property only into an unset option, so an option set by any layer wins, as the capture spec requires. Co-Authored-By: Claude Opus 5.5 --- .changeset/android-v4-major.md | 2 +- docs/migration-3.x-to-4.0.md | 2 +- posthog/src/main/java/com/posthog/PostHog.kt | 20 +++++++++---------- .../com/posthog/PostHogCaptureOptionsTest.kt | 12 ++++++----- .../com/posthog/PostHogPersonProfilesTest.kt | 10 +++++----- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.changeset/android-v4-major.md b/.changeset/android-v4-major.md index 84bcc13c4..f2563418b 100644 --- a/.changeset/android-v4-major.md +++ b/.changeset/android-v4-major.md @@ -24,7 +24,7 @@ - **Breaking:** add an `options` argument to `capture`, `screen`, `identify`, `setPersonProperties`, `alias`, `group` and `captureException` for per-event capture options such as `cookieless_mode`, sent apart from the properties and editable in `beforeSend` through `PostHogEvent.options`; Java callers pass the trailing argument or `null`, and classes that implement `PostHogInterface` add the parameter - **Breaking:** move `identify(distinctId, userProperties, userPropertiesSetOnce)` from `PostHogCoreInterface` to `PostHogStatelessInterface`; `PostHogInterface` declares `identify` with the trailing `options` parameter - **Breaking:** registered super properties now override the properties the SDK sets itself, such as `$os_name`; super and event properties also override `$is_identified`, `$process_person_profile` and the session replay debug properties, which the SDK no longer re-applies after the caller's values -- **Breaking:** the SDK sets the `process_person_profile` option to `false` only when person processing is off, and no longer adds a `$process_person_profile` property to every event +- **Breaking:** the SDK sets the `process_person_profile` option to `false` only when person processing is off, and no longer adds a `$process_person_profile` property to every event; a passed or registered `$process_person_profile` property no longer overrides that `false`, so pass the `process_person_profile` option instead - **Breaking:** keep the uuid of its own capture on the event `beforeSend` returns, so a uuid set in `beforeSend`, for example with `copy(uuid = …)`, is replaced - **Breaking:** add `captureAi` to send AI events such as `$ai_generation` to PostHog's AI endpoint (`/i/v1/ai/events`), which accepts events whose properties are up to 8 MiB, on its own disk queue; it returns the event uuid, or `null` when the event isn't queued, and classes that implement `PostHogInterface` add the method - use the current distinct ID when the `distinctId` argument to `capture` is empty or whitespace-only, instead of dropping the event diff --git a/docs/migration-3.x-to-4.0.md b/docs/migration-3.x-to-4.0.md index d7b3e6415..79ae84860 100644 --- a/docs/migration-3.x-to-4.0.md +++ b/docs/migration-3.x-to-4.0.md @@ -149,7 +149,7 @@ When the event is sent, the SDK removes each one from `properties` and uses it f - An option you pass to any of these methods or set in `beforeSend` wins over its legacy property. An option set to `null` counts as unset, so the legacy property fills it. - To change person processing in `beforeSend`, edit `event.options`. A `$process_person_profile` property the hook adds only fills an option that is still unset, so it doesn't override the `false` the SDK set for an anonymous event. -- When `personProfiles` turns person processing off, the SDK sets the `process_person_profile` option to `false`. Otherwise it sends neither, because PostHog processes person profiles by default. In 3.x, every event carried a `$process_person_profile` property. To change person processing for one event, pass the `process_person_profile` option or the `$process_person_profile` property. A registered `$process_person_profile` changes it for every event. +- When `personProfiles` turns person processing off, the SDK sets the `process_person_profile` option to `false`. Otherwise it sends neither, because PostHog processes person profiles by default. In 3.x, every event carried a `$process_person_profile` property. To change person processing for one event, pass the `process_person_profile` option. A `$process_person_profile` property, passed or registered, only fills that option when it's unset, so it can't override the `false` the SDK sets when person processing is off. - A `$set` sent with `process_person_profile: false` doesn't update the person, so it no longer suppresses a later `setPersonProperties` or `identify` call with the same properties as a duplicate. - The SDK sends option values as given. PostHog reads common forms of a value, such as `"false"` or `0` for a boolean, and drops an event whose option value it can't read. - Other option keys are sent unchanged, and PostHog ignores keys it doesn't know. diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index 516309305..11e73c359 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -809,7 +809,8 @@ public class PostHog private constructor( props["\$is_identified"] = isIdentified // No `$process_person_profile`: person processing is sent as the `process_person_profile` // option, only when it's off (see personProcessingOptions). A registered or caller - // `$process_person_profile` wins over it when the send hoists the property. + // `$process_person_profile` only fills that option when the send hoists the property and + // the option is still unset. } // Session replay should have the SDK info as well @@ -985,17 +986,14 @@ public class PostHog private constructor( /** * Adds `process_person_profile: false` when the SDK doesn't process person profiles for this - * event, unless the caller set that option or [properties] carry a non-null legacy - * `$process_person_profile` property (per call or registered). Nothing is added otherwise: - * PostHog processes person profiles by default. Returns a copy, so `beforeSend` edits never - * reach the caller's map. + * event, unless the caller set that option. A legacy `$process_person_profile` property doesn't + * prevent it: the send hoists that property only into an unset option. Nothing is added + * otherwise: PostHog processes person profiles by default. Returns a copy, so `beforeSend` + * edits never reach the caller's map. */ - private fun personProcessingOptions( - options: Map?, - properties: Map, - ): MutableMap? { + private fun personProcessingOptions(options: Map?): MutableMap? { val eventOptions = options?.toMutableMap() - if (hasPersonProcessing() || properties["\$process_person_profile"] != null) { + if (hasPersonProcessing()) { return eventOptions } val result = eventOptions ?: mutableMapOf() @@ -1194,7 +1192,7 @@ public class PostHog private constructor( // Snapshots don't carry the SDK's person-processing option. val eventOptions = - if (isSnapshotEvent) options?.toMutableMap() else personProcessingOptions(options, mergedProperties) + if (isSnapshotEvent) options?.toMutableMap() else personProcessingOptions(options) // This capture's identity, kept through beforeSend. val uuid = TimeBasedEpochGenerator.generate() diff --git a/posthog/src/test/java/com/posthog/PostHogCaptureOptionsTest.kt b/posthog/src/test/java/com/posthog/PostHogCaptureOptionsTest.kt index 76e5045b0..5d52e754e 100644 --- a/posthog/src/test/java/com/posthog/PostHogCaptureOptionsTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogCaptureOptionsTest.kt @@ -218,15 +218,17 @@ internal class PostHogCaptureOptionsTest { } @Test - fun `a registered legacy person processing property suppresses the personless option`() { - val sut = getSut(personProfiles = PersonProfiles.NEVER) + fun `a registered legacy person processing property doesn't override the personless option`() { + val http = httpServers.mockHttp() + val sut = getSut(http.url("/").toString(), personProfiles = PersonProfiles.NEVER, beforeSend = { it }) sut.register("\$process_person_profile", true) sut.capture("test_event") + queueExecutor.shutdownAndAwaitTermination() - val event = captured.single() - assertNull(event.options) - assertEquals(true, event.properties!!["\$process_person_profile"]) + val event = sentEvents(http).single() + assertEquals?>(mapOf("process_person_profile" to false), event.options) + assertFalse(event.properties!!.containsKey("\$process_person_profile")) sut.close() } diff --git a/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt b/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt index 193ee7860..963b8a067 100644 --- a/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt @@ -119,7 +119,7 @@ internal class PostHogPersonProfilesTest { } @Test - fun `caller person processing properties win if identified only and not identified`() { + fun `caller person processing properties don't override the personless option if identified only and not identified`() { val http = mockHttp() val url = http.url("/") @@ -138,8 +138,8 @@ internal class PostHogPersonProfilesTest { val event = batch.batch.first() - // the caller's legacy property is hoisted into the option when the event is sent - assertEquals(true, event.processPersonProfile) + // the SDK's personless option wins; the caller's legacy property is removed when sent + assertEquals(false, event.processPersonProfile) assertFalse(event.properties!!.containsKey("\$process_person_profile")) assertEquals(true, event.properties!!["\$is_identified"] as Boolean) @@ -424,7 +424,7 @@ internal class PostHogPersonProfilesTest { } @Test - fun `caller process person override wins if never`() { + fun `caller process person property doesn't override never`() { val http = mockHttp() val url = http.url("/") @@ -440,7 +440,7 @@ internal class PostHogPersonProfilesTest { val event = batch.batch.first() - assertEquals(true, event.processPersonProfile) + assertEquals(false, event.processPersonProfile) assertFalse(event.properties!!.containsKey("\$process_person_profile")) sut.close() From 2a3b30b504f6e72bc2449e7759847500866c5306 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 28 Oct 2026 20:08:16 -0400 Subject: [PATCH 2/2] fix: treat a 2xx without readable per-event results as a terminal failure A 2xx whose body can't be read as `{"results": {...}}` no longer counts the batch as delivered. It throws a PostHogApiError with that status, which the queue drops without retrying, and logs only the status and event count, as the capture spec requires. Co-Authored-By: Claude Opus 5.5 --- docs/migration-3.x-to-4.0.md | 1 + .../java/com/posthog/internal/PostHogApi.kt | 25 ++++--- .../posthog/internal/PostHogCaptureV1Test.kt | 71 +++++++++++-------- ...ogSendCachedLegacyEventsIntegrationTest.kt | 4 +- 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/docs/migration-3.x-to-4.0.md b/docs/migration-3.x-to-4.0.md index 79ae84860..4d1bed0aa 100644 --- a/docs/migration-3.x-to-4.0.md +++ b/docs/migration-3.x-to-4.0.md @@ -54,6 +54,7 @@ If you send events through your own reverse proxy, update it before you upgrade: - Forward `/i/v1/analytics/events` and `/i/v1/ai/events` to PostHog. - Pass `Authorization` (the project token, sent as `Bearer `), every `PostHog-*` header, `Content-Type`, `Content-Encoding` and `User-Agent` through unchanged. PostHog answers `401` without `Authorization` and `400` without the others, and the events are dropped. - Accept request bodies up to 20 MB. AI requests are often larger than 1 MB, the default limit in nginx and ingress-nginx. A `413` from the proxy makes the SDK halve the batch until it fits, and drops an event that doesn't fit on its own. +- Pass PostHog's response body through unchanged. A 2xx response whose body isn't PostHog's `{"results": {...}}` drops the events instead of retrying them. PostHog's managed reverse proxy already does this. See the [proxy guides](https://posthog.com/docs/advanced/proxy). diff --git a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt index 0e4b54a1a..13f91982a 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt @@ -174,15 +174,17 @@ public class PostHogApi( * Sends analytics events to capture v1 (`POST /i/v1/analytics/events`). * * Returns the uuids of the events the 2xx response asked to retry, with the `Retry-After` it - * sent; every other event in [events] was delivered or dropped and must not be sent again. A 2xx - * whose per-event results can't be read, including a transfer that failed after the status - * arrived, counts as delivered: the server already stored its events and a resend would store - * them again. + * sent; every other event in [events] was delivered or dropped and must not be sent again. + * + * A 2xx whose body can't be read as `{"results": {...}}`, including a transfer that failed after + * the status arrived, is a terminal failure of the whole batch: it throws a [PostHogApiError] + * with that 2xx status, which the queue doesn't retry. * * Only a 307 or 308 to the origin of [PostHogConfig.host] is followed, at most 5 times, with * the same body and headers; any other redirect is a terminal [PostHogApiError]. * - * Throws [PostHogApiError] for a non-2xx status and [IOException] for a transport failure. + * Throws [PostHogApiError] for a non-2xx status or unreadable results, and [IOException] for a + * transport failure before the status. */ @Throws(PostHogApiError::class, IOException::class) internal fun captureV1(events: List): SendResult = postCaptureV1(CAPTURE_V1_ANALYTICS_PATH, events, captureClient) @@ -302,7 +304,8 @@ public class PostHogApi( /** * Reads the per-event results of a 2xx and records which events, if any, the next request - * retries under the same identity. A non-2xx is thrown; a terminal one also drops the identity. + * retries under the same identity. A non-2xx is thrown; a terminal one also drops the identity, + * as does a 2xx without readable results, which is thrown as a terminal failure. */ @Throws(PostHogApiError::class) private fun captureV1Result( @@ -318,11 +321,13 @@ public class PostHogApi( throw PostHogApiError(response.code, response.message, response.body, parseRetryAfter(response)) } + // Counts only: the body may name event uuids. val results = readCaptureV1Results(response) if (results == null) { - config.logger.log( - "Capture returned ${response.code} without readable per-event results, treating ${events.size} events as delivered.", - ) + clearCaptureV1Retry(path, identity) + val message = "Capture returned ${response.code} without readable per-event results, dropping ${events.size} events." + config.logger.log(message) + throw PostHogApiError(response.code, message, null) } val retryUuids = mutableSetOf() @@ -331,7 +336,7 @@ public class PostHogApi( // Events missing from the results count as delivered. The uuids are read once, so a repeated // uuid's later copies (`duplicates_dropped`) are counted, not given the kept copy's verdict. for (uuid in events.mapNotNullTo(LinkedHashSet()) { it.uuid?.toString() }) { - val entry = results?.get(uuid) as? Map<*, *> ?: continue + val entry = results[uuid] as? Map<*, *> ?: continue when (entry["result"]) { "retry" -> retryUuids.add(uuid) "drop" -> dropped++ diff --git a/posthog/src/test/java/com/posthog/internal/PostHogCaptureV1Test.kt b/posthog/src/test/java/com/posthog/internal/PostHogCaptureV1Test.kt index 452b08262..328948e16 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogCaptureV1Test.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogCaptureV1Test.kt @@ -116,7 +116,7 @@ internal class PostHogCaptureV1Test { @Test fun `posts to the capture v1 endpoint with the required headers`() { - val http = httpServers.mockHttp() + val http = httpServers.mockHttp(response = resultsResponse()) clock.setCurrentDate(parseISO8601Date("2024-05-01T10:20:30.456Z")!!) api(http.url("/").toString()).captureV1(listOf(event())) @@ -136,7 +136,7 @@ internal class PostHogCaptureV1Test { @Test fun `captureAi posts to the AI endpoint with the same headers and body shape`() { - val http = httpServers.mockHttp() + val http = httpServers.mockHttp(response = resultsResponse()) clock.setCurrentDate(parseISO8601Date("2024-05-01T10:20:30.456Z")!!) val event = event("\$ai_generation") @@ -187,7 +187,7 @@ internal class PostHogCaptureV1Test { @Test fun `the capture Bearer token replaces a custom Authorization header`() { - val http = httpServers.mockHttp() + val http = httpServers.mockHttp(response = resultsResponse()) val config = config(http.url("/").toString()).apply { requestHeaders = mapOf("Authorization" to "Bearer proxy", "X-Proxy" to "yes") @@ -202,7 +202,7 @@ internal class PostHogCaptureV1Test { @Test fun `body carries created_at and the batch without api_key or sent_at`() { - val http = httpServers.mockHttp() + val http = httpServers.mockHttp(response = resultsResponse()) clock.setCurrentDate(parseISO8601Date("2024-05-01T10:20:30.456Z")!!) val event = event() @@ -396,17 +396,20 @@ internal class PostHogCaptureV1Test { } @Test - fun `a 2xx without readable results counts as delivered`() { - val http = httpServers.mockHttp(response = MockResponse().setBody("not json")) + fun `a 2xx without readable results is a terminal failure`() { + for (body in listOf("not json", "{}", "{\"results\":[]}")) { + val http = httpServers.mockHttp(response = MockResponse().setBody(body)) - val result = api(http.url("/").toString()).captureV1(listOf(event())) + val error = assertThrows(PostHogApiError::class.java) { api(http.url("/").toString()).captureV1(listOf(event())) } - assertTrue(result.retryIds.isEmpty()) - assertTrue(logger.messages.any { it.contains("without readable per-event results, treating 1 events as delivered") }) + assertEquals(200, error.statusCode) + assertFalse(isCaptureV1RetriableStatusCode(error.statusCode)) + } + assertTrue(logger.messages.any { it.contains("without readable per-event results, dropping 1 events") }) } @Test - fun `a 2xx whose transfer fails after the status counts as delivered`() { + fun `a 2xx whose transfer fails after the status is a terminal failure`() { val http = httpServers.mockHttp( response = @@ -415,13 +418,11 @@ internal class PostHogCaptureV1Test { .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) - val result = api(http.url("/").toString()).captureV1(listOf(event())) - - assertTrue(result.retryIds.isEmpty()) + assertThrows(PostHogApiError::class.java) { api(http.url("/").toString()).captureV1(listOf(event())) } } @Test - fun `a 2xx with results over 1 MiB counts as delivered`() { + fun `a 2xx with results over 1 MiB is a terminal failure`() { val event = event() val padding = "x".repeat(1024 * 1024) val http = @@ -429,13 +430,11 @@ internal class PostHogCaptureV1Test { response = MockResponse().setBody("{\"results\":{\"${event.uuid}\":{\"result\":\"retry\"}},\"pad\":\"$padding\"}"), ) - val result = api(http.url("/").toString()).captureV1(listOf(event)) - - assertTrue(result.retryIds.isEmpty()) + assertThrows(PostHogApiError::class.java) { api(http.url("/").toString()).captureV1(listOf(event)) } } @Test - fun `a 2xx whose results trickle past the read deadline counts as delivered`() { + fun `a 2xx whose results trickle past the read deadline is a terminal failure`() { val event = event() val padding = "x".repeat(16 * 1024) // throttles the request read too, so allow a whole small request per period @@ -448,9 +447,7 @@ internal class PostHogCaptureV1Test { ) val sut = api(http.url("/").toString()).apply { captureV1ResultsReadDeadlineMs = 300 } - val result = sut.captureV1(listOf(event)) - - assertTrue(result.retryIds.isEmpty()) + assertThrows(PostHogApiError::class.java) { sut.captureV1(listOf(event)) } } @Test @@ -487,7 +484,7 @@ internal class PostHogCaptureV1Test { @Test fun `a 503 with Retry-After 0 is not resent by the HTTP client`() { val http = httpServers.mockHttp(response = MockResponse().setResponseCode(503).setHeader("Retry-After", "0")) - http.enqueue(MockResponse().setBody("")) + http.enqueue(resultsResponse()) assertThrows(PostHogApiError::class.java) { api(http.url("/").toString()).captureV1(listOf(event())) } @@ -521,7 +518,7 @@ internal class PostHogCaptureV1Test { fun `a retriable status and a network error keep the request identity`() { val http = httpServers.mockHttp(response = MockResponse().setResponseCode(503)) http.enqueue(MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE)) - http.enqueue(MockResponse().setBody("")) + http.enqueue(resultsResponse()) val events = listOf(event()) // a short read timeout turns the unanswered request into an IOException OkHttp doesn't retry itself val config = @@ -541,10 +538,10 @@ internal class PostHogCaptureV1Test { @Test fun `a dropped connection is not resent by the HTTP client`() { - val http = httpServers.mockHttp(response = MockResponse().setBody("")) + val http = httpServers.mockHttp(response = resultsResponse()) // the second request reuses the pooled connection, which OkHttp would otherwise retry on a new one http.enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST)) - http.enqueue(MockResponse().setBody("")) + http.enqueue(resultsResponse()) val sut = api(http.url("/").toString()) sut.captureV1(listOf(event())) @@ -606,7 +603,7 @@ internal class PostHogCaptureV1Test { @Test fun `a terminal status starts a new request identity on the next send`() { val http = httpServers.mockHttp(response = MockResponse().setResponseCode(400)) - http.enqueue(MockResponse().setBody("")) + http.enqueue(resultsResponse()) val events = listOf(event()) val sut = api(http.url("/").toString()) @@ -635,7 +632,7 @@ internal class PostHogCaptureV1Test { fun `follows a same-origin 307 with the same body and headers`() { val http = httpServers.mockHttp(response = MockResponse().setResponseCode(307).setHeader("Location", "/proxied/events")) http.enqueue(MockResponse().setResponseCode(308).setHeader("Location", "/final/events")) - http.enqueue(MockResponse().setBody("")) + http.enqueue(resultsResponse()) api(http.url("/").toString()).captureV1(listOf(event())) @@ -653,7 +650,7 @@ internal class PostHogCaptureV1Test { @Test fun `does not follow a 302`() { val http = httpServers.mockHttp(response = MockResponse().setResponseCode(302).setHeader("Location", "/elsewhere")) - http.enqueue(MockResponse().setBody("")) + http.enqueue(resultsResponse()) val error = assertThrows(PostHogApiError::class.java) { api(http.url("/").toString()).captureV1(listOf(event())) } @@ -663,7 +660,7 @@ internal class PostHogCaptureV1Test { @Test fun `does not follow a 307 to another origin`() { - val other = httpServers.mockHttp() + val other = httpServers.mockHttp(response = resultsResponse()) val http = httpServers.mockHttp( response = MockResponse().setResponseCode(307).setHeader("Location", other.url("/events").toString()), @@ -680,7 +677,7 @@ internal class PostHogCaptureV1Test { @Test fun `stops after five redirects`() { val http = httpServers.mockHttp(total = 6, response = MockResponse().setResponseCode(307).setHeader("Location", "/again")) - http.enqueue(MockResponse().setBody("")) + http.enqueue(resultsResponse()) val error = assertThrows(PostHogApiError::class.java) { api(http.url("/").toString()).captureV1(listOf(event())) } @@ -809,6 +806,20 @@ internal class PostHogCaptureV1Test { assertEquals(1, sut.dequeList.size) } + @Test + fun `queue drops a batch whose 2xx results can't be read without resending it`() { + val http = httpServers.mockHttp(response = MockResponse().setBody("not json")) + http.enqueue(resultsResponse()) + val sut = queue(http) + + sut.add(event()) + executor.awaitExecution() + + assertTrue(sut.dequeList.isEmpty()) + assertEquals(1, http.requestCount) + assertFalse(logger.messages.any { it.contains("successfully") }) + } + @Test fun `queued events round-trip their options through disk`() { val http = httpServers.mockHttp(response = MockResponse().setResponseCode(503)) diff --git a/posthog/src/test/java/com/posthog/internal/PostHogSendCachedLegacyEventsIntegrationTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogSendCachedLegacyEventsIntegrationTest.kt index a6f527d6e..3595bce9a 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogSendCachedLegacyEventsIntegrationTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogSendCachedLegacyEventsIntegrationTest.kt @@ -163,7 +163,7 @@ internal class PostHogSendCachedLegacyEventsIntegrationTest { @Test fun `sends events from the legacy queue in batches`() { val prefix = writeLegacyFile(listOf(event, event)) - val http = servers.mockHttp(2) + val http = servers.mockHttp(2, MockResponse().setBody("{\"results\":{}}")) getSut(prefix, http.url("/").toString(), maxBatchSize = 1).install(mock()) executor.shutdownAndAwaitTermination() assertEquals(emptyList(), retained(prefix)) @@ -173,7 +173,7 @@ internal class PostHogSendCachedLegacyEventsIntegrationTest { @Test fun `send a valid event and discard a broken event`() { val prefix = writeLegacyFile(listOf("invalid event", event)) - val http = servers.mockHttp(2) + val http = servers.mockHttp(2, MockResponse().setBody("{\"results\":{}}")) getSut(prefix, http.url("/").toString(), maxBatchSize = 1).install(mock()) executor.shutdownAndAwaitTermination() assertEquals(emptyList(), retained(prefix))