Глубокое понимание принципа фронтенд-мониторинга

внешний интерфейс монитор

Фронтенд-мониторинг делится на мониторинг производительности и мониторинг ошибок. Мониторинг разделен на две части: сбор данных и отчетность по данным. В этой статье в основном рассказывается о том, как собирать данные и сообщать о них.

сбор информации

сбор данных о производительности

Сбор данных о производительности должен использоватьwindow.performanceAPI.

Интерфейс производительности может получать информацию о производительности на текущей странице.Он является частью API времени высокого разрешения, а также интегрирует API временной шкалы производительности, API синхронизации навигации, API синхронизации пользователей и API синхронизации ресурсов.

Как видно из документации MDN,window.performance.timingСодержит время начала и окончания каждого этапа загрузки страницы.

Эти свойства необходимо рассматривать вместе со следующим рисунком для лучшего понимания:

在这里插入图片描述

Для простоты пониманияtimingЗначение каждого атрибута я нашел в сети на ZhihutimingПисьменное введение воспроизводится здесь.

timing: {
        // 同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和fetchStart相同。
	navigationStart: 1543806782096,

	// 上一个页面unload事件抛出时的时间戳。如果没有上一个页面,这个值会返回0。
	unloadEventStart: 1543806782523,

	// 和 unloadEventStart 相对应,unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。
	unloadEventEnd: 1543806782523,

	// 第一个HTTP重定向开始时的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0。
	redirectStart: 0,

	// 最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的时间戳。
	// 如果没有重定向,或者重定向中的一个不同源,这个值会返回0. 
	redirectEnd: 0,

	// 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。
	fetchStart: 1543806782096,

	// DNS 域名查询开始的UNIX时间戳。
        //如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。
	domainLookupStart: 1543806782096,

	// DNS 域名查询完成的时间.
	//如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
	domainLookupEnd: 1543806782096,

	// HTTP(TCP) 域名查询结束的时间戳。
        //如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。
	connectStart: 1543806782099,

	// HTTP(TCP) 返回浏览器与服务器之间的连接建立时的时间戳。
        // 如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。
	connectEnd: 1543806782227,

	// HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。
	secureConnectionStart: 1543806782162,

	// 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。
	requestStart: 1543806782241,

	// 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。
        //如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。
	responseStart: 1543806782516,

	// 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时
        //(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。
	responseEnd: 1543806782537,

	// 当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。
	domLoading: 1543806782573,

	// 当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。
	domInteractive: 1543806783203,

	// 当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。
	domContentLoadedEventStart: 1543806783203,

	// 当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。
	domContentLoadedEventEnd: 1543806783216,

	// 当前文档解析完成,即Document.readyState 变为 'complete'且相对应的readystatechange 被触发时的时间戳
	domComplete: 1543806783796,

	// load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。
	loadEventStart: 1543806783796,

	// 当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0.
	loadEventEnd: 1543806783802
}

С помощью приведенных выше данных мы можем получить несколько полезных времен

// 重定向耗时
redirect: timing.redirectEnd - timing.redirectStart,
// DOM 渲染耗时
dom: timing.domComplete - timing.domLoading,
// 页面加载耗时
load: timing.loadEventEnd - timing.navigationStart,
// 页面卸载耗时
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 请求耗时
request: timing.responseEnd - timing.requestStart,
// 获取性能信息时当前时间
time: new Date().getTime(),

Еще одно важное время –время белого экрана, что означает время с момента ввода URL-адреса до момента, когда страница начинает отображать содержимое.

Поместите следующий скрипт в</head>Время белого экрана можно получить спереди.

<script>
    whiteScreen = new Date() - performance.timing.navigationStart
    // 通过 domLoading 和 navigationStart 也可以
    whiteScreen = performance.timing.domLoading - performance.timing.navigationStart
</script>

Через это несколько раз вы можете узнать, какова производительность загрузки первого экрана страницы.

