npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

grafana-faro-wrapper

v1.4.0

Published

Grafana Faro wrapper for web pages and React apps: one-call setup with OTLP transport, URL and label sanitization, custom metrics and step-based SLO metrics

Readme

grafana-faro-wrapper

npm CI license

Обёртка над Grafana Faro — SDK, который собирает с фронтенда ошибки, Web Vitals и события и отправляет их в Grafana. Пакет подключает Faro к обычной HTML-странице или React-приложению одним вызовом и добавляет то, чего в Faro нет:

  • Отправка по OTLP — логи, ошибки и измерения уходят в OTLP-эндпоинт; тела измерений и ошибок — в logfmt, их разбирает | logfmt в LogQL.
  • Санитизация — из URL страниц, ресурсов и стектрейсов убираются query, hash и типовые идентификаторы в пути (UUID, hex, длинные числа), чтобы в Grafana не утекали токены из query и не росла кардинальность.
  • sendMetric — пользовательская метрика с единицей, типом, метками и результатом в одном вызове.
  • trackSlo — SLO-метрика «страница дошла до состояния»: шаги с дедлайнами, пауза при скрытой вкладке, отправка в едином формате.
  • Проверки вёрстки — готовые предикаты для шагов: элементы отрисованы, их достаточно, картинки и фоны загрузились.

Работает с Faro 2.0.2+. Все бандлы собраны под ES2019: Chrome 73+, Firefox 69+, Safari 12.1+. Web Vitals из getWebInstrumentations() считаются только в браузерах, которые отдают эти метрики и умеют Array.prototype.at и findLast (Chrome 97+, Firefox 104+, Safari 15.4+): в более старых getWebInstrumentations() их не включает, остальная инструментация работает.

Установка

npm install grafana-faro-wrapper @grafana/faro-web-sdk @grafana/faro-transport-otlp-http

Для инструментации React Router нужен ещё @grafana/faro-react той же версии, что и @grafana/faro-web-sdk; @grafana/faro-react 2.12+ требует react-router 7.12+.

Без сборщика подключите один файл — dist/index.umd.full.js (его отдают unpkg и jsdelivr по умолчанию) содержит Faro внутри и выставляет глобал GrafanaFaroWrapper. Отдельно Faro на такую страницу не подключайте: getWebInstrumentations берётся из того же глобала.

<script src="https://unpkg.com/grafana-faro-wrapper@1/dist/index.umd.full.js"></script>
<script>
  const faroService = new GrafanaFaroWrapper.FaroService();
  faroService.init({
    faroUrl: 'https://otlp.example.com/v1/logs',
    faroKey: 'your-key',
    app: { name: 'my-site' },
    instrumentations: GrafanaFaroWrapper.getWebInstrumentations(),
  });
</script>

Файлы бандлов доступны и из установленного пакета — grafana-faro-wrapper/dist/index.umd.full.js, например для копирования сборкой бекенда. Второй бандл, dist/index.umd.js, не содержит Faro и ждёт его IIFE-бандлы, подключённые раньше него, — они выставляют глобалы GrafanaFaroWebSdk и GrafanaFaroTransportOtlpHttp:

<script src="https://unpkg.com/@grafana/faro-web-sdk@2/dist/bundle/faro-web-sdk.iife.js"></script>
<script src="https://unpkg.com/@grafana/faro-transport-otlp-http@2/dist/bundle/faro-transport-otlp-http.iife.js"></script>
<script src="https://unpkg.com/grafana-faro-wrapper@1/dist/index.umd.js"></script>

Быстрый старт

import { FaroService, getWebInstrumentations } from 'grafana-faro-wrapper';

export const faroService = new FaroService();

faroService.init({
  faroUrl: 'https://otlp.example.com/v1/logs',
  faroKey: 'your-key',
  app: { name: 'my-app', version: '1.0.0', environment: 'production' },
  instrumentations: getWebInstrumentations(),
});
  • faroUrl — OTLP-эндпоинт для логов, faroKey — API-ключ. Трейсы не отправляются.
  • Остальные поля — обычный BrowserConfig Faro: app, user, sessionTracking, batching и т. д. Полей url, apiKey и paused в типе нет: первые два заменяет транспорт обёртки, вместо paused — enabled. Свои transports добавляются к транспорту обёртки.
  • enabled: false инициализирует Faro на паузе — удобно для dev-окружения; после destroy() и init() с тем же флагом Faro тоже остаётся на паузе.
  • init() возвращает инстанс Faro и, как сам Faro, регистрирует его глобалом window.faro; отключается опцией preventGlobalExposure: true.

