blog-tracking-sdk 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-CC7B5HBV.js +240 -0
- package/dist/chunk-CC7B5HBV.js.map +1 -0
- package/dist/chunk-MLB6KJZI.cjs +244 -0
- package/dist/chunk-MLB6KJZI.cjs.map +1 -0
- package/dist/index.cjs +13 -237
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -238
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +2 -226
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +1 -225
- package/dist/react/index.js.map +1 -1
- package/dist/vue/index.cjs +2 -226
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.js +1 -225
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// src/core/types.ts
|
|
2
|
+
var TrackingSource = /* @__PURE__ */ ((TrackingSource2) => {
|
|
3
|
+
TrackingSource2["Search"] = "search";
|
|
4
|
+
TrackingSource2["Social"] = "social";
|
|
5
|
+
TrackingSource2["Direct"] = "direct";
|
|
6
|
+
TrackingSource2["Referral"] = "referral";
|
|
7
|
+
TrackingSource2["AppBanner"] = "app_banner";
|
|
8
|
+
TrackingSource2["AppTie"] = "app_tie";
|
|
9
|
+
TrackingSource2["Edm"] = "edm";
|
|
10
|
+
TrackingSource2["GuanPhone"] = "guan_phone";
|
|
11
|
+
TrackingSource2["GuanBanner"] = "guan_banner";
|
|
12
|
+
TrackingSource2["AppAd"] = "app_ad";
|
|
13
|
+
return TrackingSource2;
|
|
14
|
+
})(TrackingSource || {});
|
|
15
|
+
|
|
16
|
+
// src/core/Tracker.ts
|
|
17
|
+
var Tracker = class _Tracker {
|
|
18
|
+
constructor(requestFn) {
|
|
19
|
+
this.deviceStorageKey = "BLOG_TRACKING_DEVICE_ID";
|
|
20
|
+
this.requestFn = requestFn;
|
|
21
|
+
this.deviceId = this.getOrInitDeviceId();
|
|
22
|
+
}
|
|
23
|
+
static getInstance(requestFn) {
|
|
24
|
+
if (!_Tracker.instance) {
|
|
25
|
+
if (!requestFn) {
|
|
26
|
+
throw new Error("Tracker needs requestFn for the first initialization");
|
|
27
|
+
}
|
|
28
|
+
_Tracker.instance = new _Tracker(requestFn);
|
|
29
|
+
}
|
|
30
|
+
return _Tracker.instance;
|
|
31
|
+
}
|
|
32
|
+
getOrInitDeviceId() {
|
|
33
|
+
try {
|
|
34
|
+
let id = localStorage.getItem(this.deviceStorageKey);
|
|
35
|
+
if (!id) {
|
|
36
|
+
id = this.generateUUID();
|
|
37
|
+
localStorage.setItem(this.deviceStorageKey, id);
|
|
38
|
+
}
|
|
39
|
+
return id;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.warn("[BlogTracking] localStorage unavailable, using temporary ID");
|
|
42
|
+
return this.generateUUID();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
generateUUID() {
|
|
46
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
|
47
|
+
const r = Math.random() * 16 | 0;
|
|
48
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
49
|
+
return v.toString(16);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
getSource() {
|
|
53
|
+
if (typeof window === "undefined") return "direct" /* Direct */;
|
|
54
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
55
|
+
const utmSource = urlParams.get("utm_source");
|
|
56
|
+
if (utmSource) return utmSource;
|
|
57
|
+
const referrer = document.referrer || "";
|
|
58
|
+
const referrerLower = referrer.toLowerCase();
|
|
59
|
+
if (referrerLower.includes("google") || referrerLower.includes("bing")) {
|
|
60
|
+
return "search" /* Search */;
|
|
61
|
+
}
|
|
62
|
+
const socialKeywords = ["twitter", "t.co", "x.com", "instagram", "facebook", "discord", "reddit", "tiktok"];
|
|
63
|
+
if (socialKeywords.some((k) => referrerLower.includes(k))) {
|
|
64
|
+
return "social" /* Social */;
|
|
65
|
+
}
|
|
66
|
+
if (!referrer) return "direct" /* Direct */;
|
|
67
|
+
return "referral" /* Referral */;
|
|
68
|
+
}
|
|
69
|
+
validate(data) {
|
|
70
|
+
const requiredFields = [
|
|
71
|
+
"event_name",
|
|
72
|
+
"page_url",
|
|
73
|
+
"device_id",
|
|
74
|
+
"page_type",
|
|
75
|
+
"page_id",
|
|
76
|
+
"source"
|
|
77
|
+
];
|
|
78
|
+
for (const field of requiredFields) {
|
|
79
|
+
if (!data[field]) {
|
|
80
|
+
console.warn(`[BlogTracking] Missing field: ${field}`, data);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (data.event_name === "page_stay_end") {
|
|
85
|
+
if (data.stay_duration === void 0 || data.is_effective === void 0 || !data.end_reason) {
|
|
86
|
+
console.warn(`[BlogTracking] page_stay_end missing fields`, data);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
getDeviceType() {
|
|
93
|
+
if (typeof navigator === "undefined") return "desktop";
|
|
94
|
+
const ua = navigator.userAgent;
|
|
95
|
+
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
|
96
|
+
return isMobile ? "mobile" : "desktop";
|
|
97
|
+
}
|
|
98
|
+
getOS() {
|
|
99
|
+
if (typeof navigator === "undefined") return "Unknown";
|
|
100
|
+
const ua = navigator.userAgent;
|
|
101
|
+
if (/iPad|iPhone|iPod/.test(ua)) return "iOS";
|
|
102
|
+
if (/Android/.test(ua)) return "Android";
|
|
103
|
+
if (/Mac/.test(ua)) return "Mac";
|
|
104
|
+
if (/Win/.test(ua)) return "Windows";
|
|
105
|
+
if (/Linux/.test(ua)) return "Linux";
|
|
106
|
+
return "Unknown";
|
|
107
|
+
}
|
|
108
|
+
getBrowser() {
|
|
109
|
+
if (typeof navigator === "undefined") return "Unknown";
|
|
110
|
+
const ua = navigator.userAgent;
|
|
111
|
+
if (ua.indexOf("Chrome") > -1 && ua.indexOf("Safari") > -1 && ua.indexOf("Edg") === -1) return "Chrome";
|
|
112
|
+
if (ua.indexOf("Safari") > -1 && ua.indexOf("Chrome") === -1) return "Safari";
|
|
113
|
+
if (ua.indexOf("Firefox") > -1) return "Firefox";
|
|
114
|
+
if (ua.indexOf("Edg") > -1) return "Edge";
|
|
115
|
+
return "Unknown";
|
|
116
|
+
}
|
|
117
|
+
async report(event, payload) {
|
|
118
|
+
try {
|
|
119
|
+
const fullData = {
|
|
120
|
+
...payload,
|
|
121
|
+
event_name: event,
|
|
122
|
+
device_id: this.deviceId,
|
|
123
|
+
source: payload.source || this.getSource(),
|
|
124
|
+
page_url: payload.page_url || (typeof window !== "undefined" ? window.location.href : ""),
|
|
125
|
+
referrer: payload.referrer || (typeof document !== "undefined" ? document.referrer : ""),
|
|
126
|
+
page_path: payload.page_path || (typeof window !== "undefined" ? window.location.pathname : ""),
|
|
127
|
+
page_title: payload.page_title || (typeof document !== "undefined" ? document.title : ""),
|
|
128
|
+
device_type: payload.device_type || this.getDeviceType(),
|
|
129
|
+
os: payload.os || this.getOS(),
|
|
130
|
+
browser: payload.browser || this.getBrowser(),
|
|
131
|
+
screen_width: payload.screen_width || (typeof window !== "undefined" ? window.screen.width : 0)
|
|
132
|
+
};
|
|
133
|
+
if (this.validate(fullData)) {
|
|
134
|
+
await this.requestFn(fullData);
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error("[BlogTracking] Report failed", e);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/core/SessionManager.ts
|
|
143
|
+
var SessionManager = class {
|
|
144
|
+
constructor(detail) {
|
|
145
|
+
this.startTime = 0;
|
|
146
|
+
this.isEffective = false;
|
|
147
|
+
this.hasReportedEffective = false;
|
|
148
|
+
this.hasReportedEnd = false;
|
|
149
|
+
this.timeTimer = null;
|
|
150
|
+
this.detail = detail;
|
|
151
|
+
this.handleInteraction = this.triggerEffective.bind(this);
|
|
152
|
+
this.handleScroll = () => {
|
|
153
|
+
if (this.isEffective) return;
|
|
154
|
+
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
|
155
|
+
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
|
|
156
|
+
const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
|
|
157
|
+
const scrollPercent = (scrollTop + clientHeight) / scrollHeight;
|
|
158
|
+
if (scrollPercent > 0.3) {
|
|
159
|
+
this.triggerEffective();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
this.handleVisibilityChange = () => {
|
|
163
|
+
if (document.visibilityState === "hidden") {
|
|
164
|
+
this.reportEvent("background");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
this.handlePageHide = () => {
|
|
168
|
+
this.reportEvent("close");
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
start() {
|
|
172
|
+
this.startTime = Date.now();
|
|
173
|
+
this.isEffective = false;
|
|
174
|
+
this.hasReportedEffective = false;
|
|
175
|
+
this.hasReportedEnd = false;
|
|
176
|
+
Tracker.getInstance().report("page_view", {
|
|
177
|
+
page_url: window.location.href,
|
|
178
|
+
page_path: window.location.pathname,
|
|
179
|
+
page_title: this.detail.title,
|
|
180
|
+
page_type: "news_detail",
|
|
181
|
+
page_id: String(this.detail.id)
|
|
182
|
+
});
|
|
183
|
+
this.timeTimer = setTimeout(() => {
|
|
184
|
+
this.triggerEffective();
|
|
185
|
+
}, 15e3);
|
|
186
|
+
window.addEventListener("click", this.handleInteraction);
|
|
187
|
+
window.addEventListener("copy", this.handleInteraction);
|
|
188
|
+
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
189
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
190
|
+
window.addEventListener("pagehide", this.handlePageHide);
|
|
191
|
+
}
|
|
192
|
+
stop(reason = "jump") {
|
|
193
|
+
this.reportEvent(reason);
|
|
194
|
+
this.cleanup();
|
|
195
|
+
}
|
|
196
|
+
cleanup() {
|
|
197
|
+
if (this.timeTimer) {
|
|
198
|
+
clearTimeout(this.timeTimer);
|
|
199
|
+
this.timeTimer = null;
|
|
200
|
+
}
|
|
201
|
+
window.removeEventListener("click", this.handleInteraction);
|
|
202
|
+
window.removeEventListener("copy", this.handleInteraction);
|
|
203
|
+
window.removeEventListener("scroll", this.handleScroll);
|
|
204
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
205
|
+
window.removeEventListener("pagehide", this.handlePageHide);
|
|
206
|
+
}
|
|
207
|
+
triggerEffective() {
|
|
208
|
+
if (!this.isEffective) {
|
|
209
|
+
this.isEffective = true;
|
|
210
|
+
this.reportEvent("effective_trigger");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
reportEvent(reason) {
|
|
214
|
+
if (!this.detail) return;
|
|
215
|
+
if (reason !== "effective_trigger" && this.hasReportedEffective) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (this.hasReportedEnd && reason !== "effective_trigger") return;
|
|
219
|
+
const duration = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
220
|
+
const effectiveStatus = this.isEffective ? 1 : 2;
|
|
221
|
+
const backendReason = reason === "effective_trigger" ? "jump" : reason;
|
|
222
|
+
Tracker.getInstance().report("page_stay_end", {
|
|
223
|
+
page_url: window.location.href,
|
|
224
|
+
page_type: "news_detail",
|
|
225
|
+
page_id: String(this.detail.id),
|
|
226
|
+
stay_duration: duration,
|
|
227
|
+
is_effective: effectiveStatus,
|
|
228
|
+
end_reason: backendReason
|
|
229
|
+
});
|
|
230
|
+
if (reason === "effective_trigger") {
|
|
231
|
+
this.hasReportedEffective = true;
|
|
232
|
+
} else {
|
|
233
|
+
this.hasReportedEnd = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export { SessionManager, Tracker, TrackingSource };
|
|
239
|
+
//# sourceMappingURL=chunk-CC7B5HBV.js.map
|
|
240
|
+
//# sourceMappingURL=chunk-CC7B5HBV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/types.ts","../src/core/Tracker.ts","../src/core/SessionManager.ts"],"names":["TrackingSource"],"mappings":"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","file":"chunk-CC7B5HBV.js","sourcesContent":["export enum TrackingSource {\n Search = 'search',\n Social = 'social',\n Direct = 'direct',\n Referral = 'referral',\n AppBanner = 'app_banner',\n AppTie = 'app_tie',\n Edm = 'edm',\n GuanPhone = 'guan_phone',\n GuanBanner = 'guan_banner',\n AppAd = 'app_ad',\n}\n\nexport interface TrackingPayload {\n event_name: 'page_view' | 'page_stay_end';\n page_url: string;\n page_path?: string;\n page_title?: string;\n page_type: 'news_detail';\n page_id: string;\n referrer?: string;\n source: TrackingSource | string;\n device_type?: 'mobile' | 'desktop';\n os?: string;\n browser?: string;\n screen_width?: number;\n timestamp?: string;\n device_id: string;\n ip?: string;\n \n // page_stay_end specific\n stay_duration?: number;\n is_effective?: 1 | 2; // 1: effective, 2: inefficient\n end_reason?: EndReason;\n}\n\nexport type EndReason = 'close' | 'jump' | 'background';\n\nexport interface TrackingDetail {\n id: number | string;\n title: string;\n [key: string]: any;\n}\n","import { TrackingPayload, TrackingSource } from './types';\n\nexport class Tracker {\n private static instance: Tracker;\n private deviceId: string;\n private deviceStorageKey = 'BLOG_TRACKING_DEVICE_ID';\n\n private requestFn: (data: any) => Promise<any>;\n\n private constructor(requestFn: (data: any) => Promise<any>) {\n this.requestFn = requestFn;\n this.deviceId = this.getOrInitDeviceId();\n }\n\n public static getInstance(requestFn?: (data: any) => Promise<any>): Tracker {\n if (!Tracker.instance) {\n if (!requestFn) {\n throw new Error('Tracker needs requestFn for the first initialization');\n }\n Tracker.instance = new Tracker(requestFn);\n }\n return Tracker.instance;\n }\n\n private getOrInitDeviceId(): string {\n try {\n let id = localStorage.getItem(this.deviceStorageKey);\n if (!id) {\n id = this.generateUUID();\n localStorage.setItem(this.deviceStorageKey, id);\n }\n return id;\n } catch (e) {\n console.warn('[BlogTracking] localStorage unavailable, using temporary ID');\n return this.generateUUID();\n }\n }\n\n private generateUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n const r = Math.random() * 16 | 0;\n const v = c === 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n }\n\n public getSource(): string {\n if (typeof window === 'undefined') return TrackingSource.Direct;\n\n const urlParams = new URLSearchParams(window.location.search);\n const utmSource = urlParams.get('utm_source');\n\n if (utmSource) return utmSource;\n\n const referrer = document.referrer || '';\n const referrerLower = referrer.toLowerCase();\n\n if (referrerLower.includes('google') || referrerLower.includes('bing')) {\n return TrackingSource.Search;\n }\n\n const socialKeywords = ['twitter', 't.co', 'x.com', 'instagram', 'facebook', 'discord', 'reddit', 'tiktok'];\n if (socialKeywords.some(k => referrerLower.includes(k))) {\n return TrackingSource.Social;\n }\n\n if (!referrer) return TrackingSource.Direct;\n\n return TrackingSource.Referral;\n }\n\n private validate(data: Partial<TrackingPayload>): boolean {\n const requiredFields: (keyof TrackingPayload)[] = [\n 'event_name', 'page_url', 'device_id', 'page_type', 'page_id', 'source'\n ];\n\n for (const field of requiredFields) {\n if (!data[field]) {\n console.warn(`[BlogTracking] Missing field: ${field}`, data);\n return false;\n }\n }\n\n if (data.event_name === 'page_stay_end') {\n if (\n data.stay_duration === undefined ||\n data.is_effective === undefined ||\n !data.end_reason\n ) {\n console.warn(`[BlogTracking] page_stay_end missing fields`, data);\n return false;\n }\n }\n\n return true;\n }\n\n private getDeviceType(): 'mobile' | 'desktop' {\n if (typeof navigator === 'undefined') return 'desktop';\n const ua = navigator.userAgent;\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);\n return isMobile ? 'mobile' : 'desktop';\n }\n\n private getOS(): string {\n if (typeof navigator === 'undefined') return 'Unknown';\n const ua = navigator.userAgent;\n if (/iPad|iPhone|iPod/.test(ua)) return 'iOS';\n if (/Android/.test(ua)) return 'Android';\n if (/Mac/.test(ua)) return 'Mac';\n if (/Win/.test(ua)) return 'Windows';\n if (/Linux/.test(ua)) return 'Linux';\n return 'Unknown';\n }\n\n private getBrowser(): string {\n if (typeof navigator === 'undefined') return 'Unknown';\n const ua = navigator.userAgent;\n if (ua.indexOf('Chrome') > -1 && ua.indexOf('Safari') > -1 && ua.indexOf('Edg') === -1) return 'Chrome';\n if (ua.indexOf('Safari') > -1 && ua.indexOf('Chrome') === -1) return 'Safari';\n if (ua.indexOf('Firefox') > -1) return 'Firefox';\n if (ua.indexOf('Edg') > -1) return 'Edge';\n return 'Unknown';\n }\n\n public async report(event: 'page_view' | 'page_stay_end', payload: Partial<TrackingPayload>) {\n try {\n const fullData: Partial<TrackingPayload> = {\n ...payload,\n event_name: event,\n device_id: this.deviceId,\n source: payload.source || this.getSource(),\n page_url: payload.page_url || (typeof window !== 'undefined' ? window.location.href : ''),\n referrer: payload.referrer || (typeof document !== 'undefined' ? document.referrer : ''),\n page_path: payload.page_path || (typeof window !== 'undefined' ? window.location.pathname : ''),\n page_title: payload.page_title || (typeof document !== 'undefined' ? document.title : ''),\n device_type: payload.device_type || this.getDeviceType(),\n os: payload.os || this.getOS(),\n browser: payload.browser || this.getBrowser(),\n screen_width: payload.screen_width || (typeof window !== 'undefined' ? window.screen.width : 0),\n };\n\n if (this.validate(fullData)) {\n await this.requestFn(fullData);\n }\n } catch (e) {\n console.error('[BlogTracking] Report failed', e);\n }\n }\n}\n","import { Tracker } from './Tracker';\nimport { EndReason, TrackingDetail } from './types';\n\nexport class SessionManager {\n private detail: TrackingDetail;\n private startTime: number = 0;\n private isEffective: boolean = false;\n private hasReportedEffective: boolean = false;\n private hasReportedEnd: boolean = false;\n \n private timeTimer: any = null;\n private handleInteraction: () => void;\n private handleScroll: () => void;\n private handleVisibilityChange: () => void;\n private handlePageHide: () => void;\n\n constructor(detail: TrackingDetail) {\n this.detail = detail;\n\n // Bind methods to preserve 'this'\n this.handleInteraction = this.triggerEffective.bind(this);\n \n this.handleScroll = () => {\n if (this.isEffective) return;\n const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;\n const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;\n const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;\n \n const scrollPercent = (scrollTop + clientHeight) / scrollHeight;\n if (scrollPercent > 0.3) {\n this.triggerEffective();\n }\n };\n\n this.handleVisibilityChange = () => {\n if (document.visibilityState === 'hidden') {\n this.reportEvent('background');\n }\n };\n\n this.handlePageHide = () => {\n this.reportEvent('close');\n };\n }\n\n public start() {\n this.startTime = Date.now();\n this.isEffective = false;\n this.hasReportedEffective = false;\n this.hasReportedEnd = false;\n\n // Report Page View\n Tracker.getInstance().report('page_view', {\n page_url: window.location.href,\n page_path: window.location.pathname,\n page_title: this.detail.title,\n page_type: 'news_detail',\n page_id: String(this.detail.id),\n });\n\n // 1. Time trigger (15s)\n this.timeTimer = setTimeout(() => {\n this.triggerEffective();\n }, 15000);\n\n // 2. Interaction listeners\n window.addEventListener('click', this.handleInteraction);\n window.addEventListener('copy', this.handleInteraction);\n window.addEventListener('scroll', this.handleScroll, { passive: true });\n\n // 3. Visibility & PageHide\n document.addEventListener('visibilitychange', this.handleVisibilityChange);\n window.addEventListener('pagehide', this.handlePageHide);\n }\n\n public stop(reason: EndReason = 'jump') {\n this.reportEvent(reason);\n this.cleanup();\n }\n\n private cleanup() {\n if (this.timeTimer) {\n clearTimeout(this.timeTimer);\n this.timeTimer = null;\n }\n window.removeEventListener('click', this.handleInteraction);\n window.removeEventListener('copy', this.handleInteraction);\n window.removeEventListener('scroll', this.handleScroll);\n document.removeEventListener('visibilitychange', this.handleVisibilityChange);\n window.removeEventListener('pagehide', this.handlePageHide);\n }\n\n private triggerEffective() {\n if (!this.isEffective) {\n this.isEffective = true;\n this.reportEvent('effective_trigger');\n }\n }\n\n private reportEvent(reason: EndReason | 'effective_trigger') {\n if (!this.detail) return;\n \n // Avoid duplicates for effective trigger\n if (reason !== 'effective_trigger' && this.hasReportedEffective) {\n // If we already reported effective, we don't block the END report, \n // BUT the original logic says: \"If already reported effective, then subsequent END report is NOT sent?\"\n // Re-reading original logic:\n // \"if (reason !== 'effective_trigger' && hasReportedEffective.current) { return; }\"\n // Wait, this means if effective reported, we NEVER report stay_end?\n // Let's check original useNewsTracking.ts carefully.\n \n // Original: \n // if (reason !== 'effective_trigger' && hasReportedEffective.current) { return; }\n // This implies that if user stayed > 15s (effective), we do NOT send a separate 'close'/'jump' event report with duration?\n // That seems to be the logic in the original file. I will replicate it faithfully.\n return; \n }\n\n if (this.hasReportedEnd && reason !== 'effective_trigger') return;\n\n const duration = Math.floor((Date.now() - this.startTime) / 1000);\n const effectiveStatus = this.isEffective ? 1 : 2;\n const backendReason = reason === 'effective_trigger' ? 'jump' : reason;\n\n Tracker.getInstance().report('page_stay_end', {\n page_url: window.location.href,\n page_type: 'news_detail',\n page_id: String(this.detail.id),\n stay_duration: duration,\n is_effective: effectiveStatus,\n end_reason: backendReason as EndReason\n });\n\n if (reason === 'effective_trigger') {\n this.hasReportedEffective = true;\n } else {\n this.hasReportedEnd = true;\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/types.ts
|
|
4
|
+
var TrackingSource = /* @__PURE__ */ ((TrackingSource2) => {
|
|
5
|
+
TrackingSource2["Search"] = "search";
|
|
6
|
+
TrackingSource2["Social"] = "social";
|
|
7
|
+
TrackingSource2["Direct"] = "direct";
|
|
8
|
+
TrackingSource2["Referral"] = "referral";
|
|
9
|
+
TrackingSource2["AppBanner"] = "app_banner";
|
|
10
|
+
TrackingSource2["AppTie"] = "app_tie";
|
|
11
|
+
TrackingSource2["Edm"] = "edm";
|
|
12
|
+
TrackingSource2["GuanPhone"] = "guan_phone";
|
|
13
|
+
TrackingSource2["GuanBanner"] = "guan_banner";
|
|
14
|
+
TrackingSource2["AppAd"] = "app_ad";
|
|
15
|
+
return TrackingSource2;
|
|
16
|
+
})(TrackingSource || {});
|
|
17
|
+
|
|
18
|
+
// src/core/Tracker.ts
|
|
19
|
+
var Tracker = class _Tracker {
|
|
20
|
+
constructor(requestFn) {
|
|
21
|
+
this.deviceStorageKey = "BLOG_TRACKING_DEVICE_ID";
|
|
22
|
+
this.requestFn = requestFn;
|
|
23
|
+
this.deviceId = this.getOrInitDeviceId();
|
|
24
|
+
}
|
|
25
|
+
static getInstance(requestFn) {
|
|
26
|
+
if (!_Tracker.instance) {
|
|
27
|
+
if (!requestFn) {
|
|
28
|
+
throw new Error("Tracker needs requestFn for the first initialization");
|
|
29
|
+
}
|
|
30
|
+
_Tracker.instance = new _Tracker(requestFn);
|
|
31
|
+
}
|
|
32
|
+
return _Tracker.instance;
|
|
33
|
+
}
|
|
34
|
+
getOrInitDeviceId() {
|
|
35
|
+
try {
|
|
36
|
+
let id = localStorage.getItem(this.deviceStorageKey);
|
|
37
|
+
if (!id) {
|
|
38
|
+
id = this.generateUUID();
|
|
39
|
+
localStorage.setItem(this.deviceStorageKey, id);
|
|
40
|
+
}
|
|
41
|
+
return id;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.warn("[BlogTracking] localStorage unavailable, using temporary ID");
|
|
44
|
+
return this.generateUUID();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
generateUUID() {
|
|
48
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
|
49
|
+
const r = Math.random() * 16 | 0;
|
|
50
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
51
|
+
return v.toString(16);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
getSource() {
|
|
55
|
+
if (typeof window === "undefined") return "direct" /* Direct */;
|
|
56
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
57
|
+
const utmSource = urlParams.get("utm_source");
|
|
58
|
+
if (utmSource) return utmSource;
|
|
59
|
+
const referrer = document.referrer || "";
|
|
60
|
+
const referrerLower = referrer.toLowerCase();
|
|
61
|
+
if (referrerLower.includes("google") || referrerLower.includes("bing")) {
|
|
62
|
+
return "search" /* Search */;
|
|
63
|
+
}
|
|
64
|
+
const socialKeywords = ["twitter", "t.co", "x.com", "instagram", "facebook", "discord", "reddit", "tiktok"];
|
|
65
|
+
if (socialKeywords.some((k) => referrerLower.includes(k))) {
|
|
66
|
+
return "social" /* Social */;
|
|
67
|
+
}
|
|
68
|
+
if (!referrer) return "direct" /* Direct */;
|
|
69
|
+
return "referral" /* Referral */;
|
|
70
|
+
}
|
|
71
|
+
validate(data) {
|
|
72
|
+
const requiredFields = [
|
|
73
|
+
"event_name",
|
|
74
|
+
"page_url",
|
|
75
|
+
"device_id",
|
|
76
|
+
"page_type",
|
|
77
|
+
"page_id",
|
|
78
|
+
"source"
|
|
79
|
+
];
|
|
80
|
+
for (const field of requiredFields) {
|
|
81
|
+
if (!data[field]) {
|
|
82
|
+
console.warn(`[BlogTracking] Missing field: ${field}`, data);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (data.event_name === "page_stay_end") {
|
|
87
|
+
if (data.stay_duration === void 0 || data.is_effective === void 0 || !data.end_reason) {
|
|
88
|
+
console.warn(`[BlogTracking] page_stay_end missing fields`, data);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
getDeviceType() {
|
|
95
|
+
if (typeof navigator === "undefined") return "desktop";
|
|
96
|
+
const ua = navigator.userAgent;
|
|
97
|
+
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
|
98
|
+
return isMobile ? "mobile" : "desktop";
|
|
99
|
+
}
|
|
100
|
+
getOS() {
|
|
101
|
+
if (typeof navigator === "undefined") return "Unknown";
|
|
102
|
+
const ua = navigator.userAgent;
|
|
103
|
+
if (/iPad|iPhone|iPod/.test(ua)) return "iOS";
|
|
104
|
+
if (/Android/.test(ua)) return "Android";
|
|
105
|
+
if (/Mac/.test(ua)) return "Mac";
|
|
106
|
+
if (/Win/.test(ua)) return "Windows";
|
|
107
|
+
if (/Linux/.test(ua)) return "Linux";
|
|
108
|
+
return "Unknown";
|
|
109
|
+
}
|
|
110
|
+
getBrowser() {
|
|
111
|
+
if (typeof navigator === "undefined") return "Unknown";
|
|
112
|
+
const ua = navigator.userAgent;
|
|
113
|
+
if (ua.indexOf("Chrome") > -1 && ua.indexOf("Safari") > -1 && ua.indexOf("Edg") === -1) return "Chrome";
|
|
114
|
+
if (ua.indexOf("Safari") > -1 && ua.indexOf("Chrome") === -1) return "Safari";
|
|
115
|
+
if (ua.indexOf("Firefox") > -1) return "Firefox";
|
|
116
|
+
if (ua.indexOf("Edg") > -1) return "Edge";
|
|
117
|
+
return "Unknown";
|
|
118
|
+
}
|
|
119
|
+
async report(event, payload) {
|
|
120
|
+
try {
|
|
121
|
+
const fullData = {
|
|
122
|
+
...payload,
|
|
123
|
+
event_name: event,
|
|
124
|
+
device_id: this.deviceId,
|
|
125
|
+
source: payload.source || this.getSource(),
|
|
126
|
+
page_url: payload.page_url || (typeof window !== "undefined" ? window.location.href : ""),
|
|
127
|
+
referrer: payload.referrer || (typeof document !== "undefined" ? document.referrer : ""),
|
|
128
|
+
page_path: payload.page_path || (typeof window !== "undefined" ? window.location.pathname : ""),
|
|
129
|
+
page_title: payload.page_title || (typeof document !== "undefined" ? document.title : ""),
|
|
130
|
+
device_type: payload.device_type || this.getDeviceType(),
|
|
131
|
+
os: payload.os || this.getOS(),
|
|
132
|
+
browser: payload.browser || this.getBrowser(),
|
|
133
|
+
screen_width: payload.screen_width || (typeof window !== "undefined" ? window.screen.width : 0)
|
|
134
|
+
};
|
|
135
|
+
if (this.validate(fullData)) {
|
|
136
|
+
await this.requestFn(fullData);
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
console.error("[BlogTracking] Report failed", e);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/core/SessionManager.ts
|
|
145
|
+
var SessionManager = class {
|
|
146
|
+
constructor(detail) {
|
|
147
|
+
this.startTime = 0;
|
|
148
|
+
this.isEffective = false;
|
|
149
|
+
this.hasReportedEffective = false;
|
|
150
|
+
this.hasReportedEnd = false;
|
|
151
|
+
this.timeTimer = null;
|
|
152
|
+
this.detail = detail;
|
|
153
|
+
this.handleInteraction = this.triggerEffective.bind(this);
|
|
154
|
+
this.handleScroll = () => {
|
|
155
|
+
if (this.isEffective) return;
|
|
156
|
+
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
|
157
|
+
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
|
|
158
|
+
const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
|
|
159
|
+
const scrollPercent = (scrollTop + clientHeight) / scrollHeight;
|
|
160
|
+
if (scrollPercent > 0.3) {
|
|
161
|
+
this.triggerEffective();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
this.handleVisibilityChange = () => {
|
|
165
|
+
if (document.visibilityState === "hidden") {
|
|
166
|
+
this.reportEvent("background");
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
this.handlePageHide = () => {
|
|
170
|
+
this.reportEvent("close");
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
start() {
|
|
174
|
+
this.startTime = Date.now();
|
|
175
|
+
this.isEffective = false;
|
|
176
|
+
this.hasReportedEffective = false;
|
|
177
|
+
this.hasReportedEnd = false;
|
|
178
|
+
Tracker.getInstance().report("page_view", {
|
|
179
|
+
page_url: window.location.href,
|
|
180
|
+
page_path: window.location.pathname,
|
|
181
|
+
page_title: this.detail.title,
|
|
182
|
+
page_type: "news_detail",
|
|
183
|
+
page_id: String(this.detail.id)
|
|
184
|
+
});
|
|
185
|
+
this.timeTimer = setTimeout(() => {
|
|
186
|
+
this.triggerEffective();
|
|
187
|
+
}, 15e3);
|
|
188
|
+
window.addEventListener("click", this.handleInteraction);
|
|
189
|
+
window.addEventListener("copy", this.handleInteraction);
|
|
190
|
+
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
191
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
192
|
+
window.addEventListener("pagehide", this.handlePageHide);
|
|
193
|
+
}
|
|
194
|
+
stop(reason = "jump") {
|
|
195
|
+
this.reportEvent(reason);
|
|
196
|
+
this.cleanup();
|
|
197
|
+
}
|
|
198
|
+
cleanup() {
|
|
199
|
+
if (this.timeTimer) {
|
|
200
|
+
clearTimeout(this.timeTimer);
|
|
201
|
+
this.timeTimer = null;
|
|
202
|
+
}
|
|
203
|
+
window.removeEventListener("click", this.handleInteraction);
|
|
204
|
+
window.removeEventListener("copy", this.handleInteraction);
|
|
205
|
+
window.removeEventListener("scroll", this.handleScroll);
|
|
206
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
207
|
+
window.removeEventListener("pagehide", this.handlePageHide);
|
|
208
|
+
}
|
|
209
|
+
triggerEffective() {
|
|
210
|
+
if (!this.isEffective) {
|
|
211
|
+
this.isEffective = true;
|
|
212
|
+
this.reportEvent("effective_trigger");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
reportEvent(reason) {
|
|
216
|
+
if (!this.detail) return;
|
|
217
|
+
if (reason !== "effective_trigger" && this.hasReportedEffective) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (this.hasReportedEnd && reason !== "effective_trigger") return;
|
|
221
|
+
const duration = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
222
|
+
const effectiveStatus = this.isEffective ? 1 : 2;
|
|
223
|
+
const backendReason = reason === "effective_trigger" ? "jump" : reason;
|
|
224
|
+
Tracker.getInstance().report("page_stay_end", {
|
|
225
|
+
page_url: window.location.href,
|
|
226
|
+
page_type: "news_detail",
|
|
227
|
+
page_id: String(this.detail.id),
|
|
228
|
+
stay_duration: duration,
|
|
229
|
+
is_effective: effectiveStatus,
|
|
230
|
+
end_reason: backendReason
|
|
231
|
+
});
|
|
232
|
+
if (reason === "effective_trigger") {
|
|
233
|
+
this.hasReportedEffective = true;
|
|
234
|
+
} else {
|
|
235
|
+
this.hasReportedEnd = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
exports.SessionManager = SessionManager;
|
|
241
|
+
exports.Tracker = Tracker;
|
|
242
|
+
exports.TrackingSource = TrackingSource;
|
|
243
|
+
//# sourceMappingURL=chunk-MLB6KJZI.cjs.map
|
|
244
|
+
//# sourceMappingURL=chunk-MLB6KJZI.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/types.ts","../src/core/Tracker.ts","../src/core/SessionManager.ts"],"names":["TrackingSource"],"mappings":"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","file":"chunk-MLB6KJZI.cjs","sourcesContent":["export enum TrackingSource {\n Search = 'search',\n Social = 'social',\n Direct = 'direct',\n Referral = 'referral',\n AppBanner = 'app_banner',\n AppTie = 'app_tie',\n Edm = 'edm',\n GuanPhone = 'guan_phone',\n GuanBanner = 'guan_banner',\n AppAd = 'app_ad',\n}\n\nexport interface TrackingPayload {\n event_name: 'page_view' | 'page_stay_end';\n page_url: string;\n page_path?: string;\n page_title?: string;\n page_type: 'news_detail';\n page_id: string;\n referrer?: string;\n source: TrackingSource | string;\n device_type?: 'mobile' | 'desktop';\n os?: string;\n browser?: string;\n screen_width?: number;\n timestamp?: string;\n device_id: string;\n ip?: string;\n \n // page_stay_end specific\n stay_duration?: number;\n is_effective?: 1 | 2; // 1: effective, 2: inefficient\n end_reason?: EndReason;\n}\n\nexport type EndReason = 'close' | 'jump' | 'background';\n\nexport interface TrackingDetail {\n id: number | string;\n title: string;\n [key: string]: any;\n}\n","import { TrackingPayload, TrackingSource } from './types';\n\nexport class Tracker {\n private static instance: Tracker;\n private deviceId: string;\n private deviceStorageKey = 'BLOG_TRACKING_DEVICE_ID';\n\n private requestFn: (data: any) => Promise<any>;\n\n private constructor(requestFn: (data: any) => Promise<any>) {\n this.requestFn = requestFn;\n this.deviceId = this.getOrInitDeviceId();\n }\n\n public static getInstance(requestFn?: (data: any) => Promise<any>): Tracker {\n if (!Tracker.instance) {\n if (!requestFn) {\n throw new Error('Tracker needs requestFn for the first initialization');\n }\n Tracker.instance = new Tracker(requestFn);\n }\n return Tracker.instance;\n }\n\n private getOrInitDeviceId(): string {\n try {\n let id = localStorage.getItem(this.deviceStorageKey);\n if (!id) {\n id = this.generateUUID();\n localStorage.setItem(this.deviceStorageKey, id);\n }\n return id;\n } catch (e) {\n console.warn('[BlogTracking] localStorage unavailable, using temporary ID');\n return this.generateUUID();\n }\n }\n\n private generateUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n const r = Math.random() * 16 | 0;\n const v = c === 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n }\n\n public getSource(): string {\n if (typeof window === 'undefined') return TrackingSource.Direct;\n\n const urlParams = new URLSearchParams(window.location.search);\n const utmSource = urlParams.get('utm_source');\n\n if (utmSource) return utmSource;\n\n const referrer = document.referrer || '';\n const referrerLower = referrer.toLowerCase();\n\n if (referrerLower.includes('google') || referrerLower.includes('bing')) {\n return TrackingSource.Search;\n }\n\n const socialKeywords = ['twitter', 't.co', 'x.com', 'instagram', 'facebook', 'discord', 'reddit', 'tiktok'];\n if (socialKeywords.some(k => referrerLower.includes(k))) {\n return TrackingSource.Social;\n }\n\n if (!referrer) return TrackingSource.Direct;\n\n return TrackingSource.Referral;\n }\n\n private validate(data: Partial<TrackingPayload>): boolean {\n const requiredFields: (keyof TrackingPayload)[] = [\n 'event_name', 'page_url', 'device_id', 'page_type', 'page_id', 'source'\n ];\n\n for (const field of requiredFields) {\n if (!data[field]) {\n console.warn(`[BlogTracking] Missing field: ${field}`, data);\n return false;\n }\n }\n\n if (data.event_name === 'page_stay_end') {\n if (\n data.stay_duration === undefined ||\n data.is_effective === undefined ||\n !data.end_reason\n ) {\n console.warn(`[BlogTracking] page_stay_end missing fields`, data);\n return false;\n }\n }\n\n return true;\n }\n\n private getDeviceType(): 'mobile' | 'desktop' {\n if (typeof navigator === 'undefined') return 'desktop';\n const ua = navigator.userAgent;\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);\n return isMobile ? 'mobile' : 'desktop';\n }\n\n private getOS(): string {\n if (typeof navigator === 'undefined') return 'Unknown';\n const ua = navigator.userAgent;\n if (/iPad|iPhone|iPod/.test(ua)) return 'iOS';\n if (/Android/.test(ua)) return 'Android';\n if (/Mac/.test(ua)) return 'Mac';\n if (/Win/.test(ua)) return 'Windows';\n if (/Linux/.test(ua)) return 'Linux';\n return 'Unknown';\n }\n\n private getBrowser(): string {\n if (typeof navigator === 'undefined') return 'Unknown';\n const ua = navigator.userAgent;\n if (ua.indexOf('Chrome') > -1 && ua.indexOf('Safari') > -1 && ua.indexOf('Edg') === -1) return 'Chrome';\n if (ua.indexOf('Safari') > -1 && ua.indexOf('Chrome') === -1) return 'Safari';\n if (ua.indexOf('Firefox') > -1) return 'Firefox';\n if (ua.indexOf('Edg') > -1) return 'Edge';\n return 'Unknown';\n }\n\n public async report(event: 'page_view' | 'page_stay_end', payload: Partial<TrackingPayload>) {\n try {\n const fullData: Partial<TrackingPayload> = {\n ...payload,\n event_name: event,\n device_id: this.deviceId,\n source: payload.source || this.getSource(),\n page_url: payload.page_url || (typeof window !== 'undefined' ? window.location.href : ''),\n referrer: payload.referrer || (typeof document !== 'undefined' ? document.referrer : ''),\n page_path: payload.page_path || (typeof window !== 'undefined' ? window.location.pathname : ''),\n page_title: payload.page_title || (typeof document !== 'undefined' ? document.title : ''),\n device_type: payload.device_type || this.getDeviceType(),\n os: payload.os || this.getOS(),\n browser: payload.browser || this.getBrowser(),\n screen_width: payload.screen_width || (typeof window !== 'undefined' ? window.screen.width : 0),\n };\n\n if (this.validate(fullData)) {\n await this.requestFn(fullData);\n }\n } catch (e) {\n console.error('[BlogTracking] Report failed', e);\n }\n }\n}\n","import { Tracker } from './Tracker';\nimport { EndReason, TrackingDetail } from './types';\n\nexport class SessionManager {\n private detail: TrackingDetail;\n private startTime: number = 0;\n private isEffective: boolean = false;\n private hasReportedEffective: boolean = false;\n private hasReportedEnd: boolean = false;\n \n private timeTimer: any = null;\n private handleInteraction: () => void;\n private handleScroll: () => void;\n private handleVisibilityChange: () => void;\n private handlePageHide: () => void;\n\n constructor(detail: TrackingDetail) {\n this.detail = detail;\n\n // Bind methods to preserve 'this'\n this.handleInteraction = this.triggerEffective.bind(this);\n \n this.handleScroll = () => {\n if (this.isEffective) return;\n const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;\n const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;\n const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;\n \n const scrollPercent = (scrollTop + clientHeight) / scrollHeight;\n if (scrollPercent > 0.3) {\n this.triggerEffective();\n }\n };\n\n this.handleVisibilityChange = () => {\n if (document.visibilityState === 'hidden') {\n this.reportEvent('background');\n }\n };\n\n this.handlePageHide = () => {\n this.reportEvent('close');\n };\n }\n\n public start() {\n this.startTime = Date.now();\n this.isEffective = false;\n this.hasReportedEffective = false;\n this.hasReportedEnd = false;\n\n // Report Page View\n Tracker.getInstance().report('page_view', {\n page_url: window.location.href,\n page_path: window.location.pathname,\n page_title: this.detail.title,\n page_type: 'news_detail',\n page_id: String(this.detail.id),\n });\n\n // 1. Time trigger (15s)\n this.timeTimer = setTimeout(() => {\n this.triggerEffective();\n }, 15000);\n\n // 2. Interaction listeners\n window.addEventListener('click', this.handleInteraction);\n window.addEventListener('copy', this.handleInteraction);\n window.addEventListener('scroll', this.handleScroll, { passive: true });\n\n // 3. Visibility & PageHide\n document.addEventListener('visibilitychange', this.handleVisibilityChange);\n window.addEventListener('pagehide', this.handlePageHide);\n }\n\n public stop(reason: EndReason = 'jump') {\n this.reportEvent(reason);\n this.cleanup();\n }\n\n private cleanup() {\n if (this.timeTimer) {\n clearTimeout(this.timeTimer);\n this.timeTimer = null;\n }\n window.removeEventListener('click', this.handleInteraction);\n window.removeEventListener('copy', this.handleInteraction);\n window.removeEventListener('scroll', this.handleScroll);\n document.removeEventListener('visibilitychange', this.handleVisibilityChange);\n window.removeEventListener('pagehide', this.handlePageHide);\n }\n\n private triggerEffective() {\n if (!this.isEffective) {\n this.isEffective = true;\n this.reportEvent('effective_trigger');\n }\n }\n\n private reportEvent(reason: EndReason | 'effective_trigger') {\n if (!this.detail) return;\n \n // Avoid duplicates for effective trigger\n if (reason !== 'effective_trigger' && this.hasReportedEffective) {\n // If we already reported effective, we don't block the END report, \n // BUT the original logic says: \"If already reported effective, then subsequent END report is NOT sent?\"\n // Re-reading original logic:\n // \"if (reason !== 'effective_trigger' && hasReportedEffective.current) { return; }\"\n // Wait, this means if effective reported, we NEVER report stay_end?\n // Let's check original useNewsTracking.ts carefully.\n \n // Original: \n // if (reason !== 'effective_trigger' && hasReportedEffective.current) { return; }\n // This implies that if user stayed > 15s (effective), we do NOT send a separate 'close'/'jump' event report with duration?\n // That seems to be the logic in the original file. I will replicate it faithfully.\n return; \n }\n\n if (this.hasReportedEnd && reason !== 'effective_trigger') return;\n\n const duration = Math.floor((Date.now() - this.startTime) / 1000);\n const effectiveStatus = this.isEffective ? 1 : 2;\n const backendReason = reason === 'effective_trigger' ? 'jump' : reason;\n\n Tracker.getInstance().report('page_stay_end', {\n page_url: window.location.href,\n page_type: 'news_detail',\n page_id: String(this.detail.id),\n stay_duration: duration,\n is_effective: effectiveStatus,\n end_reason: backendReason as EndReason\n });\n\n if (reason === 'effective_trigger') {\n this.hasReportedEffective = true;\n } else {\n this.hasReportedEnd = true;\n }\n }\n}\n"]}
|