Начните со скрытого скрипта отчетов журнала

Vue.js

Разработайте и упакуйте интерфейсный встроенный сценарий создания отчетов о точках и постепенно подумайте об оптимизации этого процесса.

основное содержание:

  • Метод запроса: краткий (выборка) | эффективный (заголовок) | общий (пост)
  • Пакетная упаковка и отчетность
  • Нет отчетов о задержках в сети

эффект:

  • Не норма, сосредоточься на идее

окончательный код:analytics.js

Способ запроса: лаконичный | эффективный | общий

Сначала мы используем самый прямой способ реализации этого скрипта сообщения скрытых точек.
Создайте файл и назовите его Analytics.js, добавьте запрос на скрипт, и упаковывайте немного:

export default function analytics (action = 'pageview') {
  var xhr = new XMLHttpRequest()
  let uploadUrl = `https://xxx/test_upload?action=${action}&timestamp=${Date.now()}`
  xhr.open('GET', uploadUrl, true)
  xhr.send()
}

Таким образом, позвонивanalytics(), отправьте сообщение на наш сервер статистики и укажите тип поведения.
Если действительно не так много данных, которые нам нужно сообщить, например, только «поведение/событие», «время», «пользователь (идентификатор)», «среда платформы» и т. д., а количество данных находится в пределах длины URL-адреса ограничение, поддерживаемое браузером, то мы можем упростить этот запрос с помощью:

// 简洁的方式
export default function analytics (action = 'pageview') {
  (new Image()).src = `https://xxx/test_upload?action=${action}&timestamp=${Date.now()}`
}

Английский термин для метода отправки запроса с помощью img: image beacon.
Он в основном используется в тех случаях, когда на сервер необходимо отправить только данные журнала, и серверу не нужно отвечать телом сообщения. Например, сбор статистики посещений.
Разница между этим и запросом ajax заключается в следующем:


Или мы просто используем новый стандартfetchЗагрузить

// 简洁的方式
export default function analytics (action = 'pageview') {
  fetch(`https://www.baidu.com?action=${action}&timestamp=${Date.now()}`, {method: 'get'})
}

Учитывая процесс передачи данных, нас не волнует возвращаемое значение, нам нужно только знать, успешен ли отчет или нет, мы можем использоватьГлавный запросЧтобы сделать наш процесс эскалации более эффективным:

// 高效的方式
export default function analytics (action = 'pageview') {
 fetch(`https://www.baidu.com?action=${action}&timestamp=${Date.now()}`, {method: 'head'})
}

headМетод запроса и метод передачи параметров такие же, какgetЗапрос является согласованным, и он также ограничен браузером, но поскольку ему не нужно возвращать объект ответа, его эффективность намного выше, чем у метода get. Простой запрос в приведенном выше примере занимает около 20 мс, оптимизированный для хрома.

Если данных для загрузки действительно много, длина URL-адреса после объединения параметров превышает ограничение браузера, что приводит к сбою запроса. Затем идем по пути поста:

// 通用的方式 (可以采用fetch, 但fetch默认不带cookie, 可能有认证问题)
export default function analytics (action = 'pageview', params) {
  let xhr = new XMLHttpRequest()
  let data = new FormData()
  data.append('action', action)
  for (let obj in params) {
    data.append(obj, params[obj])
  }
  xhr.open('POST', 'https://xxx/test_upload')
  xhr.send(data)
}

Пакетная упаковка и отчетность

Независимо от объема данных в одной скрытой точке, теперь предполагается, что на странице есть несколько скрытых точек для анализа поведения пользователя, и частые отчеты могут иметь определенное влияние на доступ пользователя к обычным функциям.
Самый прямой способ решить эту проблему — уменьшить количество сообщаемых запросов. Поэтому давайте реализуем функцию пакетной загрузки.Простая идея состоит в том, чтобы упаковывать и сообщать о каждых 10 единицах собранных данных:

// 每10条数据数据进行打包
let logs = []
/**
 * @params {array} 日志数组
 */
function upload (logs) {
  console.log('send logs', logs)
  let xhr = new XMLHttpRequest()
  let data = new FormData()
  data.append('logs', logs)
  xhr.open('POST', this.url)
  xhr.send(data)
}

export default function analytics (action = 'pageview', params) {
  logs.push(Object.assign({
    action,
    timeStamp: Date.now()
  }, params))
  if (logs.length >= 10) {
    upload(logs)
    logs = []
  }
}

На месте закопанной точки сначала выполняем десятки раз, чтобы увидеть

import analy from '@/vendor/analytics1.js'
for (let i = 33; i--;) {
    analy1('pv')
}

хорошо, при нормальных обстоятельствах отчет должен быть успешным, и каждый запрос содержит 10 данных.
Но проблема вскоре обнаружилась. Такое поведение при сборе N фрагментов данных и последующей их равномерной отправке вызовет ошибку. Если пользователь закрывает страницу, когда не хватает N фрагментов данных, или если он превышает N кратных, но не может собрать N Эта часть данных будет потеряна, если ее не обработать.
Простое решение — прослушать страницуbeforeunloadсобытие, загрузите все журналы с менее чем N оставшимися журналами, прежде чем покинуть страницу. Поэтому добавим событие beforeunload, кстати упорядочим код и инкапсулируем его в класс:

export default class eventTrack {
  constructor (option) {
    this.option = Object.assign({
      url: 'https://www.baidu.com',
      maxLogNum: 10
    }, option)
    this.url = this.option.url
    this.maxLogNum = this.option.maxLogNum
    this.logs = []
    // 监听unload事件,
    window.addEventListener('beforeunload', this.uploadLog.bind(this), false)
  }
  /**
   * 收集日志,集满 maxLogNum 后上传
   * @param  {string} 埋点行为
   * @param  {object} 埋点附带数据
   */
  analytics (action = 'pageview', params) {
    this.logs.push(Object.assign({
      action,
      timeStamp: Date.now()
    }, params))
    if (this.logs.length >= this.maxLogNum) {
      this.send(this.logs)
      this.logs = []
    }
  }
  // 上报一个日志数组
  send (logs, sync) {
    let xhr = new XMLHttpRequest()
    let data = new FormData()
    for (var i = logs.length; i--;) {
      data.append('logs', JSON.stringify(logs[i]))
    }
    xhr.open('POST', this.url, !sync)
    xhr.send(data)
  }
  // 使用同步的xhr请求
  uploadLog () {
    this.send(this.logs, true)
  }
}

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

  1. Запрос отчета должен быть альтернативный способ: вызов формыanalytics.head(один отчет),analytics.post(По умолчанию)
  2. Когда страница выгружается, лучше используйтеsendBeacon

оsendBeacon, 该方法可以将少量数据异步传输到Web服务器。 В приведенном выше кодеuploadLogМетод, который мы использовали для синхронизации запроса xhr, делается для предотвращения страницы из-за закрытия или передачи, время выполнения скрипта, чтобы вызвать окончательный журнал, не может быть сообщено.
В сценарии перед выгрузкой синхронизацияxhrа такжеsendBeaconспециальность

  • Синхронный xhr: заблокируйте скрипт на некоторое время при выходе со страницы, чтобы обеспечить создание журнала.
  • sendBeacon: Инициировать асинхронный запрос при выходе со страницы, не блокировать и убедиться, что лог отправлен. Есть проблемы с совместимостью браузера

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

export default class eventTrack {
  constructor (option) {
    this.option = Object.assign({
      url: 'https://www.baidu.com',
      maxLogNum: 10
    }, option)
    this.url = this.option.url
    this.maxLogNum = this.option.maxLogNum
    this.logs = []

    // 拓展analytics,允许单个上报
    this.analytics['head'] = (action, params) => {
      return this.sendByHead(action, params)
    }
    this.analytics['post'] = (action, params) => {
      return this.sendByPost(action, params)
    }
    // 监听unload事件,
    window.addEventListener('beforeunload', this.unloadHandler.bind(this), false)
  }

  /**
   * 收集日志,集满 maxLogNum 后上传
   * @param  {string} 埋点行为
   * @param  {object} 埋点附带数据
   */
  analytics (action = 'pageview', params) {
    this.logs.push(JSON.stringify(Object.assign({
      action,
      timeStamp: Date.now()
    }, params)))
    if (this.logs.length >= this.maxLogNum) {
      this.sendInPack(this.logs)
      this.logs = []
    }
  }

  /**
   * 批量上报一个日志数组
   * @param  {array} logs 日志数组
   * @param  {boolean} sync 是否同步
   */
  sendInPack (logs, sync) {
    let xhr = new XMLHttpRequest()
    let data = new FormData()
    for (var i = logs.length; i--;) {
      data.append('logs', logs[i])
    }
    xhr.open('POST', this.url, !sync)
    xhr.send(data)
  }

  /**
   * POST上报单个日志
   * @param  {string} 埋点类型事件
   * @param  {object} 埋点附加参数
   */
  sendByPost (action, params) {
    let xhr = new XMLHttpRequest()
    let data = new FormData()
    data.append('action', action)
    for (let obj in params) {
      data.append(obj, params[obj])
    }
    xhr.open('POST', this.url)
    xhr.send(data)
  }

  /**
   * Head上报单个日志
   * @param  {string} 埋点类型事件
   * @param  {object} 埋点附加参数
   */
  sendByHead (action, params) {
    let str = ''
    for (let key in params) {
      str += `&${key}=${params[key]}`
    }
    fetch(`https://www.baidu.com?action=${action}&timestamp=${Date.now()}${str}`, {method: 'head'})
  }

  /**
   * unload事件触发时,执行的上报事件
   */
  unloadHandler () {
    if (navigator.sendBeacon) {
      let data = new FormData()
      for (var i = this.logs.length; i--;) {
        data.append('logs', this.logs[i])
      }
      navigator.sendBeacon(this.url, data)
    } else {
      this.sendInPack(this.logs, true)
    }
  }
}

Нет отчетов о задержках в сети

Задумайтесь над вопросом, а что если наша страница отключена от сети (например, плохой сигнал), пользователь в этот период выполнил операцию, и мы хотим собрать эту часть данных?

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

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

  1. отчетные данные,navigator.onLineОпределить состояние сети
  2. Есть сеть для отправки нормально
  3. кредит, когда нет сетиlocalstorage, задержка отчетности

Давайте изменимsendInPackи добавьте соответствующий метод

sendInPack (logs, sync) {
    if (navigator.onLine) {
      this.sendMultiData(logs, sync)
      this.sendStorageData()
    } else {
      this.storageData(logs)
    }
  }
  sendMultiData (logs, sync) {
    console.log('sendMultiData', logs)
    let xhr = new XMLHttpRequest()
    let data = new FormData()
    for (var i = logs.length; i--;) {
      data.append('logs', logs[i])
    }
    xhr.open('POST', this.url, !sync)
    xhr.send(data)
  }
  storageData (logs) {
    console.log('storageData', logs)
    let data = JSON.stringify(logs)
    let before = localStorage['analytics_logs']
    if (before) {
      data = before.replace(']', ',') + data.replace('[', '')
    }
    localStorage.setItem('analytics_logs', data)
  }
  sendStorageData () {
    let data = localStorage['analytics_logs']
    if (!data) return
    data = JSON.parse(data)
    this.sendMultiData(data)
    localStorage['analytics_logs'] = ''
  }

Уведомлениеnavigator.onLine

Лучше PV: VisibilyChange