Кроме того, поwindow.performance.getEntriesByType('resource')В этом методе мы также можем получить время загрузки связанных ресурсов (js, css, img...), и он вернет все ресурсы, загруженные в данный момент на странице.

在这里插入图片描述

Как правило, он включает следующие виды:

  • sciprt
  • link
  • img
  • css
  • fetch
  • other
  • xmlhttprequest

Нам нужна только следующая информация:

// 资源的名称
name: item.name,
// 资源加载耗时
duration: item.duration.toFixed(2),
// 资源大小
size: item.transferSize,
// 资源所用协议
protocol: item.nextHopProtocol,

Теперь напишите несколько строк кода для сбора этих данных.

// 收集性能信息
const getPerformance = () => {
    if (!window.performance) return
    const timing = window.performance.timing
    const performance = {
        // 重定向耗时
        redirect: timing.redirectEnd - timing.redirectStart,
        // 白屏时间
        whiteScreen: whiteScreen,
        // DOM 渲染耗时
        dom: timing.domComplete - timing.domLoading,
        // 页面加载耗时
        load: timing.loadEventEnd - timing.navigationStart,
        // 页面卸载耗时
        unload: timing.unloadEventEnd - timing.unloadEventStart,
        // 请求耗时
        request: timing.responseEnd - timing.requestStart,
        // 获取性能信息时当前时间
        time: new Date().getTime(),
    }

    return performance
}

// 获取资源信息
const getResources = () => {
    if (!window.performance) return
    const data = window.performance.getEntriesByType('resource')
    const resource = {
        xmlhttprequest: [],
        css: [],
        other: [],
        script: [],
        img: [],
        link: [],
        fetch: [],
        // 获取资源信息时当前时间
        time: new Date().getTime(),
    }

    data.forEach(item => {
        const arry = resource[item.initiatorType]
        arry && arry.push({
            // 资源的名称
            name: item.name,
            // 资源加载耗时
            duration: item.duration.toFixed(2),
            // 资源大小
            size: item.transferSize,
            // 资源所用协议
            protocol: item.nextHopProtocol,
        })
    })

    return resource
}

резюме

Интерпретируя информацию о производительности и ресурсах, мы можем судить о том, что загрузка страницы происходит медленно по следующим причинам:

  1. Слишком много ресурсов
  2. Скорость интернета слишком низкая
  3. Слишком много элементов DOM

Помимо того, что скорость сети у пользователя слишком низкая, у нас нет возможности решить две другие причины.В Интернете есть много статей и книг по оптимизации производительности.Если вам интересно, вы можете найти информацию самостоятельно.

PS: На самом деле есть и другие причины медленной загрузки страниц, такие как неиспользование загрузки по требованию, неиспользование CDN и т. д. Однако здесь мы подчеркиваем, что причина получается только через интерпретацию информации о производительности и ресурсах.

Сбор данных об ошибках

В настоящее время существует три типа ошибок, которые можно отловить:

  1. Ошибка загрузки ресурса, черезaddEventListener('error', callback, true)Перехватывайте ошибки сбоя загрузки ресурсов на этапе захвата.
  2. js ошибка выполнения, пройтиwindow.onerrorОтловить js-ошибки.
  3. обещание ошибка, пройтиaddEventListener('unhandledrejection', callback)Перехватывать ошибки обещаний, но нет такой информации, как количество строк и столбцов, в которых произошли ошибки, и вручную можно выдать только релевантную информацию об ошибках.

Мы можем создать переменную массива ошибокerrorsПри возникновении ошибки добавьте соответствующую информацию об ошибке в массив, а затем единообразно сообщите об этом на определенном этапе.Подробности см. в следующем коде:

// 捕获资源加载失败错误 js css img...
addEventListener('error', e => {
    const target = e.target
    if (target != window) {
        monitor.errors.push({
            type: target.localName,
            url: target.src || target.href,
            msg: (target.src || target.href) + ' is load error',
            // 错误发生的时间
            time: new Date().getTime(),
        })
    }
}, true)

// 监听 js 错误
window.onerror = function(msg, url, row, col, error) {
    monitor.errors.push({
        type: 'javascript',
        row: row,
        col: col,
        msg: error && error.stack? error.stack : msg,
        url: url,
        // 错误发生的时间
        time: new Date().getTime(),
    })
}

// 监听 promise 错误 缺点是获取不到行数数据
addEventListener('unhandledrejection', e => {
    monitor.errors.push({
        type: 'promise',
        msg: (e.reason && e.reason.msg) || e.reason || '',
        // 错误发生的时间
        time: new Date().getTime(),
    })
})

резюме

Благодаря сбору ошибок вы можете понять тип и количество ошибок, возникающих на веб-сайте, чтобы внести соответствующие коррективы, чтобы уменьшить количество ошибок. Полный код и DEMO будут опубликованы в конце статьи, вы можете скопировать код (HTML-файл), чтобы протестировать его локально.

отчетность данных

Отчет о производительности

Данные о производительности можно сообщать после загрузки страницы и стараться не влиять на производительность страницы.

window.onload = () => {
    // 在浏览器空闲时间获取性能及资源信息
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
    if (window.requestIdleCallback) {
        window.requestIdleCallback(() => {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        })
    } else {
        setTimeout(() => {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        }, 0)
    }
}

Конечно, вы также можете установить таймер и циклически сообщать об этом. Однако лучше всего выполнять сравнение и дедупликацию каждый раз, когда вы создаете отчет, чтобы избежать повторного представления одних и тех же данных.

Отчет об ошибках

Код, который я предоставил в DEMO, используетerrorsМассив собирает все ошибки, а затем единообразно сообщает о них на определенном этапе (отложенная отчетность).

На самом деле, его также можно изменить, чтобы сообщать при возникновении ошибки (мгновенный отчет). Это может избежать проблемы «собраны ошибки, но отложенный отчет не запущен, но пользователь закрыл веб-страницу, что привело к потере данных об ошибках».

// 监听 js 错误
window.onerror = function(msg, url, row, col, error) {
    const data = {
        type: 'javascript',
        row: row,
        col: col,
        msg: error && error.stack? error.stack : msg,
        url: url,
        // 错误发生的时间
        time: new Date().getTime(),
    }
    
    // 即时上报
    axios.post({ url: 'xxx', data, })
}

Получив напоминание от пользователей сети, вы можете использоватьnavigator.sendBeacon()сообщить.

window.addEventListener('unload', logData, false);

function logData() {
    navigator.sendBeacon("/log", analyticsData);
}

Его технические особенности:

Использование метода sendBeacon() позволяет пользовательскому агенту (браузеру) асинхронно отправлять данные на сервер, когда у него есть возможность, не задерживая выгрузку страницы и не влияя на производительность загрузки следующей навигации. Это решает все проблемы с отправкой данных аналитики: данные достоверны, передача асинхронна и не влияет на загрузку следующей страницы.

расширять

SPA

window.performanceAPI недоработан, когда SPA переключает маршруты,window.performance.timingданные не будут обновлены. Поэтому нам нужно найти другой способ подсчета времени от переключения маршрутов до завершения загрузки. Возьмем в качестве примера Vue. Возможным способом является защита глобального фронта маршрута при переключении маршрутов.beforeEachПолучите время начала в компонентеmountedВыполнить в хукеvm.$nextTick для получения времени завершения рендеринга компонента.

router.beforeEach((to, from, next) => {
	store.commit('setPageLoadedStartTime', new Date())
})
mounted() {
	this.$nextTick(() => {
		this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
	})
}

В дополнение к мониторингу производительности и ошибок мы можем собирать больше информации.

Сбор информации о пользователе

navigator

использоватьwindow.navigatorИнформация об устройстве пользователя, операционной системе, информация о браузере может быть собрана...

УФ (уникальный посетитель)

Относится к посетителям, которые просматривают эту веб-страницу через Интернет, и посещение одного и того же устройства в период с 00:00 до 24:00 засчитывается только один раз. Только один UV рассчитывается для нескольких посещений одним и тем же посетителем в один день.

Когда пользователь посещает веб-сайт, случайная строка + время и дата могут быть сгенерированы и сохранены локально. Когда запрашивается веб-страница (если она превышает 24 часа в сутки, она будет создана заново), передайте эти параметры серверной части, и серверная часть использует эту информацию для создания отчета статистики UV.

PV (просмотр страницы)

То есть количество просмотров страниц или кликов, каждый раз, когда пользователь посещает каждую страницу веб-сайта, записывается 1 PV. Пользователь посещает одну и ту же страницу несколько раз, и совокупное количество посещений используется для измерения количества веб-страниц, посещенных пользователями веб-сайта.

время на странице

традиционный сайт

Когда пользователь входит на страницу A, время, когда пользователь входит на страницу, передается через фоновый запрос. Через 10 минут пользователь заходит на страницу B. В это время фон может определить, что пользователь оставался на странице A в течение 10 минут по параметрам, включенным в интерфейс.

SPA

Вы можете использовать маршрутизатор, чтобы получить время пребывания пользователя, например, Vue, черезrouter.beforeEach,destroyedЭти две функции хука определяют время пребывания пользователя в компоненте маршрутизации.

Глубина просмотра

пройти черезdocument.documentElement.scrollTopАтрибуты и высота экрана могут определить, закончил ли пользователь просмотр содержимого веб-сайта.

источник перехода на страницу

пройти черезdocument.referrerатрибут, вы можете узнать, с какого веб-сайта перешел пользователь.

резюме

Анализируя пользовательские данные, мы можем узнать о привычках пользователя в Интернете, его увлечениях и другой информации, об этом страшно подумать, а приватности нет вообще.

