@roottale/analytics-runtime 0.3.1 → 0.5.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/loader.ts","../src/consent-banner.ts","../src/ga4-bridge.ts","../src/journey.ts"],"sourcesContent":["// ADR-0038 §2·§4 — 공유 주입 산출물. Astro·Next 렌더러가 동일하게 mount.\n//\n// 두 조각으로 head 에 inline:\n// 1. renderConsentBootstrap() — *가장 먼저*. Consent Mode v2 default = 전부\n// denied (fail-closed). 어떤 태그보다 앞서야 함.\n// 2. renderLoaderScript(manifest) — manifest 를 embed 한 런타임. 태그는 동의\n// update 전까지 발화하지 않음. 동의 배너가 `RootTaleAnalytics.updateConsent`\n// 를 호출하면 그때 허용된 태그만 주입.\n//\n// 설계(measurement-design-draft.md) §0 게이트 계약 — `RootTaleAnalytics.\n// isTagLoaded(provider)` 로 태그 로드 완료 상태를 노출한다(내부 `loaded` 맵\n// 그대로 read-only 조회). `ga4-bridge.ts` 가 GA4 미러 발화 전 \"동의 granted\n// 그리고 GA4 로드 완료\" 를 함께 확인하는 데 쓴다.\n\nimport type { AnalyticsManifest } from \"./types\";\n\n/**\n * Consent Mode v2 default 부트스트랩. 전 카테고리 denied + `wait_for_update`.\n * 반드시 어떤 분석/광고 태그보다 먼저 실행돼야 한다.\n */\nexport function renderConsentBootstrap(): string {\n return [\n \"window.dataLayer=window.dataLayer||[];\",\n \"function gtag(){dataLayer.push(arguments);}\",\n \"gtag('consent','default',{\",\n \"'ad_storage':'denied','ad_user_data':'denied',\",\n \"'ad_personalization':'denied','analytics_storage':'denied',\",\n \"'wait_for_update':500});\",\n ].join(\"\");\n}\n\n// 브라우저 런타임 — manifest 를 읽고 동의가 충족된 태그만 주입한다.\n// `__RT_MANIFEST__` 는 renderLoaderScript 가 JSON 으로 치환.\nconst RUNTIME = `(function(){\nvar M=__RT_MANIFEST__;\nvar consent=Object.assign({},M.consentDefault);\nvar loaded={};\nfunction allowed(req){for(var i=0;i<req.length;i++){if(consent[req[i]]!=='granted')return false;}return true;}\nfunction inject(tag){\nif(loaded[tag.provider]||!allowed(tag.requiredConsent))return;\nloaded[tag.provider]=true;\nif(tag.provider==='ga4'){\nvar s=document.createElement('script');s.async=true;\ns.src='https://www.googletagmanager.com/gtag/js?id='+encodeURIComponent(tag.id);\ndocument.head.appendChild(s);gtag('js',new Date());gtag('config',tag.id);\n}else if(tag.provider==='clarity'){\n(function(c,l,a,r,i){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};\nvar t=l.createElement(r);t.async=1;t.src='https://www.clarity.ms/tag/'+i;\nvar y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,'clarity','script',tag.id);\n}else if(tag.provider==='meta_pixel'){\n(function(f,b,e,v){if(f.fbq)return;var n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};\nif(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];var t=b.createElement(e);t.async=!0;\nt.src=v;var s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)})(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');\nfbq('init',tag.id);fbq('track','PageView');\n}else if(tag.provider==='naver'){\nvar w=document.createElement('script');w.async=true;w.src='//wcs.naver.net/wcslog.js';\nw.onload=function(){if(window.wcs){window.wcs_add={wa:tag.id};if(window.wcs.inflow)window.wcs.inflow();if(window.wcs_do)window.wcs_do();}};\ndocument.head.appendChild(w);\n}}\nfunction applyAll(){for(var i=0;i<M.tags.length;i++){inject(M.tags[i]);}}\nwindow.RootTaleAnalytics={\nmanifest:M,\ngetConsent:function(){return Object.assign({},consent);},\nisTagLoaded:function(provider){return !!loaded[provider];},\nupdateConsent:function(partial){\nfor(var k in partial){if(Object.prototype.hasOwnProperty.call(consent,k))consent[k]=partial[k];}\nif(typeof gtag==='function'){gtag('consent','update',partial);}\napplyAll();\n}};\napplyAll();\n})();`;\n\n/**\n * manifest 를 embed 한 로더 스크립트. fail-closed — 기본 동의가 denied 라\n * `applyAll()` 은 아무 태그도 주입하지 않고, 동의 배너의 `updateConsent` 후에만\n * 허용된 태그가 붙는다.\n */\nexport function renderLoaderScript(manifest: AnalyticsManifest): string {\n return \"var __RT_MANIFEST__=\" + JSON.stringify(manifest) + \";\" + RUNTIME;\n}\n","// ADR-0038 §2 — 동의 배너. plane ① 외부 트래커는 동의 전까지 미발화(fail-closed)\n// 이므로, 배너가 방문자 선택을 받아 RootTaleAnalytics.updateConsent 를 호출한다.\n//\n// 주의: `set:html`/`dangerouslySetInnerHTML` 로 주입된 <script> 는 실행되지\n// 않는다. 본 모듈은 *마크업* 과 *JS 본문* 을 분리 반환하고, 렌더러가 JS 는\n// <script is:inline> 로, 마크업은 컨테이너로 주입해야 한다.\n//\n// 마크업/스크립트가 공유하는 DOM id (계약):\n// #rt-consent · #rt-consent-all · #rt-consent-analytics · #rt-consent-deny\n\nexport interface ConsentBannerOptions {\n /** 개인정보처리방침 링크 (테넌트별). 없으면 링크 생략. */\n privacyUrl?: string;\n /** 본문 카피. 기본 한국어. */\n message?: string;\n}\n\nconst BANNER_ID = \"rt-consent\";\nconst STORAGE_KEY = \"rt_consent_v1\";\n\n/** 배너 마크업(HTML). 스크립트 미포함 — 컨테이너로 주입. */\nexport function renderConsentBannerMarkup(\n options: ConsentBannerOptions = {},\n): string {\n const message =\n options.message ??\n \"이 사이트는 방문 분석·광고 도구를 사용할 수 있습니다. 동의 여부를 선택해 주세요.\";\n const privacy = options.privacyUrl\n ? ` <a href=\"${escapeAttr(options.privacyUrl)}\" target=\"_blank\" rel=\"noopener\">개인정보처리방침</a>`\n : \"\";\n\n return [\n `<div id=\"${BANNER_ID}\" role=\"dialog\" aria-live=\"polite\" aria-label=\"동의 안내\" hidden`,\n ` style=\"position:fixed;left:0;right:0;bottom:0;z-index:2147483647;`,\n `background:#fff;border-top:1px solid #e5e7eb;padding:16px;`,\n `display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center;`,\n `font-size:14px;line-height:1.5;color:#111;box-shadow:0 -2px 12px rgba(0,0,0,.06)\">`,\n `<span style=\"flex:1 1 280px;min-width:240px\">${escapeHtml(message)}${privacy}</span>`,\n `<button id=\"rt-consent-all\" type=\"button\" style=\"${BTN_PRIMARY}\">모두 허용</button>`,\n `<button id=\"rt-consent-analytics\" type=\"button\" style=\"${BTN_SECONDARY}\">분석만 허용</button>`,\n `<button id=\"rt-consent-deny\" type=\"button\" style=\"${BTN_GHOST}\">거부</button>`,\n `</div>`,\n ].join(\"\");\n}\n\n/** 배너 동작(JS 본문). <script is:inline> 로 감싸 주입. */\nexport function renderConsentBannerScript(): string {\n return `(function(){\nvar KEY=${JSON.stringify(STORAGE_KEY)};\nvar el=document.getElementById(${JSON.stringify(BANNER_ID)});\nfunction apply(state){\nif(window.RootTaleAnalytics&&typeof window.RootTaleAnalytics.updateConsent==='function'){\nwindow.RootTaleAnalytics.updateConsent(state);\n}}\nfunction persist(state){try{localStorage.setItem(KEY,JSON.stringify(state));}catch(e){}}\nfunction read(){try{return JSON.parse(localStorage.getItem(KEY)||'null');}catch(e){return null;}}\nvar ALL={analytics_storage:'granted',ad_storage:'granted',ad_user_data:'granted',ad_personalization:'granted'};\nvar ANALYTICS={analytics_storage:'granted',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'};\nvar DENY={analytics_storage:'denied',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied'};\nfunction choose(state){apply(state);persist(state);if(el){el.hidden=true;}}\nvar prior=read();\nif(prior){apply(prior);if(el){el.hidden=true;}}\nelse if(el){el.hidden=false;}\nfunction bind(id,state){var b=document.getElementById(id);if(b){b.addEventListener('click',function(){choose(state);});}}\nbind('rt-consent-all',ALL);\nbind('rt-consent-analytics',ANALYTICS);\nbind('rt-consent-deny',DENY);\n})();`;\n}\n\nconst BTN_BASE =\n \"padding:8px 14px;border-radius:8px;font-size:14px;cursor:pointer;border:1px solid #d1d5db;\";\nconst BTN_PRIMARY = BTN_BASE + \"background:#111;color:#fff;border-color:#111;\";\nconst BTN_SECONDARY = BTN_BASE + \"background:#f3f4f6;color:#111;\";\nconst BTN_GHOST = BTN_BASE + \"background:transparent;color:#6b7280;\";\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\nfunction escapeAttr(s: string): string {\n return escapeHtml(s).replace(/\"/g, \"&quot;\");\n}\n","// 설계(measurement-design-draft.md) §0(ADR-0038 amend) — GA4 미러 브리지.\n//\n// beacon(plane ②) `send()` 이 매 이벤트마다 `window.__rtGa4Mirror` 전역 훅이\n// 있으면 호출한다(beacon.ts 참조, try/catch 로 독립). 본 스크립트는 그 훅을\n// 설치할 뿐 beacon 을 직접 건드리지 않는다 — 순서 무관하게 안전(훅이 아직\n// 없을 때 이벤트가 나면 그냥 스킵, 다음 이벤트부터 미러링).\n//\n// 게이트(설계 §0): events.ts `GA4_MIRROR` 에 등록된 이벤트 + 동의\n// `analytics_storage==='granted'` + GA4 태그 로드 완료(`isTagLoaded('ga4')`) +\n// `typeof gtag==='function'` **전부** 참일 때만 `gtag('event', ga4Name, params)`.\n// 하나라도 불충족이면 그냥 버린다 — dataLayer 에 선큐잉해 동의 이후 소급\n// 전송하지 않는다(큐·재생 금지). 확정 전환 수는 어디까지나 inquiries DB —\n// 본 미러는 광고 생태계용 근사 신호일 뿐이다(설계 §0 SSoT 계약).\n\nimport { GA4_MIRROR } from \"./events\";\n\nconst RUNTIME = `(function(){\nvar MIRROR=__RT_GA4_MIRROR__;\nwindow.__rtGa4Mirror=function(ev,extra){\nvar name=MIRROR[ev];\nif(!name)return;\nvar RA=window.RootTaleAnalytics;\nif(!RA||typeof RA.getConsent!=='function'||typeof RA.isTagLoaded!=='function')return;\nif(RA.getConsent().analytics_storage!=='granted')return;\nif(!RA.isTagLoaded('ga4'))return;\nif(typeof gtag!=='function')return;\ngtag('event',name,extra||{});\n};\n})();`;\n\n/**\n * GA4 미러 브리지 스크립트. `<RootTaleAnalytics>` 가 beacon 스크립트와 함께\n * (순서 무관) 주입한다. `enableGa4Bridge={false}` 로 아예 생략 가능(react.tsx).\n */\nexport function renderGa4BridgeScript(): string {\n return \"var __RT_GA4_MIRROR__=\" + JSON.stringify(GA4_MIRROR) + \";\" + RUNTIME;\n}\n\ndeclare global {\n interface Window {\n /** beacon send() 가 매 이벤트마다 호출하는 GA4 미러 훅(설치돼 있을 때만). */\n __rtGa4Mirror?: (ev: string, extra?: Record<string, unknown>) => void;\n }\n}\n","// 설계(measurement-design-draft.md) §3 — 방문 여정(journey) 링버퍼.\n//\n// beacon(plane ②) `send()` 이 매 이벤트마다 `window.__rtJourneyRecord` 전역\n// 훅이 있으면 호출한다(beacon.ts 참조, try/catch 로 독립). 본 스크립트는 그\n// 훅을 설치할 뿐이다 — `events.ts` `TRACK_EVENTS[ev].journey` 가 true 인\n// 이벤트(pageview + 전환 사다리)만 sessionStorage `_rt_journey` 링버퍼(최대\n// 30항목)에 적재한다. scroll_depth·section_view 등 고빈도 감지 이벤트는\n// journey:false 라 기록되지 않는다.\n//\n// 서버측 계약(설계 §3, 본 모듈 범위 밖): vertical(medical/legal) 게이트는\n// 서버가 재강제하고, 저장은 `inquiries.journey` jsonb 컬럼 + zod 재구성 +\n// 180일 보존이다 — 클라이언트 캡처는 신뢰의 원천이 아니다.\n\nimport { TRACK_EVENTS, type TrackEventName } from \"./events\";\n\n/** sessionStorage 키 — cms-client/renderer 와의 공유 계약(변경 시 양쪽 동시 갱신). */\nexport const JOURNEY_STORAGE_KEY = \"_rt_journey\";\n\n/** 링버퍼 최대 항목 수(변경 시 RUNTIME 내 하드코딩 리터럴도 동시 갱신). */\nexport const JOURNEY_MAX_ITEMS = 30;\n\n/** `_rt_journey` 항목 형태. */\nexport interface JourneyEntry {\n /** 캡처 시각 (epoch ms). */\n t: number;\n /** 이벤트 이름 (events.ts 사전). */\n ev: string;\n /** 발생 경로 (pathname). vertical 게이트 대상은 서버에서 제거될 수 있음. */\n path?: string;\n /** 소형 차원 데이터 (data-track-* 로 캡처된 extra). */\n d?: Record<string, unknown>;\n}\n\n/** journey:true 이벤트 이름 목록 — events.ts SSoT 에서 파생. */\nconst JOURNEY_EVENT_NAMES: TrackEventName[] = (\n Object.keys(TRACK_EVENTS) as TrackEventName[]\n).filter((name) => TRACK_EVENTS[name].journey);\n\nconst RUNTIME = `(function(){\nvar JE=__RT_JOURNEY_EVENTS__;\nwindow.__rtJourneyRecord=function(ev,extra){\nif(!JE||JE.indexOf(ev)===-1)return;\nvar KEY='_rt_journey';\nvar arr=null;\ntry{arr=JSON.parse(sessionStorage.getItem(KEY)||'null');}catch(e){}\nif(Object.prototype.toString.call(arr)!=='[object Array]')arr=[];\nvar item={t:Date.now(),ev:ev};\ntry{if(location&&location.pathname)item.path=location.pathname;}catch(e){}\nif(extra)item.d=extra;\narr.push(item);\nif(arr.length>30)arr=arr.slice(arr.length-30);\nsessionStorage.setItem(KEY,JSON.stringify(arr));\n};\n})();`;\n\n/**\n * journey 기록 브리지 스크립트. `<RootTaleAnalytics>` 가 beacon 스크립트와\n * 함께(순서 무관) 항상 주입한다 — 1차 캡처 인프라라 opt-out 프롭 없음(ga4\n * 미러와 달리).\n */\nexport function renderJourneyBridgeScript(): string {\n return (\n \"var __RT_JOURNEY_EVENTS__=\" +\n JSON.stringify(JOURNEY_EVENT_NAMES) +\n \";\" +\n RUNTIME\n );\n}\n\n/**\n * sessionStorage 의 journey 링버퍼를 읽는다(브라우저 전용). 파싱 실패·미존재\n * 시 빈 배열. cms-client/renderer 가 문의 제출 시 이 값을 재직렬화해 동봉한다\n * (submit capture/`formdata` 시점 — 설계 §3 P1-3, hidden 스냅샷 아님).\n */\nexport function readJourney(): JourneyEntry[] {\n if (typeof window === \"undefined\" || typeof sessionStorage === \"undefined\") {\n return [];\n }\n try {\n const raw = sessionStorage.getItem(JOURNEY_STORAGE_KEY);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? (parsed as JourneyEntry[]) : [];\n } catch {\n return [];\n }\n}\n\ndeclare global {\n interface Window {\n /** beacon send() 가 매 이벤트마다 호출하는 journey 기록 훅(설치돼 있을 때만). */\n __rtJourneyRecord?: (ev: string, extra?: Record<string, unknown>) => void;\n }\n}\n"],"mappings":";;;;;;AAoBO,SAAS,yBAAiC;AAC/C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,EAAE;AACX;AAIA,IAAM,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4CT,SAAS,mBAAmB,UAAqC;AACtE,SAAO,yBAAyB,KAAK,UAAU,QAAQ,IAAI,MAAM;AACnE;;;AC9DA,IAAM,YAAY;AAClB,IAAM,cAAc;AAGb,SAAS,0BACd,UAAgC,CAAC,GACzB;AACR,QAAM,UACJ,QAAQ,WACR;AACF,QAAM,UAAU,QAAQ,aACpB,aAAa,WAAW,QAAQ,UAAU,CAAC,0FAC3C;AAEJ,SAAO;AAAA,IACL,YAAY,SAAS;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gDAAgD,WAAW,OAAO,CAAC,GAAG,OAAO;AAAA,IAC7E,oDAAoD,WAAW;AAAA,IAC/D,0DAA0D,aAAa;AAAA,IACvE,qDAAqD,SAAS;AAAA,IAC9D;AAAA,EACF,EAAE,KAAK,EAAE;AACX;AAGO,SAAS,4BAAoC;AAClD,SAAO;AAAA,UACC,KAAK,UAAU,WAAW,CAAC;AAAA,iCACJ,KAAK,UAAU,SAAS,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmB1D;AAEA,IAAM,WACJ;AACF,IAAM,cAAc,WAAW;AAC/B,IAAM,gBAAgB,WAAW;AACjC,IAAM,YAAY,WAAW;AAE7B,SAAS,WAAW,GAAmB;AACrC,SAAO,EACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM;AACzB;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,WAAW,CAAC,EAAE,QAAQ,MAAM,QAAQ;AAC7C;;;ACrEA,IAAMA,WAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBT,SAAS,wBAAgC;AAC9C,SAAO,2BAA2B,KAAK,UAAU,UAAU,IAAI,MAAMA;AACvE;;;ACpBO,IAAM,sBAAsB;AAG5B,IAAM,oBAAoB;AAejC,IAAM,sBACJ,OAAO,KAAK,YAAY,EACxB,OAAO,CAAC,SAAS,aAAa,IAAI,EAAE,OAAO;AAE7C,IAAMC,WAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBT,SAAS,4BAAoC;AAClD,SACE,+BACA,KAAK,UAAU,mBAAmB,IAClC,MACAA;AAEJ;AAOO,SAAS,cAA8B;AAC5C,MAAI,OAAO,WAAW,eAAe,OAAO,mBAAmB,aAAa;AAC1E,WAAO,CAAC;AAAA,EACV;AACA,MAAI;AACF,UAAM,MAAM,eAAe,QAAQ,mBAAmB;AACtD,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,MAAM,QAAQ,MAAM,IAAK,SAA4B,CAAC;AAAA,EAC/D,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;","names":["RUNTIME","RUNTIME"]}
@@ -0,0 +1,123 @@
1
+ // src/events.ts
2
+ var TRACK_EVENTS = {
3
+ pageview: { ga4: false, journey: true, dims: ["pid"], wireDims: ["pid"] },
4
+ lead_submit: {
5
+ ga4: true,
6
+ ga4Name: "generate_lead",
7
+ journey: true,
8
+ dims: ["form_id"],
9
+ wireDims: ["form_id"]
10
+ },
11
+ form_submit_attempt: {
12
+ ga4: false,
13
+ journey: true,
14
+ dims: ["form_id"],
15
+ wireDims: ["form_id"]
16
+ },
17
+ phone_click: {
18
+ ga4: true,
19
+ ga4Name: "phone_call_click",
20
+ journey: true,
21
+ dims: ["placement"],
22
+ wireDims: ["trackPlacement"]
23
+ },
24
+ email_click: {
25
+ ga4: true,
26
+ ga4Name: "email_click",
27
+ journey: true,
28
+ dims: ["placement"],
29
+ wireDims: ["trackPlacement"]
30
+ },
31
+ chat_click: {
32
+ ga4: true,
33
+ ga4Name: "chat_click",
34
+ journey: true,
35
+ dims: ["provider", "placement"],
36
+ wireDims: ["trackProvider", "trackPlacement"]
37
+ },
38
+ booking_click: {
39
+ ga4: true,
40
+ ga4Name: "booking_outbound",
41
+ journey: true,
42
+ dims: ["provider", "placement"],
43
+ wireDims: ["trackProvider", "trackPlacement"]
44
+ },
45
+ cta_click: {
46
+ ga4: false,
47
+ journey: false,
48
+ dims: ["cta_id", "placement"],
49
+ wireDims: ["trackCta", "trackPlacement"]
50
+ },
51
+ pricing_view: {
52
+ ga4: false,
53
+ journey: false,
54
+ dims: ["item"],
55
+ wireDims: ["trackItem"]
56
+ },
57
+ pricing_expand: {
58
+ ga4: false,
59
+ journey: false,
60
+ dims: ["item"],
61
+ wireDims: ["trackItem"]
62
+ },
63
+ faq_open: {
64
+ ga4: false,
65
+ journey: false,
66
+ dims: ["q_index"],
67
+ wireDims: ["trackQ"]
68
+ },
69
+ gallery_interact: {
70
+ ga4: false,
71
+ journey: false,
72
+ dims: ["spot"],
73
+ wireDims: ["trackSpot"]
74
+ },
75
+ form_start: {
76
+ ga4: false,
77
+ journey: false,
78
+ dims: ["form_id"],
79
+ wireDims: ["form_id"]
80
+ },
81
+ form_abandon: {
82
+ ga4: false,
83
+ journey: false,
84
+ dims: ["form_id", "last_field"],
85
+ wireDims: ["form_id", "last_field"]
86
+ },
87
+ section_view: {
88
+ ga4: false,
89
+ journey: false,
90
+ dims: ["section_id"],
91
+ wireDims: ["section_id"]
92
+ },
93
+ scroll_depth: {
94
+ ga4: false,
95
+ journey: false,
96
+ dims: ["pct"],
97
+ wireDims: ["pct"]
98
+ },
99
+ read_complete: {
100
+ ga4: false,
101
+ journey: false,
102
+ dims: ["pid"],
103
+ wireDims: ["pid"]
104
+ }
105
+ };
106
+ function isTrackEvent(name) {
107
+ return Object.prototype.hasOwnProperty.call(TRACK_EVENTS, name);
108
+ }
109
+ var GA4_MIRROR = (() => {
110
+ const out = {};
111
+ for (const name of Object.keys(TRACK_EVENTS)) {
112
+ const def = TRACK_EVENTS[name];
113
+ if (def.ga4 && def.ga4Name) out[name] = def.ga4Name;
114
+ }
115
+ return out;
116
+ })();
117
+
118
+ export {
119
+ TRACK_EVENTS,
120
+ isTrackEvent,
121
+ GA4_MIRROR
122
+ };
123
+ //# sourceMappingURL=chunk-GBIGQHVN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/events.ts"],"sourcesContent":["// 설계(measurement-design-draft.md) §1 — 이벤트 사전 SSoT. 이름·GA4 미러 대상\n// 여부·GA4 이벤트명·journey 기록 여부·허용 차원 키(d1/d2/d3 슬롯 매핑 순서)를\n// 여기 한 곳에 고정한다. api-core `/collect` 가 본 사전을 import 해 allowlist\n// 로 쓴다(설계 §4 — 사전 밖 `ev` 는 204 drop). 브라우저 의존 0(순수 데이터) —\n// edge(api-core)·node·browser 어디서나 import 가능.\n//\n// `directions_click`·`vcard_save` 는 v1 제외(설계 §1 각주) — starter 에 실\n// 지도 링크 계약이 아직 없고 / bio-pages 는 별도 앱이라 링크 계약 신설 시\n// 사전에 등록한다.\n//\n// journey 플래그(설계 §3): sessionStorage 여정 링버퍼(journey.ts)에 담을\n// 이벤트만 true. 설계는 \"pageview + 전환 사다리 이벤트\"라 표현하는데, 본\n// 사전은 이를 pageview + GA4 미러 대상(확정 전환 신호: lead_submit·\n// phone_click·email_click·chat_click·booking_click) + 그 직전 단계인\n// form_submit_attempt 로 확정한다. cta_click·form_start·faq_open 등 참여\n// 신호는 scroll_depth·section_view 같은 고빈도 신호와 마찬가지로 journey 에서\n// 제외 — 방문 여정은 \"전환에 이르는 궤적\"만 남긴다(§3 취지).\nexport interface TrackEventDef {\n /** GA4 로 미러 발화하는 이벤트인지(설계 §0 게이트 계약). */\n readonly ga4: boolean;\n /** GA4 미러 시 사용할 gtag 이벤트명. `ga4=true` 일 때만 존재. */\n readonly ga4Name?: string;\n /** sessionStorage 여정 링버퍼(journey.ts)에 기록할지. */\n readonly journey: boolean;\n /**\n * 허용 차원 키(의미상 이름) — 순서대로 blob d1/d2/d3 슬롯에 매핑(설계 §4).\n * 어드민 표시·문서용 semantic name. 실제 payload 파싱은 {@link wireDims}.\n */\n readonly dims: readonly string[];\n /**\n * `dims` 와 순서 동일하되 **실제 /collect JSON payload 상의 키 이름**\n * (beacon.ts RUNTIME 이 두 경로로 값을 보내기 때문에 `dims` 와 다를 수 있다):\n * - 클릭 위임(`[data-track]` delegated click): `dataset` 의 `data-track-*`\n * camelCase 키를 그대로 전송(`data-track-placement`→`trackPlacement`,\n * `data-track-cta`→`trackCta`, `data-track-q`→`trackQ`,\n * `data-track-provider`→`trackProvider`).\n * - JS 직접 호출(`trackCustomEvent`/`window.__rtTrack` — observers.ts·\n * lead-form-meta.client.tsx·not-found 404 비콘 등): 호출부가 이미\n * `dims` 와 같은 snake_case 키(`section_id`·`pct`·`pid`·`form_id`·\n * `last_field`)로 extra 객체를 만들어 보낸다.\n * api-core `/collect` 가 이 키로 body 에서 값을 읽어 blob d1/d2/d3 에 채운다.\n */\n readonly wireDims?: readonly string[];\n}\n\nexport const TRACK_EVENTS = {\n pageview: { ga4: false, journey: true, dims: [\"pid\"], wireDims: [\"pid\"] },\n lead_submit: {\n ga4: true,\n ga4Name: \"generate_lead\",\n journey: true,\n dims: [\"form_id\"],\n wireDims: [\"form_id\"],\n },\n form_submit_attempt: {\n ga4: false,\n journey: true,\n dims: [\"form_id\"],\n wireDims: [\"form_id\"],\n },\n phone_click: {\n ga4: true,\n ga4Name: \"phone_call_click\",\n journey: true,\n dims: [\"placement\"],\n wireDims: [\"trackPlacement\"],\n },\n email_click: {\n ga4: true,\n ga4Name: \"email_click\",\n journey: true,\n dims: [\"placement\"],\n wireDims: [\"trackPlacement\"],\n },\n chat_click: {\n ga4: true,\n ga4Name: \"chat_click\",\n journey: true,\n dims: [\"provider\", \"placement\"],\n wireDims: [\"trackProvider\", \"trackPlacement\"],\n },\n booking_click: {\n ga4: true,\n ga4Name: \"booking_outbound\",\n journey: true,\n dims: [\"provider\", \"placement\"],\n wireDims: [\"trackProvider\", \"trackPlacement\"],\n },\n cta_click: {\n ga4: false,\n journey: false,\n dims: [\"cta_id\", \"placement\"],\n wireDims: [\"trackCta\", \"trackPlacement\"],\n },\n pricing_view: {\n ga4: false,\n journey: false,\n dims: [\"item\"],\n wireDims: [\"trackItem\"],\n },\n pricing_expand: {\n ga4: false,\n journey: false,\n dims: [\"item\"],\n wireDims: [\"trackItem\"],\n },\n faq_open: {\n ga4: false,\n journey: false,\n dims: [\"q_index\"],\n wireDims: [\"trackQ\"],\n },\n gallery_interact: {\n ga4: false,\n journey: false,\n dims: [\"spot\"],\n wireDims: [\"trackSpot\"],\n },\n form_start: {\n ga4: false,\n journey: false,\n dims: [\"form_id\"],\n wireDims: [\"form_id\"],\n },\n form_abandon: {\n ga4: false,\n journey: false,\n dims: [\"form_id\", \"last_field\"],\n wireDims: [\"form_id\", \"last_field\"],\n },\n section_view: {\n ga4: false,\n journey: false,\n dims: [\"section_id\"],\n wireDims: [\"section_id\"],\n },\n scroll_depth: {\n ga4: false,\n journey: false,\n dims: [\"pct\"],\n wireDims: [\"pct\"],\n },\n read_complete: {\n ga4: false,\n journey: false,\n dims: [\"pid\"],\n wireDims: [\"pid\"],\n },\n} as const satisfies Record<string, TrackEventDef>;\n\nexport type TrackEventName = keyof typeof TRACK_EVENTS;\n\n/** `name` 이 사전에 등록된 이벤트인지 — api-core `/collect` allowlist 가드로 재사용. */\nexport function isTrackEvent(name: string): name is TrackEventName {\n return Object.prototype.hasOwnProperty.call(TRACK_EVENTS, name);\n}\n\n/**\n * GA4 미러 대상 이벤트만 뽑은 파생 상수 — `{eventName: ga4EventName}`.\n * `ga4-bridge.ts` 가 이 맵으로 미러 여부·gtag 이벤트명을 조회한다. 사전이\n * SSoT 이므로 이 상수는 `TRACK_EVENTS` 에서 계산될 뿐 별도로 유지하지 않는다.\n */\nexport const GA4_MIRROR: Readonly<Partial<Record<TrackEventName, string>>> =\n (() => {\n const out: Partial<Record<TrackEventName, string>> = {};\n for (const name of Object.keys(TRACK_EVENTS) as TrackEventName[]) {\n const def = TRACK_EVENTS[name];\n if (def.ga4 && def.ga4Name) out[name] = def.ga4Name;\n }\n return out;\n })();\n"],"mappings":"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","names":[]}
@@ -0,0 +1,147 @@
1
+ interface TrackEventDef {
2
+ /** GA4 로 미러 발화하는 이벤트인지(설계 §0 게이트 계약). */
3
+ readonly ga4: boolean;
4
+ /** GA4 미러 시 사용할 gtag 이벤트명. `ga4=true` 일 때만 존재. */
5
+ readonly ga4Name?: string;
6
+ /** sessionStorage 여정 링버퍼(journey.ts)에 기록할지. */
7
+ readonly journey: boolean;
8
+ /**
9
+ * 허용 차원 키(의미상 이름) — 순서대로 blob d1/d2/d3 슬롯에 매핑(설계 §4).
10
+ * 어드민 표시·문서용 semantic name. 실제 payload 파싱은 {@link wireDims}.
11
+ */
12
+ readonly dims: readonly string[];
13
+ /**
14
+ * `dims` 와 순서 동일하되 **실제 /collect JSON payload 상의 키 이름**
15
+ * (beacon.ts RUNTIME 이 두 경로로 값을 보내기 때문에 `dims` 와 다를 수 있다):
16
+ * - 클릭 위임(`[data-track]` delegated click): `dataset` 의 `data-track-*`
17
+ * camelCase 키를 그대로 전송(`data-track-placement`→`trackPlacement`,
18
+ * `data-track-cta`→`trackCta`, `data-track-q`→`trackQ`,
19
+ * `data-track-provider`→`trackProvider`).
20
+ * - JS 직접 호출(`trackCustomEvent`/`window.__rtTrack` — observers.ts·
21
+ * lead-form-meta.client.tsx·not-found 404 비콘 등): 호출부가 이미
22
+ * `dims` 와 같은 snake_case 키(`section_id`·`pct`·`pid`·`form_id`·
23
+ * `last_field`)로 extra 객체를 만들어 보낸다.
24
+ * api-core `/collect` 가 이 키로 body 에서 값을 읽어 blob d1/d2/d3 에 채운다.
25
+ */
26
+ readonly wireDims?: readonly string[];
27
+ }
28
+ declare const TRACK_EVENTS: {
29
+ readonly pageview: {
30
+ readonly ga4: false;
31
+ readonly journey: true;
32
+ readonly dims: readonly ["pid"];
33
+ readonly wireDims: readonly ["pid"];
34
+ };
35
+ readonly lead_submit: {
36
+ readonly ga4: true;
37
+ readonly ga4Name: "generate_lead";
38
+ readonly journey: true;
39
+ readonly dims: readonly ["form_id"];
40
+ readonly wireDims: readonly ["form_id"];
41
+ };
42
+ readonly form_submit_attempt: {
43
+ readonly ga4: false;
44
+ readonly journey: true;
45
+ readonly dims: readonly ["form_id"];
46
+ readonly wireDims: readonly ["form_id"];
47
+ };
48
+ readonly phone_click: {
49
+ readonly ga4: true;
50
+ readonly ga4Name: "phone_call_click";
51
+ readonly journey: true;
52
+ readonly dims: readonly ["placement"];
53
+ readonly wireDims: readonly ["trackPlacement"];
54
+ };
55
+ readonly email_click: {
56
+ readonly ga4: true;
57
+ readonly ga4Name: "email_click";
58
+ readonly journey: true;
59
+ readonly dims: readonly ["placement"];
60
+ readonly wireDims: readonly ["trackPlacement"];
61
+ };
62
+ readonly chat_click: {
63
+ readonly ga4: true;
64
+ readonly ga4Name: "chat_click";
65
+ readonly journey: true;
66
+ readonly dims: readonly ["provider", "placement"];
67
+ readonly wireDims: readonly ["trackProvider", "trackPlacement"];
68
+ };
69
+ readonly booking_click: {
70
+ readonly ga4: true;
71
+ readonly ga4Name: "booking_outbound";
72
+ readonly journey: true;
73
+ readonly dims: readonly ["provider", "placement"];
74
+ readonly wireDims: readonly ["trackProvider", "trackPlacement"];
75
+ };
76
+ readonly cta_click: {
77
+ readonly ga4: false;
78
+ readonly journey: false;
79
+ readonly dims: readonly ["cta_id", "placement"];
80
+ readonly wireDims: readonly ["trackCta", "trackPlacement"];
81
+ };
82
+ readonly pricing_view: {
83
+ readonly ga4: false;
84
+ readonly journey: false;
85
+ readonly dims: readonly ["item"];
86
+ readonly wireDims: readonly ["trackItem"];
87
+ };
88
+ readonly pricing_expand: {
89
+ readonly ga4: false;
90
+ readonly journey: false;
91
+ readonly dims: readonly ["item"];
92
+ readonly wireDims: readonly ["trackItem"];
93
+ };
94
+ readonly faq_open: {
95
+ readonly ga4: false;
96
+ readonly journey: false;
97
+ readonly dims: readonly ["q_index"];
98
+ readonly wireDims: readonly ["trackQ"];
99
+ };
100
+ readonly gallery_interact: {
101
+ readonly ga4: false;
102
+ readonly journey: false;
103
+ readonly dims: readonly ["spot"];
104
+ readonly wireDims: readonly ["trackSpot"];
105
+ };
106
+ readonly form_start: {
107
+ readonly ga4: false;
108
+ readonly journey: false;
109
+ readonly dims: readonly ["form_id"];
110
+ readonly wireDims: readonly ["form_id"];
111
+ };
112
+ readonly form_abandon: {
113
+ readonly ga4: false;
114
+ readonly journey: false;
115
+ readonly dims: readonly ["form_id", "last_field"];
116
+ readonly wireDims: readonly ["form_id", "last_field"];
117
+ };
118
+ readonly section_view: {
119
+ readonly ga4: false;
120
+ readonly journey: false;
121
+ readonly dims: readonly ["section_id"];
122
+ readonly wireDims: readonly ["section_id"];
123
+ };
124
+ readonly scroll_depth: {
125
+ readonly ga4: false;
126
+ readonly journey: false;
127
+ readonly dims: readonly ["pct"];
128
+ readonly wireDims: readonly ["pct"];
129
+ };
130
+ readonly read_complete: {
131
+ readonly ga4: false;
132
+ readonly journey: false;
133
+ readonly dims: readonly ["pid"];
134
+ readonly wireDims: readonly ["pid"];
135
+ };
136
+ };
137
+ type TrackEventName = keyof typeof TRACK_EVENTS;
138
+ /** `name` 이 사전에 등록된 이벤트인지 — api-core `/collect` allowlist 가드로 재사용. */
139
+ declare function isTrackEvent(name: string): name is TrackEventName;
140
+ /**
141
+ * GA4 미러 대상 이벤트만 뽑은 파생 상수 — `{eventName: ga4EventName}`.
142
+ * `ga4-bridge.ts` 가 이 맵으로 미러 여부·gtag 이벤트명을 조회한다. 사전이
143
+ * SSoT 이므로 이 상수는 `TRACK_EVENTS` 에서 계산될 뿐 별도로 유지하지 않는다.
144
+ */
145
+ declare const GA4_MIRROR: Readonly<Partial<Record<TrackEventName, string>>>;
146
+
147
+ export { GA4_MIRROR, TRACK_EVENTS, type TrackEventDef, type TrackEventName, isTrackEvent };
package/dist/events.js ADDED
@@ -0,0 +1,11 @@
1
+ import {
2
+ GA4_MIRROR,
3
+ TRACK_EVENTS,
4
+ isTrackEvent
5
+ } from "./chunk-GBIGQHVN.js";
6
+ export {
7
+ GA4_MIRROR,
8
+ TRACK_EVENTS,
9
+ isTrackEvent
10
+ };
11
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { C as ConsentCategory, a as ConsentState, T as TagProvider, A as AnalyticsSiteConfig, b as AnalyticsManifest } from './consent-banner-WVGsxcY2.js';
2
2
  export { c as ConsentBannerOptions, d as ConsentValue, R as ResolvedTag, e as TagConfig, r as renderConsentBannerMarkup, f as renderConsentBannerScript } from './consent-banner-WVGsxcY2.js';
