From feee98aade11c3bc245be61d389c13542d08e240 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Thu, 9 Jan 2025 13:42:19 +0700 Subject: [PATCH] add tracking error and query type behavior --- next.config.js | 20 ------- src/app/api/events/route.ts | 33 ++++++++---- src/components/gui/database-gui.tsx | 27 +++++++++- src/components/gui/windows-tab.tsx | 21 ++++---- src/components/lib/multiple-query.ts | 26 +++++++++ src/components/page-tracker.tsx | 81 ++++++++++++++++++++++------ src/drivers/sql-helper.ts | 39 ++++++++++++++ src/lib/tracking.ts | 26 ++++++--- src/messages/open-tab.tsx | 1 + 9 files changed, 208 insertions(+), 66 deletions(-) create mode 100644 src/drivers/sql-helper.ts diff --git a/next.config.js b/next.config.js index 1bbf7b4a..4910094f 100644 --- a/next.config.js +++ b/next.config.js @@ -9,26 +9,6 @@ const nextConfig = { env: { NEXT_PUBLIC_STUDIO_VERSION: pkg.version, }, - headers: async () => { - return [ - { - source: "/api/events", - headers: [ - { key: "Access-Control-Allow-Origin", value: "*" }, - { key: "Access-Control-Allow-Credentials", value: "true" }, - { - key: "Access-Control-Allow-Headers", - value: - "X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date", - }, - { - key: "Access-Control-Allow-Methods", - value: "GET,DELETE,PATCH,POST,PUT", - }, - ], - }, - ]; - }, }; module.exports = withMDX(nextConfig); diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index 0d4df4b9..cb46d68d 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -4,9 +4,9 @@ // // All recorded data will be stored in the Starbase Database. -import { cookies } from "next/headers"; +import { headers } from "next/headers"; import zod from "zod"; -import { NextRequest, NextResponse } from "next/server"; +import { after, NextRequest, NextResponse } from "next/server"; import { insertTrackingRecord } from "@/lib/api/insert-tracking-record"; const eventBodySchema = zod.object({ @@ -20,17 +20,26 @@ const eventBodySchema = zod.object({ .min(1), }); +export async function OPTIONS() { + // Handle preflight requests + return new NextResponse(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization, x-ob-id", + }, + }); +} + export const POST = async (req: NextRequest) => { // Getting the device id - const cookieStore = await cookies(); - - let deviceId = cookieStore.get("od-id")?.value; + const headerStore = await headers(); + const deviceId = headerStore.get("x-od-id"); if (!deviceId) { - deviceId = crypto.randomUUID(); - - cookieStore.set("od-id", deviceId, { - expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + return NextResponse.json({ + success: false, + error: "Device ID is required", }); } @@ -46,7 +55,11 @@ export const POST = async (req: NextRequest) => { } // Save the event - await insertTrackingRecord(deviceId, validate.data.events.slice(0, 50)); + after(() => { + insertTrackingRecord(deviceId, validate.data.events.slice(0, 50)) + .then() + .catch(); + }); return NextResponse.json({ success: true, diff --git a/src/components/gui/database-gui.tsx b/src/components/gui/database-gui.tsx index d1e0bd18..c50f429f 100644 --- a/src/components/gui/database-gui.tsx +++ b/src/components/gui/database-gui.tsx @@ -4,7 +4,7 @@ import { ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { openTab } from "@/messages/open-tab"; import WindowTabs, { WindowTabItemProps } from "./windows-tab"; import useMessageListener from "@/components/hooks/useMessageListener"; @@ -21,6 +21,7 @@ import { useSchema } from "@/context/schema-provider"; import { Binoculars, GearSix, Table } from "@phosphor-icons/react"; import DoltSidebar from "./database-specified/dolt/dolt-sidebar"; import { DoltIcon } from "../icons/outerbase-icon"; +import { normalizedPathname, sendAnalyticEvents } from "@/lib/tracking"; export default function DatabaseGui() { const DEFAULT_WIDTH = 300; @@ -43,6 +44,7 @@ export default function DatabaseGui() { key: "query", component: , icon: Binoculars, + type: "query", }, ]); @@ -137,6 +139,27 @@ export default function DatabaseGui() { ].filter(Boolean) as { text: string; onClick: () => void }[]; }, [currentSchemaName, databaseDriver]); + const onTabSelectChange = useCallback( + (newTabIndex: number) => { + setSelectedTabIndex(newTabIndex); + + const currentTab = tabs[newTabIndex]; + if (currentTab) { + sendAnalyticEvents([ + { + name: "page_view", + data: { + path: normalizedPathname(window.location.pathname), + tab: currentTab.type, + tab_key: currentTab.key, + }, + }, + ]); + } + }, + [tabs] + ); + return (
@@ -149,7 +172,7 @@ export default function DatabaseGui() { menu={tabSideMenu} tabs={tabs} selected={selectedTabIndex} - onSelectChange={setSelectedTabIndex} + onSelectChange={onTabSelectChange} onTabsChange={setTabs} /> diff --git a/src/components/gui/windows-tab.tsx b/src/components/gui/windows-tab.tsx index ea5e5e1f..03be42e2 100644 --- a/src/components/gui/windows-tab.tsx +++ b/src/components/gui/windows-tab.tsx @@ -30,6 +30,7 @@ export interface WindowTabItemProps { title: string; identifier: string; key: string; + type?: string; } interface WindowTabsProps { @@ -167,18 +168,18 @@ export default function WindowTabs({ hideCloseButton ? undefined : () => { - const newTabs = tabs.filter( - (t) => t.key !== tab.key - ); + const newTabs = tabs.filter( + (t) => t.key !== tab.key + ); - if (selected >= idx) { - onSelectChange(newTabs.length - 1); - } + if (selected >= idx) { + onSelectChange(newTabs.length - 1); + } - if (onTabsChange) { - onTabsChange(newTabs); + if (onTabsChange) { + onTabsChange(newTabs); + } } - } } /> ))} @@ -220,7 +221,7 @@ export default function WindowTabs({
{tab.component} diff --git a/src/components/lib/multiple-query.ts b/src/components/lib/multiple-query.ts index b196136f..ddb00aeb 100644 --- a/src/components/lib/multiple-query.ts +++ b/src/components/lib/multiple-query.ts @@ -3,10 +3,13 @@ import { DatabaseResultSet, DatabaseResultStat, } from "@/drivers/base-driver"; +import { getSQLStatementType, SQLStatementType } from "@/drivers/sql-helper"; +import { sendAnalyticEvents } from "@/lib/tracking"; export interface MultipleQueryProgressItem { order: number; sql: string; + statementType?: SQLStatementType; start: number; end?: number; stats?: DatabaseResultStat; @@ -39,14 +42,17 @@ export async function multipleQuery( for (let i = 0; i < statements.length; i++) { const statement = statements[i] as string; + const statementType = statement ? getSQLStatementType(statement) : "OTHER"; const log: MultipleQueryProgressItem = { order: i, sql: statement, start: Date.now(), + statementType, }; logs.push(log); + if (onProgress) { onProgress({ logs, progress: i, total: statements.length }); } @@ -85,5 +91,25 @@ export async function multipleQuery( } } + sendAnalyticEvents([ + ...logs.map((entry) => { + return { + name: "evt_query_execute", + data: { + sql: entry.statementType ?? "OTHER", + type: entry.statementType ?? "OTHER", + }, + }; + }), + ...result.map((entry) => { + return { + name: "evt_rows_queried", + data: { + count: entry.result.rows.length, + }, + }; + }), + ]); + return { result, logs }; } diff --git a/src/components/page-tracker.tsx b/src/components/page-tracker.tsx index 202a98c4..5253d37b 100644 --- a/src/components/page-tracker.tsx +++ b/src/components/page-tracker.tsx @@ -1,6 +1,6 @@ "use client"; -import { addTrackEvent, normalizedPathname } from "@/lib/tracking"; +import { sendAnalyticEvents, normalizedPathname } from "@/lib/tracking"; import { usePathname } from "next/navigation"; import { useEffect } from "react"; @@ -11,33 +11,80 @@ export default function PageTracker() { useEffect(() => { const normalized = normalizedPathname(pathname); - addTrackEvent("pageview", { - path: normalized, - full_path: pathname === normalized ? undefined : pathname, - }); + sendAnalyticEvents([ + { + name: "page_view", + data: { + path: normalized, + full_path: pathname === normalized ? undefined : pathname, + }, + }, + ]); }, [pathname]); // Track unhandle rejection useEffect(() => { - window.addEventListener("unhandledrejection", (event) => { + if (typeof window === "undefined") return; + + const handler = (event: PromiseRejectionEvent) => { if (typeof event.reason === "string") { - addTrackEvent("unhandledrejection", { - message: event.reason, - }); + sendAnalyticEvents([ + { + name: "unhandledrejection", + data: { message: event.reason, path: window.location.pathname }, + }, + ]); } else if (event.reason?.message) { - addTrackEvent("unhandledrejection", { - message: event.reason.message, - stack: event.reason.stack, - }); + sendAnalyticEvents([ + { + name: "unhandledrejection", + data: { + message: event.reason.message, + stack: event.reason.stack, + path: window.location.pathname, + }, + }, + ]); } else { - addTrackEvent("unhandledrejection", event.toString()); + sendAnalyticEvents([ + { + name: "unhandledrejection", + data: { + message: event.toString(), + }, + }, + ]); } - }); + }; + + window.addEventListener("unhandledrejection", handler); return () => { - window.removeEventListener("unhandledrejection", () => {}); + window.removeEventListener("unhandledrejection", handler); }; }, []); - return null; + // Track other unhandled error + useEffect(() => { + if (typeof window === "undefined") return; + + const handler = (event: ErrorEvent) => { + sendAnalyticEvents([ + { + name: "error", + data: { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }, + }, + ]); + }; + + window.addEventListener("error", handler); + return () => { + window.removeEventListener("error", handler); + }; + }, []); } diff --git a/src/drivers/sql-helper.ts b/src/drivers/sql-helper.ts new file mode 100644 index 00000000..c29aa5d2 --- /dev/null +++ b/src/drivers/sql-helper.ts @@ -0,0 +1,39 @@ +export type SQLStatementType = + | "SELECT" + | "INSERT" + | "UPDATE" + | "CREATE_TABLE" + | "ALTER_TABLE" + | "DROP_TABLE" + | "CREATE_INDEX" + | "DROP_INDEX" + | "CREATE_VIEW" + | "DROP_VIEW" + | "CREATE_TRIGGER" + | "DROP_TRIGGER" + | "OTHER"; + +export function getSQLStatementType(statement: string): SQLStatementType { + let trimmed = statement.trim().toUpperCase(); + + // Reduce continuous whitespaces to single whitespace + trimmed = trimmed.replace(/\s+/g, " "); + + // Replace the "IF NOT EXISTS" clause with an empty string + trimmed = trimmed.replace("IF NOT EXISTS", ""); + + if (trimmed.startsWith("SELECT")) return "SELECT"; + if (trimmed.startsWith("INSERT")) return "INSERT"; + if (trimmed.startsWith("UPDATE")) return "UPDATE"; + if (trimmed.startsWith("CREATE TABLE")) return "CREATE_TABLE"; + if (trimmed.startsWith("ALTER TABLE")) return "ALTER_TABLE"; + if (trimmed.startsWith("DROP TABLE")) return "DROP_TABLE"; + if (trimmed.startsWith("CREATE INDEX")) return "CREATE_INDEX"; + if (trimmed.startsWith("DROP INDEX")) return "DROP_INDEX"; + if (trimmed.startsWith("CREATE VIEW")) return "CREATE_VIEW"; + if (trimmed.startsWith("DROP VIEW")) return "DROP_VIEW"; + if (trimmed.startsWith("CREATE TRIGGER")) return "CREATE_TRIGGER"; + if (trimmed.startsWith("DROP TRIGGER")) return "DROP_TRIGGER"; + + return "OTHER"; +} diff --git a/src/lib/tracking.ts b/src/lib/tracking.ts index 264bca52..6089865c 100644 --- a/src/lib/tracking.ts +++ b/src/lib/tracking.ts @@ -17,17 +17,29 @@ export function normalizedPathname(pathname: string) { return pathname; } -export function addTrackEvent(eventName: string, data?: unknown) { +export function sendAnalyticEvents(events: TrackEventItem[]) { if (typeof window === "undefined") { return; } if (!process.env.NEXT_PUBLIC_ANALYTIC_ENABLED) return; - window.navigator.sendBeacon( - "/api/events", - JSON.stringify({ - events: [{ name: eventName, data }], - }) - ); + let deviceId = localStorage.getItem("od-id"); + if (!deviceId) { + deviceId = crypto.randomUUID(); + localStorage.setItem("od-id", deviceId); + } + + fetch("/api/events", { + method: "POST", + body: JSON.stringify({ + events, + }), + headers: { + "Content-Type": "application/json", + "x-od-id": deviceId, + }, + }) + .then() + .catch(); } diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index 1d87c0c2..f70819ae 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -185,6 +185,7 @@ export function receiveOpenTabMessage({ key, identifier: key, component: generateComponent(newTab, title), + type: newTab.type, }, ]; });