Перейти к содержимому

Обзор ​

browserTracingIntegration даёт каждой странице свой трейс с одним корневым спаном, который покрывает всю страницу:

  • pageload: первая загрузка. Начинается с момента, когда пользователь начал переход (timeOrigin браузера), а не когда загрузился SDK.
  • navigation: каждая смена маршрута SPA (pushState/popstate). Каждая начинает новый трейс — те же границы, что у Sentry. Роутер может начать его сам, см. Начало перехода вручную.

Ещё два — по желанию, для того, что страница делает спустя долгое время после загрузки, — см. Трейсы возврата и взаимодействий:

  • resume: вкладка снова стала видимой;
  • ui.action: клик по кнопке, ссылке или другому интерактивному элементу.

Всё, что происходит, пока этот спан открыт, становится его дочерним:

  • вызовы fetch/XHR — спанами http.client (интеграция сама регистрирует httpTracingIntegration);
  • ваши собственные корневые вызовы span()/asyncSpan()/startInactiveSpan();
  • скрипты, стили, картинки, шрифты и другие ресурсы, загруженные во время спана, — спанами resource.<type>. Спан получают только ресурсы, которые шли по сети и грузились не меньше minResourceDuration (10 мс), не больше maxResourceSpans (50) на корневой спан. Попадания в кэш и мгновенные загрузки мало говорят о том, куда ушло время, а на обычной странице их большинство.

Корневой спан также несёт сводные данные:

  • resource.count, resource.cached, resource.transfer_size — все ресурсы за время спана, включая те, что не получили своего спана;
  • для pageload — сколько длилась каждая фаза загрузки (мс), из Navigation Timing: timing.redirect, timing.dns, timing.connect, timing.tls, timing.request, timing.response, timing.dom, timing.dom_content_loaded, timing.load.

Логи и исключения, пойманные, пока спан открыт, получают его trace.id и span.id.

Спан pageload также несёт web vitals как метрики спана: ttfb, fcp, lcp (миллисекунды от начала перехода) и cls (сумма неожиданных сдвигов макета за время спана).

Когда спан заканчивается ​

Явного сигнала «страница готова» нет, поэтому спан заканчивается, когда страница затихает, как idle-спаны Sentry:

  • когда проходит idleTimeout (1 с) без открытых дочерних спанов, спан заканчивается и обрезается до конца последнего дочернего. Сам таймаут не учитывается;
  • pageload остаётся открытым как минимум до события load и раньше него не заканчивается;
  • finalTimeout (30 с) ограничивает его, как бы страница ни была занята;
  • переход сразу заканчивает текущий спан и начинает следующий;
  • переключение на другую вкладку заканчивает текущий спан. Тот, что ещё был в процессе, — загрузка, переход, который не завершился, запрос в пути — получает статус cancelled; страница, которая уже закончила, сохраняет свой статус.

Причина хранится в спане как idle.finish_reason: idleTimeout, finalTimeout, externalFinish, hidden (вкладка скрыта) или cancelled (переход, который не состоялся).

Спаны, начатые после окончания корневого (клик, который отправляет запрос через минуту), всё равно идут в трейс страницы, но отдельными корнями — если не включить трейсы ниже.

Трейсы возврата и взаимодействий ​

В приложении, которое часами остаётся на одном адресе, всё после закрытия спана pageload попадает в трейс загрузки. Две опции дают этой работе собственные трейсы:

ts
browserTracingIntegration({
    instrumentResume: true,
    instrumentInteractions: true,
})
  • instrumentResume: когда вкладка снова становится видимой и корневой спан не открыт, начинается спан resume с именем страницы;
  • instrumentInteractions: при клике по интерактивному элементу (ссылке, кнопке, summary, чекбоксу, элементу с role вроде button или tab или с onclick), когда корневой спан не открыт, начинается спан ui.action, названный по элементу так же, как его называет хлебная крошка, — click button "Pay" in dialog "Confirm order", см. Как называется элемент (data-buglapse-name и data-buglapse-mask действуют и здесь).

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

Начало перехода вручную ​

pushState происходит поздно: роутер вызывает его после своих guard'ов и загрузки чанка маршрута, так что спан navigation их пропускает, называется сырым путём и никогда не видит редирект или отменённый переход. С instrumentNavigation: false переходы начинаете вы — смены истории больше не начинают ни спан, ни трейс:

ts
import { browserTracingIntegration, startBrowserTracingNavigationSpan } from '@buglapse/browser/integrations'

init({ dsn: '{dsn}', integrations: [browserTracingIntegration({ instrumentNavigation: false })] })

let navigation

router.beforeEach((to, from) => {
    navigation = startBrowserTracingNavigationSpan({
        name: to.matched.at(-1)?.path ?? to.path,   // '/users/:id'
        url: to.fullPath,
        from: from.fullPath,
        pending: true,
    })
})

router.afterEach((to, from, failure) => failure ? navigation?.cancel() : navigation?.land())

@buglapse/vue делает ровно это для vue-router.

startBrowserTracingNavigationSpan(options) заканчивает открытый корневой спан, начинает новый трейс, связанный с предыдущим, и возвращает новый спан — или undefined без интеграции или до init().

ОпцияПо умолчаниюОписание
nameпуть из url, через beforeStartSpanИмя спана
urllocation.hrefКуда идёт переход, абсолютный или относительный
fromпредыдущая страницаСохраняется как navigation.from
startTimeсейчасEpoch мс, например момент срабатывания первого guard'а роутера
pendingfalseПереход ещё не завершился: спан не может закончиться по idleTimeout до land() или cancel()

