Guillermo de la Puente

How to set up the Brevo (Sendinblue) tracker in a React TypeScript application

Development

Published on / Updated on

RSS Feed RSS Feed

Sendinblue tracker docs. This is the old brand name, now it’s called Brevo and it’s green 🌱
Sendinblue tracker docs. This is the old brand name, now it’s called Brevo and it’s green 🌱

Edit Feb 24, 2023: added a few clarifications and tips.

Edit Ago 19, 2024: updated name Sendinblue to Brevo.

We chose Brevo (formerly Sendinblue) because its pricing model: cost doesn’t increase with the amount of contacts you have. Setting it up wasn’t as easy as I had hoped, so here I go over some tips to integrate the Brevo tracker easily in React/TypeScript applications.


What is the Brevo tracker?

The Brevo tracker is the system that lets us track user interactions and page views to enrich our CRM and trigger automations. They offer 2 ways of registering events: using a JavaScript tracker library and making HTTP requests directly to their API.

The JavaScript tracker is easy to set up, and it will automatically record some information about user visits. Once loaded, tracking events is as easy as executing window.sendinblue.track(eventName).

Combined with Brevo automations, all transactional and nurture sequence emails can be moved from an application’s backend codebase to Brevo, as well as the conditions on if and when they should be sent. That’s great!


1. Combine the JS Tracker with the API

Despite the recommendation in their docs to favor using the JavaScript implementation, privacy browser extensions like AdBlock can block requests to Brevo. I imagine that unstable connections could also result in missing events. So for important events that the user experience depends on, you’re better off tracking them directly from your backend.

If you’re using Brevo for automations with transactional emails or important nurture sequences, you should track those events using the RESTful API implementation.

Alternatively, if you need to send an email right away without triggering a workflow, don’t use the tracker. It’s better to trigger the email send directly from your backend using their Email API.


2. No TypeScript support

There aren’t official types for their JavaScript tracker, so if you want to use it from your TypeScript codebase, it’s best to define the types yourself.

Sharing here my configuration in case it’s useful for you:

type SendinblueTracker = {
  identify: (
    email: string,
    visitorProperties?: { [property: string]: any },
  ) => void;

  track: (
    eventName: string,
    visitorProperties?: { [property: string]: any },
    eventData?: { id?: string; data?: { [key: string]: any } },
  ) => void;

  page: (
    pageName: string,
    properties?: { [property: string]: any },
  ) => void;

  trackLink: (
    link: HTMLElement,
    properties?: { [property: string]: any },
  ) => void;
};

declare global {
  interface Window {
    sendinblue: SendinblueTracker;
  }
}

3. Waiting for the script to be ready before using it in Next.js

In Next.js, we’ll load the script with strategy='afterInteractive' like other tracking utilities. That means that the script execution will be deferred so window.sendinblue could take a bit to be defined. There is the onReady callback that can be used to wait for it. Note the onLoad callback isn’t called for inline scripts.

You might add any functionality directly in the callback as well, or set a state variable and leverage effects when there are other async variables that need to load:

Note that I implemented this back in 2022, when the company was called Sendinblue.


import Script from 'next/script';

export default function SendinblueScript() {
  const [sendinblueLoaded, setSendinblueLoaded] = useState(false);

  // here would go effects that identify the user and potentially the page
  // they depend on sendinblueLoaded and any other parameters of your application
  // ...
 
  return (
    <Script
      id='script-sendinblue'
      strategy='afterInteractive'
      onReady={() => {
        // using onReady because onLoad doesn't fire for inline scripts
        setSendinblueLoaded(true);
      }}
      dangerouslySetInnerHTML={{
        /* the code below is provided by Sendinblue */
        __html: `
        (function() {
          window.sib = {
              equeue: [],
              client_key: "${SENDINBLUE_CLIENT_KEY}"
          };
          /* OPTIONAL: email for identify request*/
          // window.sib.email_id = 'example@domain.com';
          window.sendinblue = {};
          for (var j = ['track', 'identify', 'trackLink', 'page'], i = 0; i < j.length; i++) {
          (function(k) {
              window.sendinblue[k] = function() {
                  var arg = Array.prototype.slice.call(arguments);
                  (window.sib[k] || function() {
                          var t = {};
                          t[k] = arg;
                          window.sib.equeue.push(t);
                      })(arg[0], arg[1], arg[2], arg[3]);
                  };
              })(j[i]);
          }
          var n = document.createElement("script"),
              i = document.getElementsByTagName("script")[0];
          n.type = "text/javascript", n.id = "sendinblue-js", n.async = !0, n.src = "https://sibautomation.com/sa.js?key=" + window.sib.client_key, i.parentNode.insertBefore(n, i), window.sendinblue.page();
        })();
        `,
      }}
    />
  );
}