[!IMPORTANT] По умолчанию instrumentations — пустой список: Faro не собирает ни ошибки, ни Web Vitals, ни сессии, пока вы не передадите getWebInstrumentations(), как в примере выше.

Faro регистрируется один раз на страницу, поэтому на странице — один FaroService: второй получит ошибку. destroy() ставит Faro на паузу, а следующий init() снимает паузу с того же инстанса: применяется только новый beforeSend, а об изменении faroUrl, faroKey или app выводится предупреждение. Биконы, которые Faro не успел отправить из батча до destroy(), уходят после следующего init() через санитайзеры и beforeSend того init(), при котором они созданы; без повторного init() они не отправляются.

React Router

import { createReactRouterV7Options, ReactIntegration } from '@grafana/faro-react';
import { FaroService, getWebInstrumentations } from 'grafana-faro-wrapper';
import {
  createRoutesFromChildren,
  matchRoutes,
  Routes,
  useLocation,
  useNavigationType,
} from 'react-router';

const faroService = new FaroService();

faroService.init({
  faroUrl: 'https://otlp.example.com/v1/logs',
  faroKey: 'your-key',
  app: { name: 'my-app' },
  instrumentations: getWebInstrumentations(),
  routerAdapter: new ReactIntegration({
    router: createReactRouterV7Options({
      createRoutesFromChildren,
      matchRoutes,
      Routes,
      useLocation,
      useNavigationType,
    }),
  }),
});

Затем замените Routes на FaroRoutes из @grafana/faro-react. Хелперы для React Router 4–6 и data-router описаны в документации faro-react.

Пользовательские метрики

faroService.sendMetric({
  name: 'checkout',
  value: 1,
  unit: 'EVENTS',
  type: 'counter',
  labels: { payment: 'card' },
  result: 'success',
});

| Поле | Тип | Описание | | ------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------- | | name | string | Имя метрики | | value | number | Значение; не конечное число (NaN, ±Infinity) даёт 0 | | unit | MetricUnit | BYTES, MILLISECONDS, SECONDS, REQUESTS, ERRORS, OPERATIONS, EVENTS, UNITLESS | | type | MetricType | counter, gauge, histogram | | result | 'success' \| 'fail' | Итог, попадает и в тело лога, необязательно | | labels | Record<string, string \| number \| boolean> | Плоские метки, типы значений сохраняются, необязательно | | buckets | number[] | Границы бакетов гистограммы, необязательно | | description | string | Описание, необязательно | | timestamp | number | Время события в мс, по умолчанию — момент вызова |

Каждый вызов — отдельное измерение Faro с type: 'custom' и values: { [name]: value }; одинаковые метрики подряд не схлопываются. Пустое name, unit или type вне списка не останавливают отправку, но один раз дают предупреждение в консоли. Остальные поля уходят в контекст измерения под ключами measurement.* (экспорт MEASUREMENT_KEYS) и попадают в Grafana атрибутом faro.measurement.context. Тело лога — logfmt: faro_signal=measurement type=custom name=checkout value=1 result=success, его разбирает | logfmt в LogQL.

Метки становятся измерениями в Grafana: кладите в них только значения с небольшим числом вариантов. Идентификаторы пользователя, сессии, заказа и URL уже есть в meta бикона, а как метки они взорвут кардинальность.

SLO-метрика из нескольких шагов

trackSlo измеряет, за сколько страница дошла до состояния, описанного шагами, и отправляет гистограмму длительности в миллисекундах. Каждый шаг — предикат, который опрашивается каждые 100 мс, пока не вернёт true или промис с true; из JavaScript подойдёт и любое истинное значение, например найденный элемент. Все шаги прошли — result: 'success'; какой-то не успел к дедлайну — result: 'fail', и в метках видно, какой именно.

const pageReady = faroService.trackSlo({
  name: 'page_ready',
  failTime: 10_000,
  buckets: [100, 500, 1000, 2000, 5000, 10_000, 10_100],
  steps: {
    data: async () => (await fetchData()).ok,
    render: () => isRendered(),
    images: { check: () => asyncCheckImagesIsDisplayed('img.hero'), failTime: 3000 },
  },
});

pageReady.dispose(); // при уходе со страницы — остановить без отправки