Возвращённый спан — корневой (setData(), metric(), updateName(), end(), …) с двумя дополнительными методами:

  • land() — переход состоялся; отсюда спан закончится, когда страница затихнет;
  • cancel() — не состоялся (guard отказал, чанк не загрузился): спан заканчивается сразу со статусом cancelled.

Открытый корневой спан ​

getActiveRootSpan() (именованный экспорт @buglapse/browser, Buglapse.getActiveRootSpan() в бандле с CDN) возвращает корневой спан, открытый прямо сейчас, или undefined:

ts
import { getActiveRootSpan } from '@buglapse/browser'

getActiveRootSpan()?.updateName('/users/:id')        // имя, которое становится известно позже
getActiveRootSpan()?.setData('cart.items', 3)
const openedAt = getActiveRootSpan()?.startTime      // epoch мс

updateName() переименовывает спан и transaction спанов, начатых после вызова; hasOpenChildren() говорит, выполняется ли под ним ещё что-то.

Чтобы знать, когда корневой спан открывается и закрывается, — закрыть свои спаны вместе со страницей или не дать позднему замеру попасть в следующую, — передайте onSpanStart / onSpanEnd:

ts
browserTracingIntegration({
    onSpanStart: (span, { op, name, url }) => { /* 'pageload' | 'navigation' | 'resume' | 'ui.action' */ },
    onSpanEnd: (span, { reason, discarded }) => { /* reason: его idle.finish_reason */ },
})

onSpanEnd вызывается ровно один раз для каждого спана, который видел onSpanStart, в том числе для отброшенного как пустой (discarded: true).

Путь по визиту ​

Трейс каждой страницы записывает трейс страницы, с которой пришёл пользователь, как previous_trace.id (контекст на корневом спане). Трейс resume или ui.action входит в цепочку, только если был отправлен: отброшенный как пустой пропускается, так что ссылки никогда не ведут на несуществующий трейс. Страница трейса в панели показывает по нему ссылки Предыдущая страница / Следующая страница, так что весь визит можно пройти по одной странице. Каждая страница остаётся коротким трейсом с осмысленной длительностью и сэмплируется сама (tracesSampleRate, см. Трейсинг → Сэмплирование).

Переходы в SPA связываются в памяти. Чтобы связывать и полные загрузки страниц (обычный <a href>, отправка формы, серверный редирект, location.href = ...), трейс текущей страницы хранится в sessionStorage вкладки под ключом buglapse_previous_trace, и трейс pageload следующей страницы ссылается на него. Этим управляет linkPreviousTrace:

  • 'session-storage' (по умолчанию): связывать переходы в SPA и полные загрузки;
  • 'in-memory': связывать только переходы в SPA, каждая полная загрузка начинает несвязанный трейс;
  • 'off': не связывать трейсы.

Ограничения связи через sessionStorage:

  • трейс, начатый больше чем за час до загрузки следующей страницы, не связывается;
  • связь — в пределах вкладки и домена, так что страница на другом домене (или поддомене) назад не ссылается;
  • ссылка, открытая в новой вкладке, может скопировать sessionStorage вкладки, и две страницы будут ссылаться на одну. Следующая страница тогда ведёт на ту, что началась раньше;
  • если трейс предыдущей страницы не был сэмплирован, ссылке некуда вести, и Предыдущая страница не показывается.

Использование ​

ts
import { init } from '@buglapse/browser'
import { browserTracingIntegration } from '@buglapse/browser/integrations'

init({
    dsn: '{dsn}',
    integrations: [browserTracingIntegration()],
})

Со скриптом с CDN включите Трассировку в браузере в Настройки → Скрипт загрузчика проекта.

По умолчанию спаны называются путём адреса. Используйте beforeStartSpan, чтобы группировать страницы по маршруту, — так все /users/42 и /users/43 станут одним /users/:id:

ts
browserTracingIntegration({
    beforeStartSpan: ({ name }) => ({ name: name.replace(/\/\d+(?=\/|$)/g, '/:id') }),
})

Опции ​

ОпцияТипПо умолчаниюОписание
idleTimeoutnumber1000Мс без открытых дочерних спанов, после которых корневой спан заканчивается
finalTimeoutnumber30000Максимальная длина корневого спана в мс
instrumentPageLoadbooleantrueНачинать спан pageload для первой загрузки
instrumentNavigationbooleantrueНачинать спан navigation (и новый трейс) при каждом переходе в SPA. false: переходы начинаете вы
instrumentResumebooleanfalseНачинать спан resume, когда вкладка снова видима, см. Трейсы возврата и взаимодействий
instrumentInteractionsbooleanfalseНачинать спан ui.action при клике по интерактивному элементу
traceRequestsbooleantrueРегистрировать httpTracingIntegration для спанов fetch/XHR
traceResourcesbooleantrueЗаписывать спаны resource.* из Resource Timing
traceCachedResourcesbooleanfalseЗаписывать и ресурсы из кэша браузера
minResourceDurationnumber10Пропускать ресурсы, загрузившиеся быстрее, в мс
maxResourceSpansnumber50Предел спанов resource.* на корневой спан
beforeStartSpan({ name, op, url }) => { name }—Переименовать спан до начала
onSpanStart(span, { op, name, url }) => void—Вызывается с каждым корневым спаном, который открывает интеграция
onSpanEnd(span, { reason, discarded }) => void—Вызывается один раз, когда каждый из них закончился
linkPreviousTrace'session-storage' | 'in-memory' | 'off''session-storage'Как трейс страницы связывается с предыдущей, см. Путь по визиту

Опции-функции (beforeStartSpan, onSpanStart, onSpanEnd) — для npm-пакета; остальные можно задать и в Настройки → Скрипт загрузчика.