Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yandex Analytics Adapter: initial release #10876

Merged
merged 12 commits into from
Jan 30, 2024
222 changes: 222 additions & 0 deletions modules/yandexAnalyticsAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import buildAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
import adapterManager from '../src/adapterManager.js';
import { logError, logInfo } from '../src/utils.js';
import CONSTANTS from '../src/constants.json';
import * as events from '../src/events.js';

const tagTlds = ['com', 'ru'];
const fileNames = ['tag', 'int']
const tagUrlTemplate = 'https://mc.yandex.{tld}/metrika/{file}.js';
export const tagURLs = tagTlds.flatMap((tld) => {
const partialTemplate = tagUrlTemplate.replace('{tld}', tld);
return fileNames.map((file) => partialTemplate.replace('{file}', file));
});

const timeoutIds = {};
const tryUntil = (operationId, conditionCb, cb) => {
if (!conditionCb()) {
cb();
timeoutIds[operationId] = setTimeout(
() => tryUntil(conditionCb, conditionCb, cb),
100
);
}
};
const waitFor = (operationId, conditionCb, cb) => {
if (conditionCb()) {
cb();
} else {
timeoutIds[operationId] = setTimeout(() => waitFor(conditionCb, cb), 100);
}
};

const clearWaitForTimeouts = (timeouts) => {
timeouts.forEach((timeoutID) => {
if (timeoutIds[timeoutID]) {
clearTimeout(timeoutIds[timeoutID]);
}
});
};

const SEND_EVENTS_BUNDLE_TIMEOUT = 1500;
const {
EVENTS: {
BID_REQUESTED,
BID_RESPONSE,
BID_ADJUSTMENT,
BID_WON,
BIDDER_DONE,
AUCTION_END,
BID_TIMEOUT,
},
} = CONSTANTS;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this declaration work? I get a type error when I try this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it works, in fact it passes autotests + has been tested manually. Please share error text with me, so we can debug it.

Copy link
Contributor

@spotxslagle spotxslagle Jan 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`Uncaught` TypeError: Cannot read properties of undefined (reading 'BID_REQUESTED')
    at <anonymous>:3:5

You cannot nest destructuring assignment. You need to removed the EVENTS object layer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for improvement, done.


export const EVENTS_TO_TRACK = [
BID_REQUESTED,
BID_RESPONSE,
BID_ADJUSTMENT,
BID_WON,
BIDDER_DONE,
AUCTION_END,
BID_TIMEOUT,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't these all be undefined since they're declared inside the EVENTS object above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There would be ["bidRequested", "bidResponse", "bidAdjustment", ...]. lines 42-52 have destruction syntax, so for example BID_REQUESTED will be CONSTANTS.EVENTS.BID_REQUESTED, that is "bidRequested" as defined here and so on

];

const yandexAnalytics = Object.assign(buildAdapter({ analyticsType: 'endpoint' }), {
bufferedEvents: [],
initTimeoutId: 0,
counters: {},
counterInitTimeouts: {},
oneCounterInited: false,

onEvent: (eventName, eventData) => {
const innerEvent = {
event: eventName,
data: eventData,
};
yandexAnalytics.bufferedEvents.push(innerEvent);
},

sendEvents: () => {
if (yandexAnalytics.bufferedEvents.length) {
const data = yandexAnalytics.bufferedEvents.splice(
0,
yandexAnalytics.bufferedEvents.length
);

Object.keys(yandexAnalytics.counters).forEach((counterId) => {
yandexAnalytics.counters[counterId].pbjs(data);
});
}
setTimeout(yandexAnalytics.sendEvents, SEND_EVENTS_BUNDLE_TIMEOUT);
},

onCounterInit: (counterId) => {
yandexAnalytics.counters[counterId] = window[`yaCounter${counterId}`];
logInfo(`Initialized metrika counter ${counterId}`);
if (!yandexAnalytics.oneCounterInited) {
yandexAnalytics.oneCounterInited = true;
setTimeout(() => {
yandexAnalytics.sendEvents();
}, SEND_EVENTS_BUNDLE_TIMEOUT);
clearTimeout(yandexAnalytics.initTimeoutId);
}
},

getCounterScript: () => {
const presentScript = document.querySelector(
tagURLs.map((tagUrl) => `script[src="${tagUrl}"]`).join(',')
);
if (presentScript) {
return presentScript;
}

const script = window.document.createElement('script');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isnt allowed, please remove the script.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

script.setAttribute('src', tagURLs[0]);
window.document.body.appendChild(script);
return script;
},

enableAnalytics: (config) => {
yandexAnalytics.options = (config && config.options) || {};
const { counters } = yandexAnalytics.options || {};
const validCounters = counters.filter((counterOptions) => {
if (!counterOptions) {
return false;
}

if (isNaN(counterOptions) && isNaN(counterOptions.id)) {
return false;
}

return true;
});

if (!validCounters.length) {
logError('options.counters contains no valid counter ids');
return;
}

const unsubscribeCallbacks = [
() => clearWaitForTimeouts(['body', 'countersInit']),
];

yandexAnalytics.initTimeoutId = setTimeout(() => {
yandexAnalytics.bufferedEvents = [];
unsubscribeCallbacks.forEach((cb) => cb());
logError(`Can't find metrika counter after 25 seconds.`);
logError('Aborting analytics provider initialization.');
}, 25000);

events.getEvents().forEach((event) => {
if (event && EVENTS_TO_TRACK.indexOf(event.eventType) >= 0) {
yandexAnalytics.onEvent(event.eventType, event);
}
});

const eventsCallbacks = [];
EVENTS_TO_TRACK.forEach((eventName) => {
const eventCallback = yandexAnalytics.onEvent.bind(null, eventName);
eventsCallbacks.push([eventName, eventCallback]);
events.on(eventName, eventCallback);
});

unsubscribeCallbacks.push(() => {
eventsCallbacks.forEach(([eventName, cb]) => events.off(eventName, cb));
});

// Waiting for counters appearing on the page
const presentCounters = validCounters.map(
(options) => typeof options === 'object' ? options.id : options
);
let allCountersInited = false;
if (presentCounters.length) {
tryUntil('countersInit', () => allCountersInited, () => {
allCountersInited = presentCounters.reduce((result, counterId) => {
if (yandexAnalytics.counters[counterId]) {
return result && true;
}

if (window[`yaCounter${counterId}`]) {
yandexAnalytics.onCounterInit(counterId);
return result && true;
}

return false;
}, true);
});
}

// Inserting counter script and initializing counters
if (!allCountersInited) {
const coutnersToInit = validCounters.filter((options) => {
const id = options === 'object' ? options.id : options;
return !yandexAnalytics.counters[id];
});
waitFor('body', () => window.document.body, () => {
const tag = yandexAnalytics.getCounterScript();
const onScriptLoad = () => {
coutnersToInit.forEach((counterOptions) => {
const id = counterOptions === 'object'
? counterOptions.id
: counterOptions;
window[`yaCounter${id}`] =
new window.Ya.Metrika2(counterOptions);
yandexAnalytics.onCounterInit(id);
});
};
unsubscribeCallbacks.push(() => {
tag.removeEventListener('load', onScriptLoad);
});
tag.addEventListener('load', onScriptLoad);
logInfo('Inserting metrika tag script');
});
}
},
});

adapterManager.registerAnalyticsAdapter({
adapter: yandexAnalytics,
code: 'yandexAnalytics'
});

export default yandexAnalytics;
40 changes: 40 additions & 0 deletions modules/yandexAnalyticsAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Overview

```
Module Name: Yandex Analytics Adapter
Module Type: Analytics Adapter
Maintainer: prebid@yandex-team.com
```

# Description

This adapter is designed to work with [Yandex Metrica](https://metrica.yandex.com/about) - Top-5 worldwide web analytics tool.

Disclosure: provider loads Metrica Tag build based on https://github.com/yandex/metrica-tag, ~60 kB gzipped.

## How to setup provider

Register your application on https://metrica.yandex.com/ and get counter id

Init provider like this, where `123` is your counter id

Note: If you have Single Page Application (SPA), [configure your tag](https://yandex.com/support/metrica/code/counter-spa-setup.html).

```javascript
pbjs.enableAnalytics({
provider: 'yandexAnalytics',
options: {
counters: [
123,
{
id: 1234,
clickmap: true,
}
],
},
});
```

## Where to find data

Go to https://metrika.yandex.ru/dashboard -> Prebid Analytics
Loading