Логирование и мониторинг

TanStack Query активно управляет жизненным циклом запросов, кешем, повторными попытками, фоновыми обновлениями и мутациями. При росте приложения количество сетевых операций увеличивается, а вместе с ними усложняется диагностика проблем:

  • запросы начинают выполняться слишком часто;
  • кеш неожиданно инвалидируется;
  • мутации вызывают каскадные обновления;
  • фоновые refetch-запросы перегружают API;
  • ошибки появляются только в production;
  • некоторые запросы зацикливаются;
  • stale-данные отображаются дольше ожидаемого.

Логирование и мониторинг позволяют:

  • отслеживать жизненный цикл запросов;
  • анализировать производительность;
  • находить лишние refetch;
  • контролировать ошибки API;
  • собирать telemetry-данные;
  • интегрировать приложение с системами наблюдаемости;
  • строить DevOps-мониторинг frontend-части.

Встроенный logger в TanStack Query

TanStack Query содержит внутренний механизм логирования, используемый для отображения ошибок и предупреждений.

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

Тем не менее концепция логирования остаётся важной.


Логирование ошибок запросов

Самый распространённый сценарий — централизованный сбор ошибок запросов.

Пример:

import {
  QueryClient,
  QueryClientProvider
} from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error) => {
        console.error('Ошибка запроса:', error)
      }
    }
  }
})

Теперь каждая ошибка запроса будет автоматически логироваться.


Глобальное логирование мутаций

Ошибки мутаций зачастую критичнее ошибок чтения данных.

Пример:

const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      onError: (error) => {
        console.error('Ошибка мутации:', error)
      }
    }
  }
})

Это особенно важно для:

  • платежей;
  • отправки форм;
  • обновления профиля;
  • удаления данных;
  • административных операций.

Централизованный обработчик telemetry

В production-приложениях console.error почти не используется напрямую. Вместо этого данные отправляются в telemetry-системы.

Пример:

function reportError(error, context) {
  fetch('/telemetry', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      message: error.message,
      stack: error.stack,
      context,
      timestamp: Date.now()
    })
  })
}

Интеграция:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error) => {
        reportError(error, {
          type: 'query'
        })
      }
    },
    mutations: {
      onError: (error) => {
        reportError(error, {
          type: 'mutation'
        })
      }
    }
  }
})

Логирование queryKey

Одной из главных проблем больших приложений становится сложность понимания, какой именно запрос завершился ошибкой.

Поэтому рекомендуется логировать queryKey.

Пример:

useQuery({
  queryKey: ['users', userId],
  queryFn: fetchUser,
  onError: (error) => {
    console.error('Ошибка запроса', {
      queryKey: ['users', userId],
      error
    })
  }
})

Логирование времени выполнения запроса

Производительность API напрямую влияет на UX.

TanStack Query не измеряет длительность запроса автоматически, но это легко реализуется вручную.

Пример:

async function fetchUsers() {
  const startedAt = performance.now()

  const response = await fetch('/api/users')

  const data = await response.json()

  const finishedAt = performance.now()

  console.log('Время запроса:', finishedAt - startedAt)

  return data
}

Мониторинг медленных запросов

Медленные запросы можно автоматически выделять.

Пример:

async function monitoredFetch(url) {
  const startedAt = performance.now()

  const response = await fetch(url)

  const duration = performance.now() - startedAt

  if (duration > 1000) {
    console.warn('Медленный запрос', {
      url,
      duration
    })
  }

  return response.json()
}

Логирование retry

TanStack Query активно использует retry-механизм.

При нестабильном API важно понимать:

  • сколько retry происходит;
  • какие запросы повторяются чаще всего;
  • какие endpoints нестабильны.

Пример:

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  retry: 3,
  retryDelay: (attempt) => {
    console.log('Retry попытка:', attempt)

    return attempt * 1000
  }
})

Мониторинг фоновых refetch

TanStack Query может автоматически обновлять данные:

  • при фокусе окна;
  • при reconnect;
  • по таймеру;
  • при invalidation.

Это иногда вызывает незаметную перегрузку API.

Для диагностики полезно логировать refetch.

Пример:

useQuery({
  queryKey: ['stats'],
  queryFn: async () => {
    console.log('Выполняется refetch')

    return fetchStats()
  },
  refetchInterval: 5000
})

Подписка на QueryCache

Одним из самых мощных механизмов мониторинга является подписка на QueryCache.

Пример:

const queryClient = new QueryClient()

queryClient.getQueryCache().subscribe((event) => {
  console.log('Событие QueryCache:', event)
})

