Taking Cloudflare Worker Analytics Engine for a spin

Unsplash

Introduction

Cloudflare Workers is a serverless platform that allows you to run JavaScript code on Cloudflare’s edge network.

If you are using Cloudflare workers and are looking to show analytics from your workers in your SaaS application, you can make use of Cloudflare Worker Analytics.

Use Case

For my app — SlashURL, I need to show link analytics. SlashURL is a fast, scalable URL link shortener built using Cloudflare Workers and Workers KV store.

Some graphs that I need:

  1. Time series data for clicks
  2. Top countries
  3. Top cities
  4. .. and more

Sending Data

Steps:

  1. Create analytics data set binding
  2. Define metrics & dimensions to send
  3. Call from within the workers
export interface Env {
    ...
    URL\_CLICK\_TRACKING: AnalyticsEngineDataset
}

// request.url contains the short original url
log(request: Request<unknown, CfProperties<unknown>>): Promise<void> {
    const cfProperties = request.cf
    if (!cfProperties) {
        return Promise.resolve()
    }

    this.env.URL\_CLICK\_TRACKING.writeDataPoint({
        'blobs': \[
            request.url,
            cfProperties.city as string,
            cfProperties.country as string,
            cfProperties.continent as string,
            cfProperties.region as string,
            cfProperties.regionCode as string,
            cfProperties.timezone as string
        \],
        'doubles': \[
            cfProperties.metroCode as number,
            cfProperties.longitude as number,
            cfProperties.latitude as number
        \],
        'indexes': \[
            cfProperties.postalCode as string,
        \]
    })
    return Promise.resolve()
}

Querying Data

Steps:

  1. Generate API token to access worker analytics (from Cloudflare console)
  2. Make a cURL request to the endpoint using the API token like:

Example SQL Query to get the time series data of clicks:

The query is trying to count the clicks on a specific URL within 24-hour intervals over the past 30 days, providing a time-series-like result with the number of clicks in each interval (day).

intDiv is the function to create these 24-hour intervals

SELECT intDiv(toUInt32(timestamp), 86400) \* 86400 AS t, COUNT() as clicks FROM URL\_CLICK\_TRACKING WHERE blob1 = 'https://slsh.ws/ankit-linkedin' AND timestamp >= NOW() - INTERVAL '30' DAY GROUP BY t ORDER BY t, clicks DESC
curl --location --request POST 'https://api.cloudflare.com/client/v4/accounts/7bc1ff972e6f19acd7874cd1f12eff9b/analytics\_engine/sql' \\
\--header 'Authorization: Bearer <token>' \\
\--header 'Content-Type: text/plain' \\
\--data-raw '<query>'

Integrating into Next.js App

If you want to feed this data into your Next.js App, you can create a route analytics/[url].tsxusing the page router in Next.js like below:

import { ClickStream } from "@/components/analytics/ClickStream";
import Layout from "@/components/layout";
import { getClickStream } from "@/lib/queries";
import {
  Alert,
  AlertIcon,
  Box,
  HStack,
  Heading,
  Stack,
} from "@chakra-ui/react";
import { format, fromUnixTime } from "date-fns";
import { GetServerSidePropsContext } from "next";

export default function Analytics({ props, analytics }) {
  const clickStream = analytics.clickStream;
  const noData = clickStream.data.length === 0;
  const data = clickStream.data.map((d) => {
    return {
      date: format(fromUnixTime(d.t), "dd/M"),
      clicks: parseInt(d.clicks),
    };
  });

  return (
    <Box bg={"white"} py={7} px={7}>
      <Stack spacing={2}>
        <Heading as="h1" fontSize="3xl">
          Analytics
        </Heading>
        <HStack mt={5}>
          {noData ? (
            <Alert status="info" mt={5}>
              <AlertIcon />
              Detailed Analytics for your links will be available soon.
            </Alert>
          ) : (
            <ClickStream data={data} />
          )}
        </HStack>
      </Stack>
    </Box>
  );
}

Analytics.getLayout = function getLayout(page: any) {
  return <Layout>{page}</Layout>;
};

export const getServerSideProps = async (
  context: GetServerSidePropsContext
) => {
  const { params } = context;
  const clickStream = await getClickStream(params.url as string);

  return {
    props: {
      analytics: {
        clickStream,
      },
    },
  };
};

To get the data from the Worker Analytics Engine:

import axios from "axios";

const TIMESERIES\_GET\_CLICKS = function (url: string) {
  return \`
SELECT
  intDiv(toUInt32(timestamp), 86400) \* 86400 AS t,
  COUNT() as clicks
FROM URL\_CLICK\_TRACKING
WHERE
  blob1 = '${url}' AND
  timestamp >= NOW() - INTERVAL '30' DAY
GROUP BY t
ORDER BY t, clicks DESC\`
}

export async function getClickStream(url: string) {
  const API\_ENDPOINT = \`https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE\_ACCOUNT\_ID}/analytics\_engine/sql\`;

  const options = {
    headers: {
      "Authorization": \`Bearer ${process.env.CLOUDFLARE\_ANALYTICS\_API\_TOKEN}\`,
      "Content-Type": "text/plain",
    },
  };

  const payload = TIMESERIES\_GET\_CLICKS(url);

  let data = null
  try {
    const response = await axios.post(API\_ENDPOINT, payload, options);
    data = response.data;
    console.log(data);
  } catch (error) {
    console.error(error);
  }

  return data
}

For rendering the charts, we are using the recharts react library. There are other libraries like https://react-chartjs-2.js.org/ as well, but I went with this as it supports a lot of charts.

To create a time series click-stream chart component

// Clickstream.tsx
import { useEffect, useState } from "react";
import {
  CartesianGrid,
  Legend,
  Line,
  LineChart,
  Tooltip,
  XAxis,
  YAxis,
} from "recharts";

// This is needed as nextjs throws hydration error with recharts rendered on server/client
// More: https://github.com/recharts/recharts/issues/2272
// So we render it only on client here
export const useIsServerSide = () => {
  const \[isServerSide, setIsServerSide\] = useState(true);

  useEffect(() => {
    setIsServerSide(false);
  }, \[setIsServerSide\]);

  return isServerSide;
};

export const ClickStream = (props: Props) => {
  const isServerSide = useIsServerSide();
  if (isServerSide) return null;

  const { data } = props;

  return (
    <LineChart
      width={500}
      height={300}
      data={data}
      margin={{
        top: 5,
        right: 30,
        left: 20,
        bottom: 5,
      }}
    >
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis dataKey="date" />
      <YAxis />
      <Tooltip />
      <Legend />
      <Line type="monotone" dataKey="clicks" stroke="#82ca9d" />
    </LineChart>
  );
};

Demo

Below is how it looks. Link to the dashboard here: slashurl.co

Analytics for Clicks

References

https://developers.cloudflare.com/analytics/analytics-engine/get-started/

https://developers.cloudflare.com/analytics/analytics-engine/sql-api/

;