Improve automation, relinted webapp to tab alignment

and introduced basic EDA in a jupyter notebook of kafka export (nonmodular yet)
This commit is contained in:
2025-11-02 21:34:18 +01:00
parent 8797fb3976
commit f4da5289bd
7 changed files with 680 additions and 118 deletions

View File

@@ -5,8 +5,13 @@ TEX := main.tex
JOBNAME := main JOBNAME := main
PDF := paper/$(BUILDDIR)/$(JOBNAME).pdf PDF := paper/$(BUILDDIR)/$(JOBNAME).pdf
.DEFAULT_GOAL := help
all: pdf all: pdf
run.webapp:
@cd web && npm install && npm run dev
$(BUILDDIR): $(BUILDDIR):
mkdir -p paper/$(BUILDDIR) mkdir -p paper/$(BUILDDIR)
@@ -31,4 +36,4 @@ clean:
rm -rf paper/$(BUILDDIR)/* rm -rf paper/$(BUILDDIR)/*
.PHONY: all pdf clean watch .PHONY: all pdf clean watch run.webapp

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,6 @@
kafka-python kafka-python
dotenv dotenv
pandas
jupyter
ipykernel
matplotlib

View File

@@ -2,32 +2,32 @@ import { NextRequest, NextResponse } from 'next/server';
import { sendInteractionEvent } from '@/lib/kafka'; import { sendInteractionEvent } from '@/lib/kafka';
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const body = await req.json(); const body = await req.json();
const { sessionId, eventType, targetEl, targetUrl, metadata } = body; const { sessionId, eventType, targetEl, targetUrl, metadata } = body;
if (!sessionId || !eventType) { if (!sessionId || !eventType) {
return NextResponse.json( return NextResponse.json(
{ error: 'sessionId and eventType required' }, { error: 'sessionId and eventType required' },
{ status: 400 } { 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 }
);
} }
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,83 +1,117 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import '@/lib/experiments' // ensure experiments lib is loaded
const genSessionId = () => { const genSessionId = () => {
if (typeof window === 'undefined') return ''; if (typeof window === 'undefined') return '';
let sid = sessionStorage.getItem('phantom_session_id'); let sid = sessionStorage.getItem('phantom_session_id');
if (!sid) { if (!sid) {
sid = `${Date.now()}-${Math.random().toString(36).slice(2)}`; sid = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
sessionStorage.setItem('phantom_session_id', sid); sessionStorage.setItem('phantom_session_id', sid);
} // TODO: when creating new id send to exepriemtn tracking db
return sid; // match between sesion-id and experiment-id for this session
// so that we can identify all interactions aligning with a specific experiment goal.
}
return sid;
}; };
const track = async (ev: { const track = async (ev: {
sessionId: string; sessionId: string;
eventType: string; eventType: string;
targetEl?: string; targetEl?: string;
targetUrl?: string; targetUrl?: string;
metadata?: Record<string, any>; metadata?: Record<string, any>;
}) => { }) => {
try { try {
await fetch('/api/track', { await fetch('/api/track', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ev), body: JSON.stringify(ev),
}); });
} catch (err) { } catch (err) {
console.error('track failed:', err); console.error('track failed:', err);
} }
}; };
export const useInteractionTracking = () => { export const useInteractionTracking = () => {
const sidRef = useRef<string>(''); const sidRef = useRef<string>('');
useEffect(() => { useEffect(() => {
sidRef.current = genSessionId(); sidRef.current = genSessionId();
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
const tgt = e.target as HTMLElement; const tgt = e.target as HTMLElement;
track({ track({
sessionId: sidRef.current, sessionId: sidRef.current,
eventType: 'click', eventType: 'click',
targetEl: tgt.tagName, targetEl: tgt.tagName,
targetUrl: tgt instanceof HTMLAnchorElement ? tgt.href : undefined, targetUrl: tgt instanceof HTMLAnchorElement ? tgt.href : undefined,
metadata: { metadata: {
x: e.clientX, x: e.clientX,
y: e.clientY, y: e.clientY,
path: window.location.pathname, path: window.location.pathname,
}, },
}); });
}; };
const handleScroll = () => { const handleScroll = () => {
track({ track({
sessionId: sidRef.current, sessionId: sidRef.current,
eventType: 'scroll', eventType: 'scroll',
metadata: { metadata: {
scrollY: window.scrollY, scrollY: window.scrollY,
path: window.location.pathname, path: window.location.pathname,
}, },
}); });
}; };
const handlePageView = () => { const handlePageView = () => {
track({ track({
sessionId: sidRef.current, sessionId: sidRef.current,
eventType: 'pageview', eventType: 'pageview',
metadata: { metadata: {
path: window.location.pathname, path: window.location.pathname,
referrer: document.referrer, referrer: document.referrer,
}, },
}); });
}; };
handlePageView(); enum DefinedInteractions {
document.addEventListener('click', handleClick); ADD_TO_CART = 'add_to_cart',
window.addEventListener('scroll', handleScroll, { passive: true }); PURCHASE = 'purchase',
}
return () => { // called when clicking on "Add to Cart" button or "Purchase" button
document.removeEventListener('click', handleClick); const handleDefinedInteraction = (
window.removeEventListener('scroll', handleScroll); interactionType: DefinedInteractions,
}; metadata?: Record<string, any>
}, []); ) => {
track({
sessionId: sidRef.current,
eventType: interactionType,
metadata: {
path: window.location.pathname,
...metadata,
},
});
};
handlePageView();
document.addEventListener('click', handleClick);
document.addEventListener('definedInteraction', (e: Event) => {
const customEvent = e as CustomEvent;
handleDefinedInteraction(customEvent.detail.interactionType, customEvent.detail.metadata);
});
// TOO NOISY: enable if needed but tbh not worth it
//window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
document.removeEventListener('click', handleClick);
document.removeEventListener('definedInteraction', (e: Event) => {
const customEvent = e as CustomEvent;
handleDefinedInteraction(customEvent.detail.interactionType, customEvent.detail.metadata);
});
//window.removeEventListener('scroll', handleScroll);
};
}, []);
}; };

View File

@@ -0,0 +1 @@
//

View File

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