The recent releases of the mobile trackers version 1.x have seen a progressive reduction of the feature gap between iOS and Android and an increased reliability of the trackers. With version 2, we want to focus on easier implementation and improved long term extensibility. Furthermore, the architecture and functionality of the mobile trackers was initially very much inspired by web trackers. With the new version we want to characterize their mobile nature, by deprecating web related events and entities/contexts, and developing more meaningful data tracking for the mobile environment.
Our proposal for the new major version is focused on two main areas:
- Revision of the public API
- Mobile focused event tracking methods
Public API revision
Tracker configuration
Currently, the tracker configuration requires the creation of the Emitter component. Optionally, the Subject component can be configured as well. Then both need to be passed to the Tracker component. We want to simplify this process, letting the Tracker component build the Emitter using a basic configuration, and letting the user specify more complex configurations only when needed.
The builder pattern is largely adopted in the tracker for the configuration of the tracker and creation of events. It’s particularly useful in cases where the component has many optional parameters because it helps to reduce the number of different variants of component constructors. Unfortunately, it doesn’t solve the problem of the required arguments, as they need to be declared in the signature of the constructor in order to be checked at compile time. For this reason we will take care to use the builder pattern only for the optional parameters, keeping them in the constructor, favouring compile-time validation of them.
Below an example of configuration in the tracker v.1.x.
Emitter emitter =
new Emitter.EmitterBuilder("collector.snowplow.com", this.getApplicationContext())
.emitTimeout(5)
.customPostPath("postPath")
.byteLimitGet(60000)
.byteLimitPost(60000)
.build();
Subject subject = new Subject.SubjectBuilder()
.context(this.getApplicationContext())
.build();
Tracker.init(
new Tracker.TrackerBuilder(emitter, namespace, appId, this.getApplicationContext())
.base64(false)
.screenviewEvents(true)
.screenContext(true)
...
.installTracking(true)
.applicationContext(false)
.build()
);
The new version 2 will adopt a similar approach using configuration objects and adopting builder patterns when needed.
The tracker configuration also needs to be very straightforward. The simplest setup should only need the strictly required parameters, setting the majority of the optional ones to default values.
// basic configuration (lot of settings are set to default)
Tracker.setup("collector.snowplow.com", HTTPS, POST, namespace, appID);
Similarly, we want to allow a high level of configurability offering plenty of optional parameters. The builder pattern will still be a possible way to configure the tracker as much as the direct assignment of the property parameters.
// using configuration objects (fine granular settings)
NetworkConfiguration network =
new NetworkConfiguration("collector.snowplow.com", HTTPS, POST);
network.timeout = 5;
network.customPostPath = "postPath";
EmitterConfiguration emitter = new EmitterConfiguration();
emitter.byteLimitGet = 60000;
emitter.byteLimitPost = 60000;
TrackerConfiguration tracker =
new TrackerConfiguration(namespace, appId);
tracker.base64 = false;
tracker.screenViewAutotracking = true;
tracker.installAutotracking = true;
...
tracker.applicationContext = true;
tracker.screenContext = true;
Tracker.setup(network, tracker, Arrays.asList(emitter));
The new configuration process completely decouples the configuration of the tracker and its components by their instantiation and consequently implementation.
The tracker setup can take a variable number of configurations. The required ones will be explicitly declared in the signature (as NonNull) forcing compile-time checking. All the optional ones will be grouped in a list.
public void setup(
NetworkConfiguration network,
TrackerConfiguration tracker,
List<Configuration> configurations
);
This new approach leaves at the tracker the burden of creating the components it needs. The developer only has to create the various configuration objects, passing them directly to the tracker setup. It doesn’t need to create an internal component used by the tracker.
However, for very special cases we still allow the injection of external components as implemented in the iOS tracker (v.1.5) and Android tracker (v.1.6) for the NetworkConnection and EventStore components.
Future improvements can also define different configuration objects for different high level features. For example, a configuration for session management.
SessionConfiguration session = new SessionConfiguration();
session.foregroundTimeout = 600;
session.backgroundTimeout = 300;
...
Tracker.setup(this.getApplicationContext(), network, tracker, Arrays.asList(emitter, session))
This has some benefits:
- It lets the tracker delegate the configuration of the features to the features themselves.
- It allows a higher degree of granularity in the configuration of the added features.
- It promotes separation between core and high level features.
- It favoures future extensibility and easier open source contribution.
Event creation
The mobile trackers 1.x require the creation of the event object through a builder before it can be tracked.
Timing event = Timing.builder()
.category("category")
.variable("variable")
.timing(1)
.label("label")
.build());
tracker.track(event);
The proposed solution adopts the same configuration concept explained above for the tracker configuration.
// constructor
Timing event = new Timing("category","variable",1,"label");
tracker.track(event);
// or static factory method
Timing event = Timing.build("category","variable",1,"label");
In this example the constructor (or a static factory method) is needed because all those arguments are required arguments. Also the created event object is just a configuration of the event, hence it doesn’t allow eventID and timestamp overriding.
This solution simplifies the development of custom events as they are essentially plain objects implementing a simple interface or extending SelfDescribing
class.
The custom events are created by the developer based on the schema stored on Iglu, so they need to be simple to write and easy to maintain.
Revised public API
The public API must be the minimum interface that enables the power of all the features in the tracker. For this reason, the public API of the various components can be affected by some breaking changes.
The Tracker
interface will be simplified:
public interface TrackerInterface {
void track(final Event event);
void pause();
void resume();
String getVersion();
void setup(
String uri,
RequestSecurity protocol,
HttpMethod method
String namespace,
String appId);
void setup(
NetworkConfiguration network,
TrackerConfiguration tracker,
List<Configuration> configurations);
void getConfigurations(List<Configuration> configurations);
List<Configuration> getConfigurations();
}
Note: to avoid too much disruption, the current tracker configuration process (adopted on v1.x) will be kept available but deprecated.
All the other specialized methods will be provided through different interfaces specific for each service: Session, Diagnostic, Global Contexts, etc…
Example of tracker configuration and use:
// Setup the tracker
Tracker.setup("collector.snowplow.com", HTTPS, POST, namespace, appID);
...
// Send an event
Tracker.track(Timing.build("category","variable",1,"label"));
...
// Access internal features (e.g. SessionInterface)
Tracker.session.forceNewSession();
Android API improvements
The current codebase has a lack of Nullability annotations in the method signature which can be really helpful for the instrumentation of the tracker on Kotlin or Java based apps.
iOS API improvements
The interoperability between Swift and Objective-C is much worse than between Kotlin and Java. We plan to make the Snowplow iOS tracker version 2.x much more Swift compatible:
- Optional arguments in the API methods
- Swift name specified for all the ObjC methods
- Constants converted to Swift enumeration where possible
Where the Objective-C API can cause higher friction we will implement a Swift wrapper around the tracker library to improve some API interaction with Swift language. It would remove some of the issue faced by Swift developer working on Objective-C API:
- ObjC can’t have generics on protocols - it forces a lot of casts in the code.
- ObjC requires NSObject implementation when a Swift class implements an ObjC protocol.
- The Swift wrapper could be useful to simplify the API using constructs unavailable in Objective-C.
A special note about the management of NSError and NSException in Swift. Much of the errors are managed as exceptions. The v2.x will avoid exception-throwing as much as possible, enforcing the use of NSError, taking advantage of the Swift-ObjC bridging which automatically converts ObjC errors in Swift exceptions.
Mobile focused event tracking
The mobile trackers have grown organically influenced by the design of the web tracker. We want to make them more mobile oriented, bringing forward some improvements already partially implemented, clearing out confusion due to contrasting concepts.
Web events and contexts
We will deprecate events and contexts unuseful in the mobile tracker (to remove in the version 3.x) such as page views and web related fields in subject (more details in the section below).
Also, the geolocation context is partially implemented and considering the further restrictions added to the mobile platforms it’s hard to use it as it has been designed. It will be probably deprecated in favour of a better solution more mobile oriented in one of the next 2.x versions.
Identifiers
The current implementation lets the setting of various user identifiers: user_id, user_ipaddress, domain_userid, network_userid. This is mostly a legacy of the web tracker configuration.
The version 2.x of the tracker will deprecate them in favour of a new set of user/tracker identifiers:
- Default User ID: created by the tracker by default and it can be reset programmatically when the user logout or login. It will help to aggregate the events of the same user in the data model.
- Installation ID: created by the tracker at first execution (it’s the session_userId that can be tracked even if the session context is turned off). The installation ID will be constant until the app deletion.
- Instance ID: created at the app start. It identifies the instance of the tracker. It’s reset at each app restart.
The identifiers used currently in the versions 1.x will not be removed but just be deprecated. However, we encourage the adoption of the new identifiers.
Tracker as singleton
Trackers should work as singleton. If needed, it may be possible to create multiple instances of the tracker in the same app, but the automatic features would work only for one of the tracker instances (the default one). This is because multiple trackers can confuse auto tracking and other services. In general the multiple tracker instantiation is strongly discouraged.
Updated requirements
iOS:
- Bump min supported version to iOS 9 (No changes for the other platforms).
Android:
- Bump min supported version to sdk 16 (Android 4.1+).
- Migrate to AndroidX API.