| Поле | Описание | | ----------------- | ------------------------------------------------------------------------------------------------------- | | name | Имя метрики | | failTime | Дедлайн всего прогона в мс; по его истечении метрика отправляется с fail | | steps | Шаги: функция или { check, failTime }. Дедлайн шага не может быть больше дедлайна прогона | | buckets | Границы бакетов гистограммы, необязательно | | labels | Дополнительные метки: объект или функция, которая вернёт их в момент отправки, необязательно | | startWhen | Селектор или предикат: часы не идут, пока по селектору нет элемента или предикат false, необязательно | | pauseWhenHidden | Пауза, пока вкладка скрыта или фокус ушёл из окна; по умолчанию true | | log | Писать в консоль ход прогона и первую ошибку каждой проверки |

  • Исключение или отклонённый промис в проверке — «ещё не готов», повтор на следующем тике; асинхронная проверка не запускается заново, пока идёт предыдущая.
  • Шаг, не успевший к дедлайну, получает false окончательно, но прогон ждёт остальные — до своего failTime.
  • Хелперы картинок проваливают шаг сразу, не дожидаясь дедлайна, если у <img> нет ни src, ни srcset и она не внутри <picture>: такая вёрстка сама не исправится. Это работает, когда хелпер вызван прямо в проверке шага, например () => asyncCheckImagesIsDisplayed('img.hero'); если проверка сначала ждёт await, шаг опрашивается как обычно.
  • Время на паузе не входит ни в длительность, ни в дедлайны, поэтому пока вкладка скрыта, метрика не уходит: асинхронная проверка, завершившаяся на паузе, засчитывается, а прогон заканчивается после возврата. Уход фокуса во вложенный iframe паузой не считается; окно без фокуса на старте — тоже, пауза начинается с ухода фокуса и длится, пока фокус не вернётся, даже если вкладку за это время скрыли и показали.
  • Уходит одно измерение: unit: 'MILLISECONDS', type: 'histogram', result, а в labels — метки из labels, результат каждого шага и status (то же, что result). Имена шагов становятся ключами меток, шаг с именем status запрещён типом, а метки не перезаписывают ни status, ни результаты шагов — о таком совпадении предупреждает консоль.
  • Невалидный селектор в startWhen не бросает исключение: прогон ждёт, в консоли — одно предупреждение. Без шагов прогон не запускается и ничего не отправляет.
  • startWhen проверяется, как и шаги, каждые 100 мс — селектор срабатывает и на вставку элемента, и на смену его класса или атрибута. Пока условие не выполнено, прогон ждёт без ограничения по времени.
  • Трекер сообщает state (waiting, running, paused, done, disposed) и умеет dispose(). Без него незавершённый прогон опрашивает шаги до failTime, а ждущий startWhen — бессрочно, поэтому при уходе со страницы вызывайте dispose().

В React прогон живёт в эффекте, а смена состояния страницы — это смена зависимости эффекта:

useEffect(() => {
  if (!isOpen) return;
  const slo = faroService.trackSlo({ name: `payments:${view}`, failTime: 40_000, steps });
  return () => slo.dispose();
}, [isOpen, view]);

Проверки вёрстки

Хелперы проверяют DOM, возвращают boolean или Promise<boolean> и не бросают исключений — их можно передавать в шаги напрямую. Невалидный селектор даёт false (у getAmount — 0) и одно предупреждение в консоли.

import {
  asyncCheckBackgroundImagesIsDisplayed,
  asyncCheckImagesIsDisplayed,
  checkGTEAmount,
  checkRender,
  checkRenderInnerValue,
} from 'grafana-faro-wrapper';

faroService.trackSlo({
  name: 'payments:payments-all',
  failTime: 4000,
  steps: {
    tariff: () => checkRenderInnerValue('[data-slo="tariff-name"]'),
    methods: () => checkGTEAmount('[data-slo="payment-list"] li', 12),
    'method-images': () => asyncCheckImagesIsDisplayed('[data-slo="payment-list"] img'),
    'reseller-logos': () => asyncCheckBackgroundImagesIsDisplayed('[data-slo="reseller-link"]'),
    controls: () => checkRender(['[data-slo="code-input"]', '[data-slo="code-button"]']),
  },
});

