Is there a way I can integrate Google Analytics with the application?

I would like to integrate my app with Google Analytics. How can this be done and what are the best practices?

You can add your Google Analytics code into shared JS, however, there are Platform specific things that we need to take into account. Since we have offline capability, we need to be able to replay some google analytic functions.

We can do this by writing to a DB object if a there is no connection. I have provided some code that we as solutions engineers use in our apps.

<model name="google_analytics" label="SYSTEM: Google Analytics">
        <field name="action" label="Action" type="single-choice">
            <option key="screenview">screenview</option>
            <option key="event">event</option>
        </field>
        <field name="app_env" label="App Environment" type="text" />
        <field name="screenview_name" label="Screeview Name" type="text" />
        <field name="user_reference" label="User Reference" type="text" />
        <field name="created_at" label="Created At" type="datetime" />
        <field name="app_version" label="App Version" type="text" />
        <field name="platform" label="Platform" type="text" />
        <field name="role" label="Role" type="text" />
        <field name="event_category" label="Event Category" type="text" />
        <field name="event_action" label="Event Action" type="text" />
        <field name="event_label" label="Event Label" type="text" />
        <field name="event_value" label="Event Value" type="text" />
        <field name="connection" label="Connection" type="single-choice">
            <option key="Offline">Offline</option>
            <option key="Online">Online</option>
        </field>
        <field name="locale" label="Locale" type="text" />
        <field name="processed" label="Processed" type="boolean" />
        <webhook type="ready" receiver="cloudcode" action="google_analytics" >
            <field name="processed" required="true" embed="true" />
        </webhook>        
        <display>{screenview_name}</display>
</model>

Now that we have the DB schema sorted, we can move on to creating a lib available to all views. We add the code below to Shared JS. There are a couple of things to do and note here:

  1. Note the function sharedCaptureAnalytics and sharedCaptureActionAnalytics. They can be used to capture events and screen navigation's.
  2. You will need to define your app_name and tracking_id for the google analytics lib, this will link the GoogleAnalytics lib to your Google Analytics Account.
/**
 * Track views
 * @param  {string} path Current view path
 */
function sharedCaptureAnalytics(path) {
    GoogleAnalytics.screenview(path);
}

/**
 * Track events
 * @param  {string} category     *required, Typically the object that was interacted with (e.g. 'Video')
 * @param  {string} event_action *required, The type of interaction (e.g. 'play')
 * @param  {string} label        Useful for categorizing events (e.g. 'Fall Campaign')
 * @param  {number} value        A numeric value associated with the event (e.g. 42)
 */
function sharedCaptureActionAnalytics(category, event_action, label, value) {
    GoogleAnalytics.event(category, event_action, label, value);
}

/**
 * Get the current environment
 * @return {string} environment
 */
function sharedGetEnvironment() {
    if (journey.server) {
        if (journey.server.indexOf('testing') > 0) {
            return "testing";
        } else if (journey.server.indexOf('staging') > 0) {
            return "staging"
        } else {
            return "production";
        }
    }
}

/**
 * Google Analytics Wrapper Module
 * @type {Object}
 */
