New site cover

Introduction

Understanding visitor behavior on your website is essential for improving user experience and increasing conversions. One of the most effective techniques is to track visitors’ mouse movements, analyzing where interactions are concentrated. In this tutorial, we’ll explore how to monitor visitor mouse movements, automatically generate screenshots, and create heatmaps that highlight interaction hotspots.

Motivation

Tracking mouse movements provides valuable insights into how users navigate your website. This can help identify areas for improvement in usability and optimize your page structure to better guide user attention.

Scope

This tutorial provides a step-by-step guide to creating an automated system that tracks mouse movements, generates heatmaps, and updates Firestore with the collected data using Google Cloud Functions and Firebase.

Objective

By the end of this tutorial, you will have an application capable of:

  1. Tracking and recording visitor mouse movements.
  2. Generating screenshots of visited pages.
  3. Creating heatmaps based on mouse movements and clicks.
  4. Saving generated heatmap URLs to Firestore.

Here is an example of a generated heatmap showing movements and clicks:

Mouse Tracking Video Example

Mouse tracking example

Generated Heatmap with Mouse Positions in Red and Clicks in Blue

Mouse tracking example

In this image, red circles represent mouse movements, while blue dots indicate where users clicked.

With this system, you can improve the design and structure of your website, optimizing the user experience based on real behavioral data.

Summary of Steps

  1. Integrate Mouse Tracking Code into Your Website.
  2. Create Cloud Function to Retrieve Sitemap and Save URLs.
  3. Implement Cloud Function to Generate Page Screenshots.
  4. Detect New Sessions with Cloud Function.
  5. Generate Mouse Movement-Based Heatmaps with Cloud Function.
  6. Automatically Save Heatmap URLs to Firestore.

1. Integrate Mouse Tracking Code into Your Website

To track mouse movements and collect user session data, integrate the following JavaScript code into your website. The data will be sent to Firestore.

Code for Mouse Tracking

Add the following JavaScript file to your site to track mouse movements and clicks:

static/js/firebase/mouse-tracker.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import {
  db,
  doc,
  setDoc,
  serverTimestamp,
  arrayUnion,
} from "./firebase/init.js"; // Ensure Firestore is initialized

// Check if the user is a bot by analyzing the user agent
function isBot() {
  const botUserAgents = [
    /bot/i,
    /crawl/i,
    /spider/i,
    /slurp/i,
    /search/i,
    /bing/i,
    /yahoo/i,
    /yandex/i,
    /baidu/i,
    /duckduckgo/i,
    /semrush/i,
    /ahrefs/i,
    /googlebot/i,
    /bingbot/i,
  ];
  return botUserAgents.some((bot) => bot.test(navigator.userAgent));
}

// Only run mouse tracking if the user is not a bot
if (!isBot()) {
  console.log("Not a bot, enabling mouse tracking.");

  // Generate a unique session ID for the user (UUID or timestamp-based)
  const sessionId = "session-" + Date.now();
  console.log("Generated session ID:", sessionId);

  // Capture session information (user's browser, OS, screen size)
  const sessionData = {
    browser: navigator.userAgent,
    screenWidth: window.screen.width,
    screenHeight: window.screen.height,
    language: navigator.language,
    platform: navigator.platform,
    sessionStartTime: new Date().toISOString(),
    pageURL: window.location.href, // Capture the current page URL
  };

  console.log("Session Data Captured:", sessionData);

  let clickEvents = [];
  let movementEvents = [];

  // Throttle function to limit event processing
  function throttle(callback, limit) {
    let lastCall = 0;
    return function (...args) {
      const now = Date.now();
      if (now - lastCall >= limit) {
        lastCall = now;
        callback(...args);
      }
    };
  }

  // Function to push events to Firestore by updating the session document
  async function updateSessionInFirestore() {
    try {
      if (clickEvents.length === 0 && movementEvents.length === 0) {
        console.log("No events to push to Firestore.");
        return;
      }

      // Push the data to Firestore using set with arrayUnion to append the data
      await setDoc(
        doc(db, "sessions", sessionId),
        {
          sessionData: sessionData,
          mouseClicks: arrayUnion(...clickEvents),
          mousePositions: arrayUnion(...movementEvents),
          lastUpdated: serverTimestamp(),
        },
        { merge: true }
      );

      console.log("Session data successfully updated in Firestore.");

      // Clear the arrays after sending the data
      clickEvents = [];
      movementEvents = [];
    } catch (error) {
      console.error("Error updating session data:", error);
    }
  }

  // Track mouse movements (throttled to store locally every 200ms)
  document.addEventListener(
    "mousemove",
    throttle((event) => {
      movementEvents.push({
        x: event.clientX + window.scrollX, // Adjust for horizontal scrolling
        y: event.clientY + window.scrollY, // Adjust for vertical scrolling
        timestamp: Date.now(),
      });
    }, 200)
  );

  // Track mouse clicks (store locally)
  document.addEventListener("click", (event) => {
    clickEvents.push({
      x: event.clientX + window.scrollX, // Adjust for horizontal scrolling
      y: event.clientY + window.scrollY, // Adjust for vertical scrolling
      element: event.target.tagName,
      timestamp: Date.now(),
    });
  });

  // Push data to Firestore every 5 seconds
  setInterval(updateSessionInFirestore, 5000);
} else {
  console.log("Bot detected, mouse tracking disabled.");
}

Initialize Firebase Create the firebase/init.js file to initialize Firebase and Firestore.

firebase/init.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Import necessary modules from Firebase
import { initializeApp } from "firebase/app";
import {
  getFirestore,
  doc,
  setDoc,
  arrayUnion,
  serverTimestamp,
} from "firebase/firestore";

// Firebase Configuration (replace with your Firebase credentials)
const firebaseConfig = {
  apiKey: "YOUR-API-KEY",
  authDomain: "YOUR-AUTH-DOMAIN",
  projectId: "YOUR-PROJECT-ID",
  storageBucket: "YOUR-STORAGE-BUCKET",
  messagingSenderId: "YOUR-SENDER-ID",
  appId: "YOUR-APP-ID",
};

// Initialize the Firebase app
const app = initializeApp(firebaseConfig);

// Initialize Firestore
const db = getFirestore(app);

// Export the Firestore functions and variables for use in other files
export { db, doc, setDoc, arrayUnion, serverTimestamp };

2. Create Cloud Function to Retrieve Sitemap and Save URLs

The second step involves retrieving page URLs from the sitemap.xml file and saving them to Firestore. This allows us to know which pages to visit and generate screenshots for.

Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const { XMLParser } = require("fast-xml-parser");

// Initialize Firebase Admin SDK if not already initialized
if (!admin.apps.length) {
  admin.initializeApp();
}

const db = admin.firestore();

// Helper function to delete all documents in a collection
async function deleteCollection(collectionPath) {
  const collectionRef = db.collection(collectionPath);
  const querySnapshot = await collectionRef.get();

  const batch = db.batch();
  querySnapshot.forEach((doc) => {
    batch.delete(doc.ref);
  });

  await batch.commit();
  console.log(`Deleted all documents in collection: ${collectionPath}`);
}

