Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions docs/development-guide/03-core-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ Future<void> startMasterSearch(String query);
// Prevents race: clear() while records being written
```

Fencing lifecycle: `_pendingGenerations` + `Expando` propagation via
`copyWith`, `clearSync`/`clear` generation bump, `_sentGenerations` drop in
`_handleEntryReady` (null means fenced and must drop). Policy is
`clear-wins`: writes racing clear are dropped. Double bump is intentional —
second fence drops entries recorded during `await writer.clear()` so
`engine.entries` is empty after `await clear()`.

---

## 3.2 `CapturePipeline` & `WriterIsolate` — Off-Main-Thread Processing
Expand Down
360 changes: 249 additions & 111 deletions docs/integration-guide.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions packages/interceptly/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## Unreleased

### Fixed
- Preserve capture generation across `HttpRequestContext.copyWith` so amended scopes stay fenced after `clear()`.
- Generation-fenced `clear`/`clearRest`/`clearRealtime` with per-scope futures; `clear-wins` linearization documented.
- Writer restart aliasing via `_StopAck` handshake with `preserveExisting` resume and rotation-count propagation.
- `getRequestDetail` reads beyond UI preview via full decode and propagates `hasDataLoss` to `isTruncated`; `totalChars` now measures full bodies (previously measured UI-preview truncation).
- Rotation sync for detail loader and search; active realtime notifier retention; bound restart queue.

## 2.3.0

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'dart:typed_data';

import '../engine/capture_generation.dart';

/// Immutable context tracking an ongoing HTTP request capture lifecycle.
class HttpRequestContext {
/// Creates an immutable [HttpRequestContext].
Expand Down Expand Up @@ -45,7 +47,7 @@ class HttpRequestContext {
Uint8List? requestBodyBytes,
String? requestContentType,
}) {
return HttpRequestContext(
final copy = HttpRequestContext(
id: id,
method: method ?? this.method,
uri: uri ?? this.uri,
Expand All @@ -54,5 +56,7 @@ class HttpRequestContext {
requestBodyBytes: requestBodyBytes ?? this.requestBodyBytes,
requestContentType: requestContentType ?? this.requestContentType,
);
attachCaptureGeneration(copy, readCaptureGeneration(this));
return copy;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';

import '../capture/capture_guard.dart';
import '../engine/capture_generation.dart';
import '../model/network_simulation.dart';
import '../model/raw_capture.dart';
import '../engine/interceptly_engine.dart';
Expand Down Expand Up @@ -116,7 +117,11 @@ class DefaultHttpTrafficCollector implements HttpTrafficCollector {
if (uri == null) return true;
final predicate = engine.settings.shouldCapture;
if (predicate == null) return true;
return predicate(uri.toString());
try {
return predicate(uri.toString());
} catch (_) {
return true;
}
}

@override
Expand Down Expand Up @@ -167,6 +172,7 @@ class DefaultHttpTrafficCollector implements HttpTrafficCollector {
requestBodyBytes: bodyBytes,
requestContentType: resolvedContentType,
);
attachCaptureGeneration(context, engine.httpGeneration);

guardCapture(() {
engine.recordPending(
Expand Down Expand Up @@ -270,6 +276,7 @@ class DefaultHttpTrafficCollector implements HttpTrafficCollector {
requestTruncatedAtStorage: isRequestTruncated,
responseTruncatedAtStorage: isResponseTruncated,
);
attachCaptureGeneration(capture, readCaptureGeneration(context));

engine.record(capture);
});
Expand Down Expand Up @@ -336,6 +343,7 @@ class DefaultHttpTrafficCollector implements HttpTrafficCollector {
requestTruncatedAtStorage: isRequestTruncated,
responseTruncatedAtStorage: isResponseTruncated,
);
attachCaptureGeneration(capture, readCaptureGeneration(context));

engine.record(capture);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class RealtimeSessionScope {
});
}

/// Normalizes [data] once and forwards the computed preview and size
/// so [RealtimeTrafficCollector.recordFrame] hits its fast path without
/// renormalizing the payload.
void _record(dynamic data, FrameDirection direction, {String? eventName}) {
if (data == null) return;
final normalized = FrameNormalizer.normalize(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ class DefaultRealtimeTrafficCollector implements RealtimeTrafficCollector {
if (uri == null) return true;
final predicate = engine.settings.shouldCapture;
if (predicate == null) return true;
return predicate(uri.toString());
try {
return predicate(uri.toString());
} catch (_) {
return true;
}
}

@override
Expand Down
14 changes: 12 additions & 2 deletions packages/interceptly/lib/src/capture/frame_normalizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class FrameNormalizer {
///
/// - Computes [originalSizeBytes] with 0 heap byte allocations for UTF-8 text via [utf8ByteLength].
/// - Avoids cloning [Uint8List] / [ByteData] buffers (uses [Uint8List.sublistView] when truncating).
/// - Caps generic [List<int>] windows with take before copying to avoid full allocation.
/// - Generates a safe Unicode [shortPreview] (<= 128 chars).
/// - Flags [shouldOffload] when [originalSizeBytes] >= [offloadThreshold] and [offloadThreshold] > 0.
/// - Pre-encodes [preEncodedBytes] once when [shouldOffload] is true, eliminating downstream re-encoding.
Expand Down Expand Up @@ -85,9 +86,18 @@ class FrameNormalizer {
} else if (raw is List<int> && explicitType != FrameType.text) {
type = explicitType ?? FrameType.binary;
originalSize = raw.length;
final bytes = raw is Uint8List ? raw : Uint8List.fromList(raw);
final Uint8List bytes;
if (raw is Uint8List) {
bytes = raw;
} else if (maxBodyBytes > 0 && originalSize > maxBodyBytes) {
bytes = Uint8List.fromList(raw.take(maxBodyBytes).toList());
} else {
bytes = Uint8List.fromList(raw);
}
shouldOffload = offloadThreshold > 0 && originalSize >= offloadThreshold;
final capped = (maxBodyBytes > 0 && originalSize > maxBodyBytes)
final capped = (maxBodyBytes > 0 &&
originalSize > maxBodyBytes &&
bytes.length > maxBodyBytes)
? bytes.sublist(0, maxBodyBytes)
: bytes;
payload = capped;
Expand Down
15 changes: 10 additions & 5 deletions packages/interceptly/lib/src/capture/stream_capture.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ Stream<List<int>> teeStreamCapture(
Future<void> deliveryChain = Future<void>.value();

void handleChunk(List<int> chunk) {
if (settled || controller.isClosed) return;
if (controller.isClosed) return;
controller.add(chunk);
buffer.add(chunk);
if (!settled) {
buffer.add(chunk);
}
}

sub = source.listen(
Expand Down Expand Up @@ -133,16 +135,19 @@ Stream<List<int>> teeStreamCapture(
deliveryChain = deliveryChain.then((_) {
if (!isCanceled) {
finishDone();
controller.close();
if (!controller.isClosed) {
controller.close();
}
}
}).catchError((_) {});
},
onError: (Object e, StackTrace st) {
deliveryChain = deliveryChain.then((_) {
if (!isCanceled) {
finishError(e, st);
controller.addError(e, st);
controller.close();
if (!controller.isClosed) {
controller.addError(e, st);
}
}
}).catchError((_) {});
},
Expand Down
9 changes: 9 additions & 0 deletions packages/interceptly/lib/src/core/interceptly_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ class Interceptly {
Stream<void>? customTrigger,
InterceptlySettings? settings,
}) {
/// Release guard evaluated before mutating the singleton, so a release
/// call without opt-in leaves existing engine settings untouched.
if (kReleaseMode) {
final allowInRelease =
settings?.allowInRelease ?? engine.settings.allowInRelease;
if (!allowInRelease) {
return Future<void>.value();
}
}
if (settings != null) engine.configure(settings);

return _init(
Expand Down
8 changes: 5 additions & 3 deletions packages/interceptly/lib/src/engine/body_decode_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,14 @@ class BodyDecodeService {
/// Returns a preview of [bytes] truncated to [previewLen] bytes,
/// or `null` when [bytes] is null or empty.
///
/// If [maxBody] <= 0 (unlimited) or [bytes.length] <= [maxBody],
/// the full buffer is returned unchanged.
/// The retained window is capped by the smallest of [previewLen],
/// [maxBody] (when positive), and the buffer length, so a large preview
/// budget can never exceed the storage cap.
static Uint8List? truncate(Uint8List? bytes, int maxBody, int previewLen) {
if (bytes == null || bytes.isEmpty) return null;
if (maxBody <= 0 || bytes.length <= maxBody) return bytes;
final end = previewLen < bytes.length ? previewLen : bytes.length;
var end = previewLen < bytes.length ? previewLen : bytes.length;
if (end > maxBody) end = maxBody;
return bytes.sublist(0, end);
}

Expand Down
Loading
Loading