var GoogleAnalytics = {
    version: 1,
    app_name: <MY APP NAME>,
    tracking_id: "<MY ID>",
    url: "https://www.google-analytics.com/collect",
    aid: (function() {
        if (sharedGetEnvironment() === 'production') {
            return "production";
        } else if (sharedGetEnvironment() === 'staging') {
            return "staging";
        } else {
            return "testing";
        }
    })(),
    core: function() {
        var params = [];

        params.push('v=' + this.version);
        params.push('tid=' + this.tracking_id);
        params.push('an=' + this.app_name);
        params.push('av=' + user.current_app_version);
        params.push('aid=' + this.aid);
        params.push('ds=' + journey.platform);
        params.push('ul=' + journey.locale);

        return params;
    },
    screenview: function(screenview_name) {
        var params = this.core(),
            role = user.user_type;
        params.push('cid=' + user.id);
        params.push('t=screenview');
        params.push('uid=' + user.id);
        params.push('cd=' + screenview_name);
        params.push('cd3=' + role);
        params.push('cd4=Online');

        this.send_screenview(params, role, 'screenview', screenview_name)
    },
    event: function(category, event_action, label, value) {
        var params = this.core();
        var role = user.user_type;

        params.push('t=event');
        params.push('uid=' + user.id);
        params.push('cid=' + user.intermediary_agent_code);

        category ? params.push('ec=' + category) : null;
        event_action ? params.push('ea=' + event_action) : null;
        label ? params.push('el=' + label): null;
        value ? params.push('ev=' + value): null;

        this.send_event(params, role, 'event', category, event_action, label, value)
    },
    send_screenview: function(params, role, action, screenview_name) {
        this.send(params).caught(function(error) {
            // Attempt to store it to the DB
            GoogleAnalytics.capture_screenview(action, role, screenview_name);
            return false;
        });
    },
    send_event: function(params, role, action, category, event_action, label, value) {
        this.send(params).caught(function(error) {
            // Attempt to store it to the DB
            GoogleAnalytics.capture_event(action, role, category, event_action, label, value);
            return false;
        });
    },
    send: function(params) {
        var url = this.url;
        var options = {
            body: params.join('&'),
            method: 'POST'
        };
        // No return just go with best effort
        return fetch(url, options)
            .then(function(response) {
            // Parse JSON from the body
            if (response.status === 200) {
                return;
            } else {
                var error = new Error(response.statusText)
                error.response = response
                throw error
            }
        });
    },
    capture: function(action, role, capture_object, capture_type) {
        var ga = DB.google_analytics.create();

        if (capture_type === "screenview") {
            ga.screenview_name = capture_object.screenview_name;
        } else if (capture_type === "event") {
            ga.event_category = capture_object.category;
            ga.event_action = capture_object.event_action;
            ga.event_label = capture_object.label;
            ga.event_value = capture_object.value;
        }
        ga.role = role;
        ga.app_env = this.aid;
        ga.user_reference = user.id;
        ga.created_at = new Date();
        ga.platform = journey.platform;
        ga.app_version = user.current_app_version;
        ga.processed = false;
        ga.connection = 'Offline';
        ga.action = action;
        ga.locale = journey.locale;
        ga.save();
    },
    capture_screenview: function(action, role, screenview_name) {
        // dialog(role)
        var capture_type = "screenview";
        var capture_object = new Object();
        capture_object.screenview_name = screenview_name;
        this.capture(action, role, capture_object, capture_type);
    },
    capture_event: function(action, role, category, event_action, label, value) {
        var capture_type = "event";
        var capture_object = new Object();
        capture_object.category = category;
        capture_object.event_action = event_action;
        capture_object.label = label;
        capture_object.value = value;
        this.capture(action, role, capture_object, capture_type);
    }
}

To use google analytics in a view, you can simply call the Lib screenview function with the current path.

function init() {
    GoogleAnalytics.screenview(view.path);
}

function resume(from) {
    GoogleAnalytics.screenview(view.path);
}

Note that I am adding it to both init and resume to enable the logging of forward and backward navigation. Also keep in mind that you don't want to call init or resume again in the view as this will cause another analytics event.

So we spoke about offline capability and how we can leverage that but still get some useful analytic data. We can use Cloud Code by defining a WebHook trigger that runs once the device syncs the google_analytics objects.

  1. Create a Cloud Code task with the following code.
  2. Update the app_name and tracking_id with the same variables you used in the application.
const config = {
    version: 1,
    app_name: "<APP NAME>",
    url: "https://www.google-analytics.com/collect",
    tracking_id: "<TRACKING ID>",
}

export async function run(webhook) {

    let object;
    if(webhook && webhook.object) {
        object = webhook.object;
    } else if(this.source == CloudCode.EDITOR_TEST && CloudCode.task.env == 'testing') {
        console.log('Testing from CloudCode...');
        object = await DB.google_analytics.first('screenview_name = ?', 'test');
    }

    if(object) {

        await object.reload();

        var _google_analytics = await DB.google_analytics.first(object.id);

        var _url = config.url;

        var _options = {
            body: buildPostData(_google_analytics),
            method: 'POST'
        };

        console.log("Request : ", JSON.stringify(_options));
        var ga_response = await fetch(_url, _options);

        console.log(ga_response.status);
        console.log(ga_response.statusText);

        if(ga_response.status === 200) {
            _google_analytics.processed = true;
            await _google_analytics.save();
            return ga_response.text();
        } else {
            var error = new Error(ga_response.statusText);
            error.ga_response = ga_response;
            throw error;
        }

    }
}

/**
 * Create the message for capturing the analytics event
 * @param  {Object} google_analytics Data Model instance
 * @return {string}                  Param String
 */
function buildPostData(google_analytics) {
    let _params = [];
    var _now = new Date();

    _params.push('tid=' + config.tracking_id);
    _params.push('v=' + config.version);
    _params.push('cid=' + google_analytics.user_reference);
    _params.push('t=' + google_analytics.action);
    _params.push('an=' + config.app_name);
    _params.push('av=' + google_analytics.app_version);
    _params.push('aid=' + google_analytics.app_env);
    _params.push('cd=' + google_analytics.screenview_name);
    _params.push('cd3=' + google_analytics.role);
    _params.push('cd4=' + google_analytics.connection);
    _params.push('ds=' + google_analytics.platform);

    let _durationPassed = Math.ceil(_now.getTime() - google_analytics.created_at.getTime());
    //Google only allows replay of 4 hours
    if(_durationPassed < 14400000) {
        _params.push('qt=' + _durationPassed);
    }

    _params.push('ul=' + google_analytics.locale);

    return _params.join('&');
}

This will close the loop and allow offline Google Analytics in the JourneyApps Platform.

2 Likes