4 min read
How to integrate Betterlytics in Astro

When I migrated this site from Next.js to Astro, one of the first things I needed to re-enable was analytics. I recently switched to Betterlytics for that, an open-source, cookieless, and lightweight alternative to mainstream trackers.

Let’s walk through how to integrate it into an Astro project.

Set up your environment variables

First, let’s set up two environment variables in the .env file. These control whether the library is enabled and define the site ID you received from Betterlytics at signup.

PUBLIC_ENABLE_BETTERLYTICS=false
BETTERLYTICS_SITE_ID=XXXXXXXXX

Create the Betterlytics component for Astro

Next, create a new component, ideally src/components/BetterlyticsTracker.astro, to manage the library loading as well as the custom event tracking.

---
interface Props {
  enable?: boolean;
  siteId: string;
}

const { enable = true, siteId } = Astro.props;
---

{
  enable && (
    <script
      async
      src="https://betterlytics.io/analytics.js"
      data-site-id={siteId}
      data-server-url="https://betterlytics.io/track"
    />
  )
}

Import this component where you need it (usually in your BaseLayout.astro) to ensure it applies globally:

import BetterlyticsTracker from "../components/BetterlyticsTracker.astro";

[...]

<BetterlyticsTracker
      enable={import.meta.env.PUBLIC_ENABLE_BETTERLYTICS === "true"}
      siteId={import.meta.env.BETTERLYTICS_SITE_ID}
/>

Note that this implementation will provide you page views out of the box.

Track custom events

If you want to track something that goes beyond standard page views you can leverage custom events tracking. In my case, I used this feature to track interactions with a few specific call-to-actions. To do this, we’ll have to call betterlytics.event();

Let’s go back to src/components/BetterlyticsTracker.astro and add a global type declaration for betterlytics in the frontmatter script.

---
interface Props {
  enable?: boolean;
  siteId: string;
}

// Add global type declaration for betterlytics
declare global {
  interface Window {
    betterlytics?: {
    event: (eventName: string, params?: Record<string, unknown>) => void;
    };
    trackEvent?: (eventName: string, params?: Record<string, unknown>) => void;
  }
}

const { enable = true, siteId,  } = Astro.props;
---

Then, let’s define another inline script to allow calling betterlytics.event();.

<script>
  function waitForBetterlytics() {
        return new Promise<void>(resolve => {
          const interval = setInterval(() => {
            if (window.betterlytics) {
              clearInterval(interval);
              resolve();
            }
          }, 100);
        });
      }

  waitForBetterlytics().then(() => {
        window.trackEvent = (eventName, params = {}) => {
          if (window.betterlytics) {
            window.betterlytics.event(eventName, params);
          }
        };

        document.addEventListener("click", (e) => {

          const el = (e.target instanceof Element) ? e.target.closest("[data-event]") : null;
          if (!el) return;

          const event = (el as HTMLElement).dataset.event;
          const propertiesString = (el as HTMLElement).dataset.properties;
          let properties = {};

          if (propertiesString) {
            try {
              properties = JSON.parse(propertiesString);
            } catch {
              console.warn("Invalid JSON in data-properties:", propertiesString);
            }
          }

          if (typeof window.trackEvent === "function" && typeof event === "string") {
            window.trackEvent(event, properties);
          }
        });
      });

</script>

waitForBetterlytics() ensures that the Betterlytics library is ready before proceeding. Once the promise resolves, a new function, window.trackEvent, is defined. This function wraps betterlytics.event();.

An event listener is added to the document to handle all click events. This listener is designed to track user interactions with elements that have specific data-* attributes. The data-event attribute is retrieved as the event name. The data-properties attribute, if present, is treated as a JSON string. The code attempts to parse it into an object.

The use of TypeScript interfaces and global declarations will avoid errors when running astro check.

To finalize the custom event tracking, we must define a data-event attribute and, optionally, a data-property within the HTML object to be tracked. See the example below for more clarity:

<a
  href="https://github.com/giorgiodg/"
  data-event="cta-click"
  data-properties='{"buttonText":"GitHub","source":"index"}'
  >GitHub profile</a
>

Conclusion

Astro and Betterlytics work together pretty well, and this setup should help you integrate both quickly. This guide focuses on the script-based integration, but an npm package version might follow in the future.

And if you end up using Betterlytics, consider contributing. I recently added the italian translation!

  • astro
  • development
  • open source