How to integrate Metricalp with mobile applications

Since we released Metricalp, everyone always asked about mobile platform support. They were right, mobile sourced browsers + direct mobile app is suppressing the web platform. So, we are happy to announce that we are in the roadmap of mobile platform support. We have official SDKs for React Native, Android and iOS at the moment.

You may ask that, how is it possible using Metricalp with mobile app while there is not official library yet. Answer is simple, because Metricalp has designed fully in API-centric way. So, actually creating a Metricalp event is just a HTTP POST request. This is the way in web, mobile or any other platform. Under the hood, all integration libraries use this method, just sending a HTTP POST request. So, you can use Metricalp even with your smart washing machine if you want from day zero. Here the event HTTP POST request which used in React Native library as an example, then we will describe every detail of it below:

tsx
   fetch(attributes.endpoint || Metricalp.API_ENDPOINT, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            type,
            path: attributes.path || '(not-set)',
            metr_collected_via: attributes.platform,
            metr_os_detail: attributes.os || '(not-set)',
            metr_app_detail: attributes.app || '(not-set)',
            metr_user_language: attributes.language || '(not-set)',
            metr_unique_identifier: attributes.uuid || '',
            metr_bypass_ip: attributes.bypassIpUniqueness || false,
            tid: attributes.tid,
          })

This is all. You can fully use Metricalp with triggering above HTTP POST request. Let us explain details and tricks independently platform. We will also prepare examples platform specific and finally we will release platform specific SDKs for all platforms. But until that, we are writing this doc to allow you to use Metricalp in your mobile app from now. Also, you can be more flexible with this pure HTTP POST request approach without a SDK. Okay, Let's start with the details.

Think that, you will trigger a HTTP POST request again and again in your application. This request will have some default props like your tracker id (tid). You will also define some props on every call. What would you do? In programming, to protect DRY (Don't Repeat Yourself) principle, you will create a function or a class or a module to handle this HTTP POST request. Here, we will do this. We will create a singleton class. It will keep default props, and it will have a method to trigger the HTTP POST request. I will put React Native library singleton class here and I will explain through it. You can follow same approach in Java,Kotlin (Android), Objective-C,Swift (iOS) or with any platform.

tsx
export interface ConfigurationAttributes {
  tid: string;
  platform: string;
  uuid: string;
  os?: string;
  app?: string;
  language?: string;
  endpoint?: string;
  bypassIpUniqueness?: boolean;
}
export class Metricalp {
  private static instance: Metricalp;
  private static API_ENDPOINT = 'https://event.metricalp.com';
  private attributes: ConfigurationAttributes | undefined = undefined;
  private screenDurationStartPoint = Date.now();
  private currentScreen = '';

  private constructor() {}

  public static getOrBuildInstance(): Metricalp {
    if (!Metricalp.instance) {
      Metricalp.instance = new Metricalp();
    }

    return Metricalp.instance;
  }

  public static init(
    attributes: ConfigurationAttributes,
    initialScreen?: string,
    eventAttributes: Record<string, any> = {}
  ) {
    const instance = Metricalp.getOrBuildInstance();
    instance.setAttributes(attributes);
    if (!initialScreen) {
      return Promise.resolve(true);
    }

    return Metricalp.screenViewEvent(initialScreen, eventAttributes);
  }

  public static getInstance() {
    if (!Metricalp.instance) {
      throw new Error(
        'Metricalp not initialized, please call Metricalp.init() first.'
      );
    }
    return Metricalp.instance;
  }

  public setAttributes(attributes: ConfigurationAttributes) {
    this.attributes = attributes;
  }

  public getAttributes() {
    return this.attributes;
  }

  public getCurrentScreen() {
    return this.currentScreen;
  }

  public setCurrentScreen(screen: string) {
    this.currentScreen = screen;
  }

  public setScreenDurationStartPointToNow() {
    this.screenDurationStartPoint = Date.now();
  }

  public static resetAttributes(attributes: ConfigurationAttributes) {
    const instance = Metricalp.getInstance();
    instance.setAttributes(attributes);
  }

  public static updateAttributes(attributes: ConfigurationAttributes) {
    const instance = Metricalp.getInstance();
    const currentAttributes = instance.getAttributes();
    instance.setAttributes({ ...currentAttributes, ...attributes });
  }

  public static getAllAttributes() {
    const instance = Metricalp.getInstance();
    return instance.getAttributes();
  }

  public static sendEvent(
    type: string,
    eventAttributes: Record<string, any>,
    overrideConfigurationAttributes: Partial<ConfigurationAttributes> = {}
  ) {
    const instance = Metricalp.getInstance();
    const attributes = {
      ...instance.getAttributes(),
      ...overrideConfigurationAttributes,
    };

    if (!attributes.tid) {
      throw new Error('Metricalp Error: tid not set in Metricalp attributes.');
    }

    if (!attributes.platform) {
      throw new Error(
        'Metricalp Error: platform not set in Metricalp attributes.'
      );
    }

    if (!attributes.uuid) {
      throw new Error('Metricalp Error: uuid not set in Metricalp attributes.');
    }

    return fetch(attributes.endpoint || Metricalp.API_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        ...eventAttributes,
        type,
        path: eventAttributes.path || '(not-set)',
        metr_os_detail: attributes.os || '(not-set)',
        metr_app_detail: attributes.app || '(not-set)',
        metr_user_language: attributes.language || 'unknown-unknown',
        metr_bypass_ip: attributes.bypassIpUniqueness ?? true,
        metr_collected_via: attributes.platform,
        metr_unique_identifier: attributes.uuid,
        tid: attributes.tid,
      }),
    }).then((response) => {
      if (!response.ok) {
        return false;
      }
      return true;
    });
  }

  public static screenViewEvent(
    path: string,
    eventAttributes: Record<string, any> = {},
    overrideConfigurationAttributes: Partial<ConfigurationAttributes> = {}
  ) {
    const instance = Metricalp.getInstance();
    const prevScreen = instance.getCurrentScreen();
    let screenLeaveProps = {};
    if (prevScreen) {
      screenLeaveProps = {
        leave_from_path: prevScreen,
        leave_from_duration: Date.now() - instance.screenDurationStartPoint,
      };
    }
    instance.setCurrentScreen(path);
    instance.setScreenDurationStartPointToNow();
    return Metricalp.sendEvent(
      'screen_view',
      { path, ...screenLeaveProps, ...eventAttributes },
      overrideConfigurationAttributes
    );
  }

  public static appLeaveEvent(
    eventAttributes: Record<string, any> = {},
    overrideConfigurationAttributes: Partial<ConfigurationAttributes> = {}
  ) {
    const instance = Metricalp.getInstance();
    const prevPath = instance.getCurrentScreen();
    // You can not trigger leave event without a screen view event before it
    if (!prevPath) {
      return Promise.resolve(false);
    }
    const screenDuration = Date.now() - instance.screenDurationStartPoint;
    instance.setScreenDurationStartPointToNow();
    instance.setCurrentScreen('');
    return Metricalp.sendEvent(
      'screen_leave',
      {
        path: prevPath,
        screen_duration: screenDuration,
        ...eventAttributes,
      },
      overrideConfigurationAttributes
    );
  }

  // @deprecated No more manual session exit event
  public static sessionExitEvent(
    path: string,
    eventAttributes: Record<string, any> = {},
    overrideConfigurationAttributes: Partial<ConfigurationAttributes> = {}
  ) {
    return Metricalp.sendEvent(
      'session_exit',
      { path, ...eventAttributes },
      overrideConfigurationAttributes
    );
  }

  public static customEvent(
    type: string,
    eventAttributes: Record<string, any> = {},
    overrideConfigurationAttributes: Partial<ConfigurationAttributes> = {}
  ) {
    return Metricalp.sendEvent(
      type,
      eventAttributes,
      overrideConfigurationAttributes
    );
  }
}
 

