JustTrack
Search…
iOS SDK Readme
To integrate the JustTrack SDK into your iOS app you first need an API token and have the Appsflyer SDK [1] integrated into your app. Your API token should look like this: prod-57e...64 characters...e542 and is scoped to your bundle identifier.

Instantiate the SDK

The SDK consists of a handful of (public) classes and protocols you can interact with. The main protocol of these is JustTrackSDK which allows you to attribute the current user, send notifications to the backend or record user events. To create an instance of the SDK you have to invoke the JustTrackSDKBuilder class. Instantiating the SDK could look like this:
1
do {
2
let sdk = try JustTrackSDKBuilder(apiToken: "prod-...", trackingId: appsflyerId, trackingProvider: "appsflyer").build()
3
} catch {
4
// apiToken has invalid format...
5
}
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 App 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 call attributeUser to retrieve an attribution containing your JustTrack user id as well as information about the source of the user.
1
sdk.attributeUser().observe(using: { result in
2
switch result {
3
case .failure(let error):
4
// handle error
5
case .success(let response):
6
log("My user id is " + response.getUuid().uuidString)
7
}
8
})
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 observe on the future returned by attributeUser. This will call the provided callback with the Result of the attribution of the user.
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(listener:) to receive the attribution.

Structure of an attribution

The AttributionResponse interface implements the access layer to an attribution on Swift side, but basically an attribution can look like this:
1
{
2
"userId": "9cf85b2b-71c9-491a-a2b4-72478b3eec11",
3
"installId": "23db1b68-6ab9-4287-8c5a-969c51053cfc",
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-ad-set-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-ad-set-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, and adsetId fields will not always be set if you paid for a user.
1
{
2
"userId": "9cf85b2b-71c9-491a-a2b4-72478b3eec11",
3
"installId": "23db1b68-6ab9-4287-8c5a-969c51053cfc",
4
"userType": "acquisition",
5
"campaign": {
6
"id": 31,
7
"name": "ExampleDefaultCampaign",
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 App 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": "23db1b68-6ab9-4287-8c5a-969c51053cfc",
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": "ios"
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
let event = UserEvent("namespace_module_action").build()
2
sdk.publishEvent(event: 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 names with defined semantics for you to make use of. For each event the SDK also documents which values should be supplied as a dimension. For example, to track the navigation from your menu screen to a game screen, you could record the following event:
1
sdk.publishEvent(UserEvent(name: UserEvent.SESSION_SCREEN_SHOWN, dimension1: "game", dimension2: "menu").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
public class UserEvent {
2
public init(_ name: String)
3
public init(name: String, value: Double, unit: Unit)
4
public init(name: String, dimension1: String)
5
public init(name: String, dimension1: String, dimension2: String)
6
public init(name: String, dimension1: String, dimension2: String, dimension3: String)
7
public init(name: String, dimension1: String, dimension2: String, dimension3: String, value: Double, unit: Unit)
8
}
Copied!
value and unit can only always provided together and default to 1 and .count. The three dimensions default to "" if not specified. You can set the properties after initialization also with these methods:
1
public protocol UserEventBuilder {
2
func with(dimension1: String) -> UserEventBuilder
3
func with(dimension2: String) -> UserEventBuilder
4
func with(dimension3: String) -> UserEventBuilder
5
func with(value: Double, unit: Unit) -> UserEventBuilder
6
func with(count: Double) -> UserEventBuilder
7
func with(averageCount: Double) -> UserEventBuilder
8
func with(seconds: Double) -> UserEventBuilder
9
func with(milliseconds: Double) -> UserEventBuilder
10
func with(level: Double) -> UserEventBuilder
11
func with(currency: Double) -> UserEventBuilder
12
13
func build() -> PublishableUserEvent
14
}
Copied!

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
sdk.publishEvent(event: event).observe(using: { result in
2
switch result {
3
case .failure(let error):
4
// event failed to reach our servers
5
case .success(_):
6
// the event was successfully published
7
}
8
})
Copied!

Session tracking

The SDK provides automatic session tracking. This works by wrapping the UIApplicationDelegate set on UIApplication.shared and handling the events relevant for the SDK. If you did set a delegate there already your delegate will still get called as before. If you set another delegate after initializing the SDK you will overwrite the delegate set by the SDK and session tracking will no longer work.

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.

References

Last modified 1mo ago