PV — важная часть отчета журнала.
На данный момент мы в основном завершили отчет, и теперь мы возвращаемся к бизнес-уровню. Какова цель pv и как лучше достичь нашей цели? Рекомендуется сначала прочитать эту статью о пв:
Почему ваша статистика PV неверна

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

  1. Пользователь открывает страницу один раз, и использует сотни раз в ближайшие дни, но нет страницы обновления, это должно быть только просмотр страницы?
  2. Если два пользователя обращаются к странице одинаковое количество раз в день, но один из них обновляет, а другой поддерживает работу страницы в фоновом режиме, режим просмотра страницы с использованием статистических результатов двух должен сильно отличаться от того, что
  3. ···

Чтобы следовать лучшему PV, мы можем добавить в сценарий следующие случаи:

  1. Когда страница загружается, если отображается visibilityState страницы, отправьте статистику просмотра страницы;
  2. Когда страница загружает, если скрыта страница скрыта, слушает события видимости, и видимость становится видимой при отправке статистики просмотра страницы;
  3. Если visibilityState изменяется со скрытого на видимое и прошло «достаточно много времени» с момента последнего взаимодействия с пользователем, отправьте новую статистику просмотра страницы;
  4. Если URL-адрес изменяется (только путь или часть поиска отправляет изменения, хеш-часть следует игнорировать, поскольку она используется для обозначения переходов внутри страницы) отправить новую статистику просмотра страницы; Добавьте следующий фрагмент в наш конструктор:
this.option = Object.assign({
  url: 'https://baidu.com/api/test',
  maxLogNum: 10,
  stayTime: 2000, // ms, 页面由隐藏变为可见,并且自上次用户交互之后足够久,可以视为新pv的时间间隔
  timeout: 6000   // 页面切换间隔,小于多少ms不算间隔
}, option)
this.hiddenTime = Date.now()
···
 // 监听页面可见性
document.addEventListener('visibilitychange', () => {
  console.log(document.visibilityState, Date.now(), this.hiddenTime)
  if (document.visibilityState === 'visible' && (Date.now() - this.hiddenTime > this.option.stayTime)) {
    this.analytics('re-open')
    console.log('send pv visible')
  } else if (document.visibilityState === 'hidden') {
    this.hiddenTime = Date.now()
  }
})
···

Лучшее видео: хэш-прыжок

Учтите, что мы одностраничное приложение в хеш-режиме, то есть переходы по маршруту помечаются символом «#» и концом маршрута. Если мы хотим отслеживать каждый коммутатор маршрутизации, одним из способов является мониторинг каждого компонента маршрутизации, или мы можем напрямую и единообразно обработать его в файле отчета:

window.addEventListener('hashchange', () => {
  this.analytics()
})

Но с этим возникает проблема, как определить, является ли текущий скачок хэша действительным переходом. Например, на странице есть логика перенаправления, пользователь входит со страницы А (устаревшая страница), и наш код перебрасывает ее на страницу Б, так что pv отправляется дважды, но фактическая эффективная просматривается только страница Б один раз. Или пользователь просто просмотрел страницу А и снова перешел на страницу Б. Следует ли использовать страницу А в качестве действительного PV?
Лучший способ - установить допустимый интервал. Например, просмотр менее 5S не считается действительным PV и полученной логикой, нам нужно настроить нашanalyticsметод:

// 封装一个sendPV 专门用来发送pv
constructor (option) {
  ···
  this.sendPV = this.delay((args) => {
    this.analytics({action: 'pageview', ...args})
  })
    
  window.addEventListener('hashchange', () => {
    this.sendPV()
  })
  this.sendPV()
···
}

delay (func, time) {
    let t = 0
    let self = this
    return function (...args) {
      clearTimeout(t)
      t = setTimeout(func.bind(this, args), time || self.option.timeout)
    }
}

хорошо, вот это почти полная схема здесьanalytics.js, Добавлен пункт для вызова теста
Taking into account different business scenarios, we have to have more space to fill, in fact, is a closed-loop data for better business analysis services, although it is a traditional feature, but worth a careful fine a point or with a lot of Это