mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(analytics): defer insight views until foregrounded
Only count insight views when the page is visible and focused. Defer background activations until the first foreground transition without resuming cancelled tab dwell timers, and cover focus and visibility ordering in regression tests.
This commit is contained in:
1 parent
b9903e77e6
commit
0e8339bc4c
2 files changed
+76
-3
No files matched your search
@@ -9,7 +9,8 @@ test('only uninterrupted foreground selections count; initial views and cancella
|
||||
})
|
||||
const { useInsightTabAnalytics } = await import('./useInsightTabAnalytics')
|
||||
const browserWindow = new EventTarget()
|
||||
const browserDocument = Object.assign(new EventTarget(), { visibilityState: 'visible', hasFocus: () => true })
|
||||
let focused = true
|
||||
const browserDocument = Object.assign(new EventTarget(), { visibilityState: 'visible', hasFocus: () => focused })
|
||||
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
||||
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document')
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: browserWindow })
|
||||
@@ -83,6 +84,60 @@ test('only uninterrupted foreground selections count; initial views and cancella
|
||||
select('overview')
|
||||
t.mock.timers.tick(3000)
|
||||
assert.equal(visits().length, 2)
|
||||
|
||||
// Loading can finish in a hidden tab or an unfocused window. Neither is a view yet.
|
||||
const views = () => events.filter(({ name }) => name === 'insight_viewed').length
|
||||
for (const [visibility, returnEvent] of [
|
||||
['hidden', 'focus'],
|
||||
['visible', 'focus'],
|
||||
['hidden', 'visibilitychange'],
|
||||
]) {
|
||||
active.value = false
|
||||
focused = false
|
||||
browserDocument.visibilityState = visibility
|
||||
const before = views()
|
||||
active.value = true
|
||||
assert.equal(views(), before)
|
||||
if (returnEvent === 'focus') {
|
||||
browserDocument.visibilityState = 'visible'
|
||||
browserDocument.dispatchEvent(new Event('visibilitychange'))
|
||||
} else {
|
||||
focused = true
|
||||
browserWindow.dispatchEvent(new Event('focus'))
|
||||
}
|
||||
assert.equal(views(), before)
|
||||
focused = true
|
||||
browserDocument.visibilityState = 'visible'
|
||||
if (returnEvent === 'focus') browserWindow.dispatchEvent(new Event('focus'))
|
||||
else browserDocument.dispatchEvent(new Event('visibilitychange'))
|
||||
assert.equal(views(), before + 1)
|
||||
browserWindow.dispatchEvent(new Event('focus'))
|
||||
browserDocument.dispatchEvent(new Event('visibilitychange'))
|
||||
assert.equal(views(), before + 1)
|
||||
t.mock.timers.tick(5000)
|
||||
assert.equal(visits().length, 2)
|
||||
}
|
||||
|
||||
// Returning to the foreground cannot revive a cancelled dwell visit.
|
||||
select('topic')
|
||||
t.mock.timers.tick(2000)
|
||||
focused = false
|
||||
browserWindow.dispatchEvent(new Event('blur'))
|
||||
focused = true
|
||||
browserWindow.dispatchEvent(new Event('focus'))
|
||||
t.mock.timers.tick(5000)
|
||||
assert.equal(visits().length, 2)
|
||||
|
||||
// Leaving before the first foreground view must discard the pending denominator event.
|
||||
active.value = false
|
||||
focused = false
|
||||
active.value = true
|
||||
const beforeLeaving = views()
|
||||
active.value = false
|
||||
focused = true
|
||||
browserWindow.dispatchEvent(new Event('focus'))
|
||||
assert.equal(views(), beforeLeaving)
|
||||
active.value = true
|
||||
select('topic')
|
||||
scope.stop()
|
||||
t.mock.timers.tick(3000)
|
||||
|
||||
@@ -9,6 +9,7 @@ export function useInsightTabAnalytics(options: {
|
||||
routePath: () => string
|
||||
}) {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let pendingView = false
|
||||
|
||||
function cancel() {
|
||||
clearTimeout(timer)
|
||||
@@ -17,6 +18,12 @@ export function useInsightTabAnalytics(options: {
|
||||
|
||||
const isForeground = () => document.visibilityState === 'visible' && document.hasFocus()
|
||||
|
||||
function reportPendingView() {
|
||||
if (!pendingView || !options.isActive() || !options.sessionId.value || !isForeground()) return
|
||||
pendingView = false
|
||||
trackProductEvent('insight_viewed', { chat_type: options.chatType })
|
||||
}
|
||||
|
||||
function selectTab(tabId: string) {
|
||||
cancel()
|
||||
if (!options.isActive() || !isForeground()) return
|
||||
@@ -35,21 +42,32 @@ export function useInsightTabAnalytics(options: {
|
||||
([active, sessionId]) => {
|
||||
cancel()
|
||||
// Separate denominator: old clients without sub-tab tracking must not dilute its adoption rate.
|
||||
if (active && sessionId) trackProductEvent('insight_viewed', { chat_type: options.chatType })
|
||||
pendingView = Boolean(active && sessionId)
|
||||
reportPendingView()
|
||||
},
|
||||
{ immediate: true, flush: 'sync' }
|
||||
)
|
||||
|
||||
function onVisibilityChange() {
|
||||
if (document.visibilityState !== 'visible') cancel()
|
||||
else reportPendingView()
|
||||
}
|
||||
window.addEventListener('blur', cancel)
|
||||
window.addEventListener('focus', reportPendingView)
|
||||
// Cancel as soon as navigation commits, before an outgoing page transition finishes.
|
||||
watch(options.routePath, cancel, { flush: 'sync' })
|
||||
watch(
|
||||
options.routePath,
|
||||
() => {
|
||||
cancel()
|
||||
pendingView = false
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
onScopeDispose(() => {
|
||||
cancel()
|
||||
window.removeEventListener('blur', cancel)
|
||||
window.removeEventListener('focus', reportPendingView)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user