This is all. The Metricalp, React Native library is single file with a few lines. I meant that you can create your own in platform, not a huge thing. Do not forget that in the final it is just HTTP POST request.

First of first, we defined our class as singleton. Because we will define default props on init and we do not want to have overriden on them. These are more about programming concepts, not directly about Metricalp but good to mention here 😎

We have some static properties, instance (because nature of singleton), API_ENDPOINT and attributes. API_ENDPOINT is the Metricalp event endpoint. https://event.metricalp.com is the default value always. But, we will allow the user to override it in our methods if needed (proxied usage of Metricalp to pass ad blockers etc).

attributes is an object (map) to keep default properties. For example, tracker id (tid), current operating system info, app version and name info etc. We will send these data with every event. So, we will define them once, keep in attributes and use them in every event to protect DRY principle. But some attributes need to be defined on every request (like current path/screen). We will provide them on every request. Lets look all posible attributes and their purposes for a request:

type (required)

You should provide event type for every event. While Metricalp is an event based analytics tool, it is required and you should provide type for every request. For example, you can provide it as screen_view or any_custom_event_name etc.

path

You can provide current screen or path while triggering an event. It is optional but suggested to get better data in dashboard. For example HomePage or SettingsPage etc.

metr_collected_via (required)

You need to provide current platform. Possible values are ios, android, web or api. All must be lowercase. In future more platforms will be supported. This can be defined once on init and keep in attributes. Web means collecting through browser (requires user agent string in the headers to info extraction) and api means server/backend produced events.

metr_os_detail

You can provide user's current operating system and version. You can provide it as OSName OSVersion syntax for example iOS 14.5 or Android 11. It is optional but certainly suggested. This can be defined once on init and keep in attributes.

metr_app_detail

You should provide your app name and version. It is important to provide version to track it in dashboard to get detailed insights. You can provide it as AppName@Version syntax for example MyExampleApp@1.0.0. It is optional but certainly suggested. This can be defined once on init and keep in attributes.

metr_user_language

You should provide your current app language. You can provide it as Language (long name)-Country(ISO Code) syntaxfor example English-US or Spanish-ES orTurkish-TR etc. If you are not sure about country, you can provide unknown. For example English-unknown. This prop can be defined once on init and keep in attributes.

metr_unique_identifier

We need to explaing this metr_unique_identifier prop. Normally in Web tracking, Metricalp is not using cookies as we mentioned in many places. We are identifying users with their IP addresses in one-way hashes. The formula is something like this: hash(user_ip + user_agent + salt). Here ip + user_agent is almost unique for every user. But in mobile apps, user agent strings are inconsistent and not reliable. So we need to have a unique identifier for every user. We left this unique identifier to you. You can use any unique identifier for your app. For example if it is an authorization app and you are tracking only after users logged-in, then you can provide user ids. Or you can use device related real unique identifiers (Android and iOS has some native methods to get it). Or, you can generate a random UUID when user first opens the app and store it in storage and then use it as metr_unique_identifier. Now, the hashing algorithm will be like hash(user_ip + metr_unique_identifier + salt). In this approach, the metr_unique_identifier will be unchanged unless user removes and re-installs the app. But that is fair enough. If it is not enough for you, you can provide your own unique identifier as we mentioned above. User ID or device identifier or any other combination.

metr_bypass_ip

This is an optional prop. If you see above final hash example with metr_unique_identifier: hash(user_ip + metr_unique_identifier + salt), we still have IP in hash function. So, when user for example in Wifi and then changed to mobile network, then IP will be changed. While metr_unique_identifier is same, because of IP change, it will count as another unique count for that day. But, you can by pass this IP uniqueness with this prop. If you set it as true, then we will not use IP in hash function: hash(metr_unique_identifier + salt). We set this as true as default unless developer sets it as falsy.

tid (required)

You should provide your TID (a.k.a Tracking ID). You can get it from Metricalp dashboard. It is required. If you don't provide it, you will get an error. You can find your tid in Embed & Share Tracker page.

[any custom props]

You can provide any custom prop (custom_prop1, custom_prop2...) with every event. If you have defined aliases then you can use them as key for example user_id, theme etc. You can check Custom Event & Props for detailed info about custom events and props.

Methods of Class

getOrBuildInstance()

This is a static method to get current instance of Metricalp. If there is not any instance, it will create a new one and return it. It comes from nature of singleton.

init(attributes, initialScreen)

Metricalp.init is a static method which you should call once in your app. It should be top level of your app. You will set initial attributes in here. It takes path(screen) as second argument to set in auto triggered first scren_view event just after initialization. If you omit this path arguement, this auto trigger will not happen. So, we gave a chance to user if they want to skip this first auto screen_view event.

getInstance()

Metricalp.getInstance is a static method which you can call to get current instance of Metricalp. It is useful to get our singleton object. If there is not build one, this method will throw an error.

setAttributes(attributes)

setAttributes is a method to set attributes. We are using this instance method inside of other static methods after getting our instance.

getAttributes()

getAttributes is a method to get attributes. We are using this instance method inside of other static methods after getting our instance.

resetAttributes(attributes)

resetAttributes is a static method to reset attributes. It is useful to reset attributes to a provided initial state. It takes new attributes as argument.

updateAttributes(attributes)

updateAttributes is a static method to update attributes. It is useful to update attributes partially with new ones. It takes new partial attributes as argument.

getAllAttributes()

getAllAttributes is a static method to get all attributes. It is useful to get all attributes at once.

sendEvent(type, eventAttributes, overrideAttributes)

sendEvent is a static method to send an event. It takes type of event, event attributes and override attributes (to override initial default attributes) as arguments. It will combine attributes and send event with them. It will return a promise which resolves with true if event is sent successfully, otherwise resolves with false. We will not directly use this method in our app, we will use screenViewEvent and customEvent methods which are wrappers of this method.

screenViewEvent(path, eventAttributes, overrideAttributes)

screenViewEvent is a static method to send a screen_view event. It takes path of screen, event attributes and override attributes (to override initial default attributes) as arguments. It will combine attributes and send event with them. It will return a promise which resolves with true if event is sent successfully, otherwise resolves with false.

appLeaveEvent(eventAttributes, overrideAttributes)

appLeaveEvents is a static method to send a screen_leave events. Normally when user navigates one screen to another, library automatically creates screen_leave events between two screen_view events. But on app leaves (user closes app or app goes to background) you should trigger it. So, we provided a method to trigger on app leaves which name is appLeaveEvent. It takes event attributes and override attributes (to override initial default attributes) as arguments. It will combine attributes and send event with them. It will return a promise which resolves with true if event is sent successfully, otherwise resolves with false.

customEvent(type, eventAttributes, overrideAttributes)

customEvent is a static method to send a custom event. It takes type of event, event attributes and override attributes (to override initial default attributes) as arguments. It will combine attributes and send event with them. It will return a promise which resolves with true if event is sent successfully, otherwise resolves with false.

This is all. You can follow this approach and use Metricalp in any platform. Let us explain details and tricks independently platform. If you are a React Native developer, you can use official library without any effort. For other platforms, you need to follow this approach until we release official SDKs. Also, we are open contributions for SDKs with above approach. We are proud that we are API-centric and we are not web-only tool. We care all customers and we are doing this still in most affordable way.