export const DISCOVERY_QUERY_PARAM = 'memind_discovery'; export const DISCOVERY_SOURCE_QUERY_PARAM = 'memind_discovery_source'; const SEO_HOST_RULES = [ { source: 'google', hosts: ['google.com', 'google.com.hk', 'google.co.jp', 'google.co.uk', 'google.ca', 'google.com.tw'] }, { source: 'baidu', hosts: ['baidu.com'] }, { source: 'bing', hosts: ['bing.com'] }, { source: 'sogou', hosts: ['sogou.com'] }, { source: 'so360', hosts: ['so.com'] }, { source: 'sm', hosts: ['sm.cn'] }, { source: 'yandex', hosts: ['yandex.ru', 'yandex.com', 'yandex.by'] }, { source: 'duckduckgo', hosts: ['duckduckgo.com'] }, { source: 'yahoo', hosts: ['search.yahoo.com', 'yahoo.com'] }, { source: 'naver', hosts: ['search.naver.com', 'naver.com'] }, { source: 'ecosia', hosts: ['ecosia.org'] }, { source: 'brave', hosts: ['search.brave.com'] }, ]; const GEO_HOST_RULES = [ { source: 'chatgpt', hosts: ['chatgpt.com', 'chat.openai.com'] }, { source: 'openai', hosts: ['openai.com'] }, { source: 'perplexity', hosts: ['perplexity.ai'] }, { source: 'claude', hosts: ['claude.ai', 'anthropic.com'] }, { source: 'gemini', hosts: ['gemini.google.com'] }, { source: 'copilot', hosts: ['copilot.microsoft.com'] }, { source: 'you', hosts: ['you.com'] }, { source: 'phind', hosts: ['phind.com'] }, { source: 'kagi', hosts: ['kagi.com'] }, { source: 'poe', hosts: ['poe.com'] }, { source: 'meta_ai', hosts: ['meta.ai'] }, { source: 'doubao', hosts: ['doubao.com'] }, { source: 'tongyi', hosts: ['tongyi.aliyun.com', 'qianwen.aliyun.com'] }, { source: 'deepseek', hosts: ['chat.deepseek.com', 'deepseek.com'] }, ]; function normalizeHostname(value) { return String(value ?? '').trim().toLowerCase().replace(/^www\./, '').slice(0, 255); } function parseReferrerHost(referrer) { const raw = String(referrer ?? '').trim(); if (!raw) return ''; try { return normalizeHostname(new URL(raw).hostname); } catch { return ''; } } function matchHostRule(hostname, rules = []) { const host = normalizeHostname(hostname); if (!host) return ''; for (const rule of rules) { for (const candidate of rule.hosts) { const normalized = normalizeHostname(candidate); if (!normalized) continue; if (host === normalized || host.endsWith(`.${normalized}`)) { return rule.source; } } } return ''; } export function classifyReferrerHost(hostname) { const host = normalizeHostname(hostname); if (!host) { return { discovery_channel: 'direct', discovery_source: 'direct' }; } const geoSource = matchHostRule(host, GEO_HOST_RULES); if (geoSource) { return { discovery_channel: 'geo', discovery_source: geoSource }; } const seoSource = matchHostRule(host, SEO_HOST_RULES); if (seoSource) { return { discovery_channel: 'seo', discovery_source: seoSource }; } return { discovery_channel: 'referral', discovery_source: 'other' }; } function normalizeDiscoveryChannel(value) { const channel = String(value ?? '').trim().toLowerCase(); return channel === 'seo' || channel === 'geo' ? channel : ''; } function classifyByUtm(utmSource = '', utmMedium = '') { const source = String(utmSource ?? '').trim().toLowerCase(); const medium = String(utmMedium ?? '').trim().toLowerCase(); if (!source && !medium) return null; if (medium === 'organic' || /seo|search|sem/.test(source)) { return { discovery_channel: 'seo', discovery_source: source || 'utm' }; } if (/geo|ai|llm|gpt|copilot|perplexity|chatgpt/.test(source)) { return { discovery_channel: 'geo', discovery_source: source || 'utm' }; } return null; } export function classifyDiscoveryTrafficSource({ referrer = '', utmSource = '', utmMedium = '', discoveryChannel = '', discoverySource = '', pageHost = '', } = {}) { const normalizedChannel = normalizeDiscoveryChannel(discoveryChannel); if (normalizedChannel) { const source = String(discoverySource ?? '').trim().toLowerCase().slice(0, 32) || 'other'; return { discovery_channel: normalizedChannel, discovery_source: source, referrer_host: parseReferrerHost(referrer), }; } const utmMatch = classifyByUtm(utmSource, utmMedium); const referrerHost = parseReferrerHost(referrer); const currentHost = normalizeHostname(pageHost); const externalHost = referrerHost && (!currentHost || referrerHost !== currentHost) ? referrerHost : ''; if (utmMatch) { return { discovery_channel: utmMatch.discovery_channel, discovery_source: utmMatch.discovery_source, referrer_host: externalHost, }; } const geoSource = matchHostRule(externalHost, GEO_HOST_RULES); if (geoSource) { return { discovery_channel: 'geo', discovery_source: geoSource, referrer_host: externalHost, }; } const seoSource = matchHostRule(externalHost, SEO_HOST_RULES); if (seoSource) { return { discovery_channel: 'seo', discovery_source: seoSource, referrer_host: externalHost, }; } if (externalHost) { return { discovery_channel: 'referral', discovery_source: 'other', referrer_host: externalHost, }; } return { discovery_channel: 'direct', discovery_source: 'direct', referrer_host: '', }; } export function appendDiscoveryPropagationParams(url, discovery = {}) { const channel = normalizeDiscoveryChannel(discovery.discovery_channel); if (!channel || !url) return String(url ?? ''); let next = String(url); const joiner = next.includes('?') ? '&' : '?'; next += `${joiner}${encodeURIComponent(DISCOVERY_QUERY_PARAM)}=${encodeURIComponent(channel)}`; const source = String(discovery.discovery_source ?? '').trim().toLowerCase(); if (source && source !== 'direct' && source !== 'other') { next += `&${encodeURIComponent(DISCOVERY_SOURCE_QUERY_PARAM)}=${encodeURIComponent(source.slice(0, 32))}`; } return next; } export function buildInlineDiscoveryResolverScript(referrerHint = '') { const rules = { seo: SEO_HOST_RULES, geo: GEO_HOST_RULES, }; const serializedRules = JSON.stringify(rules) .replaceAll('<', '\\u003c') .replaceAll('>', '\\u003e') .replaceAll('&', '\\u0026'); const serializedHint = JSON.stringify(String(referrerHint ?? '').slice(0, 512)) .replaceAll('<', '\\u003c') .replaceAll('>', '\\u003e') .replaceAll('&', '\\u0026'); return `(function(){var R=${serializedRules},H=${serializedHint};function norm(v){return String(v||"").trim().toLowerCase().replace(/^www\\./,"").slice(0,255)}function hostFrom(u){try{return norm(new URL(String(u||"")).hostname)}catch(e){return""}}function matchHost(h,rules){h=norm(h);if(!h)return"";for(var i=0;i