Skip to content

Commit

Permalink
Tracker improved with more functions (#12)
Browse files Browse the repository at this point in the history
* feat: tracker improved with more functions

* BREAKING CHANGE: removing tracker cdn file replacement is pushed

* fixes: coverage tests for tracker added
  • Loading branch information
priyanshuverma-dev authored Nov 18, 2024
1 parent 450f970 commit c3585b7
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 91 deletions.
11 changes: 0 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,6 @@ jobs:
- name: Install dependencies
run: npm install # or 'bun install' if you're using Bun

- name: Set version based on branch
id: version
run: |
if [ "${GITHUB_REF}" == "refs/heads/dev" ]; then
VERSION="canary-${GITHUB_SHA}"
echo "Versioning as Canary: $VERSION"
else
VERSION="v$(stable-${GITHUB_SHA})"
echo "Versioning as Stable: $VERSION"
fi
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Set up GitHub Token
run: |
echo "GITHUB_TOKEN=${{ secrets.PERSONAL_ACCESS_TOKEN }}" >> $GITHUB_ENV
Expand Down
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"discord.js": "^14.16.3",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"terser": "^5.36.0",
"winston": "^3.17.0",
"zod": "^3.23.8"
},
Expand Down
56 changes: 0 additions & 56 deletions scripts/tracker.js

This file was deleted.

39 changes: 35 additions & 4 deletions src/api/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Request, Response } from "express";
import { TrackedDataSchema } from "../lib/schema";
import { fetchLocationData } from "../lib/utils";
import { fetchLocationData, minify } from "../lib/utils";
import { sitesConfig } from "../lib/sitesConfig";
import client from "../lib/discord";
import { ChannelType, TextChannel } from "discord.js";
import logger from "../lib/logger";
import { getDeviceInfo, getSessionId } from "../scripts/utilities";
import { sendAnalyticsFn, trackUserBehavior } from "../scripts/core";

