connectbase-client 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +181 -156
- package/dist/connect-base.umd.js +3 -3
- package/dist/index.d.mts +163 -4
- package/dist/index.d.ts +163 -4
- package/dist/index.js +650 -2
- package/dist/index.mjs +649 -2
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
GameRoom: () => GameRoom,
|
|
30
30
|
GameRoomTransport: () => GameRoomTransport,
|
|
31
31
|
NativeAPI: () => NativeAPI,
|
|
32
|
+
SessionManager: () => SessionManager,
|
|
32
33
|
VideoProcessingError: () => VideoProcessingError,
|
|
33
34
|
default: () => index_default,
|
|
34
35
|
isWebTransportSupported: () => isWebTransportSupported
|
|
@@ -53,11 +54,14 @@ var AuthError = class extends Error {
|
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
// src/core/http.ts
|
|
57
|
+
var TOKEN_STORAGE_KEY = "cb_auth_tokens";
|
|
56
58
|
var HttpClient = class {
|
|
57
59
|
constructor(config) {
|
|
58
60
|
this.isRefreshing = false;
|
|
59
61
|
this.refreshPromise = null;
|
|
60
62
|
this.config = { ...config };
|
|
63
|
+
this.storageKey = this.buildStorageKey();
|
|
64
|
+
this.restoreTokens();
|
|
61
65
|
}
|
|
62
66
|
updateConfig(config) {
|
|
63
67
|
this.config = { ...this.config, ...config };
|
|
@@ -65,10 +69,64 @@ var HttpClient = class {
|
|
|
65
69
|
setTokens(accessToken, refreshToken) {
|
|
66
70
|
this.config.accessToken = accessToken;
|
|
67
71
|
this.config.refreshToken = refreshToken;
|
|
72
|
+
this.persistTokens();
|
|
68
73
|
}
|
|
69
74
|
clearTokens() {
|
|
70
75
|
this.config.accessToken = void 0;
|
|
71
76
|
this.config.refreshToken = void 0;
|
|
77
|
+
this.removePersistedTokens();
|
|
78
|
+
}
|
|
79
|
+
// ===== Token Persistence =====
|
|
80
|
+
get persistence() {
|
|
81
|
+
return this.config.persistence ?? "localStorage";
|
|
82
|
+
}
|
|
83
|
+
getStorage() {
|
|
84
|
+
if (typeof window === "undefined") return null;
|
|
85
|
+
if (this.persistence === "localStorage" && typeof localStorage !== "undefined") return localStorage;
|
|
86
|
+
if (this.persistence === "sessionStorage" && typeof sessionStorage !== "undefined") return sessionStorage;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
buildStorageKey() {
|
|
90
|
+
const credential = this.config.publicKey ?? this.config.secretKey;
|
|
91
|
+
if (!credential) return TOKEN_STORAGE_KEY;
|
|
92
|
+
let hash = 0;
|
|
93
|
+
for (let i = 0; i < credential.length; i++) {
|
|
94
|
+
hash = (hash << 5) - hash + credential.charCodeAt(i);
|
|
95
|
+
hash = hash & hash;
|
|
96
|
+
}
|
|
97
|
+
return `${TOKEN_STORAGE_KEY}_${Math.abs(hash).toString(36)}`;
|
|
98
|
+
}
|
|
99
|
+
persistTokens() {
|
|
100
|
+
if (this.persistence === "none") return;
|
|
101
|
+
const storage = this.getStorage();
|
|
102
|
+
if (!storage || !this.config.accessToken || !this.config.refreshToken) return;
|
|
103
|
+
storage.setItem(this.storageKey, JSON.stringify({
|
|
104
|
+
accessToken: this.config.accessToken,
|
|
105
|
+
refreshToken: this.config.refreshToken
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
restoreTokens() {
|
|
109
|
+
if (this.persistence === "none") return;
|
|
110
|
+
if (this.config.accessToken && this.config.refreshToken) return;
|
|
111
|
+
const storage = this.getStorage();
|
|
112
|
+
if (!storage) return;
|
|
113
|
+
const stored = storage.getItem(this.storageKey);
|
|
114
|
+
if (!stored) return;
|
|
115
|
+
try {
|
|
116
|
+
const { accessToken, refreshToken } = JSON.parse(stored);
|
|
117
|
+
if (accessToken && refreshToken) {
|
|
118
|
+
this.config.accessToken = accessToken;
|
|
119
|
+
this.config.refreshToken = refreshToken;
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
storage.removeItem(this.storageKey);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
removePersistedTokens() {
|
|
126
|
+
if (this.persistence === "none") return;
|
|
127
|
+
const storage = this.getStorage();
|
|
128
|
+
if (!storage) return;
|
|
129
|
+
storage.removeItem(this.storageKey);
|
|
72
130
|
}
|
|
73
131
|
/**
|
|
74
132
|
* Public Key 가 설정되어 있는지 확인
|
|
@@ -137,8 +195,7 @@ var HttpClient = class {
|
|
|
137
195
|
throw new Error("Token refresh failed");
|
|
138
196
|
}
|
|
139
197
|
const data = await response.json();
|
|
140
|
-
this.
|
|
141
|
-
this.config.refreshToken = data.refresh_token;
|
|
198
|
+
this.setTokens(data.access_token, data.refresh_token);
|
|
142
199
|
this.config.onTokenRefresh?.({
|
|
143
200
|
accessToken: data.access_token,
|
|
144
201
|
refreshToken: data.refresh_token
|
|
@@ -7459,6 +7516,594 @@ var QueueAPI = class {
|
|
|
7459
7516
|
}
|
|
7460
7517
|
};
|
|
7461
7518
|
|
|
7519
|
+
// src/api/analytics.ts
|
|
7520
|
+
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
7521
|
+
var SESSION_KEY = "__cb_session";
|
|
7522
|
+
var VISITOR_KEY = "__cb_visitor_uid";
|
|
7523
|
+
var LAST_ACTIVITY_KEY = "__cb_last_activity";
|
|
7524
|
+
function generateId() {
|
|
7525
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
7526
|
+
return crypto.randomUUID();
|
|
7527
|
+
}
|
|
7528
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
7529
|
+
const r = Math.random() * 16 | 0;
|
|
7530
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
7531
|
+
return v.toString(16);
|
|
7532
|
+
});
|
|
7533
|
+
}
|
|
7534
|
+
var SessionManager = class {
|
|
7535
|
+
constructor() {
|
|
7536
|
+
this._sessionId = null;
|
|
7537
|
+
this._visitorUid = null;
|
|
7538
|
+
this._lastActivity = 0;
|
|
7539
|
+
this._isNewSession = false;
|
|
7540
|
+
}
|
|
7541
|
+
get sessionId() {
|
|
7542
|
+
this.ensureSession();
|
|
7543
|
+
return this._sessionId;
|
|
7544
|
+
}
|
|
7545
|
+
get visitorUid() {
|
|
7546
|
+
if (!this._visitorUid) {
|
|
7547
|
+
this._visitorUid = this.loadOrCreateVisitorUid();
|
|
7548
|
+
}
|
|
7549
|
+
return this._visitorUid;
|
|
7550
|
+
}
|
|
7551
|
+
get isNewSession() {
|
|
7552
|
+
return this._isNewSession;
|
|
7553
|
+
}
|
|
7554
|
+
/** 활동 기록 — 세션 타임아웃 리셋 */
|
|
7555
|
+
touch() {
|
|
7556
|
+
this._lastActivity = Date.now();
|
|
7557
|
+
this._isNewSession = false;
|
|
7558
|
+
try {
|
|
7559
|
+
if (typeof sessionStorage !== "undefined") {
|
|
7560
|
+
sessionStorage.setItem(LAST_ACTIVITY_KEY, String(this._lastActivity));
|
|
7561
|
+
}
|
|
7562
|
+
} catch {
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
/** 세션 강제 리셋 */
|
|
7566
|
+
reset() {
|
|
7567
|
+
this._sessionId = null;
|
|
7568
|
+
this._isNewSession = false;
|
|
7569
|
+
try {
|
|
7570
|
+
if (typeof sessionStorage !== "undefined") {
|
|
7571
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
7572
|
+
sessionStorage.removeItem(LAST_ACTIVITY_KEY);
|
|
7573
|
+
}
|
|
7574
|
+
} catch {
|
|
7575
|
+
}
|
|
7576
|
+
}
|
|
7577
|
+
ensureSession() {
|
|
7578
|
+
const now = Date.now();
|
|
7579
|
+
if (!this._sessionId) {
|
|
7580
|
+
try {
|
|
7581
|
+
if (typeof sessionStorage !== "undefined") {
|
|
7582
|
+
this._sessionId = sessionStorage.getItem(SESSION_KEY);
|
|
7583
|
+
const lastStr = sessionStorage.getItem(LAST_ACTIVITY_KEY);
|
|
7584
|
+
this._lastActivity = lastStr ? parseInt(lastStr, 10) : 0;
|
|
7585
|
+
}
|
|
7586
|
+
} catch {
|
|
7587
|
+
}
|
|
7588
|
+
}
|
|
7589
|
+
if (!this._sessionId || this._lastActivity > 0 && now - this._lastActivity > SESSION_TIMEOUT_MS) {
|
|
7590
|
+
this._sessionId = generateId();
|
|
7591
|
+
this._isNewSession = true;
|
|
7592
|
+
this._lastActivity = now;
|
|
7593
|
+
try {
|
|
7594
|
+
if (typeof sessionStorage !== "undefined") {
|
|
7595
|
+
sessionStorage.setItem(SESSION_KEY, this._sessionId);
|
|
7596
|
+
sessionStorage.setItem(LAST_ACTIVITY_KEY, String(now));
|
|
7597
|
+
}
|
|
7598
|
+
} catch {
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7602
|
+
loadOrCreateVisitorUid() {
|
|
7603
|
+
try {
|
|
7604
|
+
if (typeof localStorage !== "undefined") {
|
|
7605
|
+
const existing = localStorage.getItem(VISITOR_KEY);
|
|
7606
|
+
if (existing) return existing;
|
|
7607
|
+
const uid = generateId();
|
|
7608
|
+
localStorage.setItem(VISITOR_KEY, uid);
|
|
7609
|
+
return uid;
|
|
7610
|
+
}
|
|
7611
|
+
} catch {
|
|
7612
|
+
}
|
|
7613
|
+
return generateId();
|
|
7614
|
+
}
|
|
7615
|
+
};
|
|
7616
|
+
function parseUTM() {
|
|
7617
|
+
if (typeof window === "undefined") return {};
|
|
7618
|
+
try {
|
|
7619
|
+
const params = new URLSearchParams(window.location.search);
|
|
7620
|
+
const utm = {};
|
|
7621
|
+
for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]) {
|
|
7622
|
+
const val = params.get(key);
|
|
7623
|
+
if (val) utm[key] = val;
|
|
7624
|
+
}
|
|
7625
|
+
return utm;
|
|
7626
|
+
} catch {
|
|
7627
|
+
return {};
|
|
7628
|
+
}
|
|
7629
|
+
}
|
|
7630
|
+
var AnalyticsAPI = class {
|
|
7631
|
+
constructor(http) {
|
|
7632
|
+
this.storageWebId = null;
|
|
7633
|
+
this.eventQueue = [];
|
|
7634
|
+
this.batchTimer = null;
|
|
7635
|
+
this.isInitialized = false;
|
|
7636
|
+
this.heartbeatTimer = null;
|
|
7637
|
+
this.visibilityHandler = null;
|
|
7638
|
+
this.unloadHeartbeatHandler = null;
|
|
7639
|
+
this.popstateHandler = null;
|
|
7640
|
+
this.beforeUnloadHandler = null;
|
|
7641
|
+
this.origPushState = null;
|
|
7642
|
+
this.origReplaceState = null;
|
|
7643
|
+
this.heatmapClickHandler = null;
|
|
7644
|
+
this.heatmapScrollHandler = null;
|
|
7645
|
+
this.utm = {};
|
|
7646
|
+
this.handleHeatmapClick = (e) => {
|
|
7647
|
+
const xPercent = e.clientX / window.innerWidth * 100;
|
|
7648
|
+
const yPercent = e.clientY / window.innerHeight * 100;
|
|
7649
|
+
this.recordHeatmapEvent("click", xPercent, yPercent);
|
|
7650
|
+
};
|
|
7651
|
+
this.heatmapQueue = [];
|
|
7652
|
+
this.http = http;
|
|
7653
|
+
this.config = {
|
|
7654
|
+
trackPageViews: true,
|
|
7655
|
+
trackEvents: true,
|
|
7656
|
+
trackSessions: true,
|
|
7657
|
+
heatmap: false,
|
|
7658
|
+
recording: false,
|
|
7659
|
+
batchSize: 10,
|
|
7660
|
+
flushInterval: 5e3,
|
|
7661
|
+
respectDoNotTrack: true,
|
|
7662
|
+
debug: false
|
|
7663
|
+
};
|
|
7664
|
+
this.consent = {
|
|
7665
|
+
analytics: true,
|
|
7666
|
+
heatmap: false,
|
|
7667
|
+
recording: false
|
|
7668
|
+
};
|
|
7669
|
+
this.session = new SessionManager();
|
|
7670
|
+
}
|
|
7671
|
+
/**
|
|
7672
|
+
* Analytics 초기화
|
|
7673
|
+
* @param storageWebId 웹 스토리지 ID
|
|
7674
|
+
* @param config 설정 (선택)
|
|
7675
|
+
*/
|
|
7676
|
+
init(storageWebId, config) {
|
|
7677
|
+
if (this.isInitialized) {
|
|
7678
|
+
this.log("Analytics already initialized");
|
|
7679
|
+
return;
|
|
7680
|
+
}
|
|
7681
|
+
if (typeof window === "undefined") {
|
|
7682
|
+
this.log("Analytics only works in browser environment");
|
|
7683
|
+
return;
|
|
7684
|
+
}
|
|
7685
|
+
if (config?.respectDoNotTrack !== false && this.isDNT()) {
|
|
7686
|
+
this.log("Do Not Track enabled, analytics disabled");
|
|
7687
|
+
return;
|
|
7688
|
+
}
|
|
7689
|
+
this.storageWebId = storageWebId;
|
|
7690
|
+
Object.assign(this.config, config);
|
|
7691
|
+
this.isInitialized = true;
|
|
7692
|
+
this.utm = parseUTM();
|
|
7693
|
+
if (this.config.trackSessions) {
|
|
7694
|
+
this.trackSessionStart();
|
|
7695
|
+
this.startHeartbeat();
|
|
7696
|
+
}
|
|
7697
|
+
if (this.config.trackPageViews) {
|
|
7698
|
+
this.trackPageView();
|
|
7699
|
+
this.setupAutoPageView();
|
|
7700
|
+
}
|
|
7701
|
+
this.startBatchTimer();
|
|
7702
|
+
this.beforeUnloadHandler = () => this.flushSync();
|
|
7703
|
+
window.addEventListener("beforeunload", this.beforeUnloadHandler);
|
|
7704
|
+
this.log("Analytics initialized", { storageWebId });
|
|
7705
|
+
}
|
|
7706
|
+
/** Analytics 정리 */
|
|
7707
|
+
destroy() {
|
|
7708
|
+
if (!this.isInitialized) return;
|
|
7709
|
+
this.stopBatchTimer();
|
|
7710
|
+
this.stopHeartbeat();
|
|
7711
|
+
this.removeAutoPageView();
|
|
7712
|
+
this.removeHeatmapListeners();
|
|
7713
|
+
if (this.beforeUnloadHandler) {
|
|
7714
|
+
window.removeEventListener("beforeunload", this.beforeUnloadHandler);
|
|
7715
|
+
this.beforeUnloadHandler = null;
|
|
7716
|
+
}
|
|
7717
|
+
this.flush();
|
|
7718
|
+
this.isInitialized = false;
|
|
7719
|
+
this.log("Analytics destroyed");
|
|
7720
|
+
}
|
|
7721
|
+
/**
|
|
7722
|
+
* 동의 설정 변경
|
|
7723
|
+
*/
|
|
7724
|
+
setConsent(consent) {
|
|
7725
|
+
Object.assign(this.consent, consent);
|
|
7726
|
+
this.log("Consent updated", consent);
|
|
7727
|
+
if (consent.analytics === false && this.isInitialized) {
|
|
7728
|
+
this.destroy();
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
/** 현재 동의 상태 조회 */
|
|
7732
|
+
getConsent() {
|
|
7733
|
+
return { ...this.consent };
|
|
7734
|
+
}
|
|
7735
|
+
/**
|
|
7736
|
+
* 페이지뷰 수동 추적
|
|
7737
|
+
*/
|
|
7738
|
+
trackPageView(path) {
|
|
7739
|
+
if (!this.canTrack()) return;
|
|
7740
|
+
this.session.touch();
|
|
7741
|
+
const event = this.createBaseEvent("page_view");
|
|
7742
|
+
event.page_path = path || window.location.pathname;
|
|
7743
|
+
event.page_url = window.location.href;
|
|
7744
|
+
event.page_title = document.title;
|
|
7745
|
+
event.referrer = document.referrer || void 0;
|
|
7746
|
+
event.screen_width = window.screen.width;
|
|
7747
|
+
event.screen_height = window.screen.height;
|
|
7748
|
+
Object.assign(event, this.utm);
|
|
7749
|
+
this.enqueue(event);
|
|
7750
|
+
}
|
|
7751
|
+
/**
|
|
7752
|
+
* 커스텀 이벤트 추적
|
|
7753
|
+
*/
|
|
7754
|
+
trackEvent(name, properties) {
|
|
7755
|
+
if (!this.canTrack() || !this.config.trackEvents) return;
|
|
7756
|
+
this.session.touch();
|
|
7757
|
+
const event = this.createBaseEvent("event");
|
|
7758
|
+
event.event_name = name;
|
|
7759
|
+
event.event_properties = properties;
|
|
7760
|
+
event.page_path = window.location.pathname;
|
|
7761
|
+
event.page_url = window.location.href;
|
|
7762
|
+
this.enqueue(event);
|
|
7763
|
+
}
|
|
7764
|
+
/**
|
|
7765
|
+
* 사용자 식별 (로그인 시)
|
|
7766
|
+
*/
|
|
7767
|
+
identify(memberId) {
|
|
7768
|
+
if (!this.canTrack()) return;
|
|
7769
|
+
if (typeof window.__cbSetMember === "function") {
|
|
7770
|
+
window.__cbSetMember(memberId);
|
|
7771
|
+
}
|
|
7772
|
+
this.log("User identified", { memberId });
|
|
7773
|
+
}
|
|
7774
|
+
/**
|
|
7775
|
+
* 히트맵 수집 활성화 (opt-in)
|
|
7776
|
+
*/
|
|
7777
|
+
enableHeatmap(options) {
|
|
7778
|
+
if (!this.canTrack() || !this.consent.heatmap) return;
|
|
7779
|
+
const trackClick = options?.click ?? true;
|
|
7780
|
+
const trackScroll = options?.scroll ?? true;
|
|
7781
|
+
if (trackClick) {
|
|
7782
|
+
this.heatmapClickHandler = this.handleHeatmapClick;
|
|
7783
|
+
document.addEventListener("click", this.heatmapClickHandler);
|
|
7784
|
+
}
|
|
7785
|
+
if (trackScroll) {
|
|
7786
|
+
let scrollTimer = null;
|
|
7787
|
+
let maxScrollDepth = 0;
|
|
7788
|
+
this.heatmapScrollHandler = () => {
|
|
7789
|
+
const depth = Math.round(
|
|
7790
|
+
(window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
|
|
7791
|
+
);
|
|
7792
|
+
maxScrollDepth = Math.max(maxScrollDepth, depth);
|
|
7793
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
7794
|
+
scrollTimer = setTimeout(() => {
|
|
7795
|
+
this.recordHeatmapEvent("scroll", 50, maxScrollDepth * (window.innerHeight / 100), maxScrollDepth);
|
|
7796
|
+
}, 500);
|
|
7797
|
+
};
|
|
7798
|
+
window.addEventListener("scroll", this.heatmapScrollHandler, { passive: true });
|
|
7799
|
+
}
|
|
7800
|
+
this.log("Heatmap enabled", options);
|
|
7801
|
+
}
|
|
7802
|
+
/**
|
|
7803
|
+
* 세션 heartbeat 자동 전송 시작 (30초 간격)
|
|
7804
|
+
*/
|
|
7805
|
+
enableHeartbeat() {
|
|
7806
|
+
if (!this.canTrack()) return;
|
|
7807
|
+
if (this.heartbeatTimer) return;
|
|
7808
|
+
const sendHeartbeat = () => {
|
|
7809
|
+
if (!this.canTrack()) return;
|
|
7810
|
+
const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
7811
|
+
this.http.post(`${prefix}/storages/web/${this.storageWebId}/sessions/heartbeat`, {
|
|
7812
|
+
visitor_uid: this.session.visitorUid,
|
|
7813
|
+
session_id: this.session.sessionId
|
|
7814
|
+
}).catch(() => {
|
|
7815
|
+
});
|
|
7816
|
+
};
|
|
7817
|
+
this.heartbeatTimer = setInterval(sendHeartbeat, 3e4);
|
|
7818
|
+
document.addEventListener("visibilitychange", () => {
|
|
7819
|
+
if (document.hidden) {
|
|
7820
|
+
if (this.heartbeatTimer) {
|
|
7821
|
+
clearInterval(this.heartbeatTimer);
|
|
7822
|
+
this.heartbeatTimer = null;
|
|
7823
|
+
}
|
|
7824
|
+
} else {
|
|
7825
|
+
if (!this.heartbeatTimer) {
|
|
7826
|
+
this.heartbeatTimer = setInterval(sendHeartbeat, 3e4);
|
|
7827
|
+
sendHeartbeat();
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7830
|
+
});
|
|
7831
|
+
window.addEventListener("beforeunload", () => {
|
|
7832
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon && this.storageWebId) {
|
|
7833
|
+
const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
7834
|
+
const url = `${this.http.getBaseUrl()}${prefix}/storages/web/${this.storageWebId}/sessions/heartbeat`;
|
|
7835
|
+
navigator.sendBeacon(url, JSON.stringify({
|
|
7836
|
+
visitor_uid: this.session.visitorUid,
|
|
7837
|
+
session_id: this.session.sessionId
|
|
7838
|
+
}));
|
|
7839
|
+
}
|
|
7840
|
+
});
|
|
7841
|
+
sendHeartbeat();
|
|
7842
|
+
this.log("Heartbeat enabled (30s interval)");
|
|
7843
|
+
}
|
|
7844
|
+
/** 큐에 있는 이벤트 즉시 전송 */
|
|
7845
|
+
async flush() {
|
|
7846
|
+
await this.flushQueue();
|
|
7847
|
+
}
|
|
7848
|
+
/** 세션 매니저 접근 (고급) */
|
|
7849
|
+
getSession() {
|
|
7850
|
+
return this.session;
|
|
7851
|
+
}
|
|
7852
|
+
// ── Private ────────────────────────────────────────────────────
|
|
7853
|
+
canTrack() {
|
|
7854
|
+
if (!this.isInitialized || !this.storageWebId) return false;
|
|
7855
|
+
if (!this.consent.analytics) return false;
|
|
7856
|
+
return true;
|
|
7857
|
+
}
|
|
7858
|
+
isDNT() {
|
|
7859
|
+
if (typeof navigator === "undefined") return false;
|
|
7860
|
+
return navigator.doNotTrack === "1" || navigator.globalPrivacyControl === true;
|
|
7861
|
+
}
|
|
7862
|
+
createBaseEvent(type) {
|
|
7863
|
+
return {
|
|
7864
|
+
type,
|
|
7865
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7866
|
+
session_id: this.session.sessionId,
|
|
7867
|
+
visitor_uid: this.session.visitorUid
|
|
7868
|
+
};
|
|
7869
|
+
}
|
|
7870
|
+
enqueue(event) {
|
|
7871
|
+
this.eventQueue.push(event);
|
|
7872
|
+
if (this.eventQueue.length >= this.config.batchSize) {
|
|
7873
|
+
this.flushQueue();
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
async flushQueue() {
|
|
7877
|
+
if (this.eventQueue.length === 0 || !this.storageWebId) return;
|
|
7878
|
+
const events = this.eventQueue.splice(0);
|
|
7879
|
+
const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
7880
|
+
try {
|
|
7881
|
+
await this.http.post(
|
|
7882
|
+
`${prefix}/storages/web/${this.storageWebId}/visitors/batch`,
|
|
7883
|
+
{
|
|
7884
|
+
visitor_uid: this.session.visitorUid,
|
|
7885
|
+
events: events.map((e) => ({
|
|
7886
|
+
timestamp: e.timestamp,
|
|
7887
|
+
page_path: e.page_path || "",
|
|
7888
|
+
page_url: e.page_url || "",
|
|
7889
|
+
page_title: e.page_title || "",
|
|
7890
|
+
referrer: e.referrer || "",
|
|
7891
|
+
user_agent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
7892
|
+
screen_width: e.screen_width || 0,
|
|
7893
|
+
screen_height: e.screen_height || 0,
|
|
7894
|
+
session_id: e.session_id,
|
|
7895
|
+
session_start: e.type === "session_start",
|
|
7896
|
+
is_page_view: e.type === "page_view",
|
|
7897
|
+
event_name: e.event_name,
|
|
7898
|
+
event_properties: e.event_properties,
|
|
7899
|
+
utm_source: e.utm_source,
|
|
7900
|
+
utm_medium: e.utm_medium,
|
|
7901
|
+
utm_campaign: e.utm_campaign,
|
|
7902
|
+
utm_content: e.utm_content,
|
|
7903
|
+
utm_term: e.utm_term
|
|
7904
|
+
}))
|
|
7905
|
+
}
|
|
7906
|
+
);
|
|
7907
|
+
this.log(`Flushed ${events.length} events`);
|
|
7908
|
+
} catch (err) {
|
|
7909
|
+
if (this.eventQueue.length < this.config.batchSize * 3) {
|
|
7910
|
+
this.eventQueue.unshift(...events);
|
|
7911
|
+
}
|
|
7912
|
+
this.log("Flush failed", err);
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7915
|
+
/** sendBeacon으로 동기 flush (beforeunload용) */
|
|
7916
|
+
flushSync() {
|
|
7917
|
+
if (this.eventQueue.length === 0 || !this.storageWebId) return;
|
|
7918
|
+
if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
|
|
7919
|
+
const events = this.eventQueue.splice(0);
|
|
7920
|
+
const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
7921
|
+
const baseUrl = this.http.getBaseUrl();
|
|
7922
|
+
const url = `${baseUrl}${prefix}/storages/web/${this.storageWebId}/visitors/batch`;
|
|
7923
|
+
const body = JSON.stringify({
|
|
7924
|
+
visitor_uid: this.session.visitorUid,
|
|
7925
|
+
events: events.map((e) => ({
|
|
7926
|
+
timestamp: e.timestamp,
|
|
7927
|
+
page_path: e.page_path || "",
|
|
7928
|
+
page_url: e.page_url || "",
|
|
7929
|
+
page_title: e.page_title || "",
|
|
7930
|
+
referrer: e.referrer || "",
|
|
7931
|
+
session_id: e.session_id,
|
|
7932
|
+
session_start: e.type === "session_start",
|
|
7933
|
+
is_page_view: e.type === "page_view",
|
|
7934
|
+
event_name: e.event_name,
|
|
7935
|
+
event_properties: e.event_properties
|
|
7936
|
+
}))
|
|
7937
|
+
});
|
|
7938
|
+
try {
|
|
7939
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
7940
|
+
} catch {
|
|
7941
|
+
}
|
|
7942
|
+
}
|
|
7943
|
+
trackSessionStart() {
|
|
7944
|
+
const event = this.createBaseEvent("session_start");
|
|
7945
|
+
event.page_path = window.location.pathname;
|
|
7946
|
+
event.page_url = window.location.href;
|
|
7947
|
+
event.referrer = document.referrer || void 0;
|
|
7948
|
+
event.screen_width = window.screen.width;
|
|
7949
|
+
event.screen_height = window.screen.height;
|
|
7950
|
+
Object.assign(event, this.utm);
|
|
7951
|
+
this.enqueue(event);
|
|
7952
|
+
}
|
|
7953
|
+
startBatchTimer() {
|
|
7954
|
+
this.batchTimer = setInterval(() => this.flushQueue(), this.config.flushInterval);
|
|
7955
|
+
}
|
|
7956
|
+
stopBatchTimer() {
|
|
7957
|
+
if (this.batchTimer) {
|
|
7958
|
+
clearInterval(this.batchTimer);
|
|
7959
|
+
this.batchTimer = null;
|
|
7960
|
+
}
|
|
7961
|
+
}
|
|
7962
|
+
startHeartbeat() {
|
|
7963
|
+
this.heartbeatTimer = setInterval(() => {
|
|
7964
|
+
if (!this.canTrack()) return;
|
|
7965
|
+
this.sendHeartbeat();
|
|
7966
|
+
}, 30 * 1e3);
|
|
7967
|
+
if (typeof document !== "undefined") {
|
|
7968
|
+
this.visibilityHandler = () => {
|
|
7969
|
+
if (document.visibilityState === "hidden") {
|
|
7970
|
+
this.sendHeartbeatBeacon();
|
|
7971
|
+
if (this.heartbeatTimer) {
|
|
7972
|
+
clearInterval(this.heartbeatTimer);
|
|
7973
|
+
this.heartbeatTimer = null;
|
|
7974
|
+
}
|
|
7975
|
+
} else if (document.visibilityState === "visible") {
|
|
7976
|
+
if (!this.heartbeatTimer) {
|
|
7977
|
+
this.sendHeartbeat();
|
|
7978
|
+
this.heartbeatTimer = setInterval(() => {
|
|
7979
|
+
if (!this.canTrack()) return;
|
|
7980
|
+
this.sendHeartbeat();
|
|
7981
|
+
}, 30 * 1e3);
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
};
|
|
7985
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
7986
|
+
}
|
|
7987
|
+
if (typeof window !== "undefined") {
|
|
7988
|
+
this.unloadHeartbeatHandler = () => this.sendHeartbeatBeacon();
|
|
7989
|
+
window.addEventListener("beforeunload", this.unloadHeartbeatHandler);
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
stopHeartbeat() {
|
|
7993
|
+
if (this.heartbeatTimer) {
|
|
7994
|
+
clearInterval(this.heartbeatTimer);
|
|
7995
|
+
this.heartbeatTimer = null;
|
|
7996
|
+
}
|
|
7997
|
+
if (typeof document !== "undefined" && this.visibilityHandler) {
|
|
7998
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
7999
|
+
this.visibilityHandler = null;
|
|
8000
|
+
}
|
|
8001
|
+
if (typeof window !== "undefined" && this.unloadHeartbeatHandler) {
|
|
8002
|
+
window.removeEventListener("beforeunload", this.unloadHeartbeatHandler);
|
|
8003
|
+
this.unloadHeartbeatHandler = null;
|
|
8004
|
+
}
|
|
8005
|
+
}
|
|
8006
|
+
/** 하트비트를 큐에 넣어 배치 전송 */
|
|
8007
|
+
sendHeartbeat() {
|
|
8008
|
+
const event = this.createBaseEvent("heartbeat");
|
|
8009
|
+
event.page_path = window.location.pathname;
|
|
8010
|
+
this.enqueue(event);
|
|
8011
|
+
if (this.storageWebId) {
|
|
8012
|
+
const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
8013
|
+
this.http.post(
|
|
8014
|
+
`${prefix}/storages/web/${this.storageWebId}/sessions/heartbeat`,
|
|
8015
|
+
{
|
|
8016
|
+
visitor_uid: this.session.visitorUid,
|
|
8017
|
+
session_id: this.session.sessionId
|
|
8018
|
+
}
|
|
8019
|
+
).catch(() => {
|
|
8020
|
+
});
|
|
8021
|
+
}
|
|
8022
|
+
}
|
|
8023
|
+
/** sendBeacon으로 하트비트 전송 (unload/visibility hidden용) */
|
|
8024
|
+
sendHeartbeatBeacon() {
|
|
8025
|
+
if (!this.storageWebId) return;
|
|
8026
|
+
if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
|
|
8027
|
+
const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
8028
|
+
const baseUrl = this.http.getBaseUrl();
|
|
8029
|
+
const url = `${baseUrl}${prefix}/storages/web/${this.storageWebId}/sessions/heartbeat`;
|
|
8030
|
+
const body = JSON.stringify({
|
|
8031
|
+
visitor_uid: this.session.visitorUid,
|
|
8032
|
+
session_id: this.session.sessionId
|
|
8033
|
+
});
|
|
8034
|
+
try {
|
|
8035
|
+
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
8036
|
+
} catch {
|
|
8037
|
+
}
|
|
8038
|
+
}
|
|
8039
|
+
setupAutoPageView() {
|
|
8040
|
+
this.popstateHandler = () => this.trackPageView();
|
|
8041
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
8042
|
+
this.origPushState = history.pushState.bind(history);
|
|
8043
|
+
this.origReplaceState = history.replaceState.bind(history);
|
|
8044
|
+
const self = this;
|
|
8045
|
+
history.pushState = function(...args) {
|
|
8046
|
+
self.origPushState(...args);
|
|
8047
|
+
self.trackPageView();
|
|
8048
|
+
};
|
|
8049
|
+
history.replaceState = function(...args) {
|
|
8050
|
+
self.origReplaceState(...args);
|
|
8051
|
+
self.trackPageView();
|
|
8052
|
+
};
|
|
8053
|
+
}
|
|
8054
|
+
removeAutoPageView() {
|
|
8055
|
+
if (this.popstateHandler) {
|
|
8056
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
8057
|
+
this.popstateHandler = null;
|
|
8058
|
+
}
|
|
8059
|
+
if (this.origPushState) {
|
|
8060
|
+
history.pushState = this.origPushState;
|
|
8061
|
+
this.origPushState = null;
|
|
8062
|
+
}
|
|
8063
|
+
if (this.origReplaceState) {
|
|
8064
|
+
history.replaceState = this.origReplaceState;
|
|
8065
|
+
this.origReplaceState = null;
|
|
8066
|
+
}
|
|
8067
|
+
}
|
|
8068
|
+
removeHeatmapListeners() {
|
|
8069
|
+
if (this.heatmapClickHandler) {
|
|
8070
|
+
document.removeEventListener("click", this.heatmapClickHandler);
|
|
8071
|
+
this.heatmapClickHandler = null;
|
|
8072
|
+
}
|
|
8073
|
+
if (this.heatmapScrollHandler) {
|
|
8074
|
+
window.removeEventListener("scroll", this.heatmapScrollHandler);
|
|
8075
|
+
this.heatmapScrollHandler = null;
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
recordHeatmapEvent(eventType, xPercent, yPercent, scrollDepth) {
|
|
8079
|
+
if (!this.canTrack() || !this.storageWebId) return;
|
|
8080
|
+
const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
|
|
8081
|
+
this.heatmapQueue.push({
|
|
8082
|
+
page_path: window.location.pathname,
|
|
8083
|
+
event_type: eventType,
|
|
8084
|
+
x_percent: Math.round(xPercent * 100) / 100,
|
|
8085
|
+
y_percent: Math.round(yPercent * 100) / 100,
|
|
8086
|
+
viewport_width: window.innerWidth,
|
|
8087
|
+
viewport_height: window.innerHeight,
|
|
8088
|
+
scroll_depth_percent: scrollDepth,
|
|
8089
|
+
session_id: this.session.sessionId
|
|
8090
|
+
});
|
|
8091
|
+
if (this.heatmapQueue.length >= 50) {
|
|
8092
|
+
const events = this.heatmapQueue.splice(0);
|
|
8093
|
+
this.http.post(`${prefix}/storages/web/${this.storageWebId}/heatmap/batch`, {
|
|
8094
|
+
visitor_uid: this.session.visitorUid,
|
|
8095
|
+
events
|
|
8096
|
+
}).catch(() => {
|
|
8097
|
+
});
|
|
8098
|
+
}
|
|
8099
|
+
}
|
|
8100
|
+
log(...args) {
|
|
8101
|
+
if (this.config.debug) {
|
|
8102
|
+
console.log("[Analytics]", ...args);
|
|
8103
|
+
}
|
|
8104
|
+
}
|
|
8105
|
+
};
|
|
8106
|
+
|
|
7462
8107
|
// src/api/game-transport.ts
|
|
7463
8108
|
var WebTransportTransport = class {
|
|
7464
8109
|
constructor(config, onMessage, onClose, onError) {
|
|
@@ -8114,6 +8759,7 @@ var ConnectBase = class {
|
|
|
8114
8759
|
baseUrl: config.baseUrl || DEFAULT_BASE_URL,
|
|
8115
8760
|
publicKey: config.publicKey,
|
|
8116
8761
|
secretKey: config.secretKey,
|
|
8762
|
+
persistence: config.persistence,
|
|
8117
8763
|
onTokenRefresh: config.onTokenRefresh,
|
|
8118
8764
|
onAuthError: config.onAuthError,
|
|
8119
8765
|
onTokenExpired: config.onTokenExpired
|
|
@@ -8138,6 +8784,7 @@ var ConnectBase = class {
|
|
|
8138
8784
|
this.knowledge = new KnowledgeAPI(this.http);
|
|
8139
8785
|
this.ai = new AIAPI(this.http);
|
|
8140
8786
|
this.queue = new QueueAPI(this.http);
|
|
8787
|
+
this.analytics = new AnalyticsAPI(this.http);
|
|
8141
8788
|
}
|
|
8142
8789
|
/**
|
|
8143
8790
|
* 수동으로 토큰 설정 (기존 토큰으로 세션 복원 시)
|
|
@@ -8170,6 +8817,7 @@ var index_default = ConnectBase;
|
|
|
8170
8817
|
GameRoom,
|
|
8171
8818
|
GameRoomTransport,
|
|
8172
8819
|
NativeAPI,
|
|
8820
|
+
SessionManager,
|
|
8173
8821
|
VideoProcessingError,
|
|
8174
8822
|
isWebTransportSupported
|
|
8175
8823
|
});
|