Adobe Experience Platform Mobile SDKs – Analytics

This article is part of the Adobe Experience Platform Mobile SDKs mini-series. It is about tracking your app, more specifically about how to add Analytics into it.

You can find the overview here.

Analytics

Adobe Analytics is an application that allows you to measure how visitors of your site or app behave, from the generic all the way down to extremely specific things, should the need arise.

Like targeting and A/B testing, analytics is standard on web sites. Most tools in the market use Javascript to collect and assemble data, and to send it off “into the cloud” at specific points. These days, the Javascript is not managed manually. Instead, people use Tag Management Systems (TMSs) like Launch, meaning parts of the configuration of what is tracked, and when that happens, can be done via an application rather than by a programmer.

In a native mobile app, there is no Javascript, and there is mostly no TMS.

Setting aside those asides, most people these days add Analytics into their native mobile apps by adding the SDK and some code. And that is what we will do.

Adding Analytics

Let’s start with Launch, because that’s where we add the Analytics Extension into the Mobile Property for our app.

Go to “Extensions”, open the Catalog, then find the “Adobe Analytics” Extension and click the “Install” button.

[screenshot]
Installing the Analytics Extensions
Provide the necessary information like report suite, then click “Save”

[screenshot]
Analytics Extension Configuration
Because we’re using our app from the last time, your list of Extensions should now have 4 items.

[screenshot]
4 Extensions
Create a new Library (maybe called Analytics), add the Extension to it, then push it all the way through to production.

Now, when you go back to “Environments”, then open the “Production” install instructions, you will see some new stuff in there.

[screenshot]
Added code
There is one more line for the build.gradle file, and some more code to put into your MainActivity.java file.

[screenshot]
More added code
All in all, three new lines of code, and that’s it. You now have Analytics built into your app.

Unfortunately, it really doesn’t do anything for now…

There a some things you still need to do, and we’re looking at those, in turn:

  • Lifecycle metrics,
  • tracking Views / Activities, and
  • tracking interactions

Let’s start with Lifecycle metrics.

Lifecycle metrics

Lifecycle metrics were introduced to Adobe Analytics by way of what is called “Mobile Services” — a mobile product manager-centric set of functionality on top of Analytics, with a tailored interface and some new metrics.

Lifecycle metrics include things like “First Launches”, “Launches”, “Crashes” and “Upgrades”, but also dimensions like “App ID”, “Operating System Version”, “Install date”, and “Device Name”.

They are really fairly easy to implement, thanks to the way iOS and Android handle their app flow.

You initialise them in your main Activity or application:didFinishLaunchingWithOptions: delegate method, pause them in your onPause() methods or in your app’s applicationDidEnterBackground delegate method, and restart them in onResume() or the applicationWillEnterForeground delegate.

My MainActivity.java will look like this:

package com.webanalyticsfordevelopers.test.w4dmobile;

import android.content.Intent;
import android.os.Bundle;

import com.adobe.marketing.mobile.Analytics;
import com.adobe.marketing.mobile.TargetRequest;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.WebView;
import android.widget.TextView;

import com.adobe.marketing.mobile.AdobeCallback;
import com.adobe.marketing.mobile.Identity;
import com.adobe.marketing.mobile.InvalidInitException;
import com.adobe.marketing.mobile.Lifecycle;
import com.adobe.marketing.mobile.LoggingMode;
import com.adobe.marketing.mobile.MobileCore;
import com.adobe.marketing.mobile.Signal;
import com.adobe.marketing.mobile.Target;
import com.adobe.marketing.mobile.UserProfile;

import org.json.JSONException;
import org.json.JSONObject;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MobileCore.setApplication(this.getApplication());
        MobileCore.setLogLevel(LoggingMode.DEBUG);

        try {
            Analytics.registerExtension();
            Target.registerExtension();
            UserProfile.registerExtension();
            Identity.registerExtension();
            Lifecycle.registerExtension();
            Signal.registerExtension();
            MobileCore.start(new AdobeCallback() {
                @Override
                public void call(Object o) {
                    MobileCore.configureWithAppID("3028746f70eb/d6b1ea7ebece/launch-e736742547f3");
                }
            });
        } catch (InvalidInitException e) {
            e.printStackTrace();
        }

        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View view) {
                Snackbar.make(view, "Creating Target request...", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
                TargetRequest targetRequest1 = new TargetRequest("w4dmobile-targeting1", null
                        , "{\"text\":\"default\",\"url\":\"https://webanalyticsfordevelopers.com/\""
                        , new AdobeCallback<String>() {
                    @Override
                    public void call(String jsonResponse) {
                        Snackbar.make(view, "Content received", Snackbar.LENGTH_LONG)
                                .setAction("Action", null).show();
                        // so this should be JSON content...
                        try {
                            JSONObject targetJSONResponse = new JSONObject(jsonResponse);
                            // replace content as needed
                            final String textForTextView = targetJSONResponse.getString("text");
                            final TextView textView1 = findViewById(R.id.textView1);
                            final String urlForWebViewAsText = targetJSONResponse.getString("url");
                            URL url = new URL(urlForWebViewAsText); // I like to check my URLs
                            final WebView webView = findViewById(R.id.webView);
                            if (urlForWebViewAsText.length() > 0) {
                                runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        textView1.setText(textForTextView);
                                        webView.loadUrl(urlForWebViewAsText);
                                        Snackbar.make(view, "Content replaced", Snackbar.LENGTH_LONG)
                                                .setAction("Action", null).show();
                                    }
                                });
                            }
                        } catch (JSONException e) {
                            Snackbar.make(view, "Content from Target not valid JSON", Snackbar.LENGTH_LONG)
                                    .setAction("Action", null).show();
                        } catch (MalformedURLException e) {
                            Snackbar.make(view, "Target returned invalid URL", Snackbar.LENGTH_LONG)
                                    .setAction("Action", null).show();
                        }
                    }
                });
                List<TargetRequest> requests = new ArrayList<>();
                requests.add(targetRequest1);
                // prep done, now retrieve content
                Snackbar.make(view, "Retrieving content from Target", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
                Target.retrieveLocationContent(requests, null);
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            Intent intent = new Intent(this, SettingsActivity.class);
            startActivity(intent);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onResume() {
        super.onResume();
        MobileCore.setApplication(getApplication());
        MobileCore.lifecycleStart(null);
    }

    @Override
    public void onPause() {
        MobileCore.lifecyclePause();
        super.onPause();
    }
}

If you build and launch the app at this point, you will see calls to Adobe Analytics when it starts.

[screenshot]
Lifecycle call on app start
The call, in text format, looks like this:

2020-02-03 17:14:14.843 16907-16991/com.webanalyticsfordevelopers.test.w4dmobile D/AdobeExperienceSDK: AnalyticsHitsDatabase - AnalyticsExtension request was sent with body (ndh=1&amp;aamlh=6&amp;ce=UTF-8&amp;pev2=ADBINTERNAL%3ALifecycle&amp;c.&amp;a.&amp;DaysSinceFirstUse=1&amp;RunMode=Application&amp;HourOfDay=17&amp;Resolution=1080x2028&amp;locale=en-US&amp;DaysSinceLastUse=0&amp;CarrierName=Android&amp;ignoredSessionLength=-1580746379&amp;AppID=W4DMobile%201.0%20%281%29&amp;TimeSinceLaunch=0&amp;OSVersion=Android%207.1.1&amp;Launches=5&amp;DeviceName=Android%20SDK%20built%20for%20x86&amp;DayOfWeek=2&amp;CrashEvent=CrashEvent&amp;LaunchEvent=LaunchEvent&amp;internalaction=Lifecycle&amp;.a&amp;.c&amp;t=00%2F00%2F0000%2000%3A00%3A00%200%20-60&amp;mid=39052343129627861166046374451306937819&amp;pe=lnk_o&amp;pageName=W4DMobile%201.0%20%281%29&amp;aamb=j8Odv6LonN4r3an7LhD3WZrU1bUpAkFkkiY1ncBR96t2PTI&amp;cp=foreground)

The two interesting bits in this call are “CrashEvent=CrashEvent” and “LaunchEvent=LaunchEvent”. You’ll see the latter everytime the app starts, and the former in situations where it starts after a crash.

A “crash” is detected by the SDK itself. It is essentially if Lifecycle tracking wasn’t stopped properly. “Uh, an open session! There must have been a crash last time!”

Tracking Views & Activities

The equivalent of tracking a page load on a web site for mobile is the creation of an Activity or View, at least that is what the documentation says. Well, the documentation for Mobile Services, which is the predecessor of the AEP Mobile SDK.

You could argue that onResume would be a better place to track page views, but then you’d have to also argue that every switch back to a browser tab with your page should also count as a page view. To my knowledge, very few people do that.

So, let’s keep it simple, and let’s keep tracking cost lower, by adding the tracking of Activities to the onCreate method.

We need one single line of code, believe it or not:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_settings);
    MobileCore.trackState("Settings", null);
}

[screenshot]
state becomes pageName
The downside of simplicity, sometimes, is a lack of expression. In this case, our line of code will track a page view, and it will use the String we provided (“Home”) as the page name. But that’s it. And if you have read the “Basic Tracking” article from 2013, you know that I am not happy with this level or information.

You should track more than just the page name, always.

Since our app is very simple, let me just add language, as an example:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_settings);
    // create contextData map
    Map<String,String> contextData = new HashMap<>();
    // add language = en
    contextData.put("language", "en");
    MobileCore.trackState("Settings", contextData);
}

The tracking calls when our two Activities (“Home” and “Settings”) are created will now contain the language, too.

[screenshot]
Language = en on the calls

Tracking Interactions

On web sites, we often track what visitors do on a page. Things like a search, scrolling all the way to the bottom, downloading a document, starting a video, and lots more. We use “Custom Link Tracking” to do that, or, in the technical lingo, s.tl().

We can do the same in an app using the MobileCore.trackAction() method.

For our app, let’s track when users click the icon on the home screen.

We need exactly one line of code to do it, which we put first in the onClick listener of our floating button:

fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(final View view) {
        // track this into Analytics
        MobileCore.trackAction("Targeting Button tapped", null);
        // now over to Target
        Snackbar.make(view, "Creating Target request...", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show();
        TargetRequest targetRequest1 = new TargetRequest("w4dmobile-targeting1", null
                , "{\"text\":\"default\",\"url\":\"https://webanalyticsfordevelopers.com/\""
                , new AdobeCallback<String>() {
            @Override
            public void call(String jsonResponse) {
                Snackbar.make(view, "Content received", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
                // so this should be JSON content...
                try {
                    JSONObject targetJSONResponse = new JSONObject(jsonResponse);
                    // replace content as needed
                    final String textForTextView = targetJSONResponse.getString("text");
                    final TextView textView1 = findViewById(R.id.textView1);
                    final String urlForWebViewAsText = targetJSONResponse.getString("url");
                    URL url = new URL(urlForWebViewAsText); // I like to check my URLs
                    final WebView webView = findViewById(R.id.webView);
                    if (urlForWebViewAsText.length() > 0) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                textView1.setText(textForTextView);
                                webView.loadUrl(urlForWebViewAsText);
                                Snackbar.make(view, "Content replaced", Snackbar.LENGTH_LONG)
                                        .setAction("Action", null).show();
                            }
                        });
                    }
                } catch (JSONException e) {
                    Snackbar.make(view, "Content from Target not valid JSON", Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show();
                } catch (MalformedURLException e) {
                    Snackbar.make(view, "Target returned invalid URL", Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show();
                }
            }
        });
        List<TargetRequest> requests = new ArrayList<>();
        requests.add(targetRequest1);
        // prep done, now retrieve content
        Snackbar.make(view, "Retrieving content from Target", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show();
        Target.retrieveLocationContent(requests, null);
    }
});

Now when you run the app and tap that button, you can see an Analytics request being sent:

[screenshot]
Tap tracked, job jobbed

And Launch?

I have been teasing tag management twice now, in this series. In the pre-requisites, I told you how the SDK retrieves (or tries to retrieve) the current configuration of the SDK from Launch, and up in this article, I wrote about how with native apps, tag management is not as straightforward as it is on a web page. I did not, however, say it didn’t exist.

And if you know Launch, or have looked a little bit at the different options you have with Launch and a Mobile Property, the obvious question is: can I do the tracking from Launch?

The answer is: no, you can’t.

And I have to immediately qualify that: you can do some things with Launch, even for a native mobile app. We’ll get to that at a later stage in this mini series, I promise.

2 thoughts on “Adobe Experience Platform Mobile SDKs – Analytics

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.