Once you have instrumented your Flutter application, you can make further manual customizations to the instrumentation.

Network Requests

To collect metrics on network requests, you can use the HTTP client wrapper:

DART

import 'package:http/http.dart' as http;

try {
  final client = TrackedHttpClient(http.Client());
  final response = await client.get(Uri.parse("https://www.appdynamics.com"));
  print(response);
} catch (e) {
  print(e);
}
JAVA

If you need more granular control over request tracking, such as adding user data to the request, you can manually create a tracker object that will register request information on completion:

try {
  final url = "www.appdynamics.com"
  final tracker = await RequestTracker.create(url);

  final response = await http.get(url);

  await tracker.setResponseStatusCode(response.statusCode)
    ..setRequestHeaders(response.request!.headers)
    ..setResponseHeaders(response.headers)
    ..setUserDataBool("shouldRespond", false);
} catch (e) {
  await tracker.setError(e.toString());
} finally {
  await tracker.reportDone();
}
JAVA

Add Server Correlation Headers

If another Cisco AppDynamics Agent is also used to instrument the backend of the application, you can add server correlation headers to pinpoint business transactions:

import 'package:http/http.dart' as http;

try {
  final headers = await RequestTracker.getServerCorrelationHeaders();
  final url = Uri.parse("https://www.appdynamics.com");
  final client = HttpClient(http.Client());
  final response = await client.get(url, headers: headers);
  print(response);
} catch (e) {
  print(e);
}
JAVA

Methods

RequestTracker.create(url) 

Returns a RequestTracker object to be configured for manually tracking an HTTP request by providing a non-null url (after calling Instrumentation.start()).

Parameters
NameTypeRequirementsDescription

url

StringNon-null.

The target URL of the request that will be tracked.

setError(message, stackTrace) 

Sets an error message describing the failure to receive a response (if such a failure occurred). If the request was successful, this method should not be called. Additional stackTrace strings can be added.

Parameters
NameTypeRequirementsDescription

message

StringRequest did not finish successfully.

The error message or a string describing the error.

stackTraceStringRequest did not finish successfully.The error stackTrace property (if any).

setResponseStatusCode(statusCode) 

Sets the status code of the response (if any). If a response was received, this method should be an integer. If an error occurred and a response was not received, this method should not be called.

Parameters
NameTypeRequirementsDescription

statusCode

IntegerRequest finished successfully.

The status code of the response.

setRequestHeaders(requestHeaders) 

Adds the request headers to the tracker object. If an error occurred and a response was not received, this method should not be called.

Parameters
NameTypeRequirementsDescription

requestHeaders

Map<String, String>Request finished successfully.

A map of key-value pairs representing the request's header values.

setResponseHeaders(responseHeaders) 

Adds the request response to the tracker object. If an error occurred and a response was not received, this should not be called.

Parameters
NameTypeRequirementsDescription

responseHeaders

Map<String, String>Request finished successfully.

A map of key-value pairs representing the response's header values.

reportDone() 

Signals that the HTTP request has finished and logged the appropriate information. If another request needs tracking, a new RequestTracker should be created.

getServerCorrelationHeaders() 

Returns the server correlation headers to be used if an Cisco AppDynamics Agent is also used on the server-side. Headers should be appended on the tracked request.

setUserdata

You an append custom values to the request tracking object with each corresponding API:

  • setUserData(key, value)
  • setUserDataBool(key, value)
  • setUserDataDateTime(key, value)
  • setUserDataDouble(key, value)
  • setUserDataInt(key, value)
Parameters
NameTypeRequirementsDescription

key

StringMax. 2048 characters. Unique (or will be overwritten).

Key name for the corresponding value.

valueString, bool, DateTime, double, intMax. 2048 characters.Custom info appended to the request.

Crashes

Report Native Crashes

The Flutter Agent automatically logs native crash information. This is already enabled when calling Instrumentation.start(). Native crashes can be symbolicated passing the corresponding .dsym or ProGuard files in the UI Controller. See Get Human-Readable Crash Snapshots.

Report All Crashes

For capturing all errors (Flutter and native), use zones:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart';

void main() {
  runZonedGuarded(() {
    WidgetsFlutterBinding.ensureInitialized();
    FlutterError.onError = Instrumentation.errorHandler;
    runApp(MyApp());
  }, (Object error, StackTrace stack) async {
    final details = 
	  FlutterErrorDetails(exception: error.toString(), stack: stack);
    await Instrumentation.errorHandler(details);
});
CODE

Report Dart Exceptions

Use the error handler provided by the Flutter Agent to intercept Dart exceptions and send them to the Controller:

import 'package:flutter/material.dart';
import 'package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  FlutterError.onError = Instrumentation.errorHandler;
  runApp(MyApp());
}
CODE

Disable Crash Reporting

Crash reporting is enabled by default. Most applications should leave crash reporting enabled, but you can disable it only if issues occur from using a different crash reporting tool.

You can enable or disable the native crash reporting feature in the AgentConfiguration object:

AgentConfiguration config = AgentConfiguration(
    appKey: <EUM_APP_KEY>,
    crashReportingEnabled: false);
await Instrumentation.start(config);
CODE

Crash Callback

The Flutter Agent supports using a callback function that will receive a crash report. The report contains an array of the Dart objects representing native crash information.

The callback function can be configured in the AgentConfiguration object:

void crashReportCallback(List<CrashReportSummary> summaries) async {
  print(summaries);
}

AgentConfiguration config = AgentConfiguration(
    appKey: <EUM_APP_KEY>,
    crashReportCallback: crashReportCallback);
await Instrumentation.start(config);
CODE

Session Frames

Session frames provide context for what the user is doing during a session. You can either create custom user interaction frames in the Controller UI or in the SessionFrame API, in the agent instrumentation.

Some use cases for customizing session frames include: 

  • One page performs multiple functions, and you want more granular tracking of the individual functions.
  • A user flow spans multiple pages or user interactions, and you want to categorize frames within a user flow. For example, if you have a user flow for purchasing, you could create session frames named "Login," "Product Selection," and "Purchase."
  • You want to capture dynamic information based on user interactions to name session frames, such as an order ID.

Methods

Instrumentation.startSessionFrame(sessionFrameName) 

Starts and returns a SessionFrame object.

Parameters
NameTypeRequirementsDescription
sessionFrameNameStringNon-nullA descriptive name for the session frame that will be captured.

updateName(newName) 

Updates the session frame with a new name. This is generally used when the appropriate session frame name is not known when it was created.

Parameters
NameTypeRequirementsDescription
newNameStringNon-nullA descriptive name for the session frame that will be captured.

end()

Reports the end of the session frame. The SessionFrame object will no longer be usable after this call.

This is an example of how to mark the end of a current shopping session when a user presses the checkout button:

class ShoppingCart {
  late SessionFrame _sessionFrame;
  late String _orderId;

void onCheckoutCartButtonClick() async {
  // The checkout starts when the user clicks the checkout button.
  // This may be after they have updated quantities of items in their
  // cart, etc.
  _sessionFrame = await Instrumentation.startSessionFrame("Checkout");
}

void onConfirmOrderButtonClick() {
  // Once they have confirmed payment info and shipping information,
  // and they are clicking the "Confirm" button to start the backend
  // process of checking out, we may know more information about the
  // order itself, such as an order ID.
  _sessionFrame.updateName("Checkout: Order ID {this.orderId}");
}

void onProcessOrderComplete() {
  // Once the order is processed, the user is done "checking out" so we end
  // the session frame.
  _sessionFrame.end();
}

void onCheckoutCancel() {
  // If they cancel or go back, you'll want to end the session frame also, or
  // else it will be left open and appear to have never ended.
  _sessionFrame.end();
}
CODE

Metrics

You can associate numeric values with a metric name and report them via Instrumentation.reportMetric.

Methods

Instrumentation.reportMetric(name, value)

Reports a custom metric.

Parameters
NameTypeRequirementsDescription

name

String

Only alphanumeric characters and spaces.Representative name for the metric.
valueIntegerNumeric, non-null.The metric value.

This is an example of how to track the number of times users click the checkout button:

import 'package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart';
import 'package:flutter/material.dart';

class App extends StatelessWidget {
  _finishCheckout() {
    Instrumentation.reportMetric(name: "Checkout Count", value: 1);
    // ...rest of the checkout logic
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Checkout screen")),
      body: Center(
        child:
            ElevatedButton(
              child: Text('Checkout'),
              onPressed: _finishCheckout,
            )
    ));
}
CODE

Automatic Screenshots (iOS Only)

By default, screenshot capturing is enabled, but if you need to disable it (such as for privacy reasons), you can disable it in the AgentConfiguration object.

This is an example to disable screenshots:

AgentConfiguration config = AgentConfiguration(
    appKey: <EUM_APP_KEY>,
	screenshotsEnabled: false);
await Instrumentation.start(config);
CODE

User Data

You can set key-value pair identifiers for custom user data that will be included in all snapshots. Some considerations:

  • The key must be unique across your application.
  • The key and the associated value are limited to 2048 characters. 
  • A null value will clear the data, and re-using the same key overwrites the previous value.
  • The custom user data is not persisted across application runs. Once the application is destroyed, user data is cleared.

Methods

setUserData(key, value)

Used to add the string types.

removeUserData(key)

