UNPKG

@e3i3/klicklab-esm-sdk

Version:

SDK for klicklab service user_엔드포인트 변경

645 lines (585 loc) 19.1 kB
// klicklab-sdk.esm.js - ESM 방식으로 변환된 KlickLab SDK const API_BASE_URL = "http://klicklab-nlb-0f6efee8fd967688.elb.ap-northeast-2.amazonaws.com"; const COLLECT_URL = "http://klicklab-nlb-0f6efee8fd967688.elb.ap-northeast-2.amazonaws.com/api/analytics/collect"; function generateUUID() { // UUID V4 규격 따름. 3블록 첫자리에 4, y 자리에 8-11 숫자중 하나. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); } function evaluateEventRule(rule, event) { const { condition_type, condition_parameter, condition_value } = rule; const eventParamValue = event.properties[condition_parameter]; if (typeof eventParamValue === "undefined" || eventParamValue === null) return false; switch (condition_type) { case "url_contains": return String(eventParamValue).includes(condition_value); case "url_equals": return String(eventParamValue) === condition_value; case "form_id_equals": return event.properties.form_id === condition_value; default: return false; } } const SDK = {}; SDK.getClientId = function () { let clientId = localStorage.getItem("analytics_client_id"); if (!clientId) { clientId = generateUUID(); localStorage.setItem("analytics_client_id", clientId); } return clientId; }; SDK.getSessionId = function () { let sessionId = sessionStorage.getItem("analytics_session_id"); if (!sessionId) { sessionId = "sess_" + new Date().toISOString() + "_" + generateUUID().split("-")[0]; sessionStorage.setItem("analytics_session_id", sessionId); // 세션이 새로 시작될 때 session_start 이벤트 자동 전송 setTimeout(() => { SDK.sendEvent("session_start", { session_id: sessionId, page_path: window.location.pathname, page_title: document.title, referrer: document.referrer, }); }, 0); } return sessionId; }; // 위치정보 수집 함수 추가 SDK.getLocationInfo = function () { return new Promise((resolve) => { // 브라우저 Geolocation API 사용 if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: position.coords.accuracy, source: "gps", }); }, (error) => { console.log("⚠️ GPS 위치정보 수집 실패:", error.message); // GPS 실패 시 IP 기반 위치정보로 fallback SDK.getIPLocationInfo().then(resolve); }, { timeout: 5000, maximumAge: 300000, // 5분간 캐시 enableHighAccuracy: false, } ); } else { // Geolocation API 미지원 시 IP 기반으로 fallback SDK.getIPLocationInfo().then(resolve); } }); }; // IP 기반 위치정보 수집 함수 SDK.getIPLocationInfo = function () { return fetch("https://ipapi.co/json/") .then((response) => response.json()) .then((data) => ({ country: data.country_code, city: data.city, region: data.region, timezone: data.timezone || "Asia/Seoul", ip: data.ip, latitude: data.latitude, longitude: data.longitude, source: "ip", })) .catch((error) => { console.log("⚠️ IP 위치정보 수집 실패:", error); return { country: null, city: null, region: null, timezone: "Asia/Seoul", ip: null, latitude: null, longitude: null, source: "none", }; }); }; SDK.getDeviceInfo = function () { const ua = navigator.userAgent; const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(ua); let os = "Unknown"; if (/Android/i.test(ua)) { os = "Android"; } else if (/iPhone|iPad|iPod/i.test(ua)) { os = "iOS"; } else if (/Macintosh|Mac OS X/i.test(ua)) { os = "macOS"; } else if (/Windows/i.test(ua)) { os = "Windows"; } const device_type = isMobile ? "mobile" : os === "macOS" || os === "Windows" ? "desktop" : "unknown"; const browser = /Chrome/.test(ua) ? "Chrome" : /Safari/.test(ua) ? "Safari" : /Firefox/.test(ua) ? "Firefox" : "Other"; return { device_type, os, browser, language: navigator.language, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, }; }; SDK.parseTrafficSource = function () { const params = new URLSearchParams(window.location.search); return { traffic_medium: params.get("utm_medium") || "direct", traffic_source: params.get("utm_source") || document.referrer || "direct", traffic_campaign: params.get("utm_campaign"), }; }; SDK.getUserId = function () { if (window.__REDUX_STORE__) { const state = window.__REDUX_STORE__.getState(); if (state.auth && state.auth.id) { return state.auth.id; } } const userData = localStorage.getItem("user_data"); if (userData) { try { const user = JSON.parse(userData); return user.id || null; } catch (e) { return null; } } return null; }; SDK.getUserGender = function () { const userData = localStorage.getItem("user_data"); if (userData) { try { const user = JSON.parse(userData); return user.gender || null; } catch (e) { return null; } } return null; }; SDK.getUserAge = function () { const userData = localStorage.getItem("user_data"); if (userData) { try { const user = JSON.parse(userData); return user.age || null; } catch (e) { return null; } } return null; }; SDK.getElementPath = function (element) { if (!element) return null; const path = []; let current = element; while (current && current !== document.body) { let selector = current.tagName.toLowerCase(); if (current.id) { selector += "#" + current.id; } else if (current.className) { const classes = current.className.split(" ").filter((c) => c.trim()); if (classes.length > 0) selector += "." + classes.join("."); } const siblings = Array.from(current.parentNode.children).filter( (child) => child.tagName === current.tagName ); if (siblings.length > 1) { const index = siblings.indexOf(current) + 1; selector += `:nth-child(${index})`; } path.unshift(selector); current = current.parentNode; } return path.join(" > "); }; // --- 이벤트 전송 로직 --- // 위치정보 캐시 변수 let cachedLocationInfo = null; let locationInfoPromise = null; // 1. 서버로 실제 전송을 담당하는 내부 함수 const _sendToServer = async function ( eventName, properties = {}, element = null, userId = null, userGender = null, userAge = null ) { if (!window.KLICKLAB_SDK_KEY) { return console.warn("KlickLab SDK 키가 설정되지 않았습니다."); } const deviceInfo = SDK.getDeviceInfo(); const trafficInfo = SDK.parseTrafficSource(); const finalUserId = userId !== null ? userId : SDK.getUserId(); const finalUserGender = userGender !== null ? userGender : SDK.getUserGender(); const finalUserAge = userAge !== null ? userAge : SDK.getUserAge(); const elementInfo = element ? { element_path: SDK.getElementPath(element), element_tag: element.tagName, element_id: element.id, element_class: element.className, } : {}; // 위치정보 수집 (캐시된 정보가 있으면 사용, 없으면 새로 수집) let locationInfo = cachedLocationInfo; if (!locationInfo) { if (!locationInfoPromise) { locationInfoPromise = SDK.getLocationInfo(); } try { locationInfo = await locationInfoPromise; cachedLocationInfo = locationInfo; // 5분 후 캐시 무효화 setTimeout(() => { cachedLocationInfo = null; locationInfoPromise = null; }, 300000); } catch (error) { console.log("⚠️ 위치정보 수집 실패:", error); locationInfo = { country: null, city: null, region: null, timezone: "Asia/Seoul", latitude: null, longitude: null, source: "none", }; } } const eventData = { sdk_key: window.KLICKLAB_SDK_KEY, event_name: eventName, timestamp: new Date().toLocaleString("sv-SE", { timeZone: "Asia/Seoul" }), client_id: SDK.getClientId(), user_id: finalUserId, session_id: SDK.getSessionId(), user_gender: finalUserGender, user_age: finalUserAge, device_type: deviceInfo.device_type, traffic_medium: trafficInfo.traffic_medium, traffic_source: trafficInfo.traffic_source, traffic_campaign: trafficInfo.traffic_campaign, // 위치정보 추가 country: locationInfo.country, city: locationInfo.city, region: locationInfo.region, latitude: locationInfo.latitude, longitude: locationInfo.longitude, location_source: locationInfo.source, properties: { page_path: window.location.pathname + // "/demo.html" window.location.search + // "?page=product" window.location.hash, // "#/order/success" page_title: document.title, referrer: document.referrer, ...properties, }, context: { geo: { country: locationInfo.country, city: locationInfo.city, region: locationInfo.region, latitude: locationInfo.latitude, longitude: locationInfo.longitude, timezone: locationInfo.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, source: locationInfo.source, }, device: deviceInfo, traffic_source: trafficInfo, user_agent: navigator.userAgent, screen_resolution: `${screen.width}x${screen.height}`, viewport_size: `${window.innerWidth}x${window.innerHeight}`, utm_params: Object.fromEntries( new URLSearchParams(window.location.search) ), }, ...elementInfo, }; console.log("🌍 위치정보 포함 이벤트 전송:", eventData); fetch(COLLECT_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(eventData), keepalive: true, }).catch((err) => console.error(`Event sending failed:`, err)); }; // 2. 규칙을 적용하고 전송을 결정하는 공개 게이트웨이 함수 SDK.sendEvent = async function ( eventName, properties = {}, userId = null, userGender = null, userAge = null, element = null ) { await _sendToServer( eventName, properties, element, userId, userGender, userAge ); if (eventName === "page_view" || eventName === "form_submit") { const eventRules = window.KLICKLAB_EVENT_RULES || []; for (const rule of eventRules) { if ( rule.source_event_name === eventName && evaluateEventRule(rule, { properties }) ) { const transformedEventName = rule.new_event_name; // 추가 필드 없이 그대로 전송 await _sendToServer( transformedEventName, properties, element, userId, userGender, userAge ); break; } } } }; // --- 자동 추적 및 초기화 (이벤트 위임 적용) --- // 3. 페이지의 모든 클릭을 감지하고 규칙과 일치하는지 확인하는 '통합 리스너' async function masterClickListener(event) { // 클릭된 대상이 Element 가 아니라면 부모 Element 로 보정 let clickedElement = event.target; if (!(clickedElement instanceof Element)) { clickedElement = clickedElement.parentElement; } if (!clickedElement) return; // kl-disabled 클래스가 붙은 요소는 수집하지 않음 if (clickedElement.closest(".kl-disabled")) { return; } // 더 풍부한 클릭 정보 수집 await _sendToServer( "auto_click", { click_x: event.clientX, click_y: event.clientY, page_x: event.pageX, page_y: event.pageY, target_text: clickedElement.textContent?.trim(), target_tag: clickedElement.tagName, target_class: clickedElement.className, target_id: clickedElement.id, target_href: clickedElement.href || null, is_button: clickedElement.tagName === "BUTTON", is_link: clickedElement.tagName === "A", }, clickedElement ); console.log("🔍 buttonRules loaded:", window.KLICKLAB_BUTTON_EVENT_RULES); // 버튼 클릭 규칙 확인 const buttonRules = window.KLICKLAB_BUTTON_EVENT_RULES || []; for (const rule of buttonRules) { console.log( "… testing selector", rule.css_selector, "against element", clickedElement ); const matchedElement = clickedElement.closest(rule.css_selector); console.log("… closest returned:", matchedElement); if (matchedElement) { console.log("✅ selector matched! sending event:", rule.event_name); await _sendToServer( rule.event_name, { element_text: matchedElement.textContent?.trim() || null, }, matchedElement ); break; // 규칙 찾으면 종료 } } } const SENSITIVE_FIELDS = new Set([ "password", "confirmPw", "currentPw", "newPw", "creditCard", "cvv", "ssn", // 추가 민감 필드 ]); SDK.autoTrackForms = function () { document.addEventListener("submit", function (e) { const form = e.target; // kl-disabled 클래스가 붙어 있으면 수집 스킵 if (form instanceof Element && form.closest(".kl-disabled")) { return; } const formData = new FormData(form); const formFields = {}; for (let [key, value] of formData.entries()) { if (!SENSITIVE_FIELDS.has(key.toLowerCase())) { formFields[key] = value; } } SDK.sendEvent( "form_submit", { form_action: form.action, form_method: form.method, form_id: form.id, form_class: form.className, form_fields: formFields, form_field_count: Object.keys(formFields).length, }, null, form ); }); }; SDK.init = async function () { // 1. 이미 설정된 키가 있으면 우선 사용 let sdkKey = window.KLICKLAB_SDK_KEY; // 2. 설정된 키가 없으면 script 태그에서 찾기 if (!sdkKey) { const scriptTag = document.currentScript || document.querySelector("script[data-sdk-key]"); sdkKey = scriptTag ? scriptTag.getAttribute("data-sdk-key") : null; } // 3. 여전히 키가 없으면 경고 if (!sdkKey) { return console.warn("KlickLab SDK 키가 설정되지 않았습니다."); } // 4. 키 설정 window.KLICKLAB_SDK_KEY = sdkKey; // 기본값으로 빈 배열 설정 window.KLICKLAB_EVENT_RULES = []; window.KLICKLAB_BUTTON_EVENT_RULES = []; // 규칙 로드 시도 (실패해도 SDK는 정상 동작) try { const response = await fetch( `${API_BASE_URL}/api/sdk/rules?sdk_key=${sdkKey}` ); if (response.ok) { const data = await response.json(); window.KLICKLAB_EVENT_RULES = data.eventRules || []; window.KLICKLAB_BUTTON_EVENT_RULES = data.eventbutton || []; console.log("✅ SDK 규칙 로드 성공:", { eventRules: window.KLICKLAB_EVENT_RULES.length, buttonEventRules: window.KLICKLAB_BUTTON_EVENT_RULES.length, }); } else { console.log("⚠️ 규칙 서버 응답 오류, 기본 추적만 사용"); } } catch (error) { console.log("⚠️ 규칙 로드 실패, 기본 추적만 사용:", error.message); } // '이벤트 위임'을 위한 단일 클릭 리스너를 document에 부착합니다. document.removeEventListener("click", masterClickListener); // 중복 부착 방지 document.addEventListener("click", masterClickListener); // 나머지 자동 추적 기능을 활성화합니다. SDK.autoTrackForms(); SDK.trackTimeOnPage(); SDK.trackScrollDepth(); trackPageView(); console.log("✅ KlickLab SDK 초기화 완료 - SDK Key:", sdkKey); }; SDK.trackTimeOnPage = function () { const start = Date.now(); window.addEventListener("beforeunload", () => { const seconds = Math.round((Date.now() - start) / 1000); SDK.sendEvent("page_exit", { time_on_page_seconds: seconds, page_path: window.location.pathname, }); }); }; SDK.trackScrollDepth = function () { let maxScroll = 0; const steps = [25, 50, 75, 90, 100]; const sent = new Set(); let scrollTimer; window.addEventListener("scroll", () => { clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { const scrollTop = window.scrollY || document.documentElement.scrollTop; const scrollHeight = document.documentElement.scrollHeight - window.innerHeight; const percent = Math.round((scrollTop / scrollHeight) * 100); if (percent > maxScroll) maxScroll = percent; steps.forEach((threshold) => { if (percent >= threshold && !sent.has(threshold)) { sent.add(threshold); SDK.sendEvent("scroll_depth", { scroll_percentage: threshold, max_scroll_percentage: maxScroll, page_path: window.location.pathname, }); } }); }, 150); // 150ms 디바운스 }); }; // GA 방식: page_view 이벤트만 집계, fromPage 개념 제거 async function trackPageView() { await SDK.sendEvent("page_view", { page_path: window.location.pathname + window.location.search + window.location.hash, page_title: document.title, referrer: document.referrer, }); } // 브라우저 환경에서 자동 초기화 if (typeof window !== "undefined") { document.addEventListener("DOMContentLoaded", () => { SDK.init(); }); // SPA 라우팅 지원: pushState, replaceState, popstate 감지 const _pushState = history.pushState; const _replaceState = history.replaceState; history.pushState = function () { _pushState.apply(this, arguments); setTimeout(trackPageView, 0); }; history.replaceState = function () { _replaceState.apply(this, arguments); setTimeout(trackPageView, 0); }; window.addEventListener("popstate", () => { setTimeout(trackPageView, 0); }); } export default SDK;