Skip to content

feat(audience): HttpTransport — background POST with gzip + retry (SDK-141)#691

Open
ImmutableJeffrey wants to merge 3 commits intomainfrom
feat/sdk-141-http-transport
Open

feat(audience): HttpTransport — background POST with gzip + retry (SDK-141)#691
ImmutableJeffrey wants to merge 3 commits intomainfrom
feat/sdk-141-http-transport

Conversation

@ImmutableJeffrey
Copy link
Copy Markdown
Collaborator

@ImmutableJeffrey ImmutableJeffrey commented Apr 17, 2026

Summary

Adds HttpTransport and Gzip utility for sending queued events to the audience backend.

  • Gzip.Compress uses System.IO.Compression.GZipStream (pure C#, available in Unity 2021+).
  • HttpTransport reads batches from DiskStore, wraps them in {"batch":[...]}, gzips, and POSTs to /v1/audience/messages with the x-immutable-publishable-key header. Runs on background threads via HttpClient.
  • Base URL derived from the publishable key prefix: pk_imapik-test-* routes to sandbox, pk_imapik-* routes to production.
  • Retry behavior: 200 and 4xx delete the batch; 5xx and network errors keep the batch on disk with exponential backoff (5s → 10s → 20s → 60s cap). Backoff resets after a successful send.
  • Errors surface through the optional OnError callback with AudienceErrorCode values (ValidationRejected, FlushFailed, NetworkError).
  • HttpMessageHandler is injectable for testing; all transport tests use a mock handler.
  • 14 new tests (3 Gzip, 11 HttpTransport).

Linear: SDK-141


Note

Medium Risk
Introduces new background HTTP flushing logic (gzip payload construction, error handling, retry/backoff) that directly affects event delivery reliability and data loss behavior on 4xx responses.

Overview
Adds HttpTransport to read event files from DiskStore, wrap them into a {"batch":[...]} JSON payload, gzip-compress, and POST to Constants.MessagesPath with the x-immutable-publishable-key header, selecting sandbox vs prod base URL from the key prefix.

Implements outcome handling and backoff state: 2xx/4xx delete the batch (no retry), while 5xx and network errors keep events on disk and increase an exponential backoff (BackoffMs, IsBackingOff), with an optional onError callback that is swallowed if it throws.

Adds a Gzip utility plus new NUnit tests covering gzip round-trips and HttpTransport behavior for success, empty queue, header/body correctness, environment URL selection, 4xx drop vs 5xx/network retry, backoff progression/reset, and error-callback safety.

Reviewed by Cursor Bugbot for commit b9faf5a. Bugbot is set up for automated code reviews on this repo. Configure here.

@ImmutableJeffrey ImmutableJeffrey requested review from a team as code owners April 17, 2026 02:53
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f66dff6. Configure here.

Comment thread src/Packages/Audience/Runtime/Transport/HttpTransport.cs
@ImmutableJeffrey ImmutableJeffrey force-pushed the feat/sdk-141-http-transport branch from f66dff6 to b9faf5a Compare April 17, 2026 03:00
ImmutableJeffrey and others added 3 commits April 17, 2026 13:19
Gzip: compresses batch payloads using GZipStream (System.IO.Compression,
available in Unity 2021+ .NET Standard 2.1). Pure C#, all platforms.

HttpTransport: reads batches from DiskStore, wraps in {"batch":[...]},
gzips, POSTs to /v1/audience/messages with x-immutable-publishable-key
header. Derives sandbox vs production URL from key prefix.

Retry policy:
- 200: delete batch from disk
- 4xx: delete batch (validation error, won't succeed on retry)
- 5xx: keep on disk, exponential backoff (5s → 10s → 20s → 40s → 60s cap)
- Network error: same as 5xx
- Backoff resets after success

Testable via injected HttpMessageHandler — 14 tests, no network calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plan specifies: 5s → 10s → 20s → 60s cap.
Implementation had: 5s → 10s → 20s → 40s → 60s cap.

Replace the bitshift formula with an explicit switch expression so the
schedule is readable and matches the plan exactly. Update the test to
verify the jump from 20s directly to 60s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Match the pattern used elsewhere in the codebase (Session.cs uses
60_000 for heartbeat interval).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ImmutableJeffrey ImmutableJeffrey force-pushed the feat/sdk-141-http-transport branch from b9faf5a to 52381a4 Compare April 17, 2026 03:22
}
}

if (count == 0) return null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the Audience package also make it clear what's nullable like Passport? Didn't think about it until this as it can return null but it's private static string BuildPayload(IReadOnlyList<string> paths)

/// threads via <see cref="HttpClient"/> — no main thread involvement.
///
/// <para>Retry policy: 5xx and network errors keep events on disk with
/// exponential backoff (5s → 10s → 20s → 40s → 60s cap). 4xx and 200
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class doc says '5s → 10s → 20s → 40s → 60s cap' but there is no 40s step. After 3 failures (20s) the switch jumps straight to 60s. The BackoffMs method comment is correct. Update the class summary to match: '5s → 10s → 20s → 60s cap'.

sb.Append(json);
count++;
}
catch (Exception)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This swallows everything, not just the file-disappeared race. UnauthorizedAccessException means a permissions problem. DirectoryNotFoundException means the store path is wrong. Those should not be silently skipped. Narrow to IOException, which covers all the file system races without hiding anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants