Мониторинг и метрики

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

  • анализировать частоту запросов;
  • контролировать повторные обращения к API;
  • обнаруживать деградацию производительности;
  • отслеживать ошибки сети;
  • измерять эффективность кэширования;
  • выявлять утечки памяти и лишние подписки;
  • оценивать нагрузку на backend;
  • контролировать поведение polling-механизмов;
  • анализировать invalidation-процессы;
  • диагностировать race conditions.

RTK Query построен поверх Redux Toolkit, поэтому все операции отражаются через Redux Store и могут быть проанализированы средствами middleware, Redux DevTools и кастомных инструментов логирования.


Архитектура состояния RTK Query

Все данные RTK Query хранятся внутри reducer, зарегистрированного через createApi.

Пример:

import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'

export const store = configureStore({
    reducer: {
        [api.reducerPath]: api.reducer
    },
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(api.middleware)
})

Состояние RTK Query содержит несколько ключевых разделов:

state.api = {
    queries: {},
    mutations: {},
    provided: {},
    subscriptions: {},
    config: {}
}

Каждый раздел участвует в мониторинге поведения системы.


Анализ состояния queries

Раздел queries содержит информацию обо всех query-запросах.

Пример структуры:

queries: {
    'getUsers(undefined)': {
        status: 'fulfilled',
        endpointName: 'getUsers',
        requestId: 'abc123',
        startedTimeStamp: 171000000,
        fulfilledTimeStamp: 171000300,
        data: [...],
        error: undefined
    }
}

Ключевые поля:

Поле Назначение
status Состояние запроса
endpointName Имя endpoint
requestId Идентификатор запроса
startedTimeStamp Время начала
fulfilledTimeStamp Время завершения
data Кэшированные данные
error Информация об ошибке

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

RTK Query автоматически сохраняет временные метки начала и завершения запросов.

Расчет длительности:

const query =
    store.getState().api.queries['getUsers(undefined)']

const duration =
    query.fulfilledTimeStamp - query.startedTimeStamp

console.log(duration)

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

  • строить метрики latency;
  • анализировать медленные endpoint;
  • выявлять сетевые проблемы;
  • сравнивать производительность backend-сервисов.

Middleware для мониторинга RTK Query

Наиболее гибкий способ сбора метрик — кастомный middleware.

Пример middleware:

const monitoringMiddleware =
    (store) => (next) => (action) => {

        const result = next(action)

        if (action.type.endsWith('/fulfilled')) {
            console.log('SUCCESS:', action.type)
        }

        if (action.type.endsWith('/rejected')) {
            console.log('ERROR:', action.type)
        }

        return result
    }

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

middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware()
        .concat(api.middleware)
        .concat(monitoringMiddleware)

Анализ lifecycle action

RTK Query генерирует большое количество action:

Action Назначение
executeQuery/pending Начало запроса
executeQuery/fulfilled Успешный ответ
executeQuery/rejected Ошибка
invalidateTags Инвалидация кэша
removeQueryResult Очистка
subscriptionsUpdated Изменение подписок

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

const monitoringMiddleware =
    () => (next) => (action) => {

        if (action.type.includes('executeQuery')) {
            console.log(action)
        }

        return next(action)
    }

Сбор метрик успешности запросов

Часто требуется анализировать процент успешных запросов.

Пример счетчиков:

const metrics = {
    success: 0,
    failed: 0
}

const monitoringMiddleware =
    () => (next) => (action) => {

        if (action.type.endsWith('/fulfilled')) {
            metrics.success++
        }

        if (action.type.endsWith('/rejected')) {
            metrics.failed++
        }

        return next(action)
    }

Расчет error rate:

const errorRate =
    metrics.failed /
    (metrics.success + metrics.failed)

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

RTK Query позволяет выявлять избыточные сетевые обращения.

Пример:

const requests = {}

const monitoringMiddleware =
    () => (next) => (action) => {

        if (action.type.endsWith('/pending')) {

            const endpoint =
                action.meta?.arg?.endpointName

            requests[endpoint] =
                (requests[endpoint] || 0) + 1
        }

        return next(action)
    }

Это помогает обнаружить:

  • бесконечные refetch;
  • дублирование запросов;
  • ошибки invalidateTags;
  • неправильные polling-настройки.

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

RTK Query активно использует кэширование.

Эффективность кэша — важная метрика производительности.

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

const state = store.getState()

const queries =
    Object.values(state.api.queries)

const cachedQueries =
    queries.filter(
        query => query.status === 'fulfilled'
    )

