clickchutney-analytics
Version:
Privacy-first web analytics - bite-sized insights for your website 🌶️
201 lines (197 loc) • 5.98 kB
JavaScript
'use client';
import { useEffect } from 'react';
let endpoint = '';
let trackingId = '';
let debug = false;
let beforeSend;
let queue = [];
const getDefaultEndpoint = () => {
if (typeof window !== 'undefined') {
const hostname = window.location.hostname;
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('localhost')) {
return 'http://localhost:8787/api/collect';
}
}
return 'https://clickchutney-analytics.contact-sushilpandey.workers.dev/api/collect';
};
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
};
const getSessionId = () => {
if (typeof window === 'undefined')
return generateId();
let sessionId = sessionStorage.getItem('cc_session_id');
if (!sessionId) {
sessionId = generateId();
sessionStorage.setItem('cc_session_id', sessionId);
}
return sessionId;
};
const getUserId = () => {
if (typeof window === 'undefined')
return generateId();
let userId = localStorage.getItem('cc_user_id');
if (!userId) {
userId = generateId();
localStorage.setItem('cc_user_id', userId);
}
return userId;
};
const log = (message, data) => {
if (debug) {
console.log(`[ClickChutney] ${message}`, data || '');
}
};
const sendEvents = async (events) => {
try {
if (!trackingId) {
log('No tracking ID provided, events will not be sent');
return;
}
const payload = {
trackingId,
events: events.map(event => beforeSend ? beforeSend(event) : event).filter(Boolean)
};
if (payload.events.length === 0)
return;
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
keepalive: true,
});
log('Events sent', payload);
}
catch (error) {
log('Failed to send events', { error, events });
}
};
const track = (name, properties = {}) => {
if (typeof window === 'undefined')
return;
const event = {
type: name,
data: {
...properties,
url: window.location.href,
referrer: document.referrer || undefined,
title: document.title,
sessionId: getSessionId(),
userId: getUserId(),
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
screen: {
width: window.screen.width,
height: window.screen.height,
},
viewport: {
width: window.innerWidth,
height: window.innerHeight,
}
}
};
queue.push(event);
log('Event queued', event);
// For page views or if queue is full, send immediately
if (name === 'pageview' || queue.length >= 5) {
flushQueue();
}
else {
// Batch other events - send after 3 seconds
setTimeout(flushQueue, 3000);
}
};
const flushQueue = async () => {
if (queue.length === 0)
return;
const eventsToSend = [...queue];
queue = [];
await sendEvents(eventsToSend);
};
const page = (path) => {
const url = path || (typeof window !== 'undefined' ? window.location.pathname : '');
track('pageview', { path: url });
};
let hasSetupHistoryTracking = false;
const setupAutoTracking = () => {
if (typeof window === 'undefined' || hasSetupHistoryTracking)
return;
hasSetupHistoryTracking = true;
const trackPageView = () => {
setTimeout(() => page(), 0);
};
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(history, args);
trackPageView();
};
history.replaceState = function (...args) {
originalReplaceState.apply(history, args);
trackPageView();
};
window.addEventListener('popstate', trackPageView);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', trackPageView);
}
else {
trackPageView();
}
};
const inject = (options = {}) => {
trackingId = options.trackingId || '';
endpoint = options.endpoint || getDefaultEndpoint();
debug = options.debug || false;
beforeSend = options.beforeSend;
if (!trackingId) {
console.warn('[ClickChutney] No tracking ID provided. Analytics will not be sent.');
}
log('ClickChutney Analytics initialized', { trackingId: trackingId ? '***' : 'missing', endpoint });
setupAutoTracking();
};
if (typeof window !== 'undefined') {
window.ccAnalytics = { inject, track, page };
}
/**
* ClickChutney Analytics component for React/Next.js
*
* @example
* ```tsx
* import { Analytics } from '@clickchutney/analytics/react';
*
* export default function RootLayout({ children }) {
* return (
* <html>
* <body>
* {children}
* <Analytics />
* </body>
* </html>
* );
* }
* ```
*/
function Analytics({ trackingId, endpoint, debug = false, beforeSend, enabled = true, } = {}) {
useEffect(() => {
if (!enabled)
return;
// Only initialize in browser environment
if (typeof window === 'undefined')
return;
// Don't track in development mode unless debug is enabled
if (process.env.NODE_ENV === 'development' && !debug) {
console.log('[ClickChutney] Skipping analytics in development mode');
return;
}
inject({
trackingId,
endpoint,
debug,
beforeSend,
});
}, [trackingId, endpoint, debug, beforeSend, enabled]);
return null;
}
export { Analytics, Analytics as default };