mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 08:33:36 +00:00
better session cookie management
This commit is contained in:
40
web/src/app/api/session/route.ts
Normal file
40
web/src/app/api/session/route.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return NextResponse.json({ sessionId: existingSession });
|
||||||
|
}
|
||||||
|
|
||||||
|
// mint new session id
|
||||||
|
const sessionId = randomUUID();
|
||||||
|
|
||||||
|
const res = NextResponse.json({ sessionId });
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import '@/lib/experiments' // ensure experiments lib is loaded
|
import '@/lib/experiments' // ensure experiments lib is loaded
|
||||||
|
|
||||||
const genSessionId = () => {
|
const fetchSessionId = async (): Promise<string> => {
|
||||||
if (typeof window === 'undefined') return '';
|
try {
|
||||||
let sid = sessionStorage.getItem('phantom_session_id');
|
const res = await fetch('/api/session');
|
||||||
if (!sid) {
|
const data = await res.json();
|
||||||
sid = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
return data.sessionId || '';
|
||||||
sessionStorage.setItem('phantom_session_id', sid);
|
} catch (err) {
|
||||||
// TODO: when creating new id send to exepriemtn tracking db
|
console.error('failed to fetch session:', err);
|
||||||
// match between sesion-id and experiment-id for this session
|
return '';
|
||||||
// so that we can identify all interactions aligning with a specific experiment goal.
|
|
||||||
}
|
}
|
||||||
return sid;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const track = async (ev: {
|
const track = async (ev: {
|
||||||
@@ -34,11 +32,17 @@ const track = async (ev: {
|
|||||||
|
|
||||||
export const useInteractionTracking = () => {
|
export const useInteractionTracking = () => {
|
||||||
const sidRef = useRef<string>('');
|
const sidRef = useRef<string>('');
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sidRef.current = genSessionId();
|
// fetch session id from httpOnly cookie via API
|
||||||
|
fetchSessionId().then((sid) => {
|
||||||
|
sidRef.current = sid;
|
||||||
|
setReady(true);
|
||||||
|
});
|
||||||
|
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (!sidRef.current) return;
|
||||||
const tgt = e.target as HTMLElement;
|
const tgt = e.target as HTMLElement;
|
||||||
track({
|
track({
|
||||||
sessionId: sidRef.current,
|
sessionId: sidRef.current,
|
||||||
@@ -54,6 +58,7 @@ export const useInteractionTracking = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
if (!sidRef.current) return;
|
||||||
track({
|
track({
|
||||||
sessionId: sidRef.current,
|
sessionId: sidRef.current,
|
||||||
eventType: 'scroll',
|
eventType: 'scroll',
|
||||||
@@ -65,6 +70,7 @@ export const useInteractionTracking = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePageView = () => {
|
const handlePageView = () => {
|
||||||
|
if (!sidRef.current) return;
|
||||||
track({
|
track({
|
||||||
sessionId: sidRef.current,
|
sessionId: sidRef.current,
|
||||||
eventType: 'pageview',
|
eventType: 'pageview',
|
||||||
@@ -85,6 +91,7 @@ export const useInteractionTracking = () => {
|
|||||||
interactionType: DefinedInteractions,
|
interactionType: DefinedInteractions,
|
||||||
metadata?: Record<string, any>
|
metadata?: Record<string, any>
|
||||||
) => {
|
) => {
|
||||||
|
if (!sidRef.current) return;
|
||||||
track({
|
track({
|
||||||
sessionId: sidRef.current,
|
sessionId: sidRef.current,
|
||||||
eventType: interactionType,
|
eventType: interactionType,
|
||||||
@@ -95,23 +102,24 @@ export const useInteractionTracking = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const definedInteractionListener = (e: Event) => {
|
||||||
|
const customEvent = e as CustomEvent;
|
||||||
|
handleDefinedInteraction(customEvent.detail.interactionType, customEvent.detail.metadata);
|
||||||
|
};
|
||||||
|
|
||||||
|
// wait for session to be ready before tracking
|
||||||
|
if (!ready) return;
|
||||||
|
|
||||||
handlePageView();
|
handlePageView();
|
||||||
document.addEventListener('click', handleClick);
|
document.addEventListener('click', handleClick);
|
||||||
document.addEventListener('definedInteraction', (e: Event) => {
|
document.addEventListener('definedInteraction', definedInteractionListener);
|
||||||
const customEvent = e as CustomEvent;
|
|
||||||
handleDefinedInteraction(customEvent.detail.interactionType, customEvent.detail.metadata);
|
|
||||||
});
|
|
||||||
// TOO NOISY: enable if needed but tbh not worth it
|
// TOO NOISY: enable if needed but tbh not worth it
|
||||||
//window.addEventListener('scroll', handleScroll, { passive: true });
|
//window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('click', handleClick);
|
document.removeEventListener('click', handleClick);
|
||||||
document.removeEventListener('definedInteraction', (e: Event) => {
|
document.removeEventListener('definedInteraction', definedInteractionListener);
|
||||||
const customEvent = e as CustomEvent;
|
|
||||||
handleDefinedInteraction(customEvent.detail.interactionType, customEvent.detail.metadata);
|
|
||||||
});
|
|
||||||
//window.removeEventListener('scroll', handleScroll);
|
//window.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [ready]);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user