export const trackAction = async (
req: Request,
res: Response
): Promise<void> => {
try {
const { success, data, error } = TrackedDataSchema.safeParse(req.body);

if (!success || !data) {
const formattedErrors = error.errors.map((err) => ({
path: err.path.join("."), // Dot notation path
Expand All @@ -26,8 +27,6 @@ export const trackAction = async (
return;
}

logger.info("Validation successful", { data });

const locationData = await fetchLocationData(req);
logger.debug("Fetched location data", { locationData });

Expand All @@ -47,6 +46,7 @@ export const trackAction = async (
session_id: data.sessionId,
device_info: `${data.deviceInfo.platform}, ${data.deviceInfo.language}, ${data.deviceInfo.userAgent}`,
location: locationData,
additionalData: data.additionalData,
};

const channel = (await client.channels.fetch(
Expand Down Expand Up @@ -164,3 +164,34 @@ export const sendAnalytics = async (
res.status(500).json({ error: "Failed to fetch analytics data" });
}
};

export const trackingScript = async (req: Request, res: Response) => {
const trackingUrl = `${req.protocol}://${req.get("host")}/track`;
let script = `
(function () {
${getSessionId}
${getDeviceInfo}
const sessionId = getSessionId();
${sendAnalyticsFn(trackingUrl)}
`;

// TODO! ADD FEATURES
script += trackUserBehavior;

script += `
sendAnalytics("pageview");
window.addEventListener("beforeunload", () => {
sendAnalytics("leave");
});
})();
`;

try {
const minified = await minify(script);
res.type("application/javascript").send(minified.code);
} catch (error) {
logger.error("Minification error:", error);
console.error("Minification error:", error);
res.status(500).send("Error generating the script");
}
};
16 changes: 7 additions & 9 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Router } from 'express'
import { sendAnalytics, trackAction } from './handlers';
import { Router } from "express";
import { sendAnalytics, trackAction, trackingScript } from "./handlers";

const router = Router();

const router = Router()
router.post("/track", trackAction);
router.get("/api/analytics", sendAnalytics);

router.get("/scripts/tracker", trackingScript);


router.post("/track", trackAction)
router.get("/analytics", sendAnalytics)


export default router;
export default router;
10 changes: 10 additions & 0 deletions src/lib/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { z } from "zod";

export enum EventType {
PageView = "pageview",
Click = "click",
FormSubmission = "form_submission",
TimeOnPage = "time_on_page",
Leave = "leave",
Scroll = "scroll",
}

export const TrackedDataSchema = z.object({
eventType: z.string(),
page: z.string(),
Expand All @@ -12,4 +21,5 @@ export const TrackedDataSchema = z.object({
platform: z.string(),
userAgent: z.string(),
}),
additionalData: z.any().optional(),
});
6 changes: 6 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from "axios";
import { Request } from "express";
import terser from "terser";

export async function fetchLocationData(req: Request) {
let clientIP: string;
Expand Down Expand Up @@ -41,3 +42,8 @@ export async function fetchLocationData(req: Request) {

return locationData;
}

export async function minify(script: string) {
const minified = await terser.minify(script, { ecma: 2020 });
return minified;
}
77 changes: 77 additions & 0 deletions src/scripts/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export const sendAnalyticsFn = (trackingUrl: string) => `
const sendAnalytics = async (eventType, additionalData = {}) => {
try {
const payload = {
eventType,
page: window.location.pathname,
referrer: document.referrer,
timestamp: new Date().toISOString(),
sessionId,
deviceInfo: getDeviceInfo(),
url: window.location.origin,
additionalData
};
await fetch("${trackingUrl}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
} catch (error) {
console.error("Analytics tracking error:", error);
}
};
`;

export const trackUserBehavior = `
// Ensure sendAnalytics is defined
if (typeof sendAnalytics !== "function") {
console.error("sendAnalytics is not defined.");
return;
}
// Track clicks on the page
document.addEventListener("click", (event) => {
const target = event.target;
const clickData = {
tagName: target.tagName,
id: target.id || null,
classes: target.className || null,
text: target.innerText ? target.innerText.slice(0, 50) : null, // Limit to 50 characters
};
sendAnalytics("click", clickData);
});
// Track scroll depth (debounced for performance)
let maxScrollDepth = 0;
let scrollTimeout;
const handleScroll = () => {
const scrollDepth =
(window.scrollY + window.innerHeight) / document.body.scrollHeight;
maxScrollDepth = Math.max(maxScrollDepth, scrollDepth);
// Debounce updates to reduce frequency
if (scrollTimeout) clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
sendAnalytics("scroll", { maxScrollDepth });
}, 200);
};
window.addEventListener("scroll", handleScroll);
// Track form submissions
document.addEventListener("submit", (event) => {
const form = event.target;
const formData = {
action: form.action || null,
method: form.method || null,
};
sendAnalytics("form_submission", formData);
});
// Track time spent on the page
const pageStartTime = Date.now();
window.addEventListener("beforeunload", () => {
const timeSpent = Math.round((Date.now() - pageStartTime) / 1000);
sendAnalytics("time_on_page", { seconds: timeSpent, maxScrollDepth });
});
`;
19 changes: 19 additions & 0 deletions src/scripts/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const getSessionId = `
const getSessionId = () => {
const sessionKey = "statstream_session";
let sessionId = localStorage.getItem(sessionKey);
if (!sessionId) {
sessionId = "ss-" + Math.random().toString(36).substr(2, 9);
localStorage.setItem(sessionKey, sessionId);
}
return sessionId;
};
`;

export const getDeviceInfo = `
const getDeviceInfo = () => ({
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
});
`;
9 changes: 4 additions & 5 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ type LocationData = {
timezone?: string;
};



type AnalyticsEvent = {
event: string;
page: string;
Expand All @@ -28,16 +26,17 @@ type AnalyticsEvent = {
timestamp: Date;
device_info: string;
location: LocationData;
}
additionalData: Record<string, any>;
};

type AggregatedAnalytics = {
totalEvents: number;
uniqueSessions: number;
pageViews: Record<string, number>;
referrers: Record<string, number>;
locations: Record<string, number>;
}
};

type DashboardData = {
[siteName: string]: AggregatedAnalytics;
}
};
Loading

0 comments on commit c3585b7

Please sign in to comment.