mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
migrated api of ingestion
This commit is contained in:
File diff suppressed because it is too large
Load Diff
33
web/src/app/api/ingest/route.ts
Normal file
33
web/src/app/api/ingest/route.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { sendEvent } from '@/lib/kafka';
|
||||||
|
import type { EventBase } from '@/lib/events';
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
|
||||||
|
const storeMode = process.env.STORE_MODE || 'hotel';
|
||||||
|
const userAgent = req.headers.get('user-agent') || undefined;
|
||||||
|
|
||||||
|
const event: EventBase = {
|
||||||
|
...body,
|
||||||
|
storeMode,
|
||||||
|
userAgent,
|
||||||
|
ts: body.ts || new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendEvent(event);
|
||||||
|
|
||||||
|
if (process.env.NEXT_PUBLIC_APP_ENV === 'dev') {
|
||||||
|
console.log('[ingest]', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[ingest error]', err);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: err.message || 'unknown error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,3 +4,4 @@ export { default as Input } from './Input';
|
|||||||
export { default as DateInput } from './DateInput';
|
export { default as DateInput } from './DateInput';
|
||||||
export { default as RadioGroup } from './RadioGroup';
|
export { default as RadioGroup } from './RadioGroup';
|
||||||
export { default as Dropdown, DropdownCounter } from './Dropdown';
|
export { default as Dropdown, DropdownCounter } from './Dropdown';
|
||||||
|
export { default as Navigation } from './Navigation';
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import '@/lib/experiments' // ensure experiments lib is loaded
|
import '@/lib/experiments' // ensure experiments lib is loaded
|
||||||
|
import type { EventName } from '@/lib/events';
|
||||||
|
|
||||||
const fetchSessionId = async (): Promise<string> => {
|
const fetchSessionId = async (): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/session');
|
const res = await fetch('/api/session');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.sessionId || '';
|
return data.sessionId || '';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('failed to fetch session:', err);
|
console.error('failed to fetch session:', err);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const track = async (ev: {
|
const track = async (ev: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
eventType: string;
|
eventName: EventName;
|
||||||
targetEl?: string;
|
page: string;
|
||||||
targetUrl?: string;
|
productId?: string;
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, unknown>;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/track', {
|
await fetch('/api/ingest', {
|
||||||
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 = () => {
|
||||||
@@ -44,82 +45,61 @@ export const useInteractionTracking = () => {
|
|||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
if (!sidRef.current) return;
|
if (!sidRef.current) return;
|
||||||
const tgt = e.target as HTMLElement;
|
const tgt = e.target as HTMLElement;
|
||||||
|
const page = window.location.pathname;
|
||||||
track({
|
track({
|
||||||
sessionId: sidRef.current,
|
sessionId: sidRef.current,
|
||||||
eventType: 'click',
|
eventName: 'click',
|
||||||
targetEl: tgt.tagName,
|
page,
|
||||||
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,
|
targetEl: tgt.tagName,
|
||||||
},
|
targetUrl: tgt instanceof HTMLAnchorElement ? tgt.href : undefined,
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!sidRef.current) return;
|
|
||||||
track({
|
|
||||||
sessionId: sidRef.current,
|
|
||||||
eventType: 'scroll',
|
|
||||||
metadata: {
|
|
||||||
scrollY: window.scrollY,
|
|
||||||
path: window.location.pathname,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageView = () => {
|
const handlePageView = () => {
|
||||||
if (!sidRef.current) return;
|
if (!sidRef.current) return;
|
||||||
|
const page = window.location.pathname;
|
||||||
track({
|
track({
|
||||||
sessionId: sidRef.current,
|
sessionId: sidRef.current,
|
||||||
eventType: 'pageview',
|
eventName: 'page_view',
|
||||||
|
page,
|
||||||
metadata: {
|
metadata: {
|
||||||
path: window.location.pathname,
|
|
||||||
referrer: document.referrer,
|
referrer: document.referrer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
enum DefinedInteractions {
|
// called for canonical events dispatched via custom events
|
||||||
ADD_TO_CART = 'add_to_cart',
|
const handleDefinedInteraction = (e: Event) => {
|
||||||
PURCHASE = 'purchase',
|
|
||||||
}
|
|
||||||
|
|
||||||
// called when clicking on "Add to Cart" button or "Purchase" button
|
|
||||||
const handleDefinedInteraction = (
|
|
||||||
interactionType: DefinedInteractions,
|
|
||||||
metadata?: Record<string, any>
|
|
||||||
) => {
|
|
||||||
if (!sidRef.current) return;
|
if (!sidRef.current) return;
|
||||||
|
const customEvent = e as CustomEvent<{
|
||||||
|
eventName: EventName;
|
||||||
|
productId?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}>;
|
||||||
|
const page = window.location.pathname;
|
||||||
track({
|
track({
|
||||||
sessionId: sidRef.current,
|
sessionId: sidRef.current,
|
||||||
eventType: interactionType,
|
eventName: customEvent.detail.eventName,
|
||||||
metadata: {
|
page,
|
||||||
path: window.location.pathname,
|
productId: customEvent.detail.productId,
|
||||||
...metadata,
|
metadata: customEvent.detail.metadata,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const definedInteractionListener = (e: Event) => {
|
|
||||||
const customEvent = e as CustomEvent;
|
|
||||||
handleDefinedInteraction(customEvent.detail.interactionType, customEvent.detail.metadata);
|
|
||||||
};
|
|
||||||
|
|
||||||
// wait for session to be ready before tracking
|
// wait for session to be ready before tracking
|
||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
|
|
||||||
handlePageView();
|
handlePageView();
|
||||||
document.addEventListener('click', handleClick);
|
document.addEventListener('click', handleClick);
|
||||||
document.addEventListener('definedInteraction', definedInteractionListener);
|
document.addEventListener('definedInteraction', handleDefinedInteraction);
|
||||||
// TOO NOISY: enable if needed but tbh not worth it
|
|
||||||
//window.addEventListener('scroll', handleScroll, { passive: true });
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('click', handleClick);
|
document.removeEventListener('click', handleClick);
|
||||||
document.removeEventListener('definedInteraction', definedInteractionListener);
|
document.removeEventListener('definedInteraction', handleDefinedInteraction);
|
||||||
//window.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
};
|
||||||
}, [ready]);
|
}, [ready]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,42 +1,35 @@
|
|||||||
import { Kafka, Producer } from 'kafkajs';
|
import { Kafka, Producer } from 'kafkajs';
|
||||||
|
import type { EventBase } from './events';
|
||||||
|
|
||||||
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 sendEvent = async (ev: EventBase) => {
|
||||||
sessionId: string;
|
const p = await getProducer();
|
||||||
eventType: string;
|
await p.send({
|
||||||
targetEl?: string;
|
topic: 'user-interactions',
|
||||||
targetUrl?: string;
|
messages: [{
|
||||||
metadata?: Record<string, any>;
|
key: ev.sessionId,
|
||||||
ts: number;
|
value: JSON.stringify(ev),
|
||||||
}) => {
|
}],
|
||||||
const p = await getProducer();
|
});
|
||||||
// add to the metadata
|
|
||||||
await p.send({
|
|
||||||
topic: 'user-interactions',
|
|
||||||
messages: [{
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ export function middleware(req: NextRequest) {
|
|||||||
const mode = process.env.STORE_MODE;
|
const mode = process.env.STORE_MODE;
|
||||||
const { pathname } = req.nextUrl;
|
const { pathname } = req.nextUrl;
|
||||||
|
|
||||||
// skip rewrites for api routes, static files, and next internals
|
// skip rewrites for api routes, admin routes, static files, and next internals
|
||||||
if (
|
if (
|
||||||
pathname.startsWith('/api') ||
|
pathname.startsWith('/api') ||
|
||||||
|
pathname.startsWith('/admin') ||
|
||||||
pathname.startsWith('/_next') ||
|
pathname.startsWith('/_next') ||
|
||||||
pathname.startsWith('/static') ||
|
pathname.startsWith('/static') ||
|
||||||
pathname.includes('.')
|
pathname.includes('.')
|
||||||
|
|||||||
Reference in New Issue
Block a user