Impressions and clickthroughs
Impressions and clickthroughs
This article will cover impression and clickthrough events and how they’re used to measure the impact of a campaign. You’ll also see best practices for avoiding bias when implementing these events.
Measuring impact through impressions and clickthroughs
After running a campaign, merchandisers will want to know what kind of impact it had. Qubit measures impact using an A/B test (a randomized controlled test). The Placement will collect the data for this measurement.
To understand the impact, you need to know who saw it, what they did afterward, and then compare this to people who didn’t see the campaign:
-
To understand who was eligible to see the campaign, the Coveo Experience Hub has
Placement impression events
emitted when you callonImpression
. -
To measure engagement, the Coveo Experience Hub have
Placement interaction events
emitted when you callonClickthrough
. -
To determine what happened afterward, the Experience Hub uses your
qubit protocol
implementation (and particularlyecBasketTransactionSummary
for conversions).
There are both generic and product-specific impression and interaction events.
For a meaningful comparison between people that saw the campaign and people that didn’t, they must be allocated randomly. This random allocation ensures there’s no bias and that any differences measured will be because caused by the campaign itself and not because the two groups exhibit inherently different behaviors.
Therefore, Placements should call onImpression
for users:
-
who saw the campaign
-
who would have seen the campaign if they had not been randomly allocated to the control group.
While the examples below refer to Placements injected via the Smartserve script, the principles apply equally to Placements retrieved via API and reported on using callback URLs directly.
Introducing bias
Here are some examples of how inadvertent bias might be introduced within your Placement implementation:
In the following example, if there’s content, the onImpression
event is emitted after the user has scrolled.
However, if there’s no content, the onImpression
event is emitted regardless of whether they scroll.
module.exports = function renderPlacement ({
elements: [$el],
content,
onImpression,
onEnterViewport
}) {
// An implementation that introduces bias and therefore invalidates your data.
// Don't do this!
if (content) {
// When there is content, the visitor is registered only when they scroll
onEnterViewport($el, onImpression)
} else {
// When there is no content, all visitorsvare registered
onImpression()
}
}
In other words, for this campaign, you’ll be comparing people that scroll down to our element with the general population on site. These groups are not allocated randomly and are likely to exhibit inherently different behaviors.
The problem with this approach is that if you see a difference, it may not be because of the campaign but because you’re comparing highly engaged users (people who scrolled to the campaign) with everyone else.
Similarly, the following example compares users who scroll a lot with users who scroll a little. Again, you can’t be sure whether any difference you see in the reporting will be because of the campaign or because the two groups exhibit inherently different behaviors:
module.exports = function renderPlacement ({
elements: [$header, $footer],
content,
onImpression,
onEnterViewport
}) {
// An implementation that introduces bias and therefore invalidates your data.
// Don't do this!
if (content) {
// Here we only show the variant to users who scroll a little bit
onEnterViewport($header, onImpression)
} else {
// Here we only show the variant to users who scroll a lot
onEnterViewport($footer, onImpression)
}
}
Best practices for avoiding bias
Set up impression events upfront before branching into control and variant (content and no content)
Doing this has several advantages. Firstly, it will make it much harder to introduce conditions that specifically bias either the control or the variant. Secondly, it should make it much easier to review the code and determine whether it introduces bias.
Emit impression events before performing any rendering logic
In addition to the advantages stated previously, emitting the impression events first ensures that any errors or bugs in the rendering logic will not affect whether or not we send impression events. This approach also means errors can be caught and reported independently and help avoid compounding rendering problems by introducing statistical bias.
Create a common element to detect entering the viewport
To make sure you’re scrolling to an equal point in the page in the control and variation (content and no content), you can make do the following:
-
Create a common element.
-
Inject it regardless of where you intend to render your content.
Do not use the same common element for rendering the content and tracking impressions. Instead, use one common element for content and another common element for emitting the impression events. |
You can then use that element to trigger your onImpression
event.
Here’s an example of that approach:
const React = require('preact')
const {
style,
onEvent,
insertBefore,
onEnterViewport,
restoreAll
} = require('@qubit/utils/dom')()
module.exports = function renderPlacement ({
content,
onImpression,
onClickthrough,
onRemove,
elements: [target]
}) {
onRemove(restoreAll)
// Create a dummy element here that you will use to track who saw this Placement
// Using the same element regardless of content ensures consistency in your tracking
const element = document.createElement('div')
insertBefore(target, element)
// Call onImpression when our element enters the viewport
// This ensures you are not only reporting on people who have seen the banner,
// but also on people in the control and scrolled to where the banner would have been if they were in the variant.
// Calling onImpression upfront helps to ensure you don't unintentionally exclude visitors when performing an AB test
onEnterViewport(element, onImpression)
if (content) {
// Render content
} else {
// Add some logic for control
}
}
Report clickthroughs for control when possible
If you are injecting a new element on the page, you won’t be able to do this.
However, if you are replacing an existing banner, for example, that has click handlers, you’ll need to make sure you call onClickthrough
when that banner is clicked too.
This approach will ensure you can unbiasedly compare the clicks on the variant to the ones on the control.
Example:
const React = require('preact')
const {
style,
onEvent,
insertBefore,
onEnterViewport,
restoreAll,
replace
} = require('@qubit/utils/dom')()
module.exports = function renderPlacement ({
content,
onImpression,
onClickthrough,
onRemove,
elements: [target]
}) {
onRemove(restoreAll)
// Here it's assumed our content is configured to return a cta
const { cta } = content
// `onImpression` logic hidden for conciseness
if (content) {
// Ensure your new component calls `onClickthrough`
const banner = <div><a onClick={onClickthrough}>{cta}</a></div>
// Using a helper to replace the website's banner with our new one
replace(target, banner)
} else {
// Here we use another helper called `onEvent`
// and make sure our existing banner also calls `onClickthrough` for users that don't get content.
onEvent(target.querySelector('a'), 'click', onClickthrough)
}
}
Product events
In product recommendations Placements, it’s recommended that you also report on what products are being shown. This approach will give merchandisers some insight into what’s being recommended for both variant and control and what people are clicking.
To do that, you should pass 'product'
as the first argument to the function, and the product Id or product Ids as the second.
Example:
const React = require('preact')
const {
style,
onEvent,
insertBefore,
onEnterViewport,
restoreAll,
replace
} = require('@qubit/utils/dom')()
const { map, forEach } = require('slapdash')
module.exports = function renderPlacement ({
content,
onImpression,
onClickthrough,
onRemove,
elements: [target]
}) {
onRemove(restoreAll)
// This is the default schema for a Recommendation Placement
const { headline, recs } = content
const element = document.createElement('div')
insertBefore(target, element)
// Keeping this onImpression event here will ensure we are not causing bias with the generic (non-product) event
// It also makes it easier to debug and reason about your code
onEnterViewport(element, onImpression)
if (content) {
const productIds = recs.map(rec => rec.details.id)
// Both `onImpression` and `onClickthrough` can handle a string or an array of strings as the second argument.
// This way you only need to call this function once to report on all the ids being showed.
// You could add extra logic if you have products that are not immediately shown (hidden on other pages of the carousel) if you prefer.
onEnterViewport(element, () => onImpression('product', productIds))
// A simple carousel that returns a clickable image for each of the products recommended in our content
const carousel = (
<div>{renderRecs(recs, onClickthrough)}</div>
)
// Using a helper to replace the website's banner with our new one
replace(target, banner)
} else {
// If you are replacing an existing carousel, you can also report on the products that were shown, much like point 3 above.
const prodEls = target.querySelectorAll('.product_element')
// You'll need to find a way of getting the product ids from the dom. In the example below, it's assumed that your elements have a data-product-id attribute
const productIds = map(prodEls, el => el.dataset.productId)
onEnterViewport(element, () => onImpression('product', productIds))
// The `onClickthrough` events is also appended to each of the products already on the carousel
forEach(prodEls, el => {
const prodId = el.dataset.productId
const anchor = el.querySelector('a')
onEvent(anchor, 'click', () => onClickthrough('product', prodId))
})
}
}
function renderRecs(recs, onClickthrough) {
const clickHandler = (id) => onClickthrough('product', rec.details.id)
return recs.map((rec) => (
// Here the report only includes the specific ID the user clicked.
<a href={rec.details.url} onClick={clickHandler(rec.details.id)}>
<img src={rec.details.image_url} />
</a>
))
}