@roottale/analytics-runtime 0.6.0 → 0.7.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.
@@ -1,99 +0,0 @@
1
- // src/entry-channels.ts
2
- var ENTRY_CHANNEL_IDS = [
3
- "search",
4
- "direct",
5
- "referral",
6
- "social",
7
- "paid",
8
- "ai",
9
- "shortlink",
10
- "other"
11
- ];
12
- function isEntryChannelId(value) {
13
- return ENTRY_CHANNEL_IDS.includes(value);
14
- }
15
- var ENTRY_CHANNEL_LABEL = {
16
- search: "\uAC80\uC0C9",
17
- direct: "\uC8FC\uC18C \uC785\uB825\xB7\uC990\uACA8\uCC3E\uAE30",
18
- referral: "\uB2E4\uB978 \uC0AC\uC774\uD2B8 \uB9C1\uD06C",
19
- social: "\uC18C\uC15C \uBBF8\uB514\uC5B4",
20
- paid: "\uAD11\uACE0",
21
- ai: "AI \uCC57\uBD07",
22
- shortlink: "\uB2E8\uCD95\uB9C1\uD06C\xB7QR",
23
- other: "\uAE30\uD0C0 \uCEA0\uD398\uC778"
24
- };
25
- var SEARCH_HOSTS = [
26
- { host: "google.com" },
27
- { host: "google.co.kr" },
28
- { host: "search.naver.com" },
29
- { host: "naver.com", exact: true },
30
- { host: "m.naver.com", exact: true },
31
- { host: "search.daum.net" },
32
- { host: "daum.net", exact: true },
33
- { host: "m.daum.net", exact: true },
34
- { host: "bing.com" },
35
- { host: "duckduckgo.com" },
36
- { host: "search.yahoo.com" },
37
- { host: "yandex.com" },
38
- { host: "baidu.com" },
39
- { host: "search.zum.com" },
40
- { host: "zum.com", exact: true },
41
- { host: "search.nate.com" },
42
- { host: "nate.com", exact: true }
43
- ];
44
- var SOCIAL_HOSTS = [
45
- { host: "facebook.com" },
46
- { host: "instagram.com" },
47
- { host: "threads.net" },
48
- { host: "twitter.com" },
49
- { host: "x.com" },
50
- { host: "t.co", exact: true },
51
- { host: "linkedin.com" },
52
- { host: "youtube.com" },
53
- { host: "youtu.be", exact: true },
54
- { host: "tiktok.com" },
55
- { host: "pinterest.com" },
56
- { host: "band.us" },
57
- { host: "story.kakao.com" },
58
- { host: "pf.kakao.com" },
59
- { host: "cafe.naver.com" },
60
- { host: "cafe.daum.net" }
61
- ];
62
- function matchesRules(host, rules) {
63
- for (const rule of rules) {
64
- if (host === rule.host) return true;
65
- if (!rule.exact && host.endsWith(`.${rule.host}`)) return true;
66
- }
67
- return false;
68
- }
69
- var PAID_MEDIUM_RE = /^(cpc|ppc|cpm|cpv|cpa|display|banner|paid[\w-]*|retargeting|remarketing)$/i;
70
- var SOCIAL_MEDIUM_RE = /^(social|sns|social[-_]media)$/i;
71
- function classifyEntryChannel(signals) {
72
- const host = signals.referrerHost?.trim().toLowerCase() || null;
73
- const medium = signals.utmMedium?.trim() || null;
74
- const hasUtm = Boolean(
75
- signals.utmSource?.trim() || medium || signals.utmCampaign?.trim()
76
- );
77
- if (signals.aiReferralId) return "ai";
78
- if (signals.rtSrc?.trim()) return "shortlink";
79
- if (signals.hasAdClick || medium && PAID_MEDIUM_RE.test(medium)) {
80
- return "paid";
81
- }
82
- if (medium && SOCIAL_MEDIUM_RE.test(medium)) return "social";
83
- if (host) {
84
- if (matchesRules(host, SEARCH_HOSTS)) return "search";
85
- if (matchesRules(host, SOCIAL_HOSTS)) return "social";
86
- }
87
- if (hasUtm) return "other";
88
- if (host) return "referral";
89
- if (signals.hasReferrer) return null;
90
- return "direct";
91
- }
92
-
93
- export {
94
- ENTRY_CHANNEL_IDS,
95
- isEntryChannelId,
96
- ENTRY_CHANNEL_LABEL,
97
- classifyEntryChannel
98
- };
99
- //# sourceMappingURL=chunk-KIOFZXJX.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/entry-channels.ts"],"sourcesContent":["/**\n * 유입 경로(entry channel) 분류 — ROOT-ANALYTICS 자체 수집의 \"어디서 왔나\".\n *\n * 비콘은 신호(외부 referrer **호스트명**·utm·광고클릭 유무·rt_src·AI 라벨)만\n * 보내고, 판정은 서버(`api-core /v1/collect`)가 이 모듈로 한다 — 판정 목록을\n * 고치면 워커 배포만으로 전 사이트에 반영된다(고객 사이트 재배포 불필요).\n *\n * Privacy(ADR-0038 §2 동의 면제 유지): 입력이 이미 비식별이다 — referrer 는\n * 경로·쿼리 없는 호스트명뿐, gclid/fbclid 는 존재 여부 불리언뿐(원값 미수신),\n * utm 은 고객이 자기 캠페인 링크에 직접 붙인 라벨이다. 출력은 고정 사전의\n * id 하나라 임의 문자열이 차원에 들어갈 수 없다.\n *\n * GA4 화면의 6묶음(search/direct/referral/social/paid/other)과 말을 맞추되,\n * 자체 수집만 아는 두 가지를 더 가른다 — `ai`(챗봇 유입, ai-referrals.ts 와\n * 동일 사전) · `shortlink`(roottale.link 단축링크/QR 의 rt_src).\n *\n * 이 파일은 브라우저 비콘 생성기·서버 양쪽에서 참조하므로 런타임 의존이\n * 없어야 한다(ai-referrals.ts 와 동일 제약).\n */\n\nexport const ENTRY_CHANNEL_IDS = [\n \"search\",\n \"direct\",\n \"referral\",\n \"social\",\n \"paid\",\n \"ai\",\n \"shortlink\",\n \"other\",\n] as const;\n\nexport type EntryChannelId = (typeof ENTRY_CHANNEL_IDS)[number];\n\n/** 수집 라우트·어드민 조회의 입력 검증 — 사전 밖 문자열 차단. */\nexport function isEntryChannelId(value: string): value is EntryChannelId {\n return (ENTRY_CHANNEL_IDS as readonly string[]).includes(value);\n}\n\n/** 고객 화면 라벨 — GA4 6묶음과 같은 말을 쓴다(두 화면이 다르게 부르지 않게). */\nexport const ENTRY_CHANNEL_LABEL: Record<EntryChannelId, string> = {\n search: \"검색\",\n direct: \"주소 입력·즐겨찾기\",\n referral: \"다른 사이트 링크\",\n social: \"소셜 미디어\",\n paid: \"광고\",\n ai: \"AI 챗봇\",\n shortlink: \"단축링크·QR\",\n other: \"기타 캠페인\",\n};\n\n/**\n * 호스트 규칙 — 기본은 정확 일치 또는 서브도메인(`.` 접미) 일치.\n * `exact` 는 정확 일치만: `naver.com` 을 접미 매칭하면 `blog.naver.com`(블로그\n * = 다른 사이트 링크)까지 검색으로 잘못 묶인다.\n */\ntype HostRule = { readonly host: string; readonly exact?: true };\n\nconst SEARCH_HOSTS: readonly HostRule[] = [\n { host: \"google.com\" },\n { host: \"google.co.kr\" },\n { host: \"search.naver.com\" },\n { host: \"naver.com\", exact: true },\n { host: \"m.naver.com\", exact: true },\n { host: \"search.daum.net\" },\n { host: \"daum.net\", exact: true },\n { host: \"m.daum.net\", exact: true },\n { host: \"bing.com\" },\n { host: \"duckduckgo.com\" },\n { host: \"search.yahoo.com\" },\n { host: \"yandex.com\" },\n { host: \"baidu.com\" },\n { host: \"search.zum.com\" },\n { host: \"zum.com\", exact: true },\n { host: \"search.nate.com\" },\n { host: \"nate.com\", exact: true },\n];\n\nconst SOCIAL_HOSTS: readonly HostRule[] = [\n { host: \"facebook.com\" },\n { host: \"instagram.com\" },\n { host: \"threads.net\" },\n { host: \"twitter.com\" },\n { host: \"x.com\" },\n { host: \"t.co\", exact: true },\n { host: \"linkedin.com\" },\n { host: \"youtube.com\" },\n { host: \"youtu.be\", exact: true },\n { host: \"tiktok.com\" },\n { host: \"pinterest.com\" },\n { host: \"band.us\" },\n { host: \"story.kakao.com\" },\n { host: \"pf.kakao.com\" },\n { host: \"cafe.naver.com\" },\n { host: \"cafe.daum.net\" },\n];\n\n/** 부분 문자열 매칭 금지 — `naver.com.evil.com` 같은 위장 도메인 차단. */\nfunction matchesRules(host: string, rules: readonly HostRule[]): boolean {\n for (const rule of rules) {\n if (host === rule.host) return true;\n if (!rule.exact && host.endsWith(`.${rule.host}`)) return true;\n }\n return false;\n}\n\n/** utm_medium 유료 표기 — GA4 의 paid 판정 표기와 겹치는 관용 값들. */\nconst PAID_MEDIUM_RE =\n /^(cpc|ppc|cpm|cpv|cpa|display|banner|paid[\\w-]*|retargeting|remarketing)$/i;\n\nconst SOCIAL_MEDIUM_RE = /^(social|sns|social[-_]media)$/i;\n\nexport interface EntryChannelSignals {\n /** 외부 referrer 호스트명(소문자, www 제거) — 같은 호스트면 비콘이 생략. */\n readonly referrerHost?: string | null;\n readonly utmSource?: string | null;\n readonly utmMedium?: string | null;\n readonly utmCampaign?: string | null;\n /** gclid/fbclid 존재 여부 — 원값은 어디에도 없다. */\n readonly hasAdClick?: boolean;\n /** roottale.link 단축링크/QR 토큰(rt_src). */\n readonly rtSrc?: string | null;\n /** 비콘이 판정한 AI 챗봇 라벨(ai-referrals 사전 통과분). */\n readonly aiReferralId?: string | null;\n /** referrer 존재 여부(내부 이동 포함) — 비콘 `ref` 플래그. */\n readonly hasReferrer?: boolean;\n}\n\n/**\n * 한 pageview 의 유입 경로 판정. **`null` = 유입이 아님**(내부 이동 — 같은\n * 사이트 안에서 다음 페이지로 넘어온 조회). 집계에서 내부 이동을 빼야\n * \"채널별 유입\"이 실제 들어온 횟수를 뜻한다.\n *\n * 우선순위: ai > shortlink > paid > social(utm) > search(host) >\n * social(host) > other(utm만) > referral(그 외 외부 host) > 내부/직접.\n * utm_medium 명시가 host 추정보다 먼저다 — 고객이 직접 붙인 라벨이 더 정확.\n */\nexport function classifyEntryChannel(\n signals: EntryChannelSignals,\n): EntryChannelId | null {\n const host = signals.referrerHost?.trim().toLowerCase() || null;\n const medium = signals.utmMedium?.trim() || null;\n const hasUtm = Boolean(\n signals.utmSource?.trim() || medium || signals.utmCampaign?.trim(),\n );\n\n if (signals.aiReferralId) return \"ai\";\n if (signals.rtSrc?.trim()) return \"shortlink\";\n if (signals.hasAdClick || (medium && PAID_MEDIUM_RE.test(medium))) {\n return \"paid\";\n }\n if (medium && SOCIAL_MEDIUM_RE.test(medium)) return \"social\";\n if (host) {\n if (matchesRules(host, SEARCH_HOSTS)) return \"search\";\n if (matchesRules(host, SOCIAL_HOSTS)) return \"social\";\n }\n if (hasUtm) return \"other\"; // email·뉴스레터 등 — 라벨은 utm 원문으로 확인\n if (host) return \"referral\";\n // referrer 는 있는데 외부 호스트가 아니다 = 내부 이동 → 유입 아님.\n if (signals.hasReferrer) return null;\n return \"direct\";\n}\n"],"mappings":"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","names":[]}