appdynamics_mobilesdk 0.9.2

  • Readme
  • Changelog
  • Example
  • Installing
  • new62

AppDynamics Flutter Plugin #

Flutter plugin to utilize the AppDynamics SDK. This plugin is a field integration and thus not an officially licensed AppDynamics product. This plugin wraps the AppDynanmics SDK and requires AppDynamics Mobile Licenses. Any issues with the plugin itself should be created on this repo.

Quick Start #

You can use the example app, which is part of this repository, to try out how your instrumentation might look like:

  • Clone this repository: git clone https://github.com/Appdynamics/flutter-plugin
  • Copy android/app/src/main/assets/config.sample.properties to android/app/src/main/assets/config.properties and set your application key for android.
  • For iOS do the same with ios/Runner/AppDynamics.sample.plist.
  • Run flutter run in the example folder to spin up the app on your emulator or devices.

Installation #

To instrument your flutter based mobile application with AppDynamics MRUM, add this to your package's pubspec.yaml file:

dependencies:
  ...
  appdynamics_mobilesdk: ^0.9.1

Follow the additional steps below to add the AppDynamics agent to iOS and android platform.

Android configuration #

Follow the guide to manually instrument an Android application, i.e. add the class path of the AppDynamics Gradle Plugin to the build path dependencies clause in the file app/build.gradle:

buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
	      classpath 'com.appdynamics:appdynamics-gradle-plugin:20.+'
    }
}
...

And also active the plugin add the module-level build.gradle (android/app/build.gradle):

...
apply plugin: 'com.android.application'
apply plugin: 'adeum'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
...

Next, verify that the required permissions in your AndroidManifest.xml are set:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>

Finally, modify the source of your MainActivity.java to start the instrumentation:

...
import android.os.Bundle;

import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;

import com.appdynamics.eumagent.runtime.Instrumentation;

public class MainActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Instrumentation.start("<YOUR_APP_KEY>", getApplicationContext());
    GeneratedPluginRegistrant.registerWith(this);
  }
}

Replace <YOUR_APP_KEY> with the app key of your mobile application.

If you use AppDynamics OnPrem or SaaS and your controller is based in EMEA or APAC, make sure to set the right collectorURL and screenshotURL

Your android application is now instrumented and you should see data appear in the AppDynamics controller.

If necessary follow the official documentation to customize the instrumentation.

Your android application is now instrumented and you should see data appear in the AppDynamics controller.

IOS Configuration #

Since this plugin will add the iOS SDK as dependency via it's podspec file, you can start with instrumenting your application. Follow the instructions on how to instrument an iOS application. For example, if your application is ObjectiveC based, add the following code to the ios/Runner/AppDelegate.m file of your flutter project:

#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"

#import <ADEUMInstrumentation/ADEUMInstrumentation.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ADEumAgentConfiguration *config = [[ADEumAgentConfiguration alloc] initWithAppKey:@"<YOUR_APP_KEY>"];
  // Uncomment the following, to configure the iOS Agent to report the metrics and screenshots to the right EUM server
  // config.collectorURL = @"https://fra-col.eum-appdynamics.com";
  // config.screenshotURL = @"https://fra-image.eum-appdynamics.com/";
  // Uncomment the following to increase the log level of the agent
  // config.loggingLevel = ADEumLoggingLevelAll;
  [ADEumInstrumentation initWithConfiguration: config];
  // Override point for customization after application launch.
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

Replace <YOUR_APP_KEY> with the app key of your mobile application.

If you use AppDynamics OnPrem or SaaS and your controller is based in EMEA or APAC, make sure to set the right collectorURL and screenshotURL.

If necessary follow the official documentation to customize the instrumentation.

Your iOS application is now instrumented and you should see data appear in the AppDynamics controller.

Usage #

Out of the box the AppDynamics agent will deliver some information like session count, screenshots (iOS only), etc. To enrich your instrumentation you can leverage the API that comes with this plugin. This API tries to be as close as possible to the iOS and android APIs:

Collect Additional Types of Data #

As with the iOS and android agent you can use additional methods to extend the insturmentation. To use those functionalities import the appdynamics_mobilesdk.dart:

import 'package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart';

Custom Timers #

Start and end a custom timer at any place of your code.

AppdynamicsMobilesdk.startTimer('Timer Name');
...
AppdynamicsMobilesdk.stopTimer('Timer Name');

User Data #

Report user data, that will be attached to sessions and network requests. Use different methods depending on the type of the data:

AppdynamicsMobilesdk.setUserData("username", username);
AppdynamicsMobilesdk.setUserDataLong("counter", counter);
AppdynamicsMobilesdk.setUserDataDouble("cartValue", cartValue.toDouble());
AppdynamicsMobilesdk.setUserDataDate("loginTime", DateTime.now());
AppdynamicsMobilesdk.setUserDataBoolean("isRegistered", true);

Report Errors and Exceptions #

You can capture and send errors & exceptions to AppDynamics with the following:

Future<Null> main() async {
  FlutterError.onError = (FlutterErrorDetails details) async {
    Zone.current.handleUncaughtError(details.exception, details.stack);
  };
  runZoned<Future<Null>>(() async {
    runApp(new MyApp());
  }, onError: (error, stackTrace) async {
    await AppdynamicsMobilesdk.reportError(error, stackTrace);
  });
}

Note, that for iOS there will be only limited information (no stacktrace!)

Start and End Session Frames #

You can use the SessionFrame API to create session frames that will appear in the session activity:

AppdynamicsSessionFrame frame = await AppdynamicsMobilesdk.startSessionFrame('Checkout');
...
frame.updateName('Checkout (Failed)');
...
frame.end();

If your application uses route aware navigation you can add the class AppdynamicsRouteObserver to your widgets' navigatorObservers. For example, if you use MaterialApp (or CupertinoApp or any other WidgetsApp):

@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: const MainPage(),
    title: 'My App',
    navigatorObservers: [AppdynamicsRouteObserver()]
  );
}

Track Network Requests #

To detect network requests, add a tracker to your requests and report them when the request is completed:

AppdynamicsHttpRequestTracker tracker = await AppdynamicsMobilesdk.startRequest(uri);
return get(uri).then((response) async {
  tracker.withResponseCode(response.statusCode);
  tracker.withResponseHeaderFields(response.headers);
  tracker.reportDone();
  return response;
});

If you use AppDynamics also in your backend application, you can add correlation headers:

AppdynamicsHttpRequestTracker tracker = await

Map<String, String> correlationHeaders = await AppdynamicsMobilesdk.getCorrelationHeaders();

AppdynamicsMobilesdk.startRequest(uri);
return get(uri, headers: correlationHeaders).then((response) async {
  tracker.withResponseCode(response.statusCode);
  tracker.withResponseHeaderFields(response.headers);
  tracker.reportDone();
  return response;
});

Note, that this example uses the dart http package, but of course you can add this code to any other custom HTTP library.

For the dart http package mentioned above you can leverage the wrapper class AppdynamicsHttpClient to add the tracker to each request:

final client = AppdynamicsHttpClient(http.Client());

Take Screenshots #

If enabled as controller setting, you can take manual screenshots:

AppdynamicsMobilesdk.takeScreenshot();

Please note, that this only works for iOS and android will give you a black screen.

0.9.2 #

  • Fixed health suggestions in lib/appdynamics_mobilesdk.dart

  • Update README.md with new installation instructions

  • Add some documentation.

0.9.1 #

  • Update dependency for intl to ^0.16.1

0.9.0 #

  • Initial Release

  • Includes Support for instrumenting android/ios apps using the AppDynamics Mobile SDK

  • Add coverage for most features (network requests, custom data, session frames, etc.)

example/lib/main.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart';

Future<Null> _reportError(dynamic error, dynamic stackTrace) async {
  print('Caught error: $error');
  print(stackTrace);
  print('Reporting to Appdynamics...');
  AppdynamicsMobilesdk.reportError(error, stackTrace);
}

