Create Your Own Privacy-Focused Analytics Tool with JavaScript and ASP.NET Core

RajasekarRajasekar
5 min read

Every website has integrated with some form of analytics solutions including most popular Google analytics. Observing user behavior help us to take decisions based real data rather than some assumptions.

Although adding Google Analytics is easy and offers many features, it can sometimes be too much for simple analytics needs or when privacy concerns are important. (on-premises hosting)

This blog post explains how to add custom analytics to your website, and you can use it as a starting point.

Steps to Create a Custom Analytics Solution

We need the following three things to create the analytics solution:

  • JavaScript code to send user actions to the backend API

  • A backend API to receive and store the data in a database

  • A UI to view the collected analytics data

JavaScript logic

Unique user identifier

Each user actions should be tagged to the respective user irrespective of web page refresh, browser close or system restarts.

Either you can generate a unique user id and store it in the browser local storage for further use (used in this blog post) or you can use any finger printing library (Ex fingerprintjs) without storing any user identifier in the browser.

//Generate unique user id
function generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = (Math.random() * 16) | 0,
            v = c === 'x' ? r : (r & 0x3) | 0x8;
        return v.toString(16);
    });
}

User actions 1: active time on the page

Use the below code to track the users active time on the page

let startTime = Date.now();
let pageData = {
    page: window.location.pathname,
    timeSpent: 0,
    buttonClicks: [],
    searchQueries: []
};

window.addEventListener('focus', () => {
    startTime = Date.now();
});

window.addEventListener('blur', () => {
    pageData.timeSpent += Date.now() - startTime;    
});

User actions 2: Button clicks (actions)

Buttons are having important actions in any website and the below code monitors all the button ids in the page and share the details to backend

 document.querySelectorAll('button').forEach(button => {
        button.addEventListener('click', (e) => {
            pageData.buttonClicks.push({
                buttonId: e.target.id || e.target.className || 'unknown',
                time: new Date().toISOString()
            });
        });
    });

User actions 3: Track search queries

You can track search queries by listening to the search button on the page. This helps you understand what users are actually searching for or want on the page.

   document.querySelectorAll('form.search-form').forEach(form => {
        form.addEventListener('submit', (e) => {
            const queryInput = form.querySelector('input[name="search"]');
            const query = queryInput ? queryInput.value : '';
            pageData.searchQueries.push({
                query: query,
                time: new Date().toISOString()
            });
        });
    });
💡
DOM elements only available on/after the page load, so register the all the querySelectorAll on page load.

Send the collected analytics data to backend

The collected data should be sent to the backend once the user finishes activities on the current page and moves to another page, or before closing the page.

window.addEventListener('visibilitychange', async (e) => {
    pageData.timeSpent += Date.now() - startTime; // Capture remaining time
    const uniqueUser = getUniqueUserId();
    const analyticsData = {
        ...pageData,
        userId: uniqueUser,
        timestamp: new Date().toISOString()
    };
    const headers = {
        type: 'application/json',
    };
    let result = navigator.sendBeacon('/analytics', new Blob([JSON.stringify(analyticsData)], { type: 'application/json' }));
    console.log(`analytics sent successfully: ${result}`);
});

The above code adds a listener to visibilitychange event and sends the data to backend once triggered. This works on switching tabs, closing tab/browser.

💡
Avoid unload,beforeunload & pagehide: In the past, many websites used the unload or beforeunload events to send analytics at the end of a session. However, this is very unreliable, especially on mobile, as the browser often doesn't trigger these events. (source: MDN)

Backend API

The below minimal API receives the data from browser and stores in the database. Database logic is not covered in this post.

app.MapPost("/analytics", async (HttpRequest request) =>
{ 
    var options = new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true
    };
    var analyticsData = await JsonSerializer.DeserializeAsync<AnalyticsDataDto>(request.Body, options);
    //TODO - Logic to add the above data in database
    return Results.Ok(analyticsData);
});

The database logic should perform fast and efficient inserts/updates as we might get lot of analytics every minute based on the website traffic.

Example scenario where you should consider the following points while designing the database logic:

  • If the user activities are new on the particular date, add it to the database.

  • If the user activities are already found for the given date, update the data

You should also implement exception handling and set up notifications for any major issues in collecting user analytics.

UI to view the collected analytical data

Finally, you should show the user analytics data in a presentable way with necessary filter and other options.

Consider fast retrieval of the data from backend, using of cache and other possible solutions to improve user experience.

Example page from plausible.io

Conclusion:

In conclusion, implementing a custom analytics solution for your website allows you to collect basic user data without depending on third-party tools like Google Analytics.

This approach involves creating a JavaScript-based frontend to monitor user interactions and time spent on pages, a backend API for data storage, and a UI for visualizing insights.

The solution focuses on simplicity and privacy by using unique user IDs and the navigator.sendBeacon method for efficient data transfer. It also gives advice on managing the database and offers tips for building a scalable ASP.NET Core project.

0
Subscribe to my newsletter

Read articles from Rajasekar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rajasekar
Rajasekar

I am a .NET developer from India, having 10+ years of experience in web application development.