Initial commit - tokiDownloader.user.js
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
/* visitorlive tracker v2 — 대형 분석 서비스 표준 (page_view, session, UTM, referrer, SPA)
|
||||
*
|
||||
* 임베드 예시:
|
||||
* <script src="https://whoas.xyz/live/track.js" async></script>
|
||||
* <script src="https://whoas.xyz/live/track.js" data-site="myblog" async></script>
|
||||
*
|
||||
* 자동 추적: page_view (자동), session_start, session_end
|
||||
* 수동 이벤트: window.vlive('event_name', {props})
|
||||
*/
|
||||
(function () {
|
||||
if (window.__vlive) return; window.__vlive = true;
|
||||
|
||||
var script = document.currentScript || (function() {
|
||||
var ss = document.getElementsByTagName('script');
|
||||
return ss[ss.length - 1];
|
||||
})();
|
||||
|
||||
// ── SERVER & SITE ──
|
||||
var SERVER, SITE;
|
||||
try {
|
||||
var u = new URL(script.src);
|
||||
SERVER = u.host;
|
||||
SITE = script.getAttribute('data-site') || u.searchParams.get('site') || '';
|
||||
} catch(e) {
|
||||
SERVER = location.host;
|
||||
SITE = '';
|
||||
}
|
||||
if (!SITE) SITE = location.hostname.replace(/[^a-zA-Z0-9._-]/g,'').toLowerCase().slice(0,64);
|
||||
if (!SITE) SITE = 'unknown';
|
||||
|
||||
// ── ANON_ID (영구 — 브라우저 단위) ──
|
||||
var COOKIE_NAME = '__vsid';
|
||||
function readCookie() { try { var m=document.cookie.match(new RegExp('(?:^|; )'+COOKIE_NAME+'=([^;]+)')); return m?decodeURIComponent(m[1]):''; } catch(_){return '';} }
|
||||
function writeCookie(v) { try { var sec=location.protocol==='https:'?'; Secure':''; document.cookie=COOKIE_NAME+'='+encodeURIComponent(v)+'; path=/; max-age=31536000; SameSite=Lax'+sec; } catch(_){} }
|
||||
function readLS() { try { return localStorage.getItem(COOKIE_NAME)||''; } catch(_){return '';} }
|
||||
function writeLS(v) { try { localStorage.setItem(COOKIE_NAME, v); } catch(_){} }
|
||||
function readSS() { try { return sessionStorage.getItem(COOKIE_NAME)||''; } catch(_){return '';} }
|
||||
function writeSS(v) { try { sessionStorage.setItem(COOKIE_NAME, v); } catch(_){} }
|
||||
|
||||
function genId() {
|
||||
if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c){
|
||||
var r=Math.random()*16|0,v=c==='x'?r:(r&0x3)|0x8; return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAnonId() {
|
||||
var v = readCookie() || readLS() || readSS();
|
||||
if (!v) v = genId();
|
||||
writeCookie(v); writeLS(v); writeSS(v);
|
||||
return v;
|
||||
}
|
||||
var ANON_ID = resolveAnonId();
|
||||
|
||||
// ── SESSION_ID (30분 idle 만료) ──
|
||||
var SESSION_KEY = '__vlive_sess';
|
||||
var SESSION_IDLE_MS = 30 * 60 * 1000; // 30분
|
||||
|
||||
function getSession() {
|
||||
try {
|
||||
var raw = sessionStorage.getItem(SESSION_KEY) || localStorage.getItem(SESSION_KEY);
|
||||
if (raw) {
|
||||
var s = JSON.parse(raw);
|
||||
if (s && s.id && s.last && (Date.now()-s.last) < SESSION_IDLE_MS) {
|
||||
s.last = Date.now();
|
||||
var data = JSON.stringify(s);
|
||||
sessionStorage.setItem(SESSION_KEY, data);
|
||||
localStorage.setItem(SESSION_KEY, data);
|
||||
return { id: s.id, isNew: false, started: s.started };
|
||||
}
|
||||
}
|
||||
} catch(_){}
|
||||
var now = Date.now();
|
||||
var sess = { id: genId(), started: now, last: now };
|
||||
try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(sess)); } catch(_){}
|
||||
try { localStorage.setItem(SESSION_KEY, JSON.stringify(sess)); } catch(_){}
|
||||
return { id: sess.id, isNew: true, started: now };
|
||||
}
|
||||
function touchSession() {
|
||||
try {
|
||||
var raw = sessionStorage.getItem(SESSION_KEY) || localStorage.getItem(SESSION_KEY);
|
||||
if (raw) {
|
||||
var s = JSON.parse(raw); s.last = Date.now();
|
||||
var data = JSON.stringify(s);
|
||||
sessionStorage.setItem(SESSION_KEY, data);
|
||||
localStorage.setItem(SESSION_KEY, data);
|
||||
}
|
||||
} catch(_){}
|
||||
}
|
||||
var SESSION = getSession();
|
||||
|
||||
// ── UTM/REFERRER ──
|
||||
function parseUTM() {
|
||||
var p = new URLSearchParams(location.search);
|
||||
return {
|
||||
utm_source: p.get('utm_source')||'',
|
||||
utm_medium: p.get('utm_medium')||'',
|
||||
utm_campaign: p.get('utm_campaign')||'',
|
||||
utm_term: p.get('utm_term')||'',
|
||||
utm_content: p.get('utm_content')||''
|
||||
};
|
||||
}
|
||||
|
||||
// ── 이벤트 BUILDER ──
|
||||
function buildEvent(name, props) {
|
||||
var utm = parseUTM();
|
||||
return {
|
||||
n: name,
|
||||
anon_id: ANON_ID,
|
||||
session_id: SESSION.id,
|
||||
site: SITE,
|
||||
page_path: location.pathname,
|
||||
page_url: location.href.slice(0,2000),
|
||||
page_title: (document.title||'').slice(0,200),
|
||||
referrer: (document.referrer||'').slice(0,1000),
|
||||
utm_source: utm.utm_source.slice(0,100),
|
||||
utm_medium: utm.utm_medium.slice(0,100),
|
||||
utm_campaign: utm.utm_campaign.slice(0,200),
|
||||
utm_term: utm.utm_term.slice(0,200),
|
||||
utm_content: utm.utm_content.slice(0,200),
|
||||
screen_w: screen.width||0,
|
||||
screen_h: screen.height||0,
|
||||
viewport_w: window.innerWidth||0,
|
||||
viewport_h: window.innerHeight||0,
|
||||
ts: Date.now(),
|
||||
props: props || null
|
||||
};
|
||||
}
|
||||
|
||||
// ── 전송 (sendBeacon → fetch keepalive fallback) ──
|
||||
function send(ev) {
|
||||
var url = 'https://' + SERVER + '/collect';
|
||||
var body = JSON.stringify(ev);
|
||||
try {
|
||||
if (navigator.sendBeacon) {
|
||||
var blob = new Blob([body], {type:'application/json'});
|
||||
if (navigator.sendBeacon(url, blob)) return;
|
||||
}
|
||||
} catch(_){}
|
||||
try {
|
||||
fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body:body, keepalive:true, mode:'no-cors'}).catch(function(){});
|
||||
} catch(_){}
|
||||
}
|
||||
|
||||
// ── 공개 API ──
|
||||
window.vlive = function(name, props) {
|
||||
if (typeof name !== 'string' || !name) return;
|
||||
touchSession();
|
||||
send(buildEvent(name, props));
|
||||
};
|
||||
|
||||
// ── 자동: page_view ──
|
||||
function pageView() {
|
||||
touchSession();
|
||||
send(buildEvent('page_view'));
|
||||
}
|
||||
|
||||
// ── 첫 진입 ──
|
||||
if (SESSION.isNew) send(buildEvent('session_start'));
|
||||
pageView();
|
||||
|
||||
// ── SPA pushState/replaceState/popstate hook ──
|
||||
(function(){
|
||||
var lastPath = location.pathname + location.search;
|
||||
function check(){
|
||||
var cur = location.pathname + location.search;
|
||||
if (cur !== lastPath) { lastPath = cur; pageView(); }
|
||||
}
|
||||
var origPush = history.pushState;
|
||||
var origReplace = history.replaceState;
|
||||
history.pushState = function(){ var r=origPush.apply(this, arguments); setTimeout(check,0); return r; };
|
||||
history.replaceState = function(){ var r=origReplace.apply(this, arguments); setTimeout(check,0); return r; };
|
||||
window.addEventListener('popstate', check);
|
||||
})();
|
||||
|
||||
// ── 실시간 동접 alive beacon (whos.amung.us 모델, HTTP polling) ──
|
||||
// 기존 WebSocket 영속 연결 → 60초 polling 으로 교체. 30만 동접 부하 99% 감소.
|
||||
// sendBeacon 로 비차단 전송, 페이지 background 시 skip.
|
||||
function aliveBeacon() {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
var url = 'https://' + SERVER + '/beacon';
|
||||
var body = JSON.stringify({anon_id: ANON_ID, site: SITE});
|
||||
try {
|
||||
if (navigator.sendBeacon) {
|
||||
var blob = new Blob([body], {type:'application/json'});
|
||||
if (navigator.sendBeacon(url, blob)) { touchSession(); return; }
|
||||
}
|
||||
} catch(_){}
|
||||
try {
|
||||
fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body:body, keepalive:true, mode:'no-cors'}).catch(function(){});
|
||||
touchSession();
|
||||
} catch(_){}
|
||||
}
|
||||
aliveBeacon(); // 첫 진입 즉시 1번
|
||||
setInterval(aliveBeacon, 60000); // 60초마다
|
||||
// 페이지 visibility 복귀 시 즉시 1번 (background→foreground 전환 케이스)
|
||||
document.addEventListener('visibilitychange', function(){
|
||||
if (document.visibilityState === 'visible') aliveBeacon();
|
||||
});
|
||||
|
||||
// ── 페이지 이탈 시 session_end ──
|
||||
var sessionStartedAt = SESSION.started;
|
||||
var pageviewCount = 1;
|
||||
window.addEventListener('pagehide', function() {
|
||||
var ev = buildEvent('session_end');
|
||||
ev.duration_sec = Math.floor((Date.now()-sessionStartedAt)/1000);
|
||||
ev.props = JSON.stringify({pageviews: pageviewCount});
|
||||
send(ev);
|
||||
});
|
||||
|
||||
// pageView 카운트
|
||||
var origPageView = pageView;
|
||||
pageView = function() { pageviewCount++; origPageView(); };
|
||||
})();
|
||||
Reference in New Issue
Block a user