w-funnel/src/hooks/session/useSession.ts
dev.daminik00 a75ac7e8c8 session
2025-12-24 02:06:11 +03:00

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