Flutter (22.02.0)

Follow

This document will guide you through the process of Countly SDK installation and it applies to version 22.02.0

To access the documentation for version 21.11 and older, click here.

This document includes the necessary information for integrating Countly Flutter SDK in your application. Flutter SDK requires Android and iOS SDKs, hence all the features and limitations regarding those platforms also apply to Countly Flutter SDK.

Countly is an open source SDK, you can take a look at our SDK code in the Github repo

Supported Platforms: Countly SDK supports iOS and Android.

Below you can see steps to download a Flutter example application:

git clone https://github.com/Countly/countly-sdk-flutter-bridge.git
cd countly-sdk-flutter-bridge/example
flutter pub get
flutter run

This example application has all the methods mentioned in this documentation. It is a great way of understanding how different methods work, like events, custom user profiles and views.

Adding the SDK to the project

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

dependencies:
  countly_flutter:
    git: 
      url: https://github.com/Countly/countly-sdk-flutter-bridge.git
      ref: master

You can install packages from the command line with Flutter:

flutter pub get

SDK Integration

Minimal setup

The shortest way to initiate the SDK if you want Countly SDK to take care of device ID seamlessly, use the code below. You can find your app key on your Countly dashboard, under the "Applications" menu item.

Countly.isInitialized().then((bool isInitialized){
    if(!isInitialized){
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
Countly.initWithConfig(config).then((value){ Countly.start(); }); // Initialize the countly SDK. }else{ print("Countly: Already initialized."); } });

For more information on how to acquire your application key (appKey) and server URL, check here

To configure the SDK during init, a config object called "CountlyConfig" is used. Configuration is done by creating such an object and then calling it's provided function calls to enable functionality you need.
Click here for more detail about "CountlyConfig" functionalities.

If you are in doubt about the correctness of your Countly SDK integration you can learn about methods to verify it from here.

Enable logging

If logging is enabled then our SDK will print out debug messages about its internal state and encountered problems.

We advise doing this while implementing Countly features in your application.

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setLoggingEnabled(true);

Device ID

When the SDK is initialized for the first time and no device ID is provided, a device ID will be generated by SDK.

For iOS: the device ID generated by SDK is the Identifier For Vendor (IDFV)
For Android: the device ID generated by SDK is the OpenUDID

You may provide your own custom device ID when initializing the SDK

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setDeviceId(DEVICE_ID);

SDK data storage

For iOS: SDK data is stored in Application Support Directory in file named "Countly.dat"
For Android: SDK data is stored in SharedPreferences. A SharedPreferences object points to a file containing key-value pairs and provides simple methods to read and write them.

Crash reporting

This feature allows the Countly SDK to record crash reports of either encountered issues or exceptions which cause your application to crash. Those reports will be sent to your Countly server for further inspection.

If a crash report can not be delivered to the server (e.g. no internet connection, unavailable server), then SDK stores the crash report locally in order to try again later.

Automatic crash handling

If you want to enable automatic unhandled crash reporting, you need to call this before init:

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.enableCrashReporting()

By doing that it will automatically catch all errors that are thrown from within the Flutter framework.


If you want to catch Dart errors, run your app inside a Zone and supply Countly.recordDartError to the onError parameter:

void main() {
runZonedGuarded<Future<void>>(() async {
runApp(MyApp());
}, Countly.recordDartError);
}

Automatic crash report segmentation

You may add a key/value segment to crash reports. For example, you could set which specific library or framework version you used in your app. You may then figure out if there is any correlation between the specific library or another segment and the crash reports.

The following call will add the provided segmentation to all recorded crashes. Use the following function for this purpose:

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setCustomCrashSegment(Map<String, Object> segments);

Handled exceptions

There are multiple ways you could report a handled exception/error to Countly.

This call does not add a stacktrace automatically. If it is required, it should be provided to the function. A potential use case would be to exception.toString()

Countly.logException(String exception, bool nonfatal, [Map<String, Object> segmentation])

The issue is recorded with a provided Exception object. If no stacktrace is set,StackTrace.current will be used.

Countly.logExceptionEx(Exception exception, bool nonfatal, {StackTrace stacktrace, Map<String, Object> segmentation})

The exception/error is recorded through a string message. If no stack trace is provided, StackTrace.current will be used.

Countly.logExceptionManual(String message, bool nonfatal, {StackTrace stacktrace, Map<String, Object> segmentation})

Below are some examples that how to log handled/nonfatal and unhandled/fatal exceptions manually.

1. Manually report exception

bool nonfatal = true; // Set it false in case of fatal exception
// With Exception object Countly.logExceptionEx(EXCEPTION_OBJECT, nonfatal);
// With String message Countly.logExceptionManual("MESSAGE_STRING", nonfatal);

2. Manually report exception with stack trace

bool nonfatal = true; // Set it false in case of fatal exception
// With Exception object Countly.logExceptionEx(EXCEPTION_OBJECT, nonfatal, stacktrace: STACK_TRACE_OBJECT);
// With String message Countly.logExceptionManual("MESSAGE_STRING", nonfatal, stacktrace: STACK_TRACE_OBJECT);

3. Manually report exception with segmentation

bool nonfatal = true; // Set it false in case of fatal exception
// With Exception object Countly.logExceptionEx(EXCEPTION_OBJECT, nonfatal, segmentation: {"_facebook_version": "0.0.1"});
// With String message Countly.logExceptionManual("MESSAGE_STRING", nonfatal, segmentation: {"_facebook_version": "0.0.1"});

4. Manually report exception with stack trace and segmentation

bool nonfatal = true; // Set it false in case of fatal exception
// With Exception object Countly.logExceptionEx(EXCEPTION_OBJECT, nonfatal, STACK_TRACE_OBJECT, {"_facebook_version": "0.0.1"});
// With String message Countly.logExceptionManual("MESSAGE_STRING", nonfatal, STACK_TRACE_OBJECT, {"_facebook_version": "0.0.1"});

Crash breadcrumbs

Throughout your app, you can leave crash breadcrumbs which would describe previous steps that were taken in your app before the crash. After a crash happens, they will be sent together with the crash report.

The following function call adds a crash breadcrumb:

Countly.addCrashLog(String logs)

Events

Event is any type of action that you can send to a Countly instance, e.g purchase, settings changed, view enabled and so. This way it's possible to get much more information from your application compared to what is sent from Flutter SDK to Countly instance by default.

Here are the detail about properties which we can use with event:

  • key identifies the event
  • count is the number of times this event occurred
  • sum is an overall numerical data set tied to an event. For example, total amount of in-app purchase event.
  • duration is used to record and track the duration of events.
  • segmentation is a key-value pairs, we can use segmentation to track additional information. The only valid data types are: "String", "Integer", "Double" and "Boolean". All other types will be ignored
Data passed should be in UTF-8

All data passed to the Countly server via SDK or API should be in UTF-8.

Recording events

We will be recording a purchase event. Here is a quick summary of what information each usage will provide us:

  • Usage 1: how many times a purchase event occurred.
  • Usage 2: how many times a purchase event occurred + the total amount of those purchases.
  • Usage 3: how many times a purchase event occurred + which countries and application versions those purchases were made from.
  • Usage 4: how many times a purchase event occurred + the total amount both of which are also available segmented into countries and application versions.
  • Usage 5: how many times purchase event occurred + the total amount both of which are also available segmented into countries and application versions + the total duration of those events (under Timed Events topic below)

1. Event key and count

// example for sending basic event
var event = {
  "key": "Basic Event",
  "count": 1
};
Countly.recordEvent(event);

2. Event key, count and sum

// example for event with sum
var event = {
  "key": "Event With Sum",
  "count": 1,
  "sum": "0.99",
};
Countly.recordEvent(event);

3. Event key and count with segmentation(s)

// example for event with segment
var event = {
  "key": "Event With Segment",
  "count": 1
};
event["segmentation"] = {
  "Country": "Germany",
  "Age": "28"
};
Countly.recordEvent(event);

4. Event key, count and sum with segmentation(s)


// example for event with segment and sum
var event = {
  "key": "Event With Sum And Segment",
  "count": 1,
  "sum": "0.99"
};
event["segmentation"] = {
  "Country": "Germany",
  "Age": "28"
};
Countly.recordEvent(event);

5. Event key, count, sum and duration with segmentation(s)


// example for event with segment and sum
var event = {
  "key": "Event With Sum And Segment",
  "count": 1,
  "sum": "0.99",
  "duration": "0"
};
event["segmentation"] = {
  "Country": "Germany",
  "Age": "28"
};
Countly.recordEvent(event);

Timed events

It's possible to create timed events by defining a start and a stop moment

1.Timed event with key

// Basic event
Countly.startEvent("Timed Event");
Timer timer = Timer(new Duration(seconds: 5), () {
    Countly.endEvent({ "key": "Timed Event" });
});

2.Timed event with key and sum

// Event with sum
Countly.startEvent("Timed Event With Sum");
Timer timer = Timer(new Duration(seconds: 5), () {
    Countly.endEvent({ "key": "Timed Event With Sum", "sum": "0.99" });
});

3.Timed event with key, count and segmentation

// Event with segment
Countly.startEvent("Timed Event With Segment");
Timer timer = Timer(new Duration(seconds: 5), () {
    var event = {
        "key": "Timed Event With Segment",
        "count": 1,
    };
    event["segmentation"] = {
        "Country": "Germany",
        "Age": "28"
    };
    Countly.endEvent(event);
});

4.Timed event with key, count, sum and segmentation

// Event with Segment, sum and count
Countly.startEvent("Timed Event With Segment, Sum and Count");
Timer timer = Timer(new Duration(seconds: 5), () {
    var event = {
        "key": "Timed Event With Segment, Sum and Count",
        "count": 1,
        "sum": "0.99"
    };
    event["segmentation"] = {
        "Country": "Germany",
        "Age": "28"
    };
    Countly.endEvent(event);
});

Sessions

Automatic session tracking

To start recording an automatic session tracking you would call:

Countly.start();

Countly.start(); will handle the start session, update session and end session automatically.
This is how it works:

  • Start/Begin session Request: It is sent on Countly.start(); call and when the app comes back to the foreground from the background, and it includes basic metrics.
  • Update Session Request: It automatically sends a periodical (60 sec by default) update session request while the app is in the foreground.
  • End Session Request: It is sent at the end of a session when the app goes to the background or terminates.

If you want to end automatic session tracking you would call:

Countly.stop();

Manual sessions

Sometimes it might be preferable to control the session manually instead of relying on the SDK.

It can be enabled during init with:

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.enableManualSessionHandling();

Afterwards it is up to the implementer to make calls to:

  • Begin session
  • Update session duration. By default, you would call this every 60 seconds after beginning a session so that it is not closed server side. If you would want to increase that duration, you would have to increase the "Maximal Session Duration" in your server API configuration.
  • End session (also updates duration)

The approprate call to do that are:

Countly.beginSession();
Countly.updateSession();
Countly.endSession();

View tracking

Manual view recording

You can manually add your own views in your application, and each view will be visible under Views menu item. Below you can see two examples of sending a view using Countly.recordview function.

// record a view on your application
Countly.recordView("HomePage");
Countly.recordView("Dashboard");

While manually tracking views, you may want to add custom segmentation to them.

Map<String, Object> segments = {
"Cats": 123,
"Moons": 9.98,
"Moose": "Deer"
};
Countly.recordView("HomePage", segments);

Device ID management

A device ID is a unique identifier for your users. You may specify the device ID yourself or allow the SDK to generate it. When providing one yourself, keep in mind that it has to be unique for all users. Some potential sources for such an id could be the username, email or some other internal ID used by your other systems.

Device ID generation

When the SDK is initialized for the first time with no device ID, then SDK will generate a device ID.

Here are the underlying mechanisms used to generate that value for some platforms:

For iOS: the device ID generated by SDK is the Identifier For Vendor (IDFV)
For Android: the device ID generated by SDK is the OpenUDID

Changing the Device ID

You may configure/change the device ID anytime using:

Countly.changeDeviceId(DEVICE_ID, ON_SERVER);

You may either allow the device to be counted as a new device or merge existing data on the server. If theonServer bool is set to true, the old device ID on the server will be replaced with the new one, and data associated with the old device ID will be merged automatically.
Otherwise, if onServer bool is set to false, the device will be counted as a new device on the server.

Temporary Device ID

You may use a temporary device ID mode for keeping all requests on hold until the real device ID is set later.

You can enable temporary device ID when initializing the SDK:

Countly.init(SERVER_URL, APP_KEY, Countly.deviceIDType["TemporaryDeviceID"])

To enable a temporary device ID after init, you would call:

Countly.changeDeviceId(Countly.deviceIDType["TemporaryDeviceID"], ON_SERVER);

Note: When passing TemporaryDeviceID for deviceID parameter, argument for onServerparameter does not matter.

As long as the device ID value is TemporaryDeviceID, the SDK will be in temporary device ID mode and all requests will be on hold, but they will be persistently stored.

When in temporary device ID mode, method calls for presenting feedback widgets and updating remote config will be ignored.

Later, when the real device ID is set using Countly.changeDeviceId(DEVICE_ID, ON_SERVER); method, all requests which have been kept on hold until that point will start with the real device ID

Retrieving current device ID

You may want to see what device id Countly is assigning for the specific device. For that, you may use the following call:

String currentDeviceId = Countly.getCurrentDeviceId();

You can use getDeviceIDType method which returns a DeviceIDType to get the current device ID type. The id type is an enum with the possible values of:

  • "DEVELOPER_SUPPLIED" - device ID was supplied by the host app
  • "SDK_GENERATED" - device ID was generated by the SDK
  • "TEMPORARY_ID" - the SDK is in temporary device ID mode
DeviceIdType? deviceIdType = await Countly.getDeviceIDType();

Push notifications

Countly Flutter SDK comes with push notification capabilities embedded. For the flavor without the push notifications features (like Firebase libraries) please check here.

Integration

Android setup

Step 1: For FCM credentials setup please follow the instruction from this URL https://support.count.ly/hc/en-us/articles/360037754031-Android#getting-fcm-credentials.

Step 2: Make sure you have google-services.json from https://firebase.google.com/

Step 3: Make sure the app package name and the google-services.json package_name matches.

Step 4: Place the google-services.json file inside android/app

Step 5: Add the following line in file android/app/src/main/AndroidManifest.xml inside application tag.

<service android:name="ly.count.dart.countly_flutter.CountlyMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

Step 7: Use the latest version from this link https://firebase.google.com/support/release-notes/android#latest_sdk_versions and this link https://developers.google.com/android/guides/google-services-plugin

Step 6: Add the following line in file android/build.gradle

buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.3.2'
    }
}

Step 7: Add the following line in file android/app/build.gradle

dependencies {
    implementation 'ly.count.android:sdk:20.04'
    implementation 'com.google.firebase:firebase-messaging:20.0.0'
}
// Add this at the bottom of the file
apply plugin: 'com.google.gms.google-services'

iOS setup

By default push notification is enabled for iOS, to disable you need to call disablePushNotifications method:

// // Disable push notifications feature for iOS, by default it is enabled.
Countly.disablePushNotifications();

For iOS push notification please follow the instruction from this URL https://resources.count.ly/docs/countly-sdk-for-ios-and-os-x#section-push-notifications

For Flutter you can find CountlyNotificationService.h/m file under Pods/Development Pods/Countly/{PROJECT_NAME}/ios/.symlinks/plugins/countly_flutter/ios/Classes/CountlyiOS/CountlyNotificationService.h/m

Pro Tips to find the files from deep hierarchy:

  • You can filter the files in the navigator using a shortcut ⌥⌘J (Option-Command-J), in the filter box type "CountlyNotificationService" and it will show the related files only.
  • You can find the file using the shortcut ⇧⌘O (Shift-Command-O) and then navigate to that file using the shortcut ⇧⌘J (Shift-Command-J)

You can drag and drop the file from Pod to Compile Sources.

Flutter_iOS_Notifications.png

Enabling push

First, when setting up push for the Flutter SDK, you would first select the push token mode. This would allow you to choose either test or production modes, push token mode should be set before init.

// Set messaging mode for push notifications
Countly.pushTokenType(Countly.messagingMode["TEST"]);

When you are finally ready to initialise Countly push, you would call this:

// This method will ask for permission, enables push notification and send push token to countly server.
Countly.askForNotificationPermission();

Handling push callbacks

To register a Push Notification callback after initializing the SDK, use the method below.

Countly.onNotification((String notification) {
print(notification);
});

In order to listen to notification receive and click events, Place below code in AppDelegate.swift 

Add header files

import countly_flutter

Add these methods:

// Required for the notification event. You must call the completion handler after handling the remote notification.
func application(application: UIApplication,  didReceiveRemoteNotification userInfo: [NSObject : AnyObject],  fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    CountlyFlutterPlugin.onNotification(userInfo);
    completionHandler(.newData);

}

@available(iOS 10.0, \*)
override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (_ options: UNNotificationPresentationOptions) -> Void) {

    //Called when a notification is delivered to a foreground app.

    let userInfo: NSDictionary = notification.request.content.userInfo as NSDictionary
    CountlyFlutterPlugin.onNotification(userInfo as? [AnyHashable : Any])

}

@available(iOS 10.0, \*)
override func userNotificationCenter(\_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    // Called to let your app know which action was selected by the user for a given notification.
    let userInfo: NSDictionary = response.notification.request.content.userInfo as NSDictionary
    // print("\(userInfo)")
    CountlyFlutterPlugin.onNotification(userInfo as? [AnyHashable : Any])

}

Data Structure Received in Push Callbacks

Here is the example of how data will receive in push callbacks:Screenshot_2022-06-24_at_7.04.23_PM.png

Data Received for Android platform:

{
"c.e.cc": "TR",
"c.e.dt": "mobile",
"Key": "value",
"c.i": "62b59b979f05a1f5e5592036",
"c.l": "https:\/\/www.google.com\/",
"c.m": "https:\/\/count.ly\/images\/logos\/countly-logo-mark.png?v2",
"c.li": "notify_icon",
"badge": "1",
"sound": "custom",
"title": "title",
"message": "Message"
}

Data Received for iOS platform:

{
Key = value;
aps = {
alert = {
body = Message;
subtitle = subtitle;
title = title;
};
badge = 1;
"mutable-content" = 1;
sound = custom;
};
c = {
a = "https://count.ly/images/logos/countly-logo-mark.png";
e = {
cc = TR;
dt = mobile;
};
i = 62b5b945cabedb0870e9f217;
l = "https://www.google.com/";
};
}

User Location

Countly allows you to send geolocation-based push notifications to your users. By default, the Countly Server uses the GeoIP database to deduce a user's location.

Set User Location

If your app has a different way of detecting location, you may send this information to the Countly Server by using the setLocation of  CountlyConfig during init orsetUserLocation method after init.

When setting user location information, you would be setting these values:

  • countryCode a string in ISO 3166-1 alpha-2 format country code
  • city a string specifying city name
  • location a string comma-separated latitude and longitude
  • IP a string specifying an IP address in IPv4 or IPv6 formats

All values are optional, but at least one should be set.

// Example for setLocation
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setLocation(country_code: 'TR', city: 'Istanbul', gpsCoordinates: '41.0082,28.9784', ipAddress: '10.2.33.12')

Geolocation recording methods may also be called at any time after the Countly SDK has started.
To do so, use the setUserLocation method as shown below.

// Example for setUserLocation
Countly.setUserLocation(countryCode: 'TR', city: 'Istanbul', gpsCoordinates: '41.0082,28.9784', ipAddress: '10.2.33.12');

Disable Location

To erase any cached location data from the device and stop further location tracking, use the following method. Note that if after disabling location, the setUserLocation is called with any non-null value, tracking will resume.

//disable location tracking
Countly.disableLocation();

Remote config

Remote config allows you to modify how your app functions or looks by requesting key-value pairs from your Countly server. The returned values can be modified based on the user profile. For more details please see Remote Config documentation.

Automatic remote config

There are two ways of acquiring remote config data, by automatic download or manual request. By default, automatic remote config is disabled and therefore without developer intervention no remote config values will be requested.

Automatic value download happens when the SDK is initiated or when the device ID is changed. To enable it, you have to call setRemoteConfigAutomaticDownload before init. As an optional value you can provide a callback to be informed when the request is finished.

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setRemoteConfigAutomaticDownload(true, (error) {
print(error);
});

If the callback returns a non-null value, then you can expect that the request failed and no values were updated.

When doing an automatic update, all locally stored values are replaced with the ones received (all locally stored ones are deleted and in their place are put new ones). It is possible that a previously valid key returns no value after an update.

Manual remote config

There are three ways for manually requesting remote config update:

  • Manually updating everything
  • Manually updating specific keys
  • Manually updating everything except specific keys

Each of these requests also has a callback. If that returns a non-null value, the request encountered some error and failed.

Functionally the manual update for everything remoteConfigUpdate is the same as the automatic update - replaces all stored values with the ones from the server (all locally stored ones are deleted and in their place are put new ones). The advantage is that you can make the request whenever it is desirable for you. It has a callback to let you know when it has finished.

Countly.remoteConfigUpdate((result){
  print(result);
});

You might want to update only specific key values. For that you need to call updateRemoteConfigForKeysOnly with a list of keys you want to be updated. That list is an array with string values of those keys. It has a callback to let you know when the request has finished.

Countly.updateRemoteConfigForKeysOnly(["name"],(result){
  print(result);
});

You might want to update all values except a few defined keys, for that call updateRemoteConfigExceptKeys. The key list is an array with string values of the keys. It has a callback to let you know when the request has finished.

Countly.updateRemoteConfigExceptKeys(["url"],(result){
  print(result);
});

When making requests with an "inclusion" or "exclusion" array, if those arrays are empty or null, they will function the same as a simple manual request and will update all values. This means that it will also erase all keys not returned by the server.

Getting remote config values

To request a stored value, call getRemoteConfigValueForKey with the specified key. If it returns null then no value was found. The SDK has no knowledge of the returned value type and therefore returns an Object. The developer needs to cast it to the appropriate type. The returned values can also be a JSONArray, JSONObject or just a simple value like int.

Countly.getRemoteConfigValueForKey("name", (result){
  print(result);
});

Clearing stored remote config values

At some point you might want to erase all values downloaded from the server. To achieve that you need to call one function.

Countly.remoteConfigClearValues((result){
  print(result);
});

User feedback

There are a couple ways of receiving feedback from your users: star-rating dialog, the rating widget and the feedback widgets (survey, nps).

Star-rating dialog allows users to give feedback as a rating from 1 to 5. The rating widget allows users to rate using the same 1 to 5 rating system as well as leave a text comment. Feedback widgets (survey, nps) allow for even more textual feedback from users.

Star rating dialog

Star rating integration provides a dialog for getting user's feedback about the application. It contains a title, simple message explaining what it is for, a 1-to-5 star meter for getting users rating and a dismiss button in case the user does not want to give a rating.

This star-rating has nothing to do with Google Play Store ratings and reviews. It is just for getting brief feedback from users, to be displayed on the Countly dashboard. If the user dismisses star rating dialog without giving a rating, the event will not be recorded.

Countly.askForStarRating();

The star-rating dialog's title, message, and dismiss button text may be customized either through the following functions:

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setStarRatingTextTitle("Custom title"); // Only available for Android
config.setStarRatingTextMessage("Custom message");
config.setStarRatingTextDismiss("Custom message"); // Only available for Android

Rating widget

Feedback widget shows a server configured widget to your user devices.

001.png

It's possible to configure any of the shown text fields and replace them with a custom string of your choice.

In addition to a 1 to 5 rating, it is possible for users to leave a text comment and also leave an email in case the user would want some contact from the app developer.

Trying to show the rating widget is a single call, but underneath is a two-step process. Before it is shown, the SDK tries to contact the server to get more information about the dialog. Therefore a network connection to it is needed.

You can try to show the widget after you have initialized the SDK. To do that, you first have to get the widget ID from your server:

002.png

Using that you can call the function to show the widget popup:

Countly.presentRatingWidgetWithID(RATING_WIDGET_ID, closeButtonText: "close", ratingWidgetCallback: (error) {
if(error != null) {
print(error);
}
});

closeButtonText and ratingWidgetCallback are optional.

Feedback widget

It is possible to display 2 kinds of Surveys widgets: NPS and Surveys. Both widgets are shown as webviews and they both use the same code methods.

Before any Surveys widget can be shown, you need to create them in your Countly Dashboard.

When the widgets are created, you need to use 2 calls in your SDK: one to get all available widgets for a user and another to display a chosen widget.

To get your available widget list, use the call below.

FeedbackWidgetsResponse feedbackWidgetsResponse = await Countly.getAvailableFeedbackWidgets() ;

From the callback you would get FeedbackWidgetsResponse objec which contains the list of all available widgets that apply to the current device id.

The objects in the returned list look like this:

class CountlyPresentableFeedback {
public String widgetId;
public String type;
public String name;
}

To determine what kind of widget that is, check the "type" value. The potential values are "survey" and "nps".

Then use the widget type and description (which is the same as provided in the Dashboard) to decide which widget to show.

After you have decided which widget you want to display, call the function below.

You would then use the widget type and description (which is the same as provided in the dashboard) to decide which widget to show.

After you have decided which widget you want to display, you would provide that object to the following function:

await Countly.presentFeedbackWidget(widgets.first, 'Close', widgetShown: () {
print('Widget Appeared');
}, widgetClosed: () {
print('Widget Dismissed');
});

widgetShown and widgetClosed are optional callbacks, you can pass these callbacks if you want to perform some actions when widget appear or dismiss.

Feedback widget manual reporting

There might be some usecases where you might to use the native UI or a custom UI you have created instead of our webview solution. In those cases you would have to request all the widget related information and then report the result manually.

For a sample integration, have a look at our sample app in the repo.

First you would need to retrieve the available widget list with the previously mentioned getAvailableFeedbackWidgets call. After that you would have a list of possible CountlyPresentableFeedback objects. You would pick the one widget you would want to display.

Having the CountlyPresentableFeedback object of the widget you would want to display, you could use the 'getFeedbackWidgetData'  mehtod to retrieve the widget information with an optional 'onFinished' callback.
In case you want to use with callback then you can call 'getFeedbackWidgetData' in this way:

Countly.getFeedbackWidgetData(chosenWidget, onFinished: (retrievedWidgetData, error) {
if (error == null) {
}
});

If you want to use it without a callback then you can call 'getFeedbackWidgetData' in this way:

List result = await Countly.getFeedbackWidgetData(chosenWidget) {
error = result[1];
if(error == null) {
Map<String, dynamic> retrievedWidgetData = result[0];
}
}

retrievedWidgetData would contain a Map with all of the required information to present the widget yourself.

After you have collected the required information from your users, you would package the responses into a Map<String, Object> and then use it, the widgetInformation and the widgetData to report the feedback result with the following call:

//this contains the reported results
Map<String, Object> reportedResult = {};

//
// You would fill out the results here. That step is not displayed in this sample
//

//report the results to the SDK
Countly.reportFeedbackWidgetManually(chosenWidget, retrievedWidgetData , reportedResult);

If the user would have closed the widget, you would report that by passaing a "null" reportedResult.

User Profiles

Using the following calls, you can set key/value to the visitors user profile. After you send user data, it can be viewed under the User Profiles menu.

Note that this feature is available only for Enterprise Edition.

Setting User profile values during init

We recommend to set the user properties during initialization. This way they would be reflected when the session is started shortly.

Using the following call, you can set both the predefined and the custom user properties during initialization:

var userProperties = {
‘customProperty’: ‘custom Value’,
‘username’: ‘USER_NAME’,
‘email’: ‘USER_EMAIL’
};
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY); config.setUserProperties(userProperties);

Setting Predefined Values

If you want to set the user data after init, you can use Countly.setUserData(USER_DATA);

Using the call you can only set the following predefined keys.

// example for setting user data
Map<String, Object> options = {
    "name": "Nicola Tesla",
    "username": "nicola",
    "email": "info@nicola.tesla",
    "organization": "Trust Electric Ltd",
    "phone": "+90 822 140 2546",
    "picture": "http://images2.fanpop.com/images/photos/3300000/Nikola-Tesla-nikola-tesla-3365940-600-738.jpg",
    "picturePath": "",
    "gender": "M", // "F"
    "byear": "1919",
};
Countly.setUserData(options);

 

Modifying custom data

Additionally, you can do different manipulations on your custom data values, like increment current value on the server or store an array of values under the same property.

Below is the list of available methods:

//set one custom properties
Countly.setProperty("setProperty", "My Property");
//increment used value by 1
Countly.increment("increment");
//increment used value by provided value
Countly.incrementBy("incrementBy", 10);
//multiply value by provided value
Countly.multiply("multiply", 20);
//save maximal value
Countly.saveMax("saveMax", 100);
//save minimal value
Countly.saveMin("saveMin", 50);
//set value if it does not exist
Countly.setOnce("setOnce", 200);

//insert value to array of unique values
Countly.pushUniqueValue("type", "morning");;
//insert value to array which can have duplocates
Countly.pushValue("type", "morning");
//remove value from array
Countly.pullValue("type", "morning");

Application Performance Monitoring

This SDK provides a few mechanisms for APM. To start using them you would first need to enable this feature and give the required consent if it was required.

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config
.setRecordAppStartTime(true); // Enable APM features, which includes the recording of app start time.

While using APM calls, you have the ability to provide trace keys by which you can track those parameters in your dashboard.

App Start Time

For the app start time to be recorded, you need to call the appLoadingFinished method. Make sure this method is called after init.

//Example of appLoadingFinished
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY); Countly.initWithConfig(config ).then((value){
Countly.appLoadingFinished();
});

This calculates and records the app launch time for performance monitoring. It should be called when the app is loaded and it successfully displayed its first user-facing view. The time passed since the app has started to launch will be automatically calculated and recorded for performance monitoring. Note that the app launch time can be recorded only once per app launch. So, the second and following calls to this method will be ignored.

Custom trace

Currently, you can use custom traces to record the duration of application processes. At the end of them, you can also provide any additionally gathered data.

The trace key uniquely identifies the thing you are tracking and the same name will be shown in the dashboard. The SDK does not support tracking multiple events with the same key.

To start a custom trace, use:

Countly.startTrace(traceKey);

To end a custom trace, use:

String traceKey = "Trace Key";
Map<String, int> customMetric = {
"ABC": 1233,
"C44C": 1337
};
Countly.endTrace(traceKey, customMetric);

In this sample, a Map of integer values is provided when ending a trace. Those will be added to that trace in the dashboard.

Network trace

You can use the APM to track your requests. You would record the required info for your selected approach of making network requests and then call this after your network request is done:

Countly.recordNetworkTrace(networkTraceKey, responseCode, requestPayloadSize, responsePayloadSize, startTime, endTime);

networkTraceKey is a unique identifier of the API endpoint you are targeting or just the url you are targeting, all params should be stripped. You would also provide the received response code, sent payload size in bytes, received payload size in bytes, request start time timestamp in milliseconds, and request end finish timestamp in milliseconds.

User consent

For compatibility with data protection regulations, such as GDPR, the Countly iOS SDK allows developers to enable/disable any feature at any time depending on user consent. More information about GDPR can be found here.

By default the requirement for consent is disabled. To enable it, you have to call setRequiresConsent with true, before initializing Countly.

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setRequiresConsent(true);

By default, no consent is given. That means that if no consent is enabled, Countly will not work and no network requests, related to features, will be sent. When the consent status of a feature is changed, that change will be sent to the Countly server.

The Countly SDK does not persistently store the status of given consents except push notifications. You are expected to handle receiving consent from end-users using proper UIs depending on your app's context. You are also expected to store them either locally or remotely. Following this step, you will need to call thegiveConsent/giveConsentInit method on each app launch depending on the permissions you managed to get from the end-users.

The ideal location for giving consent is after Countly.init and before Countly.start(). Consent for features can be given and revoked at any time, but if it is given afterCountly.start() , some features might work partially.

If consent is removed, but the appropriate function can't be called before the app closes, it should be done at next app start so that any relevant server-side features could be disabled (like reverse geo ip for location)

Currently, available features with consent control are as follows:

  • sessions - tracking when, how often and how long users use your app
  • events - allow sending events to the server
  • views - allow tracking which views user visits
  • location - allow sending location information
  • crashes - allow tracking crashes, exceptions and errors
  • attribution - allow tracking from which campaign did user come
  • users - allow collecting/providing user information, including custom properties
  • push - allow push notifications
  • star-rating - allow sending their rating and feedback
  • apm - allow application performance monitoring
  • remote-config - allows downloading remote config values from your server

Giving consents

To give consent for features, you can use the setConsentEnabledof CountlyConfig class or giveConsent after init by passing the feature names as an Array.
We recommend using thesetConsentEnabledof CountlyConfig because some features require consents before init

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setConsentEnabled(["location", "sessions", "attribution", "push", "events", "views", "crashes", "users", "push", "star-rating", "apm", "feedback", "remote-config"]);
Countly.giveConsent(["events", "views", "star-rating", "crashes"]);

Removing consents

If the end-user changes his/her mind about consents at a later time, you will need to reflect this in the Countly SDK using the removeConsentmethod:

Countly.removeConsent(["events", "views", "star-rating", "crashes"]);

Giving all consents

If you would like to give consent for all the features, you can use the giveAllConsent method:

Countly.giveAllConsent();

Removing all consents

If you would like to remove consent for all the features, you can use the removeAllConsent method:

Countly.removeAllConsent();

Security and privacy

Parameter tampering protection

You can set optional salt to be used for calculating checksum of request data, which will be sent with each request using &checksum field. You need to set exactly the same salt on the Countly server. If salt on Countly server is set, all requests would be checked for the validity of &checksum field before being processed.

// sending data with salt
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setParameterTamperingProtectionSalt("salt");

Make sure not to use salt on the Countly server and not on the SDK side, otherwise, Countly won't accept any incoming requests.

Using Proguard

Proguard obfuscates the OpenUDID & Countly Messaging classes. If you use OpenUDID or Countly Messaging in your application, find app/proguard-rules.pro file which sits inside /android/app/ folder and adds the following lines:

-keep class org.openudid.** { *; }
-keep class ly.count.android.sdk.** { *; }

If Proguard is not already configured then first, enable shrinking and obfuscation in the build file. Find build.gradle file which sits inside /android/app/ folder and adds lines in bold

android {
    buildTypes {
        release {
            // Enables code shrinking, obfuscation, and optimization for only
            // your project's release build type.
            minifyEnabled true

            // Enables resource shrinking, which is performed by the
            // Android Gradle plugin.
            shrinkResources true

            // Includes the default ProGuard rules files that are packaged with
            // the Android Gradle plugin. To learn more, go to the section about
            // R8 configuration files.
            proguardFiles getDefaultProguardFile(
                    'proguard-android-optimize.txt'),
                    'proguard-rules.pro'
        }
    }
...
}

Next create a configuration that will preserve the entire Flutter wrapper code. Create /android/app/proguard-rules.pro file and insert inside:

#Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }

More info related to code shrinking can be found here for flutter and android.

Other Features and Notes

SDK Config Parameters Explained

