Getting started

This guide will walk you through the necessary steps to implement the NPOTag SDK, and start sending your first events. All of the code snippets below will be available for each platform SDK: iOS (Swift), Android (Kotlin) and Web (Typescript).

Installation

To install the NPOTag SDK as a dependency of your app or website, simply follow the below steps for your platform:

The web sdk is available via CDN:

<html>
  <head>
    ...
  </head>
  <body>
    // For development:
    <script src="https://npotag.npo-data.nl/prod/npo-tag.js"></script>
    // For production:
    <script src="https://npotag.npo-data.nl/prod/npo-tag.min.js"></script>
    ...
  </body>
</html>

You can also load the script asynchronously. In this case you should wait until the script is loaded before trying to access npotag, as shown below:

<html>
  <head>
    ...
    <script lang="text/javascript">
      let tag;

      // Don't do this, npotag will be undefined at this point
      tag = npotag.newTag(...);

      function onTagReady() {
        // Initialise tag here, npo-tag.min.js will have been loaded and npotag is defined
        tag = npotag.newTag(...);
      }
    </script>
  </head>
  <body>
    <script src="https://npotag.npo-data.nl/prod/npo-tag.min.js" async onload="onTagReady()"></script>
    ...
  </body>
</html>

The web sdk is available as an npm package via a private repository:

If you want to use the package you have to authenticate for this private repository. You need to request an authentication token. This can be done by contacting the Datahub team (datahub@npo.nl)

You can translate the Authentication Token into a READ token that is valid for a short time by running:

UNIQUE_ID=`hostname -f` && curl -XPOST --data "name=${UNIQUE_ID}" https://<Token>@packagecloud.io/install/repositories/npo-data/npotag-v2-web/tokens.text

When you have received your READ token you need to configure npm to use it. You can do this by editing your .npmrc file to hold the following:

always-auth=true
@npotag:registry=https://packagecloud.io/npo-data/npotag-v2-web/npm/
//packagecloud.io/npo-data/npotag-v2-web/npm/:_authToken={PUT_YOUR_TOKEN_HERE}

NOTE: Starting from version 2.0.0 the NPM scope has changed from @npo to @npotag.

Than you can install the package through npm

npm i @npotag/tag

Only installation via Cocoapods is supported for now. Add the following line to your app’s Podfile and run pod install:

pod 'NPOTag', :git => 'https://github.com/npo-topspin/npotag-v2-ios'

If you wish to install a specific version of the SDK instead of the latest available one, use the following:

pod 'NPOTag', :git => 'https://github.com/npo-topspin/npotag-v2-ios', :tag => '1.5.2'
  • Obtain a read token for the PackageCloud android repository by contacting the Datahub Team, ask datahub@npo.nl
  • Add the following dependencies to your build.gradle file:
repositories {
    maven {
        url "https://packagecloud.io/priv/${token}/npo-data/npotag-v2-android/maven2"
    }
}

dependencies {
   implementation "nl.npo.tag.sdk:npo-tag-core:<sdk-version>"
   implementation "nl.npo.tag.sdk:npo-tag-govolte:<sdk-version>" // optional
   implementation "nl.npo.tag.sdk:npo-tag-at-internet:<sdk-version>" // optional
}

Usage

Now that you have the SDK installed on your app or website, let’s get started with configuration and sending some events.

SDK Configuration

The first thing you need to do is to configure the SDK by setting the different global properties that will be sent for each event, select the destinations to which you want to send those events, and optionally enable debug mode for additional informative logging.

You can also disable any functionality related to NMO-DAM measurement by setting disable_nmo_dam to true. This will prevent generation of the NMO ID and make sure it isn’t sent with pageview measurements.

Properties shared between all events are defined in the NPOContext object. Once set, these properties will be immutable. For more information on context objects and implementation details, please refer to the General Information page.

For web sites that share IDs across subdomains, configure cookie_domain on every participating site, including the main site. Read Cross-subdomain session sharing before enabling it on an existing site.

For traditional applications we recommend that you create an instance of NPOTag in the window.onload() function:

<html>
  <head>
    ...
    <script lang="text/javascript">
      window.onload = function() {
        const tag = npotag.newTag(
          // Set properties shared by all events
          {
            brand: 'brand',
            brand_id: 1,
            platform: 'site', // or 'tvapp' for smarttv
            platform_version: '1.2.3',
            disable_nmo_dam: false, // only set this property to true if you will handle NMO DAM measurements yourself
            cookie_domain: '.example.com', // optional: for sharing, use the same value on every participating host
          },
          // Create the tag plugins you want to use
          [
            npotag.newGovoltePlugin(),
            npotag.newATInternetPlugin(),
          ]
        );
      };
    </script>
  </head>
  ...
</html>

For single-page-applications we recommend that you maintain an instance of NPOTag globally. For example in _app.tsx in NextJS:

// _app.tsx
import { newTag, newGovoltePlugin, newATInternetPlugin, NPOTag } from '@npotag/tag';

// NOTE: The result of `newTag` will be undefined if the browser window is unavailable
// (e.g. in server-side rendering)
export const npoTagInstance: NPOTag | undefined = newTag(
  // Set properties shared by all events
  {
    brand: 'brand',
    brand_id: 1,
    platform: 'site', // or 'tvapp' for smarttv
    platform_version: '1.2.3',
    disable_nmo_dam: false, // only set this property to true if you will handle NMO DAM measurements yourself
    cookie_domain: '.example.com', // optional: for sharing, use the same value on every participating host
  },
  // Create the tag plugins you want to use
  [newGovoltePlugin(), newATInternetPlugin()]
);

For iOS we recommend that you setup the SDK in your AppDelegate’s didFinishLaunchingWithOptions method.

func application(_ application: UIApplication, didFinishLaunchingWithOptions ...) -> Bool {

    // ...

    // Set properties shared by all events
    let sharedContext = NPOContext(
        brand: "Product name",
        brandId: 4,
        plaform: "app",
        platformVersion: "1.2.3",
        disable_nmo_dam: false			// Set this to true to disable everything related to NMO DAM
    )

    // Instantiate plugins that you wish to send events with
    let govoltePlugin = GovoltePlugin()
    let atInternetPlugin = ATInternetPlugin()

    // Will create and configure the shared NPOTag instance
    NPOTag.configure(
        withSharedContext: sharedContext,
        plugins: [                  // Pass the plugins you wish to send events with
          govoltePlugin,
          atInternetPlugin
        ],
        isDebugEnabled: false			// Set this to true to enable debug logging
    )

    return true
}

Fluent Builder Pattern is used to create an instance of NpoTag. That means all required parameters have to be provided in strict order and there is no way to call build() before providing all required values.

Instance of NpoTag should be created as soon as possible to allow it to send potential events left in the queue from a previous session.

class App : Application() {
    lateinit var npoTag: NpoTag

    override fun onCreate() {
        super.onCreate()

        npoTag = NpoTag.builder()
            .withContext(this)
            .withBrand(
                brand = "brand",
                brandId = 12345
            )
            .withPlatform(
                platform = "app",
                version = "1.2.3"
            )
            .withPluginsFactory { pluginContext ->
                setOf(
                    LoggerPlugin(pluginContext),
                    GovoltePlugin(
                        pluginContext = pluginContext,
                        baseUrl = "https://topspin.npo.nl/",
                    ),
                    ATInternetPlugin(pluginContext)
                )
            }
            .withEnvironment(EnvironmentType.DEV)
            .withDebug(true)
            .withLogger(logger) // Optional. Provide your own Logger implementation if you want to handle logs yourself
            .withDisabledNmoDam(false)
            .build()
    }
}

Cross-subdomain session sharing (web only)

Web SDK 3.3.2 supports sharing party and session IDs through cookie_domain. Pass the same parent domain, for example cookie_domain: '.example.com', to newTag(...) on every participating host: example.com, www.example.com if used, and each subdomain such as subdomain.example.com. The value must be a domain shared by those hosts, without a protocol or path.

