リクエストデータのキャプチャーとサニタイズ

リクエストデータをキャプチャーして、フロントエンドで発生しているミスの種類を把握します

リクエストデータのキャプチャーとサニタイズ

クライアントコードのエラーは、JavaScript コードがまったく動作せず画面が真っ白になる場合ほど明白とは限りません。 ユーザーが実行できる特定のアクションによって、サーバーへのリクエストを誤って組み立てていることが原因になる場合もあります。 クライアントコードは動作し、コンソールに JS エラーも表示されませんが、バックエンドは送信された内容を受け付けません。

OpenReplay を使用すると、クライアントとサーバー間の通信を標準のセッションリプレイの一部としてキャプチャーし、後から確認できます。それでは、その方法と、そこから得られるメリットを見ていきましょう。

このハウツーでは、Bored API を利用するシンプルな React アプリケーションを作成しました。これは、いくつかのパラメーターに基づいてランダムなアクティビティの提案を返す、非常にシンプルな 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 にリダイレクトするプロキシを設定しているためです。

コードを確認できたので、fetch プラグインなしで OpenReplay のトラッカーをインストールした場合に何が得られるかを見てみましょう。

OpenReplay による通常のデータキャプチャー

Section titled OpenReplay による通常のデータキャプチャー

この例では、パッケージの NPM 版を使用します。方法がわからない場合は、ドキュメントを確認してから、ここに戻ってきてください。

これがデフォルトのセッションリプレイの UI です。 下半分ですでに「Network」タブを選択していますが、行われているリクエストは表示されるものの、それらについての詳細は 表示されません。いずれかをクリックしても、得られる詳細は最小限のものだけです:

では、どうすればよいでしょうか。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 クライアントとサーバー間の通信を調査する

この例の目的のために、アプリケーションを公開した後に私が目にするようになった問題も見てみましょう。

この場合に表示される警告ボックスに注目してください。

これをコーディングした開発者である私は、テスト方法とバグの場所を把握する方法を知っています。しかし、ユーザーにとって、このエラーはあまり多くを語っておらず、開発チームが理解できる形でこれを伝えることもできないかもしれません。 そこで、ユーザーとしては、アプリが動作しないことを会社に伝えるだけで済みます。アプリケーションを担当する開発者であるあなたは、私のセッションを確認し、クライアントが送信したリクエストとサーバーからのレスポンスを調べることができます。

セッションリプレイの UI を見てみましょう。Network タブ内に、外部 API に対して行ったリクエストが表示されています。

あとは、エラーレスポンスが返ってきた瞬間を見つけて、送信されたリクエストを確認するだけです。 多くの場合、リクエストの詳細の中に問題が見つかります。 今回の場合、エラーは「Failed to query due to error in arguments」と表示されており、ドロップダウンで「Free」オプションを選択すると、 有効なリクエストが送信されていないことを意味します。では、その詳細を見てみましょう。

問題がおわかりでしょうか。ヒントをお見せします。

そうです、maxprice 属性の値として undefined を送信しています。ロジックでこれを完全に見落としており、リクエストを調べているときに気づきました。 問題の場所がわかった今では簡単に修正できますが、このプロセスのおかげで、非常に詳細なエラーレポートを提出したり、自分でテストして問題を再現することなく、開発者が問題を特定して解決するのを直接支援したりできたはずです。

では、この例をもう少し発展させて、このリクエストにユーザーの電話番号も必要だと仮定してみましょう。実際には必要ありませんが、しばらくお付き合いください。

フォームにフィールドを追加し、その値をキャプチャーしてリクエストの一部として送信するようにコードを更新します。

フォームの 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 -->

そして、このフィールドの内容を実際のリクエストに追加するには、1 行のコードだけで済みます。

getParams.phonenumber = form.phoneNumber.value

では、この新しいコードを使用して OpenReplay でセッションをキャプチャーすると、何が起こるでしょうか。 実は、2 つのことが起こります:

  1. 実際に 視聴する リプレイでは、電話番号フィールドの内容が自動的にサニタイズされ、視聴している誰にも表示されません。
  2. 一方、プラグインによってキャプチャーされたリクエスト情報には、その値が表示されます。

次のスクリーンショットは、今述べた内容を示しています。

画面の右側のセクションで、電話番号がそのまま表示されています。これは、通常のトラッカーは電話番号フィールドを数値フィールドとして理解できるものの、その数値が個人情報である可能性があるため、入力内容をキャプチャーしないからです。しかし、リクエスト側では、開発者がデータに対して何をしている可能性もあり、パラメーター名さえ自由なので、そのような前提を置くことはできません。 では、このプラグインでユーザーのプライバシーを保護できるのでしょうか。

そして答えは、嬉しいことに「はい、できます」です。

リクエストデータをサニタイズする

Section titled リクエストデータをサニタイズする

このチュートリアルの冒頭でネットワークオプションを設定したときに、サニタイズについて何も触れなかったことにお気づきでしょう。 しかし、それらのオプションの一部として、データをサニタイズするためのコールバックを指定できます。このコールバックは、リクエストとレスポンスの両方のオブジェクトを含む単一の引数を受け取ります。それらを自由に編集することができ、実際のリクエストには影響しませんが、OpenReplay の UI でのデータの表示方法が変わります。

たとえば、「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
    }
    }
});

ご覧のとおり、変更は簡単で、この属性内の数字だけを置き換えています。これで UI でのリクエストは次のようになります。

これで、ユーザーのデータは再び保護されました。

ご質問はありますか?

Section titled ご質問はありますか?

この例のコードを詳しく確認したい場合は、GitHub のこちらにあります。Fetch プラグインまたはトラッカー自体のセットアップで問題が発生した場合は、Slack コミュニティまでご連絡いただき、開発者に直接お問い合わせください!