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.
Now, when you go back to “Environments”, then open the “Production” install instructions, you will see some new stuff in there.
build.gradle
file, and some more code to put into your MainActivity.java
file.
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.
2020-02-03 17:14:14.843 16907-16991/com.webanalyticsfordevelopers.test.w4dmobile D/AdobeExperienceSDK: AnalyticsHitsDatabase - AnalyticsExtension request was sent with body (ndh=1&aamlh=6&ce=UTF-8&pev2=ADBINTERNAL%3ALifecycle&c.&a.&DaysSinceFirstUse=1&RunMode=Application&HourOfDay=17&Resolution=1080x2028&locale=en-US&DaysSinceLastUse=0&CarrierName=Android&ignoredSessionLength=-1580746379&AppID=W4DMobile%201.0%20%281%29&TimeSinceLaunch=0&OSVersion=Android%207.1.1&Launches=5&DeviceName=Android%20SDK%20built%20for%20x86&DayOfWeek=2&CrashEvent=CrashEvent&LaunchEvent=LaunchEvent&internalaction=Lifecycle&.a&.c&t=00%2F00%2F0000%2000%3A00%3A00%200%20-60&mid=39052343129627861166046374451306937819&pe=lnk_o&pageName=W4DMobile%201.0%20%281%29&aamb=j8Odv6LonN4r3an7LhD3WZrU1bUpAkFkkiY1ncBR96t2PTI&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); }
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]](https://webanalyticsfordevelopers.files.wordpress.com/2020/02/200204-aepm-language.png?w=656&h=423)
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]](https://webanalyticsfordevelopers.files.wordpress.com/2020/02/200204-aepm-tap.png?w=656&h=423)
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.
Hi Jan,
I’ve tried to use Launch Rules but I’m facing some issues. For example, if I try to intercept a trackState and trigger a trackAction, the SDK is sending another additional trackState (a copy of the original).
Have you tested them? Have you encountered this kind of problem
LikeLike
Hi Miguel,
The interplay of programmed calls and Launch Rules is interesting, yes. There’ll be an article on that, soon.
Best,
Jan
LikeLike
I am experiencing the exact same thing when using Launch. Is this the expected behavior?
LikeLike
Hi John,
It is, yes.
The trackAction() or trackState() calls that you program into your app are NOT triggers for Launch. They just send data to Analytics.
Launch can pick up on them, yes, but if what you want is to decide whether to track in Launch, you have to build it differently.
HTH,
Jan
LikeLike