mirror of
https://github.com/rybbit-io/rybbit.git
synced 2025-05-10 20:05:38 +02:00
* Add goal management functionality in analytics - Introduced goal creation, deletion, and updating capabilities in the analytics API, allowing users to define conversion goals based on path or event types. - Implemented corresponding React hooks for managing goals, including fetching, creating, updating, and deleting goals. - Enhanced the UI with a dedicated Goals page and components for listing and managing goals, improving user experience in tracking conversions. - Updated package dependencies to include necessary libraries for form handling and validation. * Enhance goals management with pagination and sorting - Added pagination and sorting capabilities to the goals fetching logic in the analytics API, allowing users to navigate through goals more efficiently. - Updated the GoalsPage component to manage current page state and handle page changes, improving user experience. - Modified the GoalsList component to display pagination metadata and navigation controls, facilitating better goal management. - Adjusted the server-side getGoals function to support pagination and sorting parameters, ensuring accurate data retrieval. * Refactor GoalsPage and GoalCard components for improved UI and functionality - Updated GoalsPage to include a SubHeader component and adjusted layout for better responsiveness. - Enhanced loading and empty state handling in GoalsPage for a smoother user experience. - Modified GoalCard to use icons for goal types, improving visual clarity and consistency in the UI. * Refactor CreateGoalButton and GoalCard components for improved modal handling - Updated CreateGoalButton to utilize a trigger prop for the GoalFormModal, simplifying modal state management. - Refactored GoalCard to integrate GoalFormModal for editing goals, enhancing user interaction and reducing state complexity. - Removed unnecessary state management and modal handling from both components, streamlining the codebase. * Refactor GoalCard and Clickhouse initialization for improved code clarity - Removed unnecessary imports in GoalCard component, streamlining the code. - Updated Clickhouse initialization to include a new 'props' JSON field alongside 'properties', enhancing data structure for analytics. - Added a utility function in PageviewQueue to parse properties, improving error handling and data integrity. * enable clickhouse * fix ch build * fix ch build * fix ch build * wip * wip * wip * Enable json * add network * add network * Refactor Clickhouse configuration and remove unused properties from data models * Refactor property value handling in analytics queries to utilize native JSON types in ClickHouse, improving type safety and performance.
141 lines
4.5 KiB
TypeScript
141 lines
4.5 KiB
TypeScript
import { DateTime } from "luxon";
|
|
import clickhouse from "../db/clickhouse/clickhouse.js";
|
|
import { TrackingPayload } from "../types.js";
|
|
import { getDeviceType } from "../utils.js";
|
|
import { getChannel } from "./getChannel.js";
|
|
import { clearSelfReferrer, getUTMParams } from "./trackingUtils.js";
|
|
import { getLocation } from "../db/geolocation/geolocation.js";
|
|
|
|
type TotalPayload = TrackingPayload & {
|
|
userId: string;
|
|
timestamp: string;
|
|
sessionId: string;
|
|
ua: UAParser.IResult;
|
|
referrer: string;
|
|
ipAddress: string;
|
|
type?: string;
|
|
event_name?: string;
|
|
properties?: string;
|
|
};
|
|
|
|
const getParsedProperties = (properties: string | undefined) => {
|
|
try {
|
|
return properties ? JSON.parse(properties) : undefined;
|
|
} catch (error) {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
class PageviewQueue {
|
|
private queue: TotalPayload[] = [];
|
|
private batchSize = 5000;
|
|
private interval = 10000; // 10 seconds
|
|
private processing = false;
|
|
|
|
constructor() {
|
|
// Start processing interval
|
|
setInterval(() => this.processQueue(), this.interval);
|
|
}
|
|
|
|
async add(pageview: TotalPayload) {
|
|
this.queue.push(pageview);
|
|
}
|
|
|
|
private async processQueue() {
|
|
if (this.processing || this.queue.length === 0) return;
|
|
this.processing = true;
|
|
|
|
// Get batch of pageviews
|
|
const batch = this.queue.splice(0, this.batchSize);
|
|
const ips = [...new Set(batch.map((pv) => pv.ipAddress))];
|
|
|
|
let geoData: Record<string, { data: any }> = {};
|
|
|
|
try {
|
|
// Process each IP to get geo data using local implementation
|
|
const geoPromises = ips.map(async (ip) => {
|
|
const locationData = await getLocation(ip);
|
|
return { ip, locationData };
|
|
});
|
|
|
|
const results = await Promise.all(geoPromises);
|
|
|
|
// Format results to match expected structure
|
|
results.forEach(({ ip, locationData }) => {
|
|
geoData[ip] = { data: locationData };
|
|
});
|
|
} catch (error) {
|
|
console.error("Error getting geo data:", error);
|
|
}
|
|
|
|
// Process each pageview with its geo data
|
|
const processedPageviews = batch.map((pv) => {
|
|
const dataForIp = geoData?.[pv.ipAddress];
|
|
|
|
const countryCode = dataForIp?.data?.countryIso || "";
|
|
const regionCode = dataForIp?.data?.subdivisions?.[0]?.isoCode || "";
|
|
const latitude = dataForIp?.data?.latitude || 0;
|
|
const longitude = dataForIp?.data?.longitude || 0;
|
|
const city = dataForIp?.data?.city || "";
|
|
|
|
// Check if referrer is from the same domain and clear it if so
|
|
let referrer = clearSelfReferrer(pv.referrer || "", pv.hostname || "");
|
|
|
|
// Get UTM parameters
|
|
const utmParams = getUTMParams(pv.querystring || "");
|
|
|
|
return {
|
|
site_id: pv.site_id,
|
|
timestamp: DateTime.fromISO(pv.timestamp).toFormat(
|
|
"yyyy-MM-dd HH:mm:ss"
|
|
),
|
|
session_id: pv.sessionId,
|
|
user_id: pv.userId,
|
|
hostname: pv.hostname || "",
|
|
pathname: pv.pathname || "",
|
|
querystring: pv.querystring || "",
|
|
page_title: pv.page_title || "",
|
|
referrer: referrer,
|
|
channel: getChannel(referrer, pv.querystring, pv.hostname),
|
|
browser: pv.ua.browser.name || "",
|
|
browser_version: pv.ua.browser.major || "",
|
|
operating_system: pv.ua.os.name || "",
|
|
operating_system_version: pv.ua.os.version || "",
|
|
language: pv.language || "",
|
|
screen_width: pv.screenWidth || 0,
|
|
screen_height: pv.screenHeight || 0,
|
|
device_type: getDeviceType(pv.screenWidth, pv.screenHeight, pv.ua),
|
|
country: countryCode,
|
|
region: countryCode && regionCode ? countryCode + "-" + regionCode : "",
|
|
city: city || "",
|
|
lat: latitude || 0,
|
|
lon: longitude || 0,
|
|
type: pv.type || "pageview",
|
|
event_name: pv.event_name || "",
|
|
props: getParsedProperties(pv.properties),
|
|
utm_source: utmParams["utm_source"] || "",
|
|
utm_medium: utmParams["utm_medium"] || "",
|
|
utm_campaign: utmParams["utm_campaign"] || "",
|
|
utm_term: utmParams["utm_term"] || "",
|
|
utm_content: utmParams["utm_content"] || "",
|
|
};
|
|
});
|
|
|
|
console.info("bulk insert: ", processedPageviews.length);
|
|
// Bulk insert into database
|
|
try {
|
|
await clickhouse.insert({
|
|
table: "events",
|
|
values: processedPageviews,
|
|
format: "JSONEachRow",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error processing pageview queue:", error);
|
|
} finally {
|
|
this.processing = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create singleton instance
|
|
export const pageviewQueue = new PageviewQueue();
|