Future<Null> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  FlutterError.onError = (FlutterErrorDetails details) async {
    Zone.current.handleUncaughtError(details.exception, details.stack);
  };
  runZoned<Future<Null>>(() async {
    runApp(new MyApp());
  }, onError: (error, stackTrace) async {
    await _reportError(error, stackTrace);
  });
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  AppdynamicsSessionFrame frame;
  int _counter = 0;
  int frameCounter = 0;

  List<dynamic> frames = [
    {
      "name": "Login",
      "image": "",
      "urls": ["http://www.appdynamics.com/"]
    },
    {
      "name": "Logut",
      "image": "",
      "urls": ["http://www.appdynamics.com/"]
    }
  ];

  String _frameName = 'Unknown';
  String _image = '';

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    var settings =
        json.decode(await rootBundle.loadString('assets/settings.json'));

    if (settings.containsKey("frames")) {
      frames = settings["frames"];
    }

    frame = await AppdynamicsMobilesdk.startSessionFrame("App Start");

    setState(() {
      print(_counter);
      _counter++;
      _frameName = "App Start";
    });

    _clock();
  }

  _clock() async {
    while (true) {
      await _next();
      await new Future.delayed(const Duration(seconds: 10));
      print("Next after 10 seconds");
    }
  }

  _next() async {
    await frame.end();

    var current = frames[frameCounter % frames.length];

    var frameName = current["name"];
    var image = current["image"];

    setState(() {
      _frameName = frameName;
      _image = image;
    });

    var rng = new Random();

    var breadCrumb =
        current.containsKey("breadcrumb") ? current["breadcrumb"] : false;
    var startTimer =
        current.containsKey("startTimer") ? current["startTimer"] : false;
    var stopTimer =
        current.containsKey("stopTimer") ? current["stopTimer"] : false;
    var urls = current.containsKey("urls") ? current["urls"] : false;

    if (frameCounter >= frames.length) {
      print('Starting a new session after ' + frameName);
      await AppdynamicsMobilesdk.startNextSession();
      frameCounter = 0;
      _counter++;
    }

    print('===== FRAME: ' + frameName + '======');
    frame = await AppdynamicsMobilesdk.startSessionFrame(frameName);

    /*if(startTimer != false) {
      print('Start Timer');
      await AppdynamicsMobilesdk.startTimer(current["startTimer"]);
    }

    if(stopTimer != false) {
      print('Stop Timer');
      await AppdynamicsMobilesdk.stopTimer(current["stopTimer"]);
      await new Future.delayed(const Duration(seconds: 10));
    }*/

    if (breadCrumb != false) {
      await AppdynamicsMobilesdk.leaveBreadcrumb(breadCrumb, true);
      if (rng.nextInt(100) > 50) {
        _crashMe();
      }
    }

    if (urls != false) {
      for (var i = 0; i < urls.length; i++) {
        await _makeGetRequest(urls[i]);
      }
    }

    await new Future.delayed(const Duration(seconds: 2));

    await AppdynamicsMobilesdk.takeScreenshot();

    if (rng.nextInt(100) > 50) {
      await AppdynamicsMobilesdk.setUserData("language", "de_DE");
      await AppdynamicsMobilesdk.setUserData(
          "userId", "833ED2BF-FAA4-4660-A58F-4BA1C9C953D5");
      await AppdynamicsMobilesdk.setUserDataBoolean("hasSimplifiedEnabled", true);
    } else {
      await AppdynamicsMobilesdk.setUserData("language", "fi_FI");
      await AppdynamicsMobilesdk.setUserData(
          "userId", "CCBF8FE3-20C3-48F6-822B-4FC69916B1A1");
      await AppdynamicsMobilesdk.setUserDataBoolean("hasSimplifiedEnabled", false);
    }

    //AppdynamicsMobilesdk.setUserDataLong("counter_long", _counter);
    //AppdynamicsMobilesdk.setUserDataDouble("cartValue", _counter.toDouble());
    //AppdynamicsMobilesdk.setUserDataDate("myDate", DateTime.now());
    //AppdynamicsMobilesdk.setUserDataBoolean("isRegistered", true);

    await AppdynamicsMobilesdk.reportMetric('frameCounter', frameCounter);

    frameCounter++;
  }

  _crashMe() async {
    var f;
    f();
    var x = () {
      var y = () => f();
      y();
    };
    x();
  }

  _removeButtonPressed() async {
    _crashMe();
    setState(() {
      _counter--;
    });
  }

  _addButtonPressed() async {
    await _next();
  }

  Future<Response> _makeGetRequest(uri, [responseCode = -1]) async {
    print('GET $uri');
    // AppDynamics specific request
    AppdynamicsHttpRequestTracker tracker = AppdynamicsMobilesdk.startRequest(uri);
    Map<String, String> correlationHeaders = await AppdynamicsMobilesdk.getCorrelationHeaders();

    print("CH BEGIN");
    print(correlationHeaders);
    print("CH END");
    print(uri + " start");

    // Request goes out
    return get(uri, headers: correlationHeaders).then((response) async {
      if (responseCode <= 0) {
        responseCode = response.statusCode;
      }

      print(response.headers);

      /*response.headers.forEach((k, v) {
        response.headers[k] = v.replaceAll('%3A', ':');
      });

      print(response.headers);
      */
      tracker
          .withResponseCode(responseCode)
          .withResponseHeaderFields(response.headers);

      if (responseCode > 500) {
        tracker.withError('An error!!!');
      }

      await tracker.reportDone();
      print(uri + " end");
      return response;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Image.network(
            '$_image',
            fit: BoxFit.cover,
            height: double.infinity,
            width: double.infinity,
            alignment: Alignment.center,
          ),
        ),
        floatingActionButton: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              FloatingActionButton(
                onPressed: _addButtonPressed,
                tooltip: 'Refresh',
                child: Icon(Icons.autorenew),
              ),
              FloatingActionButton(
                onPressed: _removeButtonPressed,
                tooltip: 'Cancel',
                child: Icon(Icons.cancel),
                backgroundColor: Colors.red,
              )
            ]),
      ),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  appdynamics_mobilesdk: ^0.9.2

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
24
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
62
Learn more about scoring.

We analyzed this package on Jul 9, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.14
  • Flutter: 1.17.5

Analysis suggestions

Package does not support Flutter platform linux

Because:

  • package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart that declares support for platforms: android, ios

Package does not support Flutter platform macos

Because:

  • package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart that declares support for platforms: android, ios

Package does not support Flutter platform web

Because:

  • package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart that declares support for platforms: android, ios

Package does not support Flutter platform windows

Because:

  • package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart that declares support for platforms: android, ios

Package not compatible with SDK dart

Because:

  • appdynamics_mobilesdk that is a package requiring null.

Health suggestions

Format lib/appdynamics_mobilesdk.dart.

Run flutter format to format lib/appdynamics_mobilesdk.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.2.0 <3.0.0
flutter 0.0.0
http ^0.12.0 0.12.1
intl ^0.16.1 0.16.1
Transitive dependencies
charcode 1.1.3
collection 1.14.12 1.14.13
http_parser 3.1.4
meta 1.1.8 1.2.1
path 1.7.0
pedantic 1.9.0 1.9.1
sky_engine 0.0.99
source_span 1.7.0
string_scanner 1.0.5
term_glyph 1.1.0
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test