Kotlin Documentation

Prerequisites for Integration

Android projects with minimum SDK version 14 (Ice-cream Sandwich) or above.
Version of Android Gradle plugin should be 3.2.1 or above.

  1. Register with Finotes using the 'Get Started' button in https://finotes.com and login to dashboard.
  2. Use "Add App" to link Android to Finotes.
  3. Integrate Finotes SDK to your application.
  4. Test your integration.

Current Version 3.1.0

Change log


How to Integrate


Migration for versions 2.6.0 and below: for existing integrations

Step One

In-order to integrate Finotes in Android project, add the code below to project level build.gradle

build.gradle:
allprojects {
    repositories {
        jcenter()
        maven {
            url "https://finotescore-android.s3.amazonaws.com/release"
        }
    }
}

Step Two


Integrating FinotesCore alone

If you plan to integrate only FinotesCore, copy the following code block in app level build.gradle within the dependencies section.

build.gradle:
implementation('com.finotes:finotescore:3.1.0@aar') {
    transitive = true;
}
Integrating both FinotesCore and FinotesDebug

To leverage the capabilities of both FinotesCore and FinotesDebug, copy the following code block in app level build.gradle within the dependencies section.

build.gradle:
releaseImplementation('com.finotes:finotescore:3.1.0@aar') {
    transitive = true;
}

debugImplementation('com.finotes:finotesdebug:3.1.0@aar') {
    transitive = true;
}

FinotesDebug SDK contains all features of Finotescore and:

1. Automatically track framerate issues.
2. Automatically detect Object level memory leaks.

Proguard

If you are using proguard in release build, you need to add the following to the proguard-rules.pro file.

proguard-rules.pro:
-keep class com.finotes.android.finotescore.* { *; }

-keepclassmembers class * {
    @com.finotes.android.finotescore.annotation.Observe *;
}

Add below line to keep the exact line number and source file name in the stacktrace.

proguard-rules.pro:
-keepattributes SourceFile,LineNumberTable

Please make sure that the mapping file of production build (each build) is backed up, inorder to deobfuscate the stacktrace from Finotes dashboard.
Location of mapping file: project-folder/app/build/outputs/proguard/release/mapping.txt

Initializing Finotes

You need to call Fn.init() function in the Application class onCreate() function.

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
    }
}

Testing Integration

Now that the basic integration of Finotes SDK is complete,
Let us make sure that the dashboard and SDK are in sync.

Step One

Add Fn.test() under Fn.init

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
        Fn.test()
    }
}

Step Two


The application class (here BlogApp), should be registered in your manifest file.

Now run the application in a simulator or real android device (with active network connection).

Step Three

Once the application opens up, open Finotes dash.
The test issue that we raised should be reported.

In-case the issue is not listed, make sure the right app and platform is selected at the top of the dashboard.
If the issue is still not synced in Finotes dashboard, Click Here.

Remember to remove the Fn.test() call, else every time the app is run, an issue will be reported.

Now, Finotes will report all memory leaks, abnormal memory usages and crashes if they occur in your application.

Things to take care in Release Build

Make sure that dryRun() and verbose() API are not called in release build and proguard rules are added as specified above.

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this) 
    }
}

Please make sure that the mapping file of each production build is manually backed up, inorder to deobfuscate the stacktrace from Finotes dashboard.
Location of mapping file: project-folder/app/build/outputs/proguard/release/mapping.txt

Detect memory leaks

FinotesCore sdk has the capability to detect all activity and service level leaks.
FinotesDebug sdk has the capability to detect object level memory leaks.

Service Class:
class ContactsParserService : IntentService() {

    override fun onDestroy() {
        super.onDestroy()
        Fn.watchRefLeaks(this)
    }
}

Finotes automatically detects activity and fragment level leaks.
To track Service class level leaks, you need to call Fn.watchRefLeaks() API in onDestroy function of Service Class.

Track Network Calls

If the project is using Retrofit or OkHttp (directly) or Volley to make API calls, Finotes will be able to notify of any errors in API calls with just a single line.

Retrofit- no existing okhttpclient

If the existing retrofit code does not use custom client:
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.build()

Change to,

Add Finotes client using .client():
val client = OkHttp3Client(OkHttpClient.Builder()).build()
val retrofit = Retrofit.Builder()
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Retrofit- already using okhttpclient

If the existing retrofit code already uses a custom client:
val retrofit = Retrofit.Builder()
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()

Change to,

Pass the current builder to OkHttp3Client and .build():
val currentBuilder = new OkHttpClient.Builder()
... //Builder customization codes
...
val client = new OkHttp3Client(currentBuilder).build()

OkHttp

Find the code block where the OkHttpClient is initialized
In some projects there could be more than one code block where client is initialized.
Change the default client to custom OkHttp3Client provided by Finotes.

If OkHttp is directly used to make API calls
var client = OkHttpClient.Builder().build()

Change to,

var client = OkHttp3Client(OkHttpClient.Builder()).build()

Volley 1.1.0

As volley does not directly support OkHttpClient we need to use HurlStack.

First, you need to add HurlStack class from HurlStack Gist to your project.
Second, find the below function where newRequestQueue is initiated either in a singleton (used as ApiManger for volley) class or Application class.

ApiManager Singleton or Application class:
return Volley.newRequestQueue(applicationContext)

Third, pass the custom HurlStack as a second parameter, replacing the above code.

val requestQueue: RequestQueue? = null
get() {
    if (field == null) {
        return Volley.newRequestQueue(applicationContext, HurlStack())
    }
    return field
}


Forth, add implementation 'com.squareup.okhttp3:okhttp:3.8.0' to your build.gradle.


PS: HurlStack used above is from HurlStack Gist. You will also need to add okhttp to your build.gradle file.


To integrate volley 1.0.0 please refer to Integrating Volley 1.0.0

Incase, you need more clarity on integrating Volley with HurlStack, Please initiate a chat with our development team directly using chat widget at the bottom right corner.
We will help you overcome any roadblocks that you may have.

Dynamic Path Component

When API call issues are reported, different urls are created as separate ticket.
This can cause large number of tickets generated for the same API incase the url contains an id or any other dynamic path component.

Application Class:
@Observe(urlPatterns = {"https://your-host.com/repos/{owner}/{repo}/contributors",
                                    "https://your-host.com/todos/{id}"})
        
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
    }
}

Use urlPatterns with @Observe annotation to specify the urls that contains dynamic path component.
Wrap the corresponding dynamic path component or id inside '{}'.

Privacy (Optional feature)

As Finotes reports API call issues, each issue is tagged with corresponding request response headers, request body and associated parameters.
If header fields contain any sensitive data, Finotes provides a global and easy mechanism to mask such header fields using maskHeaders in @Observe annotation as shown in code snippet.
You may provide 1 or more header keys in the 'maskHeaders' field.

Application Class:
@Observe(maskHeaders = {"X-Key", "Accept"})
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
    }
}

Use case:

Let us say API calls from the app has header field 'X-Key' which contains the authentication token.
It can be masked by providing the 'X-Key' in the field as shown above, masked header fields are filtered out from the app itself as is not sent to the Finotes dashboard, if any issues are raised.
The maskHeaders field is case insensitive and is optional.

Report low memory warnings

You may extend the Application class from ObservableApplication. This will report any app level memory issues that may arise in the application.

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
    }
}
Change to
class BlogApp: ObservableApplication() {
    override fun onCreate() {
        super.onCreate()
    }
}

Report Try..Catch Exceptions

Any exceptions that may have already been caught using try{}catch{} needs to be reported as they might prevent the application from crashing, but at the same time can make app unstable if gone unreported.

Exceptions caused during parsing a JSON object/array:
try {
    val jsonObject = JSONObject(receivedString)
    val identifier = jsonObject.getString("userID")
} catch (JSONException exception) {

    Fn.setActivityMarker(this@ParserClass, receivedString)

    Fn.reportException(this@ParserClass, exception, Severity.MAJOR)

}

Use case:

If the exception raised is due to a malformed JSON string 'receivedString' is not reported, there is a great chance the same issue can occur to multiple users.
This could be a blocker in our application.

Setting the Activity Marker with receivedString will help find the root cause of the issue..
'receivedString' value at the time of exception will be shown in the Finotes dashboard along with the issue report.

Report Custom Issues

You can report custom issues using the Fn.reportIssue() API.

Any where in the project:
override fun paymentCompleted(userIdentifier:String, type:String){
    //Handle post payment
}

override fun paymentFailed(userIdentifier:String, reason:String){

    Fn.setActivityMarker(this@PaymentActivity, "User identifer "+userIdentifier)

    Fn.reportIssue(this@PaymentActivity, "Payment Failed" ,reason)

}

Use case:

In cases where the fail situations can be anticipated, they may be reported using Fn.reportIssue().
You may pass an issue title and detailed description as parameters to report the issue.
As in custom exceptions, you will be able to make use of activity markers to aid reproducing the issue when reported.

Track Function Calls

Finotes will report all return value issues, exceptions and execution delays that may arise during function execution using Fn.call() and @Observe annotation.

Regular function call:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_user)
    val userName = getUserNameFromDb("123-sd-12")
}

fun getUserNameFromDb(userId: String): String?{
    return User.findById(userId).getName()
}

Function call via Finotes:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_user)
    val userName = Fn.call("getUserNameFromDb", this@UserActivity, "123-sd-12") as String?
}

@Observe
fun getUserNameFromDb(userId: String): String?{
    return User.findById(userId).getName()
}

Notes:
1. You replace the old method of function invocation to the Finotes based mechanism for main functions in the android project.
2. Make sure that the function is annotated using @Observe.
3. Finotes will report an issue, if the said function returns a NULL value or takes more than 1000 milliseconds for its execution or throws any exceptions even if caught using try{}catch{} block over the function call.
4. The function execution is seamless like the original method of invocation.
5. Near zero overhead in using Finotes based function invocation.
6. The function parameters that were passed to it during its invocation will be shown along with the issue in Finotes dashboard if any raised.


@Observe fields


You can make use of expectedExecutionTime, severity, expectNull fields in @Observe to control the issue reports from a function.

id

Incase, we have 2 or more functions with same name in a single class (function overloading), set a unique id to the function that is to be invoked using finotes.

If a custom id is specified in @Observe annotation, then while calling that function, you need mandatorily to pass the id and not the function name.
If no custom id is specified in @Observe annotation, then you can pass the function name itself as the id.

Function Overriding:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_user)
    // Here the function name "getUserNameFromDb" is passed in Fn.call().
    // This will trigger the getUserNameFromDb(String userId) function.
    val userName = Fn.call("getUserNameFromDb", this@MainActivity, "123-sd-12") as String?

    // Here the id "getUserNameFromDBWithEmailAndToken" specified in
    // @Observe annotation is passed in Fn.call().
    // This will trigger the getUserNameFromDb(String email, String token) function.
    val userNameFromEmail = Fn.call("getUserNameFromDbWithEmailAndToken", this@MainActivity
                                        , "support@finotes.com", "RENKDS123S") as String?
}

@Observe(expectedExecutionTime = 1400)
fun getUserNameFromDb(userId: String): String?{
    return User.findById(userId).getName()
}

@Observe(id = "getUserNameFromDbWithEmailAndToken", expectedExecutionTime = 1400)
fun getUserNameFromDb(email: String, token: String): String?{
    return User.findUniqueByProperties(email,token).getName()
}

expectedExecutionTime

Incase, we have a function that may take more than 1000 milliseconds(default value) for execution then use this field in @Observe to provide an ideal execution time.
Supplying proper values in @Observe annotation will help Finotes raise better issue reports.

expectNull

If function returns an object and return value could be NULL, then set this field to true to prevent Finotes from raising an issue when function returns NULL.
By default field is false, and issue will be raised if function returns NULL.

severity

Sets severity level of issues reported from a function using this field.
All issues raised from a function will have the same severity level, set to it using @Observe annotation.
By default severity is MAJOR.

Track Feature failures

Finotes has the ability to invoke code level functions and report any issues that may arise from it.

Feature tracking allows developers to track a particular feature in android app by chaining 2 or more functions invoked by finotes.
Let us take the example of add to cart feature in a typical android application.
The function at the time of clicking the add to cart button 'addItemToCart' and 'onItemAddedToCart' success function after the items as been added to the cart needs to be invoked using finotes.
The Finotes looks for 'onItemAddedToCart' function execution after the execution of 'addItemToCart', if the 'onItemAddedToCart' is not executed within in 5000 milliseconds then a corresponding issue will be raised.

Add to cart:
class ProductListingActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_product_listing)
        ...
        ...
        addToCartButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Fn.call("addItemToCart", this@ProductListingActivity, item.getId())
                }
            })

    }

    // Here function 'onItemAddedToCart' is expected to be called in under '5000' milliseconds.
    @Observe(nextFunctionId = "onItemAddedToCart",
            nextFunctionClass = ProductListingActivity::class,
        expectedChainedExecutionTime = 5000)
    fun addItemToCart(itemId :String): Boolean{
        if(isValid(itemId)){
            ... 
            ...
            return true
        }
        return false
    }

    @Override
    fun onApiCallComplete(response :JSONObject){
        if(validResponse(response)){
            Fn.call("onItemAddedToCart", this@ProductListingActivity, response.getString("id"))
        }
    }

    @Observe
    fun onItemAddedToCart(itemId :String){
        showMessage(Messages.CARTED_SUCCESS)
        ...
        ...
    }
}

Notes:
1. Here both functions 'addItemToCart' and 'onItemAddedToCart' are invoked using finotes.
2. Both functions are annotated with @Observe.
3. Using 'nextFunctionId' and 'nextFunctionClass' fields in @Observe annotation the functions are chained.
4. Here 'nextFunctionId' is the name/ID of the second function and 'nextFunctionClass' is the .class where the second function is defined.
5. 'expectedChainedExecutionTime' in first function 'addItemToCart' overrides time needed to execute second function after execution of first function.


@Observe fields


You can make use of nextFunctionId, nextFunctionClass, expectedChainedExecutionTime fields in @Observe to chain 2 or more functions.

expectedExecutionTime

Overrides time needed to execute second function after execution of first function.
By default the time between function executions is set to 2000 milliseconds.

nextFunctionId

Name/ID of the second function that is to be chained with the current function.

nextFunctionClass

Class where second function is defined that is to be chained with the current function.

Track Frame rate issues

This feature will be activated only in debug sdk.

With basic integration Finotes can track and report framerate issue automatically.

Setting custom Activity Markers

Activity markers are events that occur in an application during runtime. Activity and lifecycle events are automatically captured by Finotes.

You can set custom activity markers in the project using Fn.setActivityMarker(). These markers will be shown along with the activity trail when an issue is reported.

Markers are displayed in their chronological order.
Only when an issue is raised, the activity markers are sent to the server.

Call anywhere is you project:
Fn.setActivityMarker(this@PurchaseActivity, "clicked on payment_package_two")

How activity trail will look like along with reported issue in Finotes dashboard:
ActivityWelcome:onCreate                            11:19:24:469	45.79% FREE MEMORY 
MapActivity:onCreate                                11:19:24:708	44.39%
MapActivity:onStart                                 11:19:26:983	45.55%
MapActivity:onResume                                11:19:27:012	45.19%
ActivityWelcome:onDestroy                           11:19:28:515	44.53%
MapActivity:onPause                                 11:20:17:806	50.45%
PurchaseActivity:onCreate                           11:20:18:106	55.19%
PurchaseActivity:onStart                            11:20:18:404	55.43%
PurchaseActivity:onResume                           11:20:18:906	55.23%
PurchaseActivity:clicked on payment_package_two     11:20:24:235	55.20%

Map user or device

Once the user login is complete, you can set any custom user identifier with finotes, This identifier will be tagged with all issue reports raised in Finotes dashboard.
If this API is invoked multiple times, the latest identifier will be used with each issue report.

Anywere in code:
Fn.setCustomId(uniqueIdentifier)
    

Issue callback listener (Optional)

You can listen for and access every issue in realtime using the Fn.listenForIssue() API.
You need to add the listener in your Application class.

Application onCreate
Fn.listenForIssue(IssueFoundListener { 
    issueView: IssueView? ->

})

You will be provided with an Issue object that contains all the issue properties that are being synced to the server, making the whole process transparent.
The callback will be in a background thread, make sure to use a UI thread to implement any UI related actions.
This callback is triggered right after an issue occurrence.

Filter re-occurrnce of same issue (Optional feature)

Finotes reports all issues to the dashboard, in some cases same issue may reoccur in the same device.
Finotes provides a mechanism to filter out same type of issues from being raised and reported (from the same device) for a set number of hours using @Observe annotation in Application class.
Default value is 6 hours.
Minimum supported is 1 Hour to maximum of 24 hours.

Application Class:
@Observe(issueRaiseFrequencyInHours = 10)
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
    }
}

Prevent Crash Reporting

In-order to prevent Finotes from reporting uncaught exceptions that causes app to force close, use Fn.preventCrashReporting().

Application Class below init() line:
override fun onCreate() {
    super.onCreate()
    Fn.init(this)
    Fn.preventCrashReporting()
}

Whitelisting Crash Reports

Developers can whitelist crash reporting using Fn.whiteListCrashes() API.

Application Class:
class BlogApp: Application() {
    
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
        Fn.whiteListCrashes(NullPointerException.class, OutOfMemoryError.class)
    }
}

Fn.whiteListCrashes() API takes exceptions and error classes as arguments separated by coma.
Once called, only crashes that are whitelisted using this API will be reported in Finotes dashboard.
Whitelisting crashes using Fn.whiteListCrashes() API will not work if Fn.preventCrashReporting() is called.

Enabling DryRun mode

During development, you can set the dryRun mode, so that the issues raised will not be sent to the server. Every other feature except the issue sync to server will work as same.

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
        Fn.dryRun()
    }
}

When preparing for production release, you need to remove the DryRun API.

Enable Finotes Logs

You can activate logging using log() API.
Activating verbose will print all logs in LogCat including error and warning logs.

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
        Fn.log()
    }
}

When preparing for production release, you need to remove the log API.

Migration for versions 2.6.0 and below: for existing integrations

We have updated our maven access from key based to keyless mechanism.

In-order to migrate to keyless mechanism in Android project, change the code below in project level build.gradle

Existing build.gradle:
allprojects {
    repositories {
        jcenter()
        maven {
            url "s3://finotescore-android/release"
            credentials(AwsCredentials) {
                accessKey = "Access Token"
                secretKey = "Access Token Secret"
            }
        }
    }
}
Change to,

build.gradle:
allprojects {
    repositories {
        jcenter()
        maven {
            url "https://finotescore-android.s3.amazonaws.com/release"
        }
    }
}