 Removes the string corresponding to the key set with setUserData().

Parameters
NameTypeRequirementsDescription
keyStringMaximum 2048 characters (non-null).The name of the user data key.
valueStringMaximum 2048 characters.

The associated string value for the key.

setUserDataDouble(key, value)

Used to add the double types.

removeUserDataDouble(key)

 Removes the double corresponding to the key set with setUserDataDouble().

Parameters
NameTypeRequirementsDescription
keyStringMaximum 2048 characters (non-null).The name of the user data key.
valueDoubleMaximum 2048 characters.The associated double value for the key.

setUserDataInt(key, value)

Used to add the int types.

removeUserDataInt(key)

Removes the int corresponding to the key set with setUserDataInt().

Parameters
NameTypeRequirementsDescription
keyStringMax. 2048 characters, non-null.The name of the user data key.
valueIntegerMax. 2048 characters.The associated int value for the key.

setUserDataBool(key, value)

Used to add the bool types.

removeUserDataBool(key)

Removes the bool corresponding to the key set with setUserDataBool().

Parameters
NameTypeRequirementsDescription
keyStringMax. 2048 characters, non-null.The name of the user data key.
valueBoolean-The associated bool value for the key.

setUserDataDateTime(key, value)

Used to add the DateTime types.

removeUserDataDateTime(key)

Removes the DateTime corresponding to the key set with setUserDataDateTime().

Parameters
NameTypeRequirementsDescription
keyStringMax. 2048 characters, non-null.The name of the user data key.
valueDateTime-The associated DateTime value for the key.

This is an example of how to store a DateTime object as user data:

await Instrumentation.setUserDataDateTime("currentTransactionYear", DateTime.utc(2021));
CODE

Breadcrumbs

You can report points of interest throughout the application via breadcrumbs. Breadcrumbs will be included in different reports depending on the mode:

  • Use the crashesOnly mode (default) when you want breadcrumbs to appear only in crash reports (triggered on application restart).
  • Use the crashesAndSessions mode if you also want breadcrumbs to appear on the session.

Each crash report will display only the most recent 99 breadcrumbs.

Methods

leaveBreadcrumb(breadcrumb, mode)

Sets the name of the breadcrumb and where it appears on the mobile session.

Parameters
NameTypeRequirementsDescription
breadcrumbStringMax. 2048 characters (every extra character will be truncated).A descriptive string for the breadcrumb.
mode BreadcrumbVisibility (enum)-Configuring whether to see breadcrumb info on the session.

This is an example of how to leave breadcrumbs before and after switching to the sign-up screen:

Future<void> showSignUp() async {
  try {
    await Instrumentation.leaveBreadcrumb("Sign up button pressed.",
        BreadcrumbVisibility.crashesAndSessions);
    await pushSignUpScreen();


    await Instrumentation.leaveBreadcrumb("Sign Up screen pushed",
        BreadcrumbVisibility.crashesOnly);
  } catch (e) {
    ...
  }
}
CODE

Timers

You can time events by using customer timers that span across multiple methods.

Methods

startTimer(name)

Sets the timer.

Parameters
NameTypeRequirementsDescription
nameStringOnly alphanumeric characters and spaces (non-null).A unique identifier to correlate with the timer.

stopTimer(name)

Stops the timer.

Parameters
NameTypeRequirementsDescription
nameStringOnly alphanumeric characters and spaces (non-null).A unique identifier to correlate with the timer.

This is an example of how to start a timer to count time spent on checkout:

Future<void> doCheckout() async {
 final checkoutTimerName = "Time spent on checkout";
 try {
   await Instrumentation.startTimer(checkoutTimerName);
   await someCheckoutService();
   await someOtherLongTask();
 } finally {
   await Instrumentation.stopTimer(checkoutTimerName);
 }
}
CODE

Errors

You can report Dart errors, exceptions, and custom messages.

There are three error severity levels:

