Kotlin Documentation

Pre requisites

Finotes SDK supports Android projects with minimum SDK version 14 (Ice-cream Sandwich) or above.

1. Get Started using the button below.
2. Use "Add App" option in dashboard to link android app to Finotes.

Get Started

Current Version 2.5.5


Integration

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 "s3://finotescore-android/release"
            credentials(AwsCredentials) {
                accessKey = "Access Token"
                secretKey = "Access Token Secret"
            }
        }
    }
}

Step Two

You will be able to get Access Token, Access Token Secret from ‘Apps’ section in Finotes dashboard.

Step Three

Then in app level build.gradle

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

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

Initialize

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)
    }
}

Test

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

Step One

Add Fn.reportIssue(this, “Test Issue”, Severity.MINOR) under Fn.init(this)

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this)
        //Fn.reportIssue allows you to raise custom issues.
        //Refer Custom Issue section by the end of this documentation for more details.
        Fn.reportIssue(this, "Test Issue", Severity.MINOR)
    }
}

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 issue is still not synced in Finotes dashboard, Click Here.

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

Release Build

Make sure that dryRun and verbose flags are off in release build and proguard rules are added as specified above.

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Fn.init(this) //By default the dryRun and verbose flags are off.
    }
}

Please make sure that the mapping file of each production 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

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%

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 roadbloacks that you may have.

Privacy (Optional feature)

As Finotes reports API call issues, each issue is tagged with corresponding request response headers, request body and assosiated 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:

Lets 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 itelf as is not sent to the Finotes dashboard, if any issues are raised.
The maskHeaders field is case insensitive and is optional.

Global Exceptions

In-order to catch uncaught exceptions that causes app to force close, use Fn.catchUnCaughtExceptions().

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

Remember to backup the mapping file during release build. You can find more information about release build here

Low Memory Reporting

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()
    }
}

Caught 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.

Custom Issue

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, reason, Severity.MAJOR)

}

Use case:

In cases where the fail situations can be anticipated, they may be reported using Fn.reportIssue().
You may pass a string as parameter to the anticipated issue.
As in custom exceptions, you will be able to make use of activity markers to aid reproducing the issue when reported.

Function call

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 mecahnism 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 annoatation, 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.

Feature Tracking

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.
Lets 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.

Listen for Issue

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.

DryRun

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()
        //Second parameter in Fn.init() toggles dryRun flag, which is false by default.
        Fn.init(this, true, false)
    }
}

When preparing for production release, you need to unset the DryRun flag.

VerboseLog

You can toggle logging using third parameter.
Activating verbose will print all logs in LogCat including error and warning logs.

Application Class:
class BlogApp: Application() {
    override fun onCreate() {
        super.onCreate()
        //Third parameter in Fn.init() toggles the verbose mode.
        Fn.init(this, false, true)
    }
}