<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alex</title>
    <description>The latest articles on Forem by Alex (@gridpointanalytics).</description>
    <link>https://forem.com/gridpointanalytics</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3559438%2Fbb099faa-1f77-40ff-9d9e-9acbe0e16125.png</url>
      <title>Forem: Alex</title>
      <link>https://forem.com/gridpointanalytics</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gridpointanalytics"/>
    <language>en</language>
    <item>
      <title>Building a Beautiful, Performant Analytics Dashboard with React &amp; Recharts (Part 2)</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 23 Oct 2025 14:00:00 +0000</pubDate>
      <link>https://forem.com/gridpointanalytics/building-a-beautiful-performant-analytics-dashboard-with-react-recharts-part-2-fl5</link>
      <guid>https://forem.com/gridpointanalytics/building-a-beautiful-performant-analytics-dashboard-with-react-recharts-part-2-fl5</guid>
      <description>&lt;p&gt;In Part 1 of this series, I broke down how I built the serverless, privacy-first backend for my analytics tool, Gridpoint Analytics, using the Cloudflare stack. But let's be honest: a powerful backend is useless if the frontend is a pain to use.&lt;/p&gt;

&lt;p&gt;My goal was to create an analytics dashboard that people would actually want to open. It needed to be fast, intuitive, and present data with absolute clarity. Here’s a look at how I built the frontend with React, Recharts, and Tailwind CSS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing the Right Tools for the Job&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The frontend stack was chosen for speed and developer experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React:&lt;/strong&gt; For its component-based architecture and mature ecosystem. It allows for building a complex UI that is still easy to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recharts:&lt;/strong&gt; A composable charting library for React. It’s simple to get started with and flexible enough for creating beautiful, interactive charts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS:&lt;/strong&gt; For styling. Utility-first CSS is a game-changer for building consistent, responsive designs without writing a ton of custom CSS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Charting Challenge: Bringing Data to Life&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The centerpiece of any analytics dashboard is the chart. This component is responsible for fetching data from the API we built in Part 1 and rendering it visually.&lt;/p&gt;

&lt;p&gt;Here’s a simplified version of my primary chart component. It fetches data when the component mounts or when the user changes the date range, and it handles the loading state to avoid a jarring user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';

