Pulse Event Tracker - Autotracker (Web SDK/CDN build)

Build Status

This package compiles the pulse tracker to a IIFE package that is distributed to our CDN and is intended to be used asynchronously using a short <script> snipped.

If you have a singe page app you can use this package from the Schibsted CDN or optionally use the underlying tracker NPM module directly in your code.

A provider id have to be registered in Bifrost before Pulse can be used. The id you register in Bifrost is the same you input below.

TOC

Quick start

  1. Register a provider id in Bifrost

  2. Add the following script

<script type="application/javascript">
  (function (w, d, n, t, s, a, b) {w[n] = w[n] || function() {
  (window[n].q = window[n].q || []).push(arguments)}; a = d.createElement(t);
  b = document.getElementsByTagName(t)[0];a.async = 1;a.src = s;b.parentNode.insertBefore(a, b)}
  )(window, document, 'pulse', 'script', '//sdk.pulse.schibsted.com/pulse.min.js');

  pulse('init', 'PROVIDER-ID');
  pulse('trackPageView');
  </script>

Usage

What events should be sent?

This tracker is not opinionated on what events should be sent in what scenario and will not do anything automatically. If you are unsure about what events should be tracked, see the tracking guide in Pulse Monitor.

Debugging

Pulse Unicorn

Pulse Unicorn is a Google Chrome extension that allows you to easily see the events being sent and if they are valid or not.

Pulse Monitor

You can tag events to be picked up by Pulse Monitor by adding deployStage = dev|pre and deployTag = something to the root of events:

...
pulse('init', 'pulse-test')
pulse('update', {
  deployStage: 'dev',
  deployTag: 'sdk-dev'
})

Your events will then show up in here after a few seconds. This is handy way to run data quality checks on individual events.

WARNING: Do not deploy a tracker using deployStage: 'dev or deployStage: 'pre' to a production environment. This is both because of limited capacity in Pulse Monitor but also to ensure privacy compliance.

Printing exceptions

All errors and exceptions from Pulse are silently ignored by default. You can enable logging of errors by setting the cookie _pulse2cookie to true:

document.cookie = '_pulse2debug=true';

Example applications

What build type to use and how to load it

There are four builds of Pulse Autotracker. One build is intended for the fastest possible load of Pulse and at the same time the least intrusive to the site. The other build have a slightly better API to work with and with some setup on the site it will be as little intrusive as the other method. More about these two builds below.

Each of those builds comes in two varieties, with and without polyfills for the required modern web APIs. Pulse use the APIs fetch and Promise and these APIs are not ubiquitously available in the current browser marked. The default version of all builds includes the necessary code (polyfills) to deal with older browsers. It is however not necessary for pulse to include the code to patch these APIs if the underlying site already have patched the environment.

A -modern.min.js build is therefor available for sites that have this under control and have made sure that both fetch and Promise is in place before pulse is loaded. This is done at your own risk so if you are unsure about this, use the default version.

Promise API Simpler API / Fast load
All browsers loader.min.js (~3.5kb synchronously) pulse.min.js (~350b synchronously / ~32kb asynchronously)
Modern browsers loader-modern.min.js (~600b synchronously) pulse-modern.min.js (~350b synchronously / ~22kb asynchronously)

Simple API / Fast load

Place the following in the <head> section of the site.

<script type="application/javascript">
  (function (w, d, n, t, s, a, b) {w[n] = w[n] || function() {
  (window[n].q = window[n].q || []).push(arguments)}; a = d.createElement(t);
  b = document.getElementsByTagName(t)[0];a.async = 1;a.src = s;b.parentNode.insertBefore(a, b)}
  )(window, document, 'pulse', 'script', '//sdk.pulse.schibsted.com/pulse.min.js');

  pulse('init', 'PROVIDER-ID');
  pulse('trackPageView');
  </script>

