Android SDK
Add justtrack's repository to your root
build.gradle
(project level):allprojects {
repositories {
maven {
url "https://sdk.justtrack.io/maven"
}
}
}
Add the justtrack SDK dependency to your app's
build.gradle
(module level):dependencies {
implementation "io.justtrack:justtrack-android-sdk:4.3.7"
}
To integrate the justtrack SDK into your app you first need an API token. We assume this is stored in your
BuildConfig
as a constant called JUSTTRACK_SDK_API_TOKEN
for the examples given in this document. It should look like this: prod-57e...64 characters...e542
. Keep in mind that these tokens are scoped to the package id of your app, so if you are building more than one app from a single code base, you need to use different tokens for the different apps.Navigate to your apps on the justtrack dashboard and view your app. If you haven't created your app on the dashboard yet, go to Products and create the product for your app if needed, then go to Apps and create your app. This will generate the API token needed to use the justtrack SDK and it should look like this:

Thus, for this example app, you would be using
prod-c6654a0ae88b2f21111b9d69b4539fb1186de983f0ad826f0febaf28e3e3b7ed
as the API token.The main interface of the SDK is called
JustTrackSdk
. It allows you to attribute the current user and record user events. To create an instance of the SDK you have to invoke the JustTrackSdkBuilder
class. To create a new builder you need a Context
from your app. If your application extends the Android Application
class, we will automatically track the user in your app (switching between different activities of your app or sending your app to the background). You should save the reference to the SDK somewhere in your main activity. Instantiating the SDK could look like this:build.gradle:
apply plugin: 'com.android.application'
...
android {
defaultConfig {
applicationId "your.package.id"
...
}
buildTypes {
debug {
buildConfigField "String", "JUSTTRACK_SDK_API_TOKEN", "\"prod-your64charactertoken\""
...
}
release {
buildConfigField "String", "JUSTTRACK_SDK_API_TOKEN", "\"prod-your64charactertoken\""
...
}
}
}
...
MainActivity.java:
public class MainActivity extends SomeActivity {
private JustTrackSdk sdk = null;
@Override
protected void onCreate(/* ... */) {
/// ...
sdk = new JustTrackSdkBuilder(this, BuildConfig.JUSTTRACK_SDK_API_TOKEN).build();
}
@Override
protected void onDestroy(/* ... */) {
/// ...
sdk.onDestroy();
sdk = null;
}
}
You should call
onDestroy
on the SDK when your app terminates. This will unregister all listeners and tear down the session tracking. You have to create a new instance of the SDK before you can use it again.An attribution tells you from which source you got a user. Did the user click on an Ad and installed your app? Did they find it on their own in the Play Store and simply downloaded it? Did another user invite the user so they can play your game together (or against each other)? An attribution contains these information by providing the campaign, channel, and network the user was acquired through. It also assigns a unique justtrack ID to that user which is later used to identify a user. After initializing the SDK you can simply call
attributeUser
to retrieve an attribution containing your justtrack user id as well as information about the source of the user.Future <AttributionResponse> response = sdk.attributeUser();
UUID userId = response.get().getUserId();
log("My user id is " + userId);
If you are only using the justtrack dashboard to keep track of your install sources, you can ignore the result of
attributeUser
in your app. To get the information about the user attribution in your app you need to call get
on the future returned by attributeUser
. This will block the current thread until the future resolves or throws an exception.You can not call
get
on the main (UI) thread! This is a safeguard for you as a promise you get back from the SDK might actually depend on work the main/UI thread is expected to do. Blocking that thread could then deadlock your app. If you call get
on the main thread, an exception will be thrown. You can either call the method on another thread or use sdk.toPromise
to schedule a callback when the future resolves:Future <AttributionResponse> responseFuture = sdk.attributeUser();
sdk.toPromise(responseFuture, new Promise<AttributionResponse, Exception>() {
@Override
public void resolve(AttributionResponse response) {
UUID userId = response.getUserId();
log("My user id is " + userId);
}
@Override
public void reject(@NonNull Exception exception) {
log("An error occurred", exception);
}
});
If the attribution can not be performed because the user is offline, it will eventually fail. The SDK will then wait for a new internet connection and retry the attribution. An attribution obtained after the returned
Future
resolved to a failure value will not update the future. You have to subscribe to updates of the attribution using registerAttributionListener()
to receive the attribution.The
AttributionResponse
interface implements the access layer to an attribution on Java side, but basically an attribution can look like this:{
"userId": "9cf85b2b-71c9-491a-a2b4-72478b3eec11",
"installId": "7abfd27f-2a78-4198-b6b8-fad41308b4a6",
"userType": "acquisition",
"campaign": {
"id": 42,
"name": "ExampleMcoinsCampaign",
"type": "acquisition"
},
"type": "mcoins",
"channel": {
"id": 100,
"name": "ExampleDirectChannel",
"incent": false
},
"network": {
"id": 200,
"name": "ExampleAdNetwork"
},
"sourceId": "example-source-id",
"sourceBundleId": "example-source-bundle-id",
"sourcePlacement": "example-source-placement",
"adsetId": "example-adset-id",
"attributedAt": "2020-07-10T09:45:43Z",
"recruiter": null
}
Such a response corresponds to a user who clicked on an ad from the ad set
example-adset-id
in ExampleAdNetwork
which belongs to the ExampleDirectChannel
. The user thus was not recruited by another user of your app, but instead was bought by a paid advertisement. The sourceId
, sourceBundleId
, sourcePlacement
, and adsetId
fields will not always be set if you paid for a user.{
"userId": "9cf85b2b-71c9-491a-a2b4-72478b3eec11",
"installId": "7abfd27f-2a78-4198-b6b8-fad41308b4a6",
"userType": "acquisition",
"campaign": {
"id": 31,
"name": "ExampleOrganicCampaign",
"type": "acquisition"
},
"type": "organic",
"channel": {
"id": 100,
"name": "ExampleDirectChannel",
"incent": false
},
"network": {
"id": 200,
"name": "Organic"
},
"sourceId": null,
"sourceBundleId": null,
"sourcePlacement": null,
"adsetId": null,
"attributedAt": "2020-07-10T09:45:43Z",
"recruiter": null
}
This user installed your app directly from the Play Store without any advertisements we can track. A user who sees a TV ad for your app might for example end up with such an attribution.
{
"userId": "9cf85b2b-71c9-491a-a2b4-72478b3eec11",
"installId": "7abfd27f-2a78-4198-b6b8-fad41308b4a6",
"userType": "acquisition",
"campaign": {
"id": 15,
"name": "ExampleAffiliateCampaign",
"type": "acquisition"
},
"type": "affiliate",
"channel": {
"id": 120,
"name": "ExampleSocialChannel",
"incent": false
},
"network": {
"id": 210,
"name": "Affiliate"
},
"sourceId": null,
"sourceBundleId": null,
"sourcePlacement": null,
"adsetId": null,
"attributedAt": "2020-07-10T09:45:43Z",
"recruiter": {
"advertiserId": "122ea0b9-544c-4b62-9029-05282279df93",
"userId": "944d6dd3-ac46-4184-a00a-e715a675c16c",
"packageId": "com.example.packageId",
"platform": "android"
}
}
This user clicked on an affiliate link of another user of your app before installing your app.
The justtrack SDK can provide you with the advertiser id of the user if the user didn't limit ad tracking. Additionally, users are divided into three test groups (1, 2, and 3) based on their advertiser id. You can retrieve that test group id from the SDK, implement a different logic for one of the test groups, and then compare the (different) performance of that group on the justtrack dashboard.
Future <AdvertiserIdInfo> infoFuture = sdk.getAdvertiserIdInfo();
@NonNull AdvertiserIdInfo info = infoFuture.get();
@Nullable String advertiserId = info.getAdvertiserId();
boolean isLimitedAdTracking = info.isLimitedAdTracking();
log("My advertiser id is " + advertiserId);
log("Ad tracking is limited = " + isLimitedAdTracking);
Future <Integer> testGroupIdFuture = sdk.getTestGroupId();
@Nullable Integer testGroupId = testGroupIdFuture.get();
log("My test group id is " + testGroupId);
If you already have a mechanism to assign a unique id to each user, you can share this information with the justtrack SDK. This then allows the backend of the justtrack SDK to associate events received from third parties via that user id with the correct user on justtrack side. You can provide as an id multiple times, the justtrack SDK takes care about sending it to the justtrack backend as needed.
You can supply the own user id when constructing the SDK as well as sending it later once your app is already running for some time. Should your own user id change for some reason, you have to supply the new value to the justtrack SDK again:
String ownUserId = /* get your own user id from your app */;
SdkBuilder builder = /* create the builder somehow */ new JustTrackSdkBuilder(this, BuildConfig.APP_KEY);
JustTrackSdk sdk = builder
.setCustomUserId(ownUserId)
// other options...
.build();
ownUserId = /* new user id for some reason */;
sdk.setCustomUserId(ownUserId);
A custom user id must be shorter than 4096 characters and only consist of printable ASCII characters (U+0020 to U+007E).
Publishing an event requires requires you to create an event object which you then can pass to
publishEvent
:PublishableUserEvent event = new UserEvent("category_element_action").build();
sdk.publishEvent(event);
By calling
build()
on a UserEvent
instance you are creating an immutable PublishableUserEvent
object which you can pass to publishEvent()
as often as you need. Calling build()
does not modify the UserEvent
object if you need to modify it again.The SDK defines a broad range of event classes with predefined semantics. When creating those events you can specify some predefined dimensions depending on the event. For example, to track the navigation from your menu screen to a game screen, you could record the following event:
sdk.publishEvent(new UserScreenShowEvent("Game Main", "SCREEN_GAME_MAIN").build());
A
UserEvent
instance is actually a builder for the event we later send to the backend. You can either provide all parameters via the constructor if you want or you can initialize an empty event and later call setters for the different properties. The only thing you need to create a new event is the name of the event, which may also not be empty (and can later no longer be changed). The following constructors are provided:class UserEvent {
UserEvent(@NonNull String name);
UserEvent(@NonNull EventDetails name);
UserEvent(@NonNull EventDetails name, double value, @NonNull Unit unit);
UserEvent(@NonNull EventDetails name, @NonNull Money money);
UserEvent(@NonNull EventDetails name, @NonNull String dimension1);
UserEvent(@NonNull EventDetails name, @NonNull String dimension1, @NonNull String dimension2);
UserEvent(@NonNull EventDetails name, @NonNull String dimension1, @NonNull String dimension2, @NonNull String dimension3);
UserEvent(@NonNull EventDetails name, @NonNull String dimension1, @NonNull String dimension2, @NonNull String dimension3, double value, @NonNull Unit unit);
UserEvent(@NonNull EventDetails name, @Nullable String dimension1, @Nullable String dimension2, @Nullable String dimension3, @NonNull Money money);
}
The
EventDetails
class captures the name of a user event with some additional details. Those are the category
, element
, and action
of the event and can be used to group events together. For example, all events generated by the SDK regarding the session tracking share the category session
.value
and unit
can only be provided together and default to 1
and Unit.COUNT
. Alternatively, an event can carry a monetary value instead of a value with a unit. The three dimensions default to the empty string if not specified. You can also set the properties after initialization:interface UserEventBuilder {
UserEventBuilder setDimension1(@NonNull String dimension1);
UserEventBuilder setDimension2(@NonNull String dimension2);
UserEventBuilder setDimension3(@NonNull String dimension3);
UserEventBuilder setValue(double value, @NonNull Unit unit);
UserEventBuilder setValue(@NonNull Money money);
// convenience wrappers for setValue
UserEventBuilder setCount(double count);
UserEventBuilder setSeconds(double seconds);
UserEventBuilder setMilliseconds(double milliseconds);
}
setCount
, setSeconds
, and setMilliseconds
are provided for your convenience if you know an event will always use a specific unit.If you are interested in whether an event could be successfully send to the backend, you can await the future returned by
publishEvent
:interface JustTrackSdk {
Future<?> publishEvent(@NonNull PublishableUserEvent event);
}
As there is no meaningful response to return to your app the value the future resolves to is not specified by the SDK and may change with different versions of the SDK.
The following restrictions apply to user events:
- The name must not be empty.
- Name, action, category, and element must be shorter than 256 characters and can only consist of printable ISO 8859-1 characters (U+0020 to U+007E as well as U+00A0 to U+00FF).
- Dimension values must be shorter than 4096 characters and can only consist of printable ISO 8859-1 characters (U+0020 to U+007E as well as U+00A0 to U+00FF).
- The value of a user event must be finite (i.e., not NaN or ±Infinity).
- If you provide a monetary value, the currency needs to be a 3 letter uppercase ISO 4217 string.
The justtrack SDK automatically tracks the time a user spends in each level and quest you are tracking via the
PROGRESSION_LEVEL_START
and PROGRESSION_QUEST_START
events. The tracking ends once you trigger the PROGRESSION_LEVEL_FINISH
or PROGRESSION_LEVEL_FAIL
events for levels and PROGRESSION_QUEST_FINISH
or PROGRESSION_QUEST_FAIL
events for quests. These events are then automatically modified to also carry the total time the user spend with the game open on his device.Example: A user starts a level at 1pm and plays for 5 minutes. He then is interrupted by a phone call and your game is running in the background for 10 minutes. Afterwards he continues to play and finishes the level after another 3 minutes. Once you trigger the corresponding finish event the SDK computes that the user took 8 minutes to finish the level and sends this value to the backend. You can then later see on the justtrack dashboard how long users take in general to complete a single level and identify levels which are unreasonably hard or too easy compared to your expectation.
There are two important aspects to this automatic tracking. First, each time the user finishes or fails a level you have to submit another start event for that level again to restart the tracking. If a user fails to complete a level first, we would add the time between the start and the fail event and attach it to the fail event. If the user now retries the level without another start event getting triggered, the next fail or finish event will not have any duration attached. Thus, there should be one start event for every fail or finish event. The flow could look something like this:
As you can see, each event is carrying some string in the above example. They represent the element name dimension of the events. If two progression events carry different element names or IDs, we will treat them like separate levels and not match them. Thus, if you send a finish event for level 2 two seconds after starting level 1 we will not add a duration of two seconds to that event, but instead look for some other start event in the past for level 2. Similarly, quests and levels are of course different namespaces and will not be mixed, either.
The SDK can automatically integrate with the IronSource SDK and publish UserEvents for each ad impression. Additionally, the SDK can set the user id in the IronSource SDK to the justtrack user id or an id of your choice to enable the justtrack backend to match the ad impressions with the users during a revenue import. You can either configure the SDK to integrate with the IronSource SDK upon instantiation of your instance of the justtrack SDK or later perform an explicit call.
The integration uses reflection to locate and setup the IronSource SDK. It was tested with version
7.0.3.1
of com.ironsource.sdk:mediationsdk
. It should be compatible with version 6 as well as 7 of the IronSource SDK though.If you tell the SDK builder to integrate with the IronSource SDK you will not get notified about whether the integration failed or was successful. As the integration depends on the attribution, it is done asynchronously in the background, so it might not be done after the call to
build()
returns. You can enable the integration like this:SdkBuilder builder = /* create the builder somehow */ new JustTrackSdkBuilder(this, BuildConfig.APP_KEY);
JustTrackSdk sdk = builder
.setEnableIronSourceIntegration(true, IronSourceUserIdSource.JustTrack)
// or, if you want to provide your own user id:
.setEnableIronSourceIntegration(true, "...your user id...")
// other options...
.build();
Each call to
setEnableIronSourceIntegration
overrides the previous settings, only the last call before instantiating the justtrack SDK applies.You can instead also trigger the integration yourself after creating an instance of the justtrack SDK. Only one method (configuring the builder or setting up the integration manually) is needed. To set up the integration manually, take a look at the following code snippet:
JustTrackSdk sdk = /* get an SDK instance somehow */;
// kick off the integration, will eventually complete as long as attribution succeeds
Future<?> integrationFuture = sdk.integrateWithIronSource(IronSourceUserIdSource.JustTrack);
// or, if you want to provide your own user id:
Future<?> integrationFuture = sdk.integrateWithIronSource("...your user id...");
// if you need to know whether the integration succeeded and is done:
integrationFuture.get();
// if no exception is thrown, the integration was successful
If you supply your own user id to IronSource (either by letting the justtrack SDK forward it or by specifying
IronSourceUserIdSource.NoUserId
), you need to provide it as a custom user id to the justtrack SDK. Otherwise, we will not be able to import the IronSource revenue later on and match ad impressions to the correct user in every case.The justtrack SDK can forward data about ad impressions to the justtrack backend to track the ad revenue of your app. If you are using the IronSource integration, this will already happen automatically for IronSource ad impressions. For other ad network integrations (AdMob for example), you have to provide the ad impression events yourself by calling
forwardAdImpression
:JustTrackSdk sdk = /* get an SDK instance somehow */;
sdk.forwardAdImpression(adFormat, adSdkName, adNetwork, placement, abTesting, segmentName, instanceName, revenue, currency);
You need to provide the following parameters to such a call:
- adFormat: The format of the ad.
- adSdkName: The name of the SDK which provided the event, for example "ironsource" or "admob".
- adNetwork: The name of the ad network which provided the ad. May be null.
- placement: The placement of the ad. May be null.
- abTesting: The test group of the user if we are A/B testing. May be null.
- segmentName: The name of the segment of the ad. May be null.
- instanceName: The name of the instance of the ad. May be null.
- revenue: The amount of revenue this ad generated. May be zero but must not be negative.
- currency: The currency of the revenue amount. Should be a three letter uppercase ISO 4217 string.
Capitalization of the parameter values is important (i.e., "Ironsource" would not work as ad SDK name). Upon success,
forwardAdImpression
returns true, if a parameter was invalid, false is returned.If the user performs an in-app product or subscription purchase, you can forward the purchase via the justtrack SDK to our backend. You need to call
forwardInAppProductPurchase
or forwardInAppSubscriptionPurchase
for this and provide the ID of the product or subscription, a token verifying the purchase, the price, as well as two optional values identifying the type and name of the purchased product or subscription (for example, if you have many products in your app and want to distinguish between them).You get the ID of the product or subscription by calling getProducts on the Purchase. You should forward each product as a separate in-app purchase. The token can be obtained by calling getPurchaseToken and stays the same for all products in a single purchase.
Forwarding the information to the justtrack SDK looks like this:
JustTrackSdk sdk = /* get an SDK instance somehow */;
sdk.forwardInAppProductPurchase(productId, token, money, itemType, itemName);
sdk.forwardInAppSubscriptionPurchase(subscriptionId, token, money, itemType, itemName);
Upon success,
forwardInAppProductPurchase
and forwardInAppSubscriptionPurchase
return true, if a parameter was invalid, false is returned.If you are using the Firebase SDK next to the justtrack SDK, you can use the justtrack SDK to send the Firebase app instance id of a user to the justtrack backend. You can then later send events from the justtrack backend to Firebase to measure events happening outside of your app. To send these events, the justtrack backend needs the Firebase App instance id of the user. You can send this by calling
sdk.publishFirebaseAppInstanceId(firebaseAppInstanceId)
, but of course this requires you to add additional boilerplate code to your app.To help you here, the justtrack SDK detects the presence of the Firebase SDK at runtime automatically and automatically performs a call to
publishFirebaseAppInstanceId
with the correct value. If you don't want this behavior, you can disable it by using builder.setEnableFirebaseIntegration(false)
when constructing an instance of the justtrack SDK.Retargeting allows you to re-engage users with your app which already did install the app once in the past. As they already installed the app in the past, you have a high chance to get them to do it again - after all, they did like your app enough to do it once already. Maybe they just did run out of things to do in your game (but you now added new content), focused on something else for some time (but maybe need now another change in their favorite app), or were using your app on their ride to work every day, but then a global pandemic caused everyone to work from home.
A retargeting campaign thus is a little bit different from a normal campaign. Some users still have your app installed and thus the app can be opened directly with the right
Intent
send to their phones. Also a user might expect some Welcome back screen upon returning into the app.Upon encountering a retargeting opportunity the SDK checks with the justtrack backend whether a user needs to be re-attributed to a retargeting campaign. Such a retargeting opportunity arises if:
- The user did not open the app for quite some time. Default: 48 hours
- The last time we fetched an attribution for the user is some time ago. Default: 14 days
- The app was opened by an intent which looks like it might be from a click on a retargeting campaign
- The app was resumed and the current active activity now has a different intent set as before
Upon encountering a retargeting opportunity the SDK will check the justtrack backend for a new attribution. If the user was just old (second case, older than 14 days) and did not click on a retargeting campaign the old attribution will be returned. If there was however a click, we might get a new attribution back. If we receive a retargeting attribution the method
sdk.getRetargetingParameters()
will return data about the campaign the user got attributed to. These include the promotion code for the campaign as well as additional parameters configured for the campaign. You can then use those to e.g. present the user with a special Welcome back! screen or grant the user a promised gift.The retargeting parameters are only available on the first start of the SDK after the re-attribution of the user to a retargeting campaign. Thus, if the user opens the app again while still being attributed to the retargeting campaign,
sdk.getRetargetingParameters()
will return null
this time, so your app can avoid granting the user gifts multiple times or similar.As we can now trigger an attribution request while the app is already running (and therefore we already requested the attribution once) there are now new callbacks you can subscribe to:
sdk.registerAttributionListener
: Use this to get notified about the new attribution while the app is running.sdk.registerRetargetingParametersListener
: use this to get notified about new retargeting parameters while the app is running.
While most of the work is done automatically by the SDK to handle re-attribution, it can not automatically listen to intents which arrive while your app is already running. Thus you have to forward them manually to the SDK, otherwise you can miss some re-attribution opportunities and not all users clicking on an ad will get the reward you promised them for returning.
public class MainActivity extends SomeActivity {
// ...
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// EITHER:
// If your app does not make use of the intent returned by `Activity#getIntent()` you can
// just set the intent on the activity and the SDK will take care of the rest
setIntent(intent);
// OR:
// Alternatively you can forward the intent directly to the SDK. In that case you don't need
// to call `setIntent` on your activity, but have to forward all future intents like this, too:
JustTrackSdk sdk = getSdkInstance(); // get an sdk instance from somewhere in your app...
sdk.onNewIntent(intent);
}
// ...
}
Upon creation of the SDK you might have already noticed that you also have to pass an Intent to the SDK. The SDK needs this intent to detect whether your app was launched from e.g. a click on a retargeting campaign. You should normally pass the result of
Activity#getIntent()
to the SDK builder.If you want to control how often the SDK checks by itself for a new attribution, you can use
setInactivityTimeFrame
and setReAttributionTimeFrame
on the SDK builder. Using those you can change the defaults of 48 hours for inactive users as well as 14 days for the maximum age of an attribution.If a user clicks on a retargeting campaign and already has the app installed, we open the app via an
Intent
. This can happen so fast that the justtrack backend might still be processing the click and therefore it would take a few seconds to validate with the server that the re-attribution of the user was correct. If you want to display a special Welcome back! screen to the user, it can be useful to already know some of the parameters of the click in advance and just display the screen, even if it later turns out the user was not re-attributed. Therefore the justtrack SDK provides a method getPreliminaryRetargetingParameters
to retrieve an instance of PreliminaryRetargetingParameters
if any are available. Those parameters are created from data not yet validated and should not be relied on for anything which can not be reversed if they do not validate.JustTrackSdk sdk = /* get an SDK instance somehow */;
PreliminaryRetargetingParameters parameters = sdk.getPreliminaryRetargetingParameters();
if (parameters != null) {
// your app got started because of a retargeting click:
showWelcomeDialog(sdk, parameters);
} else {
Subscription subscription = sdk.registerPreliminaryRetargetingParametersListener((parameters) -> {
// your app was already running in the background and the user clicked on a retargeting campaign
showWelcomeDialog(sdk, parameters);
});
// upon termination of your app:
subscription.unsubscribe();
}
void showWelcomeScreen(JustTrackSdk sdk, PreliminaryRetargetingParameters parameters) {
navigateTo(welcomeScreen);
sdk.toPromise(parameters.validate(), new Promise<PreliminaryRetargetingParameters.ValidateResult, Exception>() {
@Override
public void resolve(PreliminaryRetargetingParameters.ValidateResult response) {
if (response.isValid()) {
// the click was valid, you can now persist any rewards you promised
persistWelcomeBonus();
} else {
// it was not valid, handle somehow (here we just navigate to some other screen,
// you might want to have some better handling here)
navigateTo(mainScreen);
}
}
@Override
public void reject(@NonNull Exception exception) {
// we could not communicate with the JustTrack backend, handle the error somehow
handleError(exception);
}
});
}
Last modified 11d ago