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

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

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

Иногда ошибки в клиентском коде не так очевидны, как когда вы получаете пустой экран из-за того, что ни одна строка вашего 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», но хотя она и показывает выполняемые запросы, в ней нет подробностей о них. Даже если вы кликнете по одному из них, вы получите минимум доступных подробностей:

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

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

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 Очистка данных запроса

Если вы вернётесь к началу этого руководства, к тому моменту, когда я настраивал сетевые параметры, вы увидите, что я ничего не говорил об очистке. Однако в составе этих параметров вы можете указать колбэк, предназначенный для очистки данных. Этот колбэк получает единственный атрибут, содержащий как объект запроса, так и объект ответа. Затем вы можете отредактировать их как угодно; они не повлияют на сам запрос, но изменят то, как данные отображаются в интерфейсе 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 и задайте вопрос нашим разработчикам напрямую!