As soon as the first line of the script above have evaluated you will have a global pulse method. You can use this immediately to interact with the Pulse tracker even though the tracker have not downloaded yet. All calls are queued up and executed some time later. You will also see that this version of pulse does not return anything.

pulse('init', 'PROVIDER-ID') => undefined

Getting the SDK instance

You can supply a callback to pulse to get a hold of a SDK instance. Calling pulse with just a function will return the default tracker instance. Note that calling pulse(() => {}) before calling pulse('init',...) will fail.

pulse('init', 'PROVIDER-ID');
pulse(defaultSDK => console.log('this is a SDK instance', defaultSDK))

If you supply a string with is a valid tracker name as the first argument and a callback function as the second you can pick out the tracker instance you want.

pulse('init', 'PROVIDER-ID', null, null, 'tracker1');
pulse('init', 'PROVIDER-ID', null, null, 'tracker2');

pulse('tracker1', tracker => console.log('This is tracker 1', tracker));
pulse('tracker2', tracker => console.log('This is tracker 2', tracker));

Network breakdown

fast loading

In the image about you can see the page being downloaded and immediately after it is done a light blue vertical line is shown in the network breakdown. This means that the page is ready to be drawn and the browser does so. At this point two tracker API calls have been queued and the download of the tracker begins.

This download will not interfere with the page in any way. When the download is done it parses the API call queue created by pulse(...) calls and executes them in order. In this case a single event is sent.

Promise API

Place the following in the <head> section of the site:

<script type="application/javascript" src="//sdk.pulse.schibsted.com/loader.min.js"></script>
<script type="application/javascript">
 pulse('init', 'PROVIDER-ID', { eventDebounce: 2000 } );
 pulse('trackPageView');
</script>

Using the method above will give a "richer" pulse function, where the result of all calls are sent back as Promises and you can get a hold of SDK instances using the pulse method. Here are some examples

pulse('init', 'PROVIDER-ID')
  .then(sdk => console.log('Pulse tracker initialized and here is the instance', sdk),
        () => console.error('Something went awry'))
pulse('trackPageView').then(() => console.log('Event successfully sent to the event receiver'))

You can also use pulse to the a hold of a previously created instance

pulse('init', 'PROVIDER-ID', 'tracker1');
pulse('init', 'PROVIDER-ID', 'tracker2');

// Get the tracker2 instance
pulse('tracker2')
  .then(tracker2 => tracker2.trackPageView())
  .then(() => console.log('Event successfully sent to the event receiver'))

Network breakdown

network breakdown

In this network diagram you can see that the light blue vertical line is drawn after both the page and the loader script have been downloaded. This is because the loader script is included in a synchronous <script> tag. The loading and parsing of the loader script will influence the initial rendering of the page. Note that the absolute time in the diagram is over a emulated GPRS line and you should consider your self if the loading time is acceptable or not.

Modern builds

The builds for modern browsers will be loaded faster but they assume that the browser is either a modern one or that the page have "upgraded" the browser to add the missing APIs. The following APIs are expected:

modern fast load

modern promise api

Using the modern builds on a page that does not have polyfills for the APIs above will not make sense. They are intended for pages or Single Page Apps that already have them in places so it is not necessary to load the polyfills multiple times. It will also be up to the site to ensure that the polyfill code is loaded and evaluated before the tracking code is loaded.

Full API description

Three arguments trailing 'init' is passed to the Pulse tracker constructor and are as following:

  1. providerId, String, required
  2. config, SDKConfigInput, see SDK documentation
  3. builders, Builders, see SDK documentation
  4. trackerName, String

A final fourth argument is a string naming the tracker. This is only used by this asynchronous tracker script and is not passed to the tracker instance.. Multiple trackers can be created by supplying a name to the tracker as a fourth argument. The name of the tracker is prepended to API calls via pulse.

pulse('init', 'PROVIDER-ID', null, null, 'tracker1');
pulse('init', 'PROVIDER-ID', null, null, 'tracker2');

