@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.
- package/dist/ai-referrals.d.ts +40 -0
- package/dist/ai-referrals.js +17 -0
- package/dist/ai-referrals.js.map +1 -0
- package/dist/chunk-2WQ7PVRU.js +59 -0
- package/dist/chunk-2WQ7PVRU.js.map +1 -0
- package/dist/chunk-ABGGPWIX.js +200 -0
- package/dist/chunk-ABGGPWIX.js.map +1 -0
- package/dist/chunk-ANB42OLW.js +111 -0
- package/dist/chunk-ANB42OLW.js.map +1 -0
- package/dist/{chunk-ZFY3KBJJ.js → chunk-G7OLBHT5.js} +61 -57
- package/dist/chunk-G7OLBHT5.js.map +1 -0
- package/dist/chunk-GBIGQHVN.js +123 -0
- package/dist/chunk-GBIGQHVN.js.map +1 -0
- package/dist/events.d.ts +147 -0
- package/dist/events.js +11 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +104 -3
- package/dist/index.js +45 -5
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +7 -0
- package/dist/next.js +34 -0
- package/dist/next.js.map +1 -0
- package/dist/react.d.ts +16 -1
- package/dist/react.js +32 -4
- package/dist/react.js.map +1 -1
- package/package.json +21 -1
- package/dist/chunk-ZFY3KBJJ.js.map +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 챗봇 유입 분류 — GA4 세션 소스와 자체 비콘이 같은 기준을 쓴다.
|
|
3
|
+
*
|
|
4
|
+
* 원문 주소는 저장하지 않는다. 아래 고정 목록의 호스트에 해당할 때만 챗봇
|
|
5
|
+
* id 라벨로 환산하고, 목록 밖은 버린다(분류 불가를 "기타"로 뭉뚱그리면
|
|
6
|
+
* 숫자의 의미가 사라진다).
|
|
7
|
+
*
|
|
8
|
+
* 이 파일은 브라우저 비콘 생성기와 서버 양쪽에서 참조하므로 런타임 의존이
|
|
9
|
+
* 없어야 한다.
|
|
10
|
+
*/
|
|
11
|
+
declare const AI_REFERRAL_IDS: readonly ["gemini", "chatgpt", "claude", "perplexity", "copilot"];
|
|
12
|
+
type AiReferralId = (typeof AI_REFERRAL_IDS)[number];
|
|
13
|
+
type AiReferralMetric = {
|
|
14
|
+
readonly id: AiReferralId;
|
|
15
|
+
readonly label: string;
|
|
16
|
+
readonly sessions: number;
|
|
17
|
+
};
|
|
18
|
+
/** 수집 라우트의 입력 검증 — 사전 밖 문자열이 차원에 들어가지 못하게. */
|
|
19
|
+
declare function isAiReferralId(value: string): value is AiReferralId;
|
|
20
|
+
declare const AI_REFERRAL_LABEL: Record<AiReferralId, string>;
|
|
21
|
+
/**
|
|
22
|
+
* 비콘 인라인 스크립트에 심을 규칙표 리터럴. 규칙이 여기 한 곳에만 있어야
|
|
23
|
+
* 브라우저 판정과 서버 판정이 갈리지 않는다.
|
|
24
|
+
*/
|
|
25
|
+
declare function aiReferralRulesLiteral(): string;
|
|
26
|
+
/**
|
|
27
|
+
* 유입 출처 문자열(호스트 또는 GA4 세션 소스)을 챗봇 id 로 환산한다.
|
|
28
|
+
* 목록 밖이면 null — 호출부가 집계에서 제외한다.
|
|
29
|
+
*/
|
|
30
|
+
declare function classifyAiReferral(source: string | null | undefined): AiReferralId | null;
|
|
31
|
+
/**
|
|
32
|
+
* (id, 세션 수) 쌍을 합산해 많은 순으로 정렬한다. 0 인 챗봇은 빼서
|
|
33
|
+
* "안 들어온 채널"이 차트를 채우지 않게 한다.
|
|
34
|
+
*/
|
|
35
|
+
declare function summarizeAiReferrals(entries: readonly {
|
|
36
|
+
readonly id: AiReferralId;
|
|
37
|
+
readonly sessions: number;
|
|
38
|
+
}[]): readonly AiReferralMetric[];
|
|
39
|
+
|
|
40
|
+
export { AI_REFERRAL_IDS, AI_REFERRAL_LABEL, type AiReferralId, type AiReferralMetric, aiReferralRulesLiteral, classifyAiReferral, isAiReferralId, summarizeAiReferrals };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AI_REFERRAL_IDS,
|
|
3
|
+
AI_REFERRAL_LABEL,
|
|
4
|
+
aiReferralRulesLiteral,
|
|
5
|
+
classifyAiReferral,
|
|
6
|
+
isAiReferralId,
|
|
7
|
+
summarizeAiReferrals
|
|
8
|
+
} from "./chunk-2WQ7PVRU.js";
|
|
9
|
+
export {
|
|
10
|
+
AI_REFERRAL_IDS,
|
|
11
|
+
AI_REFERRAL_LABEL,
|
|
12
|
+
aiReferralRulesLiteral,
|
|
13
|
+
classifyAiReferral,
|
|
14
|
+
isAiReferralId,
|
|
15
|
+
summarizeAiReferrals
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=ai-referrals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/ai-referrals.ts
|
|
2
|
+
var AI_REFERRAL_IDS = [
|
|
3
|
+
"gemini",
|
|
4
|
+
"chatgpt",
|
|
5
|
+
"claude",
|
|
6
|
+
"perplexity",
|
|
7
|
+
"copilot"
|
|
8
|
+
];
|
|
9
|
+
function isAiReferralId(value) {
|
|
10
|
+
return AI_REFERRAL_IDS.includes(value);
|
|
11
|
+
}
|
|
12
|
+
var AI_REFERRAL_LABEL = {
|
|
13
|
+
gemini: "\uC81C\uBBF8\uB098\uC774",
|
|
14
|
+
chatgpt: "GPT",
|
|
15
|
+
claude: "\uD074\uB85C\uB4DC",
|
|
16
|
+
perplexity: "\uD37C\uD50C\uB809\uC2DC\uD2F0",
|
|
17
|
+
copilot: "\uCF54\uD30C\uC77C\uB7FF"
|
|
18
|
+
};
|
|
19
|
+
var HOST_RULES = [
|
|
20
|
+
{ host: "gemini.google.com", id: "gemini" },
|
|
21
|
+
{ host: "bard.google.com", id: "gemini" },
|
|
22
|
+
{ host: "chatgpt.com", id: "chatgpt" },
|
|
23
|
+
{ host: "chat.openai.com", id: "chatgpt" },
|
|
24
|
+
{ host: "openai.com", id: "chatgpt" },
|
|
25
|
+
{ host: "claude.ai", id: "claude" },
|
|
26
|
+
{ host: "perplexity.ai", id: "perplexity" },
|
|
27
|
+
{ host: "copilot.microsoft.com", id: "copilot" },
|
|
28
|
+
{ host: "m365.cloud.microsoft", id: "copilot" }
|
|
29
|
+
];
|
|
30
|
+
function matchesHost(candidate, rule) {
|
|
31
|
+
return candidate === rule || candidate.endsWith(`.${rule}`);
|
|
32
|
+
}
|
|
33
|
+
function aiReferralRulesLiteral() {
|
|
34
|
+
return JSON.stringify(HOST_RULES.map((rule) => [rule.host, rule.id]));
|
|
35
|
+
}
|
|
36
|
+
function classifyAiReferral(source) {
|
|
37
|
+
if (!source) return null;
|
|
38
|
+
const normalized = source.trim().toLowerCase().replace(/^www\./, "");
|
|
39
|
+
if (normalized === "") return null;
|
|
40
|
+
const rule = HOST_RULES.find((candidate) => matchesHost(normalized, candidate.host));
|
|
41
|
+
return rule?.id ?? null;
|
|
42
|
+
}
|
|
43
|
+
function summarizeAiReferrals(entries) {
|
|
44
|
+
const totals = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
totals.set(entry.id, (totals.get(entry.id) ?? 0) + entry.sessions);
|
|
47
|
+
}
|
|
48
|
+
return [...totals.entries()].filter(([, sessions]) => sessions > 0).map(([id, sessions]) => ({ id, label: AI_REFERRAL_LABEL[id], sessions })).sort((left, right) => right.sessions - left.sessions);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
AI_REFERRAL_IDS,
|
|
53
|
+
isAiReferralId,
|
|
54
|
+
AI_REFERRAL_LABEL,
|
|
55
|
+
aiReferralRulesLiteral,
|
|
56
|
+
classifyAiReferral,
|
|
57
|
+
summarizeAiReferrals
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=chunk-2WQ7PVRU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ai-referrals.ts"],"sourcesContent":["/**\n * AI 챗봇 유입 분류 — GA4 세션 소스와 자체 비콘이 같은 기준을 쓴다.\n *\n * 원문 주소는 저장하지 않는다. 아래 고정 목록의 호스트에 해당할 때만 챗봇\n * id 라벨로 환산하고, 목록 밖은 버린다(분류 불가를 \"기타\"로 뭉뚱그리면\n * 숫자의 의미가 사라진다).\n *\n * 이 파일은 브라우저 비콘 생성기와 서버 양쪽에서 참조하므로 런타임 의존이\n * 없어야 한다.\n */\n\nexport const AI_REFERRAL_IDS = [\n \"gemini\",\n \"chatgpt\",\n \"claude\",\n \"perplexity\",\n \"copilot\",\n] as const;\n\nexport type AiReferralId = (typeof AI_REFERRAL_IDS)[number];\n\nexport type AiReferralMetric = {\n readonly id: AiReferralId;\n readonly label: string;\n readonly sessions: number;\n};\n\n/** 수집 라우트의 입력 검증 — 사전 밖 문자열이 차원에 들어가지 못하게. */\nexport function isAiReferralId(value: string): value is AiReferralId {\n return (AI_REFERRAL_IDS as readonly string[]).includes(value);\n}\n\nexport const AI_REFERRAL_LABEL: Record<AiReferralId, string> = {\n gemini: \"제미나이\",\n chatgpt: \"GPT\",\n claude: \"클로드\",\n perplexity: \"퍼플렉시티\",\n copilot: \"코파일럿\",\n};\n\n/**\n * 호스트 판정 규칙. 정확 일치 또는 서브도메인(`.` 접미) 일치만 인정한다 —\n * 부분 문자열 매칭은 `chatgpt.evil.com` 같은 위장 도메인을 통과시킨다.\n */\nconst HOST_RULES: readonly { readonly host: string; readonly id: AiReferralId }[] = [\n { host: \"gemini.google.com\", id: \"gemini\" },\n { host: \"bard.google.com\", id: \"gemini\" },\n { host: \"chatgpt.com\", id: \"chatgpt\" },\n { host: \"chat.openai.com\", id: \"chatgpt\" },\n { host: \"openai.com\", id: \"chatgpt\" },\n { host: \"claude.ai\", id: \"claude\" },\n { host: \"perplexity.ai\", id: \"perplexity\" },\n { host: \"copilot.microsoft.com\", id: \"copilot\" },\n { host: \"m365.cloud.microsoft\", id: \"copilot\" },\n];\n\nfunction matchesHost(candidate: string, rule: string): boolean {\n return candidate === rule || candidate.endsWith(`.${rule}`);\n}\n\n/**\n * 비콘 인라인 스크립트에 심을 규칙표 리터럴. 규칙이 여기 한 곳에만 있어야\n * 브라우저 판정과 서버 판정이 갈리지 않는다.\n */\nexport function aiReferralRulesLiteral(): string {\n return JSON.stringify(HOST_RULES.map((rule) => [rule.host, rule.id]));\n}\n\n/**\n * 유입 출처 문자열(호스트 또는 GA4 세션 소스)을 챗봇 id 로 환산한다.\n * 목록 밖이면 null — 호출부가 집계에서 제외한다.\n */\nexport function classifyAiReferral(source: string | null | undefined): AiReferralId | null {\n if (!source) return null;\n const normalized = source.trim().toLowerCase().replace(/^www\\./, \"\");\n if (normalized === \"\") return null;\n const rule = HOST_RULES.find((candidate) => matchesHost(normalized, candidate.host));\n return rule?.id ?? null;\n}\n\n/**\n * (id, 세션 수) 쌍을 합산해 많은 순으로 정렬한다. 0 인 챗봇은 빼서\n * \"안 들어온 채널\"이 차트를 채우지 않게 한다.\n */\nexport function summarizeAiReferrals(\n entries: readonly { readonly id: AiReferralId; readonly sessions: number }[],\n): readonly AiReferralMetric[] {\n const totals = new Map<AiReferralId, number>();\n for (const entry of entries) {\n totals.set(entry.id, (totals.get(entry.id) ?? 0) + entry.sessions);\n }\n return [...totals.entries()]\n .filter(([, sessions]) => sessions > 0)\n .map(([id, sessions]) => ({ id, label: AI_REFERRAL_LABEL[id], sessions }))\n .sort((left, right) => right.sessions - left.sessions);\n}\n"],"mappings":";AAWO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAWO,SAAS,eAAe,OAAsC;AACnE,SAAQ,gBAAsC,SAAS,KAAK;AAC9D;AAEO,IAAM,oBAAkD;AAAA,EAC7D,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,SAAS;AACX;AAMA,IAAM,aAA8E;AAAA,EAClF,EAAE,MAAM,qBAAqB,IAAI,SAAS;AAAA,EAC1C,EAAE,MAAM,mBAAmB,IAAI,SAAS;AAAA,EACxC,EAAE,MAAM,eAAe,IAAI,UAAU;AAAA,EACrC,EAAE,MAAM,mBAAmB,IAAI,UAAU;AAAA,EACzC,EAAE,MAAM,cAAc,IAAI,UAAU;AAAA,EACpC,EAAE,MAAM,aAAa,IAAI,SAAS;AAAA,EAClC,EAAE,MAAM,iBAAiB,IAAI,aAAa;AAAA,EAC1C,EAAE,MAAM,yBAAyB,IAAI,UAAU;AAAA,EAC/C,EAAE,MAAM,wBAAwB,IAAI,UAAU;AAChD;AAEA,SAAS,YAAY,WAAmB,MAAuB;AAC7D,SAAO,cAAc,QAAQ,UAAU,SAAS,IAAI,IAAI,EAAE;AAC5D;AAMO,SAAS,yBAAiC;AAC/C,SAAO,KAAK,UAAU,WAAW,IAAI,CAAC,SAAS,CAAC,KAAK,MAAM,KAAK,EAAE,CAAC,CAAC;AACtE;AAMO,SAAS,mBAAmB,QAAwD;AACzF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,aAAa,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,UAAU,EAAE;AACnE,MAAI,eAAe,GAAI,QAAO;AAC9B,QAAM,OAAO,WAAW,KAAK,CAAC,cAAc,YAAY,YAAY,UAAU,IAAI,CAAC;AACnF,SAAO,MAAM,MAAM;AACrB;AAMO,SAAS,qBACd,SAC6B;AAC7B,QAAM,SAAS,oBAAI,IAA0B;AAC7C,aAAW,SAAS,SAAS;AAC3B,WAAO,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,EAAE,KAAK,KAAK,MAAM,QAAQ;AAAA,EACnE;AACA,SAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,EACxB,OAAO,CAAC,CAAC,EAAE,QAAQ,MAAM,WAAW,CAAC,EACrC,IAAI,CAAC,CAAC,IAAI,QAAQ,OAAO,EAAE,IAAI,OAAO,kBAAkB,EAAE,GAAG,SAAS,EAAE,EACxE,KAAK,CAAC,MAAM,UAAU,MAAM,WAAW,KAAK,QAAQ;AACzD;","names":[]}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
trackCustomEvent
|
|
3
|
+
} from "./chunk-ANB42OLW.js";
|
|
4
|
+
|
|
5
|
+
// src/observers.ts
|
|
6
|
+
var SCROLL_MILESTONES = [25, 50, 75, 100];
|
|
7
|
+
var READ_COMPLETE_THRESHOLD = 0.75;
|
|
8
|
+
var READ_COMPLETE_DWELL_MS = 15e3;
|
|
9
|
+
var sectionObserver = null;
|
|
10
|
+
var readObserver = null;
|
|
11
|
+
var readTimer = null;
|
|
12
|
+
var readFired = false;
|
|
13
|
+
var scrollFired = /* @__PURE__ */ new Set();
|
|
14
|
+
var scrollHandler = null;
|
|
15
|
+
var formFocusHandler = null;
|
|
16
|
+
var formInputHandler = null;
|
|
17
|
+
var formSubmitHandler = null;
|
|
18
|
+
var pagehideHandler = null;
|
|
19
|
+
var formState = /* @__PURE__ */ new WeakMap();
|
|
20
|
+
function initSectionView() {
|
|
21
|
+
const els = document.querySelectorAll("[data-track-view]");
|
|
22
|
+
if (els.length === 0) return;
|
|
23
|
+
const fired = /* @__PURE__ */ new Set();
|
|
24
|
+
const observer = new IntersectionObserver(
|
|
25
|
+
(entries) => {
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (!entry.isIntersecting || fired.has(entry.target)) continue;
|
|
28
|
+
fired.add(entry.target);
|
|
29
|
+
const sectionId = entry.target.getAttribute("data-track-view");
|
|
30
|
+
trackCustomEvent(
|
|
31
|
+
"section_view",
|
|
32
|
+
sectionId ? { section_id: sectionId } : void 0
|
|
33
|
+
);
|
|
34
|
+
observer.unobserve(entry.target);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{ threshold: 0.5 }
|
|
38
|
+
);
|
|
39
|
+
els.forEach((el) => observer.observe(el));
|
|
40
|
+
sectionObserver = observer;
|
|
41
|
+
}
|
|
42
|
+
function initScrollDepth() {
|
|
43
|
+
scrollFired = /* @__PURE__ */ new Set();
|
|
44
|
+
const onScroll = () => {
|
|
45
|
+
const doc = document.documentElement;
|
|
46
|
+
const scrollable = doc.scrollHeight - doc.clientHeight;
|
|
47
|
+
const pct = scrollable <= 0 ? 100 : Math.round(window.scrollY / scrollable * 100);
|
|
48
|
+
for (const milestone of SCROLL_MILESTONES) {
|
|
49
|
+
if (pct >= milestone && !scrollFired.has(milestone)) {
|
|
50
|
+
scrollFired.add(milestone);
|
|
51
|
+
trackCustomEvent("scroll_depth", { pct: milestone });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
scrollHandler = onScroll;
|
|
56
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
57
|
+
onScroll();
|
|
58
|
+
}
|
|
59
|
+
function initReadComplete() {
|
|
60
|
+
const article = document.querySelector("[data-track-read]");
|
|
61
|
+
if (!article) return;
|
|
62
|
+
readFired = false;
|
|
63
|
+
const observer = new IntersectionObserver(
|
|
64
|
+
(entries) => {
|
|
65
|
+
const entry = entries[0];
|
|
66
|
+
if (!entry) return;
|
|
67
|
+
const visibleEnough = entry.isIntersecting && entry.intersectionRatio >= READ_COMPLETE_THRESHOLD;
|
|
68
|
+
if (visibleEnough) {
|
|
69
|
+
if (readTimer || readFired) return;
|
|
70
|
+
readTimer = setTimeout(() => {
|
|
71
|
+
readTimer = null;
|
|
72
|
+
if (readFired) return;
|
|
73
|
+
readFired = true;
|
|
74
|
+
const pid = article.getAttribute("data-track-read");
|
|
75
|
+
trackCustomEvent("read_complete", pid ? { pid } : void 0);
|
|
76
|
+
}, READ_COMPLETE_DWELL_MS);
|
|
77
|
+
} else if (readTimer) {
|
|
78
|
+
clearTimeout(readTimer);
|
|
79
|
+
readTimer = null;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{ threshold: READ_COMPLETE_THRESHOLD }
|
|
83
|
+
);
|
|
84
|
+
observer.observe(article);
|
|
85
|
+
readObserver = observer;
|
|
86
|
+
}
|
|
87
|
+
function trackedForm(target) {
|
|
88
|
+
const el = target;
|
|
89
|
+
return el?.closest?.("form[data-track-form]") ?? null;
|
|
90
|
+
}
|
|
91
|
+
function initFormTracking() {
|
|
92
|
+
const onFocusIn = (e) => {
|
|
93
|
+
const form = trackedForm(e.target);
|
|
94
|
+
if (!form) return;
|
|
95
|
+
const state = formState.get(form) ?? {
|
|
96
|
+
started: false,
|
|
97
|
+
dirty: false,
|
|
98
|
+
submitted: false
|
|
99
|
+
};
|
|
100
|
+
if (!state.started) {
|
|
101
|
+
state.started = true;
|
|
102
|
+
const formId = form.getAttribute("data-track-form");
|
|
103
|
+
trackCustomEvent("form_start", formId ? { form_id: formId } : void 0);
|
|
104
|
+
}
|
|
105
|
+
formState.set(form, state);
|
|
106
|
+
};
|
|
107
|
+
const onInput = (e) => {
|
|
108
|
+
const form = trackedForm(e.target);
|
|
109
|
+
if (!form) return;
|
|
110
|
+
const state = formState.get(form) ?? {
|
|
111
|
+
started: false,
|
|
112
|
+
dirty: false,
|
|
113
|
+
submitted: false
|
|
114
|
+
};
|
|
115
|
+
state.dirty = true;
|
|
116
|
+
const target = e.target;
|
|
117
|
+
const fieldName = target?.name || target?.id;
|
|
118
|
+
if (fieldName) state.lastField = fieldName;
|
|
119
|
+
formState.set(form, state);
|
|
120
|
+
};
|
|
121
|
+
const onSubmit = (e) => {
|
|
122
|
+
const form = e.target;
|
|
123
|
+
if (!form?.matches?.("form[data-track-form]")) return;
|
|
124
|
+
const state = formState.get(form);
|
|
125
|
+
if (state) {
|
|
126
|
+
state.submitted = true;
|
|
127
|
+
formState.set(form, state);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
document.addEventListener("focusin", onFocusIn, true);
|
|
131
|
+
document.addEventListener("input", onInput, true);
|
|
132
|
+
document.addEventListener("change", onInput, true);
|
|
133
|
+
document.addEventListener("submit", onSubmit, true);
|
|
134
|
+
formFocusHandler = onFocusIn;
|
|
135
|
+
formInputHandler = onInput;
|
|
136
|
+
formSubmitHandler = onSubmit;
|
|
137
|
+
}
|
|
138
|
+
function initFormAbandon() {
|
|
139
|
+
const onPageHide = (e) => {
|
|
140
|
+
if (e.persisted) return;
|
|
141
|
+
document.querySelectorAll("form[data-track-form]").forEach((form) => {
|
|
142
|
+
const state = formState.get(form);
|
|
143
|
+
if (!state || !state.dirty || state.submitted) return;
|
|
144
|
+
const formId = form.getAttribute("data-track-form");
|
|
145
|
+
trackCustomEvent("form_abandon", {
|
|
146
|
+
...formId ? { form_id: formId } : {},
|
|
147
|
+
...state.lastField ? { last_field: state.lastField } : {}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
pagehideHandler = onPageHide;
|
|
152
|
+
window.addEventListener("pagehide", onPageHide);
|
|
153
|
+
}
|
|
154
|
+
function initObservers() {
|
|
155
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
156
|
+
initSectionView();
|
|
157
|
+
initScrollDepth();
|
|
158
|
+
initReadComplete();
|
|
159
|
+
initFormTracking();
|
|
160
|
+
initFormAbandon();
|
|
161
|
+
}
|
|
162
|
+
function teardownObservers() {
|
|
163
|
+
sectionObserver?.disconnect();
|
|
164
|
+
sectionObserver = null;
|
|
165
|
+
readObserver?.disconnect();
|
|
166
|
+
readObserver = null;
|
|
167
|
+
if (readTimer) {
|
|
168
|
+
clearTimeout(readTimer);
|
|
169
|
+
readTimer = null;
|
|
170
|
+
}
|
|
171
|
+
readFired = false;
|
|
172
|
+
if (scrollHandler) {
|
|
173
|
+
window.removeEventListener("scroll", scrollHandler);
|
|
174
|
+
scrollHandler = null;
|
|
175
|
+
}
|
|
176
|
+
scrollFired = /* @__PURE__ */ new Set();
|
|
177
|
+
if (formFocusHandler) {
|
|
178
|
+
document.removeEventListener("focusin", formFocusHandler, true);
|
|
179
|
+
formFocusHandler = null;
|
|
180
|
+
}
|
|
181
|
+
if (formInputHandler) {
|
|
182
|
+
document.removeEventListener("input", formInputHandler, true);
|
|
183
|
+
document.removeEventListener("change", formInputHandler, true);
|
|
184
|
+
formInputHandler = null;
|
|
185
|
+
}
|
|
186
|
+
if (formSubmitHandler) {
|
|
187
|
+
document.removeEventListener("submit", formSubmitHandler, true);
|
|
188
|
+
formSubmitHandler = null;
|
|
189
|
+
}
|
|
190
|
+
if (pagehideHandler) {
|
|
191
|
+
window.removeEventListener("pagehide", pagehideHandler);
|
|
192
|
+
pagehideHandler = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
initObservers,
|
|
198
|
+
teardownObservers
|
|
199
|
+
};
|
|
200
|
+
//# sourceMappingURL=chunk-ABGGPWIX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/observers.ts"],"sourcesContent":["// 설계(measurement-design-draft.md) §1(2단계) — 브라우저 감지기: section_view·\n// scroll_depth·read_complete·form_start·form_abandon.\n//\n// 인라인 RUNTIME 문자열이 아닌 일반 ESM — react/next 어댑터가 import 해\n// 호출한다. IntersectionObserver 인스턴스·WeakMap 상태 등 모듈 스코프 가변\n// 상태가 필요해 beacon/loader 처럼 문자열 템플릿으로 표현하기 부적합하다\n// (send() 하나만 훅하면 되는 ga4-bridge/journey 와는 성격이 다르다).\n//\n// data 속성 계약(starter 등 소비 측이 emit):\n// data-track-view=\"<section_id>\" section_view, 뷰포트 도달 1회/페이지\n// article[data-track-read=\"<pid>\"] read_complete, 75%+15초 체류\n// form[data-track-form=\"<form_id>\"] form_start(첫 focusin)·form_abandon\n//\n// SPA 재바인드 계약: pathname 변화 시 next.tsx 어댑터가\n// teardownObservers() → initObservers() 를 호출해 새 DOM 에 옵저버를\n// 재바인드한다. 모든 발화는 `trackCustomEvent()`(beacon.ts) 를 거치므로\n// journey/GA4 미러 훅에도 동일하게 흘러간다(단, 본 파일 이벤트는 전부\n// events.ts 상 journey:false·ga4:false — 고빈도/참여 신호라 훅은 그냥\n// no-op한다).\n\nimport { trackCustomEvent } from \"./beacon\";\n\nconst SCROLL_MILESTONES = [25, 50, 75, 100] as const;\nconst READ_COMPLETE_THRESHOLD = 0.75;\nconst READ_COMPLETE_DWELL_MS = 15_000;\n\ninterface FormTrackState {\n started: boolean;\n dirty: boolean;\n submitted: boolean;\n lastField?: string;\n}\n\nlet sectionObserver: IntersectionObserver | null = null;\nlet readObserver: IntersectionObserver | null = null;\nlet readTimer: ReturnType<typeof setTimeout> | null = null;\nlet readFired = false;\nlet scrollFired = new Set<number>();\nlet scrollHandler: (() => void) | null = null;\nlet formFocusHandler: ((e: Event) => void) | null = null;\nlet formInputHandler: ((e: Event) => void) | null = null;\nlet formSubmitHandler: ((e: Event) => void) | null = null;\nlet pagehideHandler: ((e: PageTransitionEvent) => void) | null = null;\n\nconst formState = new WeakMap<Element, FormTrackState>();\n\nfunction initSectionView(): void {\n const els = document.querySelectorAll<HTMLElement>(\"[data-track-view]\");\n if (els.length === 0) return;\n const fired = new Set<Element>();\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting || fired.has(entry.target)) continue;\n fired.add(entry.target);\n const sectionId = entry.target.getAttribute(\"data-track-view\");\n trackCustomEvent(\n \"section_view\",\n sectionId ? { section_id: sectionId } : undefined,\n );\n observer.unobserve(entry.target);\n }\n },\n { threshold: 0.5 },\n );\n els.forEach((el) => observer.observe(el));\n sectionObserver = observer;\n}\n\nfunction initScrollDepth(): void {\n scrollFired = new Set<number>();\n const onScroll = (): void => {\n const doc = document.documentElement;\n const scrollable = doc.scrollHeight - doc.clientHeight;\n // 스크롤 가능한 영역이 없는(뷰포트보다 짧은) 페이지는 전부 보인 것으로\n // 간주 — 100% 로 간주해 4개 이정표를 즉시 만족시킨다(의도된 동작).\n const pct =\n scrollable <= 0\n ? 100\n : Math.round((window.scrollY / scrollable) * 100);\n for (const milestone of SCROLL_MILESTONES) {\n if (pct >= milestone && !scrollFired.has(milestone)) {\n scrollFired.add(milestone);\n trackCustomEvent(\"scroll_depth\", { pct: milestone });\n }\n }\n };\n scrollHandler = onScroll;\n window.addEventListener(\"scroll\", onScroll, { passive: true });\n onScroll();\n}\n\nfunction initReadComplete(): void {\n const article = document.querySelector<HTMLElement>(\"[data-track-read]\");\n if (!article) return;\n readFired = false;\n const observer = new IntersectionObserver(\n (entries) => {\n const entry = entries[0];\n if (!entry) return;\n const visibleEnough =\n entry.isIntersecting &&\n entry.intersectionRatio >= READ_COMPLETE_THRESHOLD;\n if (visibleEnough) {\n if (readTimer || readFired) return;\n // heuristic: 75%+ 가시 상태가 15초 연속 유지될 때만 발화. 중간에\n // 임계 아래로 떨어지면 타이머를 취소(재진입 시 처음부터 다시 잰다).\n readTimer = setTimeout(() => {\n readTimer = null;\n if (readFired) return;\n readFired = true;\n const pid = article.getAttribute(\"data-track-read\");\n trackCustomEvent(\"read_complete\", pid ? { pid } : undefined);\n }, READ_COMPLETE_DWELL_MS);\n } else if (readTimer) {\n clearTimeout(readTimer);\n readTimer = null;\n }\n },\n { threshold: READ_COMPLETE_THRESHOLD },\n );\n observer.observe(article);\n readObserver = observer;\n}\n\nfunction trackedForm(target: EventTarget | null): Element | null {\n const el = target as Element | null;\n return el?.closest?.(\"form[data-track-form]\") ?? null;\n}\n\nfunction initFormTracking(): void {\n const onFocusIn = (e: Event): void => {\n const form = trackedForm(e.target);\n if (!form) return;\n const state = formState.get(form) ?? {\n started: false,\n dirty: false,\n submitted: false,\n };\n if (!state.started) {\n state.started = true;\n const formId = form.getAttribute(\"data-track-form\");\n trackCustomEvent(\"form_start\", formId ? { form_id: formId } : undefined);\n }\n formState.set(form, state);\n };\n const onInput = (e: Event): void => {\n const form = trackedForm(e.target);\n if (!form) return;\n const state = formState.get(form) ?? {\n started: false,\n dirty: false,\n submitted: false,\n };\n state.dirty = true;\n const target = e.target as { name?: string; id?: string } | null;\n const fieldName = target?.name || target?.id;\n if (fieldName) state.lastField = fieldName;\n formState.set(form, state);\n };\n const onSubmit = (e: Event): void => {\n const form = e.target as Element | null;\n if (!form?.matches?.(\"form[data-track-form]\")) return;\n const state = formState.get(form);\n if (state) {\n state.submitted = true;\n formState.set(form, state);\n }\n };\n document.addEventListener(\"focusin\", onFocusIn, true);\n document.addEventListener(\"input\", onInput, true);\n document.addEventListener(\"change\", onInput, true);\n document.addEventListener(\"submit\", onSubmit, true);\n formFocusHandler = onFocusIn;\n formInputHandler = onInput;\n formSubmitHandler = onSubmit;\n}\n\nfunction initFormAbandon(): void {\n const onPageHide = (e: PageTransitionEvent): void => {\n // heuristic(설계 §1 P1-5, KPI 산정 제외): dirty && !event.persisted &&\n // 미제출. bfcache 로 보존되는(persisted=true) 페이지 은닉은 실제 이탈이\n // 아니므로 제외 — 근사 신호일 뿐이라 journey:false·ga4:false.\n if (e.persisted) return;\n document\n .querySelectorAll<HTMLFormElement>(\"form[data-track-form]\")\n .forEach((form) => {\n const state = formState.get(form);\n if (!state || !state.dirty || state.submitted) return;\n const formId = form.getAttribute(\"data-track-form\");\n trackCustomEvent(\"form_abandon\", {\n ...(formId ? { form_id: formId } : {}),\n ...(state.lastField ? { last_field: state.lastField } : {}),\n });\n });\n };\n pagehideHandler = onPageHide;\n window.addEventListener(\"pagehide\", onPageHide);\n}\n\n/**\n * 감지기 초기화 — section_view·scroll_depth·read_complete·form_start·\n * form_abandon. SSR/비브라우저 환경에서는 no-op. teardownObservers() 없이\n * 재호출하면 중복 바인딩될 수 있다 — 어댑터는 pathname 변화 시 teardown →\n * init 순서를 지킨다(SPA 재바인드 계약).\n */\nexport function initObservers(): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n initSectionView();\n initScrollDepth();\n initReadComplete();\n initFormTracking();\n initFormAbandon();\n}\n\n/** 모든 감지기 해제 — SPA pathname 변화 시 재바인드 전에 호출한다. */\nexport function teardownObservers(): void {\n sectionObserver?.disconnect();\n sectionObserver = null;\n readObserver?.disconnect();\n readObserver = null;\n if (readTimer) {\n clearTimeout(readTimer);\n readTimer = null;\n }\n readFired = false;\n if (scrollHandler) {\n window.removeEventListener(\"scroll\", scrollHandler);\n scrollHandler = null;\n }\n scrollFired = new Set<number>();\n if (formFocusHandler) {\n document.removeEventListener(\"focusin\", formFocusHandler, true);\n formFocusHandler = null;\n }\n if (formInputHandler) {\n document.removeEventListener(\"input\", formInputHandler, true);\n document.removeEventListener(\"change\", formInputHandler, true);\n formInputHandler = null;\n }\n if (formSubmitHandler) {\n document.removeEventListener(\"submit\", formSubmitHandler, true);\n formSubmitHandler = null;\n }\n if (pagehideHandler) {\n window.removeEventListener(\"pagehide\", pagehideHandler);\n pagehideHandler = null;\n }\n}\n\ndeclare global {\n interface Window {\n /**\n * react.tsx(RSC, hookless) 가 명시적으로 `enableObservers={false}` 일 때만\n * `false` 를 인라인 스크립트로 심는다 — next.tsx 어댑터가 initObservers()\n * 호출 전 이 값을 확인한다(미설정 시 기본 true, \"기본 true\" 계약 구현).\n */\n __rtObserversEnabled?: boolean;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aiReferralRulesLiteral
|
|
3
|
+
} from "./chunk-2WQ7PVRU.js";
|
|
4
|
+
|
|
5
|
+
// src/beacon.ts
|
|
6
|
+
var ATTRIBUTION_STORAGE_KEY = "_rt_attr";
|
|
7
|
+
var ATTRIBUTION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
8
|
+
var RUNTIME = `(function(){
|
|
9
|
+
var C=__RT_BEACON__;
|
|
10
|
+
var AIR=${aiReferralRulesLiteral()};
|
|
11
|
+
/* AI \uCC57\uBD07 \uC720\uC785 \uD310\uC815 \u2014 \uC6D0\uBB38 referrer \uB294 \uBCF4\uB0B4\uC9C0 \uC54A\uACE0 \uACE0\uC815 \uBAA9\uB85D\uC758 \uB77C\uBCA8\uB9CC \uB0A8\uAE34\uB2E4.
|
|
12
|
+
\uBAA9\uB85D \uBC16(\uC77C\uBC18 \uC0AC\uC774\uD2B8\xB7\uAC80\uC0C9\uC5D4\uC9C4)\uC740 null \uC774\uB77C \uC544\uBB34\uAC83\uB3C4 \uC804\uC1A1\uB418\uC9C0 \uC54A\uB294\uB2E4. */
|
|
13
|
+
function aiRef(){try{
|
|
14
|
+
if(!document.referrer)return null;
|
|
15
|
+
var h=new URL(document.referrer).hostname.toLowerCase().replace(/^www\\./,'');
|
|
16
|
+
if(!h||h===location.hostname.toLowerCase().replace(/^www\\./,''))return null;
|
|
17
|
+
for(var i=0;i<AIR.length;i++){var r=AIR[i][0];if(h===r||h.slice(-(r.length+1))==='.'+r)return AIR[i][1];}
|
|
18
|
+
return null;
|
|
19
|
+
}catch(e){return null;}}
|
|
20
|
+
function metaPid(){try{var m=document.querySelector('meta[name="rt:content-id"]');var v=m&&m.getAttribute('content');return v?String(v).slice(0,64):null;}catch(e){return null;}}
|
|
21
|
+
function send(ev,extra){
|
|
22
|
+
try{
|
|
23
|
+
var p={ev:ev,sid:C.sid,path:location.pathname,ref:document.referrer?1:0,ts:Date.now()};
|
|
24
|
+
if(extra){for(var k in extra){p[k]=extra[k];}}
|
|
25
|
+
var body=JSON.stringify(p);
|
|
26
|
+
if(navigator.sendBeacon){navigator.sendBeacon(C.ep,new Blob([body],{type:'application/json'}));}
|
|
27
|
+
else{fetch(C.ep,{method:'POST',body:body,headers:{'Content-Type':'application/json'},keepalive:true}).catch(function(){});}
|
|
28
|
+
}catch(e){}
|
|
29
|
+
try{if(window.__rtGa4Mirror)window.__rtGa4Mirror(ev,extra);}catch(e){}
|
|
30
|
+
try{if(window.__rtJourneyRecord)window.__rtJourneyRecord(ev,extra);}catch(e){}
|
|
31
|
+
}
|
|
32
|
+
if(typeof window!=='undefined'){window.__rtTrack=send;}
|
|
33
|
+
document.addEventListener('click',function(e){
|
|
34
|
+
var t=e.target;
|
|
35
|
+
var el=t&&t.closest?t.closest('[data-track]'):null;
|
|
36
|
+
if(!el)return;
|
|
37
|
+
var extra={};
|
|
38
|
+
var ds=el.dataset||{};
|
|
39
|
+
for(var k in ds){if(k.indexOf('track')===0&&k!=='track'){extra[k]=ds[k];}}
|
|
40
|
+
send(el.getAttribute('data-track'),extra);
|
|
41
|
+
},{capture:true});
|
|
42
|
+
if(C.attr){
|
|
43
|
+
try{
|
|
44
|
+
var AK='_rt_attr';
|
|
45
|
+
var cur=null;
|
|
46
|
+
try{cur=JSON.parse(localStorage.getItem(AK)||'null');}catch(e){}
|
|
47
|
+
var expired=!cur||typeof cur.ts!=='number'||Date.now()-cur.ts>2592e6;
|
|
48
|
+
var sp=new URLSearchParams(location.search);
|
|
49
|
+
var rs=sp.get('rt_src');
|
|
50
|
+
var us=sp.get('utm_source');
|
|
51
|
+
var um=sp.get('utm_medium');
|
|
52
|
+
var uc=sp.get('utm_campaign');
|
|
53
|
+
var ut=sp.get('utm_term');
|
|
54
|
+
var un=sp.get('utm_content');
|
|
55
|
+
var hasGcl=!!(sp.get('gclid')||sp.get('fbclid'));
|
|
56
|
+
var extRef=null;
|
|
57
|
+
try{if(document.referrer){var rh=new URL(document.referrer).hostname;if(rh&&rh!==location.hostname)extRef=rh.slice(0,253);}}catch(e){}
|
|
58
|
+
var env=cur;
|
|
59
|
+
if(expired){
|
|
60
|
+
env={v:2,lp:location.pathname,ts:Date.now()};
|
|
61
|
+
if(rs)env.rs=rs.slice(0,32);
|
|
62
|
+
if(us)env.us=us.slice(0,100);
|
|
63
|
+
if(um)env.um=um.slice(0,100);
|
|
64
|
+
if(uc)env.uc=uc.slice(0,100);
|
|
65
|
+
if(ut)env.ut=ut.slice(0,100);
|
|
66
|
+
if(un)env.un=un.slice(0,100);
|
|
67
|
+
if(hasGcl)env.gcl=1;
|
|
68
|
+
if(extRef)env.rf=extRef;
|
|
69
|
+
}
|
|
70
|
+
var ch=null;
|
|
71
|
+
if(rs){ch='rs';}else if(us||um||uc||ut||un){ch='utm';}else if(hasGcl){ch='gcl';}else if(extRef){ch='rf';}
|
|
72
|
+
if(ch){
|
|
73
|
+
var lt={ts:Date.now(),ch:ch};
|
|
74
|
+
if(rs)lt.rs=rs.slice(0,32);
|
|
75
|
+
if(us)lt.us=us.slice(0,100);
|
|
76
|
+
if(um)lt.um=um.slice(0,100);
|
|
77
|
+
if(uc)lt.uc=uc.slice(0,100);
|
|
78
|
+
if(ut)lt.ut=ut.slice(0,100);
|
|
79
|
+
if(un)lt.un=un.slice(0,100);
|
|
80
|
+
if(hasGcl)lt.gcl=1;
|
|
81
|
+
if(extRef)lt.rf=extRef;
|
|
82
|
+
env.lt=lt;
|
|
83
|
+
}
|
|
84
|
+
if(expired||ch){localStorage.setItem(AK,JSON.stringify(env));}
|
|
85
|
+
}catch(e){}
|
|
86
|
+
}
|
|
87
|
+
if(C.pv){var _pid=metaPid();var _air=aiRef();var _x=null;
|
|
88
|
+
if(_pid||_air){_x={};if(_pid)_x.pid=_pid;if(_air)_x.air=_air;}
|
|
89
|
+
send('pageview',_x);}
|
|
90
|
+
})();`;
|
|
91
|
+
function renderBeaconScript(options) {
|
|
92
|
+
const cfg = JSON.stringify({
|
|
93
|
+
ep: options.collectUrl,
|
|
94
|
+
sid: options.siteId,
|
|
95
|
+
pv: options.trackPageview !== false,
|
|
96
|
+
attr: options.captureAttribution !== false
|
|
97
|
+
});
|
|
98
|
+
return "var __RT_BEACON__=" + cfg + ";" + RUNTIME;
|
|
99
|
+
}
|
|
100
|
+
function trackCustomEvent(ev, extra) {
|
|
101
|
+
if (typeof window === "undefined") return;
|
|
102
|
+
window.__rtTrack?.(ev, extra);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
ATTRIBUTION_STORAGE_KEY,
|
|
107
|
+
ATTRIBUTION_TTL_MS,
|
|
108
|
+
renderBeaconScript,
|
|
109
|
+
trackCustomEvent
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=chunk-ANB42OLW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/beacon.ts"],"sourcesContent":["// ADR-0038 plane ② — first-party 비콘 dispatcher.\n//\n// `data-track` 컨벤션(className 아님): 루트 1개 delegated listener 가\n// `[data-track]` 클릭을 잡아 first-party `/collect` 로 전송. cms-blocks 가\n// 채워지면 컴포넌트가 `data-track` 을 emit → 모든 테넌트 사이트가 일관 이벤트.\n//\n// `window.__rtTrack(ev, extra?)` — 클릭이 아닌 시점(마운트 등)에 커스텀 이벤트를\n// 보내야 하는 극소수 케이스(예: starter `not-found.tsx` 의 404 비콘, W4-5 PR 2)\n// 를 위한 탈출구. `send()` 를 그대로 노출하므로 payload 형태·anonymize 정책은\n// data-track 경로와 동일(단일소스). 이 스크립트가 렌더되지 않은 사이트(비콘\n// 미설정 — collectUrl/siteId 없음)에서는 전역 자체가 정의되지 않으므로 호출부는\n// 항상 optional chaining(`window.__rtTrack?.(...)`) 으로 감싸 게이트를 존중한다.\n//\n// 동의 면제 조건(ADR-0038 §2): cookieless · cross-site 영속 ID 없음 ·\n// raw IP/UA 는 서버(/collect)에서 hash/truncate · 제3자 미공유. 따라서 본\n// dispatcher 는 동의 게이트 없이 동작(운영 목적 first-party 분석).\n//\n// dual-emit 금지: 본 비콘은 plane ② 전용. GA4(plane ①) 로는 보내지 않는다.\n//\n// ADR-0048 §1 — first-touch 어트리뷰션 캡처: 페이지 로드 시 landing path ·\n// rt_src(단축링크/QR 토큰) · utm 3종 · 외부 referrer hostname 을\n// localStorage[\"_rt_attr\"] 에 저장. 이미 있으면 유지(first-touch 우선, TTL 30일).\n// 식별자를 저장하지 않으므로 위 동의 면제 논리가 그대로 성립한다. 문의 폼이\n// `@roottale/cms-client` readAttribution() 으로 읽어 inquiry 에 동반시킨다 —\n// 키/값 형태는 cms-client 와의 공유 계약(변경 시 양쪽 동시 갱신).\n//\n// 설계(measurement-design-draft.md) §2 — 어트리뷰션 v2: utm_term(ut)·\n// utm_content(un) 추가 + gclid/fbclid 는 **원값 저장 금지**, 존재 여부만\n// `gcl:1` 플래그로 남긴다(P0-4 — cookieless·비식별 원칙 보존). 추가로 last\n// non-direct touch 슬롯 `lt` — 우선순위 rt_src > utm_* > gclid/fbclid 존재 >\n// 외부 referrer 로 판정, 신호가 있는 로드에서만 통째 교체(직접 재방문은\n// 무갱신). first-touch 필드는 절대 덮어쓰지 않음(변하지 않는 first-touch 계약\n// 유지) — `lt` 만 같은 30일 봉투 안에서 갱신된다. 새로 캡처되는 envelope 은\n// `v:2`, 기존 v1 envelope 은 그대로 읽고(하위호환) `lt` 만 추가로 얹는다(구조\n// 마이그레이션은 하지 않음 — 읽기 관용이 계약의 전부).\n//\n// 설계 §3/§0 — beacon send() 는 window.__rtGa4Mirror·window.__rtJourneyRecord\n// 전역 훅이 있으면 호출한다(각각 ga4-bridge.ts·journey.ts 가 별도 스크립트로\n// 설치). data-track 클릭 delegation 도 동일 send() 를 거치므로(클릭 이벤트가\n// 대다수인 phone_click 등도 포함) 이 지점이 유일한 공통 choke point다. 각 훅은\n// 독립 try/catch — 훅이 없거나 던져도 실제 /collect 전송에는 영향 없음.\n\nimport { aiReferralRulesLiteral } from \"./ai-referrals\";\n\nexport interface BeaconOptions {\n /** first-party 수집 엔드포인트 (예: https://api.roottale.com/v1/collect). */\n collectUrl: string;\n /** site 식별자 (테넌트 cross-site 아님 — site 단위 집계용). */\n siteId: string;\n /** pageview 자동 전송 (기본 true). */\n trackPageview?: boolean;\n /** first-touch 어트리뷰션 캡처 (ADR-0048 §1, 기본 true). */\n captureAttribution?: boolean;\n}\n\n/**\n * first-touch 저장 localStorage 키 — `@roottale/cms-client` readAttribution()\n * 과의 공유 계약. 값 형태는 {@link FirstTouchAttribution}.\n */\nexport const ATTRIBUTION_STORAGE_KEY = \"_rt_attr\";\n\n/** first-touch 보존 기간 (30일, ms). 경과 시 다음 방문이 새 first-touch. */\nexport const ATTRIBUTION_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n\n/**\n * localStorage[\"_rt_attr\"] 에 저장되는 first-touch 형태 (식별자 없음).\n * 키를 짧게 유지 — 비콘 인라인 런타임과 폼 hidden field 양쪽에서 쓰인다.\n *\n * v1 데이터 하위호환: 이미 저장된 v1 envelope(ut/un/gcl/lt 없음)도 유효하게\n * 읽는다 — TTL 이 남아있으면 구조를 마이그레이션하지 않고 그대로 유지하며,\n * 신호가 있는 로드에서 `lt` 만 추가로 얹는다. 새로 캡처되는 envelope 만 `v:2`.\n */\nexport interface FirstTouchAttribution {\n /** 포맷 버전. 저장 시점의 v1 데이터도 유효하게 읽는다(하위호환). */\n v: 1 | 2;\n /** landing pathname (query 제외). */\n lp: string;\n /** 캡처 시각 (epoch ms). */\n ts: number;\n /** rt_src — 단축링크/QR 토큰 (있을 때만). */\n rs?: string;\n /** utm_source (있을 때만). */\n us?: string;\n /** utm_medium (있을 때만). */\n um?: string;\n /** utm_campaign (있을 때만). */\n uc?: string;\n /** utm_term (있을 때만, v2). */\n ut?: string;\n /** utm_content (있을 때만, v2). */\n un?: string;\n /** 광고 클릭 유입 존재 플래그(v2) — gclid/fbclid **원값은 절대 저장하지 않음**. */\n gcl?: 1;\n /** 외부 referrer hostname — raw URL 미저장, 동일 host 면 생략. */\n rf?: string;\n /** last non-direct touch (v2) — 신호 있는 로드에서만 통째 교체. */\n lt?: LastTouchAttribution;\n}\n\n/** `lt` 슬롯을 교체시킨 채널 — 우선순위 rs > utm > gcl > rf. */\nexport type LastTouchChannel = \"rs\" | \"utm\" | \"gcl\" | \"rf\";\n\n/**\n * last non-direct touch — first-touch 와 동형이나 `ch`(판정 채널)를 더 갖는다.\n * first-touch 와 동일한 30일 봉투 안에서 관리(별도 TTL 없음).\n */\nexport interface LastTouchAttribution {\n /** 갱신 시각 (epoch ms). */\n ts: number;\n /** 이 갱신을 유발한 채널(우선순위 rs > utm > gcl > rf). */\n ch: LastTouchChannel;\n rs?: string;\n us?: string;\n um?: string;\n uc?: string;\n ut?: string;\n un?: string;\n /** 광고 클릭 유입 존재 플래그 — 원값 미저장(first-touch 와 동일 정책). */\n gcl?: 1;\n rf?: string;\n}\n\nconst RUNTIME = `(function(){\nvar C=__RT_BEACON__;\nvar AIR=${aiReferralRulesLiteral()};\n/* AI 챗봇 유입 판정 — 원문 referrer 는 보내지 않고 고정 목록의 라벨만 남긴다.\n 목록 밖(일반 사이트·검색엔진)은 null 이라 아무것도 전송되지 않는다. */\nfunction aiRef(){try{\nif(!document.referrer)return null;\nvar h=new URL(document.referrer).hostname.toLowerCase().replace(/^www\\\\./,'');\nif(!h||h===location.hostname.toLowerCase().replace(/^www\\\\./,''))return null;\nfor(var i=0;i<AIR.length;i++){var r=AIR[i][0];if(h===r||h.slice(-(r.length+1))==='.'+r)return AIR[i][1];}\nreturn null;\n}catch(e){return null;}}\nfunction metaPid(){try{var m=document.querySelector('meta[name=\"rt:content-id\"]');var v=m&&m.getAttribute('content');return v?String(v).slice(0,64):null;}catch(e){return null;}}\nfunction send(ev,extra){\ntry{\nvar p={ev:ev,sid:C.sid,path:location.pathname,ref:document.referrer?1:0,ts:Date.now()};\nif(extra){for(var k in extra){p[k]=extra[k];}}\nvar body=JSON.stringify(p);\nif(navigator.sendBeacon){navigator.sendBeacon(C.ep,new Blob([body],{type:'application/json'}));}\nelse{fetch(C.ep,{method:'POST',body:body,headers:{'Content-Type':'application/json'},keepalive:true}).catch(function(){});}\n}catch(e){}\ntry{if(window.__rtGa4Mirror)window.__rtGa4Mirror(ev,extra);}catch(e){}\ntry{if(window.__rtJourneyRecord)window.__rtJourneyRecord(ev,extra);}catch(e){}\n}\nif(typeof window!=='undefined'){window.__rtTrack=send;}\ndocument.addEventListener('click',function(e){\nvar t=e.target;\nvar el=t&&t.closest?t.closest('[data-track]'):null;\nif(!el)return;\nvar extra={};\nvar ds=el.dataset||{};\nfor(var k in ds){if(k.indexOf('track')===0&&k!=='track'){extra[k]=ds[k];}}\nsend(el.getAttribute('data-track'),extra);\n},{capture:true});\nif(C.attr){\ntry{\nvar AK='_rt_attr';\nvar cur=null;\ntry{cur=JSON.parse(localStorage.getItem(AK)||'null');}catch(e){}\nvar expired=!cur||typeof cur.ts!=='number'||Date.now()-cur.ts>2592e6;\nvar sp=new URLSearchParams(location.search);\nvar rs=sp.get('rt_src');\nvar us=sp.get('utm_source');\nvar um=sp.get('utm_medium');\nvar uc=sp.get('utm_campaign');\nvar ut=sp.get('utm_term');\nvar un=sp.get('utm_content');\nvar hasGcl=!!(sp.get('gclid')||sp.get('fbclid'));\nvar extRef=null;\ntry{if(document.referrer){var rh=new URL(document.referrer).hostname;if(rh&&rh!==location.hostname)extRef=rh.slice(0,253);}}catch(e){}\nvar env=cur;\nif(expired){\nenv={v:2,lp:location.pathname,ts:Date.now()};\nif(rs)env.rs=rs.slice(0,32);\nif(us)env.us=us.slice(0,100);\nif(um)env.um=um.slice(0,100);\nif(uc)env.uc=uc.slice(0,100);\nif(ut)env.ut=ut.slice(0,100);\nif(un)env.un=un.slice(0,100);\nif(hasGcl)env.gcl=1;\nif(extRef)env.rf=extRef;\n}\nvar ch=null;\nif(rs){ch='rs';}else if(us||um||uc||ut||un){ch='utm';}else if(hasGcl){ch='gcl';}else if(extRef){ch='rf';}\nif(ch){\nvar lt={ts:Date.now(),ch:ch};\nif(rs)lt.rs=rs.slice(0,32);\nif(us)lt.us=us.slice(0,100);\nif(um)lt.um=um.slice(0,100);\nif(uc)lt.uc=uc.slice(0,100);\nif(ut)lt.ut=ut.slice(0,100);\nif(un)lt.un=un.slice(0,100);\nif(hasGcl)lt.gcl=1;\nif(extRef)lt.rf=extRef;\nenv.lt=lt;\n}\nif(expired||ch){localStorage.setItem(AK,JSON.stringify(env));}\n}catch(e){}\n}\nif(C.pv){var _pid=metaPid();var _air=aiRef();var _x=null;\nif(_pid||_air){_x={};if(_pid)_x.pid=_pid;if(_air)_x.air=_air;}\nsend('pageview',_x);}\n})();`;\n\n/**\n * 비콘 dispatcher JS 본문. <script is:inline> 로 감싸 주입.\n * `ref` 는 referrer 존재 여부(0/1)만 — raw referrer 미전송(privacy).\n *\n * ADR-0065 — 글 조회수 식별자: pageview 시 `<meta name=\"rt:content-id\">` 의\n * content 를 읽어 `pid`(=post_id) 로 동봉한다. 사이트 레벨 단일 비콘이 페이지별\n * 글 식별자를 갖게 해 URL/컬렉션 basePath 변경에 무관한 안정 매칭을 만든다.\n * meta 미존재 시 `pid` 생략(graceful) — `/collect` 가 post_id 없는 pageview 로 처리.\n * 값 형태는 `/v1/collect` 와의 공유 계약(변경 시 양쪽 동시 갱신).\n */\nexport function renderBeaconScript(options: BeaconOptions): string {\n const cfg = JSON.stringify({\n ep: options.collectUrl,\n sid: options.siteId,\n pv: options.trackPageview !== false,\n attr: options.captureAttribution !== false,\n });\n return \"var __RT_BEACON__=\" + cfg + \";\" + RUNTIME;\n}\n\ndeclare global {\n interface Window {\n /** RUNTIME 이 노출하는 send() — data-track 클릭 delegation 과 동일 함수. */\n __rtTrack?: (ev: string, extra?: Record<string, unknown>) => void;\n }\n}\n\n/**\n * `window.__rtTrack` 안전 호출 래퍼(W4-5 PR 2) — 클릭이 아닌 시점(마운트 등)\n * 에 커스텀 이벤트를 보내야 하는 클라이언트 컴포넌트(예: starter\n * `app/not-found.tsx` 의 404 비콘)의 표준 진입점. `RootTaleAnalytics` 가 비콘\n * 없이 렌더된 사이트(collectUrl/siteId 미설정)에서는 전역 자체가 없으므로\n * 자동으로 no-op — 별도 게이트 없이 안전.\n */\nexport function trackCustomEvent(ev: string, extra?: Record<string, unknown>): void {\n if (typeof window === \"undefined\") return;\n window.__rtTrack?.(ev, extra);\n}\n"],"mappings":";;;;;AA2DO,IAAM,0BAA0B;AAGhC,IAAM,qBAAqB,KAAK,KAAK,KAAK,KAAK;AA4DtD,IAAM,UAAU;AAAA;AAAA,UAEN,uBAAuB,CAAC;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;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4F3B,SAAS,mBAAmB,SAAgC;AACjE,QAAM,MAAM,KAAK,UAAU;AAAA,IACzB,IAAI,QAAQ;AAAA,IACZ,KAAK,QAAQ;AAAA,IACb,IAAI,QAAQ,kBAAkB;AAAA,IAC9B,MAAM,QAAQ,uBAAuB;AAAA,EACvC,CAAC;AACD,SAAO,uBAAuB,MAAM,MAAM;AAC5C;AAgBO,SAAS,iBAAiB,IAAY,OAAuC;AAClF,MAAI,OAAO,WAAW,YAAa;AACnC,SAAO,YAAY,IAAI,KAAK;AAC9B;","names":[]}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GA4_MIRROR,
|
|
3
|
+
TRACK_EVENTS
|
|
4
|
+
} from "./chunk-GBIGQHVN.js";
|
|
5
|
+
|
|
1
6
|
// src/loader.ts
|
|
2
7
|
function renderConsentBootstrap() {
|
|
3
8
|
return [
|
|
@@ -39,6 +44,7 @@ function applyAll(){for(var i=0;i<M.tags.length;i++){inject(M.tags[i]);}}
|
|
|
39
44
|
window.RootTaleAnalytics={
|
|
40
45
|
manifest:M,
|
|
41
46
|
getConsent:function(){return Object.assign({},consent);},
|
|
47
|
+
isTagLoaded:function(provider){return !!loaded[provider];},
|
|
42
48
|
updateConsent:function(partial){
|
|
43
49
|
for(var k in partial){if(Object.prototype.hasOwnProperty.call(consent,k))consent[k]=partial[k];}
|
|
44
50
|
if(typeof gtag==='function'){gtag('consent','update',partial);}
|
|
@@ -103,62 +109,59 @@ function escapeAttr(s) {
|
|
|
103
109
|
return escapeHtml(s).replace(/"/g, """);
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
// src/
|
|
107
|
-
var ATTRIBUTION_STORAGE_KEY = "_rt_attr";
|
|
108
|
-
var ATTRIBUTION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
112
|
+
// src/ga4-bridge.ts
|
|
109
113
|
var RUNTIME2 = `(function(){
|
|
110
|
-
var
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
var
|
|
115
|
-
if(
|
|
116
|
-
|
|
117
|
-
if(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
var
|
|
124
|
-
var el=t&&t.closest?t.closest('[data-track]'):null;
|
|
125
|
-
if(!el)return;
|
|
126
|
-
var extra={};
|
|
127
|
-
var ds=el.dataset||{};
|
|
128
|
-
for(var k in ds){if(k.indexOf('track')===0&&k!=='track'){extra[k]=ds[k];}}
|
|
129
|
-
send(el.getAttribute('data-track'),extra);
|
|
130
|
-
},{capture:true});
|
|
131
|
-
if(C.attr){
|
|
132
|
-
try{
|
|
133
|
-
var AK='_rt_attr';
|
|
134
|
-
var cur=null;
|
|
135
|
-
try{cur=JSON.parse(localStorage.getItem(AK)||'null');}catch(e){}
|
|
136
|
-
if(!cur||typeof cur.ts!=='number'||Date.now()-cur.ts>2592e6){
|
|
137
|
-
var sp=new URLSearchParams(location.search);
|
|
138
|
-
var a={v:1,lp:location.pathname,ts:Date.now()};
|
|
139
|
-
var rs=sp.get('rt_src');if(rs)a.rs=rs.slice(0,32);
|
|
140
|
-
var us=sp.get('utm_source');if(us)a.us=us.slice(0,100);
|
|
141
|
-
var um=sp.get('utm_medium');if(um)a.um=um.slice(0,100);
|
|
142
|
-
var uc=sp.get('utm_campaign');if(uc)a.uc=uc.slice(0,100);
|
|
143
|
-
try{if(document.referrer){var rh=new URL(document.referrer).hostname;if(rh&&rh!==location.hostname)a.rf=rh.slice(0,253);}}catch(e){}
|
|
144
|
-
localStorage.setItem(AK,JSON.stringify(a));
|
|
145
|
-
}
|
|
146
|
-
}catch(e){}
|
|
114
|
+
var MIRROR=__RT_GA4_MIRROR__;
|
|
115
|
+
window.__rtGa4Mirror=function(ev,extra){
|
|
116
|
+
var name=MIRROR[ev];
|
|
117
|
+
if(!name)return;
|
|
118
|
+
var RA=window.RootTaleAnalytics;
|
|
119
|
+
if(!RA||typeof RA.getConsent!=='function'||typeof RA.isTagLoaded!=='function')return;
|
|
120
|
+
if(RA.getConsent().analytics_storage!=='granted')return;
|
|
121
|
+
if(!RA.isTagLoaded('ga4'))return;
|
|
122
|
+
if(typeof gtag!=='function')return;
|
|
123
|
+
gtag('event',name,extra||{});
|
|
124
|
+
};
|
|
125
|
+
})();`;
|
|
126
|
+
function renderGa4BridgeScript() {
|
|
127
|
+
return "var __RT_GA4_MIRROR__=" + JSON.stringify(GA4_MIRROR) + ";" + RUNTIME2;
|
|
147
128
|
}
|
|
148
|
-
|
|
129
|
+
|
|
130
|
+
// src/journey.ts
|
|
131
|
+
var JOURNEY_STORAGE_KEY = "_rt_journey";
|
|
132
|
+
var JOURNEY_MAX_ITEMS = 30;
|
|
133
|
+
var JOURNEY_EVENT_NAMES = Object.keys(TRACK_EVENTS).filter((name) => TRACK_EVENTS[name].journey);
|
|
134
|
+
var RUNTIME3 = `(function(){
|
|
135
|
+
var JE=__RT_JOURNEY_EVENTS__;
|
|
136
|
+
window.__rtJourneyRecord=function(ev,extra){
|
|
137
|
+
if(!JE||JE.indexOf(ev)===-1)return;
|
|
138
|
+
var KEY='_rt_journey';
|
|
139
|
+
var arr=null;
|
|
140
|
+
try{arr=JSON.parse(sessionStorage.getItem(KEY)||'null');}catch(e){}
|
|
141
|
+
if(Object.prototype.toString.call(arr)!=='[object Array]')arr=[];
|
|
142
|
+
var item={t:Date.now(),ev:ev};
|
|
143
|
+
try{if(location&&location.pathname)item.path=location.pathname;}catch(e){}
|
|
144
|
+
if(extra)item.d=extra;
|
|
145
|
+
arr.push(item);
|
|
146
|
+
if(arr.length>30)arr=arr.slice(arr.length-30);
|
|
147
|
+
sessionStorage.setItem(KEY,JSON.stringify(arr));
|
|
148
|
+
};
|
|
149
149
|
})();`;
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
ep: options.collectUrl,
|
|
153
|
-
sid: options.siteId,
|
|
154
|
-
pv: options.trackPageview !== false,
|
|
155
|
-
attr: options.captureAttribution !== false
|
|
156
|
-
});
|
|
157
|
-
return "var __RT_BEACON__=" + cfg + ";" + RUNTIME2;
|
|
150
|
+
function renderJourneyBridgeScript() {
|
|
151
|
+
return "var __RT_JOURNEY_EVENTS__=" + JSON.stringify(JOURNEY_EVENT_NAMES) + ";" + RUNTIME3;
|
|
158
152
|
}
|
|
159
|
-
function
|
|
160
|
-
if (typeof window === "undefined")
|
|
161
|
-
|
|
153
|
+
function readJourney() {
|
|
154
|
+
if (typeof window === "undefined" || typeof sessionStorage === "undefined") {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const raw = sessionStorage.getItem(JOURNEY_STORAGE_KEY);
|
|
159
|
+
if (!raw) return [];
|
|
160
|
+
const parsed = JSON.parse(raw);
|
|
161
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
export {
|
|
@@ -166,9 +169,10 @@ export {
|
|
|
166
169
|
renderLoaderScript,
|
|
167
170
|
renderConsentBannerMarkup,
|
|
168
171
|
renderConsentBannerScript,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
renderGa4BridgeScript,
|
|
173
|
+
JOURNEY_STORAGE_KEY,
|
|
174
|
+
JOURNEY_MAX_ITEMS,
|
|
175
|
+
renderJourneyBridgeScript,
|
|
176
|
+
readJourney
|
|
173
177
|
};
|
|
174
|
-
//# sourceMappingURL=chunk-
|
|
178
|
+
//# sourceMappingURL=chunk-G7OLBHT5.js.map
|