UNPKG

@m10s/cmp

Version:

Package containing scripts used by Schibsteds' sites to integrate with Sourcepoint CMP

740 lines (500 loc) 30.1 kB
# CMP products - Javascript SDK This repository contains a small library to simplify the implementation of popup-ups delivered by [Sourcepoint](http://sourcepoint.com/). Don't hesitate to contact us on our Slack channel: [#ask-compliance-team](https://sch-chat.slack.com/archives/C02D5RF7D7S). ## Integrations - [TCF (Transparency & Consent Framework)](https://github.schibsted.io/UFo-User-Choices/sourcepoint#transparency--consent-framework-tcf) - [SCC (Schibsted Cookie Consent)](https://github.schibsted.io/UFo-User-Choices/sourcepoint#schibsted-cookie-consent-scc) ### Config Configuration for your implementation can be found in the [CMP_Configuration_setup](https://docs.google.com/spreadsheets/d/1TQVXcoeFlqBqwxiacsfEhm5TZR0cViBTqyZqNpaK9Ik/edit?gid=893733972#gid=893733972). | Required parameters | Description | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `propertyId: number` | Maps the message to a specific property (website, app, OTT) as set up in the Sourcepoint account dashboard. | | `consentLanguage: string` | Consent message language. Available: `en`, `no`, `sv`, `fi`, `dk`. | | `baseEndpoint: string` | A single server endpoint that serves the messaging experience. | | `groupPmId: number` | **SCC and TCF only**. Allows to use the Privacy Manager ID. | | Optional parameters | Description | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `accountId: number` | Used to override organization's Sourcepoint account. Default value:`1960` | | `propertyHref: string` | Used to display a message from one property, e.g. production url, on another domain, e.g. staging/test site. | | `targetingParams: array<string>` | Array with key value pairs passed to Sourcepoint - could be used as conditions in scenarios. | | `userId: number` | The Schibsted account user identifier for the logged in user. E.g. 123456, not full SDRN. Send only if user is logged in. More info below.| | `realm: string` | The Schibsted account realm of the logged in user. E.g. schibsted.com, spid.no, schibsted.fi. More info below. | | `clientId: string` | Provide in order to redirect user to Privacy Settings page with `client_id` parameter. It's a unique identifier generated for an individual service client. | | `pulseTracker: object` | Pulse Event Tracker object. More details [here](https://github.schibsted.io/spt-dataanalytics/pulse-sdk-js/blob/master/workspaces/sdk/README.md#constructor). **Only in the client-side integration.** | `referrer: string` | Used to allow users to return to the exact page from Privacy Settings UI. Its default value is the current URL.| | `state: string` | It should be a base64-encoded string. Enables passing any query parameters as a part of a specific redirect URL. More details in [Privacy Settings docs](https://docs.schibsted.io/privacy-settings/privacy-settings-ui/#navigating-back). | | `pulseObjectName: string` | Provide when you assign Pulse to different variable than `window.pulse`. Used in the server-side integration type. | | `showInWebview: string` | Provide if you want to enable displaying modal in a mobile app webview. `"all"` - all modals will be enabled; `"scc"` - only SCC modal will be enabled; `"tcf"` - only TCF modal will be enabled | | `identityObject: object` | Schibsted Account Identity object. More details [here](https://github.com/schibsted/sdk-example/blob/master/browser/index.js#L34). | | `identityObjectName: string` | Provide in case you assigned the `Identity` object to a different global variable than `window.Identity`. | | `enableUserCentric: boolean` | Enables the brand level consent feature. The user's consent is attached to the authId and will be stored on the backend. | | `groupId: string` | Extension of the `enableUserCentric` parameter: to handle more specific cases. Provide if consent shouldn't be tied to the merchant level. | | `disableNativeConsentCheck: boolean` | Disable blocking of CMP snippet on webviews that don't provide `_sp_pass_consent` parameter in URL | ## Installation Install package from [Artifactory](https://artifacts.schibsted.io/ui/repos/tree/General/npm-local/@m10s/cmp/) using command: ``` // npm npm install @m10s/cmp // yarn yarn add @m10s/cmp ``` To configure your `.npmrc` file, look in npm-virtual under the "Set me up" section. **Note**: The package is also available on [NPMJS](https://www.npmjs.com/package/@m10s/cmp). If you don't have access to Artifactory, you can use the public package. ## Privacy Settings Introducer (PSI) The PSI is used to identify Vend as the data controller and alert users about choices available to them in Privacy Settings, along with a link to get there. Since October 2025, this integration has been replaced by an email-based solution. With the start of the PSI Email Rollout, all PSI campaigns in the CMP have been paused. As a result, the PSI banner will no longer appear on the sites (previously, it was displayed one day after a user interacted with the TCF message). ## Transparency & Consent Framework (TCF) The TCF type is most common and is used for all sites or apps that include display advertising. ### Step 1: Add Sourcepoint Client Configuration Code Snippet **Browser integration** ```javascript import { tcf } from '@m10s/cmp'; tcf(window, document, navigator, { baseEndpoint: 'https://cmpv2.YOUR_DOMAIN/', // required propertyId: 1234, // required consentLanguage: 'sv', // required groupPmId: 1234, // required }); ``` **NodeJS integration** ```javascript const { tcf } = require('@m10s/cmp'); const snippet = tcf({ baseEndpoint: 'https://cmpv2.YOUR_DOMAIN/', // required propertyId: 1234, // required consentLanguage: 'sv', // required groupPmId: 1234, // required }); // Now you can add the snippet to the page: // index.js // app.get('/', (req, res) => res.render('page', { snippet })); // page.hbs // <html><head>{{snippet}}</hread><body>...</body></html> ``` ### Step 2: TCF integration with the site In step 1 you have added required code to present user TCF and ask him about consents. In second step you need to integrate your site with TCF Library to honour user choices. Cookies (scripts) could belong to one of these categories: - `Essential` - Necessary cookies which could be set always, without asking user about the consent (this category is not supported by the library because consent is note required). - `CMP:advertising` - Advertising related Cookies - user consent is required. - `CMP:analytics` - Analytics and Product Development related cookies - user consent is required. - `CMP:marketing` - Marketing related Cookies - user consent is required. - `CMP:personalisation` - Personalisation related Cookies - user consent is required. - `CMP:performance_marketing` - Only used where there is a waiver from Privacy Legal to run marketing campaigns on third party platforms, and where consent for `CMP:marketing` does not apply. Your site could run script only when consent was given by user. To receive information about user choices you can use one of two provided methods: - `getPermission` - allows getting information about consent once (asynchronous) - `getPermissionSync` - allows getting information about consent (synchronous) - `subscribe` - allows getting information about the concent and be notified about future changes Library inform your site about one of two states (`PermissionValue`): - `"1"` - cookies could be set / script could be run - user gave consent - `"0"` - cookies could NOT be set / script could NOT be run - user hasn't given consent - `null` - user hasn't made a decision yet (returned only by synchronous function) Returned values are cached in LocalStorage to speed up integration. ### Step 3: Allow reopening Privacy Manager To allow users to change their choices, each page should contain a link to re-trigger TCF. Function: `window._tcf_.showPrivacyManager` allows reopening Privacy Manager. ### Methods #### `getPermission` Function `window._tcf_.getPermission` allows getting information about consent once: ```javascript window._tcf_.getPermission(category: Categories, callback: (value: PermissionValue) => {}) ``` Categories: - `CMP:advertising` - Advertising related Cookies - `CMP:analytics` - Analytics and Product Development related cookies - `CMP:marketing` - Marketing related Cookies - `CMP:personalisation` - Personalisation related Cookies - `CMP:performance_marketing` - Marketing campaigns on third party platforms related Cookies PermissionValue: - `"1"` - cookies could be set - `"0"` - cookies could NOT be set #### `getPermissionSync` Function `window._tcf_.getPermissionSync` allows getting information about consent once, synchronously: ```javascript window._tcf_.getPermissionSync(category: Categories) ``` Categories: - `CMP:advertising` - Advertising related Cookies - `CMP:analytics` - Analytics and Product Development related cookies - `CMP:marketing` - Marketing related Cookies - `CMP:personalisation` - Personalisation related Cookies - `CMP:performance_marketing` - Marketing campaigns on third party platforms related Cookies PermissionValue: - `"1"` - cookies could be set - `"0"` - cookies could NOT be set - `null` - user hasn't made a decision yet #### `subscribe` Function `window._tcf_.subscribe` allows getting information about the concent and be notified about future changes. ```javascript window._tcf_.subscribe(category: Categories, callback: (value: PermissionValue) => {}): () => void ``` Categories: - `CMP:advertising` - Advertising related Cookies - `CMP:analytics` - Analytics and Product Development related cookies - `CMP:marketing` - Marketing related Cookies - `CMP:personalisation` - Personalisation related Cookies - `CMP:performance_marketing` - Marketing campaigns on third party platforms related Cookies PermissionValue: - `"1"` - cookies could be set - `"0"` - cookies could NOT be set Returned value is a function which allow unsubscribing from notification about changes. #### `showPrivacyManager` Function `window._tcf_.showPrivacyManager` allows reopening Privacy Manager - thanks to that user could change settings. ```javascript window._tcf_.showPrivacyManager(); ``` This function require parameter `groupPmId` in the config to work properly. #### `isConsentedToAll` Function `window._tcf_.isConsentedToAll` allows getting information about whether user consented to all purposes and be notified about future changes. ```javascript window._tcf_.isConsentedToAll(callback: (value: boolean) => {}): () => void ``` #### `getConsentedToAllSync` Function `window._tcf_.getConsentedToAllSync` allows getting information about consentedAll status once (instead of reacting on consent change), based on the data retrieved from browser's local storage. ```javascript window._tcf_.getConsentedToAllSync() ``` Function returns `boolean | null`. #### `clearCMPData` Fuction to erase local consent-related data. Clear personal consent data when users log out. ```javascript window._tcf_.clearCMPData() ``` ## Schibsted Cookie Consent (SCC) Used on sites and apps that do not carry advertising and do not need the TCF, but still need to collect consent for Vend defined purposes. It is similar to TCF in terms of implementation but presents a simpler set of choices. ### Step 1: Add Sourcepoint Client Configuration Code Snippet **Browser integration** ```javascript import { scc } from '@m10s/cmp'; scc(window, document, navigator, { baseEndpoint: 'https://cmpv2.YOUR_DOMAIN/', // required propertyId: 1234, // required consentLanguage: 'sv', // required groupPmId: 1234, // required userId: 123456, realm: 'spid.no', }); ``` **NodeJS integration** ```javascript const { scc } = require('@m10s/cmp'); const snippet = scc({ baseEndpoint: 'https://cmpv2.YOUR_DOMAIN/', // required propertyId: 1234, // required consentLanguage: 'sv', // required groupPmId: 1234, // required userId: 123456, realm: 'spid.no', }); // Now you can add the snippet to the page: // index.js // app.get('/', (req, res) => res.render('page', { snippet })); // page.hbs // <html><head>{{snippet}}</hread><body>...</body></html> ``` ### Step 2: SCC integration with the site In step 1 you have added required code to present user SCC and ask him about consents. In second step you need to integrate your site with SCC Library to honour user choices. Cookies (scripts) could belong to one of these categories: - `Essential` - Necessary cookies which could be set always, without asking user about the consent (this category is not supported by the library because consent is not required). - `CMP:advertising` - Advertising related Cookies - user consent is required. - `CMP:analytics` - Analytics and Product Development related cookies - user consent is required. - `CMP:marketing` - Marketing related Cookies - user consent is required. - `CMP:personalisation` - Personalisation related Cookies - user consent is required. Your site could run script only when consent was given by user. To receive information about user choices you can use one of two provided methods: - `getPermission` - allows getting information about consent once (asynchronous) - `getPermissionSync` - allows getting information about consent (synchronous) - `subscribe` - allows getting information about the concent and be notified about future changes Library inform your site about one of two states (`PermissionValue`): - `"1"` - cookies could be set / script could be run - user gave consent - `"0"` - cookies could NOT be set / script could NOT be run - user hasn't given consent - `null` - user hasn't made a decision yet (returned only by synchronous function) Returned values are cached in LocalStorage to speed up integration. ### Allow reopening Privacy Manager To allow users change their choices each page should contain a link to re trigger Schibsted Cookie Consent. Function: `window._scc_.showPrivacyManager()` allows reopening Privacy Manager. ### Schibsted Cookie Consent Interface #### `getPermission` Function `window._scc_.getPermission` allows getting information about consent once: ```javascript window._scc_.getPermission(category: Categories, callback: (value: PermissionValue) => {}) ``` Categories: - `CMP:advertising` - Advertising related Cookies - `CMP:analytics` - Analytics and Product Development related cookies - `CMP:marketing` - Marketing related Cookies - `CMP:personalisation` - Personalisation related Cookies PermissionValue: - `"1"` - cookies could be set - `"0"` - cookies could NOT be set #### `getPermissionSync` Function `window._scc_.getPermissionSync` allows getting information about consent once, synchronously: ```javascript window._scc_.getPermissionSync(category: Categories) ``` Categories: - `CMP:advertising` - Advertising related Cookies - `CMP:analytics` - Analytics and Product Development related cookies - `CMP:marketing` - Marketing related Cookies - `CMP:personalisation` - Personalisation related Cookies PermissionValue: - `"1"` - cookies could be set - `"0"` - cookies could NOT be set - `null` - user hasn't made a decision yet #### `subscribe` Function `window._scc_.subscribe` allows getting information about the concent and be notified about future changes. ```javascript const unsubscribeFn = window._scc_.subscribe(category: Categories, callback: (value: PermissionValue) => {}): () => void ``` Categories: - `CMP:advertising` - Advertising related Cookies - `CMP:analytics` - Analytics and Product Development related cookies - `CMP:marketing` - Marketing related Cookies - `CMP:personalisation` - Personalisation related Cookies PermissionValue: - `"1"` - cookies could be set - `"0"` - cookies could NOT be set Returned value is a function which allow unsubscribing from notification about changes. #### `showPrivacyManager` Function `window._scc_.showPrivacyManager` allows reopening Privacy Manager - thanks to that user could change settings. ```javascript window._scc_.showPrivacyManager(); ``` This function require parameter `groupPmId` in the config to work properly. ## Core features ### Pulse SDK integration TCF and SCC are integrated with Pulse and we ask you to allow us to collect user and device data. If you use the [Pulse SDK]((https://docs.schibsted.io/pulse/integration_guide/step4/)), please add a tracker object to the config or assign it to the window. With the [Autotracker]((https://github.schibsted.io/spt-dataanalytics/pulse-autotracker)), you will have a global `pulse` method which is not available when following the SDK way. **Minimum Pulse version required:** - SDK: `5.0.0` - Autotracker: `2.0.0` **Browser integration** Provide `pulseTracker` to the config: ``` pulseTracker: new Tracker('{CLIENT-ID}', {}, {}) ``` In the case of SCC and TCF integrations, it is required to provide a `consents` object to the initial Pulse config. You can use the method `getCachedOrDefaultConsentsForPulse` for that purpose. - If you use SDK integration **TCF** ```javascript const consents = window._tcf_.getCachedOrDefaultConsentsForPulse(); const tracker = new Tracker('{CLIENT-ID}', { consents, requireAdvertisingOptIn: true }); window.pulse = (callback) => callback(tracker); ``` **SCC** ```javascript const consents = window._scc_.getCachedOrDefaultConsentsForPulse(); const tracker = new Tracker('{CLIENT-ID}', { consents, requireAdvertisingOptIn: true }); window.pulse = (callback) => callback(tracker); ``` - If you use Autotracker integration **TCF** ```javascript const consents = window._tcf_.getCachedOrDefaultConsentsForPulse(); pulse('init', 'CLIENT-ID', { consents, requireAdvertisingOptIn: true }); ``` **SCC** ```javascript const consents = window._scc_.getCachedOrDefaultConsentsForPulse(); pulse('init', 'CLIENT-ID', { consents, requireAdvertisingOptIn: true }); ``` **Server integrations** Assign Pulse tracker to the global `window` object: ``` window.pulse = (callback) => callback(pulseTracker) ``` *If you want to assign Pulse to any other variable - you can but you have to also provide `pulseObjectName` to the config. ``` window.whatever = (callback) => callback(pulseTracker) tcf(window, document, navigator, { pulseObjectName: 'whatever' }); ``` #### `getCachedOrDefaultConsentsForPulse` Function `window._scc_.getCachedOrDefaultConsentsForPulse(arg?)` returns updated consents object for Pulse purposes. It accepts a default object in the shape of ConsentsInput as its only parameter. **TCF** ```javascript const consents = window._tcf_.getCachedOrDefaultConsentsForPulse(); const tracker = new Tracker('{CLIENT-ID}', { consents, requireAdvertisingOptIn: true }); ``` **SCC** ```javascript const consents = window._scc_.getCachedOrDefaultConsentsForPulse(); const tracker = new Tracker('{CLIENT-ID}', { consents, requireAdvertisingOptIn: true }); ``` Examples below are also applicable in TCF integration. All you need to do is to call `tcf` instead of `scc`. ### Brand Level Consent Authenticated consent allows to share an end-user's consent preferences across their different authenticated (logged-in) devices. It can be applied to all properties within a brand, reducing the number of times users have to go through the popup flow. This feature is currently only available to logged in users. #### Requirements - Schibsted Account integration (see below) `identity` object should be assigned as a global variable (e.g.`window.Identity`). - The `enableUserCentric` configuration variable must be set to `true`. **Note**: `tcf` snippet in this case is async. ```javascript import { tcf } from '@m10s/cmp'; tcf(window, document, navigator, { baseEndpoint: 'https://cmpv2.YOUR_DOMAIN/', propertyId: 1234, consentLanguage: 'sv', groupPmId: 1234, enableUserCentric: true }).then(() => { // content goes here }); ``` `authId` is used as the consent identifier and can be found at `window._sp_.config.authId`. ##### Server-side integrations Snippet can be attached like this: ```javascript const sourcepointSnippet = sourcepoint.tcf(sourcepointConfig, false); <script>document.addEventListener("DOMContentLoaded", () => { ${sourcepointSnippet} })</script> ``` then to handle any action after the setup is completed: ```javascript window._tcf_?.onTcfReady(() => { console.log('TCF ready') }); ``` ### Schibsted Account integration In order to trigger a login flow, the SDK needs access to Schibsted Account methods. That's why in some cases we ask you to provide us with an `Identity` object. The easiest way is to assign to the window: ```javascript import { Identity } from '@schibsted/account-sdk-browser'; const identity = new Identity({ ... }); window.Identity = identity; ``` This method will work without any additional setup because that's the default solution. If you've already assigned `Identity` object but to the different variable, you can help us navigate it by adding it to the config: ```javascript import { Identity } from '@schibsted/account-sdk-browser'; const identity = new Identity({ ... }); window.SPiD_Identity = identity; import { tcf } from '@m10s/cmp'; tcf(window, document, navigator, { identityObjectName: 'SPiD_Identity' }); ``` You can also pass the `Identity` object directly to the config: ```javascript import { Identity } from '@schibsted/account-sdk-browser'; const identity = new Identity({ ... }); tcf(window, document, navigator, { identityObject: identity }); ``` ### Referrer For some brands, popup should not be displayed when a user is navigating between their sites, with different root domains. To help exclude such traffic information about hostname from `document.referrer` is passed to Sourcepoint using `window._sp_.config.targetingParams['referrer-hostname'] = 'www.finn.no` which allow using `String Match` condition in Scenario. ### Cookies To limit number of views for brands with multiple domains support we can use additional information stored in cookies. Example: For `document.cookie=_sch_cmp_displayed=true` key-value pair will be passed: `_sch_cmp_displayed=true` allowing usage of `String Match` condition in Scenario. Cookie could be set using custom Javascript action for button. ## Examples You can find example TCF and SCC implementation in the folder: [example](./example/). To run it you need to clone repository and run command: ```bash npm start ``` Example runs on http://localhost:10001. You can also find examples of ESI inclusion, as well as JS tag integration in these examples. Just visit http://localhost:10001/esi.html and http://localhost:10001/js-tag.html respectively. ## For Privacy Developers ### How to publish package Package is distributed by [Artifactory](https://artifacts.schibsted.io/ui/repos/tree/General/npm-local/@m10s/cmp/). To publish the package you need to: - change version in `package.json` file - create a tag from branch `master` Code will be published by Github Actions. ### How it works NodeJS version uses generated static scripts during publishing by `rollup` (script: `npm run build`) - files are stored in directory: `dist/`. Thanks to that code is converted to beeing compatible with old browsers, merged into one file per integration (TCF, SCC). For debug purpose you can build files without minimization using script: `npm run build:debug`, ### NPM Version NPM Version used by Github Actions is defined in file `.nvmrc`. This way allow you to switch to propre NodeJS version locally in an easy way too. Running code shown below the proper NodeJS version will be used / installed. ```bash nvm use ``` ### Methods: #### Opening Privacy Settings Method `openPrivacySettings` allows redirecting users to Privacy Settings. It enables also handling the user authentication state (being logged in or out). For non-logged-in users, there's `environment_id` provided as a URL parameter which means the login flow isn't triggered on the Privacy Settings page. This method also uses `client_id` as a query parameter in order to display brand-specific settings and UI elements. ```javascript window.psi.openPrivacySettings(string); ``` Example: ```javascript window.psi.openPrivacySettings('https://privacysettings.schibsted.no/'); ``` _**Note**_: This method relies on an existing Pulse integration. It means a page needs to be integrated with Pulse in order to use this method. ### Pulse configuration Events that flow into the Pulse platform via the collector needs to be routed to a destination, also called sink. Sinks collecting data from `@m10s/cmp` and sending them to Amplitude are calling `CMP-Marketplaces-Amplitude-1`. They can be found in two files: - [yggdrasil-sinks-schibsted-pro.yaml](https://github.schibsted.io/spt-dataanalytics/routing/blob/master/src/main/resources/sinks-pro/schibsted-marketplaces.yaml) - [yggdrasil-sinks-schibsted-pre.yaml](https://github.schibsted.io/spt-dataanalytics/routing/blob/master/src/main/resources/sinks-pre/schibsted-marketplaces.yaml) Mapping Pulse data to Amplitude schema is also set in routing repo in [amplitude-cmp.jstl2](https://github.schibsted.io/spt-dataanalytics/routing/blob/master/src/main/resources/transforms/amplitude-cmp.jstl2) file. ### Pulse events CMP is integrated with Pulse. Events are sent to your provided used by the page (using default Pulse object available on the page). You can analyse events on your own. All CMP-related events are tagged using the property: `provider.component = "CMP-Marketplaces"` Privacy Services team sends events from all providers to their own project in Amplitude too. CMP sends 2 types of events: * **View - CMP**: `@type: "View"`, `object.@type: "CMP"` Triggered when TCF or SCC is about to display to a user. * **Engagement - CMP**: `@type: "Engagement"`, `object.@type: "CMP"` Triggered on user action. Actions can be distinguished in Amplitude by `object.name`. Supported CMP actions: * click on "Okay" button * click on "To my saved preferences" button * click on "visit out Privacy pages" link Supported SCC actions: * click on "Accept all cookies" button * click on "Confirm selection" button * click on "Personal data and cookie policy" link Schema / documentation of `object.@type` set to `"CMP"` can be found in [event-formats repo](https://github.schibsted.io/spt-dataanalytics/event-formats/blob/master/schema/master/objects/CMP.json). ### Sourcepoint events Documentation of Sourcepoint events can be found [here](https://docs.sourcepoint.com/hc/en-us/articles/4405397484307-Event-callbacks). However, working on this project we collected additional information which can be useful in development. About `onMessageReceiveData` event: - It is a normal behaviour that this event listener fires on every pageview. - If the `messageId` in callback data is different from `0`, it means that the message was displayed to the user. - Event callback is not returning the `cmpgn_id` (listed in documentation), because the `messageId` is changing from campaign to campaign. It seems that this variable was removed from the data sent to the callback since the `messageId` is already telling you which campaign it is. About `onMessageReady` event: - This event fires/triggers just before the CMP message is shown to the user. Few of Sourcepoint clients decided that they want to trigger a Privacy manager as a first layer message. Therefore, they selected in their scenario in the “Show message” event the Privacy manager layer instead of the first layer message. They use the Privacy manager as a first layer message, because they want to disclose to the user from the beginning the whole vendor list, toggles, etc. - According to Sourcepoint team it is the best event to detect when a message is showing up to user. ### Debugging Debug messages are displayed after passing the `sp_debug=1` query param. To log out debug information, you can use the `debug` function from `src\utils.js`. ## External documentation There are external spreadsheets with useful documentation. Kind request to update it regularly :) - [Spreadsheet with CMP events details and mapping to Amplitude data](https://docs.google.com/spreadsheets/d/1PdXr_2vGqgYU-ISFmtcKen0T18vZmeWxVKI7Kkm8RAo/edit?usp=sharing) ## Links - [Sourcepoint: CMP Web Implementation](https://docs.sourcepoint.com/hc/en-us/articles/4405412395667-CMP-web-implementation) - [Pulse: documentation](https://docs.schibsted.io/pulse) - [Amplitude: HTTP API V2](https://developers.amplitude.com/docs/http-api-v2)