Захват и очистка данных запросов

Захватывайте данные запросов, чтобы понять, какие ошибки вы допускаете на фронтенде

Захват и очистка данных запросов

Иногда ошибки в вашем клиентском коде не так очевидны, как когда вы получаете пустой экран из-за того, что весь ваш JavaScript-код не работает. Иногда проблема с вашим приложением заключается в том, что при определённых действиях, которые может совершить ваш пользователь, вы некорректно формируете запрос к серверу. Клиентский код работает, вы не получаете никаких ошибок JS в консоли, но вашему бэкенду не очень нравится то, что вы ему отправляете.

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

Пример приложения

Section titled Пример приложения

Для целей этого руководства я создал простое React-приложение, которое использует Bored API. Это очень простой API, который возвращает случайное предложение занятия на основе нескольких параметров. Итак, я создал приложение «I’m bored App», которое выглядит так:

И вы можете найти его вживую на Netlify здесь, а если вы хотите ознакомиться с кодом и рассмотреть его подробнее, он полностью доступен на GitHub.

Это приложение состоит из 2 компонентов: компонент SearchForm, который отвечает за отрисовку этих 2 полей и кнопки, а также за отправку самого запроса к API. А компонент Suggestion просто отрисовывает предложение внутри красивого блока.

Я сосредоточусь на первом, поскольку это единственный компонент, отправляющий запросы с помощью функции fetch.

Обратите внимание, что этот приём также работает для любого запроса, выполняемого с помощью Axios.

Давайте бегло взглянем на компонент, чтобы понять, что он делает.

Код компонента SearchForm

Section titled Код компонента SearchForm

Это не сложный компонент, но есть один раздел, особенно важный для этого конкретного случая использования, поэтому давайте бегло на него взглянем.

import { Container, Col, Form, Row, Button } from 'react-bootstrap';

const SearchForm = ({setResult, fetcher}) => {
const getSomething = async (evt) => {
    evt.preventDefault()
    let form = evt.target

    const API_URL = "/api/activity?"
    let getParams = {}
    if(form.participants.value !== '') {
      getParams.participants = form.participants.value
    }

    if(form.priceRange.value !== '') {
      let prices = form.priceRange.value.split("_")
      getParams.minprice = prices[0]
      getParams.maxprice = prices[1]
    }

    let results = await fetcher(API_URL + new URLSearchParams(getParams), {
        mode: 'no-cors'
    })
    setResult(await results.json())

    return false
  }

    return (
        <Container>
        <Form onSubmit={getSomething}>
          <Row>
            <Col>
          <Form.Group controlId='participants' >
            <Form.Label>Participants</Form.Label>
            <Form.Control type='text' name="totalParticipants" placeholder='Leave empty if you dont care...'></Form.Control>
          </Form.Group>
          </Col>
            <Col>
          <Form.Group controlId='priceRangeId'>
            <Form.Label>Price range</Form.Label>
            <Form.Select name="priceRange" >
              <option value="" >Select one or leave empty if you dont care</option>
              <option value="0.0">Free</option>
              <option value="0.1_0.5">Cheap</option>
              <option value="0.6_1.0">Expensive</option>
            </Form.Select>
          </Form.Group>
          </Col>
          </Row>
          <Row className='m-3'>
            <Col>
            <Form.Group>
              <Button variant="primary" type="submit">Get me something!</Button>
            </Form.Group>
            </Col>
          </Row>
        </Form>
      </Container>
    )
}

export default SearchForm

Обратите внимание на функцию getSomething — именно здесь происходит большая часть магии. Функция вызывается при срабатывании события submit формы. Когда это происходит, функция получает синтетическое событие с привязанной формой внутри свойства target. Мы просто захватываем значения каждого из фильтров (поля ввода и выпадающего списка), а затем выполняем запрос с помощью функции fetch. Обратите внимание, что URL не нацелен напрямую на эндпоинт BoredAPI. Это потому, что для того, чтобы запрос работал и не блокировался из-за ограничений CORS, я настроил на бэкенде прокси для перенаправления всех запросов с /api на реальный API.

Теперь, когда вы увидели код, давайте посмотрим, что бы вы получили, если бы установили трекер OpenReplay без плагина fetch.

Обычный захват данных с OpenReplay

Section titled Обычный захват данных с OpenReplay

Для этого примера я буду использовать NPM-версию пакета; если вы не знаете, как это сделать, ознакомьтесь с документацией, а затем возвращайтесь сюда.

Это интерфейс реплея сессии по умолчанию. Обратите внимание, что в нижней половине я уже выбрал вкладку «Network», но хотя она и показывает выполняемые запросы, никаких подробностей о них нет. Даже если вы нажмёте на один из них, вы получите минимум доступных деталей:

