GASとHARMOSとSlackではじめる勤怠Bot

これはmikanのアドベントカレンダー 3日目の記事です。

2日目は@satoshin21からEvolution of mikan iOS 2022でした。今のmikanのiOSアプリの全体像とこれからについてわかる内容だったので、iOSエンジニアの方はみていただけると嬉しいです。

はじめに

こんにちは。 mikanでAndroidエンジニアをしております、zukkeyといいます。

今回は、社員数が増えてきたこともあって勤怠システムのHRMOSを利用するようにしたが、自分たちで使いやすいように勤怠用のSlack Botを作ってほしいなぁ、という依頼があった際に業務の合間に密かに作っていたので、GASとHARMOSとSlackではじめる勤怠Botの作り方について紹介していこうと思います。

対象となる読者の方はこちらを想定しています

  • GASでSlack Botを作ってみたい方
  • HRMOS APIを使ってよしなに自分のBotを作ってみたい方

GASとは

まずはじめに、GASとはなんなのかということについてです。

GASとは、Google Apps Scriptの略称で、Googleによって開発されたアプリケーション開発のためのGoogleワークスペースプラットフォームです。

どういうことができるかというと、HTML、CSSJavaScriptを使って開発を行うことができます。定期実行も容易に行うことができ、Google Spread Sheetも併用して自動化することもできます。

HRMOSとは

HRMOSは、株式会社ビズリーチの採用管理システム、タレントマネジメントシステムです。

弊社では勤怠管理に利用させていただいております。

HRMOS勤怠では、APIを利用することができ、自分たちでカスタムしたい場合はドキュメントが用意されています。

今回は、このGASとSlackとHRMOS APIを組み合わせて勤怠Botを作成していきます。

HRMOS APIを利用するための準備

まず初めに、HRMOS APIを利用するために準備が必要です。

管理画面より、API KEY の作成を行ってください。

システム管理者でないとできないので、システム管理者に問い合わせましょう。

GASの準備

Google スプレッドシートにアクセスして、空白を選択し無題のスプレッドシートを新規作成します。

作成がおわると、次のような画面にきます。

拡張機能を選択して、Apps Scriptを選択します。

選択した後に、次のような画面にくるとGASの準備は完了です。あとはコードを書いていくだけになります。

一応試しに、console.log("Hello, World!") と入力してみて、実行を行ってみましょう。

次の画面が出たら、初めての実行は成功です。

勤怠情報を取得しよう

Botを作っていく前に、せっかくHRMOS APIも利用できるようになったので、GASとHRMOS APIを使用して勤怠情報を取得する部分を作成しましょう。

勤怠情報取得までの流れとしては、次のとおりです。

  1. 管理画面で発行したAPI KEYのSecret Keyをスクリプト プロパティに保存する
  2. Secret Keyを使用してBasic認証を行ってTokenの発行を行う
  3. 発行したTokenを用いて、会社に所属するユーザーの一覧を取得する

管理画面で発行したAPI KEYのSecret Keyをスクリプト プロパティに保存する

まずは、Secret Keyのような値をベタで書くこともできますが、セキュリティ上好ましくないということでスクリプトプロパティを利用します。

スクリプトプロパティとは、GASで1つのドキュメントに対して、key-value方式で文字列を保存することができる仕組みです。

コードで設定できる方法もありますが、今回は手動で設定してしまいます。

GAS上の歯車のアイコンを選択します。

遷移後、一番下にいくとスクリプトプロパティの項目を確認できると思います。

次に、スクリプトプロパティを追加を選択します。

プロパティにHRMOS_API_KEYと入れて、値には管理画面で発行したAPI KEYのSecret Keyを入れるようにしてください。

最後に、スクリプトプロパティを保存を選択します。

これでスクリプトプロパティの保存は完了です。

Secret Keyを使用してBasic認証を行ってTokenの発行を行う

次は、HRMOS APIv1/authentication/token を利用してTokenの取得を行いましょう。

まず初めに、先ほどスクリプトプロパティに保存した値を読み取る部分から行っていきましょう。 スクリプトプロパティの値を読み取るには、PropertiesServiceを利用します。

GASでエディタに戻って、次のように記載します。

function getToken() {
  const ps = PropertiesService.getScriptProperties()
  const apiKey = ps.getProperty('HRMOS_API_KEY')
  console.log(apiKey)
}

これを実行して、下記のように先ほどスクリプトプロパティに設定した値が表示されていればOKです。

例では、hogehogefugafugaと指定したので、その値がログに表示できていることを確認できました。

次に、Basic認証を行ってHRMOS勤怠のAPIを使用するためのTokenを発行する部分を実装していきます。

Tokenを発行するためのエンドポイントは、https://ieyasu.co/api/company_url/v1/authentication/token であることが、ドキュメントより確認できます。

ここで、company_urlは、企業ごとに異なります。

次のようなHRMOS勤怠にログインする画面をみたときに、表示されているURLの中にあります。 例で挙げると、次のとおりです。

https://f.ieyasu.co/{ここに書かれています}/login

これも、API KEYの場合と同様にして、 HRMOS_COMPANY_URL というキー名でスクリプトプロパティに保存しておきましょう。先程と同じ手順になるので、ここは省略します。

エンドポイントが何かわかったので、GASで通信する部分について実装していきます。

UrlFetchAppというクラスを使うことで、容易に通信することが可能です。

UrlFetchApp.fetch(url, params)で、urlにはエンドポイントを指定し、paramsにオプションとしてリクエストのメソッドの種類やヘッダーなどの詳細情報を含めることができます。

何を指定できるかはドキュメントにも書いてありますが、次のとおりです。

これらを参考にして、実際にコードを書いていきます。これを実行してコンソールにログを出すようにすると、トークンが発行できていることがわかります。

function getToken() {
  const ps = PropertiesService.getScriptProperties()
  const apiKey = ps.getProperty('HRMOS_API_KEY')
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/authentication/token'
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Basic ' + token
  })
  const options = {
    method: 'get',
    headers: headerInfo(apiKey),
    "muteHttpExceptions": true,
  }
  const response = UrlFetchApp.fetch(endPoint, options)
  const json = JSON.parse(response.getContentText())
  return json.token
}

Headerはドキュメントにあるとおりに指定します。

muteHttpExceptionsをtrueにすると、HTTPResponseを返してくれるようになるので指定しましょう。

getContentTextで得られる値は、jsonになっているので、JSON.parseで処理可能なオブジェクトに変更します。

これで、Tokenの発行を行うことができました。

発行したTokenを用いて、会社に所属するユーザーの一覧を取得する

Tokenを発行したので、次に会社に所属するユーザーの一覧を取得するようにします。

ユーザーの取得を行う必要があるのは、打刻登録のAPIを利用するのにユーザーのIDを必要とするからです。

先程とほとんど変わらず、エンドポイントが変わることと、headerInfo部分の Auth部分が先程実装したTokenを利用する形になります。

ユーザーを取得するためのエンドポイントは、 https://ieyasu.co/api/company_url/v1/users であることが、ドキュメントより確認できます。

コードを書いていきます。これを実行してコンソールにログを出すことで、ユーザーの情報を取得できるようになっていると思います。

function getUserInfo() {
  const hrmosToken = getToken()
  const ps = PropertiesService.getScriptProperties()
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/users'
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Token ' + token
  })
  const options = {
    method: 'get',
    headers: headerInfo(hrmosToken),
    "muteHttpExceptions": true,
  }
  const response = UrlFetchApp.fetch(endPoint, options)
  const responseBody = JSON.parse(response)
  console.log(responseBody)
}

取得するページを指定したり、取得件数の制限があったりするので取得したい情報に合わせて変更してみてください。

SlackアプリとGASで連携しよう

Slackアプリの作成部分に関しては、いろんな方が書かれていることもあるので、必要な部分だけ記載してざっくりといきます。

Slack Botを作成するには、Slack appを作る必要があります。こちらより、Create an appで作成しましょう。

Create an app > From scratch を選択します。

App Name を決めて、Pick a workspace to develop your app in のところで、導入したいworkspaceを選んでください。

Slack Appの方はいくつか設定が必要です。下記の項目を有効にしてください。

そして、WorkspaceにSlackアプリをインストールしましょう。

ざっくりと準備はこんな感じです。利用するAPIに応じてScopeの設定が必要になる点が注意です。

弊社の勤怠Botの要件は次のとおりでした。

  • 出勤や退勤などの特定の単語で打刻ができること
  • 打刻が失敗したら何かわかるといい
  • 打刻が成功した場合は、リアクションがついてくれると嬉しい

