Migration from legacy tag

When you are currently using the legacy version of our tag and you want to upgrade to the new version? This is the document for you. This migration document only holds for migrating from the legacy tag to version 1.0.0 of the new tag.

Let’s start with some general changes:

  • The codebases have been unified over all the platforms. Web, Android and iOS have the same implementation methods.
  • The language of the fields has been changed to english, to make everything more cohesive and in line with the data collection.
  • No NPOLabels object anymore that you have to manage yourself. Everything is managed through factory functions and object methods.
  • Tracking has been made easier with the introduction of Tracker objects
  • All the fields are still the same (plus some additional ones), but the language, how you initialise the tag and call functions has been changed.

Initialise the tag

The idea of plugins is still the same. If you want to use only Govolte, only configure that plugin etc.

The creation of the NPOtag object has changed. In the legacy tag you created a empty tag object and added the context labels afterwards. So if you forgot to set the context labels, the events would fail. In the new tag you call the appropriate factory/builder function and add the context labels there on creation of the object.

And the page/stream/recommendation specific context labels are moved to there own tracker objects. The page tracker object can be created using the npotag object. Then from the pagetracker object you can create a streamtracker and a recommendationtracker object. All these tracker objects contain functions to send events.

The setting and clearing of user ids is now done through login and logout functions on the npoTag instead of setUser and clearUser. For platforms that have integrated the new NPO-ID a new field pseudo_id is present on the login function.

Take a look at the new Tag creation and pageTracker object first and see how you set it up. Then come back here.

Mapping

As said before, alle the fields are still there from the old tag context labels. They are only mapped differently over the top level object and the new pageTracker object.