console.log(cachedQueries.length)

Анализ invalidation

Неверная invalidation-стратегия способна создавать огромную нагрузку на API.

Пример:

invalidatesTags: ['Users']

Если тег инвалидируется слишком часто:

  • возникает cascade refetch;
  • увеличивается network traffic;
  • растет latency интерфейса;
  • backend получает лишнюю нагрузку.

Мониторинг invalidation:

const monitoringMiddleware =
    () => (next) => (action) => {

        if (action.type.includes('invalidateTags')) {
            console.log('INVALIDATION:', action)
        }

        return next(action)
    }

Отслеживание polling

Polling может быть серьезным источником нагрузки.

Пример:

useGetUsersQuery(undefined, {
    pollingInterval: 5000
})

При большом количестве вкладок:

  • нагрузка многократно возрастает;
  • увеличивается расход памяти;
  • backend получает burst traffic.

Мониторинг polling:

const pollingStats = {}

const middleware =
    () => (next) => (action) => {

        if (action.type.endsWith('/pending')) {

            const endpoint =
                action.meta.arg.endpointName

            pollingStats[endpoint] =
                (pollingStats[endpoint] || 0) + 1
        }

        return next(action)
    }

Контроль подписок

RTK Query отслеживает активные subscriptions.

Пример состояния:

subscriptions: {
    'getUsers(undefined)': {
        'component-id-1': {},
        'component-id-2': {}
    }
}

Это позволяет анализировать:

  • количество активных компонентов;
  • утечки подписок;
  • дублирование hook;
  • проблемы unmount.

Диагностика утечек памяти

Одна из распространенных проблем — накопление query cache.

Контроль количества query:

const state = store.getState()

const queryCount =
    Object.keys(state.api.queries).length

console.log(queryCount)

Если количество запросов постоянно растет — вероятны:

  • некорректные query аргументы;
  • отсутствие cleanup;
  • генерация динамических ключей;
  • ошибки сериализации.

Мониторинг сериализации аргументов

RTK Query строит cache key на основе аргументов.

Проблемный пример:

useGetUserQuery({
    id: 1,
    timestamp: Date.now()
})

Каждый вызов создает новый cache key.

Последствия:

  • рост памяти;
  • постоянные refetch;
  • отсутствие cache hit;
  • деградация производительности.

Диагностика:

const queries =
    Object.keys(store.getState().api.queries)

console.log(queries)

Мониторинг retry-механизмов

RTK Query поддерживает retry через wrapper.

Пример:

import {
    retry,
    fetchBaseQuery
} from '@reduxjs/toolkit/query/react'

const baseQuery = retry(
    fetchBaseQuery({
        baseUrl: '/api'
    }),
    {
        maxRetries: 5
    }
)

Мониторинг retry:

const middleware =
    () => (next) => (action) => {

        if (action.meta?.baseQueryMeta) {
            console.log(action.meta.baseQueryMeta)
        }

        return next(action)
    }

Анализ ошибок API

RTK Query предоставляет подробную информацию об ошибках.

Пример:

const { error } = useGetUsersQuery()

Структура:

{
    status: 500,
    data: {
        message: 'Internal Server Error'
    }
}

Централизованный мониторинг:

const middleware =
    () => (next) => (action) => {

        if (action.type.endsWith('/rejected')) {

            console.error({
                endpoint:
                    action.meta.arg.endpointName,

                error:
                    action.error
            })
        }

        return next(action)
    }

Интеграция с системами логирования

RTK Query удобно интегрируется с:

  • Sentry;
  • Datadog;
  • LogRocket;
  • New Relic;
  • Grafana;
  • Elastic Stack.

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

import * as Sentry from '@sentry/react'

const middleware =
    () => (next) => (action) => {

        if (action.type.endsWith('/rejected')) {

            Sentry.captureException(action.error)
        }

        return next(action)
    }

Performance monitoring

RTK Query позволяет анализировать frontend latency.

Пример измерения:

const middleware =
    () => (next) => (action) => {

        if (action.type.endsWith('/fulfilled')) {

            const started =
                action.meta.startedTimeStamp

            const finished =
                Date.now()

            console.log(
                finished - started
            )
        }

        return next(action)
    }

Метрики cache lifetime

RTK Query автоматически удаляет неиспользуемые данные.

Пример:

keepUnusedDataFor: 60

Мониторинг времени жизни:

const state = store.getState()

for (const query of Object.values(state.api.queries)) {

    console.log({
        endpoint: query.endpointName,
        started: query.startedTimeStamp
    })
}