pulse('tracker1.trackPageView');
pulse('tracker2.track')

Fields and values populated automatically

Each event type have their own set of default values based on the assumption of the environment they are used. Any default will be overridden either by setting a field permanently in the tracker (pulse('update', { field: 'value' })) or by passing data to individual events (pulse('track', 'trackerEvent', { field: 'value'})).

trackerEvent

This is the default event type for the web build of pulse and includes the following data by default. Any of these fields can be overwritten permanently in the tracker instance or on a per event basis.

pulse('track', 'trackerEvent', { type: 'View' }) || pulse('trackPageView') =>

{
  "device": { // https://github.schibsted.io/spt-dataanalytics/pulse-event-builders/blob/master/src/device-defaults-browser.js
    "@type": "Device",
    "environmentId": "sdrn:schibsted:environment:573e1b27-3d67-4a95-a1c7-28b93364097e",
    "acceptLanguage": "en-US",
    "screenSize": "2560x1440",
    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.109 Safari/537.36",
    "deviceType": "desktop",
    "viewportSize": "724x1306",
    "localStorageEnabled": true,
    "jweIds": "eyJpc3N1ZWRBdCI6IjIwMTctMTAtMThUMTM6MjVaIiwiZW5jIjoiQTEyOENCQy1IUzI1NiIsImFsZyI6ImRpciIsImtpZCI6IjIifQ..DHSFbETg93AXmXT1BVUF_w.KS-s2mZvS1H0euSgfD14F4FqGsEubNJPgT6bXrGhXWWpozKjlZuHrXg-M9d3hw6su8kt1SukZjBl9onD2qikUAqyMGN2qNtZMkHnePDzmHAuMPFT_cOZ9FAvveW8DJMic3YNW6bF77ldd37MsWqAd6iX4Df2DflH_vFgLrlocsA-n9O2UnEeh0_MUnY5gz-2-wt5OQxds_QUkxJB38dXXUBGZsDfyHVDBbi_lgoJoRw.AnceNWrn4-g63J60-XBVmg"
  },
  "object": { // https://github.schibsted.io/spt-dataanalytics/pulse-event-builders/blob/master/src/object-defaults-browser.js
    "@id": "sdrn:pulse-test:content:http://localhost:8080/demopages/fast-load.html",
    "@type": "Content",
    "name": "Test site for Pulse Autotracker",
    "url": "http://localhost:8080/demopages/fast-load.html"
  },
  "origin": { // https://github.schibsted.io/spt-dataanalytics/pulse-event-builders/blob/master/src/origin-defaults-browser.js
    "url": "http://other-page.com"
  },
  "provider": { // Gets the provider id from the required parameter in 'init' / tracker constructor
    "@type": "Organization",
    "@id": "sdrn:schibsted:client:pulse-test"
  },
  "tracker": { // https://github.schibsted.io/spt-dataanalytics/pulse-event-builders/blob/master/src/tracker-defaults-browser.js
    "name": "Pulse Node.js SDK",
    "type": "JS",
    "eventBuilderVersion": "1.0.11",
    "version": "4.0.6"
  },
  "@id": "7118cc69-0131-45dc-9391-a3cacbd9bcb8", // Generated per event
  "@type": "View", // Default in 'tracker.trackPageView', have to be
  "pageViewId": "b4040ce6-6855-42d4-81f5-02f0d4064d82", // Updated with `tracker.newPageView` or `tracker.trackPageView`
  "creationDate": "2017-10-25T11:22:45.564Z", // Calculated as late as possible before sending
  "schema": "http://schema.schibsted.com/events/tracker-event.json/107.json"
}

routableEvent

This event is primarily intended to be used in Node environments where there is not much default state to collect.

pulse('track', 'routableEvent') =>
{
  "provider": {
    "@type": "Organization",
    "@id": "sdrn:schibsted:client:pulse-test"
  },
  "tracker": {
    "version": "4.0.6"
  },
  "@id": "499fd621-03df-464b-b15e-7d00c04d222c",
  "creationDate": "2017-10-25T11:32:30.324Z",
  "schema": "http://schema.schibsted.com/events/base-routable-event.json/17.json#"
}