これらの要件を満たす為には、下記フローが必要でした。

  1. Event Subscriptionsを有効化して、特定のチャンネルに投稿された特定のワードで打刻を行う
  2. 打刻が失敗した場合は、対象のSlackメッセージのスレッドにBotからのエラーメッセージを表示する
  3. 打刻が成功した場合は、対象のSlackメッセージにリアクションを付与する

それぞれに分けて説明していきます。

Event Subscriptionsを有効化して、特定のチャンネルに投稿された特定のワードで打刻を行う

まずは、Slackでメッセージを送信した後にSlackからGAS側にリクエストを送信し、GAS側でSlackからのリクエストかどうか検証する部分から確認していきましょう。

Slack App側で、Event Subscriptionsを有効化すると、doPost関数で受け取ることができます。

doPost関数は次のように実装します。forwardMessage関数は後ほど説明します。

function doPost(e) {
  const ps = PropertiesService.getScriptProperties();
  const json = JSON.parse(e.postData.getDataAsString());
  if (ps.getProperty("VERIFICATION_TOKEN") != json.token) {
    throw new Error("invalid token.");
  }
  if (json.type == "url_verification") {
    return ContentService.createTextOutput(json.challenge)
  }
  const token = ps.getProperty("BOT_TOKEN")
  const text = json.event.text
  const eventType = json.event.type
  const user = json.event.user
  const ts = json.event.ts
  forwardMessage(token, eventType, user, text, ts)
}

例のごとく、スクリプトプロパティに保存した値を利用します。

Slackからのリクエスト情報として、getDataAsStringでjson形式のデータを取得することができます。

受け取ったJsonの中に、tokenがあり、これはSlackからのものであることを検証する為に利用します。

VERIFICATION_TOKENは、Slackアプリ側の Basic Information > App Credentials > Verification Token にて表示されている値を、ここではスクリプトプロパティに保存して読み取っています。

ContentService.createTextOutput(json.challenge)では、Slack のEvents API ドキュメントのURL verification handshakeに記載されているとおり、最初にイベントリクエストURLを確認する必要があるので、その確認に利用しています。

ContentService.createTextOutputは、GAS側で用意されている関数でテキストを返してくれます。

BOT_TOKENは、Slack アプリ側の OAuth & Permissions > OAuth Tokens for Your Workspace > Bot User OAuth Token の値をスクリプトプロパティに保存して読み取っています。

このようにして、まずはGAS側にSlackからの応答であることを確認する部分を最初に実装してしまいます。

次に、特定ワードの検証部分についてです。

特定ワードの検証部分については、forwardMessage関数に記載しているので、そちらをみていきましょう。

function forwardMessage(token, eventType, user, text, ts) {
  const triggerWord1 = "出勤"
  const triggerWord2 = "退勤"
  const triggerWord3 = "休憩"
  const triggerWord4 = "休憩戻り"
  const triggerWord5 = "ヘルプ"
  if (eventType == "message" && text === triggerWord1) {
    registerStamp(user, 1, "in", ts)
  } else if (eventType == "message" && text === triggerWord2) {
    registerStamp(user, 2, "out", ts)
  } else if (eventType == "message" && text === triggerWord3) {
    registerStamp(user, 7, "bi", ts)
  } else if (eventType == "message" && text === triggerWord4) {
    registerStamp(user, 8, "bo", ts)
  } else if (eventType == "message" && text === triggerWord5) {
    postToSlack(token, "<@" + user + "> さん\nこちらがヘルプのNotionです \n https://hogehogefugafuga", ts)
  }
}

registerStampとpostToSlackは後ほど説明するので、割愛します。

メッセージの検証はとても単純で、ベタで決めている文字列と、Slackからのテキストを比較して、条件分岐しているだけです。

もっと汎用性を高くするのであればスプレッドシートに保存して、特定の列にある言葉のリストのどれかにマッチしているか、というようなことをやるといいのかなと考えてはいるのですが、今回は簡易的に済ませました。

特定ワードの検証はこれだけで済むので、次は打刻部分についてみていきましょう。

registerStamp関数にまとめています。詳細は次のとおりです。コード中の...は省略を示します。

function registerStamp(user, stampType, emojiName, ts) {
  const hrmosToken = getToken()
  const ps = PropertiesService.getScriptProperties()
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/stamp_logs'
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Token ' + token
  })
  const sheetId = ps.getProperty('SHEET_ID')
  const spreadById = SpreadsheetApp.openById(sheetId)
  const spreadsheet = spreadById.getSheetByName("勤怠管理")
  const values = spreadsheet.getRange("A2:B46").getValues()
  const searchIndex = values.findIndex(value => value[1] == user)
  const targetIndex = searchIndex + 2
  const targetRange = "D" + targetIndex
  spreadsheet.getRange(targetRange).setValue(emojiName)
  const userId = values[searchIndex]
  const query = {
    user_id: userId,
    stamp_type: stampType
  }
  const options = {
    method: 'post',
    headers: headerInfo(hrmosToken),
    payload: JSON.stringify(query),
    "muteHttpExceptions": true,
  }
  const response = UrlFetchApp.fetch(endPoint, options)
  ...
}

打刻部分は、HRMOS APIの/v1/stamp_logsを利用します。

以前説明しているgetToken関数で、HRMOSのトークンを取得するようにして、例のごとく打刻登録用のエンドポイントと、通信を行うのに必要なパラメーターを設定していきます。

今回私が作成しているBotでは、先に説明している所属企業の社員のuser idをスプレッドシート側にまとめるようにしているので、SpreadsheetApp.openByIdで指定したスプレッドシー トを利用して、getSheetByNameにて対象のシートから必要な情報を取得するようにしています。

A列にUser Idを入れているので、searchIndexではSlackのメッセージを送ったuser idとシートに保存しているuser idを比較して対象の位置を割り出し、targetIndexでスプレッドシート上のヘッダー分を+2して実際のリストのindexを取るようにしています。

targetIndexを用いて、そのユーザーの現在の勤怠情報を保持しておきたいので、スプレッドシートに記録する部分をsetValueにて行っています。

あとは、HRMOSドキュメントの定義に合わせて、queryを作ります。

user_idとstamp_typeがrequiredになっているので、それらをforwardMessageでメッセージごとに指定した値を渡してあげるようにします。

あとは、ユーザー一覧を取得した時と同じようにUrlFetchApp.fetchで通信するだけです。

レスポンスを受け取っているので、成功か失敗かに応じて分岐させます。

function registerStamp(user, stampType, emojiName, ts) {
  ...
  const response = UrlFetchApp.fetch(endPoint, options)
  const botToken = ps.getProperty("BOT_TOKEN")
  const responseCode = response.getResponseCode()
  const responseBody = response.getContentText()
  const retryCounterTarget = "E" + targetIndex
  if (responseCode === 200) {
    // 成功した時の処理
  } else {
    // 失敗した時の処理
  }
}

getResponseCodeで、レスポンスコードを取得できます。とりあえずドキュメントに記載がある通り、200であれば成功なので、それだけの分岐を行うようにしています。

打刻が成功した場合は、対象のSlackメッセージにリアクションを付与する

こんな感じに投稿したメッセージ対してリアクションをつけるようにするところを説明していきます。

Slackメッセージで、リアクションをつける場合は、reactions.add APIを利用します。

注意点としましては、作成したSlack App側にScopeの設定がされていないとつきません。 まだ未設定の場合は、reactions:write を指定するようにしてください。

打刻が成功した場合なので、前の節の成功した時の処理のところに、次のaddReactionToSlackを呼ぶようにします。

function addReactionToSlack(token, emojiName, ts) {
  const ps = PropertiesService.getScriptProperties()
  const channelId = ps.getProperty("SLACK_CHANNEL_ID")
  const payload = {
    "token": token,
    "name": emojiName,
    "channel": channelId,
    "timestamp": ts
  }
  const options = {
    "method": "post",
    "payload": payload
  }
  UrlFetchApp.fetch("https://slack.com/api/reactions.add", options);
}

tokenで渡すのは、Slack BotのTokenです。xoxb- から始まる値を指定してください。

SLACK_CHANNEL_IDは、Slackのチャンネルの詳細のAboutの一番下に記載されています。SLACK_CHANNEL_IDというキー名でスクリプトプロパティに保存しているので、そこから読み取るようにしています。

timestampは対象のメッセージより取得できるので、doPostから渡してくるようにします。

