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
5 changes: 3 additions & 2 deletions .changeset/v6-desktop-capture-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
---

- **Breaking:** on every platform, events now go to capture v1 (`/i/v1/analytics/events`) instead of `/batch/`: iOS and macOS through `posthog-ios` 4.x, Android through `posthog-android` 4.x, and Windows and Linux in the Dart client. If you send events through your own reverse proxy, update it before you upgrade: forward `/i/v1/analytics/events` and `/i/v1/ai/events`, pass `Authorization` (the project token, sent as `Bearer <token>`), every `PostHog-*` header, `Content-Type`, `Content-Encoding` and `User-Agent` through unchanged, and accept request bodies up to 20 MB. PostHog's managed reverse proxy already does this. See the [iOS](https://github.com/PostHog/posthog-ios/blob/main/docs/migration-3.x-to-4.0.md#hosts-without-capture-v1) and [Android](https://github.com/PostHog/posthog-android/blob/main/docs/migration-3.x-to-4.0.md#hosts-without-capture-v1) migration guides
- **Breaking:** on Windows and Linux, a reverse proxy or self-hosted instance must serve the new path: a 404 drops the events. Events queued before the upgrade are sent through the new endpoint
- **Breaking:** on Windows and Linux, a 429 or a redirect drops the batch instead of retrying it (only same-origin 307/308 redirects are followed), and a `Retry-After` wait is capped at 30 seconds
- **Breaking:** on Windows and Linux, a reverse proxy or self-hosted instance must serve the new path: a 404 is retried, the events stay queued, and the SDK prints a warning once, also without `debug`. Events queued before the upgrade are sent through the new endpoint
- **Breaking:** on Windows and Linux, a redirect drops the batch instead of retrying it (only same-origin 307/308 redirects are followed, while a 404 or 429 is retried), and a `Retry-After` wait is capped at 30 seconds
- On Windows and Linux, a failed batch, or one whose events the server asks to retry, is no longer resent within the same flush: the queue backs off like the iOS and Android SDKs, waiting 1 second and doubling up to 30 seconds with ±25% jitter, until a batch is delivered. A `Retry-After` is a floor under that wait rather than a replacement
4 changes: 3 additions & 1 deletion posthog_flutter/lib/src/core/capture_v1.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ List<PostHogQueuedEvent> takeCaptureV1Batch(
}

/// Capture v1 statuses worth retrying. Everything else is terminal,
/// including 429 and 3xx; 413 halves the batch instead.
/// including 3xx; 413 halves the batch instead.
bool isRetriableCaptureV1Status(int status) =>
status == 404 ||
status == 408 ||
status == 429 ||
status == 500 ||
status == 502 ||
status == 503 ||
Expand Down
45 changes: 41 additions & 4 deletions posthog_flutter/lib/src/core/file_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ class FileEventQueue {
/// this client.
final Map<String, Map<String, Object?>> _unwritten = {};

/// The serialized size of each queued event, by id, and their total.
final Map<String, int> _sizes = {};
int _byteCount = 0;

List<String> get _ids => _loadedIds ??= _load();

String _pathOf(String id) =>
Expand All @@ -319,7 +323,8 @@ class FileEventQueue {
try {
return [
for (final entity in Directory(_directory).listSync())
if (entity is File && entity.path.endsWith(_extension)) _idOf(entity),
if (entity is File && entity.path.endsWith(_extension))
_track(_idOf(entity), _lengthOf(entity)),
]..sort();
} on PathNotFoundException {
return [];
Expand All @@ -332,6 +337,22 @@ class FileEventQueue {
}
}

static int _lengthOf(File file) {
try {
return file.lengthSync();
} on FileSystemException {
return 0;
}
}

String _track(String id, int size) {
_sizes[id] = size;
_byteCount += size;
return id;
}

void _untrack(String id) => _byteCount -= _sizes.remove(id) ?? 0;

static String _idOf(File file) {
final name = file.uri.pathSegments.last;
return name.substring(0, name.length - _extension.length);
Expand All @@ -340,19 +361,31 @@ class FileEventQueue {
/// The number of queued events.
int get length => _ids.length;

/// The serialized size of the queued events, in bytes.
int get byteCount {
_ids; // Loads the queue, and the sizes, on first use.
return _byteCount;
}

/// Adds [event] at the end of the queue.
void add(Map<String, Object?> event) {
// Loaded before the new file exists, which would otherwise be listed
// on top of being added.
final ids = _ids;
final id = generateUuidV7();
ids.add(id);
String? encoded;
try {
encoded = jsonEncode(event);
} catch (_) {
// Reported below when it is written.
}
ids.add(_track(id, encoded == null ? 0 : utf8.encode(encoded).length));
if (_directory.isEmpty) {
_unwritten[id] = event;
return;
}
try {
_writeAtomically(File(_pathOf(id)), jsonEncode(event));
_writeAtomically(File(_pathOf(id)), encoded ?? jsonEncode(event));
} catch (e) {
_unwritten[id] = event;
_logger()?.warn(
Expand All @@ -372,7 +405,10 @@ class FileEventQueue {
final event = _unwritten[id] ?? _read(id, gone);
if (event != null) events.add((id: id, event: event));
}
if (gone.isNotEmpty) _ids.removeWhere(gone.contains);
if (gone.isNotEmpty) {
_ids.removeWhere(gone.contains);
gone.forEach(_untrack);
}
return events;
}

Expand Down Expand Up @@ -413,6 +449,7 @@ class FileEventQueue {
}

void _forget(String id) {
_untrack(id);
if (_unwritten.remove(id) == null && _directory.isNotEmpty) {
_delete(File(_pathOf(id)));
}
Expand Down
Loading
Loading