From a59755dadcaa46c197ac942c894c64179396a845 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Mon, 16 Jan 2023 11:57:47 +0100 Subject: [PATCH] Add plugin to send requests with user ID to a Kantar FocalMeter endpoint (close #1133) PR #1134 --- .../issue-focalmeter_2022-12-22-13-04.json | 10 ++ .../rush/browser-approved-packages.json | 4 + .../browser-plugin-focalmeter/CHANGELOG.json | 5 + plugins/browser-plugin-focalmeter/LICENSE | 29 ++++ plugins/browser-plugin-focalmeter/README.md | 56 +++++++ .../browser-plugin-focalmeter/jest.config.js | 5 + .../browser-plugin-focalmeter/package.json | 53 ++++++ .../rollup.config.js | 67 ++++++++ .../browser-plugin-focalmeter/src/index.ts | 156 ++++++++++++++++++ .../test/request.test.ts | 148 +++++++++++++++++ .../browser-plugin-focalmeter/tsconfig.json | 3 + rush.json | 6 + 12 files changed, 542 insertions(+) create mode 100644 common/changes/@snowplow/browser-plugin-focalmeter/issue-focalmeter_2022-12-22-13-04.json create mode 100644 plugins/browser-plugin-focalmeter/CHANGELOG.json create mode 100644 plugins/browser-plugin-focalmeter/LICENSE create mode 100644 plugins/browser-plugin-focalmeter/README.md create mode 100644 plugins/browser-plugin-focalmeter/jest.config.js create mode 100644 plugins/browser-plugin-focalmeter/package.json create mode 100644 plugins/browser-plugin-focalmeter/rollup.config.js create mode 100644 plugins/browser-plugin-focalmeter/src/index.ts create mode 100644 plugins/browser-plugin-focalmeter/test/request.test.ts create mode 100644 plugins/browser-plugin-focalmeter/tsconfig.json diff --git a/common/changes/@snowplow/browser-plugin-focalmeter/issue-focalmeter_2022-12-22-13-04.json b/common/changes/@snowplow/browser-plugin-focalmeter/issue-focalmeter_2022-12-22-13-04.json new file mode 100644 index 000000000..325547edf --- /dev/null +++ b/common/changes/@snowplow/browser-plugin-focalmeter/issue-focalmeter_2022-12-22-13-04.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@snowplow/browser-plugin-focalmeter", + "comment": "Add plugin to send requests with user ID to a Kantar FocalMeter endpoint (#1133)", + "type": "none" + } + ], + "packageName": "@snowplow/browser-plugin-focalmeter" +} \ No newline at end of file diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 8dbba135f..a80fc2a9b 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -130,6 +130,10 @@ "name": "@snowplow/browser-plugin-youtube-tracking", "allowedCategories": [ "trackers" ] }, + { + "name": "@snowplow/browser-plugin-focalmeter", + "allowedCategories": [ "trackers" ] + }, { "name": "@snowplow/browser-tracker", "allowedCategories": [ "plugins", "trackers" ] diff --git a/plugins/browser-plugin-focalmeter/CHANGELOG.json b/plugins/browser-plugin-focalmeter/CHANGELOG.json new file mode 100644 index 000000000..7f74dc674 --- /dev/null +++ b/plugins/browser-plugin-focalmeter/CHANGELOG.json @@ -0,0 +1,5 @@ +{ + "name": "@snowplow/browser-plugin-focalmeter", + "entries": [ + ] +} diff --git a/plugins/browser-plugin-focalmeter/LICENSE b/plugins/browser-plugin-focalmeter/LICENSE new file mode 100644 index 000000000..76f1946ea --- /dev/null +++ b/plugins/browser-plugin-focalmeter/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/browser-plugin-focalmeter/README.md b/plugins/browser-plugin-focalmeter/README.md new file mode 100644 index 000000000..668656a21 --- /dev/null +++ b/plugins/browser-plugin-focalmeter/README.md @@ -0,0 +1,56 @@ +# Snowplow FocalMeter Integration + +[![npm version][npm-image]][npm-url] +[![License][license-image]](LICENSE) + +Browser Plugin to be used with `@snowplow/browser-tracker`. + +Adds integration with the Kantar FocalMeter to your Snowplow tracking. +The plugin sends requests with the domain user ID to a Kantar endpoint used with the FocalMeter system. +A request is made when the first event with a new user ID is tracked. + +## Maintainer quick start + +Part of the Snowplow JavaScript Tracker monorepo. +Build with [Node.js](https://nodejs.org/en/) (14 or 16) and [Rush](https://rushjs.io/). + +### Setup repository + +```bash +npm install -g @microsoft/rush +git clone https://github.com/snowplow/snowplow-javascript-tracker.git +rush update +``` + +## Package Installation + +With npm: + +```bash +npm install @snowplow/browser-plugin-focalmeter +``` + +## Usage + +Initialize your tracker with the FocalMeterPlugin: + +```js +import { newTracker } from '@snowplow/browser-tracker'; +import { FocalMeterPlugin } from '@snowplow/browser-plugin-focalmeter'; + +newTracker('sp1', '{{collector}}', { plugins: [ FocalMeterPlugin() ] }); // Also stores reference at module level +``` + +## Copyright and license + +Licensed and distributed under the [BSD 3-Clause License](LICENSE) ([An OSI Approved License][osi]). + +Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang. + +All rights reserved. + +[npm-url]: https://www.npmjs.com/package/@snowplow/browser-plugin-focalmeter +[npm-image]: https://img.shields.io/npm/v/@snowplow/browser-plugin-focalmeter +[docs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-tracker/ +[osi]: https://opensource.org/licenses/BSD-3-Clause +[license-image]: https://img.shields.io/npm/l/@snowplow/browser-plugin-focalmeter diff --git a/plugins/browser-plugin-focalmeter/jest.config.js b/plugins/browser-plugin-focalmeter/jest.config.js new file mode 100644 index 000000000..bd3ea4e2a --- /dev/null +++ b/plugins/browser-plugin-focalmeter/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + reporters: ['jest-standard-reporter'], + testEnvironment: 'jest-environment-jsdom-global', +}; diff --git a/plugins/browser-plugin-focalmeter/package.json b/plugins/browser-plugin-focalmeter/package.json new file mode 100644 index 000000000..67153b30a --- /dev/null +++ b/plugins/browser-plugin-focalmeter/package.json @@ -0,0 +1,53 @@ +{ + "name": "@snowplow/browser-plugin-focalmeter", + "version": "3.8.0", + "description": "Kantar FocalMeter integration for Snowplow", + "homepage": "http://bit.ly/sp-js", + "bugs": "https://github.com/snowplow/snowplow-javascript-tracker/issues", + "repository": { + "type": "git", + "url": "https://github.com/snowplow/snowplow-javascript-tracker.git" + }, + "license": "BSD-3-Clause", + "author": "Matus Tomlein", + "sideEffects": false, + "main": "./dist/index.umd.js", + "module": "./dist/index.module.js", + "types": "./dist/index.module.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "rollup -c --silent --failAfterWarnings", + "test": "jest" + }, + "dependencies": { + "@snowplow/browser-tracker-core": "workspace:*", + "@snowplow/tracker-core": "workspace:*", + "tslib": "^2.3.1" + }, + "devDependencies": { + "@ampproject/rollup-plugin-closure-compiler": "~0.27.0", + "@rollup/plugin-commonjs": "~21.0.2", + "@rollup/plugin-node-resolve": "~13.1.3", + "@types/jest": "~27.4.1", + "@types/jsdom": "~16.2.14", + "@typescript-eslint/eslint-plugin": "~5.15.0", + "@typescript-eslint/parser": "~5.15.0", + "eslint": "~8.11.0", + "jest": "~27.5.1", + "jest-environment-jsdom": "~27.5.1", + "jest-environment-jsdom-global": "~3.0.0", + "jest-standard-reporter": "~2.0.0", + "rollup": "~2.70.1", + "rollup-plugin-cleanup": "~3.2.1", + "rollup-plugin-license": "~2.6.1", + "rollup-plugin-terser": "~7.0.2", + "rollup-plugin-ts": "~2.0.5", + "ts-jest": "~27.1.3", + "typescript": "~4.6.2" + }, + "peerDependencies": { + "@snowplow/browser-tracker": "~3.8.0" + } +} diff --git a/plugins/browser-plugin-focalmeter/rollup.config.js b/plugins/browser-plugin-focalmeter/rollup.config.js new file mode 100644 index 000000000..1c024021b --- /dev/null +++ b/plugins/browser-plugin-focalmeter/rollup.config.js @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import ts from 'rollup-plugin-ts'; // Prefered over @rollup/plugin-typescript as it bundles .d.ts files +import { banner } from '../../banner'; +import compiler from '@ampproject/rollup-plugin-closure-compiler'; +import { terser } from 'rollup-plugin-terser'; +import cleanup from 'rollup-plugin-cleanup'; +import pkg from './package.json'; +import { builtinModules } from 'module'; + +const umdPlugins = [nodeResolve({ browser: true }), commonjs(), ts()]; +const umdName = 'snowplowFocalMeter'; + +export default [ + // CommonJS (for Node) and ES module (for bundlers) build. + { + input: './src/index.ts', + plugins: [...umdPlugins, banner()], + treeshake: { moduleSideEffects: ['sha1'] }, + output: [{ file: pkg.main, format: 'umd', sourcemap: true, name: umdName }], + }, + { + input: './src/index.ts', + plugins: [...umdPlugins, compiler(), terser(), cleanup({ comments: 'none' }), banner()], + treeshake: { moduleSideEffects: ['sha1'] }, + output: [{ file: pkg.main.replace('.js', '.min.js'), format: 'umd', sourcemap: true, name: umdName }], + }, + { + input: './src/index.ts', + external: [...builtinModules, ...Object.keys(pkg.dependencies), ...Object.keys(pkg.devDependencies)], + plugins: [ + ts(), // so Rollup can convert TypeScript to JavaScript + banner(), + ], + output: [{ file: pkg.module, format: 'es', sourcemap: true }], + }, +]; diff --git a/plugins/browser-plugin-focalmeter/src/index.ts b/plugins/browser-plugin-focalmeter/src/index.ts new file mode 100644 index 000000000..72841b010 --- /dev/null +++ b/plugins/browser-plugin-focalmeter/src/index.ts @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import { + attemptGetLocalStorage, + BrowserPlugin, + hasLocalStorage, + BrowserTracker, + attemptWriteLocalStorage, +} from '@snowplow/browser-tracker-core'; +import { Logger, Payload } from '@snowplow/tracker-core'; + +/** FocalMeter plugin configuration */ +export interface FocalMeterConfiguration { + /** URL of the Kantar endpoint to send the requests to (including protocol) */ + kantarEndpoint: string; + /** Whether to store information about the last submitted user ID in local storage to prevent sending it again on next load (defaults not to use local storage) */ + useLocalStorage?: boolean; +} + +const _trackers: Record = {}; +const _configurations: Record = {}; + +/** + * The FocalMeter Plugin + * + * The plugin sends requests with the domain user ID to a Kantar endpoint used with the FocalMeter system. + * A request is made when the first event with a new user ID is tracked. + * + * Call `enableFocalMeterIntegration()` to enable the integration with given configuration. + */ +export function FocalMeterPlugin(): BrowserPlugin { + let LOG: Logger; + let lastUserId: string | undefined | null; + let trackerId: string; + + return { + activateBrowserPlugin: (tracker: BrowserTracker) => { + trackerId = tracker.id; + _trackers[tracker.id] = tracker; + }, + + logger: (logger: Logger) => { + LOG = logger; + }, + + afterTrack: (payload: Payload) => { + if (!_configurations[trackerId]) { + LOG.error('FocalMeter integration not enabled'); + return; + } + + let newUserId = payload['duid'] as string; + let { kantarEndpoint, useLocalStorage } = _configurations[trackerId]; + + if (!lastUserId && useLocalStorage && hasLocalStorage()) { + let key = getLocalStorageKey(trackerId); + lastUserId = attemptGetLocalStorage(key); + } + + if (newUserId && newUserId != lastUserId) { + lastUserId = newUserId; + + sendRequest(kantarEndpoint, newUserId, LOG, () => { + // only write in local storage if the request succeeded + if (useLocalStorage && hasLocalStorage()) { + let key = getLocalStorageKey(trackerId); + attemptWriteLocalStorage(key, newUserId); + } + }); + } + }, + }; +} + +/** + * Enables the integration with Kantar FocalMeter. + * + * @param configuration - Configuration with the URL endpoint to send requests to + * @param trackers - The tracker identifiers which should have the context enabled + */ +export function enableFocalMeterIntegration( + configuration: FocalMeterConfiguration, + trackers: Array = Object.keys(_trackers) +): void { + for (const id of trackers) { + if (_trackers[id]) { + _configurations[id] = configuration; + } + } +} + +function getLocalStorageKey(trackerId: string): string { + return `sp-fclmtr-${trackerId}`; +} + +function sendRequest(url: string, userId: string, LOG: Logger, successCallback: () => void): void { + const xhr = new XMLHttpRequest(); + xhr.open('GET', getKantarURL(url, userId)); + xhr.timeout = 5000; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status >= 200) { + if (xhr.status < 300) { + successCallback(); + LOG.debug(`ID sent to Kantar: ${userId}`); + } else { + LOG.error(`Kantar request failed: ${xhr.status}: ${xhr.statusText}`); + } + } + }; + + xhr.send(); +} + +function getKantarURL(url: string, userId: string): string { + let query: Record = { + vendor: 'snowplow', + cs_fpid: userId, + c12: 'not_set', + }; + return ( + url + + '?' + + Object.keys(query) + .map((key) => key + '=' + query[key]) + .join('&') + ); +} diff --git a/plugins/browser-plugin-focalmeter/test/request.test.ts b/plugins/browser-plugin-focalmeter/test/request.test.ts new file mode 100644 index 000000000..6c609e67b --- /dev/null +++ b/plugins/browser-plugin-focalmeter/test/request.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import { addTracker, SharedState } from '@snowplow/browser-tracker-core'; +import { enableFocalMeterIntegration, FocalMeterPlugin } from '../src'; + +describe('AdTrackingPlugin', () => { + // Mock XHR network requests + let xhrMock: Partial; + let xhrOpenMock: jest.Mock; + let queuedStateChangeCallbacks: any[] = []; + let domain = 'https://kantar.com'; + + beforeEach(() => { + xhrOpenMock = jest.fn(); + xhrMock = { + open: xhrOpenMock, + send: jest.fn(), + setRequestHeader: jest.fn(), + withCredentials: true, + status: 200, + response: '', + readyState: 4, + }; + + Object.defineProperty(xhrMock, 'onreadystatechange', { + set: (val) => { + queuedStateChangeCallbacks.push(val); + }, + }); + + jest.spyOn(window, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest); + }); + + it('makes a request to Kantar endpoint with user ID', async () => { + let tracker = createTrackerWithPlugin(); + enableFocalMeterIntegration({ kantarEndpoint: domain }); + + tracker?.trackPageView(); + let userId = tracker?.getDomainUserId(); + + await checkMock(() => { + expect(xhrOpenMock).toHaveBeenCalledTimes(2); + expect(xhrOpenMock).toHaveBeenLastCalledWith('GET', `${domain}?vendor=snowplow&cs_fpid=${userId}&c12=not_set`); + }); + }); + + it('makes a request to Kantar endpoint when user ID changes', async () => { + let tracker = createTrackerWithPlugin(); + enableFocalMeterIntegration({ kantarEndpoint: domain }); + + // Doesn't make a request if anonymous tracking + tracker?.enableAnonymousTracking(); + tracker?.trackPageView(); + await checkMock(() => { + expect(xhrOpenMock).toHaveBeenCalledTimes(1); + }); + + // Makes a request when disabling anonymous tracking + tracker?.disableAnonymousTracking(); + tracker?.trackPageView(); + let userId = tracker?.getDomainUserId(); + await checkMock(() => { + expect(xhrOpenMock).toHaveBeenCalledTimes(2); + expect(xhrOpenMock).toHaveBeenLastCalledWith('GET', `${domain}?vendor=snowplow&cs_fpid=${userId}&c12=not_set`); + }); + + // Doesn't make another request since user ID didn't change + tracker?.trackPageView(); + await checkMock(() => { + expect(xhrOpenMock).toHaveBeenCalledTimes(1); + }); + }); + + it('can work with multiple trackers', async () => { + let tracker1 = createTrackerWithPlugin(); + let tracker2 = createTrackerWithPlugin(); + + enableFocalMeterIntegration( + { + kantarEndpoint: domain, + }, + [tracker1!.namespace, tracker2!.namespace] + ); + + // Makes requests for both trackers + tracker1?.trackPageView(); + await checkMock(() => expect(xhrOpenMock).toHaveBeenCalledTimes(2)); + tracker2?.trackPageView(); + await checkMock(() => expect(xhrOpenMock).toHaveBeenCalledTimes(2)); + + // Doesn't make any more requests for the trackers + tracker1?.trackPageView(); + await checkMock(() => expect(xhrOpenMock).toHaveBeenCalledTimes(1)); + tracker2?.trackPageView(); + await checkMock(() => expect(xhrOpenMock).toHaveBeenCalledTimes(1)); + }); + + function createTrackerWithPlugin(id: string | undefined = undefined) { + const state = new SharedState(); + id ??= 'sp-' + Math.random(); + + return addTracker(id, id, 'js-3.0.0', '', state, { + stateStorageStrategy: 'cookie', + encodeBase64: false, + plugins: [FocalMeterPlugin()], + }); + } + + function checkMock(callback: () => void) { + return new Promise((resolve) => { + setTimeout(() => { + callback(); + queuedStateChangeCallbacks.forEach((callback) => callback()); + queuedStateChangeCallbacks = []; + xhrOpenMock.mockReset(); + resolve(true); + }, 100); + }); + } +}); diff --git a/plugins/browser-plugin-focalmeter/tsconfig.json b/plugins/browser-plugin-focalmeter/tsconfig.json new file mode 100644 index 000000000..4082f16a5 --- /dev/null +++ b/plugins/browser-plugin-focalmeter/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/rush.json b/rush.json index 9dccb37aa..2959be589 100644 --- a/rush.json +++ b/rush.json @@ -541,6 +541,12 @@ "projectFolder": "plugins/browser-plugin-enhanced-consent", "reviewCategory": "plugins", "versionPolicyName": "tracker" + }, + { + "packageName": "@snowplow/browser-plugin-focalmeter", + "projectFolder": "plugins/browser-plugin-focalmeter", + "reviewCategory": "plugins", + "versionPolicyName": "tracker" } ] }