nameには、付けたい好きなSlack絵文字の名前を指定するようにしてください。今回のBotでは勤怠に使用しているリアクションのアイコンの名前を指定するようにしています。

それぞれ設定し終えたら、UrlFetchApp.fetchで通信するだけです。今のところ実装が雑になってしまっているのですが、ここもエラーハンドリングする場合はresponseを受け取って分岐させるようにしましょう。

打刻が失敗した場合は、対象のSlackメッセージのスレッドにBotからのエラーメッセージを表示する

こんな感じに投稿したメッセージ対してエラー内容を表示して投稿するところを説明していきます。

Slackメッセージで、メッセージを送信したい場合は、chat.postMessage APIを利用します。

注意点としましては、作成したSlack App側にScopeの設定がされていないと投稿できません。 まだ未設定の場合は、chat:write を指定するようにしてください。

今回は、打刻が失敗した場合なので、前の前の節の失敗した時の処理のところに、次のpostToSlackを呼ぶようにします。

function postToSlack(token, message, ts) {
  const ps = PropertiesService.getScriptProperties()
  const channelId = ps.getProperty("SLACK_CHANNEL_ID")
  const payload = {
    "token": token,
    "text": message,
    "channel": channelId,
    "thread_ts": ts
  }
  const options = {
    "method": "post",
    "payload": payload
  }
  UrlFetchApp.fetch("https://slack.com/api/chat.postMessage", options);
}

前の節と同様、tokenで渡すのは、Slack BotのTokenです。xoxb- から始まる値を指定してください。

SLACK_CHANNEL_IDは、Slackのチャンネルの詳細のAboutの一番下に記載されています。SLACK_CHANNEL_IDというキー名でスクリプトプロパティに保存しているので、そこから読み取るようにしています。

thread_tsでは、タイムスタンプを指定しています。指定することでスレッドに対してメッセージを送信することができるようになります。

今後の改善点と注意点

この勤怠Botの仕組みとしましては、いくつか改善点があります。

一つは、打刻の単語判定部分です。今は固定の文字列で分岐しているので、使いたい言葉を設定してそれぞれの言葉に対して設定できるようにする、といったことができると、より自分好みに打刻することができるようになると思います。

単純に、スプレッドシートに記録しておきそこに含まれているかどうかだけで判定できるようになると思いますので、空き時間が出来次第やっていこうかなと思います。 もう一つは、エラー周りです。

現在は、エラーログをGCP上に記録されるように送っているのですが、詳細がわかりづらいことと、他にはエラーが起きた際に利用者へよりわかりやすいメッセージを出す、といったことがあって良いのかなと思っています。

これも時間をとってどこかでやりたいなと思います。

他に、勤怠Botの改善点でもありつつ、注意点があります。

勤怠Botの今の仕組みの注意点として、HRMOS Tokenの更新の仕組みが必要です。やろうやろうと後回しになってしまっていてまだ実装していないのですが、更新しないとある特定の期間で動かなくなってしまいます。

これは次回更新時にきちんと更新されるように、実装しようと思います。

おわりに

今回は、業務の合間に依頼があったので、GASとHRMOS APIとSlackを連携して作る勤怠Botの紹介でした。

以前はBotを作成するとなるとHerokuを使ったりサーバー立てたりとかいろいろと面倒だったのですが、GASで作ってみるととても簡単に作れるので、非常におすすめです。

今回は勤怠Botとしての実装の紹介だったので、HRMOSのパターンでの紹介でしたが、他のAPIでも同様にして、さまざまな自動化を行うことが容易に可能です。

公式ドキュメントをみて実装していけば、誰でも簡単に自動化が可能だと思いますので、ぜひぜひやってみてください。

さいごに、mikanでは、一緒に英語学習をより良いものにしていくべくサービスを作っていく仲間を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!他の職種の方もぜひぜひ、ご応募ください〜〜

03. Android エンジニア

現在デザイナーを特に募集中です!ぜひぜひご応募いただけますと嬉しいです〜〜!

04. デザイナー

WorkManagerの内部を見てみた

f:id:qurangumio:20210907153258p:plain

はじめに

皆さん、こんにちは!mikanでAndroidエンジニアをしているgumioです。

twitter.com

今回はmikan androidにWorkManagerを導入しようとした際に色々面白かったので、それを紹介しようかなと思います。

こちらの記事は以下のような流れになっています。

  1. WorkManagerを知らない人向けに簡単な概要とメリット
  2. WorkManagerのユースケース
  3. WorkManagerの内部挙動
  4. 検証してtoolなどを使って覗いてみた
  5. さいごに

WorkManagerを知らない人向けに簡単な概要とメリット

Android Jetpackの仲間です。

developer.android.com

あるバックグラウンドタスクを実行するScheduler APIです。

今までバックグラウンドタスクに利用されていたFirebaseJobDispatcherGcmNetworkManagerJobSchedulerなどがあったが、WorkManagerを利用すれば以下の図のように内部で分岐してくれるので、API levelとかを意識する必要がないメリットがある + 電池寿命も考慮されており、電池寿命が伸びるメリットもあると公式は言うております。

f:id:qurangumio:20210907150508p:plain
WorkManagerの内部分岐について

ちなみにWorkManagerはAPI level 14から機能するので、安心してください。

WorkManagerの全体的なメリットとしては

  • API level 14から気にせずに機能する
  • シンプルで一貫性がある
  • 信頼性の高い非同期タスク
  • 電池寿命がよくなる
  • RxとCoroutineのサポートもされている

今回WorkManagerの詳細な使い方などは日本語ドキュメントが充実しているため、紹介しないです。

WorkManagerのユースケース

Coroutineとはどこで使い分けたらいいの?どういう時にWorkManagerを使うべき?とか少なからず思いますよね。

androidドキュメントににバックグラウンドタスクのタスクのガイドが用意されてますので、これに従うでいいと思います。

developer.android.com

僕なりの解釈としてはこんな感じだと思いましたが、どうでしょう?

  • 短時間 or 長時間かかって、裏側で必ず動かしてほしいタスク
    • 厳密には書かれていませんが、10 分以上は実行できるらしい
  • 電源が急に落ちる、いきなりプロセスキルされても必ず実行されたい 延期させたい: ネットワークがオフラインになっている、充電が充分にない場合に延期させて、条件を充分に満たした場合に実行したい

公式でも以下のように述べられてます

WorkManager は、ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業を対象としています。次に例を示しますログやアナリティクスをバックエンド サービスに送信するアプリデータをサーバーと定期的に同期する

mikanでもログを送るときに使用しようとしています。

アプリデータをサーバーと定期的に同期するはfetchしてくるイメージだと思います。アプリのデータをpushすることもできなくはないですが、WorkManagerが入力を受け付けれるをデータ量は10KBまでとなっているので、あまり何かを載せるのは期待できません。

それでは、次に実際の挙動を見ていきましょう!

WorkManagerの内部挙動

さて、今回一番紹介したい部分で、WorkManagerの謳い文句の

「ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業」

これって、結局どうやってるんだろう??ってなりました。 そこで内部の動きを簡易ではありますが、追ってみます。

まず、簡単なWorkManagerの流れのおさらいですが

  1. 行いたいタスク(Worker)を定義する
  2. このタスクの実行方法とタイミングを決めるRequestを作って依頼する
  3. WorkManagerを通じて、システムに2.のRequestを飛ばす(enqueue)

すごくシンプルですね。

このとき、WorkManagerのStateは以下の図のようになっていて、一度きりのRequest(OneTimeWorkRequest)と定期的なRequest(PeriodicWorkRequest)で少し異なりますが、終わりがあるかないかの違いだけで基本は同じです。

以下、処理の状態  |  Android デベロッパー  |  Android Developers より

f:id:qurangumio:20210907150506p:plain
一度きりのRequestのとき

f:id:qurangumio:20210907150503p:plain
定期的なRequestのとき

それでは、enqueueされたあとのRequestたちはどのように扱われてるのでしょうか?

WorkManager basics. Getting started with WorkManager | by Lyla Fujiwara | Android Developers | Medium より

f:id:qurangumio:20210907150513p:plain
enqueueされたあとの流れ

  • Internal TaskExecutorが、requestされたものをWorkManager DB(Room)にWorkRequest情報を保存します
    • Internal TaskExecutorはsingle threadのExecutorで、実際にenqueueされたrequestをさばく役割を持っています
  • WorkRequestのConstraintが満たされると(すぐにでも可能ですが)、Internal TaskExecutorはWorkerFactoryにWorkerを作成するように指示します
  • その後、defaultのExecutorは、mainThreadからWorkerのdoWork()メソッドを呼び出します

