Website Personalization: Tracking and Reporting Impressions and Clicks on the Personalized Messages

| | 5 min read

We are experimenting with the website personalization engine on our website. Every marketer looks at one important matrix from a personalized message delivery system: the Click Through Rates (CTR) on the ads we deliver through the system.

Click Through Rate (CTR)

The click-through rate (CTR) is a metric used to measure the effectiveness of the message you deliver to the visitor. It is calculated by taking the number of clicks the message receives and dividing it by the number of times it is shown. The formula is CTR = (clicks/impressions) * 100%. For example, if an ad receives five clicks and is shown 100 times, the CTR would be (5/100) * 100% = 5%.



The Problem with Tracking Impressions

The report generated using Looker Studio from the GA4 data

Tracking impressions is a bit tricky. Traditional methods of tracking impressions involve sending an impression tracking event as soon as the message is loaded on the page, regardless of whether the user sees it. This can lead to inflated impression counts and inaccurate campaign performance measurement.

For example, if there are three different advertisement slots on a webpage and all three ads are loaded when the page is first loaded, it would count as three impressions, even if the user never scrolls down to view the third ad or if the user bounces off the site.

I will write about the experiment we did on one of our popular pages in detail later if you observe the impressions on the advt. slots table, you will find the issue. The three slots mentioned there were placed at the articles' beginning, mid and end, but the impression count is the same for all. There is very less chance that 100% of visitors to this page scroll through the entire article (the article is a boring list of fortune 500 companies - it lists the top 500 fortune companies and their websites)

IntersectionObserver Came to Rescue

One solution to this problem is to use the IntersectionObserver API to track advertisement impressions only when the advertisement is visible in the user's viewport. The IntersectionObserver API1 allows you to detect when an element comes into or out of the viewport and can be used to trigger events based on the element's visibility.

We can use this feature to track the impressions of advertisements only when they are visible to the user. Here's an example of how this can be done: (Skip to the video demo below this section if this becomes too technical)

const ad = document.querySelectorAll('.placeholder');

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.intersectionRatio > 0 && !'tracked')) {
      // Send an impression tracking event

ad.forEach(ad => {
    ad.adDetails = {
    id: '123',
    name: 'Example Ad'

In this example,

querySelectorAll method is used to select all elements with the class placeholder , and it will return a NodeList of elements. Then using the forEach loop, we can access each element individually. For each element, an IntersectionObserver is created, and it is set to observe the element.

In the callback function IntersectionObserver, we check if the advertisement is visible in the viewport by inspecting the intersectionRatio property of the entry object. If the intersectionRatio is greater than 0, and the element does not contain the class 'tracked', we send the impression tracking event and add the class 'tracked' to the element.

By doing this, we ensure that the impression is tracked only once per page view, and the class 'tracked' helps us track which ad has been followed and which has not.

Additionally, you can also pass some information about the advertisement to the trackImpression function, such as the advertisement's ID or name, by storing it on the element as a property, as shown in the above example.

How do we track impressions if the advt needs to display at least 50% pixels for 1 sec?

To track an impression when an advertisement needs to display at least 50% pixels for 1 second, you can use the IntersectionObserver API in combination with a setTimeout function.

You can use the IntersectionObserver to check if the advertisement is visible in the viewport and if the intersection ratio is greater than or equal to 50%. If it is, you can start a setTimeout function that will wait for 1 second before sending the impression tracking event.

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.intersectionRatio > 0.5 && !'tracked')) {
      timeoutId = setTimeout(() => {
        if (entry.intersectionRatio > 0.5 ) {
      }, 1000);
    } else {
}, { threshold: 0.5 });

In this example, the system checks whether the intersectionRatio is greater than or equal to 0.5; if true it sets a timeout of 1 sec. After that, it calls the trackImpression function passing the With this approach, the impression will be tracked only when the ad is visible in the viewport and displayed at least 50% of pixels for 1 sec.

The "intersection ratio" is a value between 0 and 1 representing the percentage of the element currently visible in the viewport. For example, if an element is exactly half visible, the intersection ratio would be 0.5.

The "threshold" is the value you specify when creating the IntersectionObserver, which determines when the observer's callback function should be executed. When the intersection ratio of the observed element reaches or exceeds the threshold value, the observer's callback function is executed.

Using the IntersectionObserver API, in this way, allows for more accurate tracking of advertisement impressions and can help to understand the performance of online advertising campaigns better.

It should be noted that some older browsers do not support the IntersectionObserver API, so it's important to check the browser compatibility before using this method, and you may want to consider providing a fallback for browsers that don't support the IntersectionObserver API.

The Result

I checked with GA4 Debug mode after implementing the IntersectionObserver API, and now it is tracking the impressions correctly. The event to track impressions will get triggered only when the advt is visible on the viewport. See the video demo of this. I will publish a separate article detailing the experiment and the results.