To configure the SDK during init, a config object called "CountlyConfig" is used. Configuration is done by creating such an object and then calling it's provided function calls to enable functionality you need.
Here is the list of functionalities "CountlyConfig" provided:

  • Device Id - A device ID is a unique identifier for your users. You may specify the device ID yourself or allow the SDK to generate it.
  • Enable Logging - To enable countly internal debugging logs
  • Enable Crash Reporting - To enable uncaught crash reporting
  • Salt - Set the optional salt to be used for calculating the checksum of requested data which will be sent with each request
  • Event queue threshold - Set the threshold for event grouping. Event count that is bellow the threshold will be sent on update ticks.
  • Update Session Timer - Sets the interval for the automatic session update calls
  • Custom Crash Segment -Set custom crash segmentation which will be added to all recorded crashes
  • User consent - Set if consent should be required and give consents.
  • Forcing HTTP POST - When set to true, all requests made to the Countly server will be done using HTTP POST. Otherwise, the SDK sends all requests using the HTTP GET method. In some cases, if the data to be sent exceeds the 1800-character limit, the SDK uses the POST method. The default value is false.
  • Star Rating Text - Set shown title, message and dismiss buttim text for the star rating dialogs.
  • Application Performance Monitoring - Enable APM features, which includes the recording of app start time.
  • Set User Location - Set user location manually instead of using Countly server to use GeoIP database to deduce a user's location.
  • Max Queue Size Limit - Set maximum size for the request queue.
  • Manual Sessions - To enable manual session handling
  • Automatic Remote Config - If enable, will automatically download newest remote config values.
  • Direct Attribution - Report direct user attribution
  • Indirect Attribution - Report indirect user attribution

Attribution

Countly Attribution Analytics allows you to measure your marketing campaign performance by attributing installs from specific campaigns. This feature is available for the Enterprise Edition.

There are 2 forms of attribution: direct Attribution and indirect Attribution.

Direct Attribution

Currently, direct attribution is only available for Android.

You can pass "Campaign type" and "Campaign data". The "type" determines for what purpose the attribution data is provided. Depending on the type, the expected data will differ, but usually that will be a string representation of a JSON object.

You can use recordDirectAttribution to set attribution values during initialization.

String campaignData = 'JSON_STRING';
CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.recordDirectAttribution('CAMPAIN_TYPE', campaignData);

You can also use recordDirectAttribution function to manually report attribution later:

String campaignData = 'JSON_STRING';
Countly.recordDirectAttribution('CAMPAIN_TYPE', campaignData);

Currently this feature is limited and accepts data only in a specific format and for a single type. That type is "countly". it would be used to record install attribution. The data also needs to be formatted in a specific way. Either with the campaign id or with the campaign id and campaign user id.

String campaignData = '{cid:"[PROVIDED_CAMPAIGN_ID]", cuid:"[PROVIDED_CAMPAIGN_USER_ID]"}';
Countly.recordDirectAttribution('countly', campaignData);

Indirect Attribution

This feature would be used to report things like advertising ID's. For each platform those would be different values. For the most popular keys we have a class with predefined values to use, it is called "AttributionKey".

You can use recordDirectAttribution to set attribution values during initialization.

Map<String, String> attributionValues = {};
if(Platform.isIOS){
attributionValues[AttributionKey.IDFA] = 'IDFA';
}
else {
attributionValues[AttributionKey.AdvertisingID] = 'AdvertisingID';
}

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.recordIndirectAttribution(attributionValues);

You can also use recordIndirectAttribution function to manually report attribution later

Map<String, String> attributionValues = {};
if(Platform.isIOS){
attributionValues[AttributionKey.IDFA] = 'IDFA';
}
else {
attributionValues[AttributionKey.AdvertisingID] = 'AdvertisingID';
}

Countly.recordIndirectAttribution(attributionValues);

In case you would be accessing IDFA for ios, for iOS 14+ due to Apple changes regarding Application Tracking, you need to ask the user for permission to track the Application.

Forcing HTTP POST

If the data sent to the server is short enough, the SDK will use HTTP GET requests. In case you want an override so that HTTP POST is used in all cases, call the setHttpPostForced function after you called init. You can use the same function later in the app's life cycle to disable the override. This function has to be called every time the app starts.

 CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setHttpPostForced(true); // default is false

Interacting with the internal request queue

When recording events or activities, the requests don't always get sent immediately. Events get grouped together. All the requests contain the same app key which is provided in the init function.

There are two ways to interact with the app key in the request queue at the moment.

1. You can replace all requests with a different app key with the current app key:

//Replaces all requests with a different app key with the current app key.
Countly.replaceAllAppKeysInQueueWithCurrentAppKey();

In the request queue, if there are any requests whose app key is different than the current app key, these requests app key will be replaced with the current app key. 2. You can remove all requests with a different app key in the request queue:

//Removes all requests with a different app key in request queue.
Countly.removeDifferentAppKeysFromQueue();

In the request queue, if there are any requests whose app key is different than the current app key, these requests will be removed from the request queue.

Setting an event queue threshold

Events get grouped together and are sent either every minute or after the unsent event count reaches a threshold. By default it is 10. If you would like to change this, call:

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setEventQueueSizeToSend(6);

Setting max queue size limit

You can specify the max request queue size by calling setMaxRequestQueueSize of the CountlyConfig object before starting Countly. It is used to limit the number of requests stored when there is no connection to your Countly Server. If the max queue size limit is not explicitly set, the default setting will be at 1000.

CountlyConfig config = CountlyConfig(SERVER_URL, APP_KEY);
config.setMaxRequestQueueSize(2500);

Checking if the SDK has been initialized

In case you would like to check if init has been called, you may use the following function:

Countly.isInitialized();

Comments

0 comments

Please sign in to leave a comment.