| Хелпер | Проходит, когда | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | checkRender(selectors) | каждый селектор (строка или массив) есть в DOM | | checkRenderInnerValue(selectors) | по каждому селектору есть видимый элемент с текстом (см. ниже) | | checkEnabledButtonState(selector) | есть видимая кнопка, которую можно нажать (см. ниже) | | getAmount(selector) | возвращает число элементов | | checkAmount(selector, n), checkGTEAmount(selector, n) | элементов ровно n / не меньше n | | checkImagesIsDisplayed(selector) | синхронно: все картинки уже загружены (битый SVG не отличает, см. ниже) | | asyncCheckImagesIsDisplayed(selector, timeoutMs?) | все картинки загрузились за timeoutMs | | asyncCheckBackgroundImagesIsDisplayed(selector, timeoutMs?) | у каждого элемента загрузилась своя картинка: <img> внутри или background-image | | extractBackgroundUrl(element) | URL из background-image, у image-set() — под плотность экрана, или null | | loadImage(src, timeoutMs?) | картинка по адресу загрузилась за timeoutMs |

  • timeoutMs по умолчанию — DEFAULT_IMAGE_TIMEOUT_MS, 10 секунд; Infinity — ждать без ограничения, пока проверку не прервёт дедлайн шага. Нет элементов по селектору — false.
  • Селектор может указывать на <img> или на обёртку: тогда проверяются картинки внутри, обёртка без них (например, только с inline <svg>) — false. <img> без src и srcset — тоже false, а в trackSlo шаг с ней проваливается сразу. Картинки внутри Shadow DOM хелперы не видят.
  • Асинхронные проверки верят браузеру: картинка засчитывается, если загрузилась без ошибки, в том числе SVG без собственных размеров. Синхронная проверка видит только то, что уже загружено, и не отличает битый SVG (404, HTML-страница вместо картинки, неверный MIME) от загруженного — для SVG берите асинхронную. «IsDisplayed» значит «загружено», а не «видно на экране». Картинку с loading="lazy" за пределами экрана браузер не грузит, и проверка вернёт false по таймауту — такие картинки в шаги не добавляйте.
  • Из background-image берётся первый url(), а у image-set() — вариант под плотность экрана, как у браузера. Поддержку формата из type() хелпер узнать не может: из вариантов одной плотности он берёт первый, поэтому ставьте первым формат, который понимают все ваши браузеры. Элемент без <img> и без url() — например, с фоном из одного градиента — даёт false; фоны ::before/::after не учитываются. Фоны и loadImage держат одну картинку на адрес: битый адрес запрашивается снова не чаще раза в 5 секунд, так что опрос шага в trackSlo не шлёт запрос на каждом тике, а временный сбой сети не держит проверку в false до перезагрузки страницы.
  • checkRenderInnerValue и checkEnabledButtonState проходят, если подходит хотя бы один элемент по селектору, — так учитываются и адаптивные дубли блока. Текст должен быть виден: скрытый блок или потомок (display: none, hidden, свёрнутый <details>, visibility: hidden), стили, скрипты, пробелы и невидимые символы (нулевой ширины, bidi-метки, мягкий перенос) не в счёт. Текст, спрятанный только визуально (opacity: 0, sr-only, за экраном), засчитывается; исключение — Safari: текст в контейнере нулевой высоты или ширины с overflow: hidden он не отдаёт, и проверка там не проходит. Текст в SVG проверяется так же, у <select> засчитывается текст выбранного варианта, <option> виден, если выбран, а у веб-компонента учитывается и текст его открытого shadow root.
  • Кнопку можно нажать, если она видна (в том числе не лежит в свёрнутом <details>, hidden="until-found" или content-visibility: hidden), не :disabled (в том числе через <fieldset disabled>), не внутри inert, без aria-disabled="true", класса disabled и pointer-events: none. Перекрытие другим элементом (например, спиннером) и opacity: 0 не проверяются.
  • checkRender, getAmount, checkAmount и checkGTEAmount учитывают и скрытые элементы. Пустой список селекторов не проходит; число элементов можно передать строкой, например из data-* атрибута, но пустая строка или отсутствующее значение — не ноль, и проверка с ними не проходит.

Санитизация

Перед отправкой каждый бикон проходит через встроенные санитайзеры:

  • URL страницы (meta.page.url), атрибуты событий, значение которых целиком — http(s)-URL (ресурсы, нарушения CSP, навигация, ваши события), и файлы во фреймах стектрейса ошибок сокращаются до хоста и пути в нижнем регистре, а идентификаторы в пути (UUID любой версии, hex-строки от 12 символов, числа от 6 цифр, в том числе через _) заменяются на :id: https://shop.example.com/Orders/0190a6e2-7c3b-7d4e-9f00-1a2b3c4d5e6f?token=abc → shop.example.com/orders/:id.
  • В OTLP-транспорте обёртки метки метрик превращаются обратно в объект, чтобы в Grafana они были отдельными атрибутами, а не JSON-строкой. Ваши transports получают метки в формате Faro — JSON-строкой.

