PageSourceSearch

https://www.dholic.co.jp/tracking/page_tracker.js?t=20260925053242

js dholic.co.jp collected 2026-09-24 20:32:58 UTC 7,273 bytes, 235 lines download raw bytes

1/**
2 * 페이지 체류 시간 추적 스크립트 (최종 개선 버전)
3 * - 탭 닫기/이동 시 즉시 전송 (Debounce 제거)
4 * - BFCache 완벽 대응
5 * - navigator.sendBeacon 최적화
6 */
7
8var PageTracker = (function () {
9    var startTime = null;
10    var page_title = window.PAGE_TITLE || document.title || null;
11    var page_type = window.PAGE_TYPE || null;
12    var pageUrl = window.location.pathname + window.location.search;
13    var sessionId = getSessionId();
14
15    var heartbeatInterval = null;
16    var isActive = true;
17    var lastActivityTime = null;
18
19    var dataSent = false;   // 해당 세션(페이지 뷰)에서 최종 데이터 전송 여부
20    var isSending = false;  // 현재 전송 프로세스 진행 중 여부
21
22    // 세션 ID 관리
23    function getSessionId() {
24        var sid = getCookie('tracker_session_id');
25        if (!sid) {
26            sid = generateUUID();
27        }
28        setCookie('tracker_session_id', sid, 30);
29        return sid;
30    }
31
32    function generateUUID() {
33        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
34            var r = Math.random() * 16 | 0;
35            var v = c === 'x' ? r : (r & 0x3 | 0x8);
36            return v.toString(16);
37        });
38    }
39
40    function setCookie(name, value, minutes) {
41        var expires = "";
42        if (minutes) {
43            var date = new Date();
44            date.setTime(date.getTime() + (minutes * 60 * 1000));
45            expires = "; expires=" + date.toUTCString();
46        }
47        document.cookie = name + "=" + (value || "") + expires + "; path=/; SameSite=Lax";
48    }
49
50    function getCookie(name) {
51        var nameEQ = name + "=";
52        var ca = document.cookie.split(';');
53        for (var i = 0; i < ca.length; i++) {
54            var c = ca[i];
55            while (c.charAt(0) === ' ') c = c.substring(1, c.length);
56            if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
57        }
58        return null;
59    }
60
61    // === 메인 로직 ===
62    function start() {
63        startTime = Date.now();
64        lastActivityTime = startTime;
65        dataSent = false;
66
67        addActivityListeners();
68        startHeartbeat();
69
70        // 최신 브라우저는 visibilitychange가 가장 정확합니다.
71        document.addEventListener('visibilitychange', handleVisibilityChange, false);
72        // 모바일 사파리 및 BFCache 대응
73        window.addEventListener('pagehide', handlePageHide, false);
74    }
75
76    function startHeartbeat() {
77        if (heartbeatInterval) clearInterval(heartbeatInterval);
78        heartbeatInterval = setInterval(function () {
79            if (isActive && !document.hidden) {
80                sendHeartbeat();
81            }
82        }, 30000); 
83    }
84
85    function stopHeartbeat() {
86        if (heartbeatInterval) {
87            clearInterval(heartbeatInterval);
88            heartbeatInterval = null;
89        }
90    }
91
92    function addActivityListeners() {
93        var events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
94        var lastUpdate = 0;
95        var throttleMs = 1000;
96
97        events.forEach(function (event) {
98            document.addEventListener(event, function () {
99                var now = Date.now();
100                if (now - lastUpdate < throttleMs) return;
101                lastUpdate = now;
102                isActive = true;
103            }, { passive: true });
104        });
105    }
106
107    function handleVisibilityChange() {
108        if (document.visibilityState === 'hidden') {
109            isActive = false;
110            stopHeartbeat();
111            // 탭이 가려지는 순간 즉시 전송 시도 (비동기 대기 없음)
112            sendFinalData({ isFinal: true });
113        } else if (document.visibilityState === 'visible') {
114            isActive = true;
115            lastActivityTime = Date.now();
116            startHeartbeat();
117        }
118    }
119
120    function handlePageHide(event) {
121        // visibilitychange에서 미처 전송되지 않았을 경우를 대비한 2차 방어선
122        sendFinalData({ isFinal: true });
123    }
124
125    function sendHeartbeat() {
126        var now = Date.now();
127        var duration = Math.round((now - startTime) / 1000);
128
129        $.ajax({
130            url: '/tracking/heartbeat.asp',
131            type: 'POST',
132            data: {
133                session_id: sessionId,
134                page_url: pageUrl,
135                duration: duration,
136                is_active: isActive ? 1 : 0
137            },
138            async: true
139        });
140    }
141
142    /**
143     * 최종 데이터 전송 결정 함수
144     */
145    function sendFinalData(options) {
146        if (dataSent || isSending) return;
147
148        isSending = true;
149        
150        // 중요: 최종 전송(isFinal)시에는 setTimeout을 절대 사용하지 않습니다.
151        // 브라우저 종료 직전에는 1ms의 지연으로도 프로세스가 종료될 수 있습니다.
152        var result = sendTrackingData(options);
153        
154        if (result) {
155            dataSent = true;
156        }
157        isSending = false;
158    }
159
160	function sendTrackingData(opts) {
161		var endTime = Date.now();
162		var totalDuration = Math.round((endTime - startTime) / 1000);
163		var activeDuration = Math.round((lastActivityTime - startTime) / 1000);
164
165		if (totalDuration < 1) return false;
166
167		var data = {
168			session_id: String(sessionId || ''),
169			page_title: String(page_title || ''),
170			page_type: String(page_type || ''),
171			page_url: String(pageUrl || ''),
172			referrer: String(document.referrer || '-'),
173			total_duration: Number(totalDuration),
174			active_duration: Number(activeDuration > 0 ? activeDuration : totalDuration),
175			screen_width: Number(screen.width),
176			screen_height: Number(screen.height),
177			user_agent: String(navigator.userAgent),
178			is_final: opts.isFinal ? 1 : 0
179		};
180
181		// --- 수정 포인트: URLSearchParams 사용 ---
182		var params = new URLSearchParams();
183		for (var key in data) {
184			params.append(key, data[key]);
185		}
186
187		if (navigator.sendBeacon) {
188			// URLSearchParams를 보내면 브라우저가 자동으로 
189			// application/x-www-form-urlencoded;charset=UTF-8 로 설정합니다.
190			var beaconResult = navigator.sendBeacon('/tracking/save_tracking.asp', params);
191			if (beaconResult) return true;
192		}
193		// ---------------------------------------
194
195		// AJAX 폴백 (기존 유지)
196		try {
197			$.ajax({
198				url: '/tracking/save_tracking.asp',
199				type: 'POST',
200				data: data, // jQuery는 기본적으로 x-www-form-urlencoded로 처리함
201				async: false,
202				timeout: 500
203			});
204			return true;
205		} catch (e) {
206			return false;
207		}
208	}
209	
210    function handlePageShow(event) {
211        // BFCache(뒤로가기)로 복원된 경우 변수 초기화 및 재시작
212        if (event.persisted) {
213            startTime = Date.now();
214            lastActivityTime = startTime;
215            dataSent = false;
216            isSending = false;
217            isActive = true;
218            startHeartbeat();
219        }
220    }
221
222    return {
223        init: function () {
224            if (typeof jQuery === 'undefined') return;
225
226            window.addEventListener('pageshow', handlePageShow, false);
227
228            $(document).ready(function () {
229                start();
230            });
231        }
232    };
233})();
234
235PageTracker.init();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.