-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
patmmccann
merged 12 commits into
prebid:master
from
Stanislavsky34200:yandex-analytics-adapter
Jan 30, 2024
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
ef8737b
Yandex Analytics Adapter: Initial release
Stanislavsky34200 4928ac2
Release preparations
enovikov11 d1a3861
Merge pull request #1 from enovikov11/yandex-analytics-adapter
enovikov11 a04fc31
Updated trackable events
enovikov11 09bfc62
Updated trackable events
enovikov11 b338084
tag URL
enovikov11 44b7c15
Added tests and chanded init logic
Stanislavsky34200 54f7c8f
Fixed already loaded script scenario
Stanislavsky34200 a9d14a5
One level of object destruction
enovikov11 d524479
Global domain, yandex.com
enovikov11 1f7cf7e
Removed script insertion logic
Stanislavsky34200 27dbb4c
Update yandexAnalyticsAdapter.md
enovikov11 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: {}, | ||
}]); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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