From a8f8708f0968457f8396aa05703147bed42266eb Mon Sep 17 00:00:00 2001 From: ctufts Date: Tue, 21 Jul 2026 14:42:50 -0400 Subject: [PATCH] Forward a compile-time agent id to native for test tooling Some of our internal test tooling needs to tag outbound requests made during automated test runs with an identifier, so that request volume generated by that tooling can be distinguished from real user traffic. That identifier is resolved at Dart compile time via `--dart-define=MAPBOX_AGENT=` (never set in a normal app build) and now gets forwarded to the native side over the same per-map platform channel already used for other startup configuration, mirroring the existing `map#setMaxRequestsPerHost` handler. The native handlers on Android and iOS are currently placeholders: the underlying native API to actually apply the forwarded id doesn't exist in any released version of the native SDK dependency this plugin consumes yet, so calling it today would break the build. The handlers validate their arguments and no-op, with comments describing the exact one-line change to make once that native API ships. No public API changes. Builds that don't pass the dart-define are completely unaffected. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + .../maps/mapbox_maps/MapboxMapController.kt | 19 +++++++ example/integration_test/all_test.dart | 2 + .../mapbox_agent_id_test.dart | 49 ++++++++++++++++++ .../Classes/MapboxMapController.swift | 22 ++++++++ lib/src/mapbox_maps_platform.dart | 40 +++++++++++++++ test/mapbox_maps_platform_test.dart | 50 +++++++++++++++++++ 7 files changed, 183 insertions(+) create mode 100644 example/integration_test/mapbox_agent_id_test.dart create mode 100644 test/mapbox_maps_platform_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f067ab926..6782316d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### main +* Internal: add test-only plumbing to forward a `--dart-define=MAPBOX_AGENT=` value to the native platform channel at map creation, for use by this SDK's own test tooling. No public API changes; builds that do not pass this define are unaffected. * Add `ModelSource` API, exposing the 3D model source (a collection of 3D models, each with its own position, orientation, and node/material overrides). * Add `MapWidget.isOpaque` option to control whether the map is rendered as opaque or supports a transparent background. Set to `false` (together with a transparent style) to enable transparency on iOS; Android already supports this via `MapWidget.textureView` ([#415](https://github.com/mapbox/mapbox-maps-flutter/issues/415)). * [iOS] Fix `updateSettings` on `CompassSettings`, `AttributionSettings`, `LogoSettings`, `IndoorSelectorSettings`, `ScaleBarSettings`, `GesturesSettings`, and `LocationComponentSettings` resetting omitted fields (position, margins, `enabled`, `scrollMode`, puck configuration) to defaults instead of preserving them, matching Android's partial-update behaviour. diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt index e585d2727..ef1f313bc 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt @@ -404,6 +404,25 @@ class MapboxMapController( result.error("MAX_REQUESTS_PER_HOST_ERROR", e.message, null) } } + "platform#setMapboxAgentId" -> { + // Forwards the compile-time-resolved MAPBOX_AGENT id (see + // `--dart-define=MAPBOX_AGENT=`, only used by internal test tooling) into + // Common's agent-context registry, mirroring the `map#setMaxRequestsPerHost` + // call above. + // + // TODO: once a released `com.mapbox.common:common-android` artifact exposes + // `com.mapbox.common.MapboxAgentContextFactory`, replace this no-op with: + // com.mapbox.common.MapboxAgentContextFactory.getInstance().setMapboxAgentId(agentId) + // That class does not exist in any published Common release yet, so calling it + // here today would fail to compile; this handler is a placeholder that keeps the + // Dart-to-native wiring in place and ready to activate with a one-line change. + val agentId = call.argument("agentId") + if (agentId.isNullOrEmpty()) { + result.error("INVALID_ARGUMENTS", "agentId cannot be null or empty", null) + } else { + result.success(null) + } + } else -> { result.notImplemented() } diff --git a/example/integration_test/all_test.dart b/example/integration_test/all_test.dart index c1cf1e208..a717bd384 100644 --- a/example/integration_test/all_test.dart +++ b/example/integration_test/all_test.dart @@ -48,6 +48,7 @@ import 'compass_test.dart' as compass_test; import 'indoor_selector_test.dart' as indoor_selector_test; import 'scale_bar_test.dart' as scale_bar_test; import 'http_service_test.dart' as http_service_test; +import 'mapbox_agent_id_test.dart' as mapbox_agent_id_test; import 'offline_test.dart' as offline_test; import 'snapshotter/snapshotter_test.dart' as snapshotter_test; import 'viewport_test.dart' as viewport_test; @@ -80,6 +81,7 @@ void main() { // offline_test offline_test.main(); http_service_test.main(); + mapbox_agent_id_test.main(); // style tests style_test.main(); diff --git a/example/integration_test/mapbox_agent_id_test.dart b/example/integration_test/mapbox_agent_id_test.dart new file mode 100644 index 000000000..5fb9c3a1c --- /dev/null +++ b/example/integration_test/mapbox_agent_id_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'empty_map_widget.dart' as app; + +// Verifies the wiring that forwards a compile-time `MAPBOX_AGENT` id +// (`--dart-define=MAPBOX_AGENT=`) into the native platform channel when a +// real MapWidget is created. `MAPBOX_AGENT` is internal test tooling used to +// measure AI-coding-agent-driven traffic; it is never set for a normal app +// build. +// +// This test is self-verifying with respect to that define: +// - Run as `flutter test integration_test/mapbox_agent_id_test.dart` (no +// define, the default for this repo's test suite): confirms rendering a +// MapWidget still works with no agent id forwarded — i.e. this plumbing has +// zero effect on a normal build. +// - Run as `flutter test integration_test/mapbox_agent_id_test.dart +// --dart-define=MAPBOX_AGENT=claude-code`: additionally confirms the id is +// forwarded to native without the map failing to load. +// +// What this test does NOT verify: the resulting outbound `User-Agent` header +// actually carrying ` agent/claude-code`. This plugin has no Dart-visible API +// for inspecting raw outbound request headers, and forwarding the id into +// Mapbox Common's native agent-context registry is currently a documented +// no-op placeholder (see the `platform#setMapboxAgentId` handlers in +// MapboxMapController on Android/iOS) pending a Common release that exposes +// that registry as a dependency here. Once that ships and those handlers are +// wired up for real, header-level verification belongs in Common's own HTTP +// layer tests rather than being re-implemented in this plugin. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('MAPBOX_AGENT forwarding', () { + testWidgets( + 'renders a MapWidget successfully whether or not MAPBOX_AGENT is set', + (widgetTester) async { + final mapFuture = app.main(); + await widgetTester.pumpAndSettle(); + final mapboxMap = await mapFuture; + + // The map above must load regardless of kMapboxAgentIdForTesting's + // value; forwarding the id (or not) must never break map creation. + // Errors from the forwarding call itself are caught and logged rather + // than surfaced here (see _forwardMapboxAgentIdIfSet), so this only + // proves the forwarding attempt doesn't interfere with map creation — + // not that the native side actually received/applied the id. + expect(mapboxMap, isNotNull); + }); + }); +} diff --git a/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift index d5dfa2100..58ac55701 100644 --- a/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift +++ b/ios/mapbox_maps_flutter/Sources/mapbox_maps_flutter/Classes/MapboxMapController.swift @@ -217,6 +217,28 @@ public final class MapboxMapController: NSObject, FlutterPlatformView { HttpServiceFactory.setMaxRequestsPerHostForMax(UInt8(clamping: max)) result(nil) + case "platform#setMapboxAgentId": + // Forwards the compile-time-resolved MAPBOX_AGENT id (see + // `--dart-define=MAPBOX_AGENT=`, only used by internal test tooling) into + // Common's agent-context registry, mirroring `map#setMaxRequestsPerHost` above. + // + // TODO: once a released Mapbox Common binary exposes a Swift/Obj-C binding for + // its agent-context registry, replace this no-op with the real forwarding call. + // That binding does not exist yet in any published release, so referencing it + // here today would fail to build; this handler is a placeholder that keeps the + // Dart-to-native wiring in place and ready to activate once it ships. + guard let arguments = methodCall.arguments as? [String: Any], + let agentId = arguments["agentId"] as? String, !agentId.isEmpty + else { + result(FlutterError( + code: "setMapboxAgentId", + message: "could not decode arguments", + details: nil + )) + return + } + result(nil) + default: result(FlutterMethodNotImplemented) } diff --git a/lib/src/mapbox_maps_platform.dart b/lib/src/mapbox_maps_platform.dart index 12b261009..88dbb8fa9 100644 --- a/lib/src/mapbox_maps_platform.dart +++ b/lib/src/mapbox_maps_platform.dart @@ -2,6 +2,28 @@ part of mapbox_maps_flutter; typedef OnPlatformViewCreatedCallback = void Function(int); +/// The AI-coding-agent identifier to forward to the native Mapbox Common SDK, +/// resolved at compile time via `--dart-define=MAPBOX_AGENT=`. +/// +/// Common resolves most agent detection itself, but on platforms where the +/// value can only be baked in at compile time (as is the case here, since +/// Dart's [String.fromEnvironment] is not a runtime OS environment variable +/// Common could read via `getenv`), a platform binding has to forward the +/// resolved id explicitly. This exists purely to support internal test +/// tooling — it is empty, and forwarding is skipped entirely, for every +/// normal app build, since none of them pass this define. +/// +/// Exposed (rather than kept private) only so it can be exercised from this +/// package's tests; it is not part of the supported public API. +@internal +const String kMapboxAgentIdForTesting = + String.fromEnvironment('MAPBOX_AGENT', defaultValue: ''); + +/// The platform channel method used to forward [kMapboxAgentIdForTesting] to +/// the native side. Exposed for tests; not part of the supported public API. +@internal +const String kSetMapboxAgentIdMethod = 'platform#setMapboxAgentId'; + class _MapboxMapsPlatform { late final MethodChannel _channel = MethodChannel( 'plugins.flutter.io.${channelSuffix.toString()}', @@ -13,6 +35,24 @@ class _MapboxMapsPlatform { _MapboxMapsPlatform( {required this.binaryMessenger, required this.channelSuffix}) { _channel.setMethodCallHandler(_handleMethodCall); + _forwardMapboxAgentIdIfSet(); + } + + /// Forwards the compile-time [kMapboxAgentIdForTesting], if any, to the + /// native side as early as possible — before the platform view (and thus + /// the native map engine) is created — so it is set before this map + /// instance's requests go out. A no-op for any build that does not pass + /// `--dart-define=MAPBOX_AGENT=`. + void _forwardMapboxAgentIdIfSet() { + if (kMapboxAgentIdForTesting.isEmpty) { + return; + } + _channel.invokeMethod( + kSetMapboxAgentIdMethod, + {'agentId': kMapboxAgentIdForTesting}, + ).catchError((Object error) { + print('Failed to forward MAPBOX_AGENT id to native platform: $error'); + }); } _MapboxMapsPlatform.instance(int channelSuffix) diff --git a/test/mapbox_maps_platform_test.dart b/test/mapbox_maps_platform_test.dart new file mode 100644 index 000000000..d12bc2cb0 --- /dev/null +++ b/test/mapbox_maps_platform_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channelSuffix = 0; + final channel = MethodChannel('plugins.flutter.io.$channelSuffix'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + late List log; + + setUp(() { + log = []; + messenger.setMockMethodCallHandler(channel, (call) async { + log.add(call); + return null; + }); + }); + + tearDown(() { + messenger.setMockMethodCallHandler(channel, null); + }); + + group('MAPBOX_AGENT forwarding', () { + test( + 'MapboxMap.fromNativeController forwards the compile-time MAPBOX_AGENT ' + 'id to native only when --dart-define=MAPBOX_AGENT= was passed to ' + 'this test run', () async { + // kMapboxAgentIdForTesting is a compile-time constant resolved from + // `--dart-define=MAPBOX_AGENT=`. This test is self-verifying either + // way: normal `flutter test` runs (no define passed) exercise the + // no-forwarding branch, while `flutter test --dart-define=MAPBOX_AGENT=` + // exercises the forwarding branch. + MapboxMap.fromNativeController(channelSuffix); + // The forwarding call is fire-and-forget, so let its microtask flush. + await pumpEventQueue(); + + if (kMapboxAgentIdForTesting.isEmpty) { + expect(log, isEmpty); + } else { + expect(log, hasLength(1)); + expect(log.single.method, kSetMapboxAgentIdMethod); + expect(log.single.arguments, {'agentId': kMapboxAgentIdForTesting}); + } + }); + }); +}