developer.android.com

なるほど!RequestされたものがDBに保持されていたんですね。このDBにはRoomが使われてました!! Requestされたものは制約が満たされるまではDBでおとなしくしてるんですね。この制約は端折っていましたが、Requestを作るときに指定することができます。

例えば、ネットワークがオンラインのときかつ、充電中のときのみ実行される制約が設定されます。

val constraints = Constraints.Builder()
  .setRequiredNetworkType(NetworkType.CONNECTED)
  .setRequiresCharging(true)
  .build()

先程のStateでRUNNINGになってたとしても処理が完了する前に電源落とされたり、オフラインになったりするとENQUEUEEDに戻り、またDBに大人しく返ってたんですね。

改めて、最初の謳い文句を再度見たときに、なるほどとなるのではないでしょうか?

「ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業」

検証してtoolなどを使って覗いてみた

最後に少しだけ挙動を確認してみて、終わりにしようと思います! 以下のような1...10を出力する簡単なWorkerを作成します。

→ 例ではWorkerではなく、CoroutineWorkerを使用していますが、こうするだけでdoWorkはsuspend関数となり、内部ではCoroutineが使われるようになりますb

class TestWorker(
  appContext: Context,
  workerParams: WorkerParameters
): CoroutineWorker(appContext, workerParams) {
  override suspend fun doWork(): Result {
    repeat(10) {
      Log.d("TestWorker", it.plus(1).toString())
      delay(1000L)
    }
    return Result.success()
  }
}

次にこのWorkerをRequestします。一度きりでネットワークに繋がってるときのみの制約にしています。

補足: 前述したようにWorkerをRequestするときは、一度きりと定期的で分かれます。

また、実行条件の制約をつける場合はConstraints.Builderで指定し、setConstraintsにsetしてあげる必要があります。

val testWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<TestWorker>()
  .setConstraints(
    Constraints.Builder()
     .setRequiredNetworkType(NetworkType.CONNECTED)
     .build()
   ).build()

最後に適当なボタンを用意して、enqueueするようにして、実行してみましょう 途中でオフラインにしてみます。

f:id:qurangumio:20210907150527p:plain
実行中にオフラインにして、キャンセルされてる

Workerがキャンセルされてることが確認できますね。 このとき、最近機能が充実してAndroid StudioのApp Inspectionで覗くことができるので、見てみましょう!!

App Inspectionとは、RoomやWorkManagerといったdebugをしづらいものをAndroid Studioがdebugしやすいように用意してくれたサポートtool郡のことを指します。

まずはDB Inspector

f:id:qurangumio:20210907150523p:plain
DB InspectorでWorkManagerが作成しているworkdbを見ている

どうやら、内部でWorkManager用にworkdbというのが内部で自動作成されていることがInspectorからわかりますね。これが先程の内部挙動で紹介したRequestされたWorkerを保持しておくために必要なDBということが予測できます。

色んなtableがありますが、試しにWorkSpecというtableを開いてみましょう!

名前の通り、各Workerの詳細情報を持っていることが伝わりますね。

f:id:qurangumio:20210907150534p:plain
workdbのWorkSpecテーブルを見ている

次にArctic Fox Canary 3で新たにWorkManagerのdebug用に追加された、Background Task Inspectorを見てみましょう。

f:id:qurangumio:20210907150538p:plain
Background Task Inspectorを見ている

最後のWorkerが途中でオフラインにされたWorkerなのですが、ENQUEUEDになっていることがわかります。 この状態でアプリをオンラインにして再度立ち上げたら、再度Workerが動き出して無事に処理されました!!

さいごに

いかがだったでしょうか? 今までWorkManagerとは関わる機会がなかったのですが、今回導入することをきっかけにキャッチアップをし、気になるところなど、学んだことを書いてみました。

mikan androidではこのように新しめな技術を気軽に試して、導入できるかどうかを検証するのも業務の一環です。まだまだやりたいことがたくさんあり、メンバーが足りないので少しでも興味を持った方は是非お話しましょうー!

herp.careers

チームでスキーマ設計した時のハマりポイントと勘所

f:id:nixii_squid:20210903185934j:plain

はじめに

こんにちは。mikanでバックエンドエンジニアをしているnixiiです。

今年の7月にmikanにジョインし、そこから1ヶ月ちょっとが経ちました。最近は本格的に開発にもコミットするようになり、本エントリテーマにあるようなスキーマ設計を議論したり、がっつりコードを書いている日々です。

先日出した入社エントリから早くも1ヶ月が経ったと思うと、時の流れは早いな〜と実感しています。

note.com

こちらの入社エントリでは、私の自己紹介や、なぜmikanにジョインしたのかについて色々と深ぼって書いています。是非ちらっと読んで頂けると嬉しいです!

。。と前置きが長くなりましたが、今回は「チームでスキーマ設計した時のハマりポイントと勘所」と題して、私を含めバックエンドチームがスキーマ設計を行った際に出会ったつまづきと、そこから得た学びをシェアしていきます。

そもそもスキーマ設計とは

スキーマ(Schema)は広義には「図式・構造」などと訳され、データベース分野においては「DB(データベース)の構造」を指します。

例えばRDB(リレーショナルデータベース)であれば、現実で扱いたいデータに応じてテーブルを定義し、必要なカラムも考慮した上で、テーブル同士の関連についても考えることになると思います。こういったDBの構造(= スキーマ)の設計を考えることが、即ち「スキーマ設計」です。

特にテーブル間のリレーションなど、実世界をデータモデリングしているものは「概念スキーマ」、それをRDBであればSQL文などで表現したものを「外部スキーマ」と呼びますが、本エントリでは便宜上、両者スキーマの設計を合わせて「スキーマ設計」と呼んでいます。

ハマりポイント

今回mikanでは、英単語アプリで新たな教材をアプリ上で扱うため、その教材に応じたスキーマを新たに設計する必要がありました。

スキーマ設計を進めていくうち、こうした設計タスクなどでよく出会うハマりポイントに、今回バックエンドチームもハマってしまっていました。具体的には以下の2つが挙げられます。

  1. チーム内で設計への見通しが異なっていた

  2. 検証フェーズについて考慮漏れしていた

「チーム内で設計への見通しが異なっていた」点について

スキーマ設計の工数としては、元々チームでは1日程度と見積もっていました。が、実際には素案レベルでのスキーマをチーム内で仕上げ、ビジネスサイドと認識をすり合わせつつ調整するために、結果としてトータルで3, 4日以上かかってしまっていました。

元々1日で完了すると踏んでいた理由として、じつは過去すでに類似したスキーマを作っており、このスキーマが流用出来ると考えていた点があります。しかし実際には、このスキーマは今回RDB向けに設計しようとしていたフォーマットとは異なり、Cloud Firestore向けに設計していたものでした。

Cloud FirestoreはNoSQLのドキュメント指向データベースで、key-value値が入れ子構造のようにドキュメントとして保存されていく構造です。これは各テーブルを必要に応じて正規化したり関連させることでデータモデルを表現するRDBとは大きく構造の異なるデータベースで、当然出来上がるスキーマも自ずと違ってきます。

このように前提の異なるスキーマを再利用するのは実際にはかなり難しく、チーム内で議論しつつ、RDB向けにほぼスキーマを設計し直す形となりました。結果として工数が膨らみましたが、元々資産として参考にする予定だったスキーマについて、事前にチーム内で目線を合わせることで気づけていた点でもあり、走り出し時点でチームで認識を揃える重要さを体感しました。

「検証フェーズについて考慮漏れしていた」点について

本来スキーマ設計をチームで進める場合、議論しつつスキーマ設計の素案を作成するだけでなく、「作成したスキーマが実際に解決したい現実の問題を扱えるのか」を検証する必要もあります。

しかし、設計に着手時には後者について考慮しておらず、着手中に検証フェーズの必要性に気づいた結果、1~2日ほど工数が膨れていました。今回に限らず、新規事業でのPoCや実装におけるテストなど、考えたコト・作ったモノに対して正しく機能するかどうかを担保するための検証フェーズは不可欠なものだと思いますが、常にその点も考慮に入れるべきだと振り返りました。

チーム内で認識を揃え、問題解決にあたる際には解決案の検証まで行うというのは非常に基本的な事ながら、日々それを常に徹底することの大切さと難しさを痛感した週でした。