function AnalyticsChart({ siteId, dateRange }) {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() =&amp;gt; {
    const fetchData = async () =&amp;gt; {
      setIsLoading(true);
      // Fetch data from the Cloudflare Worker API we built in Part 1
      const response = await fetch('/api/get-chart-data', {
        method: 'POST',
        body: JSON.stringify({ siteId, dateRange }),
      });
      const result = await response.json();
      // Here you would format the result for the chart
      setData(result.formattedData);
      setIsLoading(false);
    };

    fetchData();
  }, [siteId, dateRange]); // Re-fetch when props change

  if (isLoading) {
    return &amp;lt;div&amp;gt;Loading Chart...&amp;lt;/div&amp;gt;;
  }

  return (
    &amp;lt;ResponsiveContainer width="100%" height={300}&amp;gt;
      &amp;lt;LineChart data={data}&amp;gt;
        &amp;lt;XAxis dataKey="date" /&amp;gt;
        &amp;lt;YAxis /&amp;gt;
        &amp;lt;Tooltip /&amp;gt;
        &amp;lt;Line type="monotone" dataKey="pageviews" stroke="#8884d8" strokeWidth={2} /&amp;gt;
      &amp;lt;/LineChart&amp;gt;
    &amp;lt;/ResponsiveContainer&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://recharts.org/en-US/examples/" rel="noopener noreferrer"&gt;Check out the official Recharts docs for more examples.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From Data to Direction: Displaying Insights&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A core feature of Gridpoint Analytics is the "AI Signal Engine," which surfaces plain-English insights. On the frontend, this means taking the raw data from our API and presenting it in a clear, narrative format.&lt;/p&gt;

&lt;p&gt;Instead of just showing a list of top referrers, I created a dedicated "Insights" component. It might take data points like &lt;code&gt;{ referrer: 't.co', views: 500, trend: '+35%' }&lt;/code&gt; and transform it into a human-readable sentence: "Traffic from Twitter is up 35% this week." This simple translation from data to direction is a huge step toward making analytics more accessible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Tying It All Together&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building a product is a story in two parts: the powerful engine under the hood and the beautiful, intuitive vehicle you interact with. By combining a privacy-first serverless backend with a clean and performant React frontend, I was able to build the analytics tool I always wanted.&lt;/p&gt;

&lt;p&gt;This two-part series covers the core of how Gridpoint Analytics was built. I hope it was helpful for anyone else on a similar journey. The best way to understand the philosophy is to see it in action.&lt;/p&gt;

&lt;p&gt;You can check out the final result and try &lt;a href="https://gridpointanalytics.com" rel="noopener noreferrer"&gt;Gridpoint Analytics for free on our website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>uidesign</category>
    </item>
    <item>
      <title>How I Built a Serverless, Privacy-First Analytics Tool on the Cloudflare Stack</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 16 Oct 2025 14:00:00 +0000</pubDate>
      <link>https://forem.com/gridpointanalytics/how-i-built-a-serverless-privacy-first-analytics-tool-on-the-cloudflare-stack-5je</link>
      <guid>https://forem.com/gridpointanalytics/how-i-built-a-serverless-privacy-first-analytics-tool-on-the-cloudflare-stack-5je</guid>
      <description>&lt;p&gt;I've always been frustrated by the state of web analytics. You're either stuck with a complex, privacy-invasive giant like Google Analytics or a simpler tool that might not give you the deep insights you need. I wanted to build something that hit the sweet spot: powerful, a joy to use, and built on a foundation of absolute privacy.&lt;/p&gt;

&lt;p&gt;Today, I want to show you how I built the backend for my project, Gridpoint Analytics, on a completely serverless Cloudflare stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Go All-In on Cloudflare?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before diving into the code, let's talk about the "why." I chose this stack for three main reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Cloudflare's edge network is everywhere. By running my logic as close to the user as possible, data ingestion is incredibly fast.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simplicity &amp;amp; Cost:&lt;/strong&gt; No servers to manage, no containers to configure. It's a true serverless experience, and the cost scales beautifully from zero.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trust:&lt;/strong&gt; I'm building a privacy-first tool. It felt right to build it on infrastructure that is also deeply invested in building a better, more private web.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Core Architecture: A Quick Tour&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The backend has three main jobs: ingest data, anonymize it, and serve it back to the dashboard. Here’s how each piece works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Data Ingestion (Cloudflare Workers)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a visitor loads a site with Gridpoint Analytics, our tiny tracking script sends a beacon with non-sensitive data (URL, referrer, screen size) to a Cloudflare Worker endpoint.&lt;/p&gt;

&lt;p&gt;The Worker is the front door. It grabs the request data, including the IP address and User-Agent, and prepares it for the most important step: anonymization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// A simplified look inside the Cloudflare Worker
export default {
  async fetch(request, env) {
    const { pathname, referrer } = new URL(request.url);
    const ip = request.headers.get('CF-Connecting-IP');
    const userAgent = request.headers.get('User-Agent');

    // Pass this data on for processing and storage
    await processAnalyticsData({ ip, userAgent, pathname, referrer, env });

    // Return a 1x1 pixel gif to the client
    return new Response(pixel, { headers: { 'Content-Type': 'image/gif' } });
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. The Privacy Layer: Hashing and Anonymization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the heart of the system. We never store IP addresses or raw User-Agents. To count unique visitors for a 24-hour period, we create a hash using a daily-rotating salt.&lt;/p&gt;

&lt;p&gt;The process is simple:&lt;/p&gt;

&lt;p&gt;Concatenate the anonymized IP, User-Agent, and the daily salt.&lt;/p&gt;

&lt;p&gt;Hash the resulting string using a fast, non-reversible algorithm like SHA-256.&lt;/p&gt;

&lt;p&gt;Store the hash, and immediately discard the original PII.&lt;/p&gt;

&lt;p&gt;This hash is unique enough to identify a visitor for one day without ever knowing who they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Data Storage (Cloudflare D1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the data is anonymized, the Worker writes it to Cloudflare D1, which is essentially SQLite on the edge. For an analytics tool, where you're doing a high volume of writes and aggregated reads, it's a surprisingly good fit.&lt;/p&gt;

&lt;p&gt;My primary table schema looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE pageviews (
  id TEXT PRIMARY KEY,
  timestamp INTEGER NOT NULL,
  pathname TEXT NOT NULL,
  referrer TEXT,
  user_hash TEXT NOT NULL,
  site_id TEXT NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. The API (Cloudflare Workers Again)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The dashboard frontend needs a way to get this data back. Another Cloudflare Worker acts as our API. It authenticates the user, takes a request for a specific site and date range, and queries D1 to pull the aggregated data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// A simplified API endpoint to get pageviews
async function handleApiRequest(request, env) {
  // ... authentication logic ...

  const { siteId, dateRange } = await request.json();
  const ps = env.DB.prepare('SELECT pathname, COUNT(*) as views FROM pageviews WHERE site_id = ? GROUP BY pathname');
  const { results } = await ps.bind(siteId).all();

  return new Response(JSON.stringify(results), { headers: { 'Content-Type': 'application/json' } });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://developers.cloudflare.com/d1/platform/client-api/" rel="noopener noreferrer"&gt;Link to the official Cloudflare D1 docs for more info on querying.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenges &amp;amp; Next Steps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building on the edge has been an amazing experience, but it's not without its challenges. Learning the nuances of querying SQLite for complex time-series data was a big one.&lt;/p&gt;

&lt;p&gt;This covers the backend, but what about the user-facing part? A great analytics tool should be a joy to use.&lt;/p&gt;

&lt;p&gt;In Part 2, I'll break down how I built a fast, beautiful, and insightful dashboard with React, Recharts, and Tailwind CSS. &lt;strong&gt;Follow me to get notified when it drops!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>serverless</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
