Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
50 lines
4.2 KiB
JavaScript
50 lines
4.2 KiB
JavaScript
/* 웹메이커 접속 통계 비컨 (v1.0.2)
|
|
* 생성·배포되는 모든 HTML 에 <script src=".../wm-stats.js" data-wm-stats data-site="<사이트ID>" async> 로 삽입된다.
|
|
* v1.4.17: 빌더 미리보기 안에서 열렸을 때는 부모 창(빌더)에 "지금 어느 페이지인지" 를 postMessage 로 알린다 — 아래 notifyParent.
|
|
* 페이지를 열 때 한 번, 다음 정보를 서비스 서버의 POST /api/stats/beacon 으로 보낸다.
|
|
* - path/ref/host : 어떤 페이지를 어디서 왔는지 (리퍼러는 호스트만 집계됨)
|
|
* - w/h/lang : 기기 구간(모바일/태블릿/PC) 과 언어 집계용
|
|
* - transfer : Performance API 의 transferSize 합계 = 이 페이지 열람에 실제로 전송된 바이트(추정 아웃바운드)
|
|
* (교차 출처 자원은 Timing-Allow-Origin 이 없으면 0 으로 잡힌다)
|
|
* 쿠키·로컬스토리지를 쓰지 않고, 서버는 IP 를 저장하지 않고 일자별 해시로만 방문자를 센다.
|
|
* 뒤로가기 캐시(bfcache) 복원이나 같은 페이지 재방문도 한 번의 열람으로 센다.
|
|
*/
|
|
(function () {
|
|
try {
|
|
var me = document.currentScript || (function () { var s = document.querySelectorAll('script[data-wm-stats]'); return s[s.length - 1]; })();
|
|
if (!me || !me.getAttribute('data-site')) return;
|
|
var site = me.getAttribute('data-site');
|
|
var base = (me.getAttribute('src') || '').replace(/\/wm-stats\.js(\?.*)?$/, '');
|
|
if (!/^https?:\/\//.test(base)) return;
|
|
/* v1.4.17 빌더 미리보기 페이지 동기화: 미리보기 iframe 은 sandbox(allow-same-origin 없음)라 빌더가 iframe 의 주소를 읽을 수 없다.
|
|
* 그래서 페이지 쪽에서 부모 창에 {type:'wm-page', site, path, title} 을 보낸다(대상 '*' — 이 페이지의 출처는 opaque 라 지정할 수 없고,
|
|
* 받는 쪽(public/app.js)이 보낸 창이 자기 미리보기 iframe 인지·경로가 그 사이트인지 확인한다). 빌더 밖(운영 주소·다른 사이트에 삽입)에서는
|
|
* 부모가 없거나 무시하므로 영향이 없다. bfcache 복원(pageshow)에도 다시 알린다. */
|
|
function notifyParent() {
|
|
try { if (window.parent && window.parent !== window) window.parent.postMessage({ type: 'wm-page', site: site, path: location.pathname, title: document.title || '' }, '*'); } catch (e) {}
|
|
}
|
|
notifyParent(); window.addEventListener('pageshow', function (ev) { if (ev.persisted) notifyParent(); });
|
|
var sent = false;
|
|
function transferBytes() {
|
|
try {
|
|
var total = 0; var nav = performance.getEntriesByType('navigation')[0];
|
|
if (nav && nav.transferSize) total += nav.transferSize;
|
|
performance.getEntriesByType('resource').forEach(function (r) { if (r.transferSize) total += r.transferSize; });
|
|
return total;
|
|
} catch (e) { return 0; }
|
|
}
|
|
function send() {
|
|
if (sent) return; sent = true;
|
|
var body = JSON.stringify({ site: site, path: location.pathname, ref: document.referrer || '', host: location.hostname, w: window.innerWidth || screen.width || 0, h: window.innerHeight || screen.height || 0, lang: navigator.language || '', transfer: transferBytes(), t: Date.now() });
|
|
var url = base + '/api/stats/beacon';
|
|
// sendBeacon 은 페이지를 떠나는 중에도 전송이 보장된다. 지원하지 않으면 keepalive fetch 로 대체.
|
|
if (navigator.sendBeacon) { try { if (navigator.sendBeacon(url, new Blob([body], { type: 'text/plain' }))) return; } catch (e) {} }
|
|
try { fetch(url, { method: 'POST', body: body, headers: { 'Content-Type': 'text/plain' }, keepalive: true, mode: 'cors', credentials: 'omit' }).catch(function () {}); } catch (e) {}
|
|
}
|
|
// 자원 로딩이 끝난 뒤(전송량 계산을 위해) 보내되, 그 전에 떠나면 그 시점 값으로 보낸다.
|
|
if (document.readyState === 'complete') setTimeout(send, 800); else window.addEventListener('load', function () { setTimeout(send, 800); });
|
|
window.addEventListener('pagehide', send);
|
|
document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'hidden') send(); });
|
|
} catch (e) { /* 통계 실패는 사이트 동작에 영향을 주지 않는다 */ }
|
|
})();
|