зеркало из https://github.com/github/docs.git
234 строки
5.7 KiB
JavaScript
234 строки
5.7 KiB
JavaScript
/* eslint-disable camelcase */
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
import Cookies from 'js-cookie'
|
|
import getCsrf from './get-csrf'
|
|
import parseUserAgent from './user-agent'
|
|
|
|
const COOKIE_NAME = '_docs-events'
|
|
|
|
const startVisitTime = Date.now()
|
|
|
|
let cookieValue
|
|
let pageEventId
|
|
let maxScrollY = 0
|
|
let pauseScrolling = false
|
|
let sentExit = false
|
|
|
|
export function getUserEventsId () {
|
|
if (cookieValue) return cookieValue
|
|
cookieValue = Cookies.get(COOKIE_NAME)
|
|
if (cookieValue) return cookieValue
|
|
cookieValue = uuidv4()
|
|
Cookies.set(COOKIE_NAME, cookieValue, {
|
|
secure: true,
|
|
sameSite: 'strict',
|
|
expires: 365
|
|
})
|
|
return cookieValue
|
|
}
|
|
|
|
export function sendEvent ({
|
|
type,
|
|
version = '1.0.0',
|
|
exit_render_duration,
|
|
exit_first_paint,
|
|
exit_dom_interactive,
|
|
exit_dom_complete,
|
|
exit_visit_duration,
|
|
exit_scroll_length,
|
|
link_url,
|
|
search_query,
|
|
search_context,
|
|
navigate_label,
|
|
survey_vote,
|
|
survey_comment,
|
|
survey_email,
|
|
experiment_name,
|
|
experiment_variation,
|
|
experiment_success,
|
|
clipboard_operation
|
|
}) {
|
|
const body = {
|
|
_csrf: getCsrf(),
|
|
|
|
type, // One of page, exit, link, search, navigate, survey, experiment
|
|
|
|
context: {
|
|
// Primitives
|
|
event_id: uuidv4(),
|
|
user: getUserEventsId(),
|
|
version,
|
|
created: new Date().toISOString(),
|
|
page_event_id: pageEventId,
|
|
|
|
// Content information
|
|
path: location.pathname,
|
|
hostname: location.hostname,
|
|
referrer: document.referrer,
|
|
search: location.search,
|
|
href: location.href,
|
|
site_language: location.pathname.split('/')[1],
|
|
|
|
// Device information
|
|
// os, os_version, browser, browser_version:
|
|
...parseUserAgent(),
|
|
viewport_width: document.documentElement.clientWidth,
|
|
viewport_height: document.documentElement.clientHeight,
|
|
|
|
// Location information
|
|
timezone: new Date().getTimezoneOffset() / -60,
|
|
user_language: navigator.language
|
|
},
|
|
|
|
// Page event
|
|
// No extra fields
|
|
|
|
// Exit event
|
|
exit_render_duration,
|
|
exit_first_paint,
|
|
exit_dom_interactive,
|
|
exit_dom_complete,
|
|
exit_visit_duration,
|
|
exit_scroll_length,
|
|
|
|
// Link event
|
|
link_url,
|
|
|
|
// Search event
|
|
search_query,
|
|
search_context,
|
|
|
|
// Navigate event
|
|
navigate_label,
|
|
|
|
// Survey event
|
|
survey_vote,
|
|
survey_comment,
|
|
survey_email,
|
|
|
|
// Experiment event
|
|
experiment_name,
|
|
experiment_variation,
|
|
experiment_success,
|
|
|
|
// Clipboard event
|
|
clipboard_operation
|
|
}
|
|
const blob = new Blob([JSON.stringify(body)], { type: 'application/json' })
|
|
navigator.sendBeacon('/events', blob)
|
|
return body
|
|
}
|
|
|
|
function getPerformance () {
|
|
const paint = performance?.getEntriesByType('paint')?.find(
|
|
({ name }) => name === 'first-contentful-paint'
|
|
)
|
|
const nav = performance?.getEntriesByType('navigation')?.[0]
|
|
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)
|
|
|
|
// Update maximum scroll position reached
|
|
const scrollPosition = (
|
|
(window.scrollY + window.innerHeight) /
|
|
document.documentElement.scrollHeight
|
|
)
|
|
if (scrollPosition > maxScrollY) maxScrollY = scrollPosition
|
|
}
|
|
|
|
function sendExit () {
|
|
if (sentExit) return
|
|
if (document.visibilityState !== 'hidden') return
|
|
sentExit = true
|
|
const {
|
|
render,
|
|
firstContentfulPaint,
|
|
domInteractive,
|
|
domComplete
|
|
} = getPerformance()
|
|
return sendEvent({
|
|
type: '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: maxScrollY
|
|
})
|
|
}
|
|
|
|
function initPageEvent () {
|
|
const pageEvent = sendEvent({ type: 'page' })
|
|
pageEventId = pageEvent?.context?.event_id
|
|
}
|
|
|
|
function initClipboardEvent () {
|
|
['copy', 'cut', 'paste'].forEach(verb => {
|
|
document.documentElement.addEventListener(verb, () => {
|
|
sendEvent({ type: 'clipboard', clipboard_operation: verb })
|
|
})
|
|
})
|
|
}
|
|
|
|
function initLinkEvent () {
|
|
document.documentElement.addEventListener('click', evt => {
|
|
const link = evt.target.closest('a[href^="http"]')
|
|
if (!link) return
|
|
sendEvent({
|
|
type: 'link',
|
|
link_url: link.href
|
|
})
|
|
})
|
|
}
|
|
|
|
function initExitEvent () {
|
|
window.addEventListener('scroll', trackScroll)
|
|
document.addEventListener('visibilitychange', sendExit)
|
|
}
|
|
|
|
function initNavigateEvent () {
|
|
if (!document.querySelector('.sidebar-products')) return
|
|
|
|
Array.from(
|
|
document.querySelectorAll('.sidebar-products details')
|
|
).forEach(details => details.addEventListener(
|
|
'toggle',
|
|
evt => sendEvent({
|
|
type: 'navigate',
|
|
navigate_label: `details ${evt.target.open ? 'open' : 'close'}: ${evt.target.querySelector('summary').innerText}`
|
|
})
|
|
))
|
|
|
|
document.querySelector('.sidebar-products').addEventListener('click', evt => {
|
|
const link = evt.target.closest('a')
|
|
if (!link) return
|
|
sendEvent({
|
|
type: 'navigate',
|
|
navigate_label: `link: ${link.href}`
|
|
})
|
|
})
|
|
}
|
|
|
|
export default function initializeEvents () {
|
|
initPageEvent() // must come first
|
|
initExitEvent()
|
|
initLinkEvent()
|
|
initClipboardEvent()
|
|
initNavigateEvent()
|
|
// print event in ./print.js
|
|
// survey event in ./helpfulness.js
|
|
// experiment event in ./experiment.js
|
|
// search event in ./search.js
|
|
// redirect event in middleware/record-redirect.js
|
|
}
|