Setting persistent state in the tracker

A common case of is that some piece of information should be attached to all events, e.g the id of a logged in user. This can be done like so:

pulse('update', { actor: { id: 'person@example.org', realm: 'spid.se' }});

And to log out:

pulse('update', { actor: undefined });

See the API documentation of @spt-tracking/pulse-event-builders for a full list of inputs to different event types.

Tracker API

See pulse-sdk-js for a full overview of the tracker api.

Plugins

Plugins can be provided and instantiated via the pulse queue.

Writing plugins

The example below show a trival plugin

/* global pulse */
class HelloWorldPlugin {
  constructor(tracker) {
    tracker.track('trackerEvent', {
      object: {
        custom: {
          'sendt_from_plugin': true
        }
      }
    });
  }
  ping() {
    console.log('ping');
  }
}

pulse('provide', 'helloWorld',  HelloWorldPlugin)

Using plugins

<script type="application/javascript">
pulse('require', 'helloWorld');
</script>

<script  type="application/javascript" src="path/to/hello-world-plugin.js" async></script>

The plugin will be instantiated using new and access to the plugin instance can be done via pulse queue like so:

pulse('defaultTracker.helloWorld.ping');

Engage plugins

Tracking guide

This sections deals with what events should be sent in different scenarios, not how. This package is not suitable for Node.js, see the package pulse-sdk-js for further reading. For a comprehensive guide on what values should be tracked in what situations, see this page.

Tracking logged in users

Tell pulse that a user have logged in by updating the actor key:

hypoteticalSPiDAPI().then(userId => {
    pulse('update', {
        actor: {
            id: userId,
            // 'schibsted.com' for the Swedish SPiD installation, 'spid.no' for the
            // Norwegian or the domain of the site if spid is not used, e.g 'subito.it'
            realm: 'spid-realm'
        }
    });
});

Alternatively you can block events from being sent until the user id is resolved:

// This will block events from being sent until the promise from spidAPI() resolves
// Note that you have to handle both success and error when using promises,
// if you do not a unhandled rejected promise will block events forever!

pulse('update', {
    actor: hypoteticalSPiDAPI().then(id => ({ id: id, realm: 'spid-realm' }), error => (undefined))
})

You can log out the user by setting the actor key to undefined:

pulse('update', { actor: undefined });

Media / Publishing objects

Online publishing have a couple of objects that cover most sites: Article, Frontpage and SalesPoster. The correct object type should be set in pulse:

pulse('update', { object: { type: 'Article' } });
pulse('update', { object: { type: 'Frontpage' } });
pulse('update', { object: { type: 'SalesPoster' } });

Marketplaces objects

Marketplaces have two main object types: ClassifiedAd and Listing (search result).

pulse('update', {
  object: {
    type: 'ClassifiedAd',
    id: '..ad-id..',
    category: 'car',
    name: 'Audi A4'
  }
});
pulse('update', {
  object: {
    type: 'Listing',
    id: 'search-query-string',
    query: 'search-query-string',
    items: [{
      type: 'ClassifiedAd',
      category: 'car',
      name: 'Audi A4'
    }]
  }
});

Engage

Engage requires many specific fields to be added to normal page view events. Some of these fields can be populated by Engage plugins and some have to be provided by the site directly. Below are two examples of the two most typical scenarios, Front page views with visibility tracking of DOM elements and Article views with following activity "pings" that measures how long users stay on a page.

In the examples below a named tracker is created and used. This means it will not influence the default tracker instance. Weather or not this is desirable will wary.

The two first plugins enabled are Local (Storage) History and Engage default values. Please see the repective repos if the default values are appoproiate for your site.

Article view

<script>
pulse('init', 'provider-id', null, null, 'engage-tracker');