スキーマ設計の勘所

スキーマ設計に限らないのですが、こうしたチームレベルで考え、検討した上で設計するタスクでは、事前にチーム内で

  • 今明らかになっているものは何か
  • まだ不明瞭なものは何か
  • 現在やろうとしている事は何か

について認識を揃えておくだけでも、予期せず工数が膨らむといった事態は避けやすくなるだろうと振り返りました。

今回の例だと、「明らかに過去の資産を今回の設計に活かせる」と考えていたものの、実はチーム内でその点について触れることで「実は明らかではなく、活かせるかどうかは不明瞭だった」という事が把握出来たためです。

中でも「一見明らかだけど、明らかだと思い込んでいて実際には不明瞭だった」というパターンは結構曲者だと思っていて、上記はこのパターンにチームで早期に気づくための勘所でもあるなと思います。

また、現在やろうとしている事についても認識を揃えることで、本当にそれらを完了すれば設定したゴールに辿り着けるかもチームで確認出来ます。今やっている事をこなすことで設定したゴールに到達出来るのか、最初にチームで目線を合わせるのもこうしたタスクでは重要だと思いました。

mikanでは現在ほぼフルリモートで開発を行なっていますが、今回のリモートでのスキーマ設計の議論ではmiroというオンラインホワイトボードのようなツールを使ってみました。ホワイトボード上で図を書きつつ議論するような使い方が出来る点で、非常に使いやすいな〜と感じています!

f:id:nixii_squid:20210903185650p:plain

(miroを使って概念スキーマの議論をしている画面)

他にも必要に応じてZoomにiPadを繋いでイメージ図を書いたり、チーム内で必要に応じてサッと情報を共有していくのもこうした設計の議論では大切だなと実感しています。

コロナ禍前ではオフラインで実際のホワイトボードを前にワイワイ議論出来ていたのですが、オンラインでこういった設計の話を進める上では正直やっぱりハードルが上がったなと感じています。

一方で、お互いにこうしてツールや通信環境をより良くしたり、進める上でのちょっとした障害が無いかどうか都度フィードバックをし合うなど、ハードルを下げる方法はまだまだ沢山あるな〜と思いました。

さいごに

私がmikanチームにジョインしてから度々思うのが、こうやって自分たちが過去やったこと・現在やっていることへの振り返りと、そこから学びを得ようとするカルチャーが徹底されているなという事です。

良い結果を得るために出来たこと、芳しくない結果になった経緯などを、次により良い仕事をやり切るためにチームでシェアするというmikanのカルチャーは個人的にすごく好きで、他のメンバーの振り返りも見ていて非常に勉強させて貰えたりします。

チームにとって良い影響を与える事を継続し、プロジェクトの進行を妨げる要因になりそうなものを排除する上でも振り返りはとても重要だと考えていて、個人・チームレベルのどちらでも日次や週次で振り返り、かつチーム内から色んなフィードバックをもらえるというのはすごく良い環境です。

最後になりますが、このような環境で働いてみたいと少しでも思った方、英語が好きだったり教育を変えていきたいという想いのある方、是非一緒に働いてみませんか?

以下の私のTwitterに気軽にDMして頂いても構いませんので、ぜひ気軽に声をかけてみてください!

twitter.com

mikan.link

参考にした記事

Firebase Documentation Cloud Firestore

入社して4ヶ月経つのでぶっちゃけてみる

f:id:iiizukkeyiii:20210826134024p:plain

はじめに

こんにちは。 mikanでAndroidエンジニアをしております、zukkeyといいます。

今年の4月に入社して、大体4ヶ月ぐらいが経過しました。 まだ入社して4ヶ月しか経っていないのか、という驚きと共に、4月に入社エントリーを書いたときから、何ができたんだろうかと振り返っていこうと思います。

ちなみに、入社してすぐに書いたエントリーについてはこちらになります。

note.com

入社してからやっていたこと

入社してから次のようなことをやっていました。

  • Android開発
    • 運用
    • 機能実装
    • 設計
  • 業務改善
    • Ktlint、自動レビューアサイン、PRテンプレート見直し
  • デザイン
  • 広報活動(採用)

Androidエンジニアなので、まずはAndroid開発で何をやってきたのかをお話しさせていただこうと思います。

入社する前から副業で手伝っていたリデザインの開発を主に行っていました。

mikan AndroidiOSアプリとは異なり、なかなか置き換えるための人的リソースが少なく、代表の@kazさんや副業メンバーで回している状況でしたので、以前自分が触った時と同じままの古いデザインでした。

そのため、Androidのメイン画面を新しいデザインに変更したいという要望もあり、新デザインに変更する(リデザイン)をやっていました。

f:id:iiizukkeyiii:20210826133555p:plain

リデザインの開発では、具体的に次のようなことをやっていました。

  • 画面を跨いでの作業が必要な箇所の大きい改修
    • Fragmentの状態保持の仕組みを入れる
    • 教材一覧画面でカルーセルのindexが保持されておらずリセットされていたのを保持する仕組みを入れる..etc
  • デザイン改修
    • Proラベルの改修やボタンのBackgroundカラーがMaterialComponentsで変わっていた部分を修正
    • タブレットのデザインが崩れていたところを直す..etc

他にも、業務改善や全体として必要そうなところもやっていました。

  • チームになったことで必要になった業務改善
    • Ktlintを入れてフォーマットを整える
    • GitHub Actionsを用いた自動レビューアサイン機能を導入
    • チームができたことにより、プルリクテンプレートの見直し
  • その他
    • 今まではなかなか取り組めていなかった既存のバグ対応(優先度の高いものから着手して着実に数を減らしてきて、今では残すところ7件)
    • アプリケーションの全体の設計を含めてたたき台を作って導入し、今も今後のやりたいことを見据えてブラッシュアップ

設計に関しての記事もまとめているので、よかったら見ていってください。

mikan-tech.hatenablog.jp

mikanでは、担当領域だけでなく手を挙げれば他の領域もやっていけるので、自分の新たなチャレンジとしてデザインタスクもやっていました。

このエンジニアブログのアイキャッチ画像作成にも自分が関わりました!

f:id:iiizukkeyiii:20210826133618p:plain
作成したアイキャッチ画像集

他にも、mikanの認知を広める活動の一環として、広報ブログを開始したり採用面でもお声がけさせていただくことをやっていたりしています。

note.com

mikanに入社してよかった?

総じていうとよかったなー、と思ってます。

よかったと思う点は次のとおりです。

  • いい人で構成されている
  • 施策を決める前の段階から進められる
  • 何でこれやるの?への納得感が大事にされている
  • 状況に応じて柔軟に対応している
  • ベンチャーでありつつ、ベンチャーという理由を言い訳にしない

一番入ってきてからいいなと思ったことは、Qの初めにやる施策案出しがあります。

分業されているところだと、この機能を作るためにはどう実装するのか、現在の状況から実現可能か、いつまでにやり切れるかにばかり集中していて、これってそもそも何のために作ってるんだっけという部分に声を上げられなかったり、提言してもあまり変わらなかったりします。

最初からどういう課題があるかを探しながら、案を出して納得感を大事にして上流から関わることができるのが、一番楽しいところだなと思っています。

他にも、ベンチャーでありながらも柔軟に対応していて、ベンチャーだからできないというようなことがないのも嬉しいところかなと思っていたりします。

昨今のコロナウイルスの状況を鑑みて、適宜リモートにしたり、ワクチン接種まで用意していただいたりして、無事にワクチン2回目摂取も終えることができました。

また、病気・ケガを事由とした有給休暇を年10日間、年次有給休暇とは別で付与されるSick Leaveを設けたり、「安心キット」という人事制度まで整ってきて驚くばかりです。 ベンチャーだからという理由はなく、正直ここまでやるとは思っていなかったので、すごいなぁと思いました。

mikan.link

逆に、もうちょっとここどうにかしたいなーって思うところもあります。

Androidは、今はまだ複数人で開発するためのアーキテクチャ設計をやっていたり、根幹となる技術選定と検証をしていたりする土台作りの段階で、まだ長期的に見据えて時間を割く必要があり、施策をバンバン打って事業伸ばしていくぞー、というよりも前の段階にいます。

自分としては、開発の成果をユーザーからフィードバックをもらっていけるのがとても楽しく感じるので、もどかしい気持ちが多くあります。

早く土台作りを終わらせて次に進みたい、という気持ちと、長期視点で立つときちんと作らないと後で大変なことになるな、という思いがせめぎ合って、まずは目の前の課題を片付けないといけないので、モヤモヤしているところではあります。

人が増えれば解決しそうな気もしますが、今度はマネジメント部分の問題も出てきそうで、今後どうなるかはまだわかりません。実装に突き抜けていたり、マネジメントの経験が豊富であったり、いろんなバックグラウンドを持った仲間がmikanには必要な気がしており、今後の進展が楽しみでもあります。

今はとにかく目の前の課題を先に終わらせて、次行かなくてはという気持ちで走っています。

入社してから気づいたmikanの文化

副業していたときにはあまり気づいてなかったmikanの文化について紹介させていただこうと思います。

Good&New

mikanでは毎日10時から朝会があります。ラジオ体操を終えた後に、Good&Newについてみんなで雑談する場があります。

Good&Newとは、その場で当てられた人がここ最近で嬉しかったことや楽しかったことを共有して、それについて話すということをやっています。

最近は、自粛期間ということもあってなかなかいい話題が見つかりづらいところあったりしますが、Netflixで見た映画の話とか家での過ごし方を共有しながら和気藹々とやっています。

f:id:iiizukkeyiii:20210826133701p:plain

mikan LT

月に1回程度、好きなこととか趣味とかを話す場があります。

ジャンルは何でもよくて、コミュニケーションの活性化のためにやっている催しです。

準備するのは大変ですが、聞いてる分には結構楽しくやっています。

締め会

月次で一回あり、月の振り返りとプレミアムバリューカードを送る会があります。

中でもプレミアムバリューカードは、mikanにある4つのバリューのうち体現している方に1人1枚だけ送ることができ、日々の感謝の気持ちを伝える場となっています。

日々の感謝を伝える方法として、バリューカードを送るのもあるのですが、月次で贈られるものは1枚だけの限定なので嬉しいところがありますね。

おわりに

いかがだったでしょうか?入社からのmikanの雰囲気が少しでも伝われば嬉しいと思います。

もうちょっとここどうにかできないかなーという部分でも出てきたところで、施策をバンバン打ってグロースさせるという方向に早く持っていくためにも、Androidエンジニアが増えると嬉しいなぁと思っております。

もちろん、他の職種も応募しています!

さいごに、mikanでは、一緒に英語学習をより良いものにしていくべくサービスを作っていく仲間を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!

herp.careers

初めてQA担当になってみて知った「現場をアゲルQAの行動」を紹介

f:id:hocchan-mikan:20210806152254p:plain
現場をアゲルQAの行動を紹介します

こんにちは。mikanでQAを担当するほっちゃんです。

本格的な夏が始まり、今日も汗だくで保育園の送迎をしております。

先日、QAもブログを書いてみない?とお誘いがあったので

  1. mikanのQAの仕事内容紹介
  2. 現場の雰囲気を良くするQAの行動とは?

についてお話したいと思います。

mikanのQAの仕事内容

QAの仕事内容を簡単に説明すると

プロダクトの品質保証をするために、テストをしたりテストを作ったり

他にもコードを書いて自動化したり、他部署の人を巻き込んでテストするなど、品質保証のためならあらゆることをする仕事です。

現在mikanで私はテスト実施の他に以下のような業務をしています

  • QAサイクル決定
  • テストケース作成
  • QA実施への他チーム巻き込み
  • 不具合再現
  • QA結果の効果測定

ある1日の流れ

私のある1日の様子をご紹介します。

毎日同じスケジュールではないのですが、FirebaseのCrashlyticsの確認は毎朝行い、クラッシュしない率が99%になるよう、どんな不具合が起きているか探ったりテストケースを変えたりしています。

f:id:hocchan-mikan:20210806152301p:plain
1歳の息子にあわせてこんなスケジュールで働いていますがmikanは柔軟に働きやすい!!最高です

育児をしながらの仕事は大変と思われるかもしれませんが、mikanでは子供の急な熱や就業時間を柔軟に対応してくれるので、子供や夫との時間も取りながら働くことができて、とても感謝しています。

 

なぜ「現場の雰囲気を良くするQA」について調査したのか

実は、私はQA専任にはなったことがなく、現場がどんなQAを欲しているのか分かりませんでした。

mikanにもQA組織はなかったので、まずは社内中に「どんなQAが理想ですか?」と聞きに行きました。

今日紹介する現場の声は、そのヒアリングした中で得た内容になります。

 

数々のヒアリング結果の中で今回のテーマを選んだ理由とは?

それは、社内の他の職種の人が輝いて見えたからです(笑)

困ったときはお互いフォローしあいながら専門的な仕事もバンバンこなすところが素敵だと思いました。

と、同時にQAは知名度の低さから以下のような寂しさがあり、いいなー。みんなかっこいいなー。と思いながら仕事をしています。

  • エンジニアになりたいです!というインターンは来るのにQAやりたいです。と来る人がいない
  • 職種のプルダウンにQAがないアンケートを見かける
  • 転職サイトのQA紹介でよく「縁の下の力持ち」と書かれている
  • Q&Aと勘違いされる

同じように思っているQAの方がいましたら、一緒に会社を盛り上げていけるキッカケになったらいいな。と思いこのテーマを選びました。

 

実際に聞いた「現場の雰囲気を良くするQAの行動とは?」

それでは、いよいよヒアリングした声の紹介をしていきます。

mikanで働く皆さんは、前職で様々な現場を経験してきた人たちなので、過去の経験からも含めて回答していただきました。

さらにレベルアップするためのプラスαの行動もヒアリングしましたのでぜひご覧ください。  

1.「テストケース作成」中に現場を盛り上げる行動

仲間が書いたドキュメントにフィードバックをする

この声はドキッとしました。あの人輝いているね。と言われたければ、誰よりも相手を褒め称えあっていくことが重要なのかなと思いました。

  • 作った仕様が分かりやすいと言われたときうれしかった。(エンジニア)
  • FAQを見て操作方法が分かりやすいと言われ、頑張って書いた苦労が吹き飛んだ。(CS)  
仲間に感謝する

これは、自分も子育て中に体験があるのですが、子供ってすっごい喜ぶんですよ。目を輝かせてきゃーっ!と。

いやいや、そんな喜ばなくても。といいながら、その反応が良いとまたやっちゃう。

なので感謝することで現場の雰囲気がアガるのも納得です。

  • QAを作成したり運用することに関するアドバイスや情報を渡したとき「いつもありがとうございます!」と返してもらい少しでも力になれていると感じることができた。(エンジニア)

 

プラスアルファでレベルアップさせるなら…
# デザインや仕様にもQAをしてきた観点から意見を出してみる
  • 「バグじゃなくて、体験の品質まで保証します!」とQA担当者に言われたとき心強かった。(デザイナー)  

私もQAマンならではの視点で仕様の指摘ができたらなと思います。

 

2.「QA実施への他チーム巻き込み」中に現場を盛り上げる行動

仲間が見つけたバグにエールを送る

「ナイスQA!」という声かけをご存知ですか?私はこのおもしろい声掛けをmikanに入って初めて知りました。部活みたいで楽しいなと感じました。

自分がこれを言われたとき、とても嬉しかったのを覚えています。開発者側もバグが出たら嫌だと思うのではなくこの声掛けをしているのが素敵だなと思います。

  • バグを見つけたときにQA担当者から「ナイスQA!」と言ってもらえた場合は嬉しいです。(Bizdev)

 

分かりやすい不具合報告

この意見は、実際私に対して頂いたもので、何回か試行錯誤して生まれたオペレーションを褒めて頂けたので、とても嬉しかったです。

  • 何度もQAしていただいて、丁寧に結果を伝えてもらってるのが非常に助かっております。(エンジニア)
  • 再現手順を細かに書いて頂き、動画も付いていてバグ修正のときにすぐ再現できて助かるなーという感じでした。(エンジニア)

 

プラスアルファでレベルアップさせるなら…
# 重要なバグを見つけたら開発者だけでなく関係ある部署に確認を入れてみる
  • 「現在こちらのバグを発見しましたが、ユーザーさんからすでにお問い合わせ来ていますか?」とQA担当者から共有があり、問い合わせに備えることができた(CS)

この意見をもらったとき、ハッとさせられました。「この仕事は私には関係ない」と思わず、常に高い視点を持ち、いろいろな人の動きをよく見て、連携していくことの大切さを学びました。  

 

3.「不具合再現」中に現場を盛り上げる行動

難しいテストを代わりに実施し本業に集中してもらう

mikanの助け合いの文化のおかげで私もこのような行動ができたのかなと思います。最近は、手順書に残して誰でもできるようにすることにも挑戦しています。

  • 仕様をまったく知らなかった機能をサクッとQAしてくださり、丁寧に仕様について動画付きで教えてくれたことです。神だと思いました。(CS)

 

プラスアルファでレベルアップさせるなら…
# 仲間の想像を超えるパフォーマンスについて考えてみる
  • 同じテストケースをあらゆる端末で確認してくれていたことがあって感動した。(エンジニア)
  • デザインと実装を比べてズレを指摘してくれたのは助かった。(デザイナー)

これも貴重な意見でしたが、仕事に対する、そして人に対する想像性ですよね。人生において一番大事かもしれないです。想像力を働かせて動けるようになりたいと思いました。  

4.もっと現場を盛り上げるならこんな行動

バグ修正に苦しむ人を応援する

なぜ私がこの行動を行ったかと言うと、自分が昔バグ修正をしていたからかもしれません。ほんとバグ修正って気が滅入るんです…

  • 難しいバグに立ち向かっているときは思い悩みがちですが、QA担当者に「修正方法を次から次へ思いついてすごいですね」と応援されると、気力が湧きます。(エンジニア)  
エンジニアとの協力

こちらも自分の行動に対して頂いた意見になります、社内でどんなQAが理想かについてヒアリングした結果を元に行動できたからよかったのかなと分析しています。

  • 開発が佳境のタイミングで余裕がない時に急な依頼でもすぐにQAをしてもらえたこと。(エンジニア)
  • 開発の仕様を渡すだけでテストケースを作ってテストまでしてくれたことがめちゃくちゃ助かりました。(エンジニア)

 

技術で助ける

今後の私の目標にもなりますが、QA環境が整ったら、次はここらへんにもチャレンジしてみたいと思っています。

  • 自動化でテストケースを網羅されることで安心感がありました。(エンジニア)
  • バグ調査を依頼したときに期待通りのアウトプットを提出してもらいバグ調査時間を短縮できた。(エンジニア)
  • バグが出た部分のログ情報を送ってもらえて助かったことがある。(エンジニア)  

 

まとめ

以上が「現場の雰囲気を良くするQAの行動とは?」に対する現場の生の声でした。

できるだけ技術ではなく、明日からちょっとの工夫で変えることができるものを紹介してみましたが、いかがだったでしょうか。

私はまだまだこの声全てに対応できていませんが、まずは小さなことから始め、試した結果良かったものは、またこのブログで発信していこうと思います。

実際に実践してみて「反応があった。」などの連絡を頂けるとさらに嬉しいです。

 

valueを送り合うmikanの文化

私が集めた現場の声ですが、いくつかの声は私自身が実際にもらったものでもあります。 実はmikanでは下記のように4つの行動指針が定義されており、皆このValueを元に行動をしています。

f:id:hocchan-mikan:20210806152308p:plain
mikan 4つのvalue

このValueに合致した行為を見かけたとき、仲間から感謝の気持ちを込めたValueカードが送られる仕組みがあります。

こんなふうに、Slackに届き、社内中の人がその内容を見ています。

f:id:hocchan-mikan:20210806152242p:plain
valueカードはSlackのvalueチャンネルに届き社員全員が見ています

これを見ると、「xxさんはいつも仲間を助けていてすごいな」とか「xxさんはいつも結果のために逆算した計画を立てていてすごいな」など、とても勉強になります。

私もこのカードをもらうと、もっと仲間や会社に貢献しようと元気になります。(家事と育児もこういう仕組みで誰か褒めて欲しい…)

このような、お互いを尊敬し気持ちよく働ける環境に興味があれば、ぜひ遊びにきてください。

mikanには他にも働きやすい文化がたくさんありますので、カジュアルにランチやお茶でもして情報交換してみませんか?

mikan.link

mikanのCSってどんなことしてるの?チーム加入3ヶ月の私が紹介します!

f:id:NobukoTokushige:20210730143532p:plain

はじめに

皆さん初めまして、こんにちは。mikanでCS(Customer Success)を担当しておりますnobukoです。

私は今年の4月入社ですのでまだまだ見習い中の身であります!

常に向上心と探究心を持ったmikanメンバーに囲まれながら刺激のある毎日を過ごし、個人的にはアニメ・ワンピースの一員に加入したような気持ちで毎日楽しく業務に励んでいます。

今回は、そんな見習い中の私がCSチームでどんなことをしているかについてご紹介します!

その前に私自身の紹介を少しだけお話ししますね。


<プロフィール>

なまえ: Nobuko

あだな: のぶちゃん(mikanメンバーの8割方はこのあだ名で呼んでくれています)

住んでいるところ: 鹿児島県(完全リモートのためメンバーとオフラインでお会いしたことがありません・・・いつかリアルでメンバーに会える日を夢みています✨)

ファミリー: 4人家族で3歳半と1歳半の子どもがいます👦👧

趣味: 最近はもっぱら映画鑑賞です

ストレス発散方法: 走る🏃‍♀️・運転中に大きめのボイスで歌う

嫌いな家事: 皿洗い


<大学卒業後>

大学卒業後地元に戻り、地方銀行に就職。

窓口業務や生命保険、投資信託販売業務を経験→2年間ほど勤務した後、退職。


<mikanに入社したきっかけ>

私がmikanに入社したのは、大学時代の友人で現取締役のmizoさんが声をかけてくれたことがきっかけです。

久々の社会復帰に不安だらけでしたが、私のバックグラウンドを理解したうえでの勤務体系提案など、事前に相談させてもらったことで安心して業務を行う姿を思い描くことができました。

おかげで現在とても働きやすい環境で充実した毎日を過ごせています!!!

業務内容

さて、まずはCSチームがどんなことをしているか業務内容について紹介していきます! CSチームの通常業務や定例MTGはこのような感じです!!

<通常業務>

①お問い合せ対応

mikanではメインのお問い合わせ管理ツールとしてHelpshiftを使っています。

App Store/Google playストアのレビュー返信

③開発チームへの不具合報告

④FAQの作成や変更

mikan.helpshift.com

<定例MTG

①CSタスク確認

その日のタスクを確認したり共有事項を確認するMTG(毎朝開催)

②CS定例MTG

主に1週間の振り返りを行い次の週に向けての目標・スケジュール等を確認するMTG(週1開催)

③CS開発提案

CSチームと開発チームで不具合を共有し、今後の調査・改善についてなど話し合うMTG(週1開催)

現在、4月入社の私もCSチームのメンバーに頼りながらやっと少しづつ業務に慣れてきました。 最初は初めてのことだらけ+完全リモートワークのため戸惑うことも・・・

しかし、事前にオンボーディングがしっかり準備されていたことや、わからないところがあればSlackやZoomですぐに回答してもらえる環境が整っていていたためスムーズに業務に取り組むことができました!

予想外の業務!!!

続いて、私がmikan CSチームに入って予想外だった業務についてお話しします!

mikanで働く前の私はCS=お客様対応、コールセンターのイメージが強くそのままのイメージで入社すると、ところがどっこい!!!!!

こんなこともするの???とびっくりした業務がいくつかありました。

その1. Figmaでの簡単な画像作成

mikanではFAQに掲載する画像をCSチームがFigmaを使って作成しています。

私自身はまだこの業務にチャレンジできていないのですが、これを習得した暁にはもう『CS兼デザイナー』を名乗っていいのではないかと思っています。(嘘です)

その2.Redashでユーザーさんの情報を検索

CSチームではユーザーさんの状態を確認するためにRedashというログデータ可視化ツールを使って、情報検索する作業も行います。

機械系に疎い私はRedash画面の開発系な感じを見た時から「えーーーこれ私にできるの?」

と苦手アンテナが発動しましたが、教えてもらうと簡単に検索することができました!

その3.SQLiteで特定の教材を使用可能にする操作

以前「英単語アプリmikan」では、サブスクリプション形式(mikan PRO)とは別に特定の教材を買い切り購入できるサービスを提供していました。(現在は取り扱っていません)その教材が何らかの不具合で使用不可になった場合、暫定対応で教材を利用できるよう対応するのは、もちろん開発チームの出番!!!!!!!

ではなくmikanではCSチームの出番です。

当時SQLiteを知らなかったため、またまた脳内で苦手アンテナが発動していました・・・。

しかし、操作方法をZoomで画面共有しながらわかりやすく丁寧に教えていただいたので現在は問題なく操作できています。

その4.プッシュ通知の作成