Не трогаются: текст логов и ошибок, контекст логов и измерений, имя и домен события, URL внутри строки или вложенного объекта, meta.user, meta.view, атрибуты сессии. Их чистите своим санитайзером.

Свой санитайзер получает полную копию бикона (TransportItem из Faro) — её можно менять на месте — и обязан вернуть бикон; санитайзеры выполняются по порядку после встроенных. Ваш beforeSend вызывается последним и получает уже очищенный бикон; чтобы отбросить бикон, верните null. Если санитайзер или beforeSend бросает исключение, а санитайзер — ничего не возвращает, не отправляется только этот бикон, а в консоль выводится одно предупреждение (снова — после destroy() и init()).

faroService.addSanitizer((beacon) => {
  delete beacon.meta.user?.email;
  return beacon;
});

Те же правила доступны как функции: sanitizeUrl(url) возвращает хост и очищенный путь (для строки, которая не разбирается как URL, — очищенную часть до ? и #), sanitizePath(pathname) — очищенный путь для строки без хоста, например для meta.view.name из роутера.

API

| FaroService | Описание | | -------------------------- | ---------------------------------------------------------------- | | init(config): Faro | Инициализирует Faro или снимает его с паузы после destroy() | | destroy() | Ставит Faro на паузу и сбрасывает пользовательские санитайзеры | | sendMetric(metric) | Отправляет метрику | | trackSlo(config) | Запускает SLO-прогон и возвращает SloTracker, не бросает | | addSanitizer(fn \| fn[]) | Добавляет санитайзеры после встроенных; не функция — TypeError | | getInstance(): Faro | Возвращает инстанс Faro; вне init()…destroy() бросает ошибку | | isInitialized | true между init() и destroy() |

config (тип FaroServiceConfig) — BrowserConfig из Faro плюс faroUrl, faroKey и необязательные routerAdapter и enabled. Вне init()…destroy() sendMetric и завершившийся trackSlo не отправляют метрику, а выводят предупреждение.

Кроме хелперов и санитайзеров пакет реэкспортирует из Faro getWebInstrumentations и TransportItemType — для страниц без сборщика. Типы: FaroServiceConfig, FaroConfig, Sanitizer, Metric, MetricResult, MetricUnit, MetricType, MetricLabels, SloConfig, SloTracker, SloRunState, StepCheck, StepConfig; SloRunOptions, SloRunResult и StepResults устарели и уйдут в 2.0.

Миграция с 0.x

| Было | Стало | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | new MetricsService(faro).sendCustomMetric(m) | faroService.sendMetric(m); CustomMetricBase → Metric: метки плоские, description необязателен, поля status нет, value и buckets — только числа | | MetricsCollector, addMetricStep, условия готовности, getStatus(), reset() | faroService.trackSlo({ name, failTime, steps }), шаг — предикат до true, dispose() | | MetricFn, ReadyToCheckConditionFn, MetricsCollector* | StepCheck, StepConfig, SloConfig, SloTracker | | paused в конфигурации | enabled: false; полей url и apiKey больше нет | | unpkg и jsdelivr без пути отдавали dist/index.umd.js без Faro | отдают dist/index.umd.full.js с Faro внутри — отдельно Faro не подключайте |

Старый addMetricStep(name, check, ready) молча отбрасывал лишние аргументы — при переносе на trackSlo объедините все проверки шага в одном предикате. Условие if (!params.duration) return перед отправкой отбросит метрики с нулевой длительностью, в trackSlo оно не нужно.

Разработка

Как предложить изменение — в CONTRIBUTING.md; об уязвимостях сообщайте приватно, по SECURITY.md.

Нужен Node 22.18+ (конфиги сборки и тестов — TypeScript без загрузчика). Это требование только к разработке в репозитории, на установку пакета оно не влияет.

npm install
npm run dev                  # сборка всех бандлов и типов в watch-режиме
npm run check                # формат, линт, типы, тесты, сборка, запуск бандлов, примеры README
npm run verify:peers-floor   # типы и тесты на нижней границе peerDependencies
npm run verify:browsers      # хелперы на настоящих картинках в Chromium, Firefox и WebKit

Релиз — по git-тегу v*: npm version <patch|minor|major>, затем git push --follow-tags. Воркфлоу Release прогоняет check, публикует пакет в npm через Trusted Publishing и создаёт GitHub Release. Версии с суффиксом (1.1.0-beta.1) уходят под dist-tag next.

Лицензия

Apache-2.0