Spaces:
Running
Running
Download frontend/src/lib/track.js from tntgroup/website_content: direct link, hf CLI and curl.
- Browser
- Download file 14.3 kB
-
https://huggingface.co/spaces/tntgroup/website_content/resolve/main/frontend/src/lib/track.js
- Command line
-
hf download hf://spaces/tntgroup/website_content/frontend/src/lib/track.js
-
curl -L -o track.js https://huggingface.co/spaces/tntgroup/website_content/resolve/main/frontend/src/lib/track.js
14.3 kB
| // Ghi log hành vi người dùng (phần "đồng hồ người" — thời gian tường). | |
| // | |
| // Phân vai với backend: ở đây CHỈ ghi thao tác của người. Thời gian máy chạy do | |
| // backend tự ghi trong JobManager (job_queued/job_started/job_done) vì chỉ máy | |
| // chủ biết job rời hàng đợi lúc nào — frontend không bắn job_done, tránh đếm đôi. | |
| // | |
| // Bốn tầng định danh: user_id > session_id > task_id > step. | |
| // session_id: một lần dùng app; im lặng quá 30' thì mở phiên mới. | |
| // task_id : một lần làm ra một sản phẩm. Mở khi mở tool, đóng khi có đầu ra | |
| // hoặc bỏ dở. Chạy lại/regenerate KHÔNG mở task mới, chỉ attempt+1. | |
| // | |
| // Log gom thành lô, gửi mỗi 5 giây; lúc đóng tab dùng sendBeacon để không mất | |
| // dòng feature_abandon. | |
| const ENDPOINT = "/api/events"; | |
| const FLUSH_MS = 5000; | |
| const MAX_BATCH = 20; | |
| const IDLE_MS = 30 * 60 * 1000; // ngưỡng im lặng -> hết phiên (spec §6) | |
| const STEPS = ["open", "prepare", "run", "review", "output", "done"]; | |
| // Đường dẫn -> khoá tính năng (feature) dùng trong log. | |
| export const ROUTE_FEATURE = { | |
| "/": "home", | |
| "/down-list": "down_list", | |
| "/cut": "cut", | |
| "/xao": "shuffle", | |
| "/dub": "dub", | |
| "/chen-chu": "text_video", | |
| "/listing-anh": "listing_image", | |
| "/listing-free": "listing_free", | |
| "/flux2": "flux2", | |
| "/tao-video": "create_video", | |
| "/huong-dan": "guide", | |
| "/ho-tro": "support", | |
| "/quan-tri": "admin", | |
| "/lich-su": "history", | |
| }; | |
| // Trang chỉ để XEM, không làm ra sản phẩm. Ghi `page_view` KHÔNG kèm task_id: | |
| // mở trang chủ/hướng dẫn mà tính là một "task 0% hoàn thành" thì bảng theo tool | |
| // và tỷ lệ A3 bị pha loãng bằng thứ không phải công việc. | |
| const PASSIVE = new Set(["home", "guide", "support", "admin", "history"]); | |
| const rid = () => Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4); | |
| const nowIso = () => { | |
| const d = new Date(); | |
| const off = -d.getTimezoneOffset(); | |
| const sign = off >= 0 ? "+" : "-"; | |
| const p = (n, w = 2) => String(Math.abs(n)).padStart(w, "0"); | |
| return ( | |
| `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` + | |
| `T${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}` + | |
| `${sign}${p(off / 60 | 0)}:${p(off % 60)}` | |
| ); | |
| }; | |
| const ls = { | |
| get(k) { try { return localStorage.getItem(k); } catch { return null; } }, | |
| set(k, v) { try { localStorage.setItem(k, v); } catch { /* chế độ riêng tư */ } }, | |
| }; | |
| const ss = { | |
| get(k) { try { return sessionStorage.getItem(k); } catch { return null; } }, | |
| set(k, v) { try { sessionStorage.setItem(k, v); } catch { /* noop */ } }, | |
| del(k) { try { sessionStorage.removeItem(k); } catch { /* noop */ } }, | |
| }; | |
| // ── Trạng thái phiên / task ───────────────────────────────────── | |
| let queue = []; | |
| let timer = null; | |
| let session = ""; | |
| let task = null; // { id, feature, startedAt, lastStep, closed, attempt, phase } | |
| let started = false; | |
| let lastOpen = { feature: "", ts: 0 }; // chặn mở trùng (xem featureOpen) | |
| function touch() { | |
| ls.set("tnt_track_last", String(Date.now())); | |
| } | |
| function sessionId() { | |
| if (session) return session; | |
| const last = Number(ls.get("tnt_track_last") || 0); | |
| const prev = ls.get("tnt_track_sess") || ""; | |
| if (prev && last && Date.now() - last < IDLE_MS) { | |
| session = prev; // quay lại trong 30' -> vẫn phiên cũ | |
| } else { | |
| session = mkId("s"); | |
| ls.set("tnt_track_sess", session); | |
| started = false; | |
| } | |
| return session; | |
| } | |
| function userId() { | |
| // Ưu tiên mã U00N (do backend cấp); chưa có thì lùi về tên đăng nhập. | |
| return (ls.get("tnt_uid") || ls.get("tnt_user") || "").trim().toLowerCase(); | |
| } | |
| // Mảnh định danh an toàn cho ID (bỏ dấu cách/ký tự lạ, cắt ngắn). | |
| function idSafe(x) { | |
| return (x || "").toLowerCase().replace(/[^a-z0-9]+/g, "").slice(0, 12) || "anon"; | |
| } | |
| // task_id / session_id KIỂU ĐỌC ĐƯỢC, tự sắp theo thời gian, không cần bộ đếm: | |
| // <prefix>_<user>_<YYYYMMDD>_<HHMMSS>_<rand4> | |
| // Dùng giờ+random thay số thứ tự trong ngày -> không đụng nhau khi mở 2 tab/máy. | |
| function mkId(prefix) { | |
| const d = new Date(); | |
| const p = (n, w = 2) => String(n).padStart(w, "0"); | |
| const date = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}`; | |
| const time = `${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`; | |
| const rnd = Math.random().toString(36).slice(2, 6); | |
| return `${prefix}_${idSafe(userId())}_${date}_${time}_${rnd}`; | |
| } | |
| // ── Hàng đợi gửi ──────────────────────────────────────────────── | |
| function push(row) { | |
| const user = userId(); | |
| if (!user) return; // chưa đăng nhập -> không có gì để quy về ai | |
| queue.push({ event_id: "evt_" + rid(), event_time: nowIso(), user_id: user, ...row }); | |
| touch(); | |
| if (queue.length >= MAX_BATCH) flush(); | |
| else if (!timer) timer = setTimeout(flush, FLUSH_MS); | |
| } | |
| export function flush(useBeacon = false) { | |
| if (timer) { clearTimeout(timer); timer = null; } | |
| if (!queue.length) return; | |
| const body = JSON.stringify({ events: queue }); | |
| queue = []; | |
| try { | |
| if (useBeacon && navigator.sendBeacon) { | |
| navigator.sendBeacon(ENDPOINT, new Blob([body], { type: "application/json" })); | |
| return; | |
| } | |
| fetch(ENDPOINT, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body, | |
| keepalive: true, | |
| }).catch(() => {}); | |
| } catch { /* mất log còn hơn vỡ app */ } | |
| } | |
| // ── Ghi event ─────────────────────────────────────────────────── | |
| function emit(event_name, step, props = {}, event_value = "") { | |
| push({ | |
| session_id: sessionId(), | |
| task_id: task ? task.id : "", | |
| feature: task ? task.feature : "", | |
| step: STEPS.includes(step) ? step : "prepare", | |
| event_name, | |
| event_value: event_value == null ? "" : String(event_value), | |
| properties: props, | |
| }); | |
| if (task && STEPS.includes(step) && step !== "done") task.lastStep = step; | |
| } | |
| /** Mở app / đăng nhập xong. Gọi một lần khi vào Layout. */ | |
| export function sessionStart(props = {}) { | |
| sessionId(); | |
| if (started) return; | |
| started = true; | |
| // Cờ `started` nằm trong bộ nhớ nên F5 là mất, trong khi session_id vẫn giữ | |
| // trong localStorage -> không chốt lại thì mỗi lần tải lại trang đẻ thêm một | |
| // dòng session_start cho cùng một phiên. | |
| if (ls.get("tnt_track_started") === session) return; | |
| ls.set("tnt_track_started", session); | |
| push({ | |
| session_id: session, task_id: "", feature: "", step: "open", | |
| event_name: "session_start", event_value: "", | |
| properties: { client: "web", ...props }, | |
| }); | |
| } | |
| /** Mở một tool -> đóng task cũ (nếu dở) và mở task mới. */ | |
| export function featureOpen(feature, props = {}) { | |
| if (!feature) return; | |
| // Chống mở TRÙNG: React.StrictMode (dev) chạy effect hai lần, và một lần | |
| // re-render lạc cũng gọi lại. Không chặn thì mỗi lần mở tool đẻ thêm một task | |
| // rỗng kèm feature_abandon dài 1ms -> tỷ lệ "làm xong" tụt oan. | |
| if (Date.now() - lastOpen.ts < 2000 && lastOpen.feature === feature) return; | |
| lastOpen = { feature, ts: Date.now() }; | |
| abandonIfOpen(); | |
| if (PASSIVE.has(feature)) { | |
| task = null; // trang xem: không mở task | |
| push({ | |
| session_id: sessionId(), task_id: "", feature, step: "open", | |
| event_name: "page_view", event_value: "", properties: props, | |
| }); | |
| return; | |
| } | |
| task = { | |
| id: mkId(idSafe(feature)), feature, startedAt: Date.now(), | |
| lastStep: "open", closed: false, attempt: 0, phase: "", | |
| }; | |
| // source/parent_task: task mới sinh ra từ đâu (rerun/forward/history). | |
| // Forward: tool nguồn đã gửi kèm "cha" qua sessionStorage trước khi chuyển | |
| // trang -> nhặt lại ở đây cho task của tool đích. | |
| let extra = props; | |
| try { | |
| const fwd = JSON.parse(ss.get("tnt_fwd") || "null"); | |
| if (fwd && Date.now() - fwd.ts < 60000 && | |
| (!fwd.to || fwd.to.includes(feature)) && !props.source) { | |
| extra = { source: "forward", parent_task: fwd.parent, ...props }; | |
| } | |
| ss.del("tnt_fwd"); | |
| } catch { /* noop */ } | |
| emit("feature_open", "open", extra); | |
| } | |
| /** Một thao tác thường của người (dán link, tải file lên, sửa lời...). */ | |
| export function step(event_name, stepName = "prepare", props = {}, value = "") { | |
| if (!task) return; | |
| // Task đã có đầu ra mà người dùng làm tiếp -> đó là sản phẩm mới -> task mới. | |
| if (task.closed) featureOpen(task.feature, { from: "continue" }); | |
| emit(event_name, stepName, props, value); | |
| } | |
| /** Người bấm nút chạy máy. Tăng attempt (A4) — backend đọc qua header. */ | |
| export function runClick(props = {}) { | |
| if (!task) return; | |
| if (task.closed) featureOpen(task.feature, { from: "continue" }); | |
| task.attempt += 1; | |
| // Nhớ tên pha máy sắp chạy (asr_translate / tts / burn / cut…) và gửi kèm | |
| // header, để dòng job_done do backend ghi biết mình là bước nào -> chỉ số B2 | |
| // tách được tốc độ từng bước thay vì gộp một cục. | |
| if (props.phase) task.phase = props.phase; | |
| emit("run_click", "run", { attempt: task.attempt, ...props }); | |
| return task.attempt; | |
| } | |
| /** Làm lại (Tạo lại / Yêu cầu sửa) — cùng task, attempt+1. */ | |
| export function retry(event_name = "regenerate", props = {}) { | |
| if (!task) return; | |
| task.attempt += 1; | |
| emit(event_name, "run", { attempt: task.attempt, ...props }); | |
| } | |
| /** Có sản phẩm: tải về hoặc chuyển tiếp sang tool khác. Cả hai đều là XONG. */ | |
| export function output(kind = "file", props = {}, forwardTo = "") { | |
| if (!task) return; | |
| const isForward = !!forwardTo; | |
| if (isForward) { | |
| ss.set("tnt_fwd", JSON.stringify({ parent: task.id, to: forwardTo, ts: Date.now() })); | |
| } | |
| emit( | |
| isForward ? "output_forward" : "output_download", | |
| "output", | |
| { success_type: isForward ? "forward" : "download", ...(isForward ? { to: forwardTo } : {}), ...props }, | |
| isForward ? forwardTo : kind, | |
| ); | |
| if (!task.closed) { | |
| emit("feature_complete", "done", { | |
| success_type: isForward ? "forward" : "download", | |
| dur_ms: Date.now() - task.startedAt, | |
| attempts: task.attempt, | |
| }); | |
| task.closed = true; | |
| } | |
| } | |
| /** Nút 👍/👎 ở chỗ hiện kết quả (chỉ số B3). */ | |
| export function thumbs(value, props = {}) { | |
| if (!task) return; | |
| emit("thumbs_feedback", task.closed ? "output" : "review", props, value === "up" ? "up" : "down"); | |
| } | |
| /** Nút "Làm mới / Bắt đầu lại" -> đóng task hiện tại, mở task mới. */ | |
| export function restart(props = {}) { | |
| if (!task) return; | |
| const f = task.feature; | |
| const parent = task.id; // task đang đóng lại chính là "cha" | |
| featureOpen(f, { source: "rerun", parent_task: parent, ...props }); | |
| } | |
| function abandonIfOpen(extra = {}) { | |
| if (!task || task.closed) return; | |
| emit("feature_abandon", task.lastStep || "open", { | |
| last_step: task.lastStep || "open", | |
| dur_ms: Date.now() - task.startedAt, | |
| attempts: task.attempt, | |
| ...extra, | |
| }); | |
| task.closed = true; | |
| } | |
| /** Bỏ dở có lý do rõ (vd thiếu API key). */ | |
| export function abandon(reason = "", lastStep = "") { | |
| if (!task || task.closed) return; | |
| if (lastStep) task.lastStep = lastStep; | |
| abandonIfOpen(reason ? { reason } : {}); | |
| } | |
| /** Ngữ cảnh hiện tại — dùng để gắn header X-TNT-* vào request API. */ | |
| export function currentCtx() { | |
| return { | |
| user_id: userId(), | |
| session_id: session || sessionId(), | |
| task_id: task ? task.id : "", | |
| feature: task ? task.feature : "", | |
| phase: task ? task.phase || "" : "", | |
| attempt: task ? task.attempt : 0, | |
| }; | |
| } | |
| // ── Gắn ngữ cảnh vào MỌI request /api/ ────────────────────────── | |
| // | |
| // Vá `fetch` một lần ở đây thay vì sửa ~15 chỗ gọi API: backend đọc header | |
| // X-TNT-* trong middleware rồi tự gắn vào job, nên dòng log "máy chạy" biết | |
| // thuộc task nào của ai mà không phải đổi model request nào cả. | |
| let patched = false; | |
| function patchFetch() { | |
| if (patched || typeof window === "undefined") return; | |
| patched = true; | |
| const orig = window.fetch.bind(window); | |
| window.fetch = (input, init = {}) => { | |
| try { | |
| const url = typeof input === "string" ? input : (input && input.url) || ""; | |
| const isApi = url.startsWith("/api/") || url.includes("/api/"); | |
| if (isApi && !url.includes("/api/events")) { | |
| const c = currentCtx(); | |
| if (c.user_id) { | |
| const h = new Headers((init && init.headers) || (input instanceof Request ? input.headers : undefined)); | |
| h.set("X-TNT-User", c.user_id); | |
| h.set("X-TNT-Session", c.session_id); | |
| if (c.task_id) h.set("X-TNT-Task", c.task_id); | |
| if (c.feature) h.set("X-TNT-Feature", c.feature); | |
| if (c.phase) h.set("X-TNT-Phase", c.phase); | |
| if (c.attempt) h.set("X-TNT-Attempt", String(c.attempt)); | |
| return orig(input, { ...init, headers: h }); | |
| } | |
| } | |
| } catch { /* vá lỗi thì gọi như cũ */ } | |
| return orig(input, init); | |
| }; | |
| } | |
| let inited = false; | |
| /** Gọi một lần lúc app khởi động (trong Layout). */ | |
| export function initTracking() { | |
| if (inited || typeof window === "undefined") return; | |
| inited = true; | |
| patchFetch(); | |
| sessionStart(); | |
| const bye = () => { abandonIfOpen({ reason: "close_tab" }); flush(true); }; | |
| window.addEventListener("pagehide", bye); | |
| window.addEventListener("visibilitychange", () => { | |
| if (document.visibilityState === "hidden") flush(true); | |
| }); | |
| setInterval(() => flush(), FLUSH_MS * 3); | |
| } | |
| export default { | |
| initTracking, sessionStart, featureOpen, step, runClick, retry, | |
| output, thumbs, restart, abandon, flush, currentCtx, ROUTE_FEATURE, | |
| }; | |