2 nextjs scaffold with store mode shop and admin session experiment wiring event emission v1 (#17)

* chore: cleaning gitignore

* formating and env documentation

* feat: context switching of hotel/airline depndent on env var via middleware

* fixed alignment and building

* wrong file

* prods

* fixed applying style

* better session cookie management

* tentative session storage with maybe using airtable

* migrated api of ingestion

* events and products apge

* fixing build

* 13 create outline for research paper draft (#18)

* updated outline for paper from issue

* extra paper sections and some formalization of series data

* algorithms and acknowledgements

* updated outline for paper from issue

* upadted text formating

* event unification

* refactor tracking to ues callbacks instead of refs

* implement a pricing display api with session passing

* moved middleware to proxy according to new changes in Nextjs

* refactoed kafka ingestion to go via backend not web-db

* Refactor docker-compose services to use individual Dockerfiles (#20)

* Initial plan

* Refactor services into individual Dockerfiles

Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

* Add EXPOSE directives to all Dockerfiles with port documentation

Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

* fixing small bugs and adding exepriments to tracking

* added some doc
This commit is contained in:
Daniel Alves Rösel
2025-11-13 18:07:27 +01:00
committed by GitHub
parent 7ece6e82cb
commit 37b2099ee0
50 changed files with 2865 additions and 446 deletions

View File

@@ -0,0 +1,15 @@
import { NextResponse } from 'next/server';
import { getAllExperiments } from '@/lib/sessionStore';
export async function GET() {
try {
const exps = getAllExperiments();
return NextResponse.json({ experiments: exps });
} catch (err: any) {
console.error('experiments list error:', err);
return NextResponse.json(
{ error: err.message || 'unknown error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server';
import { randomUUID } from 'crypto';
import { createExperiment, getSession } from '@/lib/sessionStore';
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { sessionId } = body;
if (!sessionId) {
return NextResponse.json(
{ error: 'sessionId required' },
{ status: 400 }
);
}
// verify session exists
const session = getSession(sessionId);
if (!session) {
return NextResponse.json(
{ error: 'session not found' },
{ status: 404 }
);
}
// generate and create experiment
const experimentId = randomUUID();
const exp = createExperiment(sessionId, experimentId);
return NextResponse.json({
experimentId: exp.id,
sessionId,
status: exp.status,
createdAt: exp.createdAt,
});
} catch (err: any) {
console.error('experiment start error:', err);
return NextResponse.json(
{ error: err.message || 'unknown error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from 'next/server';
import { stopExperimentById, getExperiment } from '@/lib/sessionStore';
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { experimentId } = body;
if (!experimentId) {
return NextResponse.json(
{ error: 'experimentId required' },
{ status: 400 }
);
}
// verify experiment exists
const existing = getExperiment(experimentId);
if (!existing) {
return NextResponse.json(
{ error: 'experiment not found' },
{ status: 404 }
);
}
// stop the experiment
const exp = stopExperimentById(experimentId);
return NextResponse.json({
experimentId: exp!.id,
status: exp!.status,
});
} catch (err: any) {
console.error('experiment stop error:', err);
return NextResponse.json(
{ error: err.message || 'unknown error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import type { EventBase } from '@/lib/events';
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:5000';
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(),
};
const res = await fetch(`${BACKEND_URL}/api/kafka/ingest`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
if (!res.ok) {
throw new Error(`Backend returned ${res.status}`);
}
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 }
);
}
}

View File

@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
interface PricingResponse {
price: number;
currency: string;
cachedAt: string;
}
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const productId = searchParams.get('productId');
const sessionId = searchParams.get('sessionId');
const experimentId = searchParams.get('experimentId');
const storeMode = process.env.NEXT_PUBLIC_STORE_MODE || 'shop';
// log in dev
if (process.env.NODE_ENV === 'development') {
console.log('[pricing-api]', {
productId,
sessionId,
experimentId,
storeMode,
timestamp: new Date().toISOString(),
});
}
if (!productId) {
return NextResponse.json(
{ error: 'productId is required' },
{ status: 400 }
);
}
// stub: call external pricing provider (random for now)
const basePrice = 100 + Math.random() * 900; // 100-1000 range
const price = Math.round(basePrice * 100) / 100;
const response: PricingResponse = {
price,
currency: 'EUR',
cachedAt: new Date().toISOString(),
};
return NextResponse.json(response);
}

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { randomUUID } from 'crypto';
import { getSession, createSession } from '@/lib/sessionStore';
const COOKIE_NAME = 'phantom_session_id';
const isProd = process.env.NODE_ENV === 'production';
export async function GET(req: NextRequest) {
try {
// check for existing session cookie
const existingSession = req.cookies.get(COOKIE_NAME)?.value;
if (existingSession) {
const sessionData = getSession(existingSession);
return NextResponse.json({
sessionId: existingSession,
experimentId: sessionData?.experimentId,
});
}
// mint new session id
const sessionId = randomUUID();
createSession(sessionId);
const res = NextResponse.json({ sessionId, experimentId: undefined });
// set httpOnly cookie with security flags
res.cookies.set({
name: COOKIE_NAME,
value: sessionId,
httpOnly: true,
sameSite: 'lax',
secure: isProd,
path: '/',
maxAge: 60 * 60 * 24 * 30, // 30 days
});
return res;
} catch (err: any) {
console.error('session error:', err);
return NextResponse.json(
{ error: err.message || 'unknown error' },
{ status: 500 }
);
}
}

View File

@@ -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 }
);
}
}