Анализ refetch поведения

RTK Query поддерживает:

  • refetchOnMountOrArgChange;
  • refetchOnFocus;
  • refetchOnReconnect.

Пример:

useGetUsersQuery(undefined, {
    refetchOnFocus: true
})

При большом количестве вкладок возможны всплески трафика.

Мониторинг:

const middleware =
    () => (next) => (action) => {

        if (
            action.type.includes('refetch')
        ) {
            console.log(action)
        }

        return next(action)
    }

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

Optimistic update может вызывать рассинхронизацию данных.

Пример:

onQueryStarted(id, { dispatch, queryFulfilled }) {

    const patch =
        dispatch(
            api.util.updateQueryData(
                'getUsers',
                undefined,
                draft => {
                    draft.push({
                        id
                    })
                }
            )
        )

    try {
        await queryFulfilled
    } catch {
        patch.undo()
    }
}

Мониторинг rollback:

catch (error) {
    console.error('ROLLBACK', error)
}

Отслеживание активности endpoint

Иногда требуется определить наиболее нагруженные endpoint.

Пример агрегатора:

const endpointStats = {}

const middleware =
    () => (next) => (action) => {

        const endpoint =
            action.meta?.arg?.endpointName

        if (endpoint) {

            endpointStats[endpoint] =
                (endpointStats[endpoint] || 0) + 1
        }

        return next(action)
    }

Создание панели метрик

Можно построить внутреннюю dashboard-систему.

Пример структуры:

const metrics = {
    totalRequests: 0,
    failedRequests: 0,
    avgResponseTime: 0,
    cacheHits: 0,
    cacheMisses: 0
}

Обновление метрик:

const middleware =
    () => (next) => (action) => {

        if (action.type.endsWith('/fulfilled')) {
            metrics.totalRequests++
        }

        if (action.type.endsWith('/rejected')) {
            metrics.failedRequests++
        }

        return next(action)
    }

Redux DevTools как инструмент мониторинга

Redux DevTools позволяет:

  • анализировать action;
  • исследовать состояние cache;
  • просматривать lifecycle запросов;
  • искать лишние invalidate;
  • отслеживать polling;
  • анализировать mutations.

Особенно полезен анализ последовательностей:

pending
fulfilled
invalidateTags
refetch
fulfilled

Подобные цепочки позволяют выявлять проблемы архитектуры API.


Метрики производительности интерфейса

RTK Query влияет не только на сеть, но и на render performance.

Проблемы:

  • лишние рендеры;
  • частые refetch;
  • огромные payload;
  • избыточные subscriptions.

Мониторинг render count:

const renders = useRef(0)

renders.current++

console.log(renders.current)

Профилирование selector

RTK Query использует memoized selector.

Плохая практика:

const result = useGetUsersQuery()

const users =
    result.data?.map(user => ({
        ...user
    }))

Каждый render создает новые ссылки.

Лучше:

const { users } =
    api.useGetUsersQuery(undefined, {
        selectFromResult: ({ data }) => ({
            users: data
        })
    })

Мониторинг network burst

Особенно опасны ситуации массового refetch.

Пример сценария:

invalidatesTags: ['Users']

При обновлении одного пользователя:

  • инвалидируются все queries;
  • запускается cascade refetch;
  • создается network burst.

Мониторинг burst:

const requests = []

const middleware =
    () => (next) => (action) => {

        if (action.type.endsWith('/pending')) {

            requests.push(Date.now())
        }

        return next(action)
    }

Сбор telemetry

Крупные приложения часто отправляют telemetry на backend.

Пример структуры события:

{
    endpoint: 'getUsers',
    duration: 230,
    success: true,
    timestamp: Date.now()
}

Отправка:

fetch('/metrics', {
    method: 'POST',
    body: JSON.stringify(metrics)
})

Мониторинг offline/reconnect сценариев

RTK Query умеет автоматически refetch после reconnect.

Пример:

setupListeners(store.dispatch)

Мониторинг reconnect:

window.addEventListener('online', () => {
    console.log('ONLINE')
})

Анализ нагрузки на backend

RTK Query способен незаметно перегружать API при:

  • неправильных pollingInterval;
  • агрессивном invalidateTags;
  • частом refetchOnFocus;
  • нестабильных query args;
  • массовых optimistic update.

Мониторинг позволяет выявлять:

  • пиковые нагрузки;
  • каскадные refetch;
  • избыточные запросы;
  • неэффективный cache reuse;
  • проблемные endpoint;
  • узкие места backend-инфраструктуры.