// Cloud Function to fetch sitemap index and save URLs from each sitemap to Firestore
exports.cf_fetchAndStoreSitemapURLs = functions
  .region("your-region")
  .pubsub.schedule("every 24 hours") // Schedule to run daily
  .onRun(async (context) => {
    try {
      const fetch = await import("node-fetch").then((mod) => mod.default); // Dynamic import
      const sitemapIndexUrl =
        process.env.SITEMAP_INDEX_URL || "https://your-default-sitemap-url.xml"; // Use environment variable
      const response = await fetch(sitemapIndexUrl);
      const xmlData = await response.text();

      const parser = new XMLParser();
      const jsonData = parser.parse(xmlData);

      // Check if it is a sitemap index and contains sitemap URLs
      const sitemaps = jsonData.sitemapindex.sitemap.map((entry) => entry.loc);

      if (sitemaps.length === 0) {
        console.error("No sitemaps found in the sitemap index.");
        return null;
      }

      // Delete existing sitemap URLs before adding new ones
      await deleteCollection("sitemaps");

      // Now fetch each individual sitemap and extract the URLs
      for (const sitemapUrl of sitemaps) {
        console.log(`Fetching sitemap: ${sitemapUrl}`);

        const sitemapResponse = await fetch(sitemapUrl);
        const sitemapXml = await sitemapResponse.text();
        const sitemapData = parser.parse(sitemapXml);

        // Extract the URLs from the individual sitemap
        const urls = sitemapData.urlset.url.map((entry) => entry.loc);

        // Save URLs to Firestore
        const batch = db.batch();
        urls.forEach((url) => {
          const urlDocRef = db.collection("sitemaps").doc();
          batch.set(urlDocRef, {
            url,
            createdAt: admin.firestore.FieldValue.serverTimestamp(),
          });
        });

        await batch.commit();
        console.log(`Saved ${urls.length} URLs from ${sitemapUrl}.`);
      }

      console.log("All sitemaps processed successfully.");
      return null;
    } catch (error) {
      console.error("Error fetching sitemap:", error);
      throw new Error("Failed to fetch sitemap");
    }
  });

3. Implement Cloud Function to Generate URL Screenshots

Once the URLs are retrieved, we utilize another Cloud Function to automatically generate screenshots of each page.

Code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const chromium = require("@sparticuz/chromium");
const puppeteer = require("puppeteer-core");

if (!admin.apps.length) {
  admin.initializeApp();
}

const db = admin.firestore();
const bucket = admin
  .storage()
  .bucket(process.env.BUCKET_NAME || "your-default-bucket-name");

// Function to retrieve screen sizes from Firestore (as array of strings)
async function getScreenSizesFromConfig() {
  const configDoc = await db
    .collection("config")
    .doc("sessionsScreensRes")
    .get();
  if (configDoc.exists) {
    return configDoc.data().screenSizes || [];
  }
  return [];
}

// Function to parse the screen size string and return an object
function parseScreenSizeString(screenSizeStr) {
  const [width, height] = screenSizeStr.split("-");
  return {
    width: parseInt(width, 10),
    height: parseInt(height, 10),
    label: screenSizeStr, // Use the string itself as the label
  };
}

