diff --git a/src/Packages/Audience/Runtime/Transport/HttpTransport.cs b/src/Packages/Audience/Runtime/Transport/HttpTransport.cs index 3e5c4a5fa..c5cc117d3 100644 --- a/src/Packages/Audience/Runtime/Transport/HttpTransport.cs +++ b/src/Packages/Audience/Runtime/Transport/HttpTransport.cs @@ -119,10 +119,22 @@ internal async Task SendBatchAsync(CancellationToken ct = default) { // 4xx: server rejected the payload. Drop it (retry won't help) and // reset backoff — server is healthy, our data was the problem. + // Pull the response body so the studio has something actionable: + // the status code alone just says "something is wrong"; the body + // names which field or event broke validation. Truncated to keep + // logs sane if the backend ever returns a long diagnostic. + string? body = null; + try { body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); } + catch { /* Content read failed (disposed, network) — fall through with body=null. */ } + _store.Delete(batch); ResetBackoff(); - NotifyError(AudienceErrorCode.ValidationRejected, - $"Batch rejected with {statusCode}"); + + const int maxBodyLen = 500; + var message = string.IsNullOrEmpty(body) + ? $"Batch rejected with {statusCode}" + : $"Batch rejected with {statusCode}: {(body.Length > maxBodyLen ? body.Substring(0, maxBodyLen) + "…" : body)}"; + NotifyError(AudienceErrorCode.ValidationRejected, message); } else { @@ -210,11 +222,11 @@ private void ResetBackoff() } // Reads each path and wraps the concatenated JSON bodies in - // {"batch":[msg1,msg2,...]}. Returns null if every path was + // {"messages":[msg1,msg2,...]}. Returns null if every path was // unreadable; the caller treats null as "nothing to send". private static string? BuildPayload(IReadOnlyList paths) { - var sb = new StringBuilder("{\"batch\":["); + var sb = new StringBuilder("{\"messages\":["); var count = 0; for (var i = 0; i < paths.Count; i++) diff --git a/src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs b/src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs index bce36b947..4b08d6f7d 100644 --- a/src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs +++ b/src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs @@ -87,7 +87,7 @@ public async Task SendBatchAsync_200_SendsGzippedPayloadWithCorrectHeaders() Assert.AreEqual("gzip", capturedContentEncoding); var decompressed = DecompressGzip(capturedBody); - StringAssert.StartsWith("{\"batch\":[", decompressed); + StringAssert.StartsWith("{\"messages\":[", decompressed); StringAssert.EndsWith("]}", decompressed); StringAssert.Contains("\"eventName\":\"test\"", decompressed); } @@ -116,7 +116,7 @@ public async Task SendBatchAsync_200_SendsPlainJsonPayloadWithoutContentEncoding Assert.AreEqual("pk_imapik-test-key1", capturedKey); Assert.AreEqual("application/json", capturedContentType); Assert.AreEqual(0, capturedContentEncodingCount, "no Content-Encoding header is permitted in v1"); - StringAssert.StartsWith("{\"batch\":[", capturedBody); + StringAssert.StartsWith("{\"messages\":[", capturedBody); StringAssert.EndsWith("]}", capturedBody); StringAssert.Contains("\"eventName\":\"test\"", capturedBody); } @@ -186,7 +186,8 @@ public async Task SendBatchAsync_4xx_DeletesFilesAndResetsBackoff() { _store.Write("{\"type\":\"track\"}"); - var handler = new MockHandler(HttpStatusCode.BadRequest, ""); + var handler = new MockHandler(HttpStatusCode.BadRequest, + "{\"error\":\"invalid eventName format at /batch/0/eventName\"}"); AudienceError reportedError = null; using var transport = new HttpTransport(_store, "pk_imapik-test-key1", onError: e => reportedError = e, handler: handler); @@ -197,6 +198,9 @@ public async Task SendBatchAsync_4xx_DeletesFilesAndResetsBackoff() Assert.IsFalse(transport.IsInBackoffWindow); Assert.IsNotNull(reportedError); Assert.AreEqual(AudienceErrorCode.ValidationRejected, reportedError.Code); + // Backend-supplied diagnostic must reach the studio, otherwise a 400 + // collapses to "something broke, good luck" with no actionable signal. + StringAssert.Contains("invalid eventName format", reportedError.Message); } [Test] diff --git a/src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs b/src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs index 074dd21f8..1957f7b22 100644 --- a/src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs +++ b/src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs @@ -29,7 +29,7 @@ public void Compress_ProducesValidGzip_ThatDecompressesToOriginal() public void Compress_OutputIsSmallerThanInput_ForRealisticPayload() { // Repeated field names compress well in JSON batches. - var sb = new StringBuilder("{\"batch\":["); + var sb = new StringBuilder("{\"messages\":["); for (var i = 0; i < 20; i++) { if (i > 0) sb.Append(',');