React Components Tracking — Impressions and Clicks

Sanish Kumar
5 min readSep 23, 2019

--

https://dev.to/maciekgrzybek/create-section-navigation-with-react-and-intersection-observer-fg0

It all started one year back, around Aug ’18 for me. We had just launched our new marketplace of wedding vendors and service providers. Like every other B2C marketplace, our home page had few widgets(eg: Carousels, Category Links, Top Picks, Top X items in Top Selling Categories and Subcategories, Recently viewed items… etc). Some of the top carousels were paid advertisement by vendors.

Then there was SRP(Search result Page), and VDP(Vendor Description Page). Each had their own configuration of design with cards and widgets floating around dynamically.

Even some of the cards in SRP were boosted by some algorithm as they belonged to paid vendors.

Within a few days, we realized we needed a way to track all the views and clicks of every item appearing on the screen. In case you have seen your tweet’s analytics or if you have noticed impressions on your Instagram feed/stories (you need a business account), you will quickly understand what the need was or the direction in which we had to think.

TL;DR

We thought of some config, basic algorithm, made some API and used jQuery + waypoint to attach viewport tracking on each card item. Yeah, we were using vanilla JS back then. I am not going into details of it as it is not the best solution as compared to what I have built now.

1 year later...

Now I was working in an eCommerce workplace, and there came a similar requirement. Only this time, we were using ReactJS and I was supposed to track both clicks and impressions(views). Clicks tracking wouldn’t be an issue. So, I started the research about impressions tracking and came across IntersectionObserver among other possible ways.

Before Intersection Observer arrived, this is what people used to do:

https://hackernoon.com/tracking-element-visibility-with-react-and-the-intersection-observer-api-7dfaf3a47218

Now, if you attach this above function to a scroll eventListener, it will work. But the only problem is the number of events fired per sec increases rapidly as you attach this to more number of elements, even when they are not in the viewport.

You might apply debouncing but it is still a performance issue and gives a lagging user experience.

What the heck is Intersection Observer?

The Intersection Observer is an API provided by modern browsers to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.

Let us first look into its use-cases. I mean, apart from Ads Impression Tracking.

  • Lazy loading of images, or anything else for that matter, that will enter the viewport
  • Occlusion culling — Rendering an object when we are about to enter the viewport edges
  • Impression discounting feedback for recommendation systems
  • Infinite scrolling

And it does all this without any performance or lagging issue. Now you are sold about the idea of it, right? Although, I am only going to talk about Ads impressions here. But, you may refer to links at the bottom of this post for other use-cases.

So, how does it work? Glad, you asked.

https://hackernoon.com/tracking-element-visibility-with-react-and-the-intersection-observer-api-7dfaf3a47218

It takes a callback as a parameter which is called when the element enters the viewport with the given threshold.

What are the options?

  • root: the relative element to which tracking should be done(or browser viewport by default)
  • rootMargin: any relative margin you want to provide to root. Example
  • threshold: a single number between 0 and 1 or an array of numbers. Every time the threshold is reached, it will fire the callback function. You might trigger separate code based on each threshold as callback receives current intersectionRatio.

Also, since we are doing impression tracking, our requirement might be to trigger the callback only when the component has enter ‘X%’ of it in the viewport and has stayed on the screen for at least ‘Y sec’. Looks like we need some customization. Let’s build a HOC(Higher Order Component). We will take both X(as in 100%) and Y(as in 1 second) as 1 in our example.

Also, since we are tracking Ads, we will be passing some data like tracker-id to the server log, which means we will be passing some custom `data-attributes` to the child component of HOC.

While we are at it, let’s also make clicks tracking a part of our HOC.

Also, clicks are required on even buttons(like: “Add to Cart” Button) but impressions are not necessarily. So, adding some optional props on our HOC.

To keep the code short, I will be using @researchgate/react-intersection-observer npm package.

npm i @researchgate/react-intersection-observer

It also exposes and unobserve function in callback which we can use if our requirement is to count only one impression per Component mount even if the user scrolls back to view the same component more than once.

So, here is our awesome HOC.

import React, { Component } from "react";import Observer from "@researchgate/react-intersection-observer";
class ImpressionClickTrackerHOC extends Component {
handleChange = (event, unobserve) => { if (event.isIntersecting && event.intersectionRatio >= 1) { this.recordedTimeout = setTimeout(() => {
// Send logs to server
console.log("IMPRESSION" , { ...event.target.dataset }); /** Use this for only tracking once per mount **/ // if (event.isIntersecting) { // unobserve(); // } }, 1000); // Assuming impression is valid only after 1 second return; } clearTimeout(this.recordedTimeout); };
handleClick = event => { // Send logs to server console.log(this.props.clickEvent || "UNKNOWN_CLICK_EVENT", { ...event.currentTarget.dataset }); };
render() { return ( <Observer onChange={this.handleChange} onClick={this.handleClick} threshold={1} disabled={this.props.disableViewportTracking || false} > {React.cloneElement(this.props.children, { onClick: this.handleClick })} </Observer> ); }}export default ImpressionClickTrackerHOC;
  • You can optionally pass ‘disableViewportTracking’ prop as ‘true’ to disable impression tracking.
  • Also, it allows you to pass a custom click event name as ‘clickEvent’.
  • Any other data related to tracker can be passed to the child component of the HOC as data-attributes and we will catch then as event.target.dataset.

Here is an example of using our awesome HOC:

<ImpressionClickTrackerHOC  clickEvent={`ADD-TO-CART`}  disableViewportTracking={false}>    <div      data-tracker-id={trackerId}      data-display-name={displayName}      data-listing-id={listingId}    >       ...    </div></ImpressionClickTrackerHOC>

Browser Support:

At my point of writing, all major browsers have already started supporting this API. So, no problem at all. Worst case scenario: there is a polyfill available for this.

--

--