Skip to content

[Flutter] Add network request/response logging (manual + automatic) - #1187

Open
iainfinlayson wants to merge 4 commits into
mainfrom
iain/flutter-network-logging
Open

iainfinlayson wants to merge 4 commits into
mainfrom
iain/flutter-network-logging

Conversation

@iainfinlayson

@iainfinlayson iainfinlayson commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Goal

Fills one of the alpha's documented gaps: no HttpRequestInfo/HttpResponseInfo equivalent was exposed in the Flutter plugin. Adds two complementary APIs:

  1. Manual: Capture.logNetworkRequest(...) / Capture.logNetworkResponse(...), bridged to the native HttpRequestInfo/HttpResponseInfo APIs on both platforms.
  2. Automatic: Capture.enableNetworkInstrumentation(), a dart:io HttpOverrides that captures requests/responses through the default HTTP stack with zero code changes at each call site — calls the exact same manual API under the hood, no new native code.

Depends on #1186 (stacked on it — the example app's Android build doesn't work without that fix). Merge #1186 first; this diff will shrink once that lands.

Manual API

  • platform/capture_flutter/lib/src/network.dartHttpRequestInfo, HttpResponse, HttpRequestMetrics, HttpUrlPath, HttpResult. Includes a small dependency-free UUID v4 generator for spanId.
  • Android (CaptureFlutterPlugin.kt) and both iOS plugin copies bridge these to native. Stateless design — each call reconstructs the request/response as a native value type rather than holding a live object across calls, since these are value types compared structurally, not by reference (no leak-prone map like activeSpans). Durations/metrics are milliseconds throughout the Dart API; the iOS bridge converts to TimeInterval seconds for the native call.

Automatic instrumentation

  • platform/capture_flutter/lib/src/network_instrumentation.dartCapture.enableNetworkInstrumentation() installs an HttpOverrides wrapping HttpClient/HttpClientRequest/HttpClientResponse. Implemented against the exact dart:io interfaces (read directly from the local Dart SDK source, not from memory, to avoid subtly wrong overrides). Chains any HttpOverrides the app already set (e.g. cert pinning) rather than clobbering it. Reads the x-capture-path-template header — the same convention the shop demos already use — for path-template collapsing, since automatic instrumentation has no other way to infer one.

  • Calls the exact same Capture.logNetworkRequest/logNetworkResponse Dart API from the manual path above — no additional native Kotlin/Swift code. Both paths converge on the identical handleLogNetworkRequest/handleLogNetworkResponse native handlers.

  • README documents an explicit "when automatic works vs. when manual is required" table, mirroring the native SDKs' own split (OkHttp EventListener/URLSession swizzling vs. their own manual HttpRequestInfo/HttpResponseInfo APIs):

    Automatic Manual
    Raw dart:io HttpClient, package:http's default client, dio's default adapter (mobile)
    dio with CronetHttpAdapter/CupertinoHttpAdapter, or any client bypassing dart:io ❌ invisible to HttpOverrides ✅ required
    Native SDKs making their own requests (Firebase, ad/payment SDKs) ❌ never touches Dart ✅ required
    WebViews ❌ requests run inside the native WebView engine Not yet bridged to Flutter at all
    HttpClient created before enableNetworkInstrumentation() ❌ already built via the un-overridden factory ✅ required for that instance
    Networking in a different isolate ❌ per-isolate override ✅ required, or call it again in that isolate
    Path template collapsing Only via x-capture-path-template header Pass HttpUrlPath(value, template:) directly

Bugs found and fixed along the way

  • iOS plugin drift: the SPM copy (ios/capture_flutter/Sources/capture_flutter/CaptureFlutterPlugin.swift) was missing setEntityId/clearEntityId, which [Flutter] Add setEntity/clearEntity + bump version #1178 only added to the CocoaPods copy (ios/Classes/CaptureFlutterPlugin.swift). Re-synced for now; worth consolidating the two copies eventually so this can't drift again.
  • Unsigned release APK: examples/flutter/android/app/build.gradle.kts's release build type had no signingConfigflutter build apk --release silently produced a genuinely unsigned APK (INSTALL_PARSE_FAILED_NO_CERTIFICATES on install). Fixed by signing with the debug key, matching the sa-public shop demo.

Example app

Added two buttons to examples/flutter: "Network Request (ping google)" (manual) and "Network Request — auto (ping google)" (zero manual logging calls, relies entirely on enableNetworkInstrumentation()). Worth noting: no existing example app in this repo — Android, iOS, or any bitdrift-shop variant — demonstrates the manual network API interactively; they all rely on automatic OkHttp/URLSession instrumentation. This seemed like the only way to actually exercise both new APIs by hand.

Verification

Built, installed, and ran on both platforms (Android emulator + iOS Simulator):

  • Manual path: tapped the button, confirmed in the bitdrift session timeline (staging tenant) — HTTPRequest/HTTPResponse entries present, correct _status_code, _result, _duration_ms, _request.* correlation fields, built-in network matchers picked it up.
  • Automatic path: tapped the "auto" button (a plain HttpClient GET with zero manual logging calls) on both platforms — real 200 response with correct byte count, zero Dart-level exceptions, zero platform-channel errors, on both the synchronous request-time log call and the unawaited async response-time log call.

  • CHANGELOG.md's "Unreleased" section has been updated, if applicable. (Flutter-specific changes go in platform/capture_flutter/ALPHA_RELEASES.md instead, per its own convention — updated there for both APIs.)

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@iainfinlayson iainfinlayson changed the title [Flutter] Add manual network request/response logging [Flutter] Add network request/response logging (manual + automatic) Sep 3, 2026
platform/capture_flutter/android/build.gradle.kts and
examples/flutter/android/app/build.gradle.kts each declare a
`kotlin { compilerOptions { jvmTarget = ... } }` block but never apply
org.jetbrains.kotlin.android in their own plugins{} block. Gradle only
generates the typed `kotlin {}` accessor for plugins applied within that
same script's plugins{} block, not for ones applied transitively (e.g. by
the Flutter Gradle plugin), so both modules failed to compile:

  Unresolved reference 'compilerOptions' / 'jvmTarget'

