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/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.config.accessToken = data.access_token;
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
  });