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