DEMO

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script>
        function monitorInit() {
            const monitor = {
                // 数据上传地址
                url: '',
                // 性能信息
                performance: {},
                // 资源信息
                resources: {},
                // 错误信息
                errors: [],
                // 用户信息
                user: {
                    // 屏幕宽度
                    screen: screen.width,
                    // 屏幕高度
                    height: screen.height,
                    // 浏览器平台
                    platform: navigator.platform,
                    // 浏览器的用户代理信息
                    userAgent: navigator.userAgent,
                    // 浏览器用户界面的语言
                    language: navigator.language,
                },
                // 手动添加错误
                addError(error) {
                    const obj = {}
                    const { type, msg, url, row, col } = error
                    if (type) obj.type = type
                    if (msg) obj.msg = msg
                    if (url) obj.url = url
                    if (row) obj.row = row
                    if (col) obj.col = col
                    obj.time = new Date().getTime()
                    monitor.errors.push(obj)
                },
                // 重置 monitor 对象
                reset() {
                    window.performance && window.performance.clearResourceTimings()
                    monitor.performance = getPerformance()
                    monitor.resources = getResources()
                    monitor.errors = []
                },
                // 清空 error 信息
                clearError() {
                    monitor.errors = []
                },
                // 上传监控数据
                upload() {
                    // 自定义上传
                    // axios.post({
                    //     url: monitor.url,
                    //     data: {
                    //         performance,
                    //         resources,
                    //         errors,
                    //         user,
                    //     }
                    // })
                },
                // 设置数据上传地址
                setURL(url) {
                    monitor.url = url
                },
            }

            // 获取性能信息
            const getPerformance = () => {
                if (!window.performance) return
                const timing = window.performance.timing
                const performance = {
                    // 重定向耗时
                    redirect: timing.redirectEnd - timing.redirectStart,
                    // 白屏时间
                    whiteScreen: whiteScreen,
                    // DOM 渲染耗时
                    dom: timing.domComplete - timing.domLoading,
                    // 页面加载耗时
                    load: timing.loadEventEnd - timing.navigationStart,
                    // 页面卸载耗时
                    unload: timing.unloadEventEnd - timing.unloadEventStart,
                    // 请求耗时
                    request: timing.responseEnd - timing.requestStart,
                    // 获取性能信息时当前时间
                    time: new Date().getTime(),
                }

                return performance
            }

            // 获取资源信息
            const getResources = () => {
                if (!window.performance) return
                const data = window.performance.getEntriesByType('resource')
                const resource = {
                    xmlhttprequest: [],
                    css: [],
                    other: [],
                    script: [],
                    img: [],
                    link: [],
                    fetch: [],
                    // 获取资源信息时当前时间
                    time: new Date().getTime(),
                }

                data.forEach(item => {
                    const arry = resource[item.initiatorType]
                    arry && arry.push({
                        // 资源的名称
                        name: item.name,
                        // 资源加载耗时
                        duration: item.duration.toFixed(2),
                        // 资源大小
                        size: item.transferSize,
                        // 资源所用协议
                        protocol: item.nextHopProtocol,
                    })
                })

                return resource
            }

            window.onload = () => {
                // 在浏览器空闲时间获取性能及资源信息 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
                if (window.requestIdleCallback) {
                    window.requestIdleCallback(() => {
                        monitor.performance = getPerformance()
                        monitor.resources = getResources()
                        console.log('页面性能信息')
                        console.log(monitor.performance)
                        console.log('页面资源信息')
                        console.log(monitor.resources)
                    })
                } else {
                    setTimeout(() => {
                        monitor.performance = getPerformance()
                        monitor.resources = getResources()
                        console.log('页面性能信息')
                        console.log(monitor.performance)
                        console.log('页面资源信息')
                        console.log(monitor.resources)
                    }, 0)
                }
            }

            // 捕获资源加载失败错误 js css img...
            addEventListener('error', e => {
                const target = e.target
                if (target != window) {
                    monitor.errors.push({
                        type: target.localName,
                        url: target.src || target.href,
                        msg: (target.src || target.href) + ' is load error',
                        // 错误发生的时间
                        time: new Date().getTime(),
                    })

                    console.log('所有的错误信息')
                    console.log(monitor.errors)
                }
            }, true)

            // 监听 js 错误
            window.onerror = function(msg, url, row, col, error) {
                monitor.errors.push({
                    type: 'javascript', // 错误类型
                    row: row, // 发生错误时的代码行数
                    col: col, // 发生错误时的代码列数
                    msg: error && error.stack? error.stack : msg, // 错误信息
                    url: url, // 错误文件
                    time: new Date().getTime(), // 错误发生的时间
                })

                console.log('所有的错误信息')
                console.log(monitor.errors)
            }

            // 监听 promise 错误 缺点是获取不到行数数据
            addEventListener('unhandledrejection', e => {
                monitor.errors.push({
                    type: 'promise',
                    msg: (e.reason && e.reason.msg) || e.reason || '',
                    // 错误发生的时间
                    time: new Date().getTime(),
                })

                console.log('所有的错误信息')
                console.log(monitor.errors)
            })

            return monitor
        }

        const monitor = monitorInit()
    </script>
    <link rel="stylesheet" href="test.css">
    <title>Document</title>
</head>
<body>
    <button class="btn1">错误测试按钮1</button>
    <button class="btn2">错误测试按钮2</button>
    <button class="btn3">错误测试按钮3</button>
    <img src="https://avatars3.githubusercontent.com/u/22117876?s=460&v=4" alt="">
    <img src="test.png" alt="">
<script src="192.168.10.15/test.js"></script>
<script>
document.querySelector('.btn1').onclick = () => {
    setTimeout(() => {
        console.log(button)
    }, 0)
}

document.querySelector('.btn2').onclick = () => {
    new Promise((resolve, reject) => {
        reject({
            msg: 'test.js promise is error'
        })
    })
}

document.querySelector('.btn3').onclick = () => {
    throw ('这是一个手动扔出的错误')
}
</script>
</body>
</html>

использованная литература