pulse('engage-tracker.update', {
  object: {
    type: 'Article',
    custom: {
      'spt:updated': '2017-10-19T13:15:36.026Z',
      'spt:authors': ['Name Nameson'],
      'spt:wordCount': 2300,
      'spt:imageUrl': 'http://example.org/img.jpg',
      'spt:shareUrl': {
        facebook: 'http://example.org/page',
        twitter: 'http://example.org/page'
      },
      'spt:previewUrl': {
        http: 'http://example.org/page',
        https: 'https://example.org/page'
      }
    }
  }
});

pulse('engage-tracker.require', 'localHistory');
pulse('engage-tracker.require', 'populateDefaultValues');
pulse('engage-tracker.trackPageView');
pulse('engage-tracker.require', 'engagementTime')
</script>
<script src="//sdk.pulse.schibsted.com/plugins/activity-pings/plugin.js" async></script>
<script src="//sdk.pulse.schibsted.com/plugins/local-history/plugin.js" async></script>
<script src="//sdk.pulse.schibsted.com/plugins/populate-default-engage-values/plugin.js" async></script>

Front page view

<script>
pulse('init', 'provider-id', null, null, 'engage-tracker');

pulse('engage-tracker.update', {
  object: {
    type: 'Frontpage',
    custom: {
      'spt:imageUrl': 'http://example.org/img.jpg',
      'spt:shareUrl': {
        facebook: 'http://example.org/page',
        twitter: 'http://example.org/page'
      },
      'spt:previewUrl': {
        http: 'http://example.org/page',
        https: 'https://example.org/page'
      }
    }
  }
});

pulse('engage-tracker.require', 'localHistory');
pulse('engage-tracker.require', 'populateDefaultValues');
pulse('engage-tracker.trackPageView');
pulse('engage-tracker.require', 'visibilityTracking');
</script>
<script src="//sdk.pulse.schibsted.com/plugins/visibility-tracking/plugin.js" async></script>
<script src="//sdk.pulse.schibsted.com/plugins/local-history/plugin.js" async></script>
<script src="//sdk.pulse.schibsted.com/plugins/populate-default-engage-values/plugin.js" async></script>

Tracking DOM elements (Clicks / Views)

Tracking clicks and views of DOM elements requires a bit of extra effort because of the schema describing UIElements. The object id requires both the type and of the context and the id if the element, e.g sdrn:aftenposten:frontpage:Forsiden:element:qnp89z. The builder that formats the object type UIElement expects there is a target as a part of the event. The type and id of the target will be included in the id of the UIElement. This can be tracked in the following way:

pulse('track', 'engagementEvent', {
  action: 'Click',
  object: {
    type: 'UIElement',
    id: 'button-x'
  },
  target: {
    type: 'Article',
    id: 'article-id-123',
    url: 'http://edxample.org'
  }
})

Instead of manually specifying the target you can pull out the default object from the tracker and set it as target. This can be done in the following way:

pulse(tracker => {
  tracker.evaluateEventInputs().then(eventDefaults => {
    tracker.track('engagementEvent', {
      action: 'Click',
      object: {
        type: 'UIElement',
        id: 'exit-link',
        name: `Exit link`
      },
      // Setting target to the current page object
      target: eventDefaults.object
    })
  });
});

Tracking 'outbound clicks'

The tracker supports using the Beacon API in modern browsers to send events after a user have left the site. This is done automatically behind the scenes and can be disabled by passing useBeaconWhenAvailable: false to the tracker config.

Actually generating the event on the click is however not done automatically. See beacon-exit.html for a full example. A summarized example:

<script>
    function sendClickEvent() {
        pulse('track', 'engagementEvent', {
            type: 'Engagement',
            action: 'Click',
            object: {
              type: 'UIElement',
              id: 'exit-link',
            }
        });
</script>


<a onClick="sendClickEvent()" href="http://other-domain.com">Leave Site</a>