エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

アプリのレビューをGASでSlack通知するようにした

【 デジスマチーム ブログリレー3日目】

エンジニアリンググループ デジスマチーム所属の荒谷(@_a_akira)です。 最近はデジカルスマート診療(以降デジスマ)という医療機関向けに予約やキャッシュレス決済、オンライン診療を導入・利用できるアプリ(Flutter)をメインで作成しつつ、バックエンドのAPIやWebフロントも機能ごとに実装しています。

digikar-smart.jp

突然ですが、アプリのレビューは普段どのように確認していますか?

レビューを定期的に確認することによって、ユーザーの喜びを実感したり、予期していないバグの発見に繋げられます。 サービスを開発・運営する側にとっては、これを効率的に行うことは重要と言えるでしょう。
例えば、Google Playはメールでのレビュー通知はありますが、App Storeではサポートしておらず他サービスへの通知はできません。 App StoreのレビューにはRSSがあるので、RSSフィードで通知できますが、そのままだと任意の形式に整形されない問題があります。 外部ツールを利用する場合でも利用数に制限があり、会社で出している複数アプリのAndroid, iOS両OSのレビューをコストをなるべくかけずにSlackへ通知したいため選択肢からは外しました。

そこで、今回はGoogle Play側はAPI経由, App Store側はRSSを利用してGoogle App Script(以下GAS)を使ってアプリのレビューをSlackに通知する方法を紹介します。

目次

レビュー取得の準備

App Store

App Storeのレビュー取得方法は

の2つが用意されています。

APIによる方法は秘密鍵をGAS側で読み込む必要があるため、レビュー取得のみで事足りる今回はRSSによる取得を選択しました。

RSSで取得する場合はこのURLで取得できます。

https://itunes.apple.com/[country code]/rss/customerreviews/id=[apple id]

レビューが50件を超える場合のpageオプションやsortByオプションなども用意されています。 今回は1時間ごとに取得するようにするため1時間で50件レビューが増えないと仮定して、ページ数は指定せず、最新順にソートします。

Apple IDがわかれば、どのアプリでもこの形式で取得できます。 AppIe IDはApp Store Connectから見れます。

app store connect apple id

Google Play

Google Play側はGoogle Play Developer APIを使ってレビューを取得します。

具体的にはGoogle Playと紐付けたGCPのプロジェクトのOAuthクライアントから

  • Refresh Token
  • クライアントID
  • クライアントシークレット

を使って取得したAccess Tokenを使ってアプリのレビューを取得します。

Access Tokenを取得するには、Google Play Developer APIを使うための準備が必要になります。
手順が多く長くなってしまったため、詳しい取得方法は個人ブログに書きましたのでそちらをご覧下さい。

aakira.app

上記の方法でAccess Tokenを取得したら

https://www.googleapis.com/androidpublisher/v3/applications/[package_name]/reviews?access_token=[access token]

の形式でレビューを取得できます。

Review APIの詳しい仕様は以下のページを見て下さい。 取得の他にもレビューに返信などがAPI経由でできますが、今回は取得APIのみ使います。

developers.google.com

GASからSlackにレビューを送信する

RSS, APIからレビューの取得ができたのでGASからSlackに送るコードを解説していきます。
今回は弊社でリリースしている複数アプリのレビューをSlackに通知したいため、1サービスではなく複数サービスで使えるよう汎用的に作っています。

取得日時を保存する

Google Play Review APIは取得日時の指定ができず、App StoreはRSSによる取得を選択したため、両プラットフォームともレビューの最終取得日時は指定できません。 GASは時間フックで定期実行したいため、取得日時の状態を持たない場合は毎回同じ通知をSlackに送ってしまいます。 そこで、取得したレビューの最後の日付をGASのScript Propertyに保存しておき、すでに送信したアプリのレビューはSlackに通知しない仕組みにしました。

function formatDateJST(date) {
  return Utilities.formatDate(date, "JST", "yyyy/MM/dd HH:mm:ss");
}