CSチームでは毎日19:50に配信している通知の作成も担当しています。

こちらの業務も予想外ではありましたが、どのような通知にすればユーザーさんに学習を促せるだろうかなどと考えながら、毎週当番制で英語のことわざを用いたり、英単語を紹介するなどの工夫して通知を作成しています。

f:id:NobukoTokushige:20210730143003p:plain
プッシュ通知作成時の下書き画像

CSチームに入って私が感じたこと

最後に私がCSチームに入って感じた雰囲気やチームの強みをお話しします!

チームの雰囲気

私がCSチームに加入して感じた雰囲気は、和気藹々(わきあいあい)です。 その中でもチームメンバーそれぞれが意見を持ち、どんな意見でも発言しやすくメンバーが肯定的に受け止めてくれる雰囲気が印象的でした。 これは前職の銀行員では味わうことのなかった雰囲気でとても安心したのを覚えています。

それぞれの意見を共有し、ユーザーさんにとってBestな状態は何かと模索する中で、最適なサポート環境を提供できるのではないかと思っています。

MTG時には「和気藹々」な雰囲気ですが、業務中では一転して下記のような印象を持っています。

①業務に取り組むスピードが早いこと

具体的な例を挙げると、毎朝のタスク確認で割り振られたタスクがあると午前中、下手をすると割り振られた直後には完了していることが多々あります。

一見簡単に聞こえるかもしれませんが、お問い合せ対応や不具合対応をしながら別タスクに取り組むのは時間管理等が大変で私個人的には対応までに時間がかかってしまうことが多いのです。そこをパパッとこなしてしまうCSチームメンバーにはいつも驚かされます。

②他チームの進捗に気を配り先を見越して周知等の準備を整えていること

こちらも具体的な例を挙げると、まず他チームがどのような状況にあるのか、どんなことに取り組んでいるのかをNotion、Slack上で積極的に情報収集を行います。

そこで例えば、新機能がリリースされるとわかると「どのような機能なのか」「従来と何が違うのか」「ユーザーさんにはどのように伝えればいいのか」などの詳細について開発チームにヒアリングを行い、それに伴って必要になるであろうFAQ等の作成に即座に取り組みます。この間のスピード感は入社から3ヶ月経った今でもびっくりします。

チームの強み

私が思うチームの一番の強みは「ゆずらない・満足しない」ところだと思います。

一見、少し強めなイメージを持たれるかもしれませんが、ユーザーさんの声を一番聞いているCSチームだからこそのユーザーファースト精神を表した言葉です。

ちなみにmikanには「With User」というValueがあって、まさにそれを体現しているのではないかと思っています。

前Qでは、ユーザーさんからの要望を何とかしてもっと社内に届けられないかということで、今までにきている要望の件数などを遡って全て集計し、ドキュメントにまとめ各チームへの提案書として提出しました。

日々ユーザーさんから要望を多くいただくのですが、いろんな事情でその要望を実現まで持っていくことはなかなか難しいことも多いです。

ですがどれだけのユーザーさんが望んでいるか数値で表したり、実際の声を再度提示することによって社内で再認識できたのではないかと思います。

f:id:NobukoTokushige:20210730142921p:plain
各チームへの提案書をSlackにて投稿した画像

これまでの要望の数値を出したりする作業は大変でしたが、これこそ「ユーザーさんの声をプロダクトに届ける」というCSの役割をゆずらず、妥協せず実行できたと思っています。

これからもmikanが進化していく中で、しっかりとユーザーさんの声を社内に届けることがCSチーム使命と肝に銘じて業務にあたっていきます💪

まとめ

今回は主に4月入社の私がCSチームの業務・雰囲気についてお話ししました。

mikan CSチームではCS業務を超えて色々なことにチャレンジする機会がたくさんあること、和気藹々としたとても働きやすい環境で仕事ができるという雰囲気は感じていただけたでしょうか?

CSチームではこれからもユーザーファーストの精神で、皆様により良い環境でmikanを使っていただけるよう励んでいきます!

そして最後になりましたがmikanでは一緒に働く仲間を募集しています!

少しでも興味を持たれた方はご連絡お待ちしております🍊

mikan.link

角丸が尖る問題の直し方

f:id:mizonit:20210717002316p:plain

こんにちは。 英単語アプリmikanのiOS開発をやっている、aviciidaです。

最近嬉しかったことは、iOSDC 2021に提出したレギュラートークのプロポーザルが採択されたことです。

ど文系でエンジニア未経験の状態から開発の世界に入り、かれこれ2年が経ちましたが、ようやくこれで自分もエンジニアです、と胸を張って言えるなと思って嬉しいです。

角丸が尖ってしまう問題を解決する方法

今回は、iOS開発で以前に詰まってしまった「角丸が尖ってしまう問題」をどうやって解決したか、という話をしたいと思います。

そもそも、どんな問題なのか

写真を見てもらうとわかりやすいと思います。

f:id:aviciida:20210716152412p:plain

学習のボタンが尖ってしまってますね。

発生しやすいパターンは

  • 小さな端末(iPhone SEなど)
  • 多重構造(UIView in UICollectionView in UIView in UIViewみたいな)

という感じでした。

mikanではこれを「ラグビーボール問題」と呼んでいました。

端的に言うと、cornerRadiusをheight / 2にしている時に、height / 2がobjectの実際のheight / 2よりも大きくなっていることが原因です。

そもそも、cornerRadiusと角丸の関係はどうなっているかというと、 あるobjectの角っちょを、cornerRadiusを半径とした円で切り抜くよ、そこが角丸になるよ、ということです。 cornerRadiusを、そのobjectのheigt / 2に指定すると、このようになります。

f:id:aviciida:20210716152457p:plain

しかしそれがheightの半分を超えてしまうと f:id:aviciida:20210716152522p:plain

このように、先っちょが尖ってしまいます。

なぜこのようなことが起きるのか

これも簡単に言うと、viewのcornerRadiusを指定するために高さを取得しようとするとview.frame.heightで取得すると思いますが、その値が間違っているということです。

例えば、この画像の例でいくと、

f:id:aviciida:20210716152412p:plain

本当は、view.frame.height31.0になるはずでした。

しかし、このviewの(Auto Layoutの制約がresolveされる)layoutSubViews()view.frame.heightをprintしてみると f:id:aviciida:20210716152645p:plain

Interface Builderで指定したサイズのままになってしまっています。

対策① CustomViewを作る

layoutは親viewから子viewに、という順番なので、今回問題になっているviewのサイズは、1番親のviewのlayoutSubview()時点ではまだ決められていないのでは、という仮説を立てました。

問題のviewは、階層構造のかなり下の方にありました。

f:id:aviciida:20210716152659p:plain

そこで、問題のviewをCustom Viewに切り出して、そこのlayoutSubviews()であれば、正しいheightを取得できるのではと考えました。 結果...

f:id:aviciida:20210716152708p:plain

無事取得できました!

対策② layoutIfNeeded()を呼び出す

親のviewを生成しているところでlayoutIfNeeded()を呼び出し、viewのクラス内でlayoutIfNeeded()をオーバーライドし、その中でheightを取ることによってでもうまく取得することができました。

layoutIfNeeded()のドキュメントを見てみると

Use this method to force the view to update its layout immediately. 
When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. 
Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. 

(雑に翻訳)
レイアウトの更新を強制的に行うために呼び出す。
Auto Layoutを使用している場合は、レイアウトエンジンが、制約の変更に対応するために必要なviewのポジションの調整を行う。
この処理が走ったら、処理対象のviewをroot viewにして、それよりも下の階層にあるviewがレイアウトされる。

つまり、強制的に制約を更新してレイアウトを行ってくれるfunctionなので、これをオーバーライドした中でheightを取得すれば正しい値が取れるということかと思います。

まとめ

ということで、今回は「ラグビーボール問題」をどうやって倒したかを解説しました。

要は、多重階層になっているviewの子viewのheightがlayoutSubviews()でも間違った値が入ることがあるので、そういう時には子viewのCustom Viewを作ったり、layoutIfNeeded()を呼び出したりして正しいheightをとりましょうね、という感じでした!

今見ると当たり前では?という感じですが、当時はまだ経験が浅く、かなり倒すのに時間がかかってしまいました。

mikanは絶賛採用中です! ご興味ある方は、お気軽にご連絡ください! mikanの採用情報

参考にした記事

Subview frame is incorrect when creating UICollectionViewCell

UIKitのView表示ライフサイクルを理解する