Files
web_huangguo_web/app/static/js/tracker.js
T
fish03andClaude Opus 5.5 bef9f8b4ed Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 14:43:16 +07:00

1484 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 数据上报 SDK 接入封装 (91porn / JHA-1029)
* 文档: http://34.146.148.117:8033/web/getting-started.html
* 参考: ttrgw-server / mdcom_web 项目 tracker.js
*
* 使用方式: 在 <head> 中先引入 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 };
}
}
// 通用:"<name>bot/x.y" 或 "<name>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) 优先;其次 "; <model> Build/";再次 "; <model>)"。
// 过滤掉操作系统/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) "; <model> Build/" 或 "; <model>)"
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 的 <a>(同窗或新开都算跳转)
// 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 ""; }
}
// 全局点击收集 - 必须能在 <head> 执行阶段就挂载(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);
}
// ------- 视频自动绑定 -------
// 适用于原生 <video>、DPlayer(.video)、xgplayer(构造实例提供 video/root)
function resolveVideoEl(player) {
if (!player) return null;
if (player.tagName && player.tagName.toLowerCase() === "video") return player;
if (player.video && player.video.addEventListener) return player.video; // DPlayer
// xgplayer: 实例上有 root 容器,内部有 video 标签
if (player.root && player.root.querySelector) {
var v = player.root.querySelector("video");
if (v) return v;
}
if (player.media && player.media.addEventListener) return player.media; // 某些播放器
return null;
}
function bindVideoEvents(player, meta) {
var videoEl = resolveVideoEl(player);
if (!videoEl || !videoEl.addEventListener) return;
meta = meta || {};
function dispatch(behaviorKey, behaviorName) {
var dur = parseInt(videoEl.duration || 0, 10) || 0;
var cur = parseInt(videoEl.currentTime || 0, 10) || 0;
var progress = dur > 0 ? Math.min(100, Math.round((cur / dur) * 100)) : 0;
trackVideoEvent({
media_id: meta.media_id || meta.video_id || "",
video_id: meta.video_id || "",
video_title: meta.video_title || "",
video_type_id: meta.video_type_id || "",
video_type_name: meta.video_type_name || "",
video_content_type: meta.video_content_type || "video",
recommend_trace_id: meta.recommend_trace_id || "",
video_tag_key: meta.video_tag_key || TAG_KEY_DEFAULT,
video_tag_name: meta.video_tag_name || TAG_NAME_DEFAULT,
video_duration: dur,
play_duration: cur,
play_progress: progress,
video_behavior_key: behaviorKey,
video_behavior_name: behaviorName,
});
}
if (videoEl.__trackerVideoBound) return;
videoEl.__trackerVideoBound = true;
videoEl.addEventListener("play", function () { dispatch("video_play", "视频播放"); });
videoEl.addEventListener("pause", function () {
if (videoEl.ended) return; // 结束触发的 pause 忽略
dispatch("video_pause", "视频暂停");
});
}
// ------- 暴露 API -------
global.trackerInit = trackerInit;
global.tracker = {
init: trackerInit,
navigation: trackNavigation,
pageView: trackPageView,
pageClick: trackPageClick,
videoEvent: trackVideoEvent,
novelEvent: trackNovelEvent,
comicEvent: trackComicEvent,
keywordSearch: trackKeywordSearch,
keywordClick: trackKeywordClick,
advertising: trackAdvertising,
adImpression: trackAdImpression,
adClick: trackAdClick,
bindVideoEvents: bindVideoEvents,
rescanAds: rescanAds,
currentSearchKeyword: currentSearchKeyword,
setUid: function (uid) {
safeCall(function () { global.WebSDK.setUid(uid || ""); });
},
setChannel: function (channel) {
safeCall(function () { global.WebSDK.setChannel(channel || ""); });
},
setContext: function (ctx) {
safeCall(function () { global.WebSDK.setContext(ctx || {}); });
},
_pageInfo: __pageInfo,
TAG_KEY_DEFAULT: TAG_KEY_DEFAULT,
TAG_NAME_DEFAULT: TAG_NAME_DEFAULT,
};
})(window);