function setLastReviewDate(appID, date) {
  PropertiesService.getScriptProperties().setProperty(appID, date);
}

function getLastReviewDate(appID) {
  return PropertiesService.getScriptProperties().getProperty(appID);
}

モデル定義

  • アプリの情報

このクラスはAPI経由で取得するためのアプリ情報を表すクラスになります。
レビューはまとめて1つのチャンネルに送っているのですが、一部のアプリで複数チャンネルに送信したい要望があったため extraSlackPostUrlというのを追加して他のチャンネルにも送れるようにしました。

class AppInfo {
  constructor(androidId, appleId, name, icon, googlePlayAccount, extraSlackPostUrl) {
    this.androidId = androidId;
    this.appleId = appleId;
    this.name = name;
    this.icon = icon;
    this.googlePlayAccount = googlePlayAccount;
    this.extraSlackPostUrl = extraSlackPostUrl;
  }
}
  • レビュー情報
const Platform = {
  ANDROID: ":android:",
  IOS: ":appleinc:",
};

class Review {
  constructor(updatedAt, author, rating, title, message) {
    this.updatedAt = updatedAt;
    this.author = author;
    this.rating = rating;
    this.title = title;
    this.message = message;
  }
}
  • アカウント

弊社ではアプリを出しているグループ会社ごとにデベロッパーアカウントが異なるため、このように定義しました。 1社のみの運用の場合はこれは定義しなくても大丈夫です。

const GooglePlayAccount = {
  M3: 1,
  Digikar: 2,
}

App Store

App Store レビューRSSのJSONをパースして返す関数です。 RSS取得URL最後の xmljson にすると、JSON形式で返ってきます。
Script Propertyに保存された日付より新しいレビューがあればレビューのリストに詰めて返却します。

function getAppleReviews(appInfo) {
  const feedURL = "https://itunes.apple.com/jp/rss/customerreviews/id=" + appInfo.appleId + "/sortBy=mostRecent/json";
  
  const response = UrlFetchApp.fetch(feedURL);
  const entries = JSON.parse(response.getContentText()).feed.entry;
  if(entries == undefined) {
    return [];
  }
  const reviews = [];

  let lastCheckedAt = getLastGetDate(appInfo.appleId);
  if (lastCheckedAt != null) {
    lastCheckedAt = new Date(lastCheckedAt);
  }

  for (let i = 0; i < entries.length; i++) {
    // 投稿日時
    const reviewUpdatedAt = new Date(entries[i].updated.label);

    // 最後に取得したレビューより後のレビューのみ送る
    if (lastCheckedAt != null && reviewUpdatedAt.getTime() <= lastCheckedAt.getTime()) {
      break;
    }

    const review = new Review(
      reviewUpdatedAt,
      entries[i].author.name.label, // author
      parseInt(entries[i]["im:rating"].label), // rating
      entries[i].title.label, // title
      entries[i].content.label, // コメント
    );
    reviews.push(review);
  }

  // 最新レビューの日付を保存
  if(reviews.length > 0) {
   setLastGetDate(appInfo.appleId, reviews[0].updatedAt);
  }

  return reviews;
}

Google Play

Google PlayはApp Storeと異なりAPIを使うため少し複雑です。

クライアントID, クライアントシークレット, Refresh Tokenを使って _getAccessTokenでアクセストークンを取得しています。 このトークンは一定時間を過ぎると利用できなくなるため毎回取得する必要があります。

リダイレクトのURLはなんでも良いので、今回は https://google.com にしています。

const DIGIKAR_CLIENT_ID = "digikar_client_id.apps.googleusercontent.com";
const DIGIKAR_CLIENT_SECRET = "digikar_client_secret";
const DIGIKAR_REFRESH_TOKEN = "1//digikar_refresh_token";

const M3_CLIENT_ID = "m3_client_id.apps.googleusercontent.com";
const M3_CLIENT_SECRET = "m3_client_secret";
const M3_REFRESH_TOKEN = "1//m3_refresh_token";