Так что же мы можем сделать? Вы можете включить захват информации о запросах с помощью объекта Network options. Давайте рассмотрим это.

Захват данных запросов в ваших реплеях сессий

Section titled Захват данных запросов в ваших реплеях сессий

Для этого всё, что нам нужно сделать, — это добавить параметр конфигурации при инстанцировании трекера. Итак, теперь, когда вы пишете строку new tracker(...), вы добавите новое свойство:

import Tracker from '@openreplay/tracker';

const tracker = new Tracker({
  projectKey: "<your project key>",
  network: {
    capturePayload: true //start capturing the payload of every request
  }
});

Это всё, что нам нужно сделать; отныне каждый раз, когда вы выполняете запрос, данные будут записываться трекером. Теперь разверните изменение, протестируйте приложение, закройте вкладку и подождите пару минут. Сессия должна появиться достаточно скоро, и вы сможете нажать кнопку «play».

Изучение обмена данными между клиентом и сервером

Section titled Изучение обмена данными между клиентом и сервером

Для целей примера давайте также рассмотрим проблему, которую я начал замечать после публикации приложения.

Обратите внимание на окно с предупреждением, которое я получаю в этом случае:

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

Теперь посмотрите на интерфейс реплея сессии. Внутри вкладки Network вы можете увидеть запросы, которые мы делали к внешнему API.

Всё, что нам нужно сделать сейчас, — это найти момент, когда мы получаем ответ с ошибкой, и посмотреть на выполняемые запросы. Скорее всего, вы увидите проблему в деталях запроса. В нашем случае ошибка гласит «Failed to query due to error in arguments», что означает, что когда мы выбираем опцию «Free» в выпадающем списке, мы отправляем недействительный запрос. Итак, давайте взглянем на его детали.

Видите проблему? Позвольте мне помочь вам:

Да, я отправляю undefined в качестве значения атрибута maxprice. Я полностью упустил это в своей логике и обнаружил это при изучении запроса. Конечно, это лёгкое исправление теперь, когда я знаю, где находится проблема, но благодаря этому процессу я смог бы либо составить очень подробный отчёт об ошибке, либо напрямую помочь разработчику выявить и решить проблему без необходимости тестировать самому и воспроизводить инцидент.

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

Section titled Проверка приватности на прочность

Хорошо, давайте продвинем этот пример немного дальше; представим, что для этого запроса мне также нужен номер телефона моего пользователя. Очевидно, что он мне не нужен, но просто подыграйте мне минутку.

Я добавлю поле в форму и обновлю код, чтобы захватывать это значение и отправлять его как часть запроса.

HTML для формы — это просто добавление нового элемента Col вот так:

<!-- previous code -->
<Col>
    <Form.Group controlId='phoneNumber'>
        <Form.Label>Phone Number</Form.Label>
        <Form.Control type='number' name="phoneNumber" placeholder='Enter your phone number here please'></Form.Control>
    </Form.Group>
</Col>
<!-- rest of the code -->

А для добавления содержимого этого поля в сам запрос нужна всего одна строка кода:

getParams.phonenumber = form.phoneNumber.value

Итак, что произойдёт, если мы используем этот новый код и захватим сессию с помощью OpenReplay? Что ж, две вещи:

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

Следующий скриншот показывает то, что я только что описал:

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

И ответ, я рад сообщить, таков: ДА, можем.

Очистка данных запроса

Section titled Очистка данных запроса

Если вы вернётесь к началу этого руководства, когда я настраивал параметры network, вы увидите, что я ничего не говорил об очистке. Однако в составе этих параметров вы можете указать колбэк, предназначенный для очистки данных. Этот колбэк получает единственный атрибут, содержащий и объект запроса, и объект ответа. Затем вы можете редактировать их так, как захотите; они не повлияют на сам запрос, но изменят способ отображения данных в интерфейсе OpenReplay.

Например, предположим, что я хочу изменить атрибут «phonenumber» и удалить цифры, чтобы избежать утечки этой информации. Это можно сделать так:

const tracker = new Tracker({
  projectKey: "<your project id>",
  network: {
    capturePayload: true,
    sanitizer: (data) => { //we change the content of the "phonenumber" parameter from the url
      data.url = data.url.replace(/phonenumber=([0-9]+)/, "phonenumber=XXXXXX")
      return data
    }
    }
});

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

Теперь данные вашего пользователя снова в безопасности.

Если вы хотите ознакомиться с кодом, чтобы рассмотреть этот пример подробнее, вы можете найти его здесь, на GitHub. Если у вас возникнут какие-либо проблемы с настройкой плагина Fetch или самого Tracker, свяжитесь с нами в нашем сообществе Slack и задайте вопрос нашим разработчикам напрямую!