4. Wrap the Brevo tracker functions in a lib file

It’s good practice to wrap libraries and re-export them preconfigured for your application. By convention, I place them in the lib folder.

My lib file handles:

  • Checking for the feature flag

  • Defining our application’s event names in an enum

  • Defining the application paths that we want to explicitly track in an enum

  • Logs calls when in development environment

Note that I implemented this back in 2022, when the company was called Sendinblue.

// lib/sendinblue.ts

/* eslint-disable no-console */

// Get the env and the feature flag from configuration.
export const sendinblueEnabled = process.env.SENDINBLUE_ENABLED === '1';

export enum EventName {
  // add here your application events
  EXAMPLE_CREATED = 'exampleCreated',
}

export enum PageName {
  // add here page names that you track explicitly
  EXAMPLE_PAGE = 'examplePage',
}

// Contact properties, like FIRSTNAME, LASTNAME and custom ones
// Once you know, define those keys and their expected values instead of using `any`
type VisitorProperties = { [property: string]: any };

/**
 * Identifies the visitor user with an email address
 */
export function identify(
  email: string,
  visitorProperties?: VisitorProperties,
) {
  if (sendinblueEnabled) {
    if (process.env.NODE_ENV === 'development') {
      console.log('[sendinblue.identify]', email, visitorProperties);
    }
    window.sendinblue.identify(email, visitorProperties);
  }
}

/**
 * Tracks an event
 */
export function track(
  eventName: EventName,
  visitorProperties?: VisitorProperties,
  eventData?: { id?: string; data?: { [key: string]: any } },
) {
  if (sendinblueEnabled) {
    if (process.env.NODE_ENV === 'development') {
      console.log(
        '[sendinblue.track]',
        eventName,
        visitorProperties,
        eventData,
      );
    }
    window.sendinblue.track(eventName, visitorProperties, eventData);
  }
}

/**
 * Tracks explicitly a page view
 */
export function page(
  eventName: PageName,
  visitorProperties?: VisitorProperties,
) {
  if (sendinblueEnabled) {
    if (process.env.NODE_ENV === 'development') {
      console.log('[sendinblue.page]', eventName, visitorProperties);
    }
    window.sendinblue.page(eventName, visitorProperties);
  }
}

The function trackLink exists to override the behavior of <a> tags, overriding their default behavior to first track that they were clicked and then redirecting the browser to the new URL. Since it acts on the DOM node, likely adding a click listener, this happens outside React’s lifecycle.

A workaround would be to use trackLink in an effect, like this:

const link = useRef<HTMLElement>();
const done = useRef<boolean>(false);
useEffect(() => {
  if (done.current) {
    // prevent the double execution of React because
    // we can't unbind the track if the effect is called more than once
    return;
  }
  if (ref.current && window.sendinblue) {
    trackLink(ref.current, { ...properties, name });
    done.current = true;
  }
  return function cleanUp() {
    // no way to undo trackLink right now
  };
}, []);

return (
  <a ref={link} ... />
);

The only problem is that there doesn’t seem to be a way to remove the listener when the effect cleans up.

So my recommendation here is don’t use trackLink, and instead override yourself the onClick event on links to track the event. In Next.js apps, if the link is internal and you use next/link, you won’t need to worry about the necessary delay to track the click since the link won’t produce a browser redirection.


6. Make sure event names match

Once I had an issue with a workflow not firing, and I spent 2 hours investigating and sharing information with the Sendinblue support team. In the end, the root cause was really silly. The event name my application was sending was userSignUp and what I configured in Sendinblue was userSignedUp. Classic.

Make sure your names are consistent when your workflows don’t fire as expected!

Brevo lib file photo
Brevo lib file photo

7. Expect occasional delays in event processing

Brevo’s events might take a while to show up in their event logs. Workflows might fire immediately or take minutes to start. It looks like their processing queues can get backed up sometimes, so be patient and don’t reach out to their support team right away. It might have to do with using the free plan, which I usually do until things are set up and the customer confirms they’re satisfied with the solution.


That’s all! I’ll keep this blog post updated as I come across new tips to share.

Back to all posts