/**
* 数据上报 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);
}
// ------- 视频自动绑定 -------
// 适用于原生