Skip to content

Commit

Permalink
Yandex Analytics Adapter: initial release (#10876)
Browse files Browse the repository at this point in the history
* Yandex Analytics Adapter: Initial release

* Release preparations

* Updated trackable events

* Updated trackable events

* tag URL

* Added tests and chanded init logic

* Fixed already loaded script scenario

* One level of object destruction

* Global domain, yandex.com

* Removed script insertion logic

* Update yandexAnalyticsAdapter.md

---------

Co-authored-by: Stanislavsky34200 <stanislavsky34@gmail.com>
  • Loading branch information
enovikov11 and Stanislavsky34200 authored Jan 30, 2024
1 parent c161e0c commit eee58b0
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 0 deletions.
154 changes: 154 additions & 0 deletions modules/yandexAnalyticsAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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 timeoutIds = {};
const tryUntil = (operationId, conditionCb, cb) => {
if (!conditionCb()) {
cb();
timeoutIds[operationId] = setTimeout(
() => tryUntil(conditionCb, conditionCb, cb),
100
);
}
};

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

const SEND_EVENTS_BUNDLE_TIMEOUT = 1500;
const {
BID_REQUESTED,
BID_RESPONSE,
BID_ADJUSTMENT,
BID_WON,
BIDDER_DONE,
AUCTION_END,
BID_TIMEOUT,
} = CONSTANTS.EVENTS;

export const EVENTS_TO_TRACK = [
BID_REQUESTED,
BID_RESPONSE,
BID_ADJUSTMENT,
BID_WON,
BIDDER_DONE,
AUCTION_END,
BID_TIMEOUT,
];

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(`Found metrika counter ${counterId}`);
if (!yandexAnalytics.oneCounterInited) {
yandexAnalytics.oneCounterInited = true;
setTimeout(() => {
yandexAnalytics.sendEvents();
}, SEND_EVENTS_BUNDLE_TIMEOUT);
clearTimeout(yandexAnalytics.initTimeoutId);
}
},

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

if (isNaN(counterId)) {
return false;
}

return true;
});

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

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

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

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

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

let allCountersInited = false;
tryUntil('countersInit', () => allCountersInited, () => {
allCountersInited = validCounters.reduce((result, counterId) => {
if (yandexAnalytics.counters[counterId]) {
return result && true;
}

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

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

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

export default yandexAnalytics;
36 changes: 36 additions & 0 deletions modules/yandexAnalyticsAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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 use 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.
Insert counter initialization code obtained from the page https://metrica.yandex.com/settings?id={counterId} into your html code.
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,
],
},
});
```

## Where to find data

Go to https://metrika.yandex.com/dashboard -> Prebid Analytics
147 changes: 147 additions & 0 deletions test/spec/modules/yandexAnalyticsAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as sinon from 'sinon';
import yandexAnalytics, { EVENTS_TO_TRACK } from 'modules/yandexAnalyticsAdapter.js';
import * as log from '../../../src/utils.js'
import * as events from '../../../src/events.js';

describe('Yandex analytics adapter testing', () => {
const sandbox = sinon.createSandbox();
let clock;
let logError;
let getEvents;
let onEvent;
const counterId = 123;
const counterWindowKey = 'yaCounter123';

beforeEach(() => {
yandexAnalytics.counters = {};
yandexAnalytics.counterInitTimeouts = {};
yandexAnalytics.bufferedEvents = [];
yandexAnalytics.oneCounterInited = false;
clock = sinon.useFakeTimers();
logError = sandbox.stub(log, 'logError');
sandbox.stub(log, 'logInfo');
getEvents = sandbox.stub(events, 'getEvents').returns([]);
onEvent = sandbox.stub(events, 'on');
sandbox.stub(window.document, 'createElement').callsFake((tag) => {
const element = {
tag,
events: {},
attributes: {},
addEventListener: (event, cb) => {
element.events[event] = cb;
},
removeEventListener: (event, cb) => {
chai.expect(element.events[event]).to.equal(cb);
},
setAttribute: (attr, val) => {
element.attributes[attr] = val;
},
};
return element;
});
});

afterEach(() => {
window.Ya = null;
window[counterWindowKey] = null;
sandbox.restore();
clock.restore();
});

it('fails if timeout for counter insertion is exceeded', () => {
yandexAnalytics.enableAnalytics({
options: {
counters: [
123,
],
},
});
clock.tick(25001);
chai.expect(yandexAnalytics.bufferedEvents).to.deep.equal([]);
sinon.assert.calledWith(logError, `Can't find metrika counter after 25 seconds.`);
sinon.assert.calledWith(logError, `Aborting yandex analytics provider initialization.`);
});

it('fails if no valid counters provided', () => {
yandexAnalytics.enableAnalytics({
options: {
counters: [
'abc',
],
},
});
sinon.assert.calledWith(logError, 'options.counters contains no valid counter ids');
});

it('subscribes to events if counter is already present', () => {
window[counterWindowKey] = {
pbjs: sandbox.stub(),
};

getEvents.returns([
{
eventType: EVENTS_TO_TRACK[0],
},
{
eventType: 'Some_untracked_event',
}
]);
const eventsToSend = [{
event: EVENTS_TO_TRACK[0],
data: {
eventType: EVENTS_TO_TRACK[0],
}
}];

yandexAnalytics.enableAnalytics({
options: {
counters: [
counterId,
],
},
});

EVENTS_TO_TRACK.forEach((eventName, i) => {
const [event, callback] = onEvent.getCall(i).args;
chai.expect(event).to.equal(eventName);
callback(i);
eventsToSend.push({
event: eventName,
data: i,
});
});

clock.tick(1501);

const [ sentEvents ] = window[counterWindowKey].pbjs.getCall(0).args;
chai.expect(sentEvents).to.deep.equal(eventsToSend);
});

it('waits for counter initialization', () => {
window.Ya = {};
// Simulatin metrika script initialization
yandexAnalytics.enableAnalytics({
options: {
counters: [
counterId,
],
},
});

// Sending event
const [event, eventCallback] = onEvent.getCall(0).args;
eventCallback({});

const counterPbjsMethod = sandbox.stub();
window[`yaCounter${counterId}`] = {
pbjs: counterPbjsMethod,
};
clock.tick(2001);

const [ sentEvents ] = counterPbjsMethod.getCall(0).args;
chai.expect(sentEvents).to.deep.equal([{
event,
data: {},
}]);
});
});

0 comments on commit eee58b0

Please sign in to comment.