ユーザにやさしいサービスを作るための3つのポイント - 問い合わせはサービス改善への近道 -

## ユーザにやさしいサービスを作るための3つのポイント - 問い合わせはサービス改善への近道 -

[mixiグループアドベントカレンダー](https://qiita.com/advent-calendar/2018/mixi) 8日目です。

前日の neglect_ypさんからバトンを受け、CREグループから 私 [@manji602](https://qiita.com/manji602) がアドベントカレンダーをお送りします。

さて突然ですが、皆さんはユーザにやさしいサービスと聞くとどんなことを思い浮かべるでしょうか。

* UIがシンプルで分かりやすい
* チュートリアルなどで、使い方を分かりやすくナビゲーションしてくれる

などユーザ目線で挙げられる観点はいくつかあると思います。

---

CREグループの業務の1つに、問い合わせの技術的調査というものがあります。これは田村による去年の[記事](https://qiita.com/nukosuke/items/ff087fc1fa033a00dbd9#cre%E3%81%AE%E5%BD%B9%E5%89%B2) にもありますとおり、ユーザからの問い合わせのうちCSスタッフでは解決できないような内容について、サーバ内のアプリケーションログやリクエストログ、サーバやクライアントの該当箇所の実装箇所を確認しながら、問い合わせにつながる事象が発生した原因について突き止める作業です。

今回この記事を執筆するにあたり、改めてCSスタッフからの依頼のうち、過去3年半の間に技術的調査につながったおよそ250件分の原因について整理しました。その結果、どういった原因がお問い合わせにつながりやすいかという傾向が見え、サービスの作り方で気をつけるべきポイントがそこから浮かび上がってきました。

本日のアドベントカレンダーでは、どういったポイントに気をつければユーザにやさしいサービス(= 使いやすく、安心して使えるサービス)を作ることができるのか、技術的調査に日常的に携わっている目線から3点ご紹介したいと思います。なお前提として、以下のようなサービスを想定して執筆しています。

* ユーザデータをサーバに保持
* スマホアプリをクライアントとする
* APIリクエストを介してユーザデータを操作する
 


## (1) 異常なAPIリクエストに負けないサービスにしよう

技術的調査で発覚した不具合のうち、サービス提供者が想定していないような異常なAPIリクエストが原因となっていたものは全体の10%弱ありました。この異常なAPIリクエストにはいくつか原因があり、主に3パターンに大別できました。

ユーザにやさしいサービスを作るためには、まずAPIリクエストにはいくつかの異常パターンがありうるということを意識しておくことが大切だと私は考えています。

### (1-a) クライアントの不具合

1つ目のパターンとして、クライアントからのリクエストが下記のような状況に陥り、それにより不具合が引き起こされている技術的調査が散見されました。

* 想定していた仕様どおり実装されておらず、異なるパラメータが送信されてきていた
* サーバからのレスポンスをうまくハンドリングできていないのか、同一のリクエストを複数回送信していた

仕様の想定漏れに関しては該当箇所を発見の上修正すれば解決するでしょう。しかしながら、そうでないものについては不具合を発生させる再現条件が難しいケースがあります。
こういった場合にはいくつか仮説を立てた上で、原因が正しい場合に疎通する処理にログを仕込むなど、原因究明に役立つ情報を得るよう心がけると良いでしょう。

### (1-b) 通信環境

モバイル端末からアクセスが想定されるようなサービスでは、通信環境にも意識を向けることが大切です。

![figure1.png](https://qiita-image-store.s3.amazonaws.com/0/36593/fe7164eb-b777-eeb6-156a-242179d04a0f.png)

混雑した電車内など、電波状況が悪い中でAPIリクエストが失敗するケースというのは往々にしてあります。APIリクエスト間で何らかの依存がある場合に、一部のAPIリクエストが失敗した状態で他のAPIリクエストを実行すると、ユーザデータに想定外の不整合が発生しうる状況になるでしょう。

* APIリクエストが疎通しなかった場合のエラーハンドリング(再リクエストやリクエスト方法の変更など)をクライアント側で実装する
* 複数のAPIリクエストが依存しあうような場合は、そのうちの一部が失敗した場合も考慮してサーバ側の実装を行う

など、そういった状況を想定した実装にしておくと不具合が発生するケースが少なくなります。

### (1-c) 悪意を持ったユーザ

サービスを利用するユーザの中には、サービスの実装の抜け穴をかいくぐり、不当に利益を得ようとするユーザも残念ながら存在します。正しくサービスを利用する中で偶然その抜け穴に遭遇したユーザからの問い合わせがエスカレーションされ、技術的調査によってサービスの脆弱性を遮断することができた事例が過去にありました。

こういった実装を放置し、それによりサービス内の利用状況に歪みが発生すると、サービスの信頼性が失墜する可能性があります。例えば、下記のようなケースが考えられます。

* ゲームサービスにおいて、ゲーム内の進行を有利に進めるため同時リクエストなどで不正にアイテムを大量に取得する
* 口コミサービスにおいて、評価を操作するため、あたかも別のユーザの評価のようにリクエストを改ざんして大量に送信する

サービスの健全化という視点で捉えると、こういった状況を放置してしまうとサービスに失望したユーザが離れていってしまうことに繋がりかねません。発覚した場合は速やかに修正を行った上で、不当に利益を得ようとしたユーザを特定し、場合によってはサービスの利用停止など然るべき対処を施すことが望ましいです。

## (2) ユーザの勘違いを引き起こさないように気をつけよう

意外に思われるかもしれませんが、ユーザの勘違いがきっかけで問い合わせにつながり、それを元にした技術的調査を行うことも少なくありません。実は前述の異常なAPIリクエストに紐づく技術的調査よりも多く、勘違いに起因する調査は全体の15%ほどありました。

使い方に迷いが生じたり、何かトラブルが発生した時に自己解決が難しいようなサービスは、ユーザにとってやさしいサービスとは言いがたいと私は考えています。下記に複数の問い合わせで寄せられた勘違いを3パターン挙げてみました。こういった事象が起きにくいサービスを作ると、安心して利用できるのではないでしょうか。

### (2-a) 仕様に関する勘違い

一番オーソドックスなものですが、サービスで提供している機能の仕様に関する勘違いです。過去の技術的調査では、

* 仕様が適用される条件が、ユーザが想定している条件よりも狭かった
* 仕様で制限している箇所が、特定の操作により制限が外れる状態になった
* ユーザから見た場合に不具合に感じられるような箇所があったが、一部操作を行ったことを失念していた

という事例がありました。

ユーザの声を受けて分かりやすい仕様に改善できる機能もありますが、実装上の制約からやむなく分かりづらい仕様にせざるを得ないケースもあるかと思います。こういった場合は、サービス内で仕様が分かりやすいようなUIを設計したり、適切なヘルプを用意するなどするとよいでしょう。

### (2-b) エラーメッセージから引き起こされる勘違い

サービス内でエラーが発生した場合、親切なユーザはエラーメッセージを問い合わせ本文に掲載してくださったり、エラー画面をスクリーンショットで撮影して問い合わせに添付してくださったりします。

過去の技術的調査では、勘違いが起きる要因として「エラーメッセージの内容が乏しい」という事例がありました。適切なエラーメッセージを定義しないと、問題が発生した箇所をユーザに適切に伝えることができないのです。
裏を返せば、エラーメッセージに具体的にどんな例外が発生したのか、またその改善手順などを盛り込むことで、ユーザ自身での問題解決を促すことができるのです。

メッセージの本文を「意図しないエラーが発生しました」「不明なエラーが発生しました」といったような抽象的な内容で省略するとどうなるでしょうか。

```ruby
class UserObjectController < ApplicationController

  def create
    # some create operation...
    unless user.valid?
      fail UnexpectedException
    end

    unless UserObject.creatable?(user)
      fail UnexpectedException
    end
  end

  def update
    # some update operation...
    unless user.valid?
      fail UnexpectedException
    end

    unless UserObject.updatable?(user)
      fail UnexpectedException
    end
  end
end
```

あるユーザオブジェクトを生成したり、更新する擬似的なRuby on RailsのControllerのサーバコードを用意してみました。異常なパターンに該当する場合に全て `UnexpectedException` で `fail` するようになっています。
このような実装では、ユーザは何が原因でエラーが発生したのか分かりません。どこで `UnexpectedException` が発生しているのか開発者はエラーログのStacktraceを見れば分かりますが、ユーザにはその術がないのです。

このような事態を防ぐには、事象に応じてエラーコードを分けて用意すると良いでしょう。その上で、例外の内容や、その改善手順をクライアント側のエラー画面に盛り込むとより一層ユーザフレンドリーです。先の例では、それぞれの `Exception` を個別に定義することで改善できます。

```ruby
class UserObjectController < ApplicationController

  def create
    # some create operation...
    unless user.valid?
      fail InvalidUserException
    end

    unless UserObject.creatable?(user)
      fail ObjectNotCreatableException
    end
  end

  def update
    # some update operation...
    unless user.valid?
      fail InvalidUserException
    end

    unless UserObject.updatable?(user)
      fail ObjectNotUpdatableException
    end
  end
end
```

### (2-c) 期間に関する勘違い

特定の期間中だけ有効なイベントやキャンペーンがある場合、それに関連するAPIのハンドリングや、ユーザデータの取扱には注意が必要です。期間外のAPIリクエストにより、下記のような不具合が発生したことがありました。

**期間中に複数のAPIをリクエストすることが想定されている場合**

![figure2.png](https://qiita-image-store.s3.amazonaws.com/0/36593/73900884-8ef6-5711-8e03-47e9ccedafea.png)

期間中に複数のAPIをリクエストすることを想定している場合に、期間をまたがったリクエストが発生したケースです。図2に示すAPIリクエストBについては、サーバ上で期間外のリクエストとして処理されたため、ユーザの想定どおりに処理されませんでした。期間外に操作したことにユーザが気づかなかったため、問い合わせにつながりました。

**繰り返し開催されるイベントで、ユーザデータを使い回す場合**

![figure3.png](https://qiita-image-store.s3.amazonaws.com/0/36593/aa8aec58-eb5f-6565-6d2d-09a233815d59.png)

図3に示すとおり、ある一定の期間で定期的に開催されるイベントがあった場合に、APIリクエストが期間を過ぎてリクエストされたことにより、次のイベント期間向けのユーザデータとして保存されてしまったというケースです。こちらはサーバ側の実装に不備があったことが原因で、図3における期間Bに正常にサービスを利用できない状況になったため、問い合わせにつながりました。

---

上記はそれぞれクライアント側、サーバ側の実装が原因で問い合わせにつながっていますが、いずれも期間外にAPIが叩かれることを考慮していなかったことが原因となります。このような事態を回避するためには、

* 期間内・外がはっきりと分かるようなクライアントのUI設計を行う
* 期間内・外の境界でのテストを書く
* 動作確認の際にもテストケースとして用意をする
* 可能であれば不整合が発生しない範囲でAPIを統合する

などの対策を施すと良いでしょう。

## (3) サービス内のユーザデータは誠実に扱おう

全体の40%前後と、技術的調査のきっかけとなった最も多い問い合わせは、サービス内のユーザデータにネガティブな影響が発生することです。具体的には、「得られるはずのユーザデータが得られない」「加算されるはずのユーザデータが加算されない」などといったものです。

これらの事象の原因としては多種多様であり、ここでそのすべてを詳細に記述するのは控えますが、大別すると「障害」と「事故」という2つの観点に分類することができました。（詳しい内容については後述します。）

根本的な解決策としては、ユーザデータを守るためにフェイルセーフな仕組みを実装することですが、100%「障害」や「事故」を回避することは極めて難しいです。
ここで重要なのは、なにか起こった後、影響を受けたユーザの信頼を失わないように心がけることです。ユーザにやさしいサービスを作るには、長い目で見てユーザが安心して使い続けることのできる環境を用意することであると私は考えています。

### 「障害」と「事故」

このエントリでは「障害」は **依存する外部環境** が原因であるもの、「事故」は **サービスの運営スタッフ** が原因であるものと定義します。

まず「障害」が要因となった技術的調査は下記のような事例がありました。

* サービスが抱えるサーバの障害
  * アプリケーションサーバ
  * DBサーバ
  * キャッシュサーバなど
* プラットフォームにおける障害

また「事故」が要因となった技術的調査は下記のような事例がありました。

* マスターデータなど、アプリ上のサービスの根幹となるデータの設定ミス
* サーバの実装ミス
* クライアントの実装ミス

長期間運営しているサービスでは、仕様の複雑化によってバグが起きやすくなったり、利用するインフラや依存する外部サービスが増えることにより障害の影響を受けやすくなりがちです。両者に共通して言えるのは、事象が発生した影響時間の範囲内では、 **不具合が発生し続ける** という点です。

### 事後対応で信頼を失わないために

CREグループで扱う問い合わせの技術的調査では、調査の結果「事故」や「障害」が発覚した際に

* 同様の事象が発生した影響範囲(条件・時間帯など)を特定し、影響を受けたユーザを洗い出す
* 影響が起こった要因が実装に関する「事故」であれば修正を行う
* 不利益を被ったユーザに対して誠実なコミュニケーションを取る

というアクションを取ります。「誠実なコミュニケーション」とは、必要に応じてアプリ内でメッセージを送信したり、不具合の影響がなければ得られたユーザデータを補填することなどです。

誠実なコミュニケーションを取る上で大事なことは、コミュニケーションの内容も勿論ですが影響範囲を正確に洗い出せるようにすることにも気を配る必要があります。ここでは、そのために必要なポイントを2点挙げます。

**不具合が発生した時間帯を正確に把握する**

影響範囲を正確に洗い出す上でまず欠かせないのがこのポイントです。サーバの障害であれば監視ログを元に、プラットフォームの障害であれば障害情報を元に判断すると良いでしょう。
運営スタッフによる事故であれば、サービスのデプロイ完了時にチャットにログを残すなどしておけば、正確な発生時間帯を把握することができます。

**必要なログを残す**

局所的な不具合が発生していた場合は、不具合を含む機能を利用していたユーザを絞り込む事が必要です。このためには、APIリクエストのログを参照できるようにすることは必須事項と言えます。

加えて、サーバ側のアプリケーションでは、必要に応じて技術的調査に役立つアプリケーションログを記録するようにしておくと、不測の事態に備えることができるでしょう。

例えば、削除されうるユーザデータは削除前の状態を、特定のスキーマ( `count` や `point` など)が頻繁に更新されるユーザデータは更新前後の状態をログに記録することが望ましいです。すべてのユーザデータに対してこういった記録を行うのは現実的ではないため、問い合わせにつながりやすいような、ユーザがサービス内で大切にしているデータから順番に対応を進めていくと安心です。

## おわりに

私たちCREグループは技術的調査に取り組む際、問い合わせを寄せてくださったユーザさんに誠実な対応を行うだけではなく、サービスを安心して利用し続けることができる状態を維持することまで見据えています。ユーザフレンドリーなサービスを作る上で、このエントリを参考にしていただけましたら幸いです。

明日は tanatanaさんがXMPについて何かを書いてくださります。お楽しみに！