legacy labelnew fieldcontext objectfield typerequiredcomments
merkbrandnpoTagstringtruebrand of the platform the tag is implemented in (e.g npostart)
merkIdbrand_idnpoTaginttrueinteger associated with the brand (ask publieks-onderzoek)
platformplatformnpoTagstringtruesite for website, app for iOS/Android, tvapp for SmartTVs
platformVersieplatform_versionnpoTagstringtrueversion of the platform
omgevingenvironmentnpoTagstringfalseindicator for prod/dev/stag, note that environments should also be differentiated by brandId
gebruiker.npoIduser_idnpoTagstringfalseid associated with the currently logged in user (set by login, logout functions)
gebruiker.profielIduser_profilenpoTagstringfalseid associated with the currently selected profile (set by login, logout functions)
gebruiker.npoTypeuser_subscriptionnpoTagstringfalsesubscription type othe the currently logged in user (set by login, logout functions)
– (new)user_pseudoidnpoTagstringfalsenew ID type that will be used by platforms leveraging the new NPO-ID (set by login, logout functions)
niveau1chapter_1pageTrackerstringfalsethe top-level context (follow tag-plan)
niveau2chapter_2pageTrackerstringfalsea narrower context (follow tag-plan)
niveau3chapter_3pageTrackerstringfalsemost specific context (follow tag-plan)
paginapagepageTrackerstringfalsethe page or view that is displayed
zoek.termquery_contextpageTrackerstringfalsethe query that generated the result page a user is on
omroeplabel1custom_label1pageTrackerstringfalsefree-form string variables for backward compatibility
omroeplabel2custom_label2pageTrackerstringfalsefree-form string variables for backward compatibility
omroeplabel3custom_label3pageTrackerstringfalsefree-form string variables for backward compatibility
omroeplabel4custom_label4pageTrackerstringfalsefree-form string variables for backward compatibility
omroeplabel5custom_label5pageTrackerstringfalsefree-form string variables for backward compatibility
omroepbroadcasterspageTrackerstringfalsea string value, where implementers fill in a `_\
programmaprogrampageTrackerstringfalselowercased, whitespace stripped name of a program on the page, filled in by implementers
was a function on appslocationpageTrackerstringfalsecurrent page url
was a function on appsreferrerpageTrackerstringfalseurl of the page where you came from
– (new)content_context_idpageTrackerstringfalseseparate field for things that used to be stored in the ‘page’
– (new)conditionpageTrackerstringfalseA/B condition indicator
– (new)error_codepageTrackerstringfalsearbitrary error code when reaching an error page
nobo.EventType–(deprecated)pageTrackerstringfalseold type of event declaration for NOBO panel, no longer used
nobo.mediaType–(deprecated)pageTrackerstringfalseold type of media on page for NOBO panel, no longer used
merkType–(deprecated)pageTrackerstringfalseold indicator of the type of brand, no longer used

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: 'web',
            platform_version: '1.2.3',
          },
          // 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: 'web',
    platform_version: '1.2.3',
  },
  // 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: "iOS",
        platformVersion: "1.2.0"
    )

    // 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 = "platform",
                version = "platformVersion"
            )
            .withPluginsFactory { pluginContext ->
                setOf(
                    LoggerPlugin(pluginContext),
                    GovoltePlugin(
                        pluginContext = pluginContext,
                        baseUrl = "https://topspin.npo.nl/",
                    ),
                    ATInternetPlugin(pluginContext)
                )
            }
            .withEnvironment(EnvironmentType.DEV)
            .withDebug(true)
            .build()
    }
}
<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',
          chapter1: 'home',
          chapter2: 'profile',
          // etc...
        });
          
      };
    </script>
  </head>
  ...
</html>
// Create a PageTracker object, passing it the NPOTag and a PageContext object
const tracker = newPageTracker(tag, {
  page: 'profile_page',
  chapter1: 'home',
  chapter2: 'profile',
  // etc...
});

// 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"
)

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

// Instantiate the tracker object from the NpoTag instance
val tracker = npoTag.pageTrackerBuilder()
    .withPageName("home")
    .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()

Pageview/Contentview

Replaces view(), page() and contentView methods in prior versions. This method is called on the pageTracker object. So make sure you created one. The pageView method is still the same with no method variables, you only call it on the pageTracker object instead of the top level object.

pageTracker.pageView();
pageTracker.pageView();
public func pageView()
override fun pageView()

Click

This method is called on the pageTracker object. So make sure you created one. The click method did not change for web, but is now called on the pageTracker object. For the apps it is a click method instead of a customEvent method.

The language of the method variables has changed though:

legacy var namenew var namefield typerequiredcomments
nameclick_namestringtrueidentifier of the click
typeclick_typestringtrue‘navigation’ \ ‘action’ \ ‘exit’ \ ‘download’ \ string"
niveau1chapter_1stringfalsethe top-level context for this click (follow tag-plan)
niveau2chapter_2stringfalsea narrower context for this click (follow tag-plan)
niveau3chapter_3stringfalsemost specific context for this click (follow tag-plan)
pageTracker.click({
  click_name: 'menu',
  click_type: 'navigation' | 'action' | 'exit' | 'download' | string,
  click_chapter_1: 'Home',
  click_chapter_2: 'Navigation bar',
  click_chapter_3: 'Left',
});

The type parameter can take any of the defined string values, or a custom string value. Parameters click_chapter_1/2/3 are optional:

pageTracker.click({
  click_name: 'menu',
  click_type: 'custom-click-type',
});
pageTracker.click({
  click_name: 'menu',
  click_type: 'navigation' | 'action' | 'exit' | 'download' | string,
  click_chapter_1: 'Home',
  click_chapter_2: 'Navigation bar',
  click_chapter_3: 'Left',
});

The type parameter can take any of the defined string values, or a custom string value. Parameters click_chapter_1/2/3 are optional:

pageTracker.click({
  click_name: 'menu',
  click_type: 'custom-click-type',
});
pageTracker.click(
    name: "menu",
    type: ClickType.action,
    chapter1: "Home",
    chapter2: "Navigation bar",
    chapter3: "Left"
)

The type parameter can take any predefined value exposed by the ClickType enum, or a custom value using the .other enum case. Parameters chapter1/2/3 are optional:

pageTracker.click(
    name: "menu",
    type: ClickType.other(value: "your_custom_type_here")
)
pageTracker.click(
    name = "menu",
    type = ClickType.Action(),
    chapter1 = "Home",
    chapter2 = "Navigation bar",
    chapter3 = "Left"
)

The type parameter can take any predefined value exposed by the ClickType sealed class, or a custom value using the .Other class. Parameters chapter1/2/3 are optional:

pageTracker.click(
    name = "menu",
    type = ClickType.Other("your_custom_type_here"),
)

Stream measurements

We now have a new player team at the NPO that builds a player and integrates our measurements. So if you use the NPO player already and have configured the tracking info for that player, you dont have to integrate stream measurements yourself.

But if you dont use the NPO player… The stream measurements have changed somewhat. Instead of creating a recorder object on web or a streamTracker on app in the legacy SDKs, a streamTracker is created from a pageTracker in the new SDKs. There are some additional variables that need to be passed to the tracker now:

legacy var namenew var namefield typerequiredcomments
midstream_idstringtrueID of the content being played
– (new)stream_lengthfloattruelength of stream in seconds
– (new)player_idstringtrueID of the player used
– (new)av_typestringtruevideo or audio
– (new)player_versionstringtrueversion of the player SDK
– (new)sko_player_versionstringtrueplayer version for Stichting KijkOnderzoek
– (new)isLiveboolfalseset to true if you want to measure a live stream
<script lang="text/javascript">
  window.onload = function() {
      ...
      const streamTracker = npotag.newStreamTracker(
          pageTracker,
          {
              stream_length: videoElement.duration,
              stream_id: 'video-stream',
              player_id: 'embedded-video',
              av_type: 'video',
              player_version: '1.0.0',
              sko_player_version: '1.0.0'
          },
          {
              isLive: true,
          }
      );
  }
</script>
const streamTracker = newStreamTracker(
  pageTracker,
  {
    stream_length: videoElement.duration,
    stream_id: 'video-stream',
    player_id: 'embedded-video',
    av_type: 'video',
    player_version: '1.0.0',
    sko_player_version: '1.0.0',
  },
  {
    isLive: true,
  }
);
let streamContext = StreamContext(
  streamLength: video.duration,
  streamID: "streamId",
  avType: StreamContext.AVType.video, // Or StreamContext.AVType.audio
  playerID: "playerId",
  playerVersion: "1.0.0",
  skoPlayerVersion: "1.0.0"
)

// Create a stream tracker instance for either a regular stream or live stream
let streamTracker = pageTracker.newStreamTracker(
  withContext: streamContext,
  isLiveStream: false
)
val streamTracker =  pageTracker.streamTrackerBuilder()
    .withStreamLength(STREAM_LENGTH)
    .withStreamId("testStreamId")
    .withPlayerId("testPlayerId")
    .withAVType("video")
    .withPlayerVersion("testPlayerVersion")
    .withSkoPlayerVersion("testSkoPlayerVersion")
    .withLiveStream(true)
    .build()

When you created a streamTracker you can call stream related tracking methods on it. The start, pause, resume, seek, complete, fullscreen, windowed, load methods are still there. But they require you to send the current stream position with them as a method variable when called.

All the add related events have been deprecated (adStart, adPause, adResume adComplete)

There are some new methods that need to be implemented that were not in the previous SDK. We had the load event already, bun now you need to call loadComplete when the loading of the stream is done. The same holds for the new buffering and bufferingComplete.

The stop method is also new, this needs to be called when you leave a stream or reset it (pressing the stop button in some cases)

The last thing is new for the app implementations, but not new for web (but it is slightly different). Every “tick” of your stream you need to call the time method with the current stream position in seconds. The SDK will determine when it needs to send waypoints to the configured plugins as long as you have implemented all the other methods.

For all the methods that need to be implemented, please look at thestreamTracker.

Offers and Choices

As all the previous methods, offers and choices have been given their own context object as well. The recommendationTracker The distinction between editorial, search and media recommender objects have been deprecated. You now always use the same recommendation object. For search offers and choices you need to fill the query_context field in the pageContext builder/factory.

For every panel in your application you create a context object just as before, but with a different method call:

legacy var namenew var namefield typerequiredcomments
panelpanel_idstringtrueidentifier of this panel
was in the offer/choice methodtotal_offersinttruetotal amount of items in this panel
– (new)panel_formatstringtruethe UX-format of the panel
– (new)panel_positionintfalsethe position of the panel on the page, can be absent for panels such as a search-results pop-over
const recommendationTracker = newRecommendationTracker(pageTracker, {
  panel_id: '23889851-fec5-4106-8501-122f1e233686',
  total_offers: 9,
  panel_format: '5grid',
  panel_position: 1,
});
const recommendationTracker = newRecommendationTracker(pageTracker, {
  panel_id: '23889851-fec5-4106-8501-122f1e233686',
  total_offers: 9,
  panel_format: '5grid',
  panel_position: 1,
});
let recommendationContext = RecommendationContext(
  panelId: "panelId",
  totalOffers: offers.count,
  panelFormat: "panelFormat",
  panelPosition: 0
)

// Create a recommendation tracker instance from our pageTracker
let recommendationTracker = pageTracker.newRecommendationTracker(
    withContext: recommendationContext
)
val recommendationTracker = pageTracker.recommendationTrackerBuilder()
    .withPanelId(panelId)
    .withTotalOffers(NUMBER_OF_ITEMS)
    .withPanelFormat("panelFormat")
    .withPanelPosition(panelPosition)
    .build()

Offer

The offer event can be called on the recommendationTracker of the corresponding panel with a slightly different method call:

legacy var namenew var namefield typerequiredcomments
recommenderrecommenderstringtruerecommender algorithm used or lane identifier
midReftarget_idstringtrueID of the content being offered
positionoffer_indexinttruethe position of the offer within the recommendation panel
recommendationTracker.offer({
  recommender: 'ps-implicit-v0',
  target_id: 'VPWON_123',
  offer_index: 2,
});
recommendationTracker.offer({
  recommender: 'ps-implicit-v0',
  target_id: 'VPWON_123',
  offer_index: 2,
});
recommendationTracker.offer(recommender: String, targetId: String, offerIndex: Int)
recommendationTracker.offer(
    recommender = "recommender",
    targetId = "targetId",
    offerIndex = 1
)

Choice

For the choice holds the same:

legacy var namenew var namefield typerequiredcomments
recommenderrecommenderstringtruerecommender algorithm used or lane identifier
midReftarget_idstringtrueID of the content being offered
positionoffer_indexinttruethe position of the offer within the recommendation panel
recommendationTracker.choice({
  recommender: 'ps-implicit-v0',
  target_id: 'VPWON_123',
  offer_index: 3,
});
recommendationTracker.choice({
  recommender: 'ps-implicit-v0',
  target_id: 'VPWON_123',
  offer_index: 3,
});
recommendationTracker.choice(recommender: String, targetId: String, offerIndex: Int)
recommendationTracker.choice(
    recommender = "recommender",
    targetId = "targetId",
    offerIndex = 1
)

Support

If you have any further questions about migration from the legacy tag to the new one, please send a mail to diaz@npo.nl