basic interactable callbacks from webapp to kafka + redpanda console

This commit is contained in:
2025-10-23 15:48:29 +02:00
parent ae202631e1
commit 8797fb3976
8 changed files with 188 additions and 5 deletions

View File

@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server';
import { sendInteractionEvent } from '@/lib/kafka';
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { sessionId, eventType, targetEl, targetUrl, metadata } = body;
if (!sessionId || !eventType) {
return NextResponse.json(
{ error: 'sessionId and eventType required' },
{ status: 400 }
);
}
await sendInteractionEvent({
sessionId,
eventType,
targetEl,
targetUrl,
metadata,
ts: Date.now(),
});
return NextResponse.json({ success: true });
} catch (err: any) {
console.error('track error:', err);
return NextResponse.json(
{ error: err.message || 'unknown error' },
{ status: 500 }
);
}
}

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { TrackingProvider } from "@/components/TrackingProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -27,7 +28,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<TrackingProvider>{children}</TrackingProvider>
</body>
</html>
);

View File

@@ -0,0 +1,8 @@
'use client';
import { useInteractionTracking } from '@/hooks/useInteractionTracking';
export const TrackingProvider = ({ children }: { children: React.ReactNode }) => {
useInteractionTracking();
return <>{children}</>;
};

View File

@@ -0,0 +1,83 @@
import { useEffect, useRef } from 'react';
const genSessionId = () => {
if (typeof window === 'undefined') return '';
let sid = sessionStorage.getItem('phantom_session_id');
if (!sid) {
sid = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
sessionStorage.setItem('phantom_session_id', sid);
}
return sid;
};
const track = async (ev: {
sessionId: string;
eventType: string;
targetEl?: string;
targetUrl?: string;
metadata?: Record<string, any>;
}) => {
try {
await fetch('/api/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ev),
});
} catch (err) {
console.error('track failed:', err);
}
};
export const useInteractionTracking = () => {
const sidRef = useRef<string>('');
useEffect(() => {
sidRef.current = genSessionId();
const handleClick = (e: MouseEvent) => {
const tgt = e.target as HTMLElement;
track({
sessionId: sidRef.current,
eventType: 'click',
targetEl: tgt.tagName,
targetUrl: tgt instanceof HTMLAnchorElement ? tgt.href : undefined,
metadata: {
x: e.clientX,
y: e.clientY,
path: window.location.pathname,
},
});
};
const handleScroll = () => {
track({
sessionId: sidRef.current,
eventType: 'scroll',
metadata: {
scrollY: window.scrollY,
path: window.location.pathname,
},
});
};
const handlePageView = () => {
track({
sessionId: sidRef.current,
eventType: 'pageview',
metadata: {
path: window.location.pathname,
referrer: document.referrer,
},
});
};
handlePageView();
document.addEventListener('click', handleClick);
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
document.removeEventListener('click', handleClick);
window.removeEventListener('scroll', handleScroll);
};
}, []);
};

41
web/src/lib/kafka.ts Normal file
View File

@@ -0,0 +1,41 @@
import { Kafka, Producer } from 'kafkajs';
let producer: Producer | null = null;
const kafka = new Kafka({
clientId: 'phantom-web',
brokers: [`${process.env.KAFKA_HOST || 'localhost'}:${process.env.KAFKA_PORT || '9092'}`],
});
export const getProducer = async (): Promise<Producer> => {
if (!producer) {
producer = kafka.producer();
await producer.connect();
}
return producer;
};
export const sendInteractionEvent = async (ev: {
sessionId: string;
eventType: string;
targetEl?: string;
targetUrl?: string;
metadata?: Record<string, any>;
ts: number;
}) => {
const p = await getProducer();
await p.send({
topic: 'user-interactions',
messages: [{
key: ev.sessionId,
value: JSON.stringify(ev),
}],
});
};
export const disconnect = async () => {
if (producer) {
await producer.disconnect();
producer = null;
}
};