Setting this option only on a subdomain does not change the main site’s configuration. A main site without the option normally continues to use its own localStorage IDs, even when a shared cookie is visible there. Updating the SDK alone does not enable sharing.

Storage and session lifetime

Without cookie_domain, the SDK uses localStorage by default. With the option set and cookies available, these values use cookies on the specified domain:

Cookie nameValue
npotag__dvpParty ID, used across sessions
npotag__dvsSession ID
npotag__dvbBrand ID associated with the session
npotag__dvslSession last-use timestamp

Other SDK storage does not automatically move to shared cookies. The default storage falls back from localStorage to cookies for the current host, then to memory if neither is writable. If cookies are blocked, shared storage is unavailable.

To continue the same session across hosts, use the same brand_id and visit while the session is still valid. A new session is created when the stored session expires after 30 minutes of inactivity, the brand changes, or the last-use time is before midnight in Europe/Amsterdam (CET/CEST). Different brands can share a party ID while having different session IDs.

Migrating existing IDs

For normal newTag(...) initialization with working domain cookies, the party ID is selected in this order:

  1. Use an existing party ID cookie.
  2. If no party ID cookie exists, copy a valid party ID from the current origin’s localStorage into the cookie.
  3. If neither exists, generate a new party ID and store it in the cookie.

A switch to cookie_domain therefore does not reset every existing party ID. If a visitor starts on the main site with a localStorage ID and no shared cookie, that ID is preserved and the subdomain can subsequently use it.

The migration also copies missing session ID, brand ID, and last-use values from localStorage. Existing cookie values take precedence for each key separately. A restored session is still subject to the validity rules above; an existing shared session can replace the main site’s local session. The migration removes the corresponding localStorage entries, including when a cookie already exists.

Migration reads only the current origin’s localStorage. https://example.com, https://www.example.com, and https://subdomain.example.com have separate storage. A subdomain cannot directly retrieve an old main-site ID. If the visitor starts on the subdomain before a shared cookie exists, its own local ID or a newly generated ID becomes the shared ID.

When different IDs already exist

For example, a visitor may have party ID A in the main site’s localStorage and party ID B in a shared cookie previously created on the subdomain. When the main site enables cookie_domain, the SDK selects B, because the cookie takes precedence. Earlier main-site events remain associated with A.

Enabling the setting on every site does not resolve this historical difference. The SDK does not automatically prefer the main-site ID or merge events recorded under different IDs. If the original main-site ID must also be preserved for these visitors, arrange a separate migration that handles the conflict before enabling the option there. The standard migration can remove the old local ID. Linking events already recorded under different IDs also requires a separate solution in the analytics processing.

These migration examples assume cookies can be written. In Web SDK 3.3.2, enabling cookie_domain when cookies are blocked can discard existing localStorage IDs during migration and generate new ones. ID preservation in that situation is a known limitation; cross-subdomain sharing is unavailable while cookies are blocked.

Verify before rollout

Use a supported SDK and the same cookie_domain on every participating host. In a test browser profile, compare tag.getParty() and tag.getSession() and the IDs sent in page-view events. Check the Domain attribute of the npotag__dvp and npotag__dvs cookies in browser developer tools.

Test these cases separately, using existing localStorage values as well as a fresh profile:

Starting state and visit orderExpected party ID
No existing IDs; main site, then subdomainThe main site’s newly generated ID is shared.
Main-site localStorage ID A; no shared cookie; main site, then subdomainA is copied to the shared cookie and used on both sites.
Main-site localStorage ID A; existing shared cookie BB is used, including on the main site. Earlier events under A are not automatically linked.
Main-site localStorage ID A; no shared cookie; subdomain firstThe subdomain uses its own local ID or generates one. That cookie takes precedence on a later main-site visit.

For session checks, keep brand_id the same and stay within the session lifetime. Also verify blocked-cookie behavior. A clean-profile test alone cannot demonstrate that existing IDs will be preserved.

Once the SDK is configured with an NPOContext instance and desired plugins are enabled, you can start sending events.

Party ID

Anonymous user activity across sessions is tracked by means of a party ID. NPOTag restores an existing ID or generates one when needed. On web, it is stored under npotag__dvp in localStorage by default, or in a shared cookie when cookie_domain is configured and cookies are available. See Cross-subdomain session sharing for migration and fallback behavior. You can retrieve the party ID as follows:

<html>
  <head>
    ...
    <script lang="text/javascript">
      window.onload = function() {
        const tag = npotag.newTag(...);
        const partyId = tag.getParty();
      };
    </script>
  </head>
  ...
</html>
const npoTagInstance: NPOTag | undefined = newTag(...);
const partyId = npoTagInstance.getParty();
let partyId = NPOTag.shared.partyID

getPartyId() is a time consuming operation and will block current thread, because it involves reading and writing the PartyId to SharedPreferences. It’s recommended to use coroutines as in the example below to prevent the main thread from blocking.

launch(Dispatchers.IO) {
    val partyId = npoTag.getPartyId()
}

PageTracker and page view events

As mentioned in the General Information page, events are sent via trackers. A tracker is an object responsible for sending events related to a certain context level. For instance, PageTracker will allow you to send events tied to a page, StreamTracker will allow you to send events tied to a video being streamed.

Below is how to create a PageTracker and send page view events tied to a certain page. It will need a PageContext object that will list all of the properties that will be shared by all events sent from this page.

<html>
  <head>
    ...
    <script lang="text/javascript">
      window.onload = function() {
        ...
        // Create a PageTracker object, passing it the NPOTag and a PageContext object
        const tracker = npotag.newPageTracker(tag, {
          page: 'profile_page',
          chapter_1: 'home',
          chapter_2: 'profile',
          // etc...
        });

        // Send a page view event
        tracker.pageView();
      };
    </script>
  </head>
  ...
</html>
// Create a PageTracker object, passing it the NPOTag and a PageContext object
const tracker = newPageTracker(tag, {
  page: 'profile_page',
  chapter_1: 'home',
  chapter_2: 'profile',
  // etc...
});

// Send a page view event
tracker.pageView();
// Create a context object with all property values
// All of the properties are optional
let context = PageContext(
  page: "page",
  chapter1: "chapter1",
  chapter2: "chapter2",
  chapter3: "chapter3",
  broadcasters: "broadcasters",
  program: "program",
  contentContextId: "contextId",
  queryContext: "queryContext",
  condition: "condition",
  errorCode: "errorCode",
  customLabel1: "label1",
  customLabel2: "label2",
  customLabel3: "label3",
  customLabel4: "label4",
  customLabel5: "label5",
  location: "location",
  referrer: "referrer",
  scrollTracking: true
)

// Instantiate the tracker object from the NPOTag shared instance
let tracker = NPOTag.shared.newPageTracker(with: context)

// Send a page view event
tracker.pageView()
// Instantiate the tracker object from the NpoTag instance
val tracker = npoTag.pageTrackerBuilder()
    .withPageName("home")
    .withScrollTracking(true)
    .withContentContextId("homeContextId")
    .withCondition("condition")
    .withQueryContext("query")
    .withErrorCode("errorContext")
    .withBroadcasters("broadcaster")
    .withProgram("program")
    .withChapters(
        chapter1 = "C1",
        chapter2 = "C2",
        chapter3 = "C3"
    )
    .withCustomLabels(
        customLabel1 = "L1",
        customLabel2 = "L2",
        customLabel3 = "L3",
        customLabel4 = "L4",
        customLabel5 = "L5"
    )
    .build()

// Send a page view event
tracker.pageView()

Once created the tracker’s context is immutable, if you need to send events with new properties (because user has switched page for instance), you will need a new tracker instance with a different PageContext. The ways this SDK is intended to be used is that you should have one PageTracker instance per page that is kept alive for the whole lifecycle of the page. Once a new page is reached, a new tracker should be instantiated. See the contexts documentation for a detailed list of properties available for each context class.

Other trackers and events

Trackers other than PageTracker will need to be instantiated from a PageTracker instance because the events they send will also be tied to a page, so the properties from the parent PageContext will be inferred. This is why PageTracker has methods to create other trackers like StreamTracker or RecommendationTracker. For details on creating and using these trackers, see Sending events with trackers.

User properties

User properties in NPOContext (user_id, user_pseudo_id, user_subscription, user_profile, user_profile_type) are not directly accessible by implementors. In order to set values for them they will need to use the login and logout methods exposed by NPOTag like so:

<html>
  <head>
    ...
    <script lang="text/javascript">
      window.onload = function() {
        ...
        // Set custom values for user properties (when a user is logged in)
        tag.login({
          user_profile: 'example',
          user_subscription: 'free',
          user_pseudoid: 'example',
          user_id: 'example',
          user_profile_type: 'kids_6'
        });

        // Reset all properties to their default value (when a user logs out)
        tag.logout();
      };
    </script>
  </head>
  ...
</html>
// Set custom values for user properties (when a user is logged in)
tag.login({
  user_profile: 'example',
  user_subscription: 'free',
  user_pseudoid: 'example',
  user_id: 'example',
  user_profile_type: 'kids_6',
});

// Reset all properties to their default value (when a user logs out)
tag.logout();
// Set custom values for user properties (when a user is logged in)
NPOTag.shared.login(
    userId: "example",
    userProfile: "example",
    userSubscription: "free",
    userPseudoId: "example"
)

// Reset all properties to their default value (when a user logs out)
NPOTag.shared.logout()
// Set custom values for user properties (when a user is logged in)
npoTag.login(
    userId = "example",
    userProfile = "example",
    userSubscription = "free",
    userPseudoId = "example"
)

// Reset all properties to their default value (when a user logs out)
npoTag.logout()

Once set using the login method, user properties will be persisted for the whole platform lifecycle. They will however not be stored in persistent storage so upon a new app start or site opening, these properties will need to be reset.

Export and transfer your session

If you happen to need to transfer sessions between different devices, you will probably need to use the getSessionInfo feature. This function will generate a string containing all the necessary properties to instantiate the new session at the same point as it was. Note that ‘resuming’ a Tag session is only implemented on web, as the primary use case is to resume a session on a Chromecast device.

let serializedSession = tag.getSerializedSessionInfo();
const serializedSession = tag.getSerializedSessionInfo();

On iOS, some properties are retrieved asynchrounsly, therefore the whole functionality is built in such a way with an escaping closure

NPOTag.shared.getSessionInfo { jsonString in
    
    guard let jsonData = jsonString!.data(using: .utf8),
          let dictionary = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else {
        return        // If there's an error, the returned string will be nil
    }
    
    // Default NPOTag properties
    let brand = dictionary["brand"] as? String
    let brandId = dictionary["brandId"] as? Int
    let platform = dictionary["platform"] as? String
    let platformVersion = dictionary["platformVersion"] as? String

    // Govolte plugin properties
    let govoltePartyId = dictionary["govoltePartyId"] as? String
    let sessionId = dictionary["sessionId"] as? String
    let npoSubscription = dictionary["npoSubscription"] as? String
    let npoUserId = dictionary["npoUserId"] as? String
    let npoProfileId = dictionary["npoProfileId"] as? String
    let npoPseudoId = dictionary["npoPseudoId"] as? String
   
    // ATInternet plugin properties
    let atVisitorId = dictionary["atVisitorId"] as? String
    let avSessionId = dictionary["avSessionId"] as? String
                              
    // ...
}

This operation is blocking. Never use it on the main thread. It wont return if you do not call it on a background thread.

val sessionInfoJson = npoTag.getSerializedSessionInfo()

Resuming a session can be done using the Web SDK by calling an alternate constructor newTagFromSession and add the serialized string:

let tag = npotag.newTagFromSession({
  ...,
  serializedSessionInfo: serializedSessionString,
)};
const tag = npotag.newTagFromSession({
  ...,
  serializedSessionInfo: serializedSessionString,
)};