Since capture_flutter's own Kotlin plugin module never compiled, any
Android app depending on it failed with:

  cannot find symbol: class CaptureFlutterPlugin

Also pins a Kotlin Gradle plugin version (2.3.20, matching platform/jvm)
in the example app's settings.gradle.kts, since neither module declared
one and Flutter's minimum supported Kotlin version (1.8.10) is newer than
Gradle's own default.

Verified: `flutter build apk --release` succeeds in examples/flutter
against this branch (previously failed on main).
Adds Capture.logNetworkRequest / Capture.logNetworkResponse to the alpha
Flutter plugin, bridging to the native HttpRequestInfo/HttpResponseInfo
APIs on both Android and iOS. Fills one of the "Not Supported Yet" gaps
called out for the alpha (no HttpRequestInfo/HttpResponseInfo equivalent
was exposed).

- lib/src/network.dart: HttpRequestInfo, HttpResponse, HttpRequestMetrics,
  HttpUrlPath, HttpResult, with a dependency-free UUID v4 generator for
  spanId (no new pub package needed).
- Android/iOS bridges reconstruct the request/response as native value
  types per call (stateless — no live-object map to leak), matching field
  shapes 1:1 against the native source. Durations/metrics are milliseconds
  throughout the Dart API; the iOS bridge converts to TimeInterval seconds.
- Also fixes a drift bug: the SPM copy of the iOS plugin
  (ios/capture_flutter/Sources/...) was missing the setEntityId/
  clearEntityId handlers that a prior PR (#1178) only added to the
  CocoaPods copy (ios/Classes/...). Both copies are now identical again.
- examples/flutter: adds a real "Network Request (ping google)" button
  (no other example app in this repo — Android, iOS, or any bitdrift-shop
  variant — demonstrates the manual network API interactively; all rely on
  automatic OkHttp/URLSession instrumentation instead, which Flutter has
  no equivalent of). Also fixes a separate pre-existing bug: the release
  build type had no signingConfig, so `flutter build apk --release`
  produced a genuinely unsigned, uninstallable APK.

Verified end-to-end on both platforms: real GET to google.com logged via
the new API, confirmed in the bitdrift session timeline with correct
_status_code, _result, _duration_ms, and _request.* correlation fields.

Stacked on the pending Kotlin-plugin build fix (this branch's parent).
Adds Capture.enableNetworkInstrumentation(), which installs a dart:io
HttpOverrides that automatically logs every request/response made
through the default HTTP stack -- no code changes needed at each call
site, unlike the manual logNetworkRequest/logNetworkResponse API added
earlier in this branch.

- lib/src/network_instrumentation.dart: wraps HttpClient/
  HttpClientRequest/HttpClientResponse, reconstructed against the exact
  dart:io interfaces (read directly from the local Dart SDK source, not
  from memory) to avoid subtly wrong overrides. Chains any HttpOverrides
  the app already set (e.g. cert pinning) rather than clobbering it.
  Reads the x-capture-path-template header (the same convention the shop
  demos already use) for path-template collapsing, since automatic
  instrumentation has no other way to infer one.
- Calls the exact same Capture.logNetworkRequest/logNetworkResponse Dart
  API added earlier in this stack -- no native Kotlin/Swift changes at
  all. Automatic and manual paths converge on the identical
  handleLogNetworkRequest/handleLogNetworkResponse native handlers.
- README.md: documents both APIs plus an explicit "when automatic works
  vs. when manual is required" table, mirroring the native SDKs' own
  automatic-vs-manual split (OkHttp EventListener/URLSession swizzling
  vs. the manual HttpRequestInfo/HttpResponseInfo API):
    - Covered automatically: raw dart:io HttpClient, package:http's
      default client, dio's default adapter on mobile.
    - Manual required: HTTP clients that bypass dart:io (dio configured
      with CronetHttpAdapter/CupertinoHttpAdapter), native SDKs making
      their own requests, WebViews (a separate, not-yet-bridged
      integration point), HttpClient instances created before
      enableNetworkInstrumentation() runs, and networking done in an
      isolate that hasn't called it.
- examples/flutter: adds a second button ("Network Request — auto") that
  fires a plain HttpClient request with zero manual logging calls around
  it, to demonstrate/prove the automatic path.

Verified on both platforms: real GET to google.com captured with correct
status code and byte count, with zero manual logging calls and zero
runtime exceptions on either the request-time or the (unawaited,
async) response-time logging call.
@iainfinlayson
iainfinlayson force-pushed the iain/fix-capture-flutter-android-kotlin-plugin branch from f4a156c to 13f7860 Compare September 4, 2026 12:13
@iainfinlayson
iainfinlayson force-pushed the iain/flutter-network-logging branch from 55ea79a to 33a7ee2 Compare September 4, 2026 12:13
@iainfinlayson

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

Base automatically changed from iain/fix-capture-flutter-android-kotlin-plugin to main September 4, 2026 13:12
…nifest (#1199)

Keeps this branch on the pre-Built-in-Kotlin toolchain (Flutter 3.10+/Dart
3.0+) while still tracking the latest native Android/iOS Capture SDK
releases, so customers who can't yet adopt Flutter 3.44+ still get current
SDK fixes. Also fixes the Package.swift swift-tools-version ordering bug
(SPM rejects the manifest otherwise) independently of the Kotlin migration.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant