JSON Schemaでお手軽アンケートフォーム

こんにちは。お手軽に自分のサイトに埋め込むアンケート的なやつを作りたいけど、Google Formsなどでは若干要件を満たさないというお悩みがありました。

  • ログインしているときだけアンケートに答えられるようにしたい
  • 入力フォームに隠しパラメータを埋め込みたい(<input type="hidden"> みたいなものを入れたい)
  • データの後加工をしたいので任意の場所にアンケート結果を保存したい

これらを解決するあれこれを作っていました(作っています)。

github.com

だいたいこんな感じのアプリケーションです。

  • JSON Schema に基づきアンケートフォームが表示される
  • アンケートを送信すると、サーバ側でJSON Schemaの内容に基づきバリデーションを行う
  • 適切な場所にアンケート結果を保存する

f:id:piyorinpa:20210608224119p:plain
こんなイメージで構成されるアプリケーション

保存したアンケートを集計したり、分析したりすることはこのアプリケーションではしないものとします。

このリポジトリはmonorepo構成で、いくつかのアプリケーションで構成されています。

アンケートフォーム

JSON Schemaに基づき、アンケートフォームを生成して表示する、Reactベースのアプリケーションです。

アンケートフォームの生成には react-jsonschema-form を使っています。JSON Schemaを与えることで、 適したInput要素を表示してくれます。このライブラリをベースに、アンケートフォームとして使いやすい形のデザインに調整しました。

このアプリケーションを起動し、 /surveys/:schemaId にアクセスすると、指定したJSON Schemaを指定したアンケート定義JSON Schema配信サーバから取得してフォームを表示します。

f:id:piyorinpa:20210608232110p:plain
アンケートフォームのルーティング

たとえば、このアプリケーションをビルドする際の環境変数 REACT_APP_SCHEMA_BASE_URL="https://example.com"として、以下のようなJSON Schemaを https://example.com/test-survey に設置するとします。

{
  "title": "アンケート",
  "description": "サンプルアンケートです",
  "type": "object",
  "required": ["useful"],
  "properties": {
    "useful": {
      "type": "string",
      "title": "満足度",
      "description": "このウェブサイトににどれくらい満足していますか?",
      "enum": [
        "とても満足",
        "まあまあ満足",
        "あまり満足じゃない",
        "不満"
      ]
    },
    "comment": {
      "title": "ご意見やご感想",
      "description": "ご意見やご感想、機能追加や改善のご要望などご自由にお書きください",
      "type": "string",
      "maxLength": 1000
    }
  }
}

/surveys/test-survey にアクセスしたら GET https://example.com/test-surveyJSON Schemaを取得し、以下のようなアンケートフォームを表示します。

f:id:piyorinpa:20210607235615p:plain
表示されるアンケート

送信ボタンを押すと、環境変数 REACT_APP_BASE_URL に指定したアンケート受付APIにアンケートをPOSTします。

埋め込みアンケートフォームと署名付きパラメータ

f:id:piyorinpa:20210608224217p:plain
埋め込みアンケートフォーム

このアンケートフォームをiframeを使ってサイトに埋め込んだとき、サイト側で生成した署名付きパラメータを受け取ることができます。アンケートと一緒に任意のパラメータを埋め込みたいときなどに使います。

アンケートを埋め込みたいページに以下のようにiframeを設置します。(アンケートフォームが http://localhost:3000 、埋め込み先ウェブサイトが http://localhost:3001 で起動しているとします。)

<!-- アンケートフォームが localhost:3000 で起動しているとする -->
<!-- 埋め込んでいるウェブサイトは localhost:3001 で起動しているとする -->
<iframe id="survey-form" src="http://localhost:3000/surveys/test-survey"></iframe>

たとえば、先ほどのアンケート test-surveyJSON Schemaに以下のようなキー reportBoxOptions を追加します。signedParameters には署名付きパラメータの定義を、embedded には埋め込み先ページのOriginを記述します。

{
 "title": "アンケート",
  "description": "サンプルアンケートです",
  "type": "object",
  "properties": { ... }
  "reportBoxOptions": {
    "signedParameters": {
      "type": "object",
      "required": ["loggedIn"],
      "properties": {
        "loggedIn": {
          "type": "boolean"
        }
      },
      "embedded": {
        "parentOrigin": "http://localhost:3001"
      }
    }
  }
}

この signedParameters に記載された形式のJSONをJWTに変換してアンケートフォームに渡します。

アンケートを埋め込みたいページのサーバサイド側でJWTを生成します(secret秘密鍵

const jwt = require('jsonwebtoken');
const secret = 'test-secret';

// 今のところパラメータの暗号化を考慮していないので、秘密の情報は入れないこと
const signedParameters= jwt.sign({ params: { loggedIn: true } }, secret);

アンケートを埋め込んだページに以下のような処理を追加します。

<script type="text/javascript">
  const iframe = document.getElementById('survey-form');
  const formOrigin = 'http://localhost:3000';

  window.addEventListener('message', e => {
    // アンケートフォームから送られたメッセージでなければ受け取らない
    if (e.origin !== formOrigin) return;

    // アンケートフォームからSignedParameters受け取り準備完了のメッセージを受け取ったら
    // アンケートフォームへSignedParameters(をJWTエンコードした値)を送信する
    if (e.data.event === 'readyToReceiveSignedParameters') {
      // (signedParameters は埋め込み先のサーバ側で生成したJWT)
      iframe.contentWindow.postMessage(signedParameters, formOrigin);
    }
  })
</script>

アンケートフォーム側から signedParameters の受け取り準備が完了したタイミングでメッセージが送出されるので、確認の上アンケートフォームへ signedParameters の JWT を渡します。

アンケート送信時にこのJWTも一緒に送信され、アンケート受付APIでデコードされます。署名検証に成功したら、データストアに保存します。

JSON Schemaに signedParameters の設定を記述した場合は、署名付きパラメータがアンケート受付APIへのリクエストボディに含まれないと保存されないので、たとえばログインしていない人にはアンケートを 送信してほしくないなどの用途にもつかえます。

アンケート回答後の遷移先の指定

JSON SchemaにreportBoxOptions.callbackUrl パラメータを指定することで、アンケートを回答した後に任意のページに遷移させることができます。 完了画面を表示したり、アンケート回答後に何らかのアクションをしたいときに使います。

{
 "title": "アンケート",
  "description": "サンプルアンケートです",
  "type": "object",
  "properties": { ... }
  "reportBoxOptions": { 
    ... ,
   "callbackUrl": "https://example.com/callback"

  }
}

アンケート受付API

アンケートを受け取り、アンケート定義JSON Schemaサーバからアンケートに対応したJSON Schemaを取得したうえで、バリデーションをしてデータストアに保存します。 サーバ側のバリデーションには ajv をつかっています。

今回は、実装のひとつの例として、AWS環境(API Gateway と Lambda)にデプロイできるSAM TemplateとLambda Functionのサンプルを用意しました。 提出されたアンケートを検証(JSON Schemaを用いた入力値バリデーションと署名付きパラメータの署名検証)し、成功した場合はreportBoxMeta というメタデータを付与したうえで、指定したS3 Bucketにデータを保存します。

例えば先ほどの test-survey フォームを送信して検証が成功したとき、こんなデータが保存されます。

{
  "useful":"まあまあ満足",
  "comment":"ここをあれこれするともっといいと思います。",
  "loggedIn":true,
  "reportBoxMeta":{
    "metaDataVersion":1,
    "schemaId":"test-survey",
    "createdAtUtc":"2021-06-07T16:01:12.672Z"
  }
}

まとめ

JSON Schemaでアンケートが作れるそれっぽいアプリケーション群をお試しで実装してみました。好きな場所にアンケート結果を収集できるようになったので、集計やデータの加工がしやすくなったかなと思っています。 私の自作アプリケーションに組み込むなどしながら引き続き手を加えていきたいなと思っています。あと、せっかく作ったのでちゃんと使い方とかをまとめなければ...。

ではでは~。