// Cloud Function to generate screenshots for multiple screen sizes
exports.generateSessionScreenshots = functions
  .region("your-region")
  .runWith({ memory: "1GB", timeoutSeconds: 300 }) // Set 1GB memory and 300s timeout
  .firestore.document("sitemaps/{urlId}")
  .onCreate(async (snap, context) => {
    const urlData = snap.data();
    let url = urlData.url;

    try {
      // Fetch screen sizes from Firestore
      const screenSizesStrings = await getScreenSizesFromConfig();
      const screenSizes = screenSizesStrings.map(parseScreenSizeString);

      // Launch Puppeteer with Chromium for serverless environments
      const browser = await puppeteer.launch({
        executablePath: await chromium.executablePath(),
        args: chromium.args,
        defaultViewport: chromium.defaultViewport,
        headless: chromium.headless,
      });

      const page = await browser.newPage();

      // Replace URL encoding with human-readable characters and remove protocol
      const sanitizedUrl = url
        .replace(/^https?:\/\//, "")
        .replace(/[^\w-]/g, "-");

      for (const size of screenSizes) {
        // Set the viewport size
        await page.setViewport({ width: size.width, height: size.height });
        await page.goto(urlData.url, { waitUntil: "networkidle2" });

        // Scroll the page to ensure all dynamic content loads
        await autoScroll(page);

        // Capture a full-page screenshot
        const screenshotBuffer = await page.screenshot({ fullPage: true });
        const filename = `screenshots/${sanitizedUrl}-${size.label}.png`;

        // Save the screenshot to Cloud Storage
        await bucket
          .file(filename)
          .save(screenshotBuffer, { contentType: "image/png" });
        console.log(
          `Full-page screenshot saved for ${urlData.url} at ${filename}`
        );
      }

      await browser.close();
      return null;
    } catch (error) {
      console.error("Error generating screenshots for URL:", error);
      throw new Error("Screenshot generation failed");
    }
  });

// Helper function to scroll the page to ensure all content is loaded
async function autoScroll(page) {
  await page.evaluate(async () => {
    await new Promise((resolve) => {
      let totalHeight = 0;
      const distance = 100;
      const timer = setInterval(() => {
        window.scrollBy(0, distance);
        totalHeight += distance;

        if (totalHeight >= document.body.scrollHeight) {
          clearInterval(timer);
          resolve();
        }
      }, 100);
    });
  });
}

/* 
  
  firebase use YOUR-PROJECT-ID &&
  firebase deploy --only functions:generateSessionScreenshots
  */

4. Detect New Sessions with Cloud Function

This function detects the creation of a new user session and collects mouse positions during the visit.

Code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const functions = require("firebase-functions");
const admin = require("firebase-admin");

// Initialize Firebase Admin SDK if not already initialized
if (!admin.apps.length) {
  admin.initializeApp();
}

const db = admin.firestore();

// Cloud Function to detect new sessions every hour
exports.detectNewUserSessions = functions
  .region("your-region")
  .pubsub.schedule("every 60 minutes")
  .onRun(async (context) => {
    try {
      const referenceSnapshot = await db.collection("referenceSessions").get();
      const referenceIds = referenceSnapshot.docs.map((doc) => doc.id);

      const userSessionsSnapshot = await db.collection("sessions").get();
      const userSessionIds = userSessionsSnapshot.docs.map((doc) => doc.id);

      const newSessionIds = userSessionIds.filter(
        (id) => !referenceIds.includes(id)
      );

      if (newSessionIds.length > 0) {
        const batch = db.batch();

        // Loop through each new session and fetch its full document
        for (const id of newSessionIds) {
          const userSessionDoc = await db.collection("sessions").doc(id).get();

          if (userSessionDoc.exists) {
            const sessionData = userSessionDoc.data(); // Get the session data

            // Reference to newSessions collection where the new session will be saved
            const docRef = db.collection("newSessions").doc(id);

            // Save the session data into the newSessions collection with createdAt timestamp
            batch.set(docRef, {
              ...sessionData, // Spread the session data here
              createdAt: admin.firestore.FieldValue.serverTimestamp(),
            });
          }
        }

        await batch.commit();
        console.log(
          `Added ${newSessionIds.length} new session IDs with full session data to newSessions.`
        );
      } else {
        console.log("No new session IDs found.");
      }

      return null;
    } catch (error) {
      console.error("Error detecting new sessions:", error);
      throw new Error("Session detection failed");
    }
  });

/* 
  
  firebase use YOUR-PROJECT-ID &&
  firebase deploy --only functions:detectNewUserSessions
  */

5. Generate Mouse Movement-Based Heatmaps

Once we’ve collected the mouse data, we can generate a heatmap by overlaying mouse movements and clicks on a screenshot of the page.

Code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const { createCanvas, loadImage } = require("canvas");
const fs = require("fs");
const path = require("path");

if (!admin.apps.length) {
  admin.initializeApp();
}

const db = admin.firestore();
const bucket = admin.storage().bucket("YOUR_BUCKET");

// Function to retrieve screen sizes from Firestore
async function getScreenSizesFromConfig() {
  const configDoc = await db
    .collection("config")
    .doc("sessionsScreensRes")
    .get();
  if (configDoc.exists) {
    return configDoc.data().screenSizes || [];
  }
  return [];
}

// Function to parse the screen size string
function parseScreenSizeString(screenSizeStr) {
  const [width, height] = screenSizeStr.split("-");
  return {
    width: parseInt(width, 10),
    height: parseInt(height, 10),
    label: screenSizeStr,
  };
}

// Function to find the screen size label based on width and height
function findScreenSizeLabel(screenSizes, width, height) {
  return screenSizes.find((sizeStr) => {
    const { width: sizeWidth, height: sizeHeight } =
      parseScreenSizeString(sizeStr);
    return sizeWidth === width && sizeHeight === height;
  });
}

// Function to add new screen resolution to Firestore if not found
async function addNewScreenSizeToConfig(width, height) {
  const newLabel = `${width}-${height}`;
  await db
    .collection("config")
    .doc("sessionsScreensRes")
    .update({
      screenSizes: admin.firestore.FieldValue.arrayUnion(newLabel),
    });
  console.log(`Added new screen size: ${newLabel} to Firestore.`);
  return newLabel;
}

// Helper function to sanitize URL for filenames
function sanitizeUrlForFilename(url) {
  return url.replace(/^https?:\/\//, "").replace(/[^\w-]/g, "-");
}

// Function to generate the mouse movement and click overlay
async function generateOverlayImage(
  sessionId,
  sessionData,
  screenshotWidth,
  screenshotHeight
) {
  const mouseMovements = sessionData.mousePositions || [];
  const mouseClicks = sessionData.mouseClicks || [];

  const canvas = createCanvas(screenshotWidth, screenshotHeight);
  const ctx = canvas.getContext("2d");

  const viewportHeight =
    sessionData.sessionData.viewportHeight || screenshotHeight;
  const fullPageHeight = sessionData.sessionData.pageHeight || screenshotHeight;
  const viewportWidth =
    sessionData.sessionData.viewportWidth || screenshotWidth;

  const yScaleFactor = screenshotHeight / fullPageHeight;
  const xScaleFactor = screenshotWidth / viewportWidth;

  // Draw mouse movements
  mouseMovements.forEach(({ x, y }) => {
    const adjustedX = x * xScaleFactor;
    const adjustedY = y * yScaleFactor;
    if (
      adjustedX >= 0 &&
      adjustedX <= screenshotWidth &&
      adjustedY >= 0 &&
      adjustedY <= screenshotHeight
    ) {
      ctx.beginPath();
      ctx.arc(adjustedX, adjustedY, 20, 0, Math.PI * 2);
      ctx.fillStyle = "rgba(255, 0, 0, 0.8)";
      ctx.fill();
    }
  });

  // Draw mouse clicks
  mouseClicks.forEach(({ x, y }) => {
    const adjustedX = x * xScaleFactor;
    const adjustedY = y * yScaleFactor;
    if (
      adjustedX >= 0 &&
      adjustedX <= screenshotWidth &&
      adjustedY >= 0 &&
      adjustedY <= screenshotHeight
    ) {
      ctx.beginPath();
      ctx.arc(adjustedX, adjustedY, 15, 0, Math.PI * 2);
      ctx.fillStyle = "rgba(0, 0, 255, 0.8)";
      ctx.fill();
    }
  });

  const overlayFilePath = `/tmp/${sessionId}-overlay.png`;
  const buffer = canvas.toBuffer("image/png");
  fs.writeFileSync(overlayFilePath, buffer);
  console.log(`Overlay image saved at ${overlayFilePath}`);

  return overlayFilePath;
}

// Function to combine screenshot and overlay into a single image
async function combineScreenshotAndOverlay(
  screenshotPath,
  overlayPath,
  sessionId
) {
  const screenshot = await loadImage(screenshotPath);
  const overlay = await loadImage(overlayPath);

  const canvas = createCanvas(screenshot.width, screenshot.height);
  const ctx = canvas.getContext("2d");

  // Draw the screenshot as the background
  ctx.drawImage(screenshot, 0, 0, screenshot.width, screenshot.height);

  // Draw the overlay on top
  ctx.drawImage(overlay, 0, 0, screenshot.width, screenshot.height);

  const finalHeatmapFilePath = `/tmp/${sessionId}-final-heatmap.png`;
  const buffer = canvas.toBuffer("image/png");
  fs.writeFileSync(finalHeatmapFilePath, buffer);
  console.log(`Final heatmap saved at ${finalHeatmapFilePath}`);

  return finalHeatmapFilePath;
}

exports.generateSessionHeatmap = functions
  .region("your-region")
  .runWith({ memory: "1GB", timeoutSeconds: 300 }) // Set 1GB memory and 300s timeout
  .firestore.document("newSessions/{sessionId}")
  .onCreate(async (snap, context) => {
    const sessionId = context.params.sessionId;
    console.log(`Generating heatmap for session: ${sessionId}`);

    try {
      const sessionDoc = await db.collection("sessions").doc(sessionId).get();
      if (!sessionDoc.exists) {
        console.error(`Session ${sessionId} does not exist.`);
        return null;
      }

      const sessionData = sessionDoc.data();
      const mouseMovements = sessionData.mousePositions || [];
      const mouseClicks = sessionData.mouseClicks || [];
      const pageURL = sessionData.sessionData.pageURL;
      const screenWidth = sessionData.sessionData.screenWidth;
      const screenHeight = sessionData.sessionData.screenHeight;

      const viewportWidth =
        sessionData.sessionData.viewportWidth || screenWidth;
      const viewportHeight =
        sessionData.sessionData.viewportHeight || screenHeight;

      if (!pageURL || !screenWidth || !screenHeight) {
        console.error("Page URL or dimensions missing for session:", sessionId);
        return;
      }

      const pageSlug = sanitizeUrlForFilename(pageURL);

      // Retrieve screen sizes from Firestore
      const screenSizes = await getScreenSizesFromConfig();
      let screenSizeLabel = findScreenSizeLabel(
        screenSizes,
        screenWidth,
        screenHeight
      );

      if (!screenSizeLabel) {
        screenSizeLabel = await addNewScreenSizeToConfig(
          screenWidth,
          screenHeight
        );
      }

      const screenshotFile = bucket.file(
        `screenshots/${pageSlug}-${screenSizeLabel}.png`
      );
      const [screenshotBuffer] = await screenshotFile.download();

      const screenshotPath = `/tmp/${sessionId}-screenshot.png`;
      fs.writeFileSync(screenshotPath, screenshotBuffer);

      const screenshotImage = await loadImage(screenshotBuffer);
      const screenshotWidth = screenshotImage.width;
      const screenshotHeight = screenshotImage.height;

      // Generate the heatmap overlay based on the screenshot dimensions
      const overlayPath = await generateOverlayImage(
        sessionId,
        sessionData,
        screenshotWidth,
        screenshotHeight
      );

      // Combine the screenshot and overlay into a final image
      const finalHeatmapPath = await combineScreenshotAndOverlay(
        screenshotPath,
        overlayPath,
        sessionId
      );

      // Save the final heatmap to Cloud Storage
      const heatmapFile = bucket.file(`heatmaps/${sessionId}.png`);
      await heatmapFile.save(fs.readFileSync(finalHeatmapPath), {
        contentType: "image/png",
      });

      console.log(`Heatmap with screenshot saved for session ${sessionId}.`);
      return null;
    } catch (error) {
      console.error("Error generating heatmap:", error);
      throw new Error("Heatmap generation failed");
    }
  });

/* 
  
  firebase use YOUR-PROJECT-ID &&
  firebase deploy --only functions:generateSessionHeatmap
  */

6. Automatically Save Heatmap URLs to Firestore

Finally, we can automate the saving of generated heatmap URLs directly to Firestore, allowing us to retrieve and analyze them later.

Conclusion

By implementing this system, you gain detailed insights into how visitors interact with your site by analyzing mouse movements and clicks. With Cloud Functions, the process is fully automated, from heatmap creation to storage in Firestore.

Cost Warning

It is important to note that implementing such a system on Google Cloud could result in unforeseen costs, especially in the event of high website traffic or intensive use of resources such as Cloud Functions, Firestore, and Cloud Storage. It is recommended to carefully monitor resource usage and configure budget alerts to avoid excessive or unexpected costs. Use this system at your own risk and consider alternative solutions to manage costs effectively.