AbdulElahGwaith's picture
Upload folder using huggingface_hub
88df9e4 verified
import Cookies from '@/frame/components/lib/cookies'
import { parseUserAgent } from './user-agent'
import { Router } from 'next/router'
import { isLoggedIn } from '@/frame/components/hooks/useHasAccount'
import { getExperimentVariationForContext } from './experiments/experiment'
import { EventType, EventPropsByType } from '../types'
import { isHeadless } from './is-headless'
const COOKIE_NAME = '_docs-events'
const startVisitTime = Date.now()
const BATCH_INTERVAL = 5000 // 5 seconds
let initialized = false
let cookieValue: string | undefined
let pageEventId: string | undefined
let sentExit = false
let pauseScrolling = false
let scrollPosition = 0
let scrollDirection = 1
let scrollFlipCount = 0
let maxScrollY = 0
let previousPath: string | undefined
let hoveredUrls = new Set()
let eventQueue: any[] = []
function scheduleNextFlush() {
setTimeout(() => {
flushQueue()
scheduleNextFlush()
}, BATCH_INTERVAL)
}
scheduleNextFlush()
function resetPageParams() {
sentExit = false
pauseScrolling = false
scrollPosition = 0
scrollDirection = 1
scrollFlipCount = 0
maxScrollY = 0
// Don't reset previousPath
hoveredUrls = new Set()
}
// Temporary polyfill for crypto.randomUUID()
// Necessary for localhost development (doesn't have https://)
export function uuidv4(): string {
try {
return crypto.randomUUID()
} catch {
// https://stackoverflow.com/a/2117523
return (<any>[1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c: number) =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16),
)
}
}
export function getUserEventsId() {
if (cookieValue) return cookieValue
cookieValue = Cookies.get(COOKIE_NAME)
if (cookieValue) return cookieValue
cookieValue = uuidv4()
Cookies.set(COOKIE_NAME, cookieValue)
return cookieValue
}
function getMetaContent(name: string) {
const metaTag = document.querySelector(`meta[name="${name}"]`) as HTMLMetaElement
return metaTag?.content
}
export function sendEvent<T extends EventType>({
type,
version = '1.0.0',
eventGroupKey,
eventGroupId,
...props
}: {
type: T
version?: string
eventGroupKey?: string
eventGroupId?: string
} & EventPropsByType[T]) {
const body = {
type,
context: {
// Primitives
event_id: uuidv4(),
user: getUserEventsId(),
version,
created: new Date().toISOString(),
page_event_id: pageEventId,
// Content information
referrer: getReferrer(document.referrer),
title: document.title,
href: location.href, // full URL
hostname: location.hostname, // origin without protocol or port
path: location.pathname, // path without search or host
search: location.search, // also known as query string
hash: location.hash, // also known as anchor
path_language: getMetaContent('path-language'),
path_version: getMetaContent('path-version'),
path_product: getMetaContent('path-product'),
path_article: getMetaContent('path-article'),
page_document_type: getMetaContent('page-document-type'),
page_type: getMetaContent('page-type'),
content_type: getMetaContent('page-content-type'),
status: Number(getMetaContent('status') || 0),
is_logged_in: isLoggedIn(),
// Device information
// os, os_version, browser, browser_version:
...parseUserAgent(),
is_headless: isHeadless(),
viewport_width: document.documentElement.clientWidth,
viewport_height: document.documentElement.clientHeight,
screen_width: window.screen.width,
screen_height: window.screen.height,
pixel_ratio: window.devicePixelRatio || 1,
user_agent: navigator.userAgent,
// Location information
timezone: new Date().getTimezoneOffset() / -60,
user_language: navigator.language,
// Preference information
application_preference: Cookies.get('toolPreferred'),
color_mode_preference: getColorModePreference(),
os_preference: Cookies.get('osPreferred'),
code_display_preference: Cookies.get('annotate-mode'),
experiment_variation:
getExperimentVariationForContext(
getMetaContent('path-language'),
getMetaContent('path-version'),
) || '',
// Event grouping
event_group_key: eventGroupKey,
event_group_id: eventGroupId,
},
...props,
}
queueEvent(body)
if (type === EventType.exit) {
flushQueue()
}
return body
}
function flushQueue() {
if (!eventQueue.length) return
const endpoint = '/api/events'
const eventsBody = JSON.stringify(eventQueue)
eventQueue = []
try {
navigator.sendBeacon(endpoint, new Blob([eventsBody], { type: 'application/json' }))
} catch (err) {
console.warn(`sendBeacon to '${endpoint}' failed.`, err)
}
}
function queueEvent(eventBody: unknown) {
eventQueue.push(eventBody)
}
// Sometimes using the back button means the internal referrer path is not there,
// So this fills it in with a JavaScript variable
function getReferrer(documentReferrer: string) {
if (!previousPath) return documentReferrer
try {
// new URL() throws an error if not a valid URL
const referrerUrl = new URL(documentReferrer)
if (!referrerUrl.pathname || referrerUrl.pathname === '/') {
return location.origin + previousPath
}
} catch {}
return documentReferrer
}
function getColorModePreference() {
// color mode is set as attributes on <html>, we'll use that information
// along with media query checking rather than parsing the cookie value
// set by github.com
let color_mode_preference = document.querySelector('html')?.dataset.colorMode
if (color_mode_preference === 'auto') {
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
color_mode_preference += ':light'
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
color_mode_preference += ':dark'
}
}
return color_mode_preference
}
function getPerformance() {
const paint = performance
?.getEntriesByType('paint')
?.find(({ name }) => name === 'first-contentful-paint')
const nav = performance?.getEntriesByType('navigation')?.[0] as
| PerformanceNavigationTiming
| undefined
return {
firstContentfulPaint: paint ? paint.startTime / 1000 : undefined,
domInteractive: nav ? nav.domInteractive / 1000 : undefined,
domComplete: nav ? nav.domComplete / 1000 : undefined,
render: nav ? (nav.responseEnd - nav.requestStart) / 1000 : undefined,
}
}
function trackScroll() {
// Throttle the calculations to no more than five per second
if (pauseScrolling) return
pauseScrolling = true
setTimeout(() => {
pauseScrolling = false
}, 200)
// Calculate where we are on the page
const scrollPixels = window.scrollY + window.innerHeight
const newScrollPosition = scrollPixels / document.documentElement.scrollHeight
// Count scroll flips
const newScrollDirection = Math.sign(newScrollPosition - scrollPosition)
if (newScrollDirection !== scrollDirection) scrollFlipCount++
// Update maximum scroll position reached
if (newScrollPosition > maxScrollY) maxScrollY = newScrollPosition
// Update before the next event
scrollDirection = newScrollDirection
scrollPosition = newScrollPosition
}
function sendPage() {
const pageEvent = sendEvent({ type: EventType.page })
pageEventId = pageEvent?.context?.event_id
}
function sendExit() {
if (sentExit) return
sentExit = true
const { render, firstContentfulPaint, domInteractive, domComplete } = getPerformance()
const clampedScrollLength = Math.min(maxScrollY, 1)
return sendEvent({
type: EventType.exit,
exit_render_duration: render,
exit_first_paint: firstContentfulPaint,
exit_dom_interactive: domInteractive,
exit_dom_complete: domComplete,
exit_visit_duration: (Date.now() - startVisitTime) / 1000,
exit_scroll_length: clampedScrollLength,
exit_scroll_flip: scrollFlipCount,
})
}
function initPageAndExitEvent() {
sendPage() // Initial page hit
// Regular page exits
window.addEventListener('scroll', trackScroll)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
sendExit()
} else {
flushQueue()
}
})
// Client-side routing
Router.events.on('routeChangeStart', async (url) => {
// Don't trigger page events on query string or hash changes
previousPath = location.pathname // pathname set to "prior" url, arg "upcoming" url
const newPath = url?.toString().split('?')[0].split('#')[0]
const shouldSendEvents = newPath !== previousPath
if (shouldSendEvents) {
sendExit()
await waitForPageReady()
resetPageParams()
sendPage()
}
})
}
// We want to wait for the DOM to mutate the <meta> tags
// as well as finish routeChangeComplete (location.pathname)
// before sending the page event in order to get accurate data
async function waitForPageReady() {
const route = new Promise((resolve) => {
const handler = () => {
Router.events.off('routeChangeComplete', handler)
setTimeout(() => resolve(true))
}
Router.events.on('routeChangeComplete', handler)
})
const mutate = new Promise((resolve) => {
const observer = new MutationObserver((mutations) => {
const metaMutated = mutations.find(
(mutation) =>
mutation.target?.nodeName === 'META' ||
Array.from(mutation.addedNodes).find((node) => node.nodeName === 'META') ||
Array.from(mutation.removedNodes).find((node) => node.nodeName === 'META'),
)
if (metaMutated) {
observer.disconnect()
setTimeout(() => resolve(true))
}
})
observer.observe(document.getElementsByTagName('head')[0], {
subtree: true,
childList: true,
attributes: true,
})
})
return Promise.all([route, mutate])
}
function initClipboardEvent() {
for (const verb of ['copy', 'cut', 'paste']) {
document.documentElement.addEventListener(verb, () => {
sendEvent({ type: EventType.clipboard, clipboard_operation: verb })
})
}
}
function initCopyButtonEvent() {
document.documentElement.addEventListener('click', (evt) => {
const target = evt.target as HTMLElement
const button = target.closest('.js-btn-copy') as HTMLButtonElement
if (!button) return
sendEvent({
type: EventType.clipboard,
clipboard_operation: 'copy',
clipboard_target: '.js-btn-copy',
})
})
}
function initLinkEvent() {
document.documentElement.addEventListener('click', (evt) => {
const target = evt.target as HTMLElement
const link = target.closest('a[href]') as HTMLAnchorElement
if (!link) return
const sameSite = link.origin === location.origin
const container = target.closest(`[data-container]`) as HTMLElement | null
// We can attach `data-group-key` and `data-group-id` to any anchor element to include them in the event
const eventGroupKey = link?.dataset?.groupKey || undefined
const eventGroupId = link?.dataset?.groupId || undefined
sendEvent({
type: EventType.link,
link_url: link.href,
link_samesite: sameSite,
link_samepage: sameSite && link.pathname === location.pathname,
link_container: container?.dataset.container,
eventGroupKey,
eventGroupId,
})
})
// Add tracking for scroll to top button
document.documentElement.addEventListener('click', (evt) => {
const target = evt.target as HTMLElement
if (!target.closest('.ghd-scroll-to-top')) return
const url = window.location.href.split('#')[0] // Remove hash
sendEvent({
type: EventType.link,
link_url: `${url}#scroll-to-top`,
link_samesite: true,
link_samepage: true,
link_container: 'static',
})
})
}
function initHoverEvent() {
let timer: number | null = null
document.documentElement.addEventListener('mouseover', (evt) => {
const target = evt.target as HTMLElement
const link = target.closest('a[href]') as HTMLAnchorElement | null
if (!link) return
// For hover events, we only want to record them for links inside the
// content area.
const mainContent = document.querySelector('#main-content') as HTMLElement | null
if (!mainContent || !mainContent.contains(link)) return
if (hoveredUrls.has(link.href)) return // Otherwise this is a flood of events
if (timer) {
window.clearTimeout(timer)
}
timer = window.setTimeout(() => {
const sameSite = link.origin === location.origin
hoveredUrls.add(link.href)
sendEvent({
type: EventType.hover,
hover_url: link.href,
hover_samesite: sameSite,
})
}, 500)
})
// Doesn't matter which link you hovered on that triggered a timer,
// you're clearly not hovering over it anymore.
document.documentElement.addEventListener('mouseout', () => {
if (timer) {
window.clearTimeout(timer)
}
})
}
function initPrintEvent() {
window.addEventListener('beforeprint', () => {
sendEvent({ type: EventType.print })
})
}
export function initializeEvents() {
if (initialized) return
initialized = true
initPageAndExitEvent() // must come first
initLinkEvent()
initHoverEvent()
initClipboardEvent()
initCopyButtonEvent()
initPrintEvent()
}