3
+ export { AI_REFERRAL_IDS, AI_REFERRAL_LABEL, AiReferralId, AiReferralMetric, aiReferralRulesLiteral, classifyAiReferral, isAiReferralId, summarizeAiReferrals } from './ai-referrals.js';
4
+ export { GA4_MIRROR, TRACK_EVENTS, TrackEventDef, TrackEventName, isTrackEvent } from './events.js';
3
5
 
4
6
  declare const ALL_CONSENT_CATEGORIES: readonly ConsentCategory[];
5
7
  /** fail-closed 기본값 — 전부 denied. */
@@ -55,10 +57,14 @@ declare const ATTRIBUTION_TTL_MS: number;
55
57
  /**
56
58
  * localStorage["_rt_attr"] 에 저장되는 first-touch 형태 (식별자 없음).
57
59
  * 키를 짧게 유지 — 비콘 인라인 런타임과 폼 hidden field 양쪽에서 쓰인다.
60
+ *
61
+ * v1 데이터 하위호환: 이미 저장된 v1 envelope(ut/un/gcl/lt 없음)도 유효하게
62
+ * 읽는다 — TTL 이 남아있으면 구조를 마이그레이션하지 않고 그대로 유지하며,
63
+ * 신호가 있는 로드에서 `lt` 만 추가로 얹는다. 새로 캡처되는 envelope 만 `v:2`.
58
64
  */