Через события кеша можно отслеживать:

  • создание запросов;
  • обновление состояния;
  • удаление кеша;
  • invalidation;
  • refetch;
  • ошибки;
  • переходы между состояниями.

Структура событий QueryCache

Событие содержит подробную информацию:

{
  type,
  query
}

Объект query включает:

query.queryKey
query.state.status
query.state.fetchStatus
query.state.data
query.state.error

Пример анализа:

queryClient.getQueryCache().subscribe((event) => {
  if (event.type === 'updated') {
    console.log({
      key: event.query.queryKey,
      status: event.query.state.status,
      fetchStatus: event.query.state.fetchStatus
    })
  }
})

Подписка на MutationCache

Мутации также поддерживают централизованный мониторинг.

Пример:

queryClient.getMutationCache().subscribe((event) => {
  console.log('Mutation event:', event)
})

Это особенно полезно для:

  • аудита действий;
  • логирования изменений;
  • аналитики;
  • мониторинга бизнес-операций.

Отслеживание количества запросов

Иногда важно понимать общую нагрузку frontend-приложения.

Пример:

const queries = queryClient.getQueryCache().getAll()

console.log('Количество запросов:', queries.length)

Мониторинг активных запросов

Активные запросы можно анализировать отдельно.

Пример:

const fetchingCount = queryClient.isFetching()

console.log('Активных запросов:', fetchingCount)

Для мутаций:

const mutatingCount = queryClient.isMutating()

console.log('Активных мутаций:', mutatingCount)

React Query Devtools

Для разработки TanStack Query предоставляет мощные Devtools.

Основные возможности:

  • просмотр всех запросов;
  • анализ queryKey;
  • просмотр stale/fresh состояния;
  • анализ времени жизни кеша;
  • ручной refetch;
  • invalidation;
  • просмотр observer-ов;
  • анализ retry;
  • отслеживание background fetch;
  • просмотр ошибок.

Подключение:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function App() {
  return (
    <>
      <Application />

      <ReactQueryDevtools initialIsOpen={false} />
    </>
  )
}

Анализ состояний запросов

Каждый запрос имеет несколько ключевых состояний:

status

Возможные значения:

pending
success
error

Дополнительно:

fetchStatus

Возможные значения:

idle
fetching
paused

Мониторинг этих состояний помогает находить:

  • подвисшие запросы;
  • сетевые проблемы;
  • offline-сценарии;
  • бесконечные refetch.

Логирование stale-состояния

TanStack Query строится вокруг концепции stale/fresh данных.

Полезно отслеживать момент устаревания данных.

Пример:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  staleTime: 5000,
  onSuccess: () => {
    console.log('Данные обновлены')
  }
})

Интеграция с Sentry

Sentry — одна из самых популярных систем мониторинга ошибок frontend-приложений.

Пример интеграции:

import * as Sentry from '@sentry/react'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      onError: (error) => {
        Sentry.captureException(error)
      }
    }
  }
})

Расширенный вариант:

onError: (error, query) => {
  Sentry.captureException(error, {
    tags: {
      queryKey: JSON.stringify(query.queryKey)
    }
  })
}

Интеграция с Datadog

Datadog позволяет собирать frontend telemetry.

Пример:

import { datadogLogs } from '@datadog/browser-logs'

function logQueryError(error, queryKey) {
  datadogLogs.logger.error('Query error', {
    queryKey,
    message: error.message
  })
}

Интеграция с OpenTelemetry

Современные distributed-системы часто используют OpenTelemetry.

Пример:

function traceRequest(queryKey, duration) {
  console.log('trace', {
    queryKey,
    duration
  })
}

Интеграция с queryFn:

async function fetchData() {
  const startedAt = performance.now()

  const response = await fetch('/api/data')

  const duration = performance.now() - startedAt

  traceRequest(['data'], duration)

  return response.json()
}

Корреляция frontend и backend логов

Крупные системы часто используют request-id.

Frontend генерирует идентификатор:

const requestId = crypto.randomUUID()

И передаёт его в заголовках:

fetch('/api/users', {
  headers: {
    'X-Request-Id': requestId
  }
})

Backend сохраняет тот же идентификатор в логах.

Это позволяет:

  • связывать frontend-ошибки с backend-логами;
  • анализировать цепочку запросов;
  • отслеживать деградацию производительности.

Логирование offline-сценариев

TanStack Query поддерживает offline-first архитектуру.

Мониторинг offline-режима критически важен.

