JustTrack
Search…
Android SDK Readme
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.

Getting an API token

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.

Instantiate the SDK

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:
1
apply plugin: 'com.android.application'
2
...
3
4
android {
5
defaultConfig {
6
applicationId "your.package.id"
7
...
8
}
9
buildTypes {
10
debug {
11
buildConfigField "String", "JUSTTRACK_SDK_API_TOKEN", "\"prod-your64charactertoken\""
12
...
13
}
14
release {
15
buildConfigField "String", "JUSTTRACK_SDK_API_TOKEN", "\"prod-your64charactertoken\""
16
...
17
}
18
}
19
}
20
21
...
Copied!
MainActivity.java:
1
public class MainActivity extends SomeActivity {
2
private JustTrackSdk sdk = null;
3
4
@Override
5
protected void onCreate(/* ... */) {
6
/// ...
7
sdk = new JustTrackSdkBuilder(this, BuildConfig.JUSTTRACK_SDK_API_TOKEN).build();
8
}
9
10
@Override
11
protected void onDestroy(/* ... */) {
12
/// ...
13
sdk.onDestroy();
14
sdk = null;
15
}
16
}
Copied!
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.

Get an attribution

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.
1
Future <AttributionResponse> response = sdk.attributeUser();
2
UUID userId = response.get().getUserId();
3
log("My user id is " + userId);
Copied!
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:
1
Future <AttributionResponse> responseFuture = sdk.attributeUser();
2
sdk.toPromise(responseFuture, new Promise<AttributionResponse, Exception>() {
3
@Override
4
public void resolve(AttributionResponse response) {
5
UUID userId = response.getUserId();
6
log("My user id is " + userId);
7
}
8
9
@Override
10
public void reject(@NonNull Exception exception) {
11
log("An error occurred", exception);
12
}
13
});
Copied!
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.

Structure of an attribution

The AttributionResponse interface implements the access layer to an attribution on Java side, but basically an attribution can look like this:
1
{
2
"userId": "9cf85b2b-71c9-491a-a2b4-72478b3eec11",
3
"installId": "7abfd27f-2a78-4198-b6b8-fad41308b4a6",
4
"userType": "acquisition",
5
"campaign": {
6
"id": 42,
7
"name": "ExampleMcoinsCampaign",
8
"type": "acquisition"
9
},
10
"type": "mcoins",
11
"channel": {
12
"id": 100,
13
"name": "ExampleDirectChannel",
14
"incent": false
15
},
16
"network": {
17
"id": 200,
18
"name": "ExampleAdNetwork"
19
},
20
"sourceId": "example-source-id",
21
"sourceBundleId": "example-source-bundle-id",
22
"sourcePlacement": "example-source-placement",
23
"adsetId": "example-adset-id",
24
"attributedAt": "2020-07-10T09:45:43Z",
25
"recruiter": null
26
}
Copied!
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.
1
{
2
"userId": "9cf85b2b-71c9-491a-a2b4-72478b3eec11",
3
"installId": "7abfd27f-2a78-4198-b6b8-fad41308b4a6",
4
"userType": "acquisition",
5
"campaign": {
6
"id": 31,
7
"name": "ExampleOrganicCampaign",
8
"type": "acquisition"
9
},
10
"type": "organic",
11
"channel": {
12
"id": 100,
13
"name": "ExampleDirectChannel",
14
"incent": false
15
},
16
"network": {
17
"id": 200,
18
"name": "Organic"
19
},
20
"sourceId": null,
21
"sourceBundleId": null,
22
"sourcePlacement": null,
23
"adsetId": null,
24
"attributedAt": "2020-07-10T09:45:43Z",
25
"recruiter": null
26
}
Copied!
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.
1
{
2
"userId": "9cf85b2b-71c9-491a-a2b4-72478b3eec11",
3
"installId": "7abfd27f-2a78-4198-b6b8-fad41308b4a6",
4
"userType": "acquisition",
5
"campaign": {
6
"id": 15,
7
"name": "ExampleAffiliateCampaign",
8
"type": "acquisition"
9
},
10
"type": "affiliate",
11
"channel": {
12
"id": 120,
13
"name": "ExampleSocialChannel",
14
"incent": false
15
},
16
"network": {
17
"id": 210,
18
"name": "Affiliate"
19
},
20
"sourceId": null,
21
"sourceBundleId": null,
22
"sourcePlacement": null,
23
"adsetId": null,
24
"attributedAt": "2020-07-10T09:45:43Z",
25
"recruiter": {
26
"advertiserId": "122ea0b9-544c-4b62-9029-05282279df93",
27
"userId": "944d6dd3-ac46-4184-a00a-e715a675c16c",
28
"packageId": "com.example.packageId",
29
"platform": "android"
30
}
31
}
Copied!
This user clicked on an affiliate link of another user of your app before installing your app.

User events

Publishing a simple event requires two steps: Create the event and call publish on the event:
1
PublishableUserEvent event = new UserEvent("category_element_action").build();
2
sdk.publishEvent(event);
Copied!
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:
1
sdk.publishEvent(new UserScreenShowEvent("Game Main", "SCREEN_GAME_MAIN").build());
Copied!

Building a user event

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:
1
class UserEvent {
2
UserEvent(@NonNull String name);
3
UserEvent(@NonNull EventDetails name);
4
UserEvent(@NonNull EventDetails name, double value, @NonNull Unit unit);
5
UserEvent(@NonNull EventDetails name, @NonNull String dimension1);
6
UserEvent(@NonNull EventDetails name, @NonNull String dimension1, @NonNull String dimension2);
7
UserEvent(@NonNull EventDetails name, @NonNull String dimension1, @NonNull String dimension2, @NonNull String dimension3);
8
UserEvent(@NonNull EventDetails name, @NonNull String dimension1, @NonNull String dimension2, @NonNull String dimension3, double value, @NonNull Unit unit);
9
}
Copied!
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 always provided together and default to 1 and Unit.COUNT. The three dimensions default to "" if not specified. You can set the properties after initialization also with these methods:
1
class UserEvent {
2
void setDimension1(@NonNull String dimension1);
3
void setDimension2(@NonNull String dimension2);
4
void setDimension3(@NonNull String dimension3);
5
void setValue(double value, @NonNull Unit unit);
6
// convenience wrappers for setValue
7
void setCount(double count);
8
void setSeconds(double seconds);
9
void setMilliseconds(double milliseconds);
10
}
Copied!
setCount, setSeconds and setMilliseconds are provided for your convenience if you know for some event that it will always use a specific unit.

Sending a user event to the backend

If you are interested in whether an event could be successfully send to the backend, you can await the future returned by publishEvent:
1
interface JustTrackSdk {
2
Future<?> publishEvent(@NonNull PublishableUserEvent event);
3
}
Copied!
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.

Progression time tracking

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.

IronSource integration

The SDK can automatically integrate with the IronSource SDK and publish UserEvents for each ad impression of the user. Additionally, the SDK sets the user id in the IronSource SDK to the JustTrack user id 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. You have to add the following to your proguard-rules.pro for the sdk to be able to find the IronSource SDK at runtime:
1
-keepclasseswithmembernames class com.ironsource.mediationsdk.IronSource
2
-keepclassmembernames class com.ironsource.mediationsdk.impressionData.ImpressionData
3
-keep interface com.ironsource.mediationsdk.impressionData.ImpressionDataListener
Copied!
If you tell the 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:
1
SdkBuilder builder = /* create the builder somehow */ new JustTrackSdkBuilder(this, BuildConfig.APP_KEY);
2
JustTrackSdk sdk = builder
3
.setEnableIronSourceIntegration(true)
4
// other options...
5
.build();
Copied!
If these restrictions are not tolerable for your app, you also have the option to trigger the integration yourself. Keep in mind that you only need one of the two methods (setEnableIronSourceIntegration or integrateWithIronSource) - using both will integrate two times and might result in duplicate events. The following code illustrates how integrating directly might look:
1
JustTrackSdk sdk = /* get an SDK instance somehow */;
2
// kick off the integration, will eventually complete as long as attribution succeeds
3
Future<?> integrationFuture = sdk.integrateWithIronSource();
4
// if you need to know whether the integration succeeded and is done:
5
integrationFuture.get();
6
// if no exception is thrown, the integration was successful
Copied!

Retargeting

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.

How retargeting works

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.

Retargeting requirements and configuration

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.
1
public class MainActivity extends SomeActivity {
2
// ...
3
4
@Override
5
protected void onNewIntent(Intent intent) {
6
super.onNewIntent(intent);
7
8
// EITHER:
9
10
// If your app does not make use of the intent returned by `Activity#getIntent()` you can
11
// just set the intent on the activity and the SDK will take care of the rest
12
setIntent(intent);
13
14
// OR:
15
16
// Alternatively you can forward the intent directly to the SDK. In that case you don't need
17
// to call `setIntent` on your activity, but have to forward all future intents like this, too:
18
JustTrackSdk sdk = getSdkInstance(); // get an sdk instance from somewhere in your app...
19
sdk.onNewIntent(intent);
20
}
21
22
// ...
23
}
Copied!
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.

Preliminary retargeting parameters

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.
1
JustTrackSdk sdk = /* get an SDK instance somehow */;
2
PreliminaryRetargetingParameters parameters = sdk.getPreliminaryRetargetingParameters();
3
if (parameters != null) {
4
// your app got started because of a retargeting click:
5
showWelcomeDialog(sdk, parameters);
6
} else {
7
Subscription subscription = sdk.registerPreliminaryRetargetingParametersListener((parameters) -> {
8
// your app was already running in the background and the user clicked on a retargeting campaign
9
showWelcomeDialog(sdk, parameters);
10
});
11
// upon termination of your app:
12
subscription.unsubscribe();
13
}
14
15
void showWelcomeScreen(JustTrackSdk sdk, PreliminaryRetargetingParameters parameters) {
16
navigateTo(welcomeScreen);
17
sdk.toPromise(parameters.validate(), new Promise<PreliminaryRetargetingParameters.ValidateResult, Exception>() {
18
@Override
19
public void resolve(PreliminaryRetargetingParameters.ValidateResult response) {
20
if (response.isValid()) {
21
// the click was valid, you can now persist any rewards you promised
22
persistWelcomeBonus();
23
} else {
24
// it was not valid, handle somehow (here we just navigate to some other screen,
25
// you might want to have some better handling here)
26
navigateTo(mainScreen);
27
}
28
}
29
30
@Override
31
public void reject(@NonNull Exception exception) {
32
// we could not communicate with the JustTrack backend, handle the error somehow
33
handleError(exception);
34
}
35
});
36
}
Copied!
Last modified 1mo ago