From 90778d4ab32543212c93d9ccd4ba8971e5a33041 Mon Sep 17 00:00:00 2001 From: angelawu236 Date: Fri, 6 Mar 2026 23:50:59 -0800 Subject: [PATCH 1/2] add time tracking for component --- app/api/timeTracker.ts | 83 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 app/api/timeTracker.ts diff --git a/app/api/timeTracker.ts b/app/api/timeTracker.ts new file mode 100644 index 0000000..fb6ad98 --- /dev/null +++ b/app/api/timeTracker.ts @@ -0,0 +1,83 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +export function useTimeTracker(sectionId: string) { + const ref = useRef(null); + const startTime = useRef(null); + + useEffect(() => { + if (!ref.current) return; + + const sendTime = async (durationMs: number, startedAt: number, endedAt: number) => { + const event = { + event_type: "component_view", + payload: { + sectionId, + durationMs, + }, + }; + + console.log("timetracking sending:", event); + + try { + const res = await fetch("/api/track", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(event), + keepalive: true, + }); + + const data = await res.json(); + console.log("response:", data); + } catch (err) { + console.error("error:", err); + } + }; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (startTime.current === null) { + startTime.current = Date.now(); + console.log("feature entered viewport:", sectionId); + } + } else { + if (startTime.current !== null) { + const endedAt = Date.now(); + const startedAt = startTime.current; + const durationMs = endedAt - startedAt; + + console.log("feature left viewport:", sectionId, durationMs); + + void sendTime(durationMs, startedAt, endedAt); + startTime.current = null; + } + } + }); + }, + { threshold: 0 } + ); + + observer.observe(ref.current); + + //when component unmound + return () => { + if (startTime.current !== null) { + const endedAt = Date.now(); + const startedAt = startTime.current; + const durationMs = endedAt - startedAt; + + console.log("[TimeTracker] unmount send:", sectionId, durationMs); + void sendTime(durationMs, startedAt, endedAt); + + startTime.current = null; + } + + observer.disconnect(); + }; + }, [sectionId]); + + return ref; +} \ No newline at end of file From 457d9c2edb79348cadca6317cecd639926ae110c Mon Sep 17 00:00:00 2001 From: angelawu236 Date: Fri, 6 Mar 2026 23:57:17 -0800 Subject: [PATCH 2/2] simplify data sent and only send when feature is gone from viewport --- app/api/timeTracker.ts | 86 ++++++++++++------------------------------ 1 file changed, 25 insertions(+), 61 deletions(-) diff --git a/app/api/timeTracker.ts b/app/api/timeTracker.ts index fb6ad98..e65ae98 100644 --- a/app/api/timeTracker.ts +++ b/app/api/timeTracker.ts @@ -9,72 +9,36 @@ export function useTimeTracker(sectionId: string) { useEffect(() => { if (!ref.current) return; - const sendTime = async (durationMs: number, startedAt: number, endedAt: number) => { - const event = { - event_type: "component_view", - payload: { - sectionId, - durationMs, - }, - }; - - console.log("timetracking sending:", event); - - try { - const res = await fetch("/api/track", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(event), - keepalive: true, - }); - - const data = await res.json(); - console.log("response:", data); - } catch (err) { - console.error("error:", err); + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting) { + if (startTime.current === null) { + startTime.current = Date.now(); + } + } else { + if (startTime.current !== null) { + const durationMs = Date.now() - startTime.current; + + void fetch("/api/track", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + event_type: "component_view", + payload: { + sectionId, + durationMs, + }, + }), + keepalive: true, + }); + + startTime.current = null; + } } - }; - - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - if (startTime.current === null) { - startTime.current = Date.now(); - console.log("feature entered viewport:", sectionId); - } - } else { - if (startTime.current !== null) { - const endedAt = Date.now(); - const startedAt = startTime.current; - const durationMs = endedAt - startedAt; - - console.log("feature left viewport:", sectionId, durationMs); - - void sendTime(durationMs, startedAt, endedAt); - startTime.current = null; - } - } - }); - }, - { threshold: 0 } - ); + }, { threshold: 0 }); //start when feature first appears observer.observe(ref.current); - //when component unmound return () => { - if (startTime.current !== null) { - const endedAt = Date.now(); - const startedAt = startTime.current; - const durationMs = endedAt - startedAt; - - console.log("[TimeTracker] unmount send:", sectionId, durationMs); - void sendTime(durationMs, startedAt, endedAt); - - startTime.current = null; - } - observer.disconnect(); }; }, [sectionId]);