const REDIRECT_URI = "https://google.com";

function getAndroidReviews(appInfo) {
  let accessToken;
  if(appInfo.googlePlayAccount == GooglePlayAccount.M3) {
    accessToken = _getAccessToken(M3_CLIENT_ID, M3_CLIENT_SECRET, REDIRECT_URI, M3_REFRESH_TOKEN);
  } else if(appInfo.googlePlayAccount == GooglePlayAccount.Digikar) {
    accessToken = _getAccessToken(DIGIKAR_CLIENT_ID, DIGIKAR_CLIENT_SECRET, REDIRECT_URI, DIGIKAR_REFRESH_TOKEN);
  } else {
    return [];
  }

  const url = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/" + appInfo.androidId + "/reviews\?access_token=" + accessToken;

  const response = UrlFetchApp.fetch(url);
  const entries = JSON.parse(response.getContentText()).reviews;
  if(entries == undefined) {
    return [];
  }
  const reviews = [];

  var lastCheckedAt = getLastGetDate(appInfo.androidId);
  if (lastCheckedAt != null) {
    lastCheckedAt = new Date(lastCheckedAt);
  }

  for (var i = 0; i < entries.length; i++) {
    const comment = entries[i].comments[0].userComment;

    // 投稿日時
    const reviewUpdatedAt = new Date(comment.lastModified.seconds * 1000);

    // 最後に取得したレビューより後のレビューのみ送る
    if (lastCheckedAt != null && reviewUpdatedAt.getTime() <= lastCheckedAt.getTime()) {
      break;
    }

    const review = new Review(
      reviewUpdatedAt,
      entries[i].authorName,
      comment.starRating,
      comment.deviceMetadata.productName, // GoogleはTitleない
      comment.text.trim(),
    );
    reviews.push(review);
  }

  // 最新レビューの日付を保存
  if(reviews.length > 0) {
    setLastGetDate(appInfo.androidId, reviews[0].updatedAt);
  }

  return reviews;
}

function _getAccessToken(clientId, clientSecret, redirectUri, refreshToken) {
  const payload = {
    "client_id": clientId,
    "client_secret": clientSecret,
    "redirect_uri": redirectUri,
    "grant_type": "refresh_token",
    "refresh_token": refreshToken,
  };

  const params = {
    "method": "POST",
    "payload": payload,
    "muteHttpExceptions": true
  };
  const response = UrlFetchApp.fetch('https://accounts.google.com/o/oauth2/token', params);
  const data = JSON.parse(response.getContentText());
  const access_token = data.access_token;

  return access_token;
}

Slack送信

取得した情報を元に整形してSlackに通知する関数です。

const BOT_NAME = "App Review Reporter"
const SLACK_URL = "https://hooks.slack.com/services/****/****/****";

const STAR_ENABLE = "★";
const STAR_DISABLE = "☆";

function sendSlack(platform, appInfo, reviews) {
  let message = "";
  for (var i = 0; i < reviews.length; i++) {
    const review = reviews[i];
    message += ` 
${platform} ${appInfo.icon} ${appInfo.name} 
${review.title}
${review.author}
${formatRating(review.rating)}
${review.message}
${formatDateJST(review.updatedAt)}
  \n`;
  }

  if (message.length == 0) {
    return;
  }

  const jsonData =
  {
    "text": message,
  };
  let options = {
    "method": "post",
    "contentType": "application/json",
    "payload": JSON.stringify(jsonData),
  };
  UrlFetchApp.fetch(SLACK_URL, options);

  // 追加で別のチャンネルに送る場合
  if(appInfo.extraSlackPostUrl != null) {
    UrlFetchApp.fetch(appInfo.extraSlackPostUrl, options);
  }
}

function formatRating(rating) {
  var result = "";
  for (var i = 0; i < rating; i++) {
    result += STAR_ENABLE;
  }
  for (var i = 5 - rating; i > 0; i--) {
    result += STAR_DISABLE;
  }
  return result;
}

呼び出し(main)関数

今までのコードを使って取得したレビューをSlackにポストします。 実際は7件のアプリのレビューを監視しているのですが、例では2つのアプリのレビューを監視しています。

function getReview() {
  const digisma = new AppInfo(
    "jp.digikar.smart.app",
    "1563102530",
    "M3デジカルスマート診療",
    ":digisma:",
    GooglePlayAccount.Digikar,
  );
  const m3comApp = new AppInfo(
    "com.m3.app.android",
    "868617306",
    "m3.com",
    ":m3com:",
    GooglePlayAccount.M3,
  );

  _getAppReviews(digisma);
  _getAppReviews(m3comApp);
}

function _getAppReviews(appInfo) {
  // Google Play
  const androidReviews = getAndroidReviews(appInfo);
  sendSlack(Platform.ANDROID, appInfo, androidReviews);

  // App Store
  const appleReviews = getAppleReviews(appInfo);
  sendSlack(Platform.IOS, appInfo, appleReviews);
}

トリガーの設定

最後に呼び出し関数を時間主導型のトリガーに設定しましょう。 よほどアプリのレビューが来ているアプリでなければ、そこまで頻繁に呼ばなくても良いと思いますがカスタマーサポートが必要なレビューが来た場合は迅速に対応できるように1時間に1回発動するように設定しています。

GAS時間トリガー

Slackのスクリーンショット

上記の設定が終われば定期的にアプリのレビューがSlackに送られてくるようになります 🎉
プラットフォーム、アプリがアイコンにできてわかりやすかったり、それぞれのレビューにリアクションできるのは確認しやすい以上にSlackに送るメリットなのかなと思います。

まとめ

GASを使ってアプリのレビューをSlackに通知する方法を紹介しました。
Slackに通知することで、絵文字リアクションで反応できるようになったり、カスタマーサポートの方がバグやお問い合わせに迅速に対応できるようになった結果、チーム全体がアプリレビューへの意識が高まる良いきっかけになりました。

今回行ったレビュー通知はAPIの都合上コメント付きのレビューのみSlackに通知しているため、今後はコメントなしのレビューも送れるようになにかしらの方法を考えていきたいと思っています。

おまけ

アプリレビューのSlack通知と同時に、アプリ内レビュー( iOS, Android ) の実装をしました。

このAPIを使うとストアに飛ばずにアプリ内からレビューを送信できるようになります。

出典: https://developer.android.com/static/images/google/play/in-app-review/iar-flow.jpg

デジスマアプリでは事前に登録したクレジットカードから自動で会計を行い、すぐに帰れて、薬局に処方箋が送られているという今までにはない病院体験を与えられるため、診察後にユーザーの満足度が一番高くなると考え、表示タイミングは診察後にホーム画面を表示したタイミングでレビュー訴求をしています。
(1回目以降の表示ロジックはプラットフォームによって異なるため毎回表示はされません)

アプリ内レビュー実装前の約1年前時点はユーザー数が少なかったのもありますが、アプリのリリースから約1年でレビューがiOSで37件, Androidが9件のみでした。

しかし、アプリ内レビュー機能のリリース後6日でiOSが110件、Androidが20件にまで増えました!

そして、アプリのリリースから約2年、アプリ内レビュー機能リリースから約1年たった今はiOSが6012件、Androidは691件に増え、評価もそれぞれ4.5, 4.25とそれなりに高い数字を維持できるようになりました🎉
アプリ内レビューを入れるだけでまさかこんなにもレビュー数が増えるとは思わなかったです。

We are hiring!

デジスマをもっと知りたいと思ってくれた方はぜひ紹介資料を覧下さい!

speakerdeck.com

エムスリーでは、デジスマ以外のサービスもたくさんあります。
エンジニアに限らずデザイナーやプロダクトマネージャーも積極的に採用しています。 他のサービスにも興味を持たれた方は下記よりお問い合わせください。

jobs.m3.com