279 lines
8.9 KiB
TypeScript
279 lines
8.9 KiB
TypeScript
import { IUpdateSessionRequest } from "@/entities/session/types";
|
|
import { useCallback, useMemo, useState, useEffect } from "react";
|
|
import { setSessionIdToCookie } from "@/entities/session/serverActions";
|
|
import { metricService } from "@/services/analytics/metricService";
|
|
import {
|
|
SessionCollector,
|
|
FingerprintCollector,
|
|
FacebookCollector,
|
|
parseUtmParams,
|
|
getOrCreateAnonymousId,
|
|
type CreateSessionResponse,
|
|
type UpdateSessionData,
|
|
type SessionFingerprintData,
|
|
type SessionFacebookData,
|
|
} from "@wit-lab-llc/frontend-shared";
|
|
import { parseQueryParams } from "@/shared/utils/url";
|
|
|
|
// TODO: Get locale from context or i18n
|
|
const locale = "en";
|
|
|
|
// Debug logging helper - only logs in development
|
|
const isDev = process.env.NODE_ENV === "development";
|
|
const debugLog = (message: string, ...args: unknown[]) => {
|
|
if (isDev) {
|
|
console.log(`🔍 [useSession] ${message}`, ...args);
|
|
}
|
|
};
|
|
|
|
// Singleton instances
|
|
let sessionCollectorInstance: SessionCollector | null = null;
|
|
let fingerprintCollectorInstance: FingerprintCollector | null = null;
|
|
let facebookCollectorInstance: FacebookCollector | null = null;
|
|
|
|
// Module-level flag to track if data collection has been done (persists across remounts)
|
|
let dataCollectionDone = false;
|
|
|
|
function getSessionCollector(): SessionCollector {
|
|
if (!sessionCollectorInstance) {
|
|
sessionCollectorInstance = new SessionCollector();
|
|
}
|
|
return sessionCollectorInstance;
|
|
}
|
|
|
|
function getFingerprintCollector(): FingerprintCollector {
|
|
if (!fingerprintCollectorInstance) {
|
|
fingerprintCollectorInstance = new FingerprintCollector();
|
|
}
|
|
return fingerprintCollectorInstance;
|
|
}
|
|
|
|
function getFacebookCollector(): FacebookCollector {
|
|
if (!facebookCollectorInstance) {
|
|
facebookCollectorInstance = new FacebookCollector();
|
|
}
|
|
return facebookCollectorInstance;
|
|
}
|
|
|
|
interface IUseSessionProps {
|
|
funnelId: string;
|
|
googleAnalyticsId?: string;
|
|
}
|
|
|
|
export const useSession = ({ funnelId, googleAnalyticsId }: IUseSessionProps) => {
|
|
const [collector] = useState(() => getSessionCollector());
|
|
const [fingerprintCollector] = useState(() => getFingerprintCollector());
|
|
const [facebookCollector] = useState(() => getFacebookCollector());
|
|
const [isError, setIsError] = useState(false);
|
|
|
|
// Get current session ID from collector
|
|
const sessionId = collector.getSessionId();
|
|
|
|
// Collect fingerprint and facebook data on mount (only once per app lifecycle)
|
|
useEffect(() => {
|
|
if (typeof window === "undefined" || dataCollectionDone) return;
|
|
dataCollectionDone = true;
|
|
|
|
// Collect data in background
|
|
debugLog("Starting fingerprint and Facebook data collection...");
|
|
Promise.all([
|
|
fingerprintCollector.collect().catch((e) => {
|
|
debugLog("Fingerprint collection failed:", e);
|
|
return null;
|
|
}),
|
|
Promise.resolve(facebookCollector.collect()),
|
|
]).then(([fpResult, fbResult]) => {
|
|
debugLog("✅ Data collection completed", {
|
|
fingerprint: fpResult ? { visitorId: fpResult.visitorId, confidence: fpResult.confidence } : null,
|
|
facebook: fbResult ? { fbp: fbResult.fbp, fbc: fbResult.fbc, fbclid: fbResult.fbclid } : null,
|
|
});
|
|
});
|
|
}, [fingerprintCollector, facebookCollector]);
|
|
|
|
const createSession = useCallback(async (): Promise<CreateSessionResponse> => {
|
|
if (typeof window === "undefined") {
|
|
return { sessionId: "", status: "error" };
|
|
}
|
|
|
|
try {
|
|
const query = parseQueryParams();
|
|
const utm = parseUtmParams();
|
|
const anonymousId = getOrCreateAnonymousId();
|
|
const lastActivityAt = new Date().toISOString();
|
|
|
|
// Get fingerprint data for session
|
|
let fingerprint: SessionFingerprintData | undefined;
|
|
try {
|
|
debugLog("Getting fingerprint data for session...");
|
|
const fpData = await fingerprintCollector.getOrCollect();
|
|
if (fpData) {
|
|
const payload = fingerprintCollector.toServerPayload();
|
|
fingerprint = {
|
|
visitorId: fpData.visitorId,
|
|
confidence: fpData.confidence,
|
|
collectedAt: fpData.collectedAt,
|
|
...payload,
|
|
} as SessionFingerprintData;
|
|
debugLog("✅ Fingerprint data ready", {
|
|
visitorId: fingerprint.visitorId,
|
|
confidence: fingerprint.confidence,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn("[useSession] Failed to collect fingerprint:", e);
|
|
}
|
|
|
|
// Get facebook data for session
|
|
let facebookData: SessionFacebookData | undefined;
|
|
try {
|
|
debugLog("Getting Facebook data for session...");
|
|
const fbData = facebookCollector.getData() || facebookCollector.collect();
|
|
if (fbData) {
|
|
facebookData = {
|
|
fbp: fbData.fbp ?? undefined,
|
|
fbc: fbData.fbc ?? undefined,
|
|
fbclid: fbData.fbclid ?? undefined,
|
|
externalId: fingerprint?.visitorId, // Use visitorId as external_id
|
|
landingPage: fbData.landingPage ?? undefined,
|
|
referrer: fbData.referrer,
|
|
clientUserAgent: fbData.userAgent,
|
|
eventSourceUrl: fbData.currentUrl,
|
|
browserLanguage: fbData.browserLanguage,
|
|
screenResolution: fbData.screenResolution,
|
|
viewportSize: fbData.viewportSize,
|
|
colorDepth: fbData.colorDepth,
|
|
devicePixelRatio: fbData.devicePixelRatio,
|
|
touchSupport: fbData.touchSupport,
|
|
cookiesEnabled: fbData.cookiesEnabled,
|
|
doNotTrack: fbData.doNotTrack,
|
|
collectedAt: fbData.collectedAt,
|
|
};
|
|
debugLog("✅ Facebook data ready", {
|
|
fbp: facebookData.fbp,
|
|
fbc: facebookData.fbc,
|
|
fbclid: facebookData.fbclid,
|
|
externalId: facebookData.externalId,
|
|
landingPage: facebookData.landingPage,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn("[useSession] Failed to collect Facebook data:", e);
|
|
}
|
|
|
|
debugLog("Creating session with data:", {
|
|
source: funnelId,
|
|
hasFingerprint: !!fingerprint,
|
|
hasFacebookData: !!facebookData,
|
|
utm,
|
|
});
|
|
|
|
const response = await collector.create({
|
|
source: funnelId,
|
|
feature: "stripe",
|
|
locale,
|
|
sign: false,
|
|
utm,
|
|
anonymousId,
|
|
query,
|
|
landingQuery: query,
|
|
lastActivityAt,
|
|
fingerprint,
|
|
facebookData,
|
|
});
|
|
|
|
debugLog("Session creation response:", response);
|
|
|
|
if (response.sessionId && response.status !== "error") {
|
|
// Set cookie for server-side access
|
|
await setSessionIdToCookie("activeSessionId", response.sessionId);
|
|
|
|
// For old sessions, update with current feature and data
|
|
if (response.status === "old") {
|
|
debugLog("Updating old session with current feature and data...");
|
|
await collector.update({
|
|
feature: "stripe",
|
|
anonymousId,
|
|
query,
|
|
lastActivityAt: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
// Send analytics only for new sessions
|
|
if (response.status === "success") {
|
|
metricService.userParams({
|
|
sessionId: response.sessionId,
|
|
});
|
|
|
|
metricService.sendVisitContext({
|
|
sessionId: response.sessionId,
|
|
funnelId,
|
|
gaId: googleAnalyticsId,
|
|
});
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
setIsError(true);
|
|
return { status: "error", sessionId: "" };
|
|
} catch (error) {
|
|
debugLog("❌ Session creation failed:", error);
|
|
console.error("Session creation failed:", error);
|
|
setIsError(true);
|
|
return { status: "error", sessionId: "" };
|
|
}
|
|
}, [collector, funnelId, googleAnalyticsId, fingerprintCollector, facebookCollector]);
|
|
|
|
const updateSession = useCallback(
|
|
async (data: IUpdateSessionRequest["data"]) => {
|
|
try {
|
|
const query = parseQueryParams();
|
|
const anonymousId = getOrCreateAnonymousId();
|
|
|
|
// Convert to library's UpdateSessionData format
|
|
const updateData: UpdateSessionData = {
|
|
feature: "stripe",
|
|
anonymousId,
|
|
query,
|
|
lastActivityAt: new Date().toISOString(),
|
|
...data,
|
|
};
|
|
|
|
const result = await collector.update(updateData, {
|
|
source: funnelId,
|
|
feature: "stripe",
|
|
locale,
|
|
sign: false,
|
|
utm: parseUtmParams(),
|
|
anonymousId,
|
|
query,
|
|
landingQuery: query,
|
|
lastActivityAt: new Date().toISOString(),
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
debugLog("❌ Session update failed:", error);
|
|
console.error("Session update failed:", error);
|
|
return null;
|
|
}
|
|
},
|
|
[collector, funnelId]
|
|
);
|
|
|
|
const deleteSession = useCallback(() => {
|
|
collector.delete(funnelId);
|
|
}, [collector, funnelId]);
|
|
|
|
return useMemo(
|
|
() => ({
|
|
session: sessionId,
|
|
isError,
|
|
createSession,
|
|
updateSession,
|
|
deleteSession,
|
|
}),
|
|
[sessionId, isError, createSession, deleteSession, updateSession]
|
|
);
|
|
};
|