Skip to content

Commit

Permalink
Merge pull request #1 from lohnn/tracking_interceptor
Browse files Browse the repository at this point in the history
Created TrackingInterceptor as an alternative to TrackedDioClient and supporting Dio 5.
  • Loading branch information
teodor-appd authored Mar 30, 2023
2 parents eda770d + 756e01f commit a76ca20
Show file tree
Hide file tree
Showing 10 changed files with 378 additions and 13 deletions.
28 changes: 28 additions & 0 deletions example/integration_test/features/request_tracker_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,31 @@ extension on WidgetTester {
expect(actualHeaders[key.toLowerCase()], value.first);
});
}

assertDioInterceptorBeaconSent() async {
final requestSentLabel = find.text("Success with 200.");
await ensureVisible(requestSentLabel);
expect(requestSentLabel, findsOneWidget);

final trackerRequests = await findRequestsBy(
url: successURL,
type: "network-request",
hrc: "200",
$is: "Manual HttpTracker",
);
expect(trackerRequests.length, 3);

final actualRequests = await findRequestsBy(type: "trackeddiointerceptor");
expect(actualRequests.length, 1);

// Also assert correlation headers are added
final Map<String, dynamic> actualHeaders =
actualRequests[0]["request"]["headers"];
final btHeaders = await RequestTracker.getServerCorrelationHeaders();
btHeaders.forEach((key, value) {
expect(actualHeaders[key.toLowerCase()], value.first);
});
}
}

void main() {
Expand Down Expand Up @@ -147,6 +172,9 @@ void main() {
await tester.tapAndSettle("manualDioClientGetRequestButton");
await tester.flushBeacons();
await tester.assertDioTrackerBeaconSent();
await tester.tapAndSettle("manualDioInterceptorGetRequestButton");
await tester.flushBeacons();
await tester.assertDioInterceptorBeaconSent();
});

