/** * 数据上报 SDK 接入封装 (91porn / JHA-1029) * 文档: http://34.146.148.117:8033/web/getting-started.html * 参考: ttrgw-server / mdcom_web 项目 tracker.js * * 使用方式: 在 中先引入 web-sdk-vX.X.X.js,再引入本文件 tracker.js, * 然后通过 trackerInit 注入配置。模板做法见 base/trackerInit.html。 */ (function (global) { "use strict"; // 是否已经初始化(避免页面切换/重复 init) var __inited = false; // 当前页面信息 var __pageInfo = { page_key: "", page_name: "" }; // 来源页面信息 var __referrerPageInfo = { page_key: "", page_name: "" }; // 广告已上报展示去重 Set (key = slot+ids) var __adShownSet = {}; // 页面加载开始时间 var __pageStart = (global.performance && global.performance.now) ? global.performance.now() : Date.now(); // ------- 工具方法 ------- function isReady() { return typeof global.WebSDK !== "undefined" && global.WebSDK && global.WebSDK.track; } function safeCall(fn) { if (!isReady()) { // 等待 SDK 加载完成后再执行 var timer = setInterval(function () { if (isReady()) { clearInterval(timer); try { fn(); } catch (e) { logError(e); } } }, 50); return; } try { fn(); } catch (e) { logError(e); } } // sid 是必填公共字段。确认一次存在后置位,后续事件直接同步上报。 var __sidKnownPresent = false; // 发送事件前确保 sid 存在:能取到则上报;为空则尝试补齐(notifyPageLoad); // 仍无法生成则丢弃该事件(满足"无法生成 sid 时丢弃或补齐"的要求)。 function sendTrack(send) { safeCall(function () { if (__sidKnownPresent || !global.WebSDK.getCommonField) { send(); return; } global.WebSDK.getCommonField("sid").then(function (sid) { if (sid) { __sidKnownPresent = true; send(); return; } // 补齐:强制重建会话生成 sid try { global.WebSDK.notifyPageLoad && global.WebSDK.notifyPageLoad(); } catch (e) {} global.WebSDK.getCommonField("sid").then(function (sid2) { if (sid2) { __sidKnownPresent = true; send(); } else { logError(new Error("sid 无法生成,事件已丢弃")); } // 丢弃 }).catch(function () { __sidKnownPresent = true; send(); }); }).catch(function () { __sidKnownPresent = true; send(); }); }); } function logError() {} // 必填非空字符串字段的安全默认值 function nonEmpty(v, fallback) { var s = (v == null) ? "" : String(v); return s.length ? s : fallback; } // 把数值限制在 [min, max] 区间(用于点击百分比 0-100) function clamp(v, min, max) { var n = parseInt(v, 10); if (isNaN(n)) n = 0; if (n < min) n = min; if (n > max) n = max; return n; } // Web SDK 文档约定:无标签时 tag_key=default,tag_name=默认标签 var TAG_KEY_DEFAULT = "default"; var TAG_NAME_DEFAULT = "默认标签"; function tagKeyOrDefault(v) { return nonEmpty(v, TAG_KEY_DEFAULT); } function tagNameOrDefault(v) { return nonEmpty(v, TAG_NAME_DEFAULT); } var __deviceInfo = null; function parseDeviceInfoFromUA(ua) { ua = ua || (typeof navigator !== "undefined" ? navigator.userAgent : "") || ""; var brand = ""; var model = ""; var systemName = ""; var systemVersion = ""; if (/iPhone|iPad|iPod/i.test(ua)) { systemName = "iOS"; brand = "Apple"; var ios = ua.match(/OS (\d+[_\d]*) like Mac OS X/i); systemVersion = ios ? ios[1].replace(/_/g, ".") : ""; if (/iPad/i.test(ua)) model = "iPad"; else if (/iPod/i.test(ua)) model = "iPod"; else model = "iPhone"; } else if (/Android/i.test(ua)) { systemName = "Android"; var and = ua.match(/Android ([\d.]+)/i); systemVersion = and ? and[1] : ""; var andModel = ua.match(/Android\s+[\d.]+;\s*([^;)]+?)(?:\s+Build\/|\))/i); if (andModel) model = andModel[1].trim(); if (/HUAWEI|HW-|HONOR/i.test(ua)) brand = "HUAWEI"; else if (/Xiaomi|Redmi|MI\s/i.test(ua)) brand = "Xiaomi"; else if (/OPPO/i.test(ua)) brand = "OPPO"; else if (/vivo/i.test(ua)) brand = "vivo"; else if (/Samsung|SM-/i.test(ua)) brand = "Samsung"; else if (/OnePlus/i.test(ua)) brand = "OnePlus"; } else if (/Windows/i.test(ua)) { systemName = "Windows"; brand = "PC"; model = "PC"; var win = ua.match(/Windows NT ([\d.]+)/i); systemVersion = win ? win[1] : ""; } else if (/Mac OS X|Macintosh/i.test(ua)) { systemName = "macOS"; brand = "Apple"; model = "Mac"; var mac = ua.match(/Mac OS X ([\d_]+)/i); systemVersion = mac ? mac[1].replace(/_/g, ".") : ""; } else if (/Linux/i.test(ua)) { systemName = "Linux"; brand = "PC"; model = "PC"; } else { brand = "PC"; model = "PC"; systemName = "Other"; } return { deviceBrand: brand, deviceModel: model, systemName: systemName, systemVersion: systemVersion, }; } function getDeviceInfo() { if (__deviceInfo) return __deviceInfo; __deviceInfo = parseDeviceInfoFromUA(); return __deviceInfo; } function applyDeviceContextToSdk(info) { if (!info || !global.WebSDK || !global.WebSDK.setContext) return; global.WebSDK.setContext({ deviceBrand: info.deviceBrand || "", deviceModel: info.deviceModel || "", systemName: info.systemName || "", systemVersion: info.systemVersion || "", }); } function enrichDeviceInfoAsync() { if (typeof navigator === "undefined" || !navigator.userAgentData || !navigator.userAgentData.getHighEntropyValues) return; navigator.userAgentData.getHighEntropyValues(["platform", "platformVersion", "model", "brands"]) .then(function (hints) { hints = hints || {}; var info = getDeviceInfo(); if (hints.platform) { var p = String(hints.platform).toLowerCase(); if (p.indexOf("win") >= 0) info.systemName = "Windows"; else if (p.indexOf("mac") >= 0) info.systemName = "macOS"; else if (p.indexOf("android") >= 0) info.systemName = "Android"; else if (p === "ios") info.systemName = "iOS"; else info.systemName = hints.platform; } if (hints.platformVersion) info.systemVersion = String(hints.platformVersion); if (hints.model) info.deviceModel = String(hints.model); if (hints.brands && hints.brands.length) { for (var i = hints.brands.length - 1; i >= 0; i--) { var b = hints.brands[i]; if (b && b.brand && b.brand !== "Not)A;Brand" && b.brand !== "Chromium") { info.deviceBrand = b.brand; break; } } } __deviceInfo = info; applyDeviceContextToSdk(info); }) .catch(function () {}); } var REFERRER_SPECIAL_NAMES = { direct: "直接访问", external: "外部站点", unknown: "未知来源", }; function normalizeReferrer(info) { info = info || {}; var pk = nonEmpty(info.page_key, ""); var pn = nonEmpty(info.page_name, ""); if (!pk) { return { page_key: "direct", page_name: "直接访问" }; } if (REFERRER_SPECIAL_NAMES[pk]) { return { page_key: pk, page_name: pn || REFERRER_SPECIAL_NAMES[pk], }; } return { page_key: pk, page_name: pn || pk }; } function getReferrerFromURLHint() { try { var search = (global.location && global.location.search) || ""; if (!search) return null; var p = new URLSearchParams(search); var src = p.get("utm_source") || p.get("from") || p.get("source") || ""; src = String(src).trim(); if (src) { return { page_key: "external", page_name: src.slice(0, 128) }; } } catch (e) {} return null; } /** * 获取来源页面信息. 来源优先级: * 1) sessionStorage 里上一次 trackPageView 时存的 {page_key, page_name} * (同一 tab 内站内跳转最准, 能保留 page_name 中文等) * 2) document.referrer 解析: * - 与当前页同源: 用 URL pattern 反推 page_key / page_name * - 不同源: page_key="external", page_name=referrer 的 host * 3) 无 referrer 且 sessionStorage 空: page_key="direct", page_name="直接访问" * (典型场景: 用户从书签 / 地址栏 / 二维码 / app 内嵌外跳直接进站) * * 经此调整后 referrer_page_key / referrer_page_name 不再出现空字符串, * 数据分析侧可按 "direct" / "external" / 具体业务 key 区分来路。 */ function getReferrerPageInfo() { // 1) sessionStorage (站内跳转, 最准) try { var raw = sessionStorage.getItem("__tracker_last_page"); if (raw) { var obj = JSON.parse(raw); if (obj && obj.page_key) { return normalizeReferrer({ page_key: String(obj.page_key), page_name: String(obj.page_name || ""), }); } } } catch (e) {} // 2) document.referrer(含外链点击进入) var ref = ""; try { ref = document.referrer || ""; } catch (e) {} if (ref) { try { var u = new URL(ref, location.href); if (u.host === location.host) { // 同源, 按 URL pattern 推断 page_key / page_name var hit = inferPageFromPath(u.pathname); if (hit && hit.page_key) { return normalizeReferrer(hit); } return normalizeReferrer({ page_key: "internal_other", page_name: "站内其他页面", }); } return normalizeReferrer({ page_key: "external", page_name: u.host || "外部站点", }); } catch (e) {} } // 3) URL 投放参数(referrer 为空但属外站导流) var hint = getReferrerFromURLHint(); if (hint) return normalizeReferrer(hint); // 4) 无来源:书签 / 地址栏 / 二维码 / 外链 noreferrer 等 return normalizeReferrer({ page_key: "direct", page_name: "直接访问" }); } /** * 根据 URL pathname 反推 page_key / page_name. * 顺序需要与各页面模板里 window.__SDK_PAGE__ / trackerInit pageKey 保持一致, * 后续模板新增页面时记得在这里同步登记。 */ function inferPageFromPath(p) { p = String(p || ""); if (p === "" || p === "/") { return { page_key: "home", page_name: "首页" }; } // 与 app/api/web/tracker_page.go、各页 trackerInit pageKey 保持一致 // 专题列表与专题详情共用 /topic 前缀,必须先匹配列表分页。 if (/^\/(?:topic|more)(?:\/page\/\d+)?\/?$/.test(p)) { return { page_key: "theme_index", page_name: "专题列表" }; } var RULES = [ [/^\/list(\/|$)/, "list", "短剧列表"], [/^\/series(\/|$)/, "series", "AI成人剧集"], [/^\/(?:tag|tag-list)(\/|$)/, "tag_list", "标签详情"], [/^\/rank(\/|$)/, "rank", "排行榜"], [/^\/today-update(\/|$)/, "today_update", "今日更新"], [/^\/category(\/|$)/, "category_detail", "分类详情"], [/^\/(?:topic|more)(\/|$)/, "topic", "专题详情"], [/^\/download-app(\/|$)/, "download_app", "下载APP"], [/^\/application(\/|$)/, "application", "应用推荐"], [/^\/home-road(\/|$)/, "home_road", "回家的路"], [/^\/detail(\/|$)/, "detail", "短剧详情"], [/^\/play(\/|$)/, "play", "播放页"], [/^\/login(\/|$)/, "login", "登录"], [/^\/register(\/|$)/, "register", "注册"], [/^\/user-center(\/|$)/, "user_center", "个人中心"], [/^\/deposit(\/|$)/, "deposit", "充值中心"], [/^\/collect(\/|$)/, "collect", "我的收藏"], [/^\/history(\/|$)/, "history", "观看历史"], [/^\/setting(\/|$)/, "setting", "设置"], [/^\/edit-profile(\/|$)/, "edit_profile", "编辑资料"], [/^\/edit-password(\/|$)/, "edit_password", "修改密码"], [/^\/search(\/|$)/, "search", "搜索结果页"], [/^\/about-us(\/|$)/, "about_us", "关于我们"], [/^\/user-agreement(\/|$)/, "user_agreement", "用户协议"], [/^\/privacy-policy(\/|$)/, "privacy_policy", "隐私政策"], [/^\/help-center(\/|$)/, "help_center", "帮助中心"], [/^\/richtext(\/|$)/, "rich_text", "富文本页"], [/^\/feedback(\/|$)/, "feedback", "问题反馈"], [/^\/(?:nav|dh)(\/|$)/, "nav", "官方导航"] ]; for (var i = 0; i < RULES.length; i++) { if (RULES[i][0].test(p)) { return { page_key: RULES[i][1], page_name: RULES[i][2] }; } } return null; } function savePageInfo(info) { try { var pk = nonEmpty(info.page_key, ""); if (!pk) return; sessionStorage.setItem( "__tracker_last_page", JSON.stringify({ page_key: pk, page_name: nonEmpty(info.page_name, "") }) ); } catch (e) {} } function normalizeReferrer(info) { info = info || {}; var pk = nonEmpty(info.page_key, ""); var pn = nonEmpty(info.page_name, ""); if (!pk) { return { page_key: "direct", page_name: "直接访问" }; } if (REFERRER_SPECIAL_NAMES[pk]) { return { page_key: pk, page_name: pn || REFERRER_SPECIAL_NAMES[pk], }; } return { page_key: pk, page_name: pn || pk }; } // 修正无效 navigation_key(如 "0"、cat_、subcat_) function normalizeNavigationKey(navKey, navName, href) { var k = String(navKey || "").trim(); var invalid = !k || k === "0" || k === "cat_" || k === "subcat_" || /^cat_0$/.test(k) || /^subcat_0$/.test(k); if (!invalid) return k; if (href) { try { var u = new URL(href, location.href); var pathKey = u.pathname.replace(/^\/+|\/+$/g, "").replace(/[^\w]+/g, "_"); if (pathKey) return "nav_path_" + pathKey; } catch (e) {} } var slug = String(navName || "").trim().replace(/\s+/g, "_").replace(/[^\w一-龥]+/g, "_"); if (slug) return "nav_name_" + slug.slice(0, 48); return "nav_other"; } // ------- 公共字段补齐:device_brand / device_model / system_name / system_version ------- // 背景:web-sdk 仅上报 device(Android/iOS/PC) 与 user_agent, // device_brand / device_model 被 SDK 硬编码为空,且不含 system_name / system_version, // 也无公开 API 注入顶层公共字段。这里基于 user_agent 解析并在上报前补齐这四个字段。 function guessAndroidBrand(model, ua) { var s = (model || "") + " " + (ua || ""); var map = [ [/Build\/HONOR|\bHONOR\b/i, "Honor"], [/Build\/HUAWEI|HUAWEI|HW-|HMA-|ELS-|ANA-|\bJAD-|\bNOH-/i, "Huawei"], // 华为/荣耀机型代号:XXX-AL00 / -AN00 / -TL00 / -AN90 等 [/\b[A-Z]{2,4}-(AL|AN|TL|EM|DL)\d{2}\b/, "Huawei"], [/SM-|samsung|Galaxy/i, "Samsung"], [/Pixel/i, "Google"], [/Redmi|POCO|XiaoMi|\bMi\b|Mi\s?\d/i, "Xiaomi"], // 小米/红米数字机型代号:2xxxxxxx / 2xxxxAA 等(以 2 开头的全大写数字混合码) [/\b2[0-9]{3,4}[A-Z][A-Z0-9]{1,}\b/, "Xiaomi"], [/OPPO|\bCPH\d{3,4}|\bPB\w{2}|\bP[A-Z]{2}\d{2}\b/, "OPPO"], [/vivo|\bV\d{4}[A-Z]?\b/i, "vivo"], [/OnePlus|ONEPLUS|\bGM19|\bHD19|\bKB20|\bLE21/i, "OnePlus"], [/realme|\bRMX\d{3,4}/i, "realme"], [/Meizu/i, "Meizu"], [/Lenovo/i, "Lenovo"], [/\bZTE\b/i, "ZTE"], [/Nokia/i, "Nokia"], [/Sony|\bSO-|\bXQ-/i, "Sony"], [/Motorola|\bmoto\b/i, "Motorola"], [/asus/i, "ASUS"] ]; for (var i = 0; i < map.length; i++) { if (map[i][0].test(s)) return map[i][1]; } return ""; } // 爬虫/机器人识别:这类 UA 无真实设备/系统,统一标记 system_name="Bot", // 避免设备字段为空(满足"无法解析需补充特殊情况说明")。 function parseBotInfo(ua) { var known = [ [/bingbot/i, "Bing", "bingbot"], [/googlebot/i, "Google", "Googlebot"], [/baiduspider/i, "Baidu", "Baiduspider"], [/yandex(bot|\.com\/bots)/i, "Yandex", "YandexBot"], [/duckduckbot/i, "DuckDuckGo", "DuckDuckBot"], [/bytespider/i, "ByteDance", "Bytespider"], [/petalbot/i, "Huawei", "PetalBot"], [/applebot/i, "Apple", "Applebot"], [/sogou.{0,6}spider/i, "Sogou", "Sogou Spider"], [/360spider/i, "360", "360Spider"], [/semrushbot/i, "Semrush", "SemrushBot"], [/ahrefsbot/i, "Ahrefs", "AhrefsBot"], [/facebookexternalhit/i, "Facebook", "facebookexternalhit"] ]; var ver = (ua.match(/(?:bot|spider)[\/ ]v?([\d.]+)/i) || [])[1] || ""; for (var i = 0; i < known.length; i++) { if (known[i][0].test(ua)) { return { device_brand: known[i][1], device_model: known[i][2], system_name: "Bot", system_version: ver }; } } // 通用:"bot/x.y" 或 "spider/x.y"(正常浏览器不含该模式,避免误判) var gm = ua.match(/([A-Za-z][A-Za-z]*bot)\/([\d.]+)/) || ua.match(/([A-Za-z][A-Za-z]*spider)\/?([\d.]*)/i); if (gm) return { device_brand: "", device_model: gm[1], system_name: "Bot", system_version: gm[2] || "" }; return null; } // 低熵 Client Hints(同步可得),用于 UA 为空时兜底 function getClientHintsInfo() { var out = { system_name: "", mobile: null, brands: "" }; try { var uad = (typeof navigator !== "undefined") && navigator.userAgentData; if (uad) { if (uad.platform) out.system_name = String(uad.platform); // Windows/macOS/Android/iOS/Linux/Chrome OS... if (typeof uad.mobile === "boolean") out.mobile = uad.mobile; if (Array.isArray(uad.brands)) { out.brands = uad.brands.map(function (b) { return b.brand + "/" + b.version; }).join(" "); } } } catch (e) {} return out; } // user_agent 兜底:navigator.userAgent 为空时,用 Client Hints / navigator 信息拼一个非空值, // 保证 PC/Web 场景 user_agent 至少有值(要求:无法取到时也不能为空)。 function buildUAFallback() { try { var nav = (typeof navigator !== "undefined") ? navigator : null; if (nav && nav.userAgent) return nav.userAgent; var ch = getClientHintsInfo(); var parts = []; if (ch.system_name) parts.push(ch.system_name); if (ch.brands) parts.push(ch.brands); if (nav && nav.platform) parts.push(nav.platform); if (nav && nav.appVersion) parts.push(nav.appVersion); if (nav && nav.language) parts.push(nav.language); var s = parts.filter(Boolean).join("; ").replace(/\s+/g, " ").trim(); return s || "unknown"; } catch (e) { return "unknown"; } } // 提取安卓/鸿蒙机型:设备代号(如 ADY-AL10) 优先;其次 "; Build/";再次 "; )"。 // 过滤掉操作系统/SDK/语言等非机型 token,避免把 "OpenHarmony 6.1"、"HMSCore x.y" 当成机型。 function extractAndroidModel(ua) { // 1) 设备代号:3 段式(XXX-AL00 / ADY-AL10 等) var code = ua.match(/;\s*([A-Z][A-Z0-9]{1,5}-[A-Z0-9]{2,})/); if (code) return code[1].trim(); // 2) "; Build/" 或 "; )" var mm = ua.match(/;\s*([^;()]+?)\s+Build\//) || ua.match(/;\s*([^;()]+?)\)\s*(?:AppleWebKit|Gecko|$)/); var model = mm ? mm[1].trim() : ""; model = model.replace(/\bwv\b/i, "").replace(/^U\s+/i, "") .replace(/\bzh[-_][A-Za-z-]+\b/gi, "").replace(/\s+/g, " ").trim(); // 丢弃系统/SDK/无效 token if (/^(rv:|Mobile$|Tablet$)/i.test(model) || /OpenHarmony|HarmonyOS|HMSCore|^Android\b|^Linux\b/i.test(model) || !/[A-Za-z0-9]/.test(model)) model = ""; return model; } function parseUAClientInfo(ua) { ua = ua || ((typeof navigator !== "undefined" && navigator.userAgent) || ""); var out = { device_brand: "", device_model: "", system_name: "", system_version: "" }; try { // 0) 爬虫/机器人优先识别 var bot = parseBotInfo(ua); if (bot) return bot; var m; if (/iPhone|iPad|iPod/.test(ua)) { out.device_brand = "Apple"; out.device_model = /iPad/.test(ua) ? "iPad" : (/iPod/.test(ua) ? "iPod" : "iPhone"); out.system_name = "iOS"; m = ua.match(/OS (\d+[_.]\d+(?:[_.]\d+)?)/); if (m) out.system_version = m[1].replace(/_/g, "."); } else if (/OpenHarmony|HarmonyOS|ArkWeb/i.test(ua)) { // 鸿蒙(含 "Android x; HarmonyOS" 兼容标识) out.system_name = "HarmonyOS"; m = ua.match(/(?:OpenHarmony|HarmonyOS)\s+([\d.]+)/i) || ua.match(/Android\s+([\d.]+)/); if (m) out.system_version = m[1]; out.device_model = extractAndroidModel(ua) || (/Tablet/i.test(ua) ? "Tablet" : "Phone"); out.device_brand = guessAndroidBrand(out.device_model, ua); } else if (/Android/.test(ua)) { out.system_name = "Android"; m = ua.match(/Android\s+([\d.]+)/); if (m) out.system_version = m[1]; var model = extractAndroidModel(ua); // 部分浏览器(Firefox 等)不带机型,回退为 Mobile/Tablet,避免空值 if (!model) model = /Tablet/i.test(ua) ? "Tablet" : "Mobile"; out.device_model = model; out.device_brand = guessAndroidBrand(model, ua); } else if (/Windows NT/.test(ua)) { out.system_name = "Windows"; m = ua.match(/Windows NT ([\d.]+)/); var winMap = { "10.0": "10", "6.3": "8.1", "6.2": "8", "6.1": "7" }; if (m) out.system_version = winMap[m[1]] || m[1]; out.device_model = "PC"; } else if (/Windows/i.test(ua)) { // 非标准 Windows UA(如 "X11; Windows" 的 HeyTap/Oppo 浏览器) out.system_name = "Windows"; out.device_model = "PC"; } else if (/CrOS/.test(ua)) { // ChromeOS out.system_name = "Chrome OS"; out.device_model = "PC"; m = ua.match(/CrOS\s+\S+\s+([\d.]+)/); if (m) out.system_version = m[1]; } else if (/Macintosh|Mac OS X/.test(ua)) { // iPadOS 13+ 的 Safari 桌面模式会伪装成 Macintosh,需用触点数纠正为 iPad var isTouchIPad = false; try { isTouchIPad = (typeof navigator !== "undefined") && navigator.maxTouchPoints && navigator.maxTouchPoints > 1 && /Macintosh/.test(ua); } catch (e) {} if (isTouchIPad) { out.system_name = "iOS"; out.device_brand = "Apple"; out.device_model = "iPad"; m = ua.match(/Version\/(\d+[_.]\d+(?:[_.]\d+)?)/); if (m) out.system_version = m[1].replace(/_/g, "."); } else { out.system_name = "macOS"; out.device_brand = "Apple"; out.device_model = "Mac"; m = ua.match(/Mac OS X (\d+[_.]\d+(?:[_.]\d+)?)/); if (m) out.system_version = m[1].replace(/_/g, "."); } } else if (/Linux/.test(ua)) { out.system_name = "Linux"; out.device_model = "PC"; } // UA 为空或未解析出时,用 Client Hints(navigator.userAgentData) 兜底系统名/机型 if (!out.system_name || !out.device_model) { var ch = getClientHintsInfo(); if (!out.system_name && ch.system_name) out.system_name = ch.system_name; if (!out.device_model) { if (ch.mobile === false) out.device_model = "PC"; else if (ch.mobile === true) out.device_model = "Mobile"; } if (!out.device_brand && ch.system_name === "macOS") out.device_brand = "Apple"; } // 最终兜底:保证 system_name / system_version 不为空(需求:不能为空) if (!out.system_name) { // 依据设备类型给出系统名,最差为 Unknown var dev = guessDeviceTypeFromUA(ua); out.system_name = dev === "Android" ? "Android" : dev === "iOS" ? "iOS" : (dev === "PC" ? "PC" : "Unknown"); } if (!out.system_version) out.system_version = "0.0.0"; if (!out.device_model) out.device_model = "Unknown"; } catch (e) {} return out; } // 设备类型(Android/iOS/PC)推断,与 web-sdk 内部口径一致,UA 兜底 function guessDeviceTypeFromUA(ua) { ua = ua || ((typeof navigator !== "undefined" && navigator.userAgent) || ""); if (/Android/i.test(ua)) return "Android"; if (/iPhone|iPad|iPod/i.test(ua)) return "iOS"; try { // iPadOS 桌面模式 if (/Macintosh/.test(ua) && typeof navigator !== "undefined" && navigator.maxTouchPoints > 1) return "iOS"; } catch (e) {} return "PC"; } // 设备指纹兜底:当 SDK 未能生成 device_fingerprint 时,基于稳定特征生成一个非空指纹。 function buildFingerprintFallback() { try { var nav = (typeof navigator !== "undefined") ? navigator : {}; var scr = (typeof screen !== "undefined") ? screen : {}; var parts = [ nav.userAgent || "", nav.platform || "", nav.language || "", (scr.width || "") + "x" + (scr.height || ""), scr.colorDepth || "", (new Date()).getTimezoneOffset(), nav.hardwareConcurrency || "", nav.deviceMemory || "" ].join("||"); // 简易 FNV-1a 32 位哈希 var h = 2166136261 >>> 0; for (var i = 0; i < parts.length; i++) { h ^= parts.charCodeAt(i); h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0; } var hex = h.toString(16); while (hex.length < 8) hex = "0" + hex; return "fp_fb_" + hex; } catch (e) { return "fp_fb_unknown"; } } // 安装上报请求拦截,在 SDK 发送批量上报前为每条记录补齐公共字段: // device_brand / device_model / system_name / system_version(基于 UA / Client Hints) // user_agent(为空时用兜底值,保证非空) // 仅对上报端点(eventTracking/batchReport)生效;全程 try/catch,任何异常都回退原始请求,不影响上报。 function installCommonFieldPatch() { if (global.__trackerCFPatch) return; global.__trackerCFPatch = true; var info = parseUAClientInfo(); var uaFallback = buildUAFallback(); // 始终非空(最差为 "unknown") var deviceFallback = guessDeviceTypeFromUA(); // Android / iOS / PC var fpFallback = buildFingerprintFallback(); // device_fingerprint 兜底(非空) var SDK_VERSION_FALLBACK = "1.1.8"; // 与所引入的 web-sdk 版本一致 var FP_VERSION_FALLBACK = "1.0.0"; var APP_VERSION_FALLBACK = "1.0.0"; // 始终安装:即便设备字段解析为空,也要保证公共字段非空 function isReportUrl(u) { try { return /eventTracking\/batchReport/i.test(String(u || "")); } catch (e) { return false; } } function augmentBody(body) { try { if (typeof body !== "string" || !body) return null; var arr = JSON.parse(body); if (!Array.isArray(arr) || !arr.length) return null; var changed = false; for (var i = 0; i < arr.length; i++) { var r = arr[i]; if (!r || typeof r !== "object") continue; if (!r.device_brand && info.device_brand) { r.device_brand = info.device_brand; changed = true; } if (!r.device_model && info.device_model) { r.device_model = info.device_model; changed = true; } if (!r.system_name && info.system_name) { r.system_name = info.system_name; changed = true; } if (!r.system_version && info.system_version) { r.system_version = info.system_version; changed = true; } // 以下字段强制保证非空(需求:不能为空) if (!r.device) { r.device = deviceFallback; changed = true; } if (!r.system_name) { r.system_name = "Unknown"; changed = true; } if (!r.system_version) { r.system_version = "0.0.0"; changed = true; } if (!r.device_fingerprint) { r.device_fingerprint = fpFallback; changed = true; } if (!r.fp_version) { r.fp_version = FP_VERSION_FALLBACK; changed = true; } if (!r.sdk_version) { r.sdk_version = SDK_VERSION_FALLBACK; changed = true; } if (!r.app_version) { r.app_version = APP_VERSION_FALLBACK; changed = true; } // user_agent 为空(含 undefined/null/空串)时补齐,保证非空 if (!r.user_agent && uaFallback) { r.user_agent = uaFallback; changed = true; } } return changed ? JSON.stringify(arr) : null; } catch (e) { return null; } } // fetch(SDK 默认走 fetch) if (typeof global.fetch === "function") { var origFetch = global.fetch; global.fetch = function (input, init) { try { var url = (typeof input === "string") ? input : (input && input.url); if (isReportUrl(url) && init && typeof init.body === "string") { var nb = augmentBody(init.body); if (nb != null) init.body = nb; } } catch (e) {} return origFetch.apply(this, arguments); }; } // XHR 兜底 if (typeof XMLHttpRequest !== "undefined") { var op = XMLHttpRequest.prototype.open; var os = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { try { this.__trackerReportUrl = isReportUrl(url); } catch (e) {} return op.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { try { if (this.__trackerReportUrl && typeof body === "string") { var nb = augmentBody(body); if (nb != null) return os.call(this, nb); } } catch (e) {} return os.apply(this, arguments); }; } // sendBeacon(默认关闭,仅兜底字符串入参) try { if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") { var ob = navigator.sendBeacon.bind(navigator); navigator.sendBeacon = function (url, data) { try { if (isReportUrl(url) && typeof data === "string") { var nb = augmentBody(data); if (nb != null) data = nb; } } catch (e) {} return ob(url, data); }; } } catch (e) {} } // ------- 初始化 ------- function trackerInit(opts) { opts = opts || {}; // 0) 安装公共字段补齐(须在 SDK 发出任何上报前完成) installCommonFieldPatch(); // 设置当前页面信息(即便 SDK 尚未加载也先保存,后续点击事件能用到) var inferredPage = inferPageFromPath((global.location && global.location.pathname) || "") || { page_key: "internal_other", page_name: "站内其他页面", }; var configuredPageKey = nonEmpty(opts.pageKey, ""); var configuredPageName = nonEmpty(opts.pageName, ""); __pageInfo.page_key = (!configuredPageKey || configuredPageKey === "unknown") ? inferredPage.page_key : configuredPageKey; __pageInfo.page_name = (!configuredPageName || configuredPageName === "未知页面") ? inferredPage.page_name : configuredPageName; __referrerPageInfo = getReferrerPageInfo(); // 1) 先注册全局点击监听(document 一定存在,不依赖 DOM 完成) bindClickListeners(); safeCall(function () { if (!__inited) { global.WebSDK.init({ appId: opts.appId, uid: opts.uid || "", channel: opts.channel || "", encryptedConfig: opts.encryptedConfig || "", appVersion: opts.appVersion || "1.0.1", // 项目会手动上报带业务页面字段的 PV/点击,关闭 SDK 自动事件避免重复上报。 disableAutoEvents: ["app_page_view", "page_click"], }); // 补齐 sid:会话开始即强制生成并持久化 sid(必填公共字段), // 避免首个事件因 sid 尚未生成而上报空 sid。 try { global.WebSDK.notifyPageLoad && global.WebSDK.notifyPageLoad(); } catch (e) {} // 预热并缓存 sid 状态,确认存在后后续事件无需再异步校验 try { if (global.WebSDK.getCommonField) { global.WebSDK.getCommonField("sid").then(function (sid) { if (sid) __sidKnownPresent = true; }).catch(function () {}); } } catch (e) {} __inited = true; } // 2) 页面展示:等 DOM 解析完再上报,避免 SDK 还在做域名 ping 时事件丢失 whenDomReady(function () { trackPageView(); // 保存当前页面信息以便下一次跳转作为 referrer savePageInfo(__pageInfo); if (!global.__trackerReady) { global.__trackerReady = true; try { global.dispatchEvent(new CustomEvent("tracker:ready", { detail: currentPageInfo() })); } catch (e) { try { var readyEvent = document.createEvent("Event"); readyEvent.initEvent("tracker:ready", false, false); global.dispatchEvent(readyEvent); } catch (ignored) {} } } }); // 3) 广告展示观察必须等到 DOM 解析完 whenDomReady(function () { bindAdImpressionObserver(); bindAdMutationObserver(); if (document.readyState === "complete") { rescanAds(); } else { global.addEventListener("load", rescanAds); } }); }); } // 等 DOMContentLoaded function whenDomReady(cb) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { cb(); }, { once: true }); } else { cb(); } } // ------- 公共上报方法 ------- // 1. 导航路径行为 function trackNavigation(navKey, navName, href) { var k = normalizeNavigationKey(navKey, navName, href); var n = nonEmpty(navName, k); if (!k || k === "0") return; sendTrack(function () { global.WebSDK.track.navigation({ navigation_key: k, navigation_name: n, }); }); } // 2. 应用页面展示 function trackPageView(extra) { extra = extra || {}; var loadTime = 0; try { var nav = (global.performance && typeof global.performance.getEntriesByType === "function") ? (global.performance.getEntriesByType("navigation")[0] || null) : null; if (nav && nav.loadEventEnd > 0 && nav.startTime != null) { loadTime = Math.max(0, Math.round(nav.loadEventEnd - nav.startTime)); } else { var now = (global.performance && global.performance.now) ? global.performance.now() : Date.now(); loadTime = Math.max(0, Math.round(now - __pageStart)); } } catch (e) {} var pi = currentPageInfo(); var pageKey = pi.page_key; var pageName = pi.page_name; var ref = normalizeReferrer(getReferrerPageInfo()); sendTrack(function () { global.WebSDK.track.appPageView({ user_type: nonEmpty(extra.user_type, "guest"), page_key: pageKey, page_name: pageName, referrer_page_key: ref.page_key, referrer_page_name: ref.page_name, current_page_key: pageKey, current_page_name: pageName, page_load_time: parseInt(extra.page_load_time != null ? extra.page_load_time : loadTime, 10) || 0, recommend_trace_id: extra.recommend_trace_id || "", }); }); } // 3. 应用点击事件 // 注意(需求): click_x_percent / click_y_percent = 点击位置横/纵坐标百分比(0-100)。 // SDK 1.1.8 起 click_page_x/click_page_y 使用视口坐标(与 clientX/clientY 一致), // 百分比也按视口尺寸计算;screen.width/height 仅作为独立的屏幕分辨率字段上报。 function trackPageClick(e) { if (!isReady()) return; try { var sw = (global.screen && global.screen.width) || 0; // 屏幕分辨率(单独字段上报) var sh = (global.screen && global.screen.height) || 0; var docEl = document.documentElement || {}; var viewportW = global.innerWidth || docEl.clientWidth || 0; var viewportH = global.innerHeight || docEl.clientHeight || 0; // 点击的视口坐标(不含页面滚动距离) var x = e.clientX != null ? e.clientX : 0; var y = e.clientY != null ? e.clientY : 0; // 横/纵坐标百分比(0-100) var xPct = viewportW > 0 ? clamp((x / viewportW) * 100, 0, 100) : 0; var yPct = viewportH > 0 ? clamp((y / viewportH) * 100, 0, 100) : 0; var pi = currentPageInfo(); sendTrack(function () { global.WebSDK.track.pageClick({ page_key: pi.page_key, page_name: pi.page_name, click_page_x: parseInt(x, 10) || 0, click_page_y: parseInt(y, 10) || 0, click_x_percent: xPct, click_y_percent: yPct, screen_width: parseInt(sw, 10) || 0, screen_height: parseInt(sh, 10) || 0, recommend_trace_id: "", }); }); } catch (err) { logError(err); } } // 4. 视频事件(播放、暂停) function trackVideoEvent(params) { params = params || {}; var video_id = nonEmpty(params.video_id, ""); if (!video_id) return; sendTrack(function () { global.WebSDK.track.videoEvent({ media_id: String(params.media_id || video_id), video_id: video_id, video_title: nonEmpty(params.video_title, "未命名视频"), video_type_id: nonEmpty(params.video_type_id, "0"), video_type_name: nonEmpty(params.video_type_name, "未分类"), video_content_type: String(params.video_content_type || "video"), recommend_trace_id: String(params.recommend_trace_id || ""), video_tag_key: tagKeyOrDefault(params.video_tag_key), video_tag_name: tagNameOrDefault(params.video_tag_name), video_duration: parseInt(params.video_duration || 0, 10) || 0, play_duration: parseInt(params.play_duration || 0, 10) || 0, play_progress: clamp(params.play_progress || 0, 0, 100), video_behavior_key: nonEmpty(params.video_behavior_key, "video_view"), video_behavior_name: nonEmpty(params.video_behavior_name, "视频行为"), }); }); } // 阅读进度百分比限制在 0-100 function clampProgress(n) { var v = parseInt(n || 0, 10); if (!isFinite(v) || v < 0) return 0; if (v > 100) return 100; return v; } // 4.1 小说事件 novel_event(按埋点文档对齐) // 上报来源:客户端;必填 novel_id/novel_title/novel_type_id/novel_type_name/ // novel_behavior_key/novel_behavior_name;必填可空 novel_tag_key/novel_tag_name; // 整数 read_progress(0-100)/page_no(从 1 开始;列表页 0)。 // chapter_id / chapter_name 没有章节传 "";recommend_trace_id 未对接推荐引擎传 ""。 // novel_behavior_key 取值:view(展示) / page_next(下一页) / page_prev(上一页) / complete(读完)。 function trackNovelEvent(params) { params = params || {}; var novel_id = nonEmpty(params.novel_id, ""); if (!novel_id) return; sendTrack(function () { global.WebSDK.track.novelEvent({ media_id: String(params.media_id || novel_id), novel_id: novel_id, novel_title: nonEmpty(params.novel_title, "未命名小说"), chapter_id: String(params.chapter_id || ""), chapter_name: String(params.chapter_name || ""), novel_type_id: nonEmpty(params.novel_type_id, "default"), novel_type_name: nonEmpty(params.novel_type_name, "默认分类"), recommend_trace_id: String(params.recommend_trace_id || ""), novel_tag_key: tagKeyOrDefault(params.novel_tag_key), novel_tag_name: tagNameOrDefault(params.novel_tag_name), read_progress: clampProgress(params.read_progress), page_no: parseInt(params.page_no || 0, 10) || 0, novel_behavior_key: nonEmpty(params.novel_behavior_key, "view"), novel_behavior_name: nonEmpty(params.novel_behavior_name, "展示"), }); }); } // 4.2 漫画事件 comic_event(按埋点文档对齐,字段口径同 novel_event) // comic_behavior_key 取值:view(展示) / page_next(下一页) / page_prev(上一页) / complete(读完)。 function trackComicEvent(params) { params = params || {}; var comic_id = nonEmpty(params.comic_id, ""); if (!comic_id) return; sendTrack(function () { global.WebSDK.track.comicEvent({ media_id: String(params.media_id || comic_id), comic_id: comic_id, comic_title: nonEmpty(params.comic_title, "未命名漫画"), chapter_id: String(params.chapter_id || ""), chapter_name: String(params.chapter_name || ""), comic_type_id: nonEmpty(params.comic_type_id, "default"), comic_type_name: nonEmpty(params.comic_type_name, "默认分类"), recommend_trace_id: String(params.recommend_trace_id || ""), comic_tag_key: tagKeyOrDefault(params.comic_tag_key), comic_tag_name: tagNameOrDefault(params.comic_tag_name), read_progress: clampProgress(params.read_progress), page_no: parseInt(params.page_no || 0, 10) || 0, comic_behavior_key: nonEmpty(params.comic_behavior_key, "view"), comic_behavior_name: nonEmpty(params.comic_behavior_name, "展示"), }); }); } // 5. 关键词搜索事件 function trackKeywordSearch(params) { params = params || {}; var keyword = nonEmpty(params.keyword, ""); if (!keyword) return; sendTrack(function () { global.WebSDK.track.keywordSearch({ keyword: keyword, search_result_count: parseInt(params.search_result_count || 0, 10) || 0, search_content_type: String(params.search_content_type || ""), search_trace_id: String(params.search_trace_id || ""), search_id: String(params.search_id || ""), }); }); } // 6. 关键词搜索结果点击事件 function trackKeywordClick(params) { params = params || {}; var keyword = nonEmpty(params.keyword, ""); var itemId = nonEmpty(params.click_item_id, ""); if (!keyword || !itemId) return; sendTrack(function () { global.WebSDK.track.keywordClick({ keyword: keyword, click_item_id: itemId, click_item_type_key: nonEmpty(params.click_item_type_key, "video"), click_item_type_name: nonEmpty(params.click_item_type_name, "视频"), click_position: parseInt(params.click_position || 0, 10) || 0, search_trace_id: String(params.search_trace_id || ""), }); }); } // 9. 广告页面点击事件 (event_type: click/close/show) function trackAdvertising(params) { params = params || {}; var id = nonEmpty(params.advertising_id, ""); if (!id) return; sendTrack(function () { global.WebSDK.track.advertising({ event_type: nonEmpty(params.event_type, "click"), advertising_key: nonEmpty(params.advertising_key, id), advertising_name: nonEmpty(params.advertising_name, "广告_" + id), advertising_id: id, }); }); } // 7. 广告展示日志 function trackAdImpression(params) { params = params || {}; var ad_id = nonEmpty(params.ad_id, ""); var pi = currentPageInfo(); var ad_slot_key = nonEmpty(params.ad_slot_key, pi.page_key + "_ad"); var ad_slot_name = nonEmpty(params.ad_slot_name, pi.page_name + "广告位"); if (!ad_id) return; var dedupKey = [ad_slot_key, ad_id, params.creative_id || ""].join("|"); if (__adShownSet[dedupKey]) return; __adShownSet[dedupKey] = true; var payload = { page_key: nonEmpty(params.page_key, pi.page_key), page_name: nonEmpty(params.page_name, pi.page_name), ad_slot_key: ad_slot_key, ad_slot_name: ad_slot_name, ad_id: ad_id, creative_id: String(params.creative_id || ad_id), ad_type: nonEmpty(params.ad_type, "banner"), }; // 强制保证关键字段非空(需求:page_key/page_name/ad_type/creative_id 不能为空) if (!payload.page_key) payload.page_key = "internal_other"; if (!payload.page_name) payload.page_name = "站内其他页面"; if (!payload.ad_type) payload.ad_type = "banner"; if (!payload.creative_id) payload.creative_id = ad_id; sendTrack(function () { global.WebSDK.track.adImpression(payload); }); } // 点击前补发展示,保证 ad_impression.client_ts <= ad_click.client_ts function ensureAdImpressionBeforeClick(ad) { if (!ad || !ad.ad_id) return; var ad_slot_key = nonEmpty(ad.ad_slot_key, currentPageInfo().page_key + "_ad"); var dedupKey = [ad_slot_key, ad.ad_id, ad.creative_id || ""].join("|"); if (!__adShownSet[dedupKey]) { trackAdImpression(ad); } } // 8. 广告点击日志 // 注意(需求): page_key、page_name、ad_type、creative_id 不能为空,缺省取当前页面值 / banner / ad_id function trackAdClick(params) { params = params || {}; var ad_id = nonEmpty(params.ad_id, ""); if (!ad_id) return; ensureAdImpressionBeforeClick(params); var pi = currentPageInfo(); var payload = { page_key: nonEmpty(params.page_key, pi.page_key), page_name: nonEmpty(params.page_name, pi.page_name), ad_slot_key: nonEmpty(params.ad_slot_key, pi.page_key + "_ad"), ad_slot_name: nonEmpty(params.ad_slot_name, pi.page_name + "广告位"), ad_id: ad_id, creative_id: String(params.creative_id || ad_id), ad_type: nonEmpty(params.ad_type, "banner"), }; // 强制保证关键字段非空(需求 1:page_key/page_name/ad_type/creative_id 不能为空) if (!payload.page_key) payload.page_key = "internal_other"; if (!payload.page_name) payload.page_name = "站内其他页面"; if (!payload.ad_type) payload.ad_type = "banner"; if (!payload.creative_id) payload.creative_id = ad_id; sendTrack(function () { global.WebSDK.track.adClick(payload); }); } // ------- 自动收集逻辑 ------- // 通过 data-* 属性的 DOM 收集广告展示(IntersectionObserver) function bindAdImpressionObserver() { if (!global.IntersectionObserver) return; if (global.__trackerAdObserver) return; global.__trackerAdObserver = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { var el = entry.target; var ad = parseAdDataset(el); if (ad && ad.ad_id) { trackAdImpression(ad); global.__trackerAdObserver.unobserve(el); } } }); }, { threshold: 0.1 }); rescanAds(); fireVisibleAdsSync(); } // 同步上报首屏可见广告 function fireVisibleAdsSync() { var vw = global.innerWidth || (document.documentElement && document.documentElement.clientWidth) || 0; var vh = global.innerHeight || (document.documentElement && document.documentElement.clientHeight) || 0; var nodes = document.querySelectorAll("[data-ad-id]"); for (var i = 0; i < nodes.length; i++) { var el = nodes[i]; if (el.__trackerSyncFired) continue; if (!isElementInViewport(el, vw, vh)) continue; var ad = parseAdDataset(el); if (!ad || !ad.ad_id) continue; trackAdImpression(ad); el.__trackerSyncFired = true; try { global.__trackerAdObserver && global.__trackerAdObserver.unobserve(el); } catch (e) {} } } function isElementInViewport(el, vw, vh) { try { var r = el.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return false; var horizOk = r.right > 0 && r.left < vw; var vertOk = r.bottom > 0 && r.top < vh; return horizOk && vertOk; } catch (e) { return false; } } // 重新扫描所有 [data-ad-id] 元素并注册到 observer function rescanAds() { if (!global.__trackerAdObserver) return; var nodes = document.querySelectorAll("[data-ad-id]"); for (var i = 0; i < nodes.length; i++) { var el = nodes[i]; if (el.__trackerObserved) continue; el.__trackerObserved = true; try { global.__trackerAdObserver.observe(el); } catch (e) {} } } // 监听 DOM 动态插入广告元素(懒加载、弹窗显示、swiper clone 等) function bindAdMutationObserver() { if (typeof MutationObserver === "undefined") return; if (global.__trackerMutationObserver) return; global.__trackerMutationObserver = new MutationObserver(function (mutations) { var needRescan = false; for (var i = 0; i < mutations.length; i++) { var m = mutations[i]; if (m.addedNodes && m.addedNodes.length) { needRescan = true; break; } if (m.type === "attributes" && m.attributeName === "data-ad-id") { needRescan = true; break; } } if (needRescan) rescanAds(); }); try { global.__trackerMutationObserver.observe(document.body || document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-ad-id"], }); } catch (e) {} } // 当前页面信息:优先 trackerInit 设置的值;为空则按 URL 推断;最后归为站内其他页面。 // 保证 page_key / page_name 取到真实当前页面值,不会为空。 function currentPageInfo() { var pk = nonEmpty(__pageInfo.page_key, ""); var pn = nonEmpty(__pageInfo.page_name, ""); if (pk) return { page_key: pk, page_name: nonEmpty(pn, pk) }; var hit = inferPageFromPath((global.location && global.location.pathname) || ""); if (hit && hit.page_key) { return { page_key: hit.page_key, page_name: nonEmpty(hit.page_name, hit.page_key) }; } return { page_key: "internal_other", page_name: "站内其他页面" }; } function parseAdDataset(el) { if (!el || !el.dataset) return null; var pi = currentPageInfo(); // 槽位兜底:优先 data-ad-slot-key;缺失时用 data-ad-location 推导(旧版脚本只写了 location), // 保证不再出现 unknown_slot。 var slotKey = el.dataset.adSlotKey || ""; var slotName = el.dataset.adSlotName || ""; if (!slotKey && el.dataset.adLocation != null && el.dataset.adLocation !== "") { slotKey = "location_" + String(el.dataset.adLocation).replace(/[^\w]+/g, "_"); slotName = slotName || ("广告位置" + el.dataset.adLocation); } if (!slotKey) { slotKey = pi.page_key + "_ad"; slotName = slotName || (pi.page_name + "广告位"); } return { ad_id: el.dataset.adId || "", creative_id: el.dataset.creativeId || el.dataset.adId || "", ad_slot_key: slotKey, ad_slot_name: slotName, ad_type: el.dataset.adType || "banner", page_key: pi.page_key, page_name: pi.page_name, }; } // href 是否为可跳转链接(排除空、#、javascript: 等不跳转的情况) function hrefIsNavigable(href) { if (!href) return false; href = String(href).trim(); if (!href || href === "#") return false; if (/^javascript:/i.test(href)) return false; return true; } // 判断本次广告点击是否会真正“跳转”。 // 需求:ad_click 只有广告跳转才上报;滑动/拖拽、关闭等不上报。 // 命中条件(任一): // 1) 广告元素自身或祖先是带有效 href 的 (同窗或新开都算跳转) // 2) clickDD 跳转模式:元素链上带有效的 data-link function isNavigatingAdClick(el) { try { var a = el.closest ? el.closest("a[href]") : null; if (a && hrefIsNavigable(a.getAttribute("href"))) return true; } catch (e) {} var node = el; while (node && node !== document) { if (node.dataset && hrefIsNavigable(node.dataset.link)) return true; node = node.parentNode; } return false; } // 关闭广告弹窗(event_type=close)。从 [data-ad-close] 元素读取广告信息。 function parseAdvDataset(el, eventType) { if (!el || !el.dataset) return null; var id = el.dataset.adId || el.dataset.advertisingId || ""; if (!id) return null; return { event_type: eventType, advertising_id: id, advertising_key: el.dataset.advertisingKey || el.dataset.adSlotKey || id, advertising_name: el.dataset.advertisingName || el.dataset.adSlotName || ("广告_" + id), }; } // 搜索结果点击:从 [data-search-item] 读取信息 function trackSearchResultClick(linkEl) { if (!linkEl || !linkEl.getAttribute) return; var itemId = linkEl.getAttribute("data-search-item") || linkEl.getAttribute("data-id") || ""; if (!itemId) return; var keyword = linkEl.getAttribute("data-search-keyword") || currentSearchKeyword() || ""; if (!keyword) return; trackKeywordClick({ keyword: keyword, click_item_id: itemId, click_item_type_key: linkEl.getAttribute("data-search-item-type-key") || "video", click_item_type_name: linkEl.getAttribute("data-search-item-type-name") || "视频", click_position: parseInt(linkEl.getAttribute("data-search-position") || 0, 10) || 0, search_trace_id: linkEl.getAttribute("data-search-trace-id") || "", }); } // 从 URL 推断当前搜索词(/search/关键词/ 或查询参数)。 function currentSearchKeyword() { try { var q = new URLSearchParams(location.search || ""); var queryKeyword = q.get("q") || q.get("title") || q.get("keyword") || q.get("key") || ""; if (queryKeyword) return queryKeyword; var match = String(location.pathname || "").match(/^\/search\/([^/]+)(?:\/|$)/); return match ? decodeURIComponent(match[1]) : ""; } catch (e) { return ""; } } // 全局点击收集 - 必须能在 执行阶段就挂载(document 一定存在) function bindClickListeners() { if (global.__trackerClickBound) return; global.__trackerClickBound = true; // 拖拽/滑动检测:按下后位移超过阈值即视为"滑动/拖拽",本次产生的 click 不算广告点击。 // 同时覆盖触摸(touch)、鼠标(mouse)与指针(pointer),因为 Swiper 等轮播在桌面端是 // 鼠标拖拽、移动端是触摸/指针拖拽;仅监听 touch 会漏掉桌面端拖拽(如浮动广告滑动)。 var DRAG_THRESHOLD = 6; var __dragStart = null; function __onDragStart(x, y) { __dragStart = { x: x, y: y }; global.__suppressAdClick = false; // 新手势开始,清除上一次标记 } function __onDragMove(x, y) { if (!__dragStart) return; // 仅在按下拖动时判断(pointermove/mousemove 悬停也会触发) if (Math.abs(x - __dragStart.x) > DRAG_THRESHOLD || Math.abs(y - __dragStart.y) > DRAG_THRESHOLD) { global.__suppressAdClick = true; } } function __onDragEnd() { __dragStart = null; } // 触摸(移动端) document.addEventListener("touchstart", function (e) { if (e.touches && e.touches[0]) __onDragStart(e.touches[0].clientX, e.touches[0].clientY); }, { passive: true, capture: true }); document.addEventListener("touchmove", function (e) { if (e.touches && e.touches[0]) __onDragMove(e.touches[0].clientX, e.touches[0].clientY); }, { passive: true, capture: true }); document.addEventListener("touchend", __onDragEnd, { passive: true, capture: true }); document.addEventListener("touchcancel", __onDragEnd, { passive: true, capture: true }); // 指针(鼠标/触控笔/触摸,覆盖桌面端 Swiper 拖拽);不支持则用鼠标事件兜底 if (typeof global.PointerEvent !== "undefined") { document.addEventListener("pointerdown", function (e) { __onDragStart(e.clientX, e.clientY); }, { passive: true, capture: true }); document.addEventListener("pointermove", function (e) { __onDragMove(e.clientX, e.clientY); }, { passive: true, capture: true }); document.addEventListener("pointerup", __onDragEnd, { passive: true, capture: true }); document.addEventListener("pointercancel", __onDragEnd, { passive: true, capture: true }); } else { document.addEventListener("mousedown", function (e) { __onDragStart(e.clientX, e.clientY); }, { passive: true, capture: true }); document.addEventListener("mousemove", function (e) { __onDragMove(e.clientX, e.clientY); }, { passive: true, capture: true }); document.addEventListener("mouseup", __onDragEnd, { passive: true, capture: true }); } document.addEventListener("click", function (e) { trackPageClick(e); // 搜索结果点击 var searchLink = e.target && e.target.closest ? e.target.closest("[data-search-item]") : null; if (searchLink) { trackSearchResultClick(searchLink); } // 广告关闭弹窗 → advertising(event_type=close) var closeEl = e.target && e.target.closest ? e.target.closest("[data-ad-close]") : null; if (closeEl) { var advClose = parseAdvDataset(closeEl, "close"); if (advClose) trackAdvertising(advClose); } // 关闭/禁止上报区域不算广告点击 var isCloseClick = false; try { isCloseClick = !!(e.target && e.target.closest && e.target.closest( '[data-ad-close], [data-no-ad-click], .closeBtn, .nineClose' )); } catch (_) { isCloseClick = false; } var suppressAd = !!global.__suppressAdClick || isCloseClick; if (global.__suppressAdClick) { global.__suppressAdClick = false; } // 自动捕获广告点击 (data-ad-id) 和导航点击 (data-nav-key) var target = e.target; var adReported = false; while (target && target !== document) { if (target.dataset) { // ad_click 仅在“广告跳转”时上报;滑动/拖拽(suppressAd)、关闭、无跳转的点击均不上报 if (target.dataset.adId && !suppressAd && !adReported && isNavigatingAdClick(target)) { var ad = parseAdDataset(target); trackAdClick(ad); adReported = true; } if (target.dataset.navKey) { var href = (target.getAttribute && target.getAttribute("href")) || ""; trackNavigation( target.dataset.navKey, target.dataset.navName || target.textContent || "", href ); } } target = target.parentNode; } }, true); } // ------- 视频自动绑定 ------- // 适用于原生