diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8ed17..c3919d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.6 + +* Adds configurable request header instrumentation to network events + The agent will now produce network event attributes for select header values if the headers are detected on the request. The header names to instrument are passed into the agent when started. +* Updated the native Android agent to version 7.2.0. +* Updated the native iOS agent to version 7.4.8. + ## 1.0.5 * Fixed issue in Flutter agent causing appbuild and appversion fields to overwrite for iOS mobile-handled exceptions. diff --git a/README.md b/README.md index b0d81a0..82d5cd5 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Install NewRelic plugin into your dart project by adding it to dependecies in yo ```yaml dependencies: - newrelic_mobile: 1.0.5 + newrelic_mobile: 1.0.6 ``` @@ -203,7 +203,7 @@ final router = GoRouter( } dependencies { ... - classpath "com.newrelic.agent.android:agent-gradle-plugin:7.1.0" + classpath "com.newrelic.agent.android:agent-gradle-plugin:7.2.0" } } ``` @@ -353,6 +353,18 @@ or [Android SDK](https://docs.newrelic.com/docs/mobile-monitoring/new-relic-mobi NewrelicMobile.instance.incrementAttribute("FlutterCustomAttrNumber",value :5.0); ``` +### [shutdown](https://docs.newrelic.com/docs/mobile-monitoring/new-relic-mobile-android/android-sdk-api/shut-down/)() : void; +> Shut down the agent within the current application lifecycle during runtime. +```dart + NewrelicMobile.instance.shutdown(); +``` +### [shutdown](https://docs.newrelic.com/docs/mobile-monitoring/new-relic-mobile/mobile-sdk/add-tracked-headers/)() : void; +> This API allows you to add any header field strings to a list that gets recorded as attributes with networking request events. After header fields have been added using this function, if the headers are in a network call they will be included in networking events in NR1. +```dart + NewrelicMobile.instance.addHTTPHeadersTrackingFor(["Car","Music"]); +``` + + ## Manual Error reporting You can register non fatal exceptions using the following method with Custom Attributes: diff --git a/android/build.gradle b/android/build.gradle index 0ab8b74..9b8ae08 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -35,6 +35,10 @@ android { if (agpVersion >= 7) { namespace "com.newrelic.newrelic_mobile" } + + defaultConfig { + minSdkVersion 24 + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -55,6 +59,8 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.newrelic.agent.android:android-agent:7.0.0' + implementation 'com.newrelic.agent.android:android-agent:7.2.0' +// implementation "com.newrelic.agent.android:agent-ndk:1.0.4" + } diff --git a/android/src/main/kotlin/com/newrelic/newrelic_mobile/NewrelicMobilePlugin.kt b/android/src/main/kotlin/com/newrelic/newrelic_mobile/NewrelicMobilePlugin.kt index 1934731..e0ec478 100644 --- a/android/src/main/kotlin/com/newrelic/newrelic_mobile/NewrelicMobilePlugin.kt +++ b/android/src/main/kotlin/com/newrelic/newrelic_mobile/NewrelicMobilePlugin.kt @@ -10,6 +10,7 @@ import android.os.Build import androidx.annotation.NonNull import com.newrelic.agent.android.ApplicationFramework import com.newrelic.agent.android.FeatureFlag +import com.newrelic.agent.android.HttpHeaders import com.newrelic.agent.android.NewRelic import com.newrelic.agent.android.metric.MetricUnit import com.newrelic.agent.android.stats.StatsEngine @@ -93,9 +94,9 @@ class NewrelicMobilePlugin : FlutterPlugin, MethodCallHandler { applicationToken ).withLoggingEnabled(loggingEnabled!!) .withLogLevel(5) - .withApplicationFramework(ApplicationFramework.Flutter, "1.0.5").start(context) + .withApplicationFramework(ApplicationFramework.Flutter, "1.0.6").start(context) NewRelic.setAttribute("DartVersion", dartVersion) - StatsEngine.get().inc("Supportability/Mobile/Android/Flutter/Agent/1.0.5"); + StatsEngine.get().inc("Supportability/Mobile/Android/Flutter/Agent/1.0.6"); result.success("Agent Started") } "setUserId" -> { @@ -149,7 +150,7 @@ class NewrelicMobilePlugin : FlutterPlugin, MethodCallHandler { } } val eventRecorded = - NewRelic.recordCustomEvent(eventType, eventName, eventAttributes); + NewRelic.recordCustomEvent(eventType, eventName, eventAttributes) result.success(eventRecorded) } "startInteraction" -> { @@ -213,6 +214,8 @@ class NewrelicMobilePlugin : FlutterPlugin, MethodCallHandler { val bytesReceived: Long = call.argument("bytesReceived")!! val responseBody: String? = call.argument("responseBody")!! val traceAttributes: HashMap? = call.argument("traceAttributes") + val params: HashMap? = call.argument("params") + NewRelic.noticeHttpTransaction( url, @@ -223,11 +226,11 @@ class NewrelicMobilePlugin : FlutterPlugin, MethodCallHandler { bytesSent, bytesReceived, responseBody, - null, + params, null, traceAttributes ) - result.success("Http Transcation Recorded") + result.success("Http Transaction Recorded") } "noticeNetworkFailure" -> { @@ -318,6 +321,13 @@ class NewrelicMobilePlugin : FlutterPlugin, MethodCallHandler { "currentSessionId" -> { result.success(NewRelic.currentSessionId()) } + "addHTTPHeadersTrackingFor" -> { + val headers: ArrayList? = call.argument("headers") as ArrayList? + result.success(NewRelic.addHTTPHeadersTrackingFor(headers)) + } + "getHTTPHeadersTrackingFor" -> { + result.success(HttpHeaders.getInstance().httpHeaders.toList()) + } else -> { result.notImplemented() } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index ce70b2a..d0edfa2 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -45,7 +45,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.newrelic.newrelic_mobile_example" - minSdkVersion flutter.minSdkVersion + minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/build.gradle b/example/android/build.gradle index f0a01b9..0e0dc15 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -10,7 +10,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.newrelic.agent.android:agent-gradle-plugin:7.1.0' + classpath 'com.newrelic.agent.android:agent-gradle-plugin:7.2.0' } } diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 42bcc8d..d4ea8da 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -346,7 +346,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6P59W973U9; + DEVELOPMENT_TEAM = SU7SUNGZJP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -475,7 +475,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6P59W973U9; + DEVELOPMENT_TEAM = SU7SUNGZJP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -498,7 +498,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6P59W973U9; + DEVELOPMENT_TEAM = SU7SUNGZJP; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/example/lib/main.dart b/example/lib/main.dart index 8cc92c7..d7925ff 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -59,6 +59,7 @@ void main() { }); NewrelicMobile.instance.setMaxEventPoolSize(3000); NewrelicMobile.instance.setMaxEventBufferTime(200); + NewrelicMobile.instance.addHTTPHeadersTrackingFor(["Car","Music"]); } /// The main app. @@ -141,6 +142,7 @@ class Page1Screen extends StatelessWidget { "https://8f1d-2600-1006-b003-7627-ca1-491c-9b0-25ff.ngrok.io/notice_error")); request.headers.set(HttpHeaders.contentTypeHeader, "application/json; charset=UTF-8"); + request.headers.set("Car", "Honda"); request.headers.set("ngrok-skip-browser-warning", 69420); request.write( '{"title": "Foo","body": "Bar", "userId": 99}'); @@ -158,7 +160,8 @@ class Page1Screen extends StatelessWidget { final client = HttpClient(); var uri = Uri.parse("http://graph.facebook.com/"); var request = await client.getUrl(uri); - request.followRedirects = false; + request.headers.set("Car", "BMW"); + // request.followRedirects = false; var response = await request.close(); // var url = Uri.parse( @@ -173,6 +176,7 @@ class Page1Screen extends StatelessWidget { onPressed: () async { try { var dio = Dio(); + dio.options.headers['Car'] = 'Toyota'; dio.options.followRedirects = false; var response = await dio.get('http://graph.facebook.com/'); diff --git a/ios/Classes/SwiftNewrelicMobilePlugin.swift b/ios/Classes/SwiftNewrelicMobilePlugin.swift index fd7fe4d..f2764f1 100644 --- a/ios/Classes/SwiftNewrelicMobilePlugin.swift +++ b/ios/Classes/SwiftNewrelicMobilePlugin.swift @@ -127,6 +127,16 @@ public class SwiftNewrelicMobilePlugin: NSObject, FlutterPlugin { case "noticeDistributedTrace": result(NewRelic.generateDistributedTracingHeaders()) + + case "addHTTPHeadersTrackingFor": + + let headers = args!["headers"] as! [String] + NewRelic.addHTTPHeaderTracking(for: headers) + result("headers added") + + case "getHTTPHeadersTrackingFor": + + result([]) case "noticeHttpTransaction": diff --git a/ios/newrelic_mobile.podspec b/ios/newrelic_mobile.podspec index 8d03e6d..edb20dd 100644 --- a/ios/newrelic_mobile.podspec +++ b/ios/newrelic_mobile.podspec @@ -9,7 +9,7 @@ # Pod::Spec.new do |s| s.name = 'newrelic_mobile' - s.version = '1.0.5' + s.version = '1.0.6' s.summary = 'Flutter plugin for NewRelic Mobile.' s.description = <<-DESC Flutter plugin for NewRelic Mobile. @@ -22,7 +22,7 @@ Flutter plugin for NewRelic Mobile. s.dependency 'Flutter' s.platform = :ios, '9.0' - s.dependency 'NewRelicAgent', '7.4.7' + s.dependency 'NewRelicAgent', '7.4.8' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/lib/newrelic_http_client.dart b/lib/newrelic_http_client.dart index 52f5139..28fa814 100644 --- a/lib/newrelic_http_client.dart +++ b/lib/newrelic_http_client.dart @@ -181,6 +181,10 @@ Future _wrapRequest( Map traceAttributes = await NewrelicMobile.instance.noticeDistributedTrace({}); + Map params = Map(); + + + return request.then((actualRequest) { actualRequest.headers .add(DTTraceTags.traceState, traceAttributes[DTTraceTags.traceState]); @@ -192,21 +196,33 @@ Future _wrapRequest( return request as Future; } + + + return Future.value( - NewRelicHttpClientRequest(actualRequest, timestamp, traceAttributes)); + NewRelicHttpClientRequest(actualRequest, timestamp, traceAttributes,params)); }, onError: (dynamic err) { NewrelicMobile.instance.recordError(err, StackTrace.current); throw err; }); } -NewRelicHttpClientResponse _wrapResponse(HttpClientResponse response, - HttpClientRequest request, int timestamp, Map traceData) { +Future _wrapResponse(HttpClientResponse response, + HttpClientRequest request, int timestamp, Map traceData) async { if (response is NewRelicHttpClientResponse) { return response; } - return NewRelicHttpClientResponse(response, request, timestamp, traceData); + dynamic headersList = await NewrelicMobile.instance.getHTTPHeadersTrackingFor(); + Map params = Map(); + + for(String header in headersList) { + if(request.headers.value(header) != null) { + params.putIfAbsent(header, () => request.headers.value(header)!); + } + } + + return NewRelicHttpClientResponse(response, request, timestamp, traceData,params: params); } class NewRelicHttpClientRequest extends HttpClientRequest { @@ -214,13 +230,15 @@ class NewRelicHttpClientRequest extends HttpClientRequest { final HttpClientRequest _httpClientRequest; StringBuffer? _sendBuffer = StringBuffer(); Map traceData; + Map? params; + NewRelicHttpClientRequest( - this._httpClientRequest, this.timestamp, this.traceData) { + this._httpClientRequest, this.timestamp, this.traceData,[this.params]) { var request = this; request.done.then((value) { var response = - _wrapResponse(value, request, this.timestamp, this.traceData); + _wrapResponse(value, request, this.timestamp, this.traceData,); return response; }, onError: (dynamic err) { NewrelicMobile.instance.recordError(err, StackTrace.current); @@ -376,9 +394,10 @@ class NewRelicHttpClientResponse extends HttpClientResponse { StringBuffer? _receiveBuffer = StringBuffer(); String? responseData; dynamic traceData; + dynamic params; NewRelicHttpClientResponse( - this._httpClientResponse, this.request, this.timestamp, this.traceData) { + this._httpClientResponse, this.request, this.timestamp, this.traceData,{this.params}) { _wrapperStream = _readAndRecreateStream(_httpClientResponse); } @@ -416,6 +435,7 @@ class NewRelicHttpClientResponse extends HttpClientResponse { request.contentLength, _httpClientResponse.contentLength, traceData, + httpParams: params, responseBody: responseData ?? ''); } diff --git a/lib/newrelic_mobile.dart b/lib/newrelic_mobile.dart index d8bcf86..b78a694 100644 --- a/lib/newrelic_mobile.dart +++ b/lib/newrelic_mobile.dart @@ -38,7 +38,7 @@ class NewrelicMobile { await NewrelicMobile.instance.startAgent(config); runApp(); await NewrelicMobile.instance - .setAttribute("Flutter Agent Version", "1.0.5"); + .setAttribute("Flutter Agent Version", "1.0.6"); }, (Object error, StackTrace stackTrace) { NewrelicMobile.instance.recordError(error, stackTrace); FlutterError.presentError( @@ -216,6 +216,18 @@ class NewrelicMobile { return interactionId; } + void addHTTPHeadersTrackingFor(List headers) async { + final Map> params = >{ + 'headers': headers + }; + + await _channel.invokeMethod('addHTTPHeadersTrackingFor', params); + } + + Future getHTTPHeadersTrackingFor() async { + return await _channel.invokeMethod('getHTTPHeadersTrackingFor'); + } + Future> noticeDistributedTrace( Map requestAttributes) async { final dynamic traceAttributes = @@ -267,7 +279,8 @@ class NewrelicMobile { int bytesSent, int bytesReceived, Map? traceData, - {String responseBody = ""}) async { + {Map? httpParams, + String responseBody = ""}) async { Map? traceAttributes; if (traceData != null) { if (PlatformManager.instance.isAndroid()) { @@ -294,7 +307,8 @@ class NewrelicMobile { 'bytesSent': bytesSent != -1 ? bytesSent : 0, 'bytesReceived': bytesReceived != -1 ? bytesReceived : 0, 'responseBody': responseBody, - 'traceAttributes': traceAttributes + 'traceAttributes': traceAttributes, + 'params':httpParams }; return await _channel.invokeMethod('noticeHttpTransaction', params); } diff --git a/pubspec.yaml b/pubspec.yaml index 0d01f00..d9f86d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: newrelic_mobile description: Flutter plugin for NewRelic Mobile. This plugin allows you to instrument Flutter apps with help of native New Relic Android and iOS agents. -version: 1.0.5 +version: 1.0.6 homepage: https://github.com/newrelic/newrelic-flutter-agent diff --git a/test/newrelic_http_client_test.dart b/test/newrelic_http_client_test.dart index a2b64b7..b1b36ff 100644 --- a/test/newrelic_http_client_test.dart +++ b/test/newrelic_http_client_test.dart @@ -61,25 +61,6 @@ void main() { return true; } }); - - // const MethodChannel('newrelic_mobile') - // .setMockMethodCallHandler((MethodCall methodCall) async { - // log.add(methodCall); - // switch (methodCall.method) { - // case 'noticeDistributedTrace': - // Map map = { - // 'id': 'test1', - // 'newrelic': 'test3', - // 'guid': 'test3', - // 'trace.id': 'test3', - // 'tracestate': 'test3', - // 'traceparent': 'test3' - // }; - // return map; - // default: - // return true; - // } - // }); }); setUp(() { diff --git a/test/newrelic_mobile_test.dart b/test/newrelic_mobile_test.dart index 5bfda5c..398fbf3 100644 --- a/test/newrelic_mobile_test.dart +++ b/test/newrelic_mobile_test.dart @@ -59,6 +59,10 @@ void main() { "tracestate": "testtststst", "traceparent": "rereteutueyuyeuyeuye" }; + const httpParams = { + "Car":"Honda", + "Music":"Jazz" + }; const dartError = '#0 Page2Screen.bar. (package:newrelic_mobile_example/main.dart:185:17)\n' '#1 new Future. (dart:async/future.dart:252:37)\n#2 _rootRun (dart:async/zone.dart:1418:47)\n#3 _CustomZone.run (dart:async/zone.dart:1328:19)\n#4 _CustomZone.runGuarded (dart:async/zone.dart:1236:7)\n#5 _CustomZone.bindCallbackGuarded. (dart:async/zone.dart:1276:23)'; @@ -106,6 +110,8 @@ void main() { case 'noticeDistributedTrace': Map map = {'test': 'test1', 'test1': 'test3'}; return map; + case 'getHTTPHeadersTrackingFor': + return ['Car', 'Music']; default: return true; } @@ -287,6 +293,37 @@ void main() { expect(result.keys.length, 2); }); + test( + 'test getHTTPHeadersTrackingFor should be called and Return List with Headers ', + () async { + final List result = await NewrelicMobile.instance.getHTTPHeadersTrackingFor() ; + expect(methodCalLogs, [ + isMethodCall( + 'getHTTPHeadersTrackingFor', + arguments: null, + ) + ]); + expect(result.length, 2); + }); + + test( + 'test addHTTPHeadersTrackingFor should be called with parameters ', + () async { + + List list = ["Car","Music"]; + final Map params = { + 'headers': list, + }; + + NewrelicMobile.instance.addHTTPHeadersTrackingFor(list) ; + expect(methodCalLogs, [ + isMethodCall( + 'addHTTPHeadersTrackingFor', + arguments: params, + ) + ]); + }); + test('test endInteraction should be called with interActionId ', () async { NewrelicMobile.instance.endInteraction(interActionId); final Map params = { @@ -369,7 +406,7 @@ void main() { }; await NewrelicMobile.instance.noticeHttpTransaction(url, httpMethod, statusCode, startTime, endTime, bytesSent, bytesReceived, traceData, - responseBody: responseBody); + responseBody: responseBody,httpParams: httpParams); final Map params = { 'url': url, @@ -380,7 +417,8 @@ void main() { 'bytesSent': bytesSent, 'bytesReceived': bytesReceived, 'responseBody': responseBody, - 'traceAttributes': traceAttributes + 'traceAttributes': traceAttributes, + 'params': httpParams }; expect(methodCalLogs, [ isMethodCall( @@ -391,7 +429,7 @@ void main() { }); test( - 'test noticeHttpTransaction should be called on Android Platform when traceAttributes is null', + 'test noticeHttpTransaction should be called on Android Platform when traceAttributes is null and params is null', () async { var platformManger = MockPlatformManager(); PlatformManager.setPlatformInstance(platformManger); @@ -410,7 +448,8 @@ void main() { 'bytesSent': bytesSent, 'bytesReceived': bytesReceived, 'responseBody': responseBody, - 'traceAttributes': null + 'traceAttributes': null, + 'params': null }; expect(methodCalLogs, [ isMethodCall( @@ -433,7 +472,7 @@ void main() { }; await NewrelicMobile.instance.noticeHttpTransaction(url, httpMethod, statusCode, startTime, endTime, bytesSent, bytesReceived, traceData, - responseBody: responseBody); + responseBody: responseBody,httpParams: httpParams); final Map params = { 'url': url, @@ -444,7 +483,8 @@ void main() { 'bytesSent': bytesSent, 'bytesReceived': bytesReceived, 'responseBody': responseBody, - 'traceAttributes': traceAttributes + 'traceAttributes': traceAttributes, + 'params': httpParams }; expect(methodCalLogs, [ isMethodCall( @@ -455,7 +495,7 @@ void main() { }); test( - 'test noticeHttpTransaction should be called on iOS Platform when traceAttributes is null', + 'test noticeHttpTransaction should be called on iOS Platform when traceAttributes is null and httpParams is null', () async { var platformManger = MockPlatformManager(); PlatformManager.setPlatformInstance(platformManger); @@ -475,7 +515,8 @@ void main() { 'bytesSent': bytesSent, 'bytesReceived': bytesReceived, 'responseBody': responseBody, - 'traceAttributes': null + 'traceAttributes': null, + 'params': null }; expect(methodCalLogs, [ isMethodCall( @@ -946,7 +987,7 @@ void main() { final Map attributeParams = { 'name': 'Flutter Agent Version', - 'value': '1.0.5', + 'value': '1.0.6', }; expect(methodCalLogs, [ @@ -993,7 +1034,7 @@ void main() { final Map attributeParams = { 'name': 'Flutter Agent Version', - 'value': '1.0.5', + 'value': '1.0.6', }; expect(methodCalLogs, [