connectbase-client 3.3.0 → 3.4.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.d.mts CHANGED
@@ -314,6 +314,18 @@ declare class SessionManager {
314
314
  touch(): void;
315
315
  /** 세션 강제 리셋 */
316
316
  reset(): void;
317
+ /**
318
+ * visitor_uid 를 새로 발급하고 세션도 함께 초기화.
319
+ *
320
+ * 사용 시점:
321
+ * - 사용자 로그아웃 (이전 사용자 활동이 다음 사용자에 attribution 되는 것 방지)
322
+ * - 다른 사용자 로그인 감지 (link-member 가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답)
323
+ *
324
+ * localStorage 의 `__cb_visitor_uid` 가 새 UUID 로 교체되며 sessionStorage 의
325
+ * 세션 키도 같이 비워진다. 이후의 모든 batch 는 새 visitor 로 기록되어 멤버 간
326
+ * 데이터 오염이 차단된다.
327
+ */
328
+ regenerateVisitorUid(): string;
317
329
  private ensureSession;
318
330
  private loadOrCreateVisitorUid;
319
331
  }
@@ -368,6 +380,11 @@ declare class AnalyticsAPI {
368
380
  * 백엔드 `link-member` 엔드포인트를 한 번 호출합니다 (1.11.0+). 호출 실패는
369
381
  * silent — 다음 batch 가 닿을 때 백엔드 자동 매핑이 동일하게 처리하므로 자가 복구.
370
382
  *
383
+ * **사용자 전환 자동 처리 (1.13.0+)**: 동일 브라우저에서 다른 사용자가 로그인해
384
+ * 백엔드가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답을 보내면, 큐를 비우고
385
+ * `visitor_uid` 를 새로 발급한 뒤 link-member 를 한 번 더 호출해 새 visitor 가
386
+ * 즉시 회원으로 기록되도록 자가 복구한다.
387
+ *
371
388
  * 산업 표준(GA4 User-ID, Mixpanel/PostHog `identify`) 과 동작 정합.
372
389
  *
373
390
  * @example
@@ -378,11 +395,30 @@ declare class AnalyticsAPI {
378
395
  * ```
379
396
  */
380
397
  identify(memberId: string): void;
398
+ /**
399
+ * 로그아웃 / 사용자 전환 시 호출. 익명 상태로 복귀하면서 visitor_uid 를 새로
400
+ * 발급해 다음 사용자의 활동이 이전 사용자에게 attribution 되는 데이터 오염을 차단.
401
+ *
402
+ * 동작:
403
+ * 1. memberId 를 null 로 설정 (이후 batch 는 익명으로 기록)
404
+ * 2. 큐에 쌓인 미전송 이벤트 폐기 (이전 visitor 로 가는 것을 막기 위함)
405
+ * 3. localStorage `__cb_visitor_uid` 새 UUID 로 교체 + sessionStorage 세션 키 정리
406
+ * 4. heatmap 큐도 함께 비움
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * // 로그아웃 핸들러
411
+ * await cb.auth.signOut()
412
+ * cb.analytics.reset()
413
+ * ```
414
+ */
415
+ reset(): void;
381
416
  /**
382
417
  * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출).
383
418
  *
384
419
  * `identify()` 와 달리 즉시 backfill 호출은 하지 않습니다 — 단순히 이후 batch 의
385
- * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃).
420
+ * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃 시에는
421
+ * 데이터 오염 방지를 위해 `reset()` 를 권장).
386
422
  */
387
423
  setMemberId(memberId: string | null): void;
388
424
  /**
@@ -390,6 +426,9 @@ declare class AnalyticsAPI {
390
426
  *
391
427
  * - 첫 페이지뷰가 아직 백엔드에 닿기 전이면 visitor 가 없어 404. 무시 — 다음 batch 가
392
428
  * 가면 백엔드 BatchRecordVisit 가 자동 LinkMember 를 호출.
429
+ * - 다른 멤버에 이미 연결된 visitor 인 경우 (`VISITOR_LINKED_TO_OTHER_MEMBER`)
430
+ * visitor_uid 를 자동 재발급하고 link-member 를 한 번 더 호출 — 한 단계의 자가
431
+ * 복구로 끝나며 무한 재귀를 막기 위해 두 번째 시도는 응답을 더 보지 않는다.
393
432
  * - storage_web_id 가 init 안 됐거나 모든 종류의 네트워크 오류 — silent fail.
394
433
  */
395
434
  private linkMemberSilent;
@@ -510,7 +549,13 @@ declare class AnalyticsAPI {
510
549
  private createBaseEvent;
511
550
  private enqueue;
512
551
  private flushQueue;
513
- /** sendBeacon으로 동기 flush (beforeunload용) */
552
+ /**
553
+ * sendBeacon 으로 동기 flush (beforeunload 용).
554
+ *
555
+ * sendBeacon 은 일반 fetch 와 달리 User-Agent 헤더가 신뢰할 수 있지만, 백엔드 봇
556
+ * 탐지가 body 의 `user_agent` 필드 첫 번째 값만 보므로 여기서도 동일하게 채워
557
+ * 첫 방문이 unload 타이밍에 도달했을 때 봇으로 오판되는 것을 방지한다.
558
+ */
514
559
  private flushSync;
515
560
  private trackSessionStart;
516
561
  private startBatchTimer;
package/dist/index.d.ts CHANGED
@@ -314,6 +314,18 @@ declare class SessionManager {
314
314
  touch(): void;
315
315
  /** 세션 강제 리셋 */
316
316
  reset(): void;
317
+ /**
318
+ * visitor_uid 를 새로 발급하고 세션도 함께 초기화.
319
+ *
320
+ * 사용 시점:
321
+ * - 사용자 로그아웃 (이전 사용자 활동이 다음 사용자에 attribution 되는 것 방지)
322
+ * - 다른 사용자 로그인 감지 (link-member 가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답)
323
+ *
324
+ * localStorage 의 `__cb_visitor_uid` 가 새 UUID 로 교체되며 sessionStorage 의
325
+ * 세션 키도 같이 비워진다. 이후의 모든 batch 는 새 visitor 로 기록되어 멤버 간
326
+ * 데이터 오염이 차단된다.
327
+ */
328
+ regenerateVisitorUid(): string;
317
329
  private ensureSession;
318
330
  private loadOrCreateVisitorUid;
319
331
  }
@@ -368,6 +380,11 @@ declare class AnalyticsAPI {
368
380
  * 백엔드 `link-member` 엔드포인트를 한 번 호출합니다 (1.11.0+). 호출 실패는
369
381
  * silent — 다음 batch 가 닿을 때 백엔드 자동 매핑이 동일하게 처리하므로 자가 복구.
370
382
  *
383
+ * **사용자 전환 자동 처리 (1.13.0+)**: 동일 브라우저에서 다른 사용자가 로그인해
384
+ * 백엔드가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답을 보내면, 큐를 비우고
385
+ * `visitor_uid` 를 새로 발급한 뒤 link-member 를 한 번 더 호출해 새 visitor 가
386
+ * 즉시 회원으로 기록되도록 자가 복구한다.
387
+ *
371
388
  * 산업 표준(GA4 User-ID, Mixpanel/PostHog `identify`) 과 동작 정합.
372
389
  *
373
390
  * @example
@@ -378,11 +395,30 @@ declare class AnalyticsAPI {
378
395
  * ```
379
396
  */
380
397
  identify(memberId: string): void;
398
+ /**
399
+ * 로그아웃 / 사용자 전환 시 호출. 익명 상태로 복귀하면서 visitor_uid 를 새로
400
+ * 발급해 다음 사용자의 활동이 이전 사용자에게 attribution 되는 데이터 오염을 차단.
401
+ *
402
+ * 동작:
403
+ * 1. memberId 를 null 로 설정 (이후 batch 는 익명으로 기록)
404
+ * 2. 큐에 쌓인 미전송 이벤트 폐기 (이전 visitor 로 가는 것을 막기 위함)
405
+ * 3. localStorage `__cb_visitor_uid` 새 UUID 로 교체 + sessionStorage 세션 키 정리
406
+ * 4. heatmap 큐도 함께 비움
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * // 로그아웃 핸들러
411
+ * await cb.auth.signOut()
412
+ * cb.analytics.reset()
413
+ * ```
414
+ */
415
+ reset(): void;
381
416
  /**
382
417
  * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출).
383
418
  *
384
419
  * `identify()` 와 달리 즉시 backfill 호출은 하지 않습니다 — 단순히 이후 batch 의
385
- * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃).
420
+ * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃 시에는
421
+ * 데이터 오염 방지를 위해 `reset()` 를 권장).
386
422
  */
387
423
  setMemberId(memberId: string | null): void;
388
424
  /**
@@ -390,6 +426,9 @@ declare class AnalyticsAPI {
390
426
  *
391
427
  * - 첫 페이지뷰가 아직 백엔드에 닿기 전이면 visitor 가 없어 404. 무시 — 다음 batch 가
392
428
  * 가면 백엔드 BatchRecordVisit 가 자동 LinkMember 를 호출.
429
+ * - 다른 멤버에 이미 연결된 visitor 인 경우 (`VISITOR_LINKED_TO_OTHER_MEMBER`)
430
+ * visitor_uid 를 자동 재발급하고 link-member 를 한 번 더 호출 — 한 단계의 자가
431
+ * 복구로 끝나며 무한 재귀를 막기 위해 두 번째 시도는 응답을 더 보지 않는다.
393
432
  * - storage_web_id 가 init 안 됐거나 모든 종류의 네트워크 오류 — silent fail.
394
433
  */
395
434
  private linkMemberSilent;
@@ -510,7 +549,13 @@ declare class AnalyticsAPI {
510
549
  private createBaseEvent;
511
550
  private enqueue;
512
551
  private flushQueue;
513
- /** sendBeacon으로 동기 flush (beforeunload용) */
552
+ /**
553
+ * sendBeacon 으로 동기 flush (beforeunload 용).
554
+ *
555
+ * sendBeacon 은 일반 fetch 와 달리 User-Agent 헤더가 신뢰할 수 있지만, 백엔드 봇
556
+ * 탐지가 body 의 `user_agent` 필드 첫 번째 값만 보므로 여기서도 동일하게 채워
557
+ * 첫 방문이 unload 타이밍에 도달했을 때 봇으로 오판되는 것을 방지한다.
558
+ */
514
559
  private flushSync;
515
560
  private trackSessionStart;
516
561
  private startBatchTimer;
package/dist/index.js CHANGED
@@ -8034,6 +8034,29 @@ var SessionManager = class {
8034
8034
  } catch {
8035
8035
  }
8036
8036
  }
8037
+ /**
8038
+ * visitor_uid 를 새로 발급하고 세션도 함께 초기화.
8039
+ *
8040
+ * 사용 시점:
8041
+ * - 사용자 로그아웃 (이전 사용자 활동이 다음 사용자에 attribution 되는 것 방지)
8042
+ * - 다른 사용자 로그인 감지 (link-member 가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답)
8043
+ *
8044
+ * localStorage 의 `__cb_visitor_uid` 가 새 UUID 로 교체되며 sessionStorage 의
8045
+ * 세션 키도 같이 비워진다. 이후의 모든 batch 는 새 visitor 로 기록되어 멤버 간
8046
+ * 데이터 오염이 차단된다.
8047
+ */
8048
+ regenerateVisitorUid() {
8049
+ const newUid = generateId();
8050
+ this._visitorUid = newUid;
8051
+ try {
8052
+ if (typeof localStorage !== "undefined") {
8053
+ localStorage.setItem(VISITOR_KEY, newUid);
8054
+ }
8055
+ } catch {
8056
+ }
8057
+ this.reset();
8058
+ return newUid;
8059
+ }
8037
8060
  ensureSession() {
8038
8061
  const now = Date.now();
8039
8062
  if (!this._sessionId) {
@@ -8239,6 +8262,11 @@ var AnalyticsAPI = class {
8239
8262
  * 백엔드 `link-member` 엔드포인트를 한 번 호출합니다 (1.11.0+). 호출 실패는
8240
8263
  * silent — 다음 batch 가 닿을 때 백엔드 자동 매핑이 동일하게 처리하므로 자가 복구.
8241
8264
  *
8265
+ * **사용자 전환 자동 처리 (1.13.0+)**: 동일 브라우저에서 다른 사용자가 로그인해
8266
+ * 백엔드가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답을 보내면, 큐를 비우고
8267
+ * `visitor_uid` 를 새로 발급한 뒤 link-member 를 한 번 더 호출해 새 visitor 가
8268
+ * 즉시 회원으로 기록되도록 자가 복구한다.
8269
+ *
8242
8270
  * 산업 표준(GA4 User-ID, Mixpanel/PostHog `identify`) 과 동작 정합.
8243
8271
  *
8244
8272
  * @example
@@ -8249,14 +8277,42 @@ var AnalyticsAPI = class {
8249
8277
  * ```
8250
8278
  */
8251
8279
  identify(memberId) {
8280
+ if (this.memberId && this.memberId !== memberId) {
8281
+ this.reset();
8282
+ }
8252
8283
  this.setMemberId(memberId);
8253
8284
  this.linkMemberSilent(memberId);
8254
8285
  }
8286
+ /**
8287
+ * 로그아웃 / 사용자 전환 시 호출. 익명 상태로 복귀하면서 visitor_uid 를 새로
8288
+ * 발급해 다음 사용자의 활동이 이전 사용자에게 attribution 되는 데이터 오염을 차단.
8289
+ *
8290
+ * 동작:
8291
+ * 1. memberId 를 null 로 설정 (이후 batch 는 익명으로 기록)
8292
+ * 2. 큐에 쌓인 미전송 이벤트 폐기 (이전 visitor 로 가는 것을 막기 위함)
8293
+ * 3. localStorage `__cb_visitor_uid` 새 UUID 로 교체 + sessionStorage 세션 키 정리
8294
+ * 4. heatmap 큐도 함께 비움
8295
+ *
8296
+ * @example
8297
+ * ```ts
8298
+ * // 로그아웃 핸들러
8299
+ * await cb.auth.signOut()
8300
+ * cb.analytics.reset()
8301
+ * ```
8302
+ */
8303
+ reset() {
8304
+ this.setMemberId(null);
8305
+ this.eventQueue = [];
8306
+ this.heatmapQueue = [];
8307
+ const newUid = this.session.regenerateVisitorUid();
8308
+ this.log("Analytics reset", { newVisitorUid: newUid });
8309
+ }
8255
8310
  /**
8256
8311
  * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출).
8257
8312
  *
8258
8313
  * `identify()` 와 달리 즉시 backfill 호출은 하지 않습니다 — 단순히 이후 batch 의
8259
- * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃).
8314
+ * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃 시에는
8315
+ * 데이터 오염 방지를 위해 `reset()` 를 권장).
8260
8316
  */
8261
8317
  setMemberId(memberId) {
8262
8318
  this.memberId = memberId || null;
@@ -8278,9 +8334,12 @@ var AnalyticsAPI = class {
8278
8334
  *
8279
8335
  * - 첫 페이지뷰가 아직 백엔드에 닿기 전이면 visitor 가 없어 404. 무시 — 다음 batch 가
8280
8336
  * 가면 백엔드 BatchRecordVisit 가 자동 LinkMember 를 호출.
8337
+ * - 다른 멤버에 이미 연결된 visitor 인 경우 (`VISITOR_LINKED_TO_OTHER_MEMBER`)
8338
+ * visitor_uid 를 자동 재발급하고 link-member 를 한 번 더 호출 — 한 단계의 자가
8339
+ * 복구로 끝나며 무한 재귀를 막기 위해 두 번째 시도는 응답을 더 보지 않는다.
8281
8340
  * - storage_web_id 가 init 안 됐거나 모든 종류의 네트워크 오류 — silent fail.
8282
8341
  */
8283
- linkMemberSilent(memberId) {
8342
+ linkMemberSilent(memberId, isRetry = false) {
8284
8343
  if (!this.storageWebId) return;
8285
8344
  const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
8286
8345
  this.http.post(
@@ -8289,7 +8348,18 @@ var AnalyticsAPI = class {
8289
8348
  visitor_uid: this.session.visitorUid,
8290
8349
  app_member_id: memberId
8291
8350
  }
8292
- ).catch(() => {
8351
+ ).then((resp) => {
8352
+ if (!resp) return;
8353
+ this.log("link-member response", resp);
8354
+ if (!isRetry && resp.success === false && resp.code === "VISITOR_LINKED_TO_OTHER_MEMBER") {
8355
+ this.log("user-switch detected \u2014 regenerating visitor_uid");
8356
+ this.eventQueue = [];
8357
+ this.heatmapQueue = [];
8358
+ this.session.regenerateVisitorUid();
8359
+ this.linkMemberSilent(memberId, true);
8360
+ }
8361
+ }).catch((err) => {
8362
+ this.log("link-member silent fail", err);
8293
8363
  });
8294
8364
  }
8295
8365
  /** 현재 설정된 회원 ID 조회 (미설정 시 null) */
@@ -8523,6 +8593,7 @@ var AnalyticsAPI = class {
8523
8593
  createBaseEvent(type) {
8524
8594
  return {
8525
8595
  type,
8596
+ event_id: generateId(),
8526
8597
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8527
8598
  session_id: this.session.sessionId,
8528
8599
  visitor_uid: this.session.visitorUid
@@ -8545,6 +8616,7 @@ var AnalyticsAPI = class {
8545
8616
  visitor_uid: this.session.visitorUid,
8546
8617
  ...this.memberId ? { app_member_id: this.memberId } : {},
8547
8618
  events: events.map((e) => ({
8619
+ event_id: e.event_id,
8548
8620
  timestamp: e.timestamp,
8549
8621
  page_path: e.page_path || "",
8550
8622
  page_url: e.page_url || "",
@@ -8574,7 +8646,13 @@ var AnalyticsAPI = class {
8574
8646
  this.log("Flush failed", err);
8575
8647
  }
8576
8648
  }
8577
- /** sendBeacon으로 동기 flush (beforeunload용) */
8649
+ /**
8650
+ * sendBeacon 으로 동기 flush (beforeunload 용).
8651
+ *
8652
+ * sendBeacon 은 일반 fetch 와 달리 User-Agent 헤더가 신뢰할 수 있지만, 백엔드 봇
8653
+ * 탐지가 body 의 `user_agent` 필드 첫 번째 값만 보므로 여기서도 동일하게 채워
8654
+ * 첫 방문이 unload 타이밍에 도달했을 때 봇으로 오판되는 것을 방지한다.
8655
+ */
8578
8656
  flushSync() {
8579
8657
  if (this.eventQueue.length === 0 || !this.storageWebId) return;
8580
8658
  if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
@@ -8582,20 +8660,30 @@ var AnalyticsAPI = class {
8582
8660
  const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
8583
8661
  const baseUrl = this.http.getBaseUrl();
8584
8662
  const url = `${baseUrl}${prefix}/storages/web/${this.storageWebId}/visitors/batch`;
8663
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
8585
8664
  const body = JSON.stringify({
8586
8665
  visitor_uid: this.session.visitorUid,
8587
8666
  ...this.memberId ? { app_member_id: this.memberId } : {},
8588
8667
  events: events.map((e) => ({
8668
+ event_id: e.event_id,
8589
8669
  timestamp: e.timestamp,
8590
8670
  page_path: e.page_path || "",
8591
8671
  page_url: e.page_url || "",
8592
8672
  page_title: e.page_title || "",
8593
8673
  referrer: e.referrer || "",
8674
+ user_agent: ua,
8675
+ screen_width: e.screen_width || 0,
8676
+ screen_height: e.screen_height || 0,
8594
8677
  session_id: e.session_id,
8595
8678
  session_start: e.type === "session_start",
8596
8679
  is_page_view: e.type === "page_view",
8597
8680
  event_name: e.event_name,
8598
- event_properties: e.event_properties
8681
+ event_properties: e.event_properties,
8682
+ utm_source: e.utm_source,
8683
+ utm_medium: e.utm_medium,
8684
+ utm_campaign: e.utm_campaign,
8685
+ utm_content: e.utm_content,
8686
+ utm_term: e.utm_term
8599
8687
  }))
8600
8688
  });
8601
8689
  try {
package/dist/index.mjs CHANGED
@@ -7994,6 +7994,29 @@ var SessionManager = class {
7994
7994
  } catch {
7995
7995
  }
7996
7996
  }
7997
+ /**
7998
+ * visitor_uid 를 새로 발급하고 세션도 함께 초기화.
7999
+ *
8000
+ * 사용 시점:
8001
+ * - 사용자 로그아웃 (이전 사용자 활동이 다음 사용자에 attribution 되는 것 방지)
8002
+ * - 다른 사용자 로그인 감지 (link-member 가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답)
8003
+ *
8004
+ * localStorage 의 `__cb_visitor_uid` 가 새 UUID 로 교체되며 sessionStorage 의
8005
+ * 세션 키도 같이 비워진다. 이후의 모든 batch 는 새 visitor 로 기록되어 멤버 간
8006
+ * 데이터 오염이 차단된다.
8007
+ */
8008
+ regenerateVisitorUid() {
8009
+ const newUid = generateId();
8010
+ this._visitorUid = newUid;
8011
+ try {
8012
+ if (typeof localStorage !== "undefined") {
8013
+ localStorage.setItem(VISITOR_KEY, newUid);
8014
+ }
8015
+ } catch {
8016
+ }
8017
+ this.reset();
8018
+ return newUid;
8019
+ }
7997
8020
  ensureSession() {
7998
8021
  const now = Date.now();
7999
8022
  if (!this._sessionId) {
@@ -8199,6 +8222,11 @@ var AnalyticsAPI = class {
8199
8222
  * 백엔드 `link-member` 엔드포인트를 한 번 호출합니다 (1.11.0+). 호출 실패는
8200
8223
  * silent — 다음 batch 가 닿을 때 백엔드 자동 매핑이 동일하게 처리하므로 자가 복구.
8201
8224
  *
8225
+ * **사용자 전환 자동 처리 (1.13.0+)**: 동일 브라우저에서 다른 사용자가 로그인해
8226
+ * 백엔드가 `VISITOR_LINKED_TO_OTHER_MEMBER` 응답을 보내면, 큐를 비우고
8227
+ * `visitor_uid` 를 새로 발급한 뒤 link-member 를 한 번 더 호출해 새 visitor 가
8228
+ * 즉시 회원으로 기록되도록 자가 복구한다.
8229
+ *
8202
8230
  * 산업 표준(GA4 User-ID, Mixpanel/PostHog `identify`) 과 동작 정합.
8203
8231
  *
8204
8232
  * @example
@@ -8209,14 +8237,42 @@ var AnalyticsAPI = class {
8209
8237
  * ```
8210
8238
  */
8211
8239
  identify(memberId) {
8240
+ if (this.memberId && this.memberId !== memberId) {
8241
+ this.reset();
8242
+ }
8212
8243
  this.setMemberId(memberId);
8213
8244
  this.linkMemberSilent(memberId);
8214
8245
  }
8246
+ /**
8247
+ * 로그아웃 / 사용자 전환 시 호출. 익명 상태로 복귀하면서 visitor_uid 를 새로
8248
+ * 발급해 다음 사용자의 활동이 이전 사용자에게 attribution 되는 데이터 오염을 차단.
8249
+ *
8250
+ * 동작:
8251
+ * 1. memberId 를 null 로 설정 (이후 batch 는 익명으로 기록)
8252
+ * 2. 큐에 쌓인 미전송 이벤트 폐기 (이전 visitor 로 가는 것을 막기 위함)
8253
+ * 3. localStorage `__cb_visitor_uid` 새 UUID 로 교체 + sessionStorage 세션 키 정리
8254
+ * 4. heatmap 큐도 함께 비움
8255
+ *
8256
+ * @example
8257
+ * ```ts
8258
+ * // 로그아웃 핸들러
8259
+ * await cb.auth.signOut()
8260
+ * cb.analytics.reset()
8261
+ * ```
8262
+ */
8263
+ reset() {
8264
+ this.setMemberId(null);
8265
+ this.eventQueue = [];
8266
+ this.heatmapQueue = [];
8267
+ const newUid = this.session.regenerateVisitorUid();
8268
+ this.log("Analytics reset", { newVisitorUid: newUid });
8269
+ }
8215
8270
  /**
8216
8271
  * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출).
8217
8272
  *
8218
8273
  * `identify()` 와 달리 즉시 backfill 호출은 하지 않습니다 — 단순히 이후 batch 의
8219
- * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃).
8274
+ * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃 시에는
8275
+ * 데이터 오염 방지를 위해 `reset()` 를 권장).
8220
8276
  */
8221
8277
  setMemberId(memberId) {
8222
8278
  this.memberId = memberId || null;
@@ -8238,9 +8294,12 @@ var AnalyticsAPI = class {
8238
8294
  *
8239
8295
  * - 첫 페이지뷰가 아직 백엔드에 닿기 전이면 visitor 가 없어 404. 무시 — 다음 batch 가
8240
8296
  * 가면 백엔드 BatchRecordVisit 가 자동 LinkMember 를 호출.
8297
+ * - 다른 멤버에 이미 연결된 visitor 인 경우 (`VISITOR_LINKED_TO_OTHER_MEMBER`)
8298
+ * visitor_uid 를 자동 재발급하고 link-member 를 한 번 더 호출 — 한 단계의 자가
8299
+ * 복구로 끝나며 무한 재귀를 막기 위해 두 번째 시도는 응답을 더 보지 않는다.
8241
8300
  * - storage_web_id 가 init 안 됐거나 모든 종류의 네트워크 오류 — silent fail.
8242
8301
  */
8243
- linkMemberSilent(memberId) {
8302
+ linkMemberSilent(memberId, isRetry = false) {
8244
8303
  if (!this.storageWebId) return;
8245
8304
  const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
8246
8305
  this.http.post(
@@ -8249,7 +8308,18 @@ var AnalyticsAPI = class {
8249
8308
  visitor_uid: this.session.visitorUid,
8250
8309
  app_member_id: memberId
8251
8310
  }
8252
- ).catch(() => {
8311
+ ).then((resp) => {
8312
+ if (!resp) return;
8313
+ this.log("link-member response", resp);
8314
+ if (!isRetry && resp.success === false && resp.code === "VISITOR_LINKED_TO_OTHER_MEMBER") {
8315
+ this.log("user-switch detected \u2014 regenerating visitor_uid");
8316
+ this.eventQueue = [];
8317
+ this.heatmapQueue = [];
8318
+ this.session.regenerateVisitorUid();
8319
+ this.linkMemberSilent(memberId, true);
8320
+ }
8321
+ }).catch((err) => {
8322
+ this.log("link-member silent fail", err);
8253
8323
  });
8254
8324
  }
8255
8325
  /** 현재 설정된 회원 ID 조회 (미설정 시 null) */
@@ -8483,6 +8553,7 @@ var AnalyticsAPI = class {
8483
8553
  createBaseEvent(type) {
8484
8554
  return {
8485
8555
  type,
8556
+ event_id: generateId(),
8486
8557
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8487
8558
  session_id: this.session.sessionId,
8488
8559
  visitor_uid: this.session.visitorUid
@@ -8505,6 +8576,7 @@ var AnalyticsAPI = class {
8505
8576
  visitor_uid: this.session.visitorUid,
8506
8577
  ...this.memberId ? { app_member_id: this.memberId } : {},
8507
8578
  events: events.map((e) => ({
8579
+ event_id: e.event_id,
8508
8580
  timestamp: e.timestamp,
8509
8581
  page_path: e.page_path || "",
8510
8582
  page_url: e.page_url || "",
@@ -8534,7 +8606,13 @@ var AnalyticsAPI = class {
8534
8606
  this.log("Flush failed", err);
8535
8607
  }
8536
8608
  }
8537
- /** sendBeacon으로 동기 flush (beforeunload용) */
8609
+ /**
8610
+ * sendBeacon 으로 동기 flush (beforeunload 용).
8611
+ *
8612
+ * sendBeacon 은 일반 fetch 와 달리 User-Agent 헤더가 신뢰할 수 있지만, 백엔드 봇
8613
+ * 탐지가 body 의 `user_agent` 필드 첫 번째 값만 보므로 여기서도 동일하게 채워
8614
+ * 첫 방문이 unload 타이밍에 도달했을 때 봇으로 오판되는 것을 방지한다.
8615
+ */
8538
8616
  flushSync() {
8539
8617
  if (this.eventQueue.length === 0 || !this.storageWebId) return;
8540
8618
  if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
@@ -8542,20 +8620,30 @@ var AnalyticsAPI = class {
8542
8620
  const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
8543
8621
  const baseUrl = this.http.getBaseUrl();
8544
8622
  const url = `${baseUrl}${prefix}/storages/web/${this.storageWebId}/visitors/batch`;
8623
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
8545
8624
  const body = JSON.stringify({
8546
8625
  visitor_uid: this.session.visitorUid,
8547
8626
  ...this.memberId ? { app_member_id: this.memberId } : {},
8548
8627
  events: events.map((e) => ({
8628
+ event_id: e.event_id,
8549
8629
  timestamp: e.timestamp,
8550
8630
  page_path: e.page_path || "",
8551
8631
  page_url: e.page_url || "",
8552
8632
  page_title: e.page_title || "",
8553
8633
  referrer: e.referrer || "",
8634
+ user_agent: ua,
8635
+ screen_width: e.screen_width || 0,
8636
+ screen_height: e.screen_height || 0,
8554
8637
  session_id: e.session_id,
8555
8638
  session_start: e.type === "session_start",
8556
8639
  is_page_view: e.type === "page_view",
8557
8640
  event_name: e.event_name,
8558
- event_properties: e.event_properties
8641
+ event_properties: e.event_properties,
8642
+ utm_source: e.utm_source,
8643
+ utm_medium: e.utm_medium,
8644
+ utm_campaign: e.utm_campaign,
8645
+ utm_content: e.utm_content,
8646
+ utm_term: e.utm_term
8559
8647
  }))
8560
8648
  });
8561
8649
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",