tearDown(() async {
Expand Down
2 changes: 1 addition & 1 deletion example/integration_test/tests.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

// Configure these based on the system's emulator/simulator names.
def android_device = "Android SDK built for x86"
def iOS_device = "iPhone 13"
def iOS_device = "iPhone 14"

def test_driver_path = "integration_test/test_driver/integration_test.dart"
def integration_tests_path = "integration_test/features/"
Expand Down
39 changes: 33 additions & 6 deletions example/lib/feature_list/features/manual_network_requests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,32 @@ class _ManualNetworkRequestsState extends State<ManualNetworkRequests> {
}
}

Future<void> _sendDioInterceptorRequestButtonPressed() async {
var urlString = urlFieldController.text;
if (urlString.trim().isEmpty) {
return;
}

try {
setState(() {
responseText = "Loading...";
});

final dioClient = Dio();
dioClient.interceptors.add(TrackedDioInterceptor());

final response = await dioClient.post(urlString,
data: "[{\"type\": \"trackeddiointerceptor\"}]");
setState(() {
responseText = "Success with ${response.statusCode}.";
});
} catch (e) {
setState(() {
responseText = "Failed with ${e.toString()}.";
});
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
Expand Down Expand Up @@ -218,18 +244,19 @@ class _ManualNetworkRequestsState extends State<ManualNetworkRequests> {
onPressed: _sendPostRequestButtonPressed),
ElevatedButton(
key: const Key("manualHttpClientGetRequestButton"),
child: const Text(
'TrackedHttpClient GET request\n'
'(has custom header: "foo")',
child: const Text('TrackedHttpClient GET request',
textAlign: TextAlign.center),
onPressed: _sendHttpClientRequestButtonPressed),
ElevatedButton(
key: const Key("manualDioClientGetRequestButton"),
child: const Text(
'TrackedDioClient GET request\n'
'(has custom header: "foo")',
child: const Text('TrackedDioClient GET request',
textAlign: TextAlign.center),
onPressed: _sendDioClientRequestButtonPressed),
ElevatedButton(
key: const Key("manualDioInterceptorGetRequestButton"),
child: const Text('TrackedDioInterceptor GET request',
textAlign: TextAlign.center),
onPressed: _sendDioInterceptorRequestButtonPressed),
const SizedBox(
height: 30,
),
Expand Down
1 change: 1 addition & 0 deletions lib/appdynamics_agent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export 'src/instrumentation.dart';
export 'src/request_tracker.dart';
export 'src/tracked_clients/tracked_http_client.dart';
export 'src/tracked_clients/tracked_dio_client.dart';
export 'src/tracked_clients/tracked_dio_interceptor.dart';
export 'src/session_frame.dart' hide createSessionFrame;
export 'src/activity_tracking/widget_tracker.dart' hide TrackedWidget;
export 'src/activity_tracking/navigation_observer.dart';
3 changes: 2 additions & 1 deletion lib/src/request_tracker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ class RequestTracker {
/// Sets a dictionary representing the keys and values from the server
/// response's headers.
///
/// If an error occurred and a response was not received, this not be called.
/// If an error occurred and a response was not received, this should not be
/// called.
///
/// Method might throw [Exception].
Future<RequestTracker> setResponseHeaders(
Expand Down
1 change: 1 addition & 0 deletions lib/src/tracked_clients/tracked_dio_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:dio/io.dart';
import '../../appdynamics_agent.dart';

/// Use this client to track requests made via the `dio` package (version <5).
/// For Dio version 5 and above, see [TrackedDioInterceptor].
///
/// ```dart
/// import 'package:dio/dio.dart';
Expand Down
103 changes: 103 additions & 0 deletions lib/src/tracked_clients/tracked_dio_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import 'package:appdynamics_agent/appdynamics_agent.dart';
import 'package:dio/dio.dart';

/// Use this class to create a custom Dio [Interceptor] that tracks requests
/// automatically. Alternative to [TrackedDioClient].
///
/// ```dart
/// import 'package:dio/dio.dart';
///
/// try {
/// final dio = Dio();
/// dio.interceptors.add(TrackedDioInterceptor());
/// final response = await dio.post(urlString, data: {"foo": "bar"});
/// // handle response
/// } catch (e) {
/// // handle error
/// }
/// ```
class TrackedDioInterceptor implements Interceptor {
final bool addCorrelationHeaders;

final Map<String, RequestTracker> _activeTrackers = {};

TrackedDioInterceptor({this.addCorrelationHeaders = true});

static const _trackerId = 'trackerId';

@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
try {
if (addCorrelationHeaders) {
final correlationHeaders =
await RequestTracker.getServerCorrelationHeaders();
final headers = correlationHeaders.map(
(key, value) => MapEntry(key, value.first),
);
options.headers.addAll(headers);
}

var url = options.uri.toString();
final tracker = await RequestTracker.create(url);
_activeTrackers[tracker.id] = tracker;
options.extra[_trackerId] = tracker.id;
} finally {
handler.next(options);
}
}

@override
void onResponse(
Response<dynamic> response,
ResponseInterceptorHandler handler,
) async {
try {
final tracker = _activeTrackers.remove(
response.requestOptions.extra[_trackerId],
);
if (tracker != null) {
await tracker.setResponseStatusCode(response.statusCode ?? 404);
await _logResponse(response, tracker);
await tracker.reportDone();
}
} finally {
handler.next(response);
}
}

@override
void onError(DioError err, ErrorInterceptorHandler handler) async {
try {
final tracker = _activeTrackers.remove(
err.requestOptions.extra[_trackerId],
);
if (tracker != null) {
final statusCode = err.response?.statusCode;
if (statusCode != null) {
// TODO: Find a way to test this (coverage).
// Errors for when status code is not in accepted range should be recorded as normal
await tracker.setResponseStatusCode(statusCode);
await _logResponse(err.response, tracker);
} else {
// If status code is null, it means that the error is not a response, but rather a network error
await tracker.setError(err.toString(), err.stackTrace.toString());
}
await tracker.reportDone();
}
} finally {
handler.next(err);
}
}

Future _logResponse(Response? response, RequestTracker tracker) async {
if (response != null) {
await tracker.setRequestHeaders(
response.requestOptions.headers.map((k, v) => MapEntry(k, <String>[v])),
);
await tracker.setResponseHeaders(response.headers.map);
}
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dependencies:
flutter:
sdk: flutter
http: ^0.13.3
dio: ^4.0.6
dio: ^5.1.0

dev_dependencies:
flutter_test:
Expand Down
8 changes: 4 additions & 4 deletions test/tracked_dio_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ void main() {

// Used to not duplicate logic that has same results but only one different
// parameter (i.e. request options).
void happyPathTestLogic(
Future happyPathTestLogic(
Options? options, Map<String, List<String>> expectedHeaders) async {
final dio = Dio();
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
Expand Down Expand Up @@ -91,20 +91,20 @@ void main() {
final headers = await RequestTracker.getServerCorrelationHeaders();
headers.addAll(
customHeaders.map((key, value) => MapEntry(key, <String>[value])));
happyPathTestLogic(Options(headers: customHeaders), headers);
await happyPathTestLogic(Options(headers: customHeaders), headers);
});

test('TrackedDioClient happy path works with `null` options', () async {
log = [];
final headers = await RequestTracker.getServerCorrelationHeaders();
happyPathTestLogic(null, headers);
await happyPathTestLogic(null, headers);
});

test('TrackedDioClient happy path works with `null` option headers',
() async {
log = [];
final headers = await RequestTracker.getServerCorrelationHeaders();
happyPathTestLogic(Options(headers: null), headers);
await happyPathTestLogic(Options(headers: null), headers);
});

test('TrackedDioClient error-path methods are called correctly', () async {
Expand Down
Loading

0 comments on commit a76ca20

Please sign in to comment.