  • info: an error occurred, but it did not cause a problem.
  • warning: an error occurred, but the application recovered gracefully.
  • critical: an error occurred and caused problems to the app.

Methods

reportException(exception, severityLevel)

Sets an error exception and the associated severity level.

Parameters
NameTypeRequirementsDescription
exceptionException-

Dart Exception type.

severityLevelErrorSeverityLevel (enum)Default: warning

The corresponding level of severity.


reportError(error, severityLevel)

Creates an error report with an associated severity level.

Parameters
NameTypeRequirementsDescription
errorError-

Dart Error type.

severityLevelErrorSeverityLevel (enum)Default: warning

The corresponding level of severity.

reportMessage(message, severityLevel)

Creates a message report (should be used if neither reportError() nor reportException() are suitable.

Parameters
NameTypeRequirementsDescription
messageString-

Dart String type.

severityLevelErrorSeverityLevel (enum)Default: warning

The corresponding level of severity.

This is an example of how to report NoSuchMethodError when calling an undeclared method:

try {
  final myMethod = null;
  myMethod();
} on NoSuchMethodError catch (e) {
  await Instrumentation.reportError(e, severityLevel: ErrorSeverityLevel.CRITICAL);
}
CODE

Application Not Responding (ANR)

You can configure automatic detection and reporting of cases when the application is not responding (ANR) when creating the AgentConfiguration object. 

This is an example of how to enable the automatic ANR detection feature:

AgentConfiguration config = AgentConfiguration(
  appKey: <EUM_APP_KEY>
);
await Instrumentation.start(config);
CODE

Info Points

You can track specific points of interest in the app by using the Instrumentation class.

Method

trackCall(className, methodName, methodBody, methodArgs, uniqueCallId)

Sets a tracker for a call.

Parameters
NameTypeRequirementsDescription
classNameString-Name of the class or object of the method call.
methodName String-Name of the method.
methodBodyFutureOr<T> Function()-The body of the method that is being tracked.
methodArgsdynamic-The arguments of the method.
uniqueCallIdString?-A unique custom ID for tracking this call.

This is an example of tracking an asynchronous method that updates the users' first and last names on the server:

const firstName = "John";
const lastName = "Peters";

final result = await Instrumentation.trackCall(
    className: "MyAccountScreen",
    methodName: "updateUserName",
    methodArgs: [firstName, lastName],
    methodBody: () async {
      await http.post(
        Uri.parse('https://jsonplaceholder.typicode.com/albums'),
        body: jsonEncode(
            <String, String>{'firstName': firstName, 'lastName': lastName}),
      );
    }
);
CODE

Sessions

You can start a new session and mark the end of the current one.

Method

startNextSession()

Sets the beginning and end of a session.

This is an example of marking a new customer checkout session after the old one has ended:

Future<void> checkout(dynamic data) async {
  try {
    final response = http.post("https://server.com/checkout", data);
    await Instrumentation.startNextSession();
  } catch (e) {
    print(e);
  }
}
CODE

App Key

You can change the app key after initialization. Older reports will be discarded when a new key is applied. Invoking this method has no effect unless the agent was already initialized by calling one of the start methods.

Method

changeAppKey(newKey)

Parameters
NameTypeRequirementsDescription
newKeyStringMust respect app key format (must be valid).A new key that will replace the old one.

This is an example of updating the app key to AA-BBB-CCC.

try {
  await Instrumentation.changeAppKey("AA-BBB-CCC");
} catch (e) {
  print(e);
}
CODE

Screen Tracking

You can add the NavigationObserver to your app to automatically detect app widget transitions using named routes. You can manually track screens via the WidgetTracker class if your app isn't implementing named routes.

Methods

NavigationObserver

import 'package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        initialRoute: MyRoutes.mainScreen,
        onGenerateRoute: MyRouter.onGenerateRoute,
        navigatorObservers: [NavigationObserver()]);
  }
}
CODE

WidgetTracker

You can add tracking to Flutter widgets to monitor a specific UI element. For example, if your Flutter app isn't automatically implementing named routes, you could manually track screens via the WidgetTracker class.

Methods
trackWidgetStart()

Starts tracking a widget.

trackWidgetEnd()

Stops tracking a widget.

Parameters
NameTypeRequirementsDescription
widgetNameStringMust be unique.Name of the class or object of the method call.

This is an example of tracking the widget CheckoutPage.screenName.

import 'package:appdynamics_mobilesdk/appdynamics_mobilesdk.dart';
import 'package:flutter/material.dart';

class CheckoutPage extends StatefulWidget {
  const CheckoutPage({Key? key}) : super(key: key);

  static String screenName = "Checkout Page";

  @override
  _CheckoutPageState createState() => _CheckoutPageState();
}

class _CheckoutPageState extends State<CheckoutPage> {
  @override
  void initState() async {
    super.initState();
    await WidgetTracker.instance.trackWidgetStart(CheckoutPage.screenName);
  }

  _backToMainScreen() async {
    await WidgetTracker.instance.trackWidgetEnd(CheckoutPage.screenName);
    Navigator.pop(context);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
        child:
        Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
          ElevatedButton(
            child: const Text('Proceed'),
            onPressed: _backToMainScreen,
          )
        ]));
  }
}
CODE

Device Metrics

Various metrics regarding device status are automatically tracked. See Mobile Device Metric Collection.

Flutter Agent API Documentation

See the latest Flutter Agent API documentation or the previous versions listed below: