Skip to content
Draft

` #1

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
956 changes: 956 additions & 0 deletions devtools/__chromium_devtools_metrics_reporter.js

Large diffs are not rendered by default.

8,146 changes: 8,146 additions & 0 deletions github/3.css

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions quickeditdevprodcloudflare.js

Large diffs are not rendered by default.

429 changes: 429 additions & 0 deletions w3fixer.js

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions web-vitals/base/metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export abstract class BaseMetric<AttributionType, EntryType> {
value: number
entries: EntryType[]

constructor(value: number, entries: EntryType[]) {
this.value = value
this.entries = entries
}

abstract get attribution(): AttributionType
}
66 changes: 66 additions & 0 deletions web-vitals/base/observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {ssrSafeDocument} from '@github-ui/ssr-utils'
import type {BaseProcessor} from './processor'
import type {BaseMetric} from './metric'

type ObserverCallback<MetricType> = (metric: MetricType, opts: {url?: string}) => void

/*
* The CLSObserver is responsible for listening to Performance events and routing them to the entryProcessor.
* It also manages resetting CLS and reporting it when navigating or hiding a page.
*/
export abstract class BaseObserver<MetricType extends BaseMetric<unknown, unknown>, EntryType> {
cb: ObserverCallback<MetricType>
entryProcessor: BaseProcessor<MetricType, EntryType>
observer?: PerformanceObserver
url?: string

constructor(cb: ObserverCallback<MetricType>) {
this.cb = cb
this.entryProcessor = this.initializeProcessor()
this.setupListeners()
}

abstract initializeProcessor(): BaseProcessor<MetricType, EntryType>
abstract get supported(): boolean
abstract get softNavEventToListen(): string

setupListeners() {
if (!this.supported) return

const onHiddenOrPageHide = (event: Event) => {
if (event.type === 'pagehide' || document.visibilityState === 'hidden') {
this.report()
}
}

// Similar to web-vitals, we report the current CLS when hard navigating or
// when the page is hidden
ssrSafeDocument?.addEventListener('visibilitychange', onHiddenOrPageHide, true)
ssrSafeDocument?.addEventListener('pagehide', onHiddenOrPageHide, true)

ssrSafeDocument?.addEventListener(this.softNavEventToListen, () => {
this.report()
this.reset()
})
}

abstract observe(initialLoad: boolean): void

report() {
if (!this.entryProcessor.metric || this.entryProcessor.metric.value < 0) return

this.cb(this.entryProcessor.metric, {url: this.url})
}

teardown() {
this.observer?.takeRecords()
this.observer?.disconnect()
}

reset() {
this.teardown()
this.entryProcessor.teardown()
this.entryProcessor = this.initializeProcessor()
this.observe(false)
}
}
6 changes: 6 additions & 0 deletions web-vitals/base/processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export abstract class BaseProcessor<MetricType, EntryType> {
abstract processEntries(entries: EntryType[]): void
abstract get metric(): MetricType | null

teardown() {}
}
65 changes: 65 additions & 0 deletions web-vitals/cls/layout-shift-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {BaseProcessor} from '../base/processor'
import {getSelector} from '../get-selector'
import {CLSMetric, getLargestLayoutShiftSource} from './metric'

// From https://github.com/GoogleChrome/web-vitals/blob/1b872cf5f2159e8ace0e98d55d8eb54fb09adfbe/src/lib/LayoutShiftManager.ts#L17
// with a few modifications to fit our needs.
export class LayoutShiftProcessor extends BaseProcessor<CLSMetric, LayoutShift> {
sessionValue = 0
sessionEntries: LayoutShift[] = []
layoutShiftTargetMap: Map<LayoutShiftAttribution, string> = new Map()

get metric() {
// Pages without entries report CLS = 0
if (this.sessionEntries.length === 0) {
return new CLSMetric(0, [], new Map())
}

return new CLSMetric(this.sessionValue, this.sessionEntries, this.layoutShiftTargetMap)
}

processEntries(entries: LayoutShift[]) {
for (const entry of entries) {
this.processEntry(entry)
}
}

processEntry(entry: LayoutShift) {
// Only count layout shifts without recent user input.
if (entry.hadRecentInput) return

const firstSessionEntry = this.sessionEntries[0]
const lastSessionEntry = this.sessionEntries.at(-1)

// If the entry occurred less than 1 second after the previous entry
// and less than 5 seconds after the first entry in the session,
// include the entry in the current session. Otherwise, start a new
// session.
if (
this.sessionValue &&
firstSessionEntry &&
lastSessionEntry &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
this.sessionValue += entry.value
this.sessionEntries.push(entry)
} else {
this.sessionValue = entry.value
this.sessionEntries = [entry]
}

this.setLargestShiftSource(entry)
}

setLargestShiftSource(entry: LayoutShift) {
if (entry?.sources?.length) {
const largestSource = getLargestLayoutShiftSource(entry.sources)
const node = largestSource?.node
if (node) {
const customTarget = getSelector(node)
this.layoutShiftTargetMap.set(largestSource, customTarget)
}
}
}
}
40 changes: 40 additions & 0 deletions web-vitals/cls/metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {BaseMetric} from '../base/metric'

export interface CLSAttribution {
largestShiftTarget?: string
}

const getLargestLayoutShiftEntry = (entries: LayoutShift[]) => {
return entries.reduce((a, b) => (a.value > b.value ? a : b))
}

export const getLargestLayoutShiftSource = (sources: LayoutShiftAttribution[]) => {
return sources.find(s => s.node?.nodeType === 1) || sources[0]
}

/*
* The CLS metric. This class is compatible with web-vitals' CLSMetric interface that we expect to report to DataDog and Hydro.
*/
export class CLSMetric extends BaseMetric<CLSAttribution, LayoutShift> {
name = 'CLS' as const
targetMap: Map<LayoutShiftAttribution, string>

constructor(value: number, entries: LayoutShift[], targetMap: Map<LayoutShiftAttribution, string>) {
super(value, entries)
this.targetMap = targetMap
}

get attribution(): CLSAttribution {
if (!this.entries.length) return {}

const largestEntry = getLargestLayoutShiftEntry(this.entries)
if (!largestEntry?.sources?.length) return {}

const largestSource = getLargestLayoutShiftSource(largestEntry.sources)
if (!largestSource) return {}

return {
largestShiftTarget: this.targetMap.get(largestSource),
}
}
}
34 changes: 34 additions & 0 deletions web-vitals/cls/observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {ssrSafeWindow} from '@github-ui/ssr-utils'
import type {CLSMetric} from './metric'
import {LayoutShiftProcessor} from './layout-shift-processor'
import {BaseObserver} from '../base/observer'
import {SOFT_NAV_STATE} from '@github-ui/soft-nav/states'

const supportsCLS = ssrSafeWindow && 'LayoutShift' in ssrSafeWindow

/*
* The CLSObserver is responsible for listening to Performance events and routing them to the entryProcessor.
* It also manages resetting CLS and reporting it when navigating or hiding a page.
*/
export class CLSObserver extends BaseObserver<CLSMetric, LayoutShift> {
get softNavEventToListen() {
return SOFT_NAV_STATE.START
}

initializeProcessor() {
return new LayoutShiftProcessor()
}

override get supported(): boolean {
return !!supportsCLS
}

observe(initialLoad = true) {
this.url = ssrSafeWindow?.location.href
this.observer = new PerformanceObserver(list => {
this.entryProcessor.processEntries(list.getEntries() as LayoutShift[])
})

this.observer.observe({type: 'layout-shift', buffered: initialLoad})
}
}
21 changes: 21 additions & 0 deletions web-vitals/dom-nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {isFeatureEnabled} from '@github-ui/feature-flags'
import {SOFT_NAV_STATE} from '@github-ui/soft-nav/states'
import {ssrSafeDocument} from '@github-ui/ssr-utils'

let previousDomNodeCount: number = 0

ssrSafeDocument?.addEventListener(SOFT_NAV_STATE.START, () => {
if (!isFeatureEnabled('dom_node_counts')) return
previousDomNodeCount = countNodes() // nodes may have changes with user interactions / deferred renders
})

function countNodes() {
return ssrSafeDocument?.getElementsByTagName('*').length || 0
}

export function getDomNodes() {
return {
previous: previousDomNodeCount,
current: countNodes(),
}
}
20 changes: 20 additions & 0 deletions web-vitals/element-timing/metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {getSelector} from '../get-selector'

export class ElementTimingMetric {
name = 'ElementTiming' as const
value: number
identifier: string
attribution: {
target?: string
}

declare app: string

constructor(value: number, element: Element, identifier: string) {
this.value = value
this.identifier = identifier
this.attribution = {
target: getSelector(element),
}
}
}
69 changes: 69 additions & 0 deletions web-vitals/element-timing/observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {SOFT_NAV_STATE} from '@github-ui/soft-nav/states'
import {ssrSafeDocument, ssrSafeWindow} from '@github-ui/ssr-utils'
import {ElementTimingMetric} from './metric'

const supportsElementTiming = ssrSafeWindow && 'PerformanceElementTiming' in ssrSafeWindow

type ElementTimingTCallback = (elementTiming: ElementTimingMetric, opts: {url?: string}) => void

interface PerformanceElementTiming extends PerformanceEntry {
renderTime: number
observer?: PerformanceObserver
element: Element
identifier: string
}
/*
* The ElementTimingObserver is responsible for listening to PerformanceElementTiming events and reporting them.
*/
export class ElementTimingObserver {
cb: ElementTimingTCallback
observer?: PerformanceObserver
url?: string

constructor(cb: ElementTimingTCallback) {
this.cb = cb
this.setupListeners()
}

setupListeners() {
if (!supportsElementTiming) return

// SOFT_NAV_STATE.RENDER is dispatched when the soft navigation finished rendering.
// That means that the previous page is fully hidden so we can stop listening for its events.
ssrSafeDocument?.addEventListener(SOFT_NAV_STATE.RENDER, () => {
this.reset()
})
}

observe(initialLoad = true) {
if (!supportsElementTiming) return

this.observer = new PerformanceObserver(list => {
const entries = list.getEntries() as PerformanceElementTiming[]
for (const {renderTime, element, identifier} of entries) {
this.report(new ElementTimingMetric(renderTime, element, identifier))
}
})

this.observer.observe({
type: 'element',
// buffered events are important on first page load since we may have missed
// a few until the observer was set up.
buffered: initialLoad,
})
}

report(metric: ElementTimingMetric) {
this.cb(metric, {url: this.url})
}

teardown() {
this.observer?.takeRecords()
this.observer?.disconnect()
}

reset() {
this.teardown()
this.observe(false)
}
}
32 changes: 32 additions & 0 deletions web-vitals/get-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* From https://github.com/GoogleChrome/web-vitals/blob/7b44bea0d5ba6629c5fd34c3a09cc683077871d0/src/lib/getSelector.ts
* I want to make sure we get element names the same way as web-vitals does.
*/

const getName = (node: Node) => {
const name = node.nodeName
return node.nodeType === 1 ? name.toLowerCase() : name.toUpperCase().replace(/^#/, '')
}

export const getSelector = (node: Node | null | undefined, maxLen?: number) => {
let sel = ''

try {
while (node && node.nodeType !== 9) {
const el: Element = node as Element
const part = el.id
? `#${el.id}`
: getName(el) +
(el.classList && el.classList.value && el.classList.value.trim() && el.classList.value.trim().length
? `.${el.classList.value.trim().replace(/\s+/g, '.')}`
: '')
if (sel.length + part.length > (maxLen || 100) - 1) return sel || part
sel = sel ? `${part}>${sel}` : part
if (el.id) break
node = el.parentNode
}
} catch {
// Do nothing...
}
return sel
}
Loading