connectbase-client 0.13.0 → 0.15.0

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
@@ -7422,6 +7422,594 @@ var QueueAPI = class {
7422
7422
  }
7423
7423
  };
7424
7424
 
7425
+ // src/api/analytics.ts
7426
+ var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
7427
+ var SESSION_KEY = "__cb_session";
7428
+ var VISITOR_KEY = "__cb_visitor_uid";
7429
+ var LAST_ACTIVITY_KEY = "__cb_last_activity";
7430
+ function generateId() {
7431
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
7432
+ return crypto.randomUUID();
7433
+ }
7434
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
7435
+ const r = Math.random() * 16 | 0;
7436
+ const v = c === "x" ? r : r & 3 | 8;
7437
+ return v.toString(16);
7438
+ });
7439
+ }
7440
+ var SessionManager = class {
7441
+ constructor() {
7442
+ this._sessionId = null;
7443
+ this._visitorUid = null;
7444
+ this._lastActivity = 0;
7445
+ this._isNewSession = false;
7446
+ }
7447
+ get sessionId() {
7448
+ this.ensureSession();
7449
+ return this._sessionId;
7450
+ }
7451
+ get visitorUid() {
7452
+ if (!this._visitorUid) {
7453
+ this._visitorUid = this.loadOrCreateVisitorUid();
7454
+ }
7455
+ return this._visitorUid;
7456
+ }
7457
+ get isNewSession() {
7458
+ return this._isNewSession;
7459
+ }
7460
+ /** 활동 기록 — 세션 타임아웃 리셋 */
7461
+ touch() {
7462
+ this._lastActivity = Date.now();
7463
+ this._isNewSession = false;
7464
+ try {
7465
+ if (typeof sessionStorage !== "undefined") {
7466
+ sessionStorage.setItem(LAST_ACTIVITY_KEY, String(this._lastActivity));
7467
+ }
7468
+ } catch {
7469
+ }
7470
+ }
7471
+ /** 세션 강제 리셋 */
7472
+ reset() {
7473
+ this._sessionId = null;
7474
+ this._isNewSession = false;
7475
+ try {
7476
+ if (typeof sessionStorage !== "undefined") {
7477
+ sessionStorage.removeItem(SESSION_KEY);
7478
+ sessionStorage.removeItem(LAST_ACTIVITY_KEY);
7479
+ }
7480
+ } catch {
7481
+ }
7482
+ }
7483
+ ensureSession() {
7484
+ const now = Date.now();
7485
+ if (!this._sessionId) {
7486
+ try {
7487
+ if (typeof sessionStorage !== "undefined") {
7488
+ this._sessionId = sessionStorage.getItem(SESSION_KEY);
7489
+ const lastStr = sessionStorage.getItem(LAST_ACTIVITY_KEY);
7490
+ this._lastActivity = lastStr ? parseInt(lastStr, 10) : 0;
7491
+ }
7492
+ } catch {
7493
+ }
7494
+ }
7495
+ if (!this._sessionId || this._lastActivity > 0 && now - this._lastActivity > SESSION_TIMEOUT_MS) {
7496
+ this._sessionId = generateId();
7497
+ this._isNewSession = true;
7498
+ this._lastActivity = now;
7499
+ try {
7500
+ if (typeof sessionStorage !== "undefined") {
7501
+ sessionStorage.setItem(SESSION_KEY, this._sessionId);
7502
+ sessionStorage.setItem(LAST_ACTIVITY_KEY, String(now));
7503
+ }
7504
+ } catch {
7505
+ }
7506
+ }
7507
+ }
7508
+ loadOrCreateVisitorUid() {
7509
+ try {
7510
+ if (typeof localStorage !== "undefined") {
7511
+ const existing = localStorage.getItem(VISITOR_KEY);
7512
+ if (existing) return existing;
7513
+ const uid = generateId();
7514
+ localStorage.setItem(VISITOR_KEY, uid);
7515
+ return uid;
7516
+ }
7517
+ } catch {
7518
+ }
7519
+ return generateId();
7520
+ }
7521
+ };
7522
+ function parseUTM() {
7523
+ if (typeof window === "undefined") return {};
7524
+ try {
7525
+ const params = new URLSearchParams(window.location.search);
7526
+ const utm = {};
7527
+ for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]) {
7528
+ const val = params.get(key);
7529
+ if (val) utm[key] = val;
7530
+ }
7531
+ return utm;
7532
+ } catch {
7533
+ return {};
7534
+ }
7535
+ }
7536
+ var AnalyticsAPI = class {
7537
+ constructor(http) {
7538
+ this.storageWebId = null;
7539
+ this.eventQueue = [];
7540
+ this.batchTimer = null;
7541
+ this.isInitialized = false;
7542
+ this.heartbeatTimer = null;
7543
+ this.visibilityHandler = null;
7544
+ this.unloadHeartbeatHandler = null;
7545
+ this.popstateHandler = null;
7546
+ this.beforeUnloadHandler = null;
7547
+ this.origPushState = null;
7548
+ this.origReplaceState = null;
7549
+ this.heatmapClickHandler = null;
7550
+ this.heatmapScrollHandler = null;
7551
+ this.utm = {};
7552
+ this.handleHeatmapClick = (e) => {
7553
+ const xPercent = e.clientX / window.innerWidth * 100;
7554
+ const yPercent = e.clientY / window.innerHeight * 100;
7555
+ this.recordHeatmapEvent("click", xPercent, yPercent);
7556
+ };
7557
+ this.heatmapQueue = [];
7558
+ this.http = http;
7559
+ this.config = {
7560
+ trackPageViews: true,
7561
+ trackEvents: true,
7562
+ trackSessions: true,
7563
+ heatmap: false,
7564
+ recording: false,
7565
+ batchSize: 10,
7566
+ flushInterval: 5e3,
7567
+ respectDoNotTrack: true,
7568
+ debug: false
7569
+ };
7570
+ this.consent = {
7571
+ analytics: true,
7572
+ heatmap: false,
7573
+ recording: false
7574
+ };
7575
+ this.session = new SessionManager();
7576
+ }
7577
+ /**
7578
+ * Analytics 초기화
7579
+ * @param storageWebId 웹 스토리지 ID
7580
+ * @param config 설정 (선택)
7581
+ */
7582
+ init(storageWebId, config) {
7583
+ if (this.isInitialized) {
7584
+ this.log("Analytics already initialized");
7585
+ return;
7586
+ }
7587
+ if (typeof window === "undefined") {
7588
+ this.log("Analytics only works in browser environment");
7589
+ return;
7590
+ }
7591
+ if (config?.respectDoNotTrack !== false && this.isDNT()) {
7592
+ this.log("Do Not Track enabled, analytics disabled");
7593
+ return;
7594
+ }
7595
+ this.storageWebId = storageWebId;
7596
+ Object.assign(this.config, config);
7597
+ this.isInitialized = true;
7598
+ this.utm = parseUTM();
7599
+ if (this.config.trackSessions) {
7600
+ this.trackSessionStart();
7601
+ this.startHeartbeat();
7602
+ }
7603
+ if (this.config.trackPageViews) {
7604
+ this.trackPageView();
7605
+ this.setupAutoPageView();
7606
+ }
7607
+ this.startBatchTimer();
7608
+ this.beforeUnloadHandler = () => this.flushSync();
7609
+ window.addEventListener("beforeunload", this.beforeUnloadHandler);
7610
+ this.log("Analytics initialized", { storageWebId });
7611
+ }
7612
+ /** Analytics 정리 */
7613
+ destroy() {
7614
+ if (!this.isInitialized) return;
7615
+ this.stopBatchTimer();
7616
+ this.stopHeartbeat();
7617
+ this.removeAutoPageView();
7618
+ this.removeHeatmapListeners();
7619
+ if (this.beforeUnloadHandler) {
7620
+ window.removeEventListener("beforeunload", this.beforeUnloadHandler);
7621
+ this.beforeUnloadHandler = null;
7622
+ }
7623
+ this.flush();
7624
+ this.isInitialized = false;
7625
+ this.log("Analytics destroyed");
7626
+ }
7627
+ /**
7628
+ * 동의 설정 변경
7629
+ */
7630
+ setConsent(consent) {
7631
+ Object.assign(this.consent, consent);
7632
+ this.log("Consent updated", consent);
7633
+ if (consent.analytics === false && this.isInitialized) {
7634
+ this.destroy();
7635
+ }
7636
+ }
7637
+ /** 현재 동의 상태 조회 */
7638
+ getConsent() {
7639
+ return { ...this.consent };
7640
+ }
7641
+ /**
7642
+ * 페이지뷰 수동 추적
7643
+ */
7644
+ trackPageView(path) {
7645
+ if (!this.canTrack()) return;
7646
+ this.session.touch();
7647
+ const event = this.createBaseEvent("page_view");
7648
+ event.page_path = path || window.location.pathname;
7649
+ event.page_url = window.location.href;
7650
+ event.page_title = document.title;
7651
+ event.referrer = document.referrer || void 0;
7652
+ event.screen_width = window.screen.width;
7653
+ event.screen_height = window.screen.height;
7654
+ Object.assign(event, this.utm);
7655
+ this.enqueue(event);
7656
+ }
7657
+ /**
7658
+ * 커스텀 이벤트 추적
7659
+ */
7660
+ trackEvent(name, properties) {
7661
+ if (!this.canTrack() || !this.config.trackEvents) return;
7662
+ this.session.touch();
7663
+ const event = this.createBaseEvent("event");
7664
+ event.event_name = name;
7665
+ event.event_properties = properties;
7666
+ event.page_path = window.location.pathname;
7667
+ event.page_url = window.location.href;
7668
+ this.enqueue(event);
7669
+ }
7670
+ /**
7671
+ * 사용자 식별 (로그인 시)
7672
+ */
7673
+ identify(memberId) {
7674
+ if (!this.canTrack()) return;
7675
+ if (typeof window.__cbSetMember === "function") {
7676
+ window.__cbSetMember(memberId);
7677
+ }
7678
+ this.log("User identified", { memberId });
7679
+ }
7680
+ /**
7681
+ * 히트맵 수집 활성화 (opt-in)
7682
+ */
7683
+ enableHeatmap(options) {
7684
+ if (!this.canTrack() || !this.consent.heatmap) return;
7685
+ const trackClick = options?.click ?? true;
7686
+ const trackScroll = options?.scroll ?? true;
7687
+ if (trackClick) {
7688
+ this.heatmapClickHandler = this.handleHeatmapClick;
7689
+ document.addEventListener("click", this.heatmapClickHandler);
7690
+ }
7691
+ if (trackScroll) {
7692
+ let scrollTimer = null;
7693
+ let maxScrollDepth = 0;
7694
+ this.heatmapScrollHandler = () => {
7695
+ const depth = Math.round(
7696
+ (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
7697
+ );
7698
+ maxScrollDepth = Math.max(maxScrollDepth, depth);
7699
+ if (scrollTimer) clearTimeout(scrollTimer);
7700
+ scrollTimer = setTimeout(() => {
7701
+ this.recordHeatmapEvent("scroll", 50, maxScrollDepth * (window.innerHeight / 100), maxScrollDepth);
7702
+ }, 500);
7703
+ };
7704
+ window.addEventListener("scroll", this.heatmapScrollHandler, { passive: true });
7705
+ }
7706
+ this.log("Heatmap enabled", options);
7707
+ }
7708
+ /**
7709
+ * 세션 heartbeat 자동 전송 시작 (30초 간격)
7710
+ */
7711
+ enableHeartbeat() {
7712
+ if (!this.canTrack()) return;
7713
+ if (this.heartbeatTimer) return;
7714
+ const sendHeartbeat = () => {
7715
+ if (!this.canTrack()) return;
7716
+ const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
7717
+ this.http.post(`${prefix}/storages/web/${this.storageWebId}/sessions/heartbeat`, {
7718
+ visitor_uid: this.session.visitorUid,
7719
+ session_id: this.session.sessionId
7720
+ }).catch(() => {
7721
+ });
7722
+ };
7723
+ this.heartbeatTimer = setInterval(sendHeartbeat, 3e4);
7724
+ document.addEventListener("visibilitychange", () => {
7725
+ if (document.hidden) {
7726
+ if (this.heartbeatTimer) {
7727
+ clearInterval(this.heartbeatTimer);
7728
+ this.heartbeatTimer = null;
7729
+ }
7730
+ } else {
7731
+ if (!this.heartbeatTimer) {
7732
+ this.heartbeatTimer = setInterval(sendHeartbeat, 3e4);
7733
+ sendHeartbeat();
7734
+ }
7735
+ }
7736
+ });
7737
+ window.addEventListener("beforeunload", () => {
7738
+ if (typeof navigator !== "undefined" && navigator.sendBeacon && this.storageWebId) {
7739
+ const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
7740
+ const url = `${this.http.getBaseUrl()}${prefix}/storages/web/${this.storageWebId}/sessions/heartbeat`;
7741
+ navigator.sendBeacon(url, JSON.stringify({
7742
+ visitor_uid: this.session.visitorUid,
7743
+ session_id: this.session.sessionId
7744
+ }));
7745
+ }
7746
+ });
7747
+ sendHeartbeat();
7748
+ this.log("Heartbeat enabled (30s interval)");
7749
+ }
7750
+ /** 큐에 있는 이벤트 즉시 전송 */
7751
+ async flush() {
7752
+ await this.flushQueue();
7753
+ }
7754
+ /** 세션 매니저 접근 (고급) */
7755
+ getSession() {
7756
+ return this.session;
7757
+ }
7758
+ // ── Private ────────────────────────────────────────────────────
7759
+ canTrack() {
7760
+ if (!this.isInitialized || !this.storageWebId) return false;
7761
+ if (!this.consent.analytics) return false;
7762
+ return true;
7763
+ }
7764
+ isDNT() {
7765
+ if (typeof navigator === "undefined") return false;
7766
+ return navigator.doNotTrack === "1" || navigator.globalPrivacyControl === true;
7767
+ }
7768
+ createBaseEvent(type) {
7769
+ return {
7770
+ type,
7771
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7772
+ session_id: this.session.sessionId,
7773
+ visitor_uid: this.session.visitorUid
7774
+ };
7775
+ }
7776
+ enqueue(event) {
7777
+ this.eventQueue.push(event);
7778
+ if (this.eventQueue.length >= this.config.batchSize) {
7779
+ this.flushQueue();
7780
+ }
7781
+ }
7782
+ async flushQueue() {
7783
+ if (this.eventQueue.length === 0 || !this.storageWebId) return;
7784
+ const events = this.eventQueue.splice(0);
7785
+ const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
7786
+ try {
7787
+ await this.http.post(
7788
+ `${prefix}/storages/web/${this.storageWebId}/visitors/batch`,
7789
+ {
7790
+ visitor_uid: this.session.visitorUid,
7791
+ events: events.map((e) => ({
7792
+ timestamp: e.timestamp,
7793
+ page_path: e.page_path || "",
7794
+ page_url: e.page_url || "",
7795
+ page_title: e.page_title || "",
7796
+ referrer: e.referrer || "",
7797
+ user_agent: typeof navigator !== "undefined" ? navigator.userAgent : "",
7798
+ screen_width: e.screen_width || 0,
7799
+ screen_height: e.screen_height || 0,
7800
+ session_id: e.session_id,
7801
+ session_start: e.type === "session_start",
7802
+ is_page_view: e.type === "page_view",
7803
+ event_name: e.event_name,
7804
+ event_properties: e.event_properties,
7805
+ utm_source: e.utm_source,
7806
+ utm_medium: e.utm_medium,
7807
+ utm_campaign: e.utm_campaign,
7808
+ utm_content: e.utm_content,
7809
+ utm_term: e.utm_term
7810
+ }))
7811
+ }
7812
+ );
7813
+ this.log(`Flushed ${events.length} events`);
7814
+ } catch (err) {
7815
+ if (this.eventQueue.length < this.config.batchSize * 3) {
7816
+ this.eventQueue.unshift(...events);
7817
+ }
7818
+ this.log("Flush failed", err);
7819
+ }
7820
+ }
7821
+ /** sendBeacon으로 동기 flush (beforeunload용) */
7822
+ flushSync() {
7823
+ if (this.eventQueue.length === 0 || !this.storageWebId) return;
7824
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
7825
+ const events = this.eventQueue.splice(0);
7826
+ const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
7827
+ const baseUrl = this.http.getBaseUrl();
7828
+ const url = `${baseUrl}${prefix}/storages/web/${this.storageWebId}/visitors/batch`;
7829
+ const body = JSON.stringify({
7830
+ visitor_uid: this.session.visitorUid,
7831
+ events: events.map((e) => ({
7832
+ timestamp: e.timestamp,
7833
+ page_path: e.page_path || "",
7834
+ page_url: e.page_url || "",
7835
+ page_title: e.page_title || "",
7836
+ referrer: e.referrer || "",
7837
+ session_id: e.session_id,
7838
+ session_start: e.type === "session_start",
7839
+ is_page_view: e.type === "page_view",
7840
+ event_name: e.event_name,
7841
+ event_properties: e.event_properties
7842
+ }))
7843
+ });
7844
+ try {
7845
+ navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
7846
+ } catch {
7847
+ }
7848
+ }
7849
+ trackSessionStart() {
7850
+ const event = this.createBaseEvent("session_start");
7851
+ event.page_path = window.location.pathname;
7852
+ event.page_url = window.location.href;
7853
+ event.referrer = document.referrer || void 0;
7854
+ event.screen_width = window.screen.width;
7855
+ event.screen_height = window.screen.height;
7856
+ Object.assign(event, this.utm);
7857
+ this.enqueue(event);
7858
+ }
7859
+ startBatchTimer() {
7860
+ this.batchTimer = setInterval(() => this.flushQueue(), this.config.flushInterval);
7861
+ }
7862
+ stopBatchTimer() {
7863
+ if (this.batchTimer) {
7864
+ clearInterval(this.batchTimer);
7865
+ this.batchTimer = null;
7866
+ }
7867
+ }
7868
+ startHeartbeat() {
7869
+ this.heartbeatTimer = setInterval(() => {
7870
+ if (!this.canTrack()) return;
7871
+ this.sendHeartbeat();
7872
+ }, 30 * 1e3);
7873
+ if (typeof document !== "undefined") {
7874
+ this.visibilityHandler = () => {
7875
+ if (document.visibilityState === "hidden") {
7876
+ this.sendHeartbeatBeacon();
7877
+ if (this.heartbeatTimer) {
7878
+ clearInterval(this.heartbeatTimer);
7879
+ this.heartbeatTimer = null;
7880
+ }
7881
+ } else if (document.visibilityState === "visible") {
7882
+ if (!this.heartbeatTimer) {
7883
+ this.sendHeartbeat();
7884
+ this.heartbeatTimer = setInterval(() => {
7885
+ if (!this.canTrack()) return;
7886
+ this.sendHeartbeat();
7887
+ }, 30 * 1e3);
7888
+ }
7889
+ }
7890
+ };
7891
+ document.addEventListener("visibilitychange", this.visibilityHandler);
7892
+ }
7893
+ if (typeof window !== "undefined") {
7894
+ this.unloadHeartbeatHandler = () => this.sendHeartbeatBeacon();
7895
+ window.addEventListener("beforeunload", this.unloadHeartbeatHandler);
7896
+ }
7897
+ }
7898
+ stopHeartbeat() {
7899
+ if (this.heartbeatTimer) {
7900
+ clearInterval(this.heartbeatTimer);
7901
+ this.heartbeatTimer = null;
7902
+ }
7903
+ if (typeof document !== "undefined" && this.visibilityHandler) {
7904
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
7905
+ this.visibilityHandler = null;
7906
+ }
7907
+ if (typeof window !== "undefined" && this.unloadHeartbeatHandler) {
7908
+ window.removeEventListener("beforeunload", this.unloadHeartbeatHandler);
7909
+ this.unloadHeartbeatHandler = null;
7910
+ }
7911
+ }
7912
+ /** 하트비트를 큐에 넣어 배치 전송 */
7913
+ sendHeartbeat() {
7914
+ const event = this.createBaseEvent("heartbeat");
7915
+ event.page_path = window.location.pathname;
7916
+ this.enqueue(event);
7917
+ if (this.storageWebId) {
7918
+ const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
7919
+ this.http.post(
7920
+ `${prefix}/storages/web/${this.storageWebId}/sessions/heartbeat`,
7921
+ {
7922
+ visitor_uid: this.session.visitorUid,
7923
+ session_id: this.session.sessionId
7924
+ }
7925
+ ).catch(() => {
7926
+ });
7927
+ }
7928
+ }
7929
+ /** sendBeacon으로 하트비트 전송 (unload/visibility hidden용) */
7930
+ sendHeartbeatBeacon() {
7931
+ if (!this.storageWebId) return;
7932
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
7933
+ const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
7934
+ const baseUrl = this.http.getBaseUrl();
7935
+ const url = `${baseUrl}${prefix}/storages/web/${this.storageWebId}/sessions/heartbeat`;
7936
+ const body = JSON.stringify({
7937
+ visitor_uid: this.session.visitorUid,
7938
+ session_id: this.session.sessionId
7939
+ });
7940
+ try {
7941
+ navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
7942
+ } catch {
7943
+ }
7944
+ }
7945
+ setupAutoPageView() {
7946
+ this.popstateHandler = () => this.trackPageView();
7947
+ window.addEventListener("popstate", this.popstateHandler);
7948
+ this.origPushState = history.pushState.bind(history);
7949
+ this.origReplaceState = history.replaceState.bind(history);
7950
+ const self = this;
7951
+ history.pushState = function(...args) {
7952
+ self.origPushState(...args);
7953
+ self.trackPageView();
7954
+ };
7955
+ history.replaceState = function(...args) {
7956
+ self.origReplaceState(...args);
7957
+ self.trackPageView();
7958
+ };
7959
+ }
7960
+ removeAutoPageView() {
7961
+ if (this.popstateHandler) {
7962
+ window.removeEventListener("popstate", this.popstateHandler);
7963
+ this.popstateHandler = null;
7964
+ }
7965
+ if (this.origPushState) {
7966
+ history.pushState = this.origPushState;
7967
+ this.origPushState = null;
7968
+ }
7969
+ if (this.origReplaceState) {
7970
+ history.replaceState = this.origReplaceState;
7971
+ this.origReplaceState = null;
7972
+ }
7973
+ }
7974
+ removeHeatmapListeners() {
7975
+ if (this.heatmapClickHandler) {
7976
+ document.removeEventListener("click", this.heatmapClickHandler);
7977
+ this.heatmapClickHandler = null;
7978
+ }
7979
+ if (this.heatmapScrollHandler) {
7980
+ window.removeEventListener("scroll", this.heatmapScrollHandler);
7981
+ this.heatmapScrollHandler = null;
7982
+ }
7983
+ }
7984
+ recordHeatmapEvent(eventType, xPercent, yPercent, scrollDepth) {
7985
+ if (!this.canTrack() || !this.storageWebId) return;
7986
+ const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
7987
+ this.heatmapQueue.push({
7988
+ page_path: window.location.pathname,
7989
+ event_type: eventType,
7990
+ x_percent: Math.round(xPercent * 100) / 100,
7991
+ y_percent: Math.round(yPercent * 100) / 100,
7992
+ viewport_width: window.innerWidth,
7993
+ viewport_height: window.innerHeight,
7994
+ scroll_depth_percent: scrollDepth,
7995
+ session_id: this.session.sessionId
7996
+ });
7997
+ if (this.heatmapQueue.length >= 50) {
7998
+ const events = this.heatmapQueue.splice(0);
7999
+ this.http.post(`${prefix}/storages/web/${this.storageWebId}/heatmap/batch`, {
8000
+ visitor_uid: this.session.visitorUid,
8001
+ events
8002
+ }).catch(() => {
8003
+ });
8004
+ }
8005
+ }
8006
+ log(...args) {
8007
+ if (this.config.debug) {
8008
+ console.log("[Analytics]", ...args);
8009
+ }
8010
+ }
8011
+ };
8012
+
7425
8013
  // src/api/game-transport.ts
7426
8014
  var WebTransportTransport = class {
7427
8015
  constructor(config, onMessage, onClose, onError) {
@@ -8101,6 +8689,7 @@ var ConnectBase = class {
8101
8689
  this.knowledge = new KnowledgeAPI(this.http);
8102
8690
  this.ai = new AIAPI(this.http);
8103
8691
  this.queue = new QueueAPI(this.http);
8692
+ this.analytics = new AnalyticsAPI(this.http);
8104
8693
  }
8105
8694
  /**
8106
8695
  * 수동으로 토큰 설정 (기존 토큰으로 세션 복원 시)
@@ -8132,6 +8721,7 @@ export {
8132
8721
  GameRoom,
8133
8722
  GameRoomTransport,
8134
8723
  NativeAPI,
8724
+ SessionManager,
8135
8725
  VideoProcessingError,
8136
8726
  index_default as default,
8137
8727
  isWebTransportSupported
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,6 +33,7 @@
33
33
  "build": "tsup",
34
34
  "dev": "tsup --watch",
35
35
  "typecheck": "tsc --noEmit",
36
+ "test": "vitest run",
36
37
  "test:types": "tsc --noEmit -p test/tsconfig.json",
37
38
  "release": "pnpm build && npm publish --access public"
38
39
  },
@@ -55,7 +56,8 @@
55
56
  "devDependencies": {
56
57
  "@types/node": "^22.15.18",
57
58
  "tsup": "^8.5.1",
58
- "typescript": "^5.9.3"
59
+ "typescript": "^5.9.3",
60
+ "vitest": "^3.2.4"
59
61
  },
60
62
  "engines": {
61
63
  "node": ">=16"