59
65
  interface FirstTouchAttribution {
60
- /** 포맷 버전. */
61
- v: 1;
66
+ /** 포맷 버전. 저장 시점의 v1 데이터도 유효하게 읽는다(하위호환). */
67
+ v: 1 | 2;
62
68
  /** landing pathname (query 제외). */
63
69
  lp: string;
64
70
  /** 캡처 시각 (epoch ms). */
@@ -71,8 +77,37 @@ interface FirstTouchAttribution {
71
77
  um?: string;
72
78
  /** utm_campaign (있을 때만). */
73
79
  uc?: string;
80
+ /** utm_term (있을 때만, v2). */
81
+ ut?: string;
82
+ /** utm_content (있을 때만, v2). */
83
+ un?: string;
84
+ /** 광고 클릭 유입 존재 플래그(v2) — gclid/fbclid **원값은 절대 저장하지 않음**. */
85
+ gcl?: 1;
74
86
  /** 외부 referrer hostname — raw URL 미저장, 동일 host 면 생략. */
75
87
  rf?: string;
88
+ /** last non-direct touch (v2) — 신호 있는 로드에서만 통째 교체. */
89
+ lt?: LastTouchAttribution;
90
+ }
91
+ /** `lt` 슬롯을 교체시킨 채널 — 우선순위 rs > utm > gcl > rf. */
92
+ type LastTouchChannel = "rs" | "utm" | "gcl" | "rf";
93
+ /**
94
+ * last non-direct touch — first-touch 와 동형이나 `ch`(판정 채널)를 더 갖는다.
95
+ * first-touch 와 동일한 30일 봉투 안에서 관리(별도 TTL 없음).
96
+ */
97
+ interface LastTouchAttribution {
98
+ /** 갱신 시각 (epoch ms). */
99
+ ts: number;
100
+ /** 이 갱신을 유발한 채널(우선순위 rs > utm > gcl > rf). */
101
+ ch: LastTouchChannel;
102
+ rs?: string;
103
+ us?: string;
104
+ um?: string;
105
+ uc?: string;
106
+ ut?: string;
107
+ un?: string;
108
+ /** 광고 클릭 유입 존재 플래그 — 원값 미저장(first-touch 와 동일 정책). */
109
+ gcl?: 1;
110
+ rf?: string;
76
111
  }
77
112
  /**
78
113
  * 비콘 dispatcher JS 본문. <script is:inline> 로 감싸 주입.
@@ -100,4 +135,70 @@ declare global {
100
135
  */
101
136
  declare function trackCustomEvent(ev: string, extra?: Record<string, unknown>): void;
102
137
 
103
- export { ALL_CONSENT_CATEGORIES, ATTRIBUTION_STORAGE_KEY, ATTRIBUTION_TTL_MS, AnalyticsManifest, AnalyticsSiteConfig, type BeaconOptions, ConsentCategory, ConsentState, DENIED_CONSENT, type FirstTouchAttribution, PROVIDER_CONSENT, TagProvider, buildManifest, isTagAllowed, renderBeaconScript, renderConsentBootstrap, renderLoaderScript, trackCustomEvent };
138
+ /**
139
+ * GA4 미러 브리지 스크립트. `<RootTaleAnalytics>` 가 beacon 스크립트와 함께
140
+ * (순서 무관) 주입한다. `enableGa4Bridge={false}` 로 아예 생략 가능(react.tsx).
141
+ */
142
+ declare function renderGa4BridgeScript(): string;
143
+ declare global {
144
+ interface Window {
145
+ /** beacon send() 가 매 이벤트마다 호출하는 GA4 미러 훅(설치돼 있을 때만). */
146
+ __rtGa4Mirror?: (ev: string, extra?: Record<string, unknown>) => void;
147
+ }
148
+ }
149
+
150
+ /** sessionStorage 키 — cms-client/renderer 와의 공유 계약(변경 시 양쪽 동시 갱신). */
151
+ declare const JOURNEY_STORAGE_KEY = "_rt_journey";
152
+ /** 링버퍼 최대 항목 수(변경 시 RUNTIME 내 하드코딩 리터럴도 동시 갱신). */
153
+ declare const JOURNEY_MAX_ITEMS = 30;
154
+ /** `_rt_journey` 항목 형태. */
155
+ interface JourneyEntry {
156
+ /** 캡처 시각 (epoch ms). */
157
+ t: number;
158
+ /** 이벤트 이름 (events.ts 사전). */
159
+ ev: string;
160
+ /** 발생 경로 (pathname). vertical 게이트 대상은 서버에서 제거될 수 있음. */
161
+ path?: string;
162
+ /** 소형 차원 데이터 (data-track-* 로 캡처된 extra). */
163
+ d?: Record<string, unknown>;
164
+ }
165
+ /**
166
+ * journey 기록 브리지 스크립트. `<RootTaleAnalytics>` 가 beacon 스크립트와
167
+ * 함께(순서 무관) 항상 주입한다 — 1차 캡처 인프라라 opt-out 프롭 없음(ga4
168
+ * 미러와 달리).
169
+ */
170
+ declare function renderJourneyBridgeScript(): string;
171
+ /**
172
+ * sessionStorage 의 journey 링버퍼를 읽는다(브라우저 전용). 파싱 실패·미존재
173
+ * 시 빈 배열. cms-client/renderer 가 문의 제출 시 이 값을 재직렬화해 동봉한다
174
+ * (submit capture/`formdata` 시점 — 설계 §3 P1-3, hidden 스냅샷 아님).
175
+ */
176
+ declare function readJourney(): JourneyEntry[];
177
+ declare global {
178
+ interface Window {
179
+ /** beacon send() 가 매 이벤트마다 호출하는 journey 기록 훅(설치돼 있을 때만). */
180
+ __rtJourneyRecord?: (ev: string, extra?: Record<string, unknown>) => void;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * 감지기 초기화 — section_view·scroll_depth·read_complete·form_start·
186
+ * form_abandon. SSR/비브라우저 환경에서는 no-op. teardownObservers() 없이
187
+ * 재호출하면 중복 바인딩될 수 있다 — 어댑터는 pathname 변화 시 teardown →
188
+ * init 순서를 지킨다(SPA 재바인드 계약).
189
+ */
190
+ declare function initObservers(): void;
191
+ /** 모든 감지기 해제 — SPA pathname 변화 시 재바인드 전에 호출한다. */
192
+ declare function teardownObservers(): void;
193
+ declare global {
194
+ interface Window {
195
+ /**
196
+ * react.tsx(RSC, hookless) 가 명시적으로 `enableObservers={false}` 일 때만
197
+ * `false` 를 인라인 스크립트로 심는다 — next.tsx 어댑터가 initObservers()
198
+ * 호출 전 이 값을 확인한다(미설정 시 기본 true, "기본 true" 계약 구현).
199
+ */
200
+ __rtObserversEnabled?: boolean;
201
+ }
202
+ }
203
+
204
+ export { ALL_CONSENT_CATEGORIES, ATTRIBUTION_STORAGE_KEY, ATTRIBUTION_TTL_MS, AnalyticsManifest, AnalyticsSiteConfig, type BeaconOptions, ConsentCategory, ConsentState, DENIED_CONSENT, type FirstTouchAttribution, JOURNEY_MAX_ITEMS, JOURNEY_STORAGE_KEY, type JourneyEntry, type LastTouchAttribution, type LastTouchChannel, PROVIDER_CONSENT, TagProvider, buildManifest, initObservers, isTagAllowed, readJourney, renderBeaconScript, renderConsentBootstrap, renderGa4BridgeScript, renderJourneyBridgeScript, renderLoaderScript, teardownObservers, trackCustomEvent };
package/dist/index.js CHANGED
@@ -1,13 +1,37 @@
1
1
  import {
2
- ATTRIBUTION_STORAGE_KEY,
3
- ATTRIBUTION_TTL_MS,
4
- renderBeaconScript,
2
+ JOURNEY_MAX_ITEMS,
3
+ JOURNEY_STORAGE_KEY,
4
+ readJourney,
5
5
  renderConsentBannerMarkup,
6
6
  renderConsentBannerScript,
7
7
  renderConsentBootstrap,
8
- renderLoaderScript,
8
+ renderGa4BridgeScript,
9
+ renderJourneyBridgeScript,
10
+ renderLoaderScript
11
+ } from "./chunk-G7OLBHT5.js";
12
+ import {
13
+ initObservers,
14
+ teardownObservers
15
+ } from "./chunk-ABGGPWIX.js";
16
+ import {
17
+ ATTRIBUTION_STORAGE_KEY,
18
+ ATTRIBUTION_TTL_MS,
19
+ renderBeaconScript,
9
20
  trackCustomEvent
10
- } from "./chunk-ZFY3KBJJ.js";
21
+ } from "./chunk-ANB42OLW.js";
22
+ import {
23
+ GA4_MIRROR,
24
+ TRACK_EVENTS,
25
+ isTrackEvent
26
+ } from "./chunk-GBIGQHVN.js";
27
+ import {
28
+ AI_REFERRAL_IDS,
29
+ AI_REFERRAL_LABEL,
30
+ aiReferralRulesLiteral,
31
+ classifyAiReferral,
32
+ isAiReferralId,
33
+ summarizeAiReferrals
34
+ } from "./chunk-2WQ7PVRU.js";
11
35
 
12
36
  // src/consent.ts
13
37
  var ALL_CONSENT_CATEGORIES = [
@@ -53,18 +77,34 @@ function buildManifest(config) {
53
77
  };
54
78
  }
55
79
  export {
80
+ AI_REFERRAL_IDS,
81
+ AI_REFERRAL_LABEL,
56
82
  ALL_CONSENT_CATEGORIES,
57
83
  ATTRIBUTION_STORAGE_KEY,
58
84
  ATTRIBUTION_TTL_MS,
59
85
  DENIED_CONSENT,
86
+ GA4_MIRROR,
87
+ JOURNEY_MAX_ITEMS,
88
+ JOURNEY_STORAGE_KEY,
60
89
  PROVIDER_CONSENT,
90
+ TRACK_EVENTS,
91
+ aiReferralRulesLiteral,
61
92
  buildManifest,
93
+ classifyAiReferral,
94
+ initObservers,
95
+ isAiReferralId,
62
96
  isTagAllowed,
97
+ isTrackEvent,
98
+ readJourney,
63
99
  renderBeaconScript,
64
100
  renderConsentBannerMarkup,
65
101
  renderConsentBannerScript,
66
102
  renderConsentBootstrap,
103
+ renderGa4BridgeScript,
104
+ renderJourneyBridgeScript,
67
105
  renderLoaderScript,
106
+ summarizeAiReferrals,
107
+ teardownObservers,
68
108
  trackCustomEvent
69
109
  };
70
110
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/consent.ts","../src/manifest.ts"],"sourcesContent":["// ADR-0038 §2 — 동의 fail-closed + provider↔consent 매핑.\n\nimport type { ConsentCategory, ConsentState, TagProvider } from \"./types\";\n\nexport const ALL_CONSENT_CATEGORIES: readonly ConsentCategory[] = [\n \"analytics_storage\",\n \"ad_storage\",\n \"ad_user_data\",\n \"ad_personalization\",\n] as const;\n\n/** fail-closed 기본값 — 전부 denied. */\nexport const DENIED_CONSENT: ConsentState = {\n analytics_storage: \"denied\",\n ad_storage: \"denied\",\n ad_user_data: \"denied\",\n ad_personalization: \"denied\",\n};\n\n/**\n * provider 가 발화하기 전 granted 여야 하는 동의 카테고리.\n * - analytics 도구(GA4·Clarity·Naver) → `analytics_storage`\n * - 광고 도구(Meta Pixel) → `ad_storage` + `ad_user_data` + `ad_personalization`\n * (국외이전·광고목적이라 동의 요건 가장 강함)\n */\nexport const PROVIDER_CONSENT: Record<TagProvider, ConsentCategory[]> = {\n ga4: [\"analytics_storage\"],\n clarity: [\"analytics_storage\"],\n naver: [\"analytics_storage\"],\n meta_pixel: [\"ad_storage\", \"ad_user_data\", \"ad_personalization\"],\n};\n\n/** required 카테고리가 *모두* granted 일 때만 true. */\nexport function isTagAllowed(\n required: readonly ConsentCategory[],\n consent: ConsentState,\n): boolean {\n return required.every((c) => consent[c] === \"granted\");\n}\n","// ADR-0038 §1 — site 태그 구성 → manifest (순수 함수).\n\nimport { DENIED_CONSENT, PROVIDER_CONSENT } from \"./consent\";\nimport type { AnalyticsManifest, AnalyticsSiteConfig } from \"./types\";\n\n/**\n * site 태그 구성을 manifest 로 해석.\n * - `enabled=false` 또는 빈 id 는 제외.\n * - provider 당 1개 (중복은 첫 항목만).\n * - 각 태그에 필요한 동의 카테고리(`PROVIDER_CONSENT`)를 부착.\n * - 동의 기본값은 항상 전부 denied (fail-closed).\n */\nexport function buildManifest(config: AnalyticsSiteConfig): AnalyticsManifest {\n const seen = new Set<string>();\n const tags = config.tags\n .filter(\n (t) => t.enabled && typeof t.id === \"string\" && t.id.trim() !== \"\",\n )\n .filter((t) => {\n if (seen.has(t.provider)) return false;\n seen.add(t.provider);\n return true;\n })\n .map((t) => ({\n provider: t.provider,\n id: t.id.trim(),\n requiredConsent: PROVIDER_CONSENT[t.provider],\n }));\n\n return {\n siteId: config.siteId,\n tags,\n consentDefault: { ...DENIED_CONSENT },\n };\n}\n"],"mappings":";;;;;;;;;;;;AAIO,IAAM,yBAAqD;AAAA,EAChE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,iBAA+B;AAAA,EAC1C,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,oBAAoB;AACtB;AAQO,IAAM,mBAA2D;AAAA,EACtE,KAAK,CAAC,mBAAmB;AAAA,EACzB,SAAS,CAAC,mBAAmB;AAAA,EAC7B,OAAO,CAAC,mBAAmB;AAAA,EAC3B,YAAY,CAAC,cAAc,gBAAgB,oBAAoB;AACjE;AAGO,SAAS,aACd,UACA,SACS;AACT,SAAO,SAAS,MAAM,CAAC,MAAM,QAAQ,CAAC,MAAM,SAAS;AACvD;;;AC1BO,SAAS,cAAc,QAAgD;AAC5E,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,OAAO,OAAO,KACjB;AAAA,IACC,CAAC,MAAM,EAAE,WAAW,OAAO,EAAE,OAAO,YAAY,EAAE,GAAG,KAAK,MAAM;AAAA,EAClE,EACC,OAAO,CAAC,MAAM;AACb,QAAI,KAAK,IAAI,EAAE,QAAQ,EAAG,QAAO;AACjC,SAAK,IAAI,EAAE,QAAQ;AACnB,WAAO;AAAA,EACT,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,IACX,UAAU,EAAE;AAAA,IACZ,IAAI,EAAE,GAAG,KAAK;AAAA,IACd,iBAAiB,iBAAiB,EAAE,QAAQ;AAAA,EAC9C,EAAE;AAEJ,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf;AAAA,IACA,gBAAgB,EAAE,GAAG,eAAe;AAAA,EACtC;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/consent.ts","../src/manifest.ts"],"sourcesContent":["// ADR-0038 §2 — 동의 fail-closed + provider↔consent 매핑.\n\nimport type { ConsentCategory, ConsentState, TagProvider } from \"./types\";\n\nexport const ALL_CONSENT_CATEGORIES: readonly ConsentCategory[] = [\n \"analytics_storage\",\n \"ad_storage\",\n \"ad_user_data\",\n \"ad_personalization\",\n] as const;\n\n/** fail-closed 기본값 — 전부 denied. */\nexport const DENIED_CONSENT: ConsentState = {\n analytics_storage: \"denied\",\n ad_storage: \"denied\",\n ad_user_data: \"denied\",\n ad_personalization: \"denied\",\n};\n\n/**\n * provider 가 발화하기 전 granted 여야 하는 동의 카테고리.\n * - analytics 도구(GA4·Clarity·Naver) → `analytics_storage`\n * - 광고 도구(Meta Pixel) → `ad_storage` + `ad_user_data` + `ad_personalization`\n * (국외이전·광고목적이라 동의 요건 가장 강함)\n */\nexport const PROVIDER_CONSENT: Record<TagProvider, ConsentCategory[]> = {\n ga4: [\"analytics_storage\"],\n clarity: [\"analytics_storage\"],\n naver: [\"analytics_storage\"],\n meta_pixel: [\"ad_storage\", \"ad_user_data\", \"ad_personalization\"],\n};\n\n/** required 카테고리가 *모두* granted 일 때만 true. */\nexport function isTagAllowed(\n required: readonly ConsentCategory[],\n consent: ConsentState,\n): boolean {\n return required.every((c) => consent[c] === \"granted\");\n}\n","// ADR-0038 §1 — site 태그 구성 → manifest (순수 함수).\n\nimport { DENIED_CONSENT, PROVIDER_CONSENT } from \"./consent\";\nimport type { AnalyticsManifest, AnalyticsSiteConfig } from \"./types\";\n\n/**\n * site 태그 구성을 manifest 로 해석.\n * - `enabled=false` 또는 빈 id 는 제외.\n * - provider 당 1개 (중복은 첫 항목만).\n * - 각 태그에 필요한 동의 카테고리(`PROVIDER_CONSENT`)를 부착.\n * - 동의 기본값은 항상 전부 denied (fail-closed).\n */\nexport function buildManifest(config: AnalyticsSiteConfig): AnalyticsManifest {\n const seen = new Set<string>();\n const tags = config.tags\n .filter(\n (t) => t.enabled && typeof t.id === \"string\" && t.id.trim() !== \"\",\n )\n .filter((t) => {\n if (seen.has(t.provider)) return false;\n seen.add(t.provider);\n return true;\n })\n .map((t) => ({\n provider: t.provider,\n id: t.id.trim(),\n requiredConsent: PROVIDER_CONSENT[t.provider],\n }));\n\n return {\n siteId: config.siteId,\n tags,\n consentDefault: { ...DENIED_CONSENT },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIO,IAAM,yBAAqD;AAAA,EAChE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,iBAA+B;AAAA,EAC1C,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,oBAAoB;AACtB;AAQO,IAAM,mBAA2D;AAAA,EACtE,KAAK,CAAC,mBAAmB;AAAA,EACzB,SAAS,CAAC,mBAAmB;AAAA,EAC7B,OAAO,CAAC,mBAAmB;AAAA,EAC3B,YAAY,CAAC,cAAc,gBAAgB,oBAAoB;AACjE;AAGO,SAAS,aACd,UACA,SACS;AACT,SAAO,SAAS,MAAM,CAAC,MAAM,QAAQ,CAAC,MAAM,SAAS;AACvD;;;AC1BO,SAAS,cAAc,QAAgD;AAC5E,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,OAAO,OAAO,KACjB;AAAA,IACC,CAAC,MAAM,EAAE,WAAW,OAAO,EAAE,OAAO,YAAY,EAAE,GAAG,KAAK,MAAM;AAAA,EAClE,EACC,OAAO,CAAC,MAAM;AACb,QAAI,KAAK,IAAI,EAAE,QAAQ,EAAG,QAAO;AACjC,SAAK,IAAI,EAAE,QAAQ;AACnB,WAAO;AAAA,EACT,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,IACX,UAAU,EAAE;AAAA,IACZ,IAAI,EAAE,GAAG,KAAK;AAAA,IACd,iBAAiB,iBAAiB,EAAE,QAAQ;AAAA,EAC9C,EAAE;AAEJ,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf;AAAA,IACA,gBAAgB,EAAE,GAAG,eAAe;AAAA,EACtC;AACF;","names":[]}
package/dist/next.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * pathname 변화를 감지해 pageview 재발화 + observer 재바인드를 수행하는
3
+ * 드롭인 컴포넌트. 렌더 출력 없음(`null`) — 부수효과 전용.
4
+ */
5
+ declare function RootTaleAnalyticsNextAdapter(): null;
6
+
7
+ export { RootTaleAnalyticsNextAdapter };
package/dist/next.js ADDED
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import {
3
+ initObservers,
4
+ teardownObservers
5
+ } from "./chunk-ABGGPWIX.js";
6
+ import {
7
+ trackCustomEvent
8
+ } from "./chunk-ANB42OLW.js";
9
+ import "./chunk-2WQ7PVRU.js";
10
+
11
+ // src/next.tsx
12
+ import { useEffect, useRef } from "react";
13
+ import { usePathname } from "next/navigation";
14
+ function observersEnabled() {
15
+ if (typeof window === "undefined") return false;
16
+ return window.__rtObserversEnabled !== false;
17
+ }
18
+ function RootTaleAnalyticsNextAdapter() {
19
+ const pathname = usePathname();
20
+ const mounted = useRef(false);
21
+ useEffect(() => {
22
+ if (mounted.current) {
23
+ trackCustomEvent("pageview");
24
+ }
25
+ mounted.current = true;
26
+ if (observersEnabled()) initObservers();
27
+ return () => teardownObservers();
28
+ }, [pathname]);
29
+ return null;
30
+ }
31
+ export {
32
+ RootTaleAnalyticsNextAdapter
33
+ };
34
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next.tsx"],"sourcesContent":["\"use client\";\n\n// 설계(measurement-design-draft.md) §5 — Next App Router SPA 어댑터.\n//\n// App Router 클라이언트 네비게이션은 풀 페이지 로드가 아니므로 beacon\n// RUNTIME 의 최초 1회 pageview(스크립트 파싱 시점)만으로는 이후 라우트\n// 전환을 놓친다. 본 어댑터가 usePathname() 변화를 감지해 pageview 재발화 +\n// observer 재바인드(teardownObservers → initObservers)를 담당한다.\n//\n// 초기 마운트에서는 pageview 를 재발화하지 않는다 — beacon RUNTIME 이 이미\n// 최초 1회 담당(중복 방지, useRef 로 첫 렌더 skip). 초기 마운트에서도\n// observer 초기화는 수행한다(첫 페이지에도 section_view 등 감지가 필요).\n//\n// `RootTaleAnalytics`(react.tsx, RSC 호환·hookless)와 함께 root layout 에\n// 나란히 배치한다:\n// <body>\n// {children}\n// <RootTaleAnalytics manifest={manifest} collectUrl={url} siteId={id} />\n// <RootTaleAnalyticsNextAdapter />\n// </body>\n\nimport { useEffect, useRef } from \"react\";\nimport { usePathname } from \"next/navigation\";\n\nimport { trackCustomEvent } from \"./beacon\";\nimport { initObservers, teardownObservers } from \"./observers\";\n\n/** `window.__rtObserversEnabled` 미설정 시 기본 활성(react.tsx 의 기본값과 동일). */\nfunction observersEnabled(): boolean {\n if (typeof window === \"undefined\") return false;\n return window.__rtObserversEnabled !== false;\n}\n\n/**\n * pathname 변화를 감지해 pageview 재발화 + observer 재바인드를 수행하는\n * 드롭인 컴포넌트. 렌더 출력 없음(`null`) — 부수효과 전용.\n */\nexport function RootTaleAnalyticsNextAdapter(): null {\n const pathname = usePathname();\n const mounted = useRef(false);\n\n useEffect(() => {\n // pathname 변화(재실행)일 때만 pageview 재발화 — 최초 마운트는 beacon\n // RUNTIME 이 이미 담당(중복 방지). teardown 은 React 가 이전 실행의\n // cleanup(아래 return)으로 자동 수행하므로 여기선 init 만 하면 SPA\n // 재바인드 계약(teardown → pageview 재발화 → init)이 자연히 성립한다.\n if (mounted.current) {\n trackCustomEvent(\"pageview\");\n }\n mounted.current = true;\n if (observersEnabled()) initObservers();\n return () => teardownObservers();\n \n }, [pathname]);\n\n return null;\n}\n"],"mappings":";;;;;;;;;;;AAqBA,SAAS,WAAW,cAAc;AAClC,SAAS,mBAAmB;AAM5B,SAAS,mBAA4B;AACnC,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAO,OAAO,yBAAyB;AACzC;AAMO,SAAS,+BAAqC;AACnD,QAAM,WAAW,YAAY;AAC7B,QAAM,UAAU,OAAO,KAAK;AAE5B,YAAU,MAAM;AAKd,QAAI,QAAQ,SAAS;AACnB,uBAAiB,UAAU;AAAA,IAC7B;AACA,YAAQ,UAAU;AAClB,QAAI,iBAAiB,EAAG,eAAc;AACtC,WAAO,MAAM,kBAAkB;AAAA,EAEjC,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAO;AACT;","names":[]}
package/dist/react.d.ts CHANGED
@@ -14,11 +14,26 @@ interface RootTaleAnalyticsProps {
14
14
  trackPageview?: boolean;
15
15
  /** 동의 배너 카피 override. */
16
16
  bannerOptions?: ConsentBannerOptions;
17
+ /**
18
+ * GA4 미러 브리지 스크립트 주입 여부(설계 §0, 기본 true). beacon 이 없으면
19
+ * (collectUrl/siteId 미설정) 어차피 미주입 — 이벤트를 흘려보낼 send() 자체가
20
+ * 없기 때문.
21
+ */
22
+ enableGa4Bridge?: boolean;
23
+ /**
24
+ * 방문 여정(observers.ts) 감지기 활성 여부(설계 §1 2단계, 기본 true). 본
25
+ * 컴포넌트는 RSC 호환(hook 없음)이라 IntersectionObserver 등을 직접
26
+ * 구동하지 못한다 — `false` 로 지정하면 `window.__rtObserversEnabled=false`
27
+ * 를 심어 `RootTaleAnalyticsNextAdapter`(next.tsx)가 initObservers() 호출
28
+ * 전 이 값을 읽고 건너뛰게 한다. `true`(기본)는 아무 것도 심지 않음(next.tsx
29
+ * 의 기본값이 이미 활성이므로 무동작이 곧 활성).
30
+ */
31
+ enableObservers?: boolean;
17
32
  }
18
33
  /**
19
34
  * 단일 드롭인 컴포넌트. RSC 호환(hook/상태 없음) — Server Component layout 에
20
35
  * 바로 배치 가능.
21
36
  */
22
- declare function RootTaleAnalytics({ manifest, collectUrl, siteId, privacyUrl, trackPageview, bannerOptions, }: RootTaleAnalyticsProps): react_jsx_runtime.JSX.Element;
37
+ declare function RootTaleAnalytics({ manifest, collectUrl, siteId, privacyUrl, trackPageview, bannerOptions, enableGa4Bridge, enableObservers, }: RootTaleAnalyticsProps): react_jsx_runtime.JSX.Element;
23
38
 
24
39
  export { RootTaleAnalytics, type RootTaleAnalyticsProps };