Пример:

window.addEventListener('offline', () => {
  console.warn('Приложение offline')
})

window.addEventListener('online', () => {
  console.log('Соединение восстановлено')
})

Мониторинг memory usage

Избыточный кеш способен вызывать утечки памяти.

Полезно отслеживать:

  • количество query;
  • размер данных;
  • время жизни кеша;
  • частоту invalidation.

Пример:

const queries = queryClient.getQueryCache().getAll()

queries.forEach((query) => {
  console.log({
    key: query.queryKey,
    updatedAt: query.state.dataUpdatedAt
  })
})

Мониторинг invalidation

Чрезмерная invalidation — одна из главных причин лишней сетевой активности.

Пример логирования:

function invalidateUsers() {
  console.log('Invalidate users')

  queryClient.invalidateQueries({
    queryKey: ['users']
  })
}

Мониторинг optimistic updates

Optimistic update может приводить к сложным race condition.

Пример логирования:

useMutation({
  mutationFn: updatePost,

  onMutate: async (variables) => {
    console.log('Optimistic update', variables)
  },

  onError: (error) => {
    console.error('Rollback optimistic update', error)
  }
})

Мониторинг hydration

В SSR-приложениях полезно анализировать hydration-процесс.

Пример:

console.log('Hydration started')

После восстановления кеша:

console.log('Hydration finished')

Это помогает находить:

  • двойные запросы;
  • рассинхронизацию SSR/CSR;
  • лишние refetch после hydration.

Production-логирование

В production логирование должно быть:

  • централизованным;
  • структурированным;
  • минимально шумным;
  • безопасным;
  • асинхронным.

Плохой пример:

console.log(error)

Хороший пример:

logger.error({
  type: 'query_error',
  queryKey,
  message: error.message,
  timestamp: Date.now()
})

Структурированные логи

Структурированные JSON-логи значительно удобнее для аналитики.

Пример:

{
  type: 'query_error',
  queryKey: ['users'],
  duration: 1200,
  retryCount: 2,
  timestamp: 1716540000
}

Такие логи легко анализируются:

  • ELK Stack;
  • Grafana;
  • Loki;
  • Datadog;
  • Splunk;
  • OpenSearch.

Метрики frontend-приложения

Мониторинг TanStack Query часто включает сбор метрик:

  • среднее время запроса;
  • количество ошибок;
  • retry rate;
  • cache hit rate;
  • число refetch;
  • объём трафика;
  • количество активных query;
  • latency API.

Пример простого счётчика ошибок:

let errorCount = 0

function trackError() {
  errorCount++
}

Мониторинг cache hit rate

Высокий cache hit rate означает эффективную работу кеша.

Низкий показатель может говорить о:

  • слишком частой invalidation;
  • нестабильных queryKey;
  • неправильной архитектуре кеширования.

Пример примитивного анализа:

function logCacheState(query) {
  console.log({
    key: query.queryKey,
    hasData: !!query.state.data
  })
}

Devtools в production

Иногда Devtools подключаются и в production-среде, но только для администраторов или debug-режима.

Пример:

{
  process.env.NODE_ENV === 'development' && (
    <ReactQueryDevtools />
  )
}

Либо:

const isDebugEnabled =
  localStorage.getItem('debug') === 'true'

Типичные проблемы мониторинга

Избыточное логирование

Слишком большое количество логов:

  • ухудшает производительность;
  • перегружает telemetry;
  • усложняет диагностику.

Логирование персональных данных

Нельзя логировать:

  • токены;
  • пароли;
  • банковские данные;
  • cookies;
  • access token;
  • refresh token.

Логирование больших payload

Огромные JSON-ответы способны:

  • переполнять telemetry;
  • замедлять приложение;
  • увеличивать стоимость хранения логов.

Дублирование ошибок

Одна и та же ошибка может логироваться:

  • queryFn;
  • onError;
  • Error Boundary;
  • Sentry;
  • глобальным interceptor.

Это создаёт шум и затрудняет анализ.


Архитектура production-мониторинга

Крупные frontend-приложения обычно строят следующую схему:

  1. TanStack Query генерирует события.
  2. События проходят через централизованный logger.
  3. Logger фильтрует шум.
  4. Telemetry отправляется батчами.
  5. Ошибки агрегируются.
  6. Метрики визуализируются в dashboard.
  7. Алерты уведомляют о деградации API.

Такая архитектура позволяет превратить TanStack Query из обычной библиотеки управления серверным состоянием в полноценный источник observability-данных frontend-приложения.