クラシル開発ブログ

クラシル開発ブログ

エンジニアは体が資本でしょ。と思って始めた習慣とその続け方

f:id:kenzo_aiue:20191219102803p:plain

本記事は dely Advent Calendar 2019 19日目の記事です。

昨日はWebフロントエンドエンジニアのしらりんくんが「Vue.jsでカスタムディレクティブを使ってユーザーの「見てる」を可視化する」という記事を書きました。ぜひご一読を。表示されて1秒経ったら色が変わる動画が見ていて気持ちいいです。

こんにちは。継続の鬼、kenzoです。
冬ですね。寒いですね。みなさまにおかれましてはますますご健勝のこととおよろこび申し上げます。

。。本当にご健勝でしょうか。風邪をひいていたり、なんとなく体調悪い日が続いている、なんてことはないでしょうか。

「日々開発に打ち込むためにはまず健康でなければならない」
そのような思いからこの1年、自分の体のためにいくつか続けてきたことがあります。
この記事ではその続けてきたこと、それを始めてから続けられた理由、そこから見えてきた新しいことを始めて習慣化させる方法についてお伝えします。

続けてきたこと

ごはんログ

健康は食事から。ということで、自分の食生活を見直し、改善するために1年間ほぼ全ての食事や間食の写真を撮ってTwitterに投稿してきました。
以前、弊社の管理栄養士の同僚が趣味で食生活を見てくれるという取り組みがあり、それに手を上げてお願いしたのが始まりでした。

食事を投稿することで、自分が普段食べているものをきちんと把握したり、人の目を意識することでヤバい食べ方を減らすことを期待してのことでした。
一部の例外(家で飲む水や薬、サプリメント等)を除き、朝昼晩の食事に加えて仕事中に食べるおやつ、筋トレ後のプロテイン、デパートでの試食、果ては駅伝中にもらった水まで撮影してTwitterに投稿してきました。

この取り組みは1年間続いたところで卒業?という形で終了することになりました。

体組成記録

日々の自分の体の変化を認識できるよう、毎朝体組成計で自分の体組成を計測しています。その内容はTwitterにも投稿しています。
これも前述の同僚の取り組みに合わせて始めましたが、ごはんログを卒業?した今でも続けられています。

タニタの体組成計を使っているので、Twitter連携で自動でこのような投稿をしてくれます。

家トレ

毎日家で筋トレをしています。こればかりは色々な理由(けが等)により1年間は続いてはいませんが、いろんなパターンをやってみて、ここ3ヶ月ほどは継続中です。

筋トレアプリ

筋トレ用のアプリを使って日々家トレをしていました。様々な部位の筋トレメニューを日替わりで提供してくれるアプリでした。けがをして一度途絶えた後、再開時の強度が高めだったためか、復帰したものの長続きさせることはできませんでした。

筋トレグループ

家で腹筋をした回数を報告し合うLINEグループを友人と作って競い合っていました(二人でしたが)。当時はその回数をスプレッドシートにメモして月々の記録をしていました。こちらは数年間続きましたが、他の競合筋トレ台頭の憂き目に遭い、今ではそのグループも静かになってしまいました。

f:id:kenzo_aiue:20191218200740p:plain

プランクアプリ

これは今でも続いている習慣です。上記の筋トレアプリと同様にメニューを提供してくれるアプリでプランクに特化したものを使っています。こちらも1度けがによる離脱ピンチがありましたが、前回(上記の筋トレアプリ)の反省を踏まえ、復帰時には強度を弱くしてハードルを下げた状態で再開したためか、再度習慣化させることができています。

f:id:kenzo_aiue:20191219095807p:plain

続けられた理由

以上の習慣について振り返ってみると、やってこれた(やめてしまった)のにはいくつかの理由がありました。

ごはんログ

やります宣言・人に見てもらう

そもそもの始まりが食事を見てもらうためだったということもあるのですが、きちんと「やります!」と宣言し、Twitterに投稿して見てもらうことで、続けざるを得ない環境となりました。

褒めてもらう・定期的なリアクション

ランチや飲み会の場でも摂取する全てのものを撮影してきたので、同席した方から「え、インスタ?インスタ?ww」などと聞かれることが度々ありました。
その度にこういう取り組みをしていますと伝えてきましたが、その際によく「へーよく続けられるね」のようなお褒めの言葉をいただくことができ、続けるモチベーションになっていました。
また、Twitterをフォローしてくれている同僚からも、たまに自分の投稿した食事を話題に上げてもらえることがあり、ちゃんと見てもらえているし続けようという気持ちになりました。

体組成記録

起きたら計る

毎朝起きたら必ず体組成計に乗る。というやり方で実施してきました。
起きたら目覚ましを止めて毎回同じところに向かうというルーティーンの動作ができ、忘れずに実施することができていました。
「起きたら◯◯する」は他の習慣づくりにも良さそうです。ただ、朝起きたら家トレも並行して実施していた時期には、体組成計に乗るのを忘れてしまうこともありました。同時にいくつものことを忘れずにやるのは難しいですね。

仕組みづくり

毎日体組成計に乗っていると、自分の組成がどんな変化をしているか知りたくなります。
そこで、GASでいくつかの項目の実測値と移動平均のグラフを作成し、その画像をTwitterに投稿するスクリプトを作成しました。詳しくは後述します。
ちょっと手間をかけてスクリプトを作ったので途中でやめたらもったいない。何より自分でスクリプト作って投稿されるグラフが見たい。という気持ちで続けることができました。

家トレ

カレンダーに○

使用していたアプリではトレーニングが完了するとカレンダーに◯がつきました。 f:id:kenzo_aiue:20191219094050p:plain ただやった日に○がつくだけなのに、それが並んで増えてくるとどうしても途絶えさせたくなくて続けていたところがありました。

成果を報告

友人と毎日成果を報告し合うことで、やらないといけない雰囲気になっていました。 時に褒め合い、時に煽り合うことで互いにモチベーションを高められていました。 ただし、これについては双方のモチベーションや回数に偏りがある場合にはうまく噛み合わないこともありました。

充実感・体の変化

他の2つと比べて時間もかかり負荷も高い家トレでしたが、その分をこなしたときの充実感は高く、毎日やることで「自分、めっちゃやってる」感を得られていました。
これは意外と大事で、休みの日にこれ以外なにもしなくても、「今日は筋トレしたから充実した日だった。 」と思えるくらいのパワーがありました。
また、やっているうちにお腹周りがすっきりしてくる等の体の変化を感じられるようになってきました。

起きたらやる

これも上記の体組成記録と同様、朝目が覚めたらやる形式でやっていました。
筋トレ → プロテイン → シャワーという流れができ、ちゃんと目も覚めるため、うまく続けられていました。
しかし、上記にもあるように、他の朝起きたらシリーズと被ってしまい、その習慣の実施を忘れてしまうこともありました。

高負荷(ダメだった理由)

一度は習慣となっていた筋トレアプリでしたが、一旦離れてしまった後に再開する際にきつめのトレーニングから始めてしまったため、「これからこんなんやってくの、、無理」という気持ちになってしまいました。こうなってしまうと再び習慣化するのは難しく、数日で終わってしまいました。

習慣化させるには

人を巻き込む

一人だけで新しいこと*1を始め、習慣化させて実施し続けるのはかなり難しいことだと思います。
しかし、上記の理由にいくつかある

  • 人に宣言する
  • 人に見てもらう
  • 人と競う

のように他の人を巻き込むことで、「見られているしやらないと」だったり「あいつには負けてられない」というように、始めることや続けることに対するモチベーションを高めることができます。
私はこれによってハードルをかなり下げることができました。ぜひ何かを始める際には身近な方を巻き込んでやってみてください。

負荷を減らす

しんどいものはなかなか続きません。
負荷の高いものでも一度や二度なら我慢できるかもしれませんが、それを長期的に継続していくのはかなり難しいと思います。*2
逆に言うと、初めのうちはかなり簡単なこと(こんなんでいいの?くらい)から始めると、比較的簡単に続けることができると思います。
負荷を高めるのはそうして簡単なことを続けられた後でよいと思います。

もったいない

「もったいない」という気持ちも習慣を続けていく上では役に立ちました。
上記の例であげた「せっかくスクリプトを作ったし使わないと」「せっかくここまで◯をつけたし途切れさせたくない」といった、せっかくやってきたのにもったいないという気持ちによって続けられた部分はありました。
一見するとサンクコストの説明で悪い例として使われそうなこの心の動きも、良い行動の習慣化にはうまく利用できました。
習慣化のためにはちょっとひと手間かけたり、自分のやってきた積み重ねが見えるようになっていると、離脱の防止になるのでおすすめです。

まとめ

習慣化することは気持ちの勝負のようなところがあります。ですので、いかに自分がそれをやる気持ちを高められるか、やりたくない気持ちを抑えられるかが決め手となります。上記の内容もほぼそのどちらかに当てはまるものとなっています。
今回は私が習慣化してきた内容を振り返り、その要素をご紹介いたしました。これがみなさまの習慣化に少しでも役立てばと思います。
また、これらは個人の経験ベースの話なので、心理学等に基づいた習慣化の話とは同じところも異なるところもあり、見比べてみても面白いと思います。

おまけ

体組成のTwitterへのグラフ投稿の自動化

体組成計記録において作成した、Twitterに投稿した毎日の体組成データを元にグラフを描画し、その画像をTwitterに毎週投稿するスクリプトの詳細です。 基本的にはGASで下記のことをやっている感じです。

  • Twitter APIの使用準備
  • 毎日の投稿を蓄積

    • 自分のTwitterの投稿から特定のタグをついたものを取得 https://api.twitter.com/1.1/statuses/user_timeline.json?user_id=TwitterのID&count=20&trim_user=t

    • パースして項目毎にスプレッドシートに保存 f:id:kenzo_aiue:20191219094353p:plain

    • ついでに平均値を計算してそれも保存 f:id:kenzo_aiue:20191219094410p:plain
  • 毎週グラフをTwitterに投稿

実際に使っているコードに近いものがこちらです。

TANITAの体組成計連携Tweetを元に作成したグラフを投稿 · GitHub

おわりに

明日はAndroidエンジニアのtummyさんによる「何もわからない状態からいち早く脱するためのコードリーディング法(Android 版)」です。お楽しみに!

delyではエンジニアをめちゃめちゃ募集中です。ご興味ありましたらぜひこちらから!

delyの開発について知りたい方はこちらもあわせてご覧ください!

*1:難しかったり苦痛を伴うものです。ゲームとか楽しいから習慣化するのめちゃ簡単ですもんね。

*2:もちろん他のやる理由があればその限りではありません(お金を払って通うジム、業務上やらないといけないことなど)

Redashを便利に使うためのTips3選

はじめまして。

dely, Inc. の @sakura です。
本記事では、Redash の使用状況及び便利なTipsを3つ紹介します。

f:id:sakura818uuu:20191219073915p:plain:w300

この記事はBIツール研究所 Advent Calendar 2019 - Qiitaの19日目の記事です。

社内のBIツール使用状況

f:id:sakura818uuu:20191218174701p:plain
Redash公式のトップページ

社内のBIツールは主に Redash を使用しています。
バージョンは7系を使用しています。

過去には Metabase と併用していたこともあるのですが、同じ用途のものが複数あることで「どんな時にどっちを使用するの?」など利用者の混乱を招いてしまっていたため現在は Redash のみを使用しています。

誰が使用するか

エンジニア、マーケ、調理部、セールス など色々な人が使用します。
クエリを書くのはエンジニアやマーケが中心です。

どのくらいのペースでクエリが増えるか

入社した当初(約2年前)はRedashにほぼクエリがなく、
「何をお手本にクエリを書けばいいんだ…」と絶望したことを覚えています。
あれから会社の人数も増え、知見もどんどん溜まり、クエリを書く人も増え、
今では毎日クエリが増えていっています

1日で数個〜数十個増えます。
自分の場合、今年は200個以上クエリを書きました。

Redashのここが便利

Redashを使っていて、便利で頻繁に使用するTipsを紹介します。

クエリパラメータ

f:id:sakura818uuu:20191218180223p:plain
クエリパラメータを日付に適用している例

公式ドキュメント https://redash.io/help/user-guide/querying/query-parameters

クエリパラメータとは、クエリの一部をパラメータ化して、クエリ枠外にそのパラメータに値する数値や文字列をいれるだけで、 クエリを実行できる優れものです。

これの何が良いかというと、普段クエリを書かない方にとってとても操作が簡単になることです。
あの初めて見るものにとっては不思議な文字列であるSQLを直接触らなくて済むというのは心理的負荷がぐっと下がります。

私はこの機能を頻繁に利用しています。

f:id:sakura818uuu:20191218181119p:plain:w250
クエリパラメータを使用したクエリを共有している例

クエリパラメータはとても便利なのでぜひ一度使ってみてください。

ダッシュボード

f:id:sakura818uuu:20191218182006p:plain
公式ドキュメントdashboard-editing内にある画像 ダッシュボードの例

公式ドキュメント https://redash.io/help/user-guide/dashboards/dashboard-editing

ダッシュボードは複数のクエリの結果をまとめて、1ページ内にカスタマイズして見ることができる機能です。
クエリの結果の表示の仕方は、表形式かグラフ形式か選択することができます。

全体を俯瞰して結果を確認したいときや新機能の結果を複数のデータから判断したい時などにダッシュボードを使用すると便利です。

私の使用例をあげると、クラシルの検索機能を定期的に監視する用にダッシュボードを使用しました。
キーワードの検索回数のグラフ、OS別の検索離脱率のグラフ、検索結果が0件だったキーワードの表など検索に関する様々な情報についてまとめました。
毎日確認するのでも1ページにまとまっているので確認が非常に便利でした。

クエリのお気に入り登録

地味すぎてこの機能を使っている人は思ったよりそんなに多くないかもしれません。
その名の通り、クエリをお気に入り登録でき、お気に入りしたクエリ一覧を見ることができる機能です。

この機能はRedashのバージョン5から導入されました。
公式ドキュメント https://redash.io/help/user-guide/dashboards/favorites-tagging

お気に入りする方法は、該当クエリのクエリタイトルの横にある星をクリックするだけです。
白枠の星から黄色になったらお気に入りした証拠です。

f:id:sakura818uuu:20191218184530p:plain:w250
右側に表示されているこの枠のFavoritesを押すと、お気に入りしたクエリ一覧が確認できる

お気に入りしたクエリを見るには、ホーム画面もしくはQueriesを押した時にでる右枠のFavoritesを押すと確認できます。

Redashにクエリが溜まれば溜まるほど、「あれ?あのクエリどこいったっけ」「検索してもあのクエリ見つからない」 ということが多くなります。頻繁に使用するクエリやストックしておきたいクエリにはお気に入りをしておくと便利です。

まとめ

本記事では、Redashの使用状況及び便利に使うためのTips3選を紹介しました。

BIツールを導入しても知らない機能っていっぱいありますよね。
私もまだまだRedashの知らない機能があると思うので、発掘して便利に活用していこうと思います。

さいごに

最後に告知です。delyではエンジニアを絶賛募集中です。
ぜひお気軽にご連絡ください。

https://www.wantedly.com/projects/329047

 

 

Vue.jsでカスタムディレクティブを使ってユーザーの「見てる」を可視化する

f:id:srrn:20191218131825j:plain

目次

はじめに

こんにちは、dely株式会社 開発部の白石(しらりん)です。
2019年新卒として入社し、現在Webフロントエンドエンジニアを担当しています。

昨日はiOSエンジニアのtakaoさんが、「個人アプリの開発で陥った6つの失敗とそこから学んだやらないことの重要性」という記事を書いてくれました。
本記事は dely Advent Calendar 2019 18日目の記事になります。

qiita.com

本記事のテーマは、タイトル通りVue.js(以下Vue)でユーザーの「見てる」を可視化することです。 delyに入社して実際に行った業務を通して考えたこと・学んだことをなぞりながら書いていきます。

この記事内では以降「ユーザーの見てる」を「インプレッション」と表現します。

とある日のこと

入社して1ヶ月半ほど経った5月半ば、この頃の自分の業務はOJTを兼ねたクラシルWebのリファクタリングが中心で、slim-railsで記述されていたコードをTypeScript + Vue + Node.js(+ vue-server-renderer)でSPA・SSR対応したものに書き換えたりしていました。
そんなある日、Web開発チームにビジネスサイドでWebの広告担当をしているAさんからこんな依頼がきました。

Aさん「(とあるページ)の広告枠が全然ユーザーの目に留まっていない気がする。ユーザーがページのどこを見ているのか調べる仕組みが欲しいです。」

その依頼自体は先輩フロントエンドエンジニアのOさんに来ていたのですが、そのやりとりを盗み聞きしてた自分が、

「そのタスク、おもしろそうなのでください^^」

と迫り奪取に成功、初めて分析系のタスクを担当することになりました。 タスクをもらえたのは良いものの、どう実装しようかなと悩んでいたところにOさんがアドバイスをくれました。

Oさん「これならカスタムディレクティブを使うといいかも。」
自分「?」

Vue歴 ≒ 社会人歴な当時の自分はカスタムディレクティブを知りませんでした。

カスタムディレクティブとは

ディレクティブには「指令・命令」という意味があり、Vueにおけるディレクティブについて以下のサイトではこのように説明されています。

ディレクティブとは、 DOM 要素に対して何かを実行することをライブラリに伝達する、マークアップ中の特別なトークンです。

012-jp.vuejs.org

Vueを使われている方にはお馴染みのv-ifv-forなど、接頭辞にvが付くこれらがVueのディレクティブです。
(-より後ろはディレクティブIDといいます)

そしてこのディレクティブは自分で作ることができ、それをカスタムディレクティブといいます。
詳しい内容は以下を参照してください。

jp.vuejs.org

やりたいこと

ページがユーザーにどこまで読まれているかを計測する必要があったとして、恐らくそれを実現する一番単純な方法は、ページの高さに対してスクロールされた高さで割合を求めることだと思います。しかし、これではスクロールされた位置までに表示されていた要素がユーザーに見られていたかは正確に分かりません。
例えば、とあるページが上から順にa, b, cという要素で構成されている時、cの位置までスクロールされているからといってaとbがユーザーに見られているとは限りません。aとbをスクロールで飛ばしてcを見ているかもしれないからです。

そこで、インプレッションを計測したい特定の要素の高さと幅の指定割合が画面内に指定秒数の間継続的に表示されていることを一度だけ検知できるカスタムディレクティブを実装することにしました。

クラシルのデータ分析基盤

この記事ではカスタムディレクティブを使ってインプレッションを検知するサンプルを実装しますが、データの送信や保存に関する実装には言及しません。参考までにクラシルではこんな感じでやってるよというのを簡単に書いておきます。

f:id:srrn:20191218130320p:plain
構成

クラシルで使用している分析基盤はAWSを利用しており、運用するイベントはスプレッドシートに定義するようにしています。
スプレッドシートに定義したイベントは、特定のプリフィックスをつけたブランチをpushすることでコードを自動生成することができ、そのブランチを取り込み(またはそのブランチ上で)開発していきます。

f:id:srrn:20191217221614p:plain
イメージ図

イベントは、

  • イベントの識別子(必須)
  • イベントのカテゴリ(任意)
  • イベントのパラメーター(必須)

といった感じで値を定義できるようになっており、1行が1イベントになります。イベントパラメーターにはTypeScriptの型を指定することができ、複数イベントで使いまわせるものや膨らみがちなものはイベントパラメーターの型定義用シートがあるのでそちらで管理するようにしています。
その他にも、イベントおよびパラメータの詳細説明や開発ステータス(0だったら実装中、1だったら実装完了(運用可能))などを記述し、実装者以外のメンバーと認識を合わせやすいように運用しています。

この分析基盤を活用し、サービスをより良くするために様々な施策を回しています。

以下は今回のインプレッション計測のイベント定義の例です。

イベント定義シート

列の名前 行の値
イベント識別子 element_imp
イベントの説明 指定した要素のインプレッションを計測するイベント
イベントパラメーター ImpTargetElementName imp_target_element_name
int? index
イベントパラメーターの説明 imp_target_element_name...要素の名前、必須
index...要素が表示される順番(0始まり)、任意

日付や実行されたページのパスなどを生成するベースの分析イベントを継承したイベントが生成される

イベントパラメーター型定義シート

列の名前 行の値
型名 ImpTargetElementName
key carousel_slide
ads
horizontal_scroll_item
card_layout_item
value carousel_slide
ads
horizontal_scroll_item
card_layout_item

↓ 列挙型が生成される

export enum ImpTargetElementName {
  carousel_slide = 'carousel_slide',
  ads = 'ads',
  horizontal_scroll_item = 'horizontal_scroll_item',
  card_layout_item = 'card_layout_item',
}

サンプル実装

それでは実際にサンプルを実装してみます。

カスタムディレクティブのコード

ここでは分かりやすくするために、インプレッションの条件を満たした要素の色を変更するようにしています。

import _Vue, { PluginFunction, VNode } from "vue";
import { DirectiveBinding } from "vue/types/options";
import { ImpTargetElementName } from '../enums/autogen/ImpTargetElementName';

export const impressionDerective = {
  inserted: (el: HTMLElement, binding: DirectiveBinding, vnode: VNode) => {
    const { name, index, time, threshold } = typeof binding.value === 'object'
      ? binding.value
      : { name: binding.value, index: null, time: null, threshold: null };

    const handler = (() => {
      let timer = null as null | number;
      let isExecuted = false;

      return (entries: IntersectionObserverEntry[]) => {
        if (!isExecuted) {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              const key: keyof typeof ImpTargetElementName = name;
              if (!(key in ImpTargetElementName)) {
                console.error(`${key}はImpTargetElementNameに定義されていません`);
                return;
              }

              timer = window.setTimeout(
                () => {
                  if (timer) {
                    /*
                      データを送信する処理
                      例)
                      vnode.context.$pushLog({
                        name: EventName.element_imp,
                        params: {
                          imp_target_element_name: ImpTargetElementName[key],
                          ...(index == null ? {} : { index }),
                        },
                      });
                    */
                    el.classList.add("isActive");
                  }
                  isExecuted = true;
                  timer = null;
                },
                time || 1000
              );
            } else if (timer) {
              clearTimeout(timer);
              timer = null;
            }
          });
        }
      };
    })();

    const observer = new IntersectionObserver(handler, { threshold: threshold || 0.5 });
    const observeHander = () => observer.observe(el);
    const unobserveHandler = () => observer.unobserve(el);
    const removeAllHandlers = () => {
      el.removeEventListener("impPluginObserve", observeHander);
      el.removeEventListener("impPluginUnobserve", unobserveHandler);
      el.removeEventListener("impPluginRemoveAllHandler", removeAllHandlers);
    };

    el.addEventListener("impPluginObserve", observeHander);
    el.addEventListener("impPluginUnobserve", unobserveHandler);
    el.addEventListener("impPluginRemoveAllHandlers", removeAllHandlers);

    el.dispatchEvent(new CustomEvent("impPluginObserve"));
  },
  unbind: (el: HTMLElement) => {
    el.dispatchEvent(new CustomEvent("impPluginRemoveAllHandlers"));
  }
};

const install: PluginFunction<never> = (Vue: typeof _Vue) => {
  Vue.directive("imp", impressionDerective);
};

export default install;
};

const install: PluginFunction<never> = (Vue: typeof _Vue) => {
  Vue.directive("imp", impressionDerective);
};

export default install;

Vueコンポーネントに適用してみる

ページっぽいコンポーネントを作り、カスタムディレクティブを適用してみます。

任意のファイルでVue.use(インポートしたv-impプラグイン)する。

<template>
    <div class="SpRoot">
      <div class="SpRoot-carousel">
        <Carousel
          :per-page="1"
          :autoplay="true"
          :loop="true"
          :autoplayTimeout="5000"
          paginationPosition="bottom-overlay"
          paginationColor="#ccc"
        >
          <Slide v-for="(item, i) in slideItem" :key="i">
            <div
              v-imp="{
                name: 'carousel_slide',
                time: 2000,
                threshold: 1,
                index: i
              }"
              class="SpRoot-carouselSlide"
            >
              Slide {{ i }}
            </div>
          </Slide>
        </Carousel>
      </div>
      <h2>Section Title</h2>
      <div class="SpRoot-horizontalScroll">
        <div
          v-for="(item, i) in horizontalScrollItem"
          :key="i"
          v-imp="{ name: 'horizontal_scroll_item', index: i }"
          class="SpRoot-horizontalScrollItem"
        >
          Card {{i}}
        </div>
      </div>
      <Ads v-imp="'ads_1'" />
      <h2>Section Title</h2>
      <div class="SpRoot-cards">
        <div
          class="SpRoot-card"
          v-for="(item, i) in cardItems"
          :key="i"
        >
          <div v-imp="{ name: 'card_item', index: i }" class="SpRoot-cardContent">Card {{i}}</div>
        </div>
      </div>
    </div>
</template>

インプレッションを取りたい要素にv-impディレクティブを付与し、imp_target_element_nameに定義済みの値を設定します。 デフォルトでは指定した要素の50%が画面内に1秒以上表示されていることをインプレッションとみなすように条件にしています。

<Ads v-imp="'ads'" />

イベントパラメーターにインデックス値を含めたりインプレッションとみなす条件を変更したいときは、v-impディレクティブに対して以下のようにイベント名を含めたオブジェクトでオプションを指定することができます。以下の例では、カルーセルのスライド要素の100%が画面内に2秒間継続的に表示されていた場合をインプレッションとみなすようにオプションを指定しています。

<Slide v-for="(item, i) in slideItem" :key="i">
  <div
    v-imp="{
      name: 'carousel_slide',
      time: 2000,
      threshold: 1,
      index: i
    }"
    class="SpRoot-carouselSlide"
  >
    Slide {{ i }}
  </div>
</Slide>

実際に動かしてみるとこんな感じになります。

ディレクティブを指定した要素の50%が継続的に1秒以上(スライドは100%が2秒以上)画面内に表示された時色が変わったのが確認できました。

クラシルWebに実際に導入してみた結果

f:id:srrn:20191217114437p:plain
実際に導入し、グラフ化してみた

こちらが実際にクラシルWebの本番環境のとあるページに導入し、計測した値をグラフにしたものです。 青い棒はカスタムディレクティブを適用した要素のインプレッション数で、一番左がページトップの要素で左から順にページの上から表示される要素となっています。赤い折れ線はユーザーの離脱率を表しています。 当然ページの下に行くほど離脱率は高くなっていきますがこのページは特にその傾向が強く、目を止めて欲しいコンテンツにたどり着く前に80%近くのユーザーが離脱していることが分かったりしました。

この実装を通して学んだこと

このタスクをこなす以前の自分は「効率的な実装をしたい」とか「ここのUIやアニメーションもっとこだわりたい」とかエンジニアリングに対する意識にばかり頭がいきがちでした。
この機能を実装し、実際にユーザーの動きが目に見える形になって初めて、入社した時から言われていたデータを見ることの重要性を深く理解することができ、より良いサービスづくりをする上でどちらも欠かせないものだと気づきました。

まだまだ自分が見えている範囲は周りの先輩たちに比べて狭いですが、より良いものを作るために必要な技術を伸ばし、データを見る → どうしてそうなるのか考える(法則を見つけるための実装をする) → データを活かす実装をする → データを見る の積み重ねができるエンジニアを目指して頑張ります。

おわりに

現在delyではエンジニアを積極募集中です。フロントエンドエンジニアの方も大大大歓迎なので、興味のあるかたはぜひご応募ください。

www.wantedly.com

delyの開発部について少しでも気になる方は、ぜひこちらも読んでみてください。

明日はAndroidエンジニアのkenzoさんが、「エンジニアは体が資本でしょ。と思って始めた習慣とその続け方」という記事を書いてくれます。お楽しみに!

adventar.org

Rails初心者がハマったCapistranoの環境変数

こんにちは。
delyコマース事業部エンジニアのjohnです。
もともとは開発部でiOSエンジニアとしてクラシルのiOSアプリ開発をやっていましたが、今年のはじめから新規事業のコマース事業部でwebのフロントエンドやRailsアプリケーションとかいろいろと開発をしています。

この記事は「dely Advent Calendar 2019」の16日目の記事です。
昨日はSREの井上さんによる「10分で完成!WEBサイトパフォーマンス計測基盤 ver.2019」という記事でした。
tech.dely.jp

今回は、Capistranoを使ってRailsアプリケーションをデプロイしたときに環境変数でハマった話を書きます。
なかなか、これ系の記事が少なかったので、gemの中を見るところまでしてみました。
1つのサーバーを使いまわしてのデプロイの話です。インフラがコード化(Infrastructure as Code)され、使い捨てサーバー(Disposable Components)でのデプロイでは当てはまらないかもしれません。

1. Capistranoとは?

まず最初に軽くCapistranoの説明をします。

https://capistranorb.com にはこう書かれています。

A remote server automation and deployment tool written in Ruby.

https://github.com/capistrano/capistrano にはこう書かれています。

Capistrano is a framework for building automated deployment scripts. Although Capistrano itself is written in Ruby, it can easily be used to deploy projects of any language or framework, be it Rails, Java, or PHP.

リモートサーバーの自動化のデプロイツールのようです。
デプロイツールなので、Railsアプリケーションだけでなく、他の言語のプロジェクトでも使えるようですね。
また、Rubyで書かれているのでRubyのプロジェクトでは使いやすいですね。

オフィシャルのプラグインとして
Capistrano::Bundler
Capistrano::Rails

サードパーティのプラグインとして
Capistrano::Puma
Capistrano::yarn

などのRailsに関連するプラグインがあって、デプロイのプロセスの間にタスクを走らせることができます。

たとえば、Capistrano::Puma lib/capistrano/puma.rb#L90-L93では、プラグインをインストールすると

def register_hooks
    after 'deploy:check', 'puma:check'
    after 'deploy:finished', 'puma:smart_restart'
end

上記のようにdeployが終わったら、puma:smart_restartするようになっていて、再起動してくれるようになっています。
実際のpuma:smart_restartはこちらに書いてあります。
Capistrano::Puma lib/capistrano/tasks/puma.rake#L59-L78

if test "[ -f #{fetch(:puma_pid)} ]" and test :kill, "-0 $( cat #{fetch(:puma_pid)} )"
  # NOTE pid exist but state file is nonsense, so ignore that case
  execute :pumactl, "-S #{fetch(:puma_state)} -F #{fetch(:puma_conf)} #{command}"
else
  # Puma is not running or state file is not present : Run it
  invoke 'puma:start'
end

起動していなかったらstartするようになっています。

次から、本題に入っていきます。

2. .bash_profileに記載した環境変数が読み込まれない

あらかじめ、sshでログインして、.bash_profileに環境変数を記載していました。
先ほどのpumaの例に示したとおり、デプロイ後に再起動したのに、環境変数が読み込まれていないという現象にぶち当たりました。
sshでサーバーにログインしてrails sするとちゃんと読み込まれているのに、なぜでしょう?🤔

理由は、公式の記事を見つけたので、こちらをみたらわかりました。
capistranorb.com

By default Capistrano always assigns a non-login, non-interactive shell.

Capistranoはnon-login, non-interactiveなshellだからでした。

f:id:JohnnyKei:20191211200326p:plain
BashInitialisationFiles
上記のように.bash_profileや.bashrcは読み込まれないという、shellのお話でした。

対応策としては、capistranoで定義されているdefault_envという変数に渡すということをしています。
default_envは最終的にはcapistrano内部で使っているsshkitに渡されるようです。
capistrano/sshkit lib/sshkit/command.rb#L149-L151

上記の方法をとった理由は、dotenvを本番環境で使いたくなかったのと、他に簡単にできる方法を思いつかなかったからです。もしかしたら別の方法があるかもしれません。
イマイチなのが、すべてのコマンドで渡されてしまっているところと、環境変数が増えてくると辛いことです。
環境変数に関しては、別のtaskの中で処理するのもありかと思っています。
以下はaws systems manager パラメータストアを使った場合のtaskになります。
aws systems manager パラメータストアについての説明は省かせていただきます。

require 'aws-sdk-ssm'

namespace :env do

  def ssm_client
    Aws::SSM::Client.new
  end

  def ssm_path
    "/path/to/ssm"
  end

  def fetch_parameters
    parameters = []
    is_finished = false
    next_token = nil
    until is_finished
      result = ssm_client.get_parameters_by_path(
        {
          path: ssm_path,
          recursive: false,
          with_decryption: true,
          next_token: next_token
        }
      )
      parameters += result.parameters
      next_token = result.next_token
      is_finished = next_token.nil?
    end
    puts "parameter count: #{parameters.size}"
    parameters
  end

  def ssm_env
    dict = {}
    fetch_parameters.each do |params|
      key = params.name.gsub(ssm_path, '')
      dict[key] = params.value
    end
    dict
  end

  task :set_default_env do
    set :default_env, fetch(:default_env).merge(ssm_env)
  end

end

deploy.rbでdeloyのtaskが呼ばれる前に呼ぶにしています。

invoke 'env:set_default_env'

3. pumaをrestartしても環境変数が読み込まれない

2. の対処でようやく環境変数が読み込まれるようにはなったのですが、環境変数を追加・変更してdefault_envに反映しても、デプロイ後の再起動では反映された状態での再起動が起きませんでした。

GitHubのIssueで聞いているものもあります。
github.com
どうやら原因はpumaの再起動の仕組みによるものでした。
puma/restart.md at master · puma/puma · GitHub
puma/signals.md at master · puma/puma · GitHub

pumaはシグナルを使ってプロセス間の通信を行っているようです。
再起動時は"SIGUSR2"を送っています。

bundle exec pumactl restart --state path/to/state_file

# lib/puma/control_cli.rb#L206
when "restart"
  Process.kill "SIGUSR2", @pid

起動中のプロセスがそれを受け取ってリスタートさせるようです。

#lib/puma/launcher.rb#L407
Signal.trap "SIGUSR2" do
  restart
end

起動中のプロセスで以下のメソッドが呼ばれ、再起動されます。
最終的にはKernel.exec(*argv)をして再起動しているようです。

# lib/puma/launcher.rb#L407

log "* Restarting..."
ENV.replace(previous_env)
@runner.before_restart
restart!

# /lib/puma/launcher.rb#L238
def restart!

argv = restart_args
Dir.chdir(@restart_dir)
argv += [@binder.redirects_for_restart]
Kernel.exec(*argv)

# このときのargsは
# ["path/to/ruby", "/path/to/bin/puma", "-C", "config/puma.rb", {:close_others=>true, 10=>10}]
# なので、pumaをもう一度起動するように実行しているようです。

この際、pumactl restart時に渡した変数は、起動中のプロセスへは渡されないので、環境変数は読み込まれないということになるようです。

対応として、pumaを停止して、再度起動するということをしています。

bundle exec cap production puma:stop
bundle exec cap production puma:start

一旦アプリケーションが停止してしまうので、あまり良い対処ではないのですがこれでしのいでいます。

今回ブログを書くにあたって、普段使用しているだけのpumaやcapustranoの中の実装を探ってみて、いろいろ新しい発見があって楽しかったです。 

明日は、iOSエンジニアのtakaoさんの記事です。お楽しみに!

最後に、コマース事業部では事業も開発も挑戦することが多く、エンジニアを募集しています!
また、デザイナーも大大大募集しているので、お知り合いの方で興味がありそうなかたがいれば、ぜひ教えてあげてください。
もし興味があるかたがいれば、お気軽にご連絡ください。

www.wantedly.com
www.wantedly.com

参考
Pumaの使い方 まとめ - 猫Rails
RackサーバーのPumaについて調べてみる - ゆーじのろぐ

 

10分で完成!WEBサイトパフォーマンス計測基盤 ver.2019

f:id:gomesuit:20191214231124p:plain

はじめに

本記事は dely Advent Calendar 2019 の15日目の記事です。

昨日は開発部サーバサイドエンジニアの高橋くんが「Rails6の複数データベースの仕組みと実装時にハマったところ」という記事を書きましたので是非読んでみてください。

tech.dely.jp

こんにちは!dely開発部SREの井上です。 本記事ではWEBサイトのパフォーマンスを定期的に計測する仕組みについて紹介をしたいと思います。

実は去年のAdvent Calendarでも同じような記事を書いたのですが、時代背景に沿って計測するツールをsitespeed.ioからLighthouseに変更したので理由も含めて紹介させてください。

基盤の構築においては下記のサービスやツールを利用しています。

  • AWS
    • CodeBuild
    • S3
    • Athena
  • Terraform
  • Lighthouse

前置きはいいから構築方法だけ知りたいという方は こちら にどうぞ!

目次

WEBサイトのパフォーマンスについて

もう今さら言うことでもないと思いますが、WEBサイトのパフォーマンスはユーザ体験に大きく影響を与えます。

直近(2019/12/10)でも下記のような記事をGoogleが公開しています。

developers-jp.googleblog.com

Chromeブラウザにおいて読み込みが速いサイトと遅いサイトを見分けるためのバッジが将来的に付けることを検討しているという内容ですが、サイトの読み込み速度をより一層ユーザとサービス提供者に意識させようという意思を感じます。

上記のような動きもあり、今後もWEBサイトのパフォーマンスを重視する傾向は変わることはないだろうと個人的には予想しています。

WEBサイトのパフォーマンスの計測手段

WEBサイトのパフォーマンスは重要だという話ですが、改善を行うためにもまずは現状を計測する必要があります。 2019年12月現在、WEBのパフォーマンスを計測する主な手段としては下記のようなものが存在します。

Google自身もパフォーマンス計測ツールについてまとめているので興味があれば見てみてください。とても参考になります。

developers.google.com

定期的に計測することの必要性について

WEBのパフォーマンスにおいてボトルネックを特定するだけであれば、その時点で数回の計測を実施すればよいですが、対策に伴う効果測定をしたい場合は対策後に再度計測を実施する必要があります。複数の対策を長期的に実施していく場合などは、対策を行う度に計測しなおす必要があります。

また、上記とは反対に何かのタイミングで意図せずパフォーマンスが悪くなっていないかを検知したいときやパフォーマンスが悪化した前後で何が原因だったのかなどを遡って確認したいこともあります。

上記のような要件を満たすためには、一定間隔で繰り返しWEBパフォーマンス計測を実施する必要があります。

定期的なWEBパフォーマンス計測について

定期計測における要件は下記のようになると考えます。

  • 一定の条件下において指標の変動を可視化することが可能

    • 計測する環境(例えばPCの性能やネットワーク環境)が起因して計測値がずれてしまうのであれば、何が原因で指標が上下しているのか正しく分からなくなるため、一定の条件化で計測し続けることが必要です。
  • パフォーマンスの問題のデバッグが可能

    • パフォーマンスが劣化したと考えられるタイミングにおいて、例えば問題がcssにあるのかjsにあるのかサーバのレイテンシにあるのかなどといった原因分析を可能とする指標が記録されているべきです。
  • ユーザ体験に関連するさまざまな指標が計測可能

    • ときにはリスク込みでパフォーマンスを犠牲にしなければいけない選択をする場合もあると思います。そういった選択をできるようにしておくためにも、パフォーマンスの劣化がどれだけユーザ体験に影響を与えているのかということも指標として記録できている必要があります。

定期計測において重要なのは「指標の変動が分かり」、「変動の原因がどこにあるのか明確にできること」だと考えます。

逆に言えば定期のパフォーマンス計測においては上記の要件を満たせば成り立つため、詳細なボトルネックの原因分析などを行うタイミングのパフォーマンス計測とは計測ツールを分けても問題ないと考えます。

定期計測に適したパフォーマンス計測ツールについて

2019年12月現在においてはLighthouse一択だと思っています。

LighthouseはGoogleがオープンソースで開発・公開しているWeb開発向けの診断ツールです。 下記のさくらのナレッジさんの記事が非常に詳しく解説してくれているのでこちらをご参照ください。

knowledge.sakura.ad.jp

WEBサイトのパフォーマンスを手軽に計測するためにGoogleが提供しているサービスがPageSpeed Insightsになりますが、PageSpeed Insightsの分析エンジンにはLighthouseが利用されていることがGoogleによって公開されています。

webmaster-ja.googleblog.com

WEBサイトのパフォーマンスを向上することの目的は当然ユーザ体験になるべきですが、その前段としてSEOが語られることは少なからずあると思います。そういったときは大抵の場合、指標のマスターとしてPageSpeed Insightsが挙げられます。定期計測において計測する指標や数値は可能な限りPageSpeed Insightsのものと一致させておいたほうが意思決定の根拠として扱いやすいのは間違いありません。

その証拠として前述で挙げているツールのほとんどが内部でLighthouseを利用する手段を用意しています。

www.sitespeed.io

www.webpagetest.org

去年の記事においてはsitespeed.ioを測定ツールとして採用しましたが、上記の理由からLighthouseを使った計測にリプレースしました。

定期計測に適したパフォーマンス計測手法について

定期計測においては前述の通り計測する環境に左右されないことが必須になるため、ローカルPCの環境に依存するツールを用いた測定手法は適していません。また人間が定期的にツールを実行するのは現実的ではないため、自動で定期的にツールが実行される仕組みが必要になります。

計測ツールにLighthouseを使う場合でも、Lighthouseを「計測する環境に左右されず」かつ「自動で定期的に動作させる」ための機構は別途考える必要があります。

PageSpeed Insights API について

PageSpeed Insights API を使うことで環境に左右されずLighthouseによる計測を行うことが可能です。

developers.google.com

PageSpeed Insights API については低頻度であれば、利用するにあたってAPIキーすら不要なため非常に手軽に使えます。しかし、Lighthouseの設定が出来ないため、例えばPCとmobileの二通りで計測したり、ユーザエージェントをカスタマイズして計測したりといったユースケースに合わせたカスタマイズが出来ないことがデメリットとして挙げれれるため今回は採用していません。

WEBパフォーマンス計測に特化した有料外部サービスについて

最近はLighthouseが組み込まれた有料サービスが出てきています。

こういったサービスを使えば「計測する環境に左右されず」かつ「自動で定期的に動作させる」を達成することが可能そうです。(使ったことがないので明確には言えません)

とても便利そうですがもちろん利用料がかかります。そもそも使ったことがなく評価出来ていないので検討の余地はありますが、今回はそこまで出来ていないので今後の課題にしようと思います。

calibreapp.com

speedcurve.com

Lighthouseを「計測する環境に左右されず」かつ「自動で定期的に動作させる」ための機構

やっと本題になりますが、Lighthouseを「計測する環境に左右されず」かつ「自動で定期的に動作させる」ための機構をAWSを使って考えてみました。

f:id:gomesuit:20191214200841p:plain

  • CodeBuild

Lighthouseを実行する実行環境として利用します。 CloudWatchEventによって定期的に動作します。

  • S3

Lighthouseによる計測結果の格納と、HTMLのホスティングとして利用します。

  • Athena

S3に格納された計測結果に対してSQLを実行するために利用します。

  • 可視化ツール

Athenaをデータソースとしてグラフ化などを行います。 可視化ツールに何を使うのかまではこの記事内には記載しませんが、delyではRedashを利用しています。

可視化例

delyでは例えば下記のSQLを実行することによって、

SELECT 
    'https://webperf-by-codebuild-58b94c1bbe1c0755.s3-ap-northeast-1.amazonaws.com/html/' || DOMAIN || '/' || device || '/' || category || '/' || YEAR || '/' || MONTH || '/' || DAY || '/' || hour || '/' || MINUTE || '/output.report.html' as link,
    DOMAIN,
    device,
    category,
    YEAR,
    MONTH,
    DAY,
    hour,
    MINUTE,
    CAST(YEAR || '-' || MONTH || '-' || DAY || ' ' || hour || ':' || MINUTE as timestamp) AS time,
    metrics.details.items[1].speedIndex / 1000.0 as speedIndex,
    metrics.details.items[1].firstContentfulPaint / 1000.0 as firstContentfulPaint,
    metrics.details.items[1].firstMeaningfulPaint / 1000.0 as firstMeaningfulPaint,
    categories.performance.score * 100 as performance_score,
    categories.accessibility.score * 100  as accessibility_score,
    categories."best-practices".score * 100  as best_practices_score,
    categories.seo.score * 100 as seo_score
FROM
    "<Terraformによって生成されたデータベース>"."lighthouse"
where
    DOMAIN = '{{ domain }}' and device = '{{ device }}' and category = '{{ category }}'
    AND CAST(YEAR || '-' || MONTH || '-' || DAY || ' ' || hour || ':' || MINUTE as timestamp) >= current_timestamp - interval '1' month
    order by CAST(YEAR || '-' || MONTH || '-' || DAY || ' ' || hour || ':' || MINUTE as timestamp) desc;

下記のような結果を得ることができます。

f:id:gomesuit:20191214201024p:plain

また上記の結果を下記のようにRedashを使ってグラフ化しています。

f:id:gomesuit:20191214201134p:plain

上記は弊社での可視化例ですが、Lighthouseによって様々なメトリクスが取得されているので色々な指標の相対的な変動を計測することが可能になっています。

構築方法

ここまで読んで頂いた方には伝わると思いますが、こんなに色々考えるのは面倒ですよね。 やりたいことはただ「WEBサイトのパフォーマンス計測を定期的に行いたい」だけなのに、どこまで考えなければいけないのだろう・・・という印象を持ってしまうと思います。

なのでこの基盤を10分で構築する方法を手順化しましたので紹介させて頂きます。

本記事で使用するコードは全てGithubにあげています。 github.com

前提

基盤構築に伴って最低限下記が実行環境に設定されている必要があります。

  • awscliのインストールとcredentialの設定

Terraformの利用に伴ってAWSのアクセスキーとシークレットキーの設定が必要です。

  • Terraformのインストール

  • IAMに対するTerraformを実行するための十分な権限設定

  • AWS CodeBuildとGitHubの接続

AWSコンソール上のCodeBuildにおいてプロジェクト設定時のページで下記のように、「GitHubアカウントを切断」と表示されている必要があります。されていない場合は「GitHubに接続」ボタンからGitHub連携を済ませてください。

f:id:gomesuit:20181201200034p:plain

CodeBuildとGitHubが接続されていないとTerraformのapply時にエラーになりますのでご注意ください。

準備

1.リポジトリのフォーク

下記のリポジトリをご自身のアカウントにForkします。Cloneして新しくリポジトリを作成しても大丈夫です。

github.com

2.Terraformのtfstateファイル管理用のS3バケットの作成

空のS3バケットを1つ作成します。 既存のものでも大丈夫ですが、tfstateファイルのkeyをコードにべた書きしているので新規でS3バケットを作成することをおすすめします。

3.terraform.tfvarsの作成

sitespeed.ioの出力したhtmlをS3のホスティング機能を使って参照するため、自身のIPでアクセス制限をかけます。 またCodeBuildがソースを取得する先を、手順で作成したリポジトリのURLに変更します。

サンプルファイルがあるのでコピーしてから、

$ cd terraform
$ cp terraform.tfvars.sample terraform.tfvars

ファイル内のリポジトリのURLを作成した自身のGitHubリポジトリに変更します。

# 作成した自身のGitHubリポジトリ
git_repository = "https://github.com/<user-name>/<repository-name>"

4.Terraform作業ディレクトリの初期化

下記のコマンドでTerraformの作業ディレクトリを初期化します。

$ make terraform-init
cd terraform && \
        terraform init
Initializing the backend...
bucket
  The name of the S3 bucket
  Enter a value: 

S3バケットの入力を求められるので手順2で作成したtfstate用のS3バケットの名前を入力します。

5.対象サイトの指定

計測するページのURLをurls.csvに記載します。 一行に ドメイン名,対象URL,カテゴリ の順で記載します。 複数行記載すると1回のCodeBuildの実行で複数のURLが計測されます。 カテゴリ名は対象URLをカテゴライズするために付与します。任意の文字列を入力してください。

例えば https://www.kurashiru.com/ を計測対象URLとする場合、下記のようになります。 トップページなのでカテゴリはtopとしました。 ※計測対象のWEBサイトはご自身が管理されているものを記載してください。

www.kurashiru.com,https://www.kurashiru.com/,top

構築

Terraformを実行します。

$ make terraform-apply

しばらくすると下記のようにコンソールに出力されるので、確認の後に「yes」と入力するとリソースの生成が始まります。

Plan: 16 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

上記コマンドで生成したリソースを削除する場合は下記コマンドを実行します。

$ make terraform-destroy

計測結果の参照

最大1時間待つか設定済みのCodeBuildを手動で1度実行するとS3に計測結果が置かれます。

HTML

S3バケットにhtmlが出力されているのでブラウザで開くことで計測結果を閲覧することができます。

例えばURLは下記のようなものになります。

https://s3-ap-northeast-1.amazonaws.com/webperf-by-codebuild-33916b229c860831/html/example.com/desktop/top/2019/12/14/10/37/output.report.html

f:id:gomesuit:20191214201406p:plain

SQL

AWSコンソールのAthenaでSQLを実行すると結果が取得できるようになっています。

SQL例

SELECT * FROM "webperf_by_codebuild_33916b229c860831"."lighthouse" limit 10;

f:id:gomesuit:20191214201520p:plain

カスタマイズ

Lighthouseの設定

Lighthouseには実行時に独自のヘッダを設定することが可能です。

https://github.com/GoogleChrome/lighthouse#cli-options

またネットワーク速度をエミュレートすることなども可能です。

https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md

検証環境などベーシック認証などをかけている環境へも上記のような設定を含めることで計測対象にすることが出来ます。

計測間隔の調整

計測のトリガーはCloudWatch Eventで行っています。そのためCloudWatch Eventの設定を変更することで計測間隔の調整をすることが可能です。

Terraformの該当ファイルは terraform/codebuild_trigger.tf です。schedule_expressionの cron(0 * * * ? *) を変更することで間隔を変更することが可能です。

反映するにはもう一度 make terraform-apply を実行します。

料金について

本記事の設定で実際にかかっている料金をご参考までにお伝えします。

現在4つのURLの計測を1時間毎に実行していますが、AWSの料金は1日$0.6程になります。格安かどうかで言うと活用次第だとは思いますが、サーバの運用や管理もいらないため、とりあえず動かしておくというのもありなのではないでしょうか。

さいごに

去年作ったWEBパフォーマンスの定点観測の仕組みをバージョンアップしてみました。

WEBパフォーマンスの計測はフロントエンド技術の進化や検索エンジンのアルゴリズムの変化に伴ってどんどん変化する分野だと思うので、常に同じ計測方法をし続けるのではなく定期的に見直していかなければいけない分野だと再認識しました。

本記事の内容はSREとしてのお仕事とは少しずれているのでSREのことが気になる方は下記の記事を是非読んでみてください!

tech.dely.jp

delyではSREを絶賛募集しています!クラシルの開発スピードを落とさず、信頼性も担保するという難易度の高い課題に挑戦したい方はぜひ声をかけてください!

www.wantedly.com

明日はdelyコマース事業部エンジニアのjohnさんが投稿します!お楽しみに!

delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ!

Rails6の複数データベースの仕組みと実装時にハマったところ

こんにちは、開発部の高橋です。

本記事はdely Advent Calendar 2019の14日目の記事です。

昨日はミカサ(acke_red)さんの「デザイン負債を返済する - クラシルのデザインの展望2020」という記事でした。

dely.design

目次

はじめに

10月の半ば辺りにRails6の複数機能を利用し、master/slave構成に対応した新規アプリケーションを本番リリースしました。

Rails5まではこのような対応する場合は他のgemを利用する必要がありましたが、これらのgemはActiveRecordの内部をオーバーライドしていたりするため、Railsのバージョンを上げた際に壊れるみたいなことはあるあるなのではないかと思います。

今回は新規アプリケーションということもあり、またRailsもちょうど6が出たタイミングだったため、gemを使うのではなくRailsの複数データの機能を利用してmaster/slave構成に対応することにしました。

基本的にRailsガイドに大抵の設定・実装項目は書いてあるのでそれを読みつつ実装することで組み込み自体はスムーズに行うことができました。

railsguides.jp

ただその一方で、一重にRails6の複数データベースといっても実態としては単にreader/writerへ外にもいくつかの機能が合わさっており、どの機能がどこに作用するかという部分がイマイチ明確ではなく混乱した部分もありました。

今回は自分なりに調べた複数データベースの仕組みや、導入した際にハマった部分を知見として共有できればと思っています。

複数データベースの仕組み

複数データベースに関連するActiveRecordの全体像

複数データベースを理解するにあたり、コネクション周りの全体像がいまいちよくわからなかったので全体図を作ってみました。

複数データベースの機能がActiveRecordのどの辺りに作用しているかという観点でまとめています。

紐付けは各要素同士の参照を表しています。

注: Rails 6.0.1時点でのものです。

f:id:jity:20191212174554p:plain

上記画像をもとにRails6の複数データベースの機能を大別すると以下のようになります。

  1. master/slave構成
    • コネクション自動振り分け機能(DatabaseSelector)
  2. 複数のデータベース利用

なおRails6.0ではシャーディングの機能はなく、シャーディングをしたい場合は依然としてoctopusのようなgemを利用するなど別途対応する必要があります。

今後機能入れる予定ではあるらしく、シャーディングを入れる準備段階の実装のPRなども上がっているようでした。

github.com

1. master/slave構成

上記の画像の①の部分を振り分ける機能に当たります。

writing/readingというロールに対して、ActiveRecord::ConnectionAdapters::ConnectionHandlerのインスタンスがそれぞれに作成されます。

またそれぞれのconnection_handlerの間にprevent_writesという参照がありますが、これはRDBへの書き込みをRails側で抑制する機能です。

実行スレッドに対して値が設定されます。

rails/connection_pool.rb at v6.0.1 · rails/rails · GitHub

つまり、状態としては以下の4通りがありえることになります。

  1. 向き先がWriter・書き込み可能
  2. 向き先がWriter・書き込み不可
  3. 向き先がReader・書き込み可能
  4. 向き先がReader・書き込み不可

通常は1と4の状態が利用され、2は書き込み直後の読み取り時などに利用されることになります。

3はその状態にはできますが、意味はありません。

利用方法

利用方法は以下のようにdatabase.ymlの各envの直下にwriter/reader名を記述し、ApplicationRecordにてconnects_toメソッドでreading/writingのロールをそれぞれのDBにマッピングして使います。

production:
  primary:
    <<: *default
    host: <%= ENV["DB_HOST"] %>
  primary_replica:
    <<: *default
    host: <%= ENV["DB_REPLICA_HOST"] %>
    replica: true

replica: trueにすると、ActiveRecordのConnectionAdapterに情報が渡され、そのコネクションを経由した書き込みクエリは実行できないようになります。

例えばMySQLでは

BEGIN,COMMIT,EXPLAIN,SELECT,SET,SHOW,RELEASE,SAVEPOINT,ROLLBACK,DESCRIBE,DESC,WITH以外が書き込みクエリに該当します。

rails/database_statements.rb at v6.0.1 · rails/rails · GitHub

モデルでの定義は以下のように抽象クラスに定義します。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :primary, reading: :primary_replica }
end

これでApplicationRecordのロード時にActiveRecord::Base.connection_handlersにwriting/readingのconnection_handlerが作成されます。

# - config.eager_load = false
# - bin/rails console実行直後(pry)

[Ruby:2.6.5][Rails:6.0.1] pry(main)> ActiveRecord::Base.connection_handlers.transform_values(&:class)
=> {:writing=>ActiveRecord::ConnectionAdapters::ConnectionHandler}
[Ruby:2.6.5][Rails:6.0.1] pry(main)> ApplicationRecord.connection_handlers.transform_values(&:class)
=> {:writing=>ActiveRecord::ConnectionAdapters::ConnectionHandler, :reading=>ActiveRecord::ConnectionAdapters::ConnectionHandler}
[Ruby:2.6.5][Rails:6.0.1] pry(main)> ActiveRecord::Base.connection_handlers.transform_values(&:class)
=> {:writing=>ActiveRecord::ConnectionAdapters::ConnectionHandler, :reading=>ActiveRecord::ConnectionAdapters::ConnectionHandler}

呼び出し方は以下のようになります。

ActiveRecord::Base.connected_to(role: :reading) do
  # 読み取り処理
end

ActiveRecord::Base.connected_to(role: :writing) do
  # 書き込み処理
end

接続しているコネクションもreadingロールとwritingロールで異なります

# default_connection_handlerはwritingロール
[Ruby:2.6.5][Rails:6.0.1] pry(main)> ActiveRecord::Base.connected_to(role: :reading) { ActiveRecord::Base.connection_pool.equal? ActiveRecord::Base.default_connection_handler.retrieve_connection_pool('primary') }
=> false

DatabaseSelectorの利用方法

上記で呼び出し処理を書きましたが、これを逐一実装の中で手書きするのは骨が折れますし、ヒューマンエラーも起きがちになりそうです。

そこでRailsはRackミドルウェアとしてDatabaseSelectorという仕組みを用意してくれています。

これは以下のような特性を持ちます。

  • HTTPリクエストがGET/HEADの場合はreadingロールを使う
  • GET/HEAD以外の場合はwritingロールを使う
    • writingへ向いてから一定時間内(デフォルト2秒)のリクエストに対しては、writingロールを使う
      • この間、書き込みは不可(prevent_writes == true)
      • この際、リクエスト元の判別はデフォルトでsession_store(cookie)を利用する
      • writingロールへの処理の最後に、session[:last_write]に現在時刻のタイムスタンプを挿入します

利用方法は以下のようにconfig/application.rbなどに設定します。

config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

自動切り替えの仕組みやリクエスト判別の仕組みは自前で実装することも可能で、その際は上に設定する自作クラスに変更すればOKです。

2. 複数のデータベースの利用

前掲の画像の②の部分の機能に当たります。

こちらはmaster/slave切り替え機能とは異なり、別のデータベースを利用するための仕組みとなります。 例えば、foo_productionというメインのDBとbar_productionという別のDBを併用することができます。

内部的にはConnectionHandlerの先のConnectionPoolを切り替える仕組みになっています。 同一スレッド内でprevent_writesをtrueにした場合、primaryとAnimalBaseの両方に影響があります。

[Ruby:2.6.5][Rails:6.0.1] pry(main)> ActiveRecord::Base.connected_to(role: :writing, prevent_writes: true) do
[Ruby:2.6.5][Rails:6.0.1] pry(main)>   Foo.first
[Ruby:2.6.5][Rails:6.0.1] pry(main)>   Animal.create!
[Ruby:2.6.5][Rails:6.0.1] pry(main)> end
  Foo Load (2.0ms)  SELECT `foos`.* FROM `foos` ORDER BY `foos`.`id` ASC LIMIT 1
ActiveRecord::ReadOnlyError: Write query attempted while in readonly mode: INSERT INTO `animals` (`created_at`, `updated_at`) VALUES ('2019-12-11 12:31:37.696328', '2019-12-11 12:31:37.696328')

利用方法

こちらもdatabase.ymlにDB名を設定し、モデルでマッピングします。(Railsガイドと合わせて名前はanimalsとします)

production:
  animals:
    <<: *default
    host: <%= ENV["ANIMAL_DB_HOST"] %>
    migrations_paths: db/animals_migrate
  animals:
    <<: *default
    host: <%= ENV["ANIMAL_DB_REPLICA_HOST"] %>
    replica: true

migrations_pathsでmigrationファイルの置き場を分けることができます。

class AnimalBase < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :animals, reading: :animals_replica }
end

class Animal < AnimalBase
end

このように実装すると、紐づくConnectionPoolが異なるようになります。

[Ruby:2.6.5][Rails:6.0.1] pry(main)> Foo.connection_specification_name
=> "primary"
[Ruby:2.6.5][Rails:6.0.1] pry(main)> Animal.connection_specification_name
=> "AnimalBase"

[Ruby:2.6.5][Rails:6.0.1] pry(main)> ApplicationRecord.connection_pool.equal? Foo.connection_pool
=> true
[Ruby:2.6.5][Rails:6.0.1] pry(main)> ApplicationRecord.connection_pool.equal? Animal.connection_pool
=> false

またdatabase.ymlに追加するとdbコマンドにもanimals用のものが追加されます。

bin/rails -T | grep db:
rails db:create                          # Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases
rails db:create:animals                  # Create animals database for current environment
rails db:create:primary                  # Create primary database for current environment
rails db:drop                            # Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases
rails db:drop:animals                    # Drop animals database for current environment
rails db:drop:primary                    # Drop primary database for current environment
rails db:environment:set                 # Set the environment value for the database
rails db:fixtures:load                   # Loads fixtures into the current environment's database
rails db:migrate                         # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)
rails db:migrate:animals                 # Migrate animals database for current environment
rails db:migrate:primary                 # Migrate primary database for current environment
rails db:migrate:status                  # Display status of migrations
rails db:migrate:status:animals          # Display status of migrations for animals database
rails db:migrate:status:primary          # Display status of migrations for primary database
rails db:prepare                         # Runs setup if database does not exist, or runs migrations if it does
rails db:rollback                        # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rails db:schema:cache:clear              # Clears a db/schema_cache.yml file
rails db:schema:cache:dump               # Creates a db/schema_cache.yml file
rails db:schema:dump                     # Creates a db/schema.rb file that is portable against any DB supported by Active Record
rails db:schema:load                     # Loads a schema.rb file into the database
rails db:seed                            # Loads the seed data from db/seeds.rb
rails db:seed:replant                    # Truncates tables of each database for current environment and loads the seeds
rails db:setup                           # Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)
rails db:structure:dump                  # Dumps the database structure to db/structure.sql
rails db:structure:load                  # Recreates the databases from the structure.sql file
rails db:version                         # Retrieves the current schema version number

利用する際は通常はconnects_toが設定されてあるモデル(上記の場合はAnimalクラス)をいつもどおり使います。

[Ruby:2.6.5][Rails:6.0.1] pry(main)> Animal.create
   (0.8ms)  BEGIN
  Animal Create (1.9ms)  INSERT INTO `animals` (`created_at`, `updated_at`) VALUES ('2019-12-11 13:20:03.478803', '2019-12-11 13:20:03.478803')
   (4.0ms)  COMMIT
=> #<Animal:0x00007ff4b817f890 id: 1, created_at: Wed, 11 Dec 2019 13:20:03 JST +09:00, updated_at: Wed, 11 Dec 2019 13:20:03 JST +09:00>
[Ruby:2.6.5][Rails:6.0.1] pry(main)> Animal.first
  Animal Load (2.9ms)  SELECT `animals`.* FROM `animals` ORDER BY `animals`.`id` ASC LIMIT 1
=> #<Animal:0x00007ff4b72f5b58 id: 1, created_at: Wed, 11 Dec 2019 13:20:03 JST +09:00, updated_at: Wed, 11 Dec 2019 13:20:03 JST +09:00>
root@localhost (13:20:44) [animal_development]> select * from animals;
+----+----------------------------+----------------------------+
| id | created_at                 | updated_at                 |
+----+----------------------------+----------------------------+
|  1 | 2019-12-11 13:20:03.478803 | 2019-12-11 13:20:03.478803 |
+----+----------------------------+----------------------------+
1 row in set (0.00 sec)

また一応conncted_toメソッドの引数としてdatabase引数を渡せるため、それ経由でActiveRecord::Base経由からもアクセスできます。(後述しますが非推奨です)

[Ruby:2.6.5][Rails:6.0.1] pry(main)> ActiveRecord::Base.connection.execute('select * from animals')
   (2.3ms)  select * from animals
ActiveRecord::StatementInvalid: Mysql2::Error: Table 'foo_development.animals' doesn't exist
[Ruby:2.6.5][Rails:6.0.1] pry(main)> ActiveRecord::Base.connected_to(database: :animals) do
[Ruby:2.6.5][Rails:6.0.1] pry(main)>   ActiveRecord::Base.connection.execute('select * from animals').to_a
[Ruby:2.6.5][Rails:6.0.1] pry(main)> end
   (4.9ms)  SET NAMES utf8mb4 COLLATE utf8mb4_general_ci,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (1.8ms)  select * from animals
=> [[1, 2019-12-11 13:20:03 +0900, 2019-12-11 13:20:03 +0900]]

ただし、このアクセス方法にはいくつか問題があります。

一つは、内部でestablish_connectionが呼ばれてConnectionPoolの再生成処理が走ることです。これによりパフォーマンス劣化などの問題が懸念されます。

また、上記のブロックを抜けても自動でprimaryに接続が戻らないという問題もあります。

[Ruby:2.6.5][Rails:6.0.1] pry(main)> ActiveRecord::Base.connected_to(database: :animals) do
[Ruby:2.6.5][Rails:6.0.1] pry(main)>   ActiveRecord::Base.connection.execute('select * from animals').to_a
[Ruby:2.6.5][Rails:6.0.1] pry(main)> end
   (4.9ms)  SET NAMES utf8mb4 COLLATE utf8mb4_general_ci,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (1.8ms)  select * from animals
=> [[1, 2019-12-11 13:20:03 +0900, 2019-12-11 13:20:03 +0900]]
[Ruby:2.6.5][Rails:6.0.1] pry(main)> Foo.first
  Foo Load (2.5ms)  SELECT `foos`.* FROM `foos` ORDER BY `foos`.`id` ASC LIMIT 1
ActiveRecord::StatementInvalid: Mysql2::Error: Table 'animal_development.foos' doesn't exist

そのため、自分でprimaryへ戻る処理を書く必要があります。

[Ruby:2.6.5][Rails:6.0.1] pry(main)> ActiveRecord::Base.connected_to(database: :animals) do
[Ruby:2.6.5][Rails:6.0.1] pry(main)>   ActiveRecord::Base.connection.execute('select * from animals').to_a
[Ruby:2.6.5][Rails:6.0.1] pry(main)> ensure
[Ruby:2.6.5][Rails:6.0.1] pry(main)>   config_hash = ActiveRecord::Base.resolve_config_for_connection(:primary)
[Ruby:2.6.5][Rails:6.0.1] pry(main)>   ActiveRecord::Base.establish_connection(config_hash)
[Ruby:2.6.5][Rails:6.0.1] pry(main)> end
   (1.9ms)  SET NAMES utf8mb4 COLLATE utf8mb4_general_ci,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (1.4ms)  select * from animals
=> [[1, 2019-12-11 13:20:03 +0900, 2019-12-11 13:20:03 +0900]]
[Ruby:2.6.5][Rails:6.0.1] pry(main)> Foo.first
  Foo Load (1.5ms)  SELECT `foos`.* FROM `foos` ORDER BY `foos`.`id` ASC LIMIT 1
=> #<Foo:0x00007ff4c8476d68 id: 1, created_at: Wed, 11 Dec 2019 13:35:49 JST +09:00, updated_at: Wed, 11 Dec 2019 13:35:49 JST +09:00>

一応できるってだけで、基本的には事前にconnectes_toで設定していたモデルから参照するのが良さそうという所感です。

アプリケーションでの実際の実装

今回自分が担当した新規アプリケーションではmaster/slave機能のみを利用し、またDatabaseSelectorによる自動振り分け機能も利用しています。

基本的にDatabaseSelectorに乗っかる形で問題なく稼働できていますが、

  • GETリクエストで作成・更新したいケース
  • 更新処理はないが、POSTリクエストで大量にリクエストをさばきたいケース

という2つの例外ケースがアプリケーションの要件上一部存在してしまっています。

これらを解決するために、現状は以下のようなようなメソッドをコントローラに追加しています。

def with_reader(&block)
  ActiveRecord::Base.connected_to(role: :reading, &block)
end

def with_writer(&block)
  ActiveRecord::Base.connected_to(role: :writing, &block)
end

これらをaround_actionなどを利用して必要な箇所で呼び出すことによってDatabaseSelectorでまかない切れないケースに対応しています。

開発時にハマった箇所

POSTのあとのGETでの更新処理で競合が発生

アプリケーションの仕様として、POSTリクエストが走ったあとで、GETリクエストでデータベースに更新がかかるケースがあったのですが、その際に状況によってエラーが出たり出なかったりするという現象が起きていました。

これはDatabaseSelectorでPOSTリクエストのあとに2秒の間、Rackミドルウェア上でprevent_writes = trueがセットされており、またRailsアプリケーション側でそれをfalseにする処理を挟んでいなかったためにエラーが出たり出なかったりしていました。

つまり、POSTのあと2秒未満のGET更新の場合はエラーが発生し、2秒以上経過した後にGETリクエスト経由での更新処理の場合にエラーはでず、時間要因で結果が変わっているという状況でした。

これに関してはissueにて議論がなされていました。

github.com

またその結果としてv6.0.1ではconnected_toメソッドの引数にprevent_writesが追加されています

Call `while_preventing_writes` from `connected_to` by eileencodes · Pull Request #37065 · rails/rails · GitHub

ただし、今回の場合はバージョンアップまだ行えてなかったため、コントローラーの処理でwriterに向ける際には以下のようにprevent_writesにfalseを入れる処理を追加しました。

def with_writer(&block)
  ActiveRecord::Base.connected_to(role: :writing) do
    ActiveRecord::Base.connection_handler.while_preventing_writes(false, &block)
  end
end

Rails6.0.1では以下のように書けます。(prevent_writesオプションがデフォルトでfalseなので)

def with_writer(&block)
  ActiveRecord::Base.connected_to(role: :writing, &block)
end

readingロールに対して更新していることがテストで気付きにくい

以下のissueでも議論されていました。

github.com

Railsにはuse_transactional_testsというオプションがあり、これをtrueにしているとDBへの更新処理はCOMMITされず各example後にROLLBACKされます。

これによってテスト後に毎回DBをTRUNCATEする必要がなく、テストのパフォーマンスも向上するため、できるだけtrueにしたまま開発したいという気持ちがあります。

このオプションをONにすると内部的にはreadingロールのコネクションプールがwritingロールのコネクションプールにすり替わるようになります。

これによりCOMMITせずともwritingロールで更新を行ったデータをreadingロールでも読み取ることができるようになります。

rails/test_fixtures.rb at v6.0.1 · rails/rails · GitHub

その一方で、コネクションへreplicaフラグが渡されなくなるため、テスト中にreadingロールへの更新処理を行っていてもActiveRecord::ReadOnlyErrorが発生しなくなります。

そのため、readingロールへ更新処理を行っていることが自動テストでは検知できませんでした。

今回の自分のプロジェクトでは規模的には小さかったのもあり、実機検証するタイミングで検知するという方針をとって開発を進めました。

一応use_transactional_testsを必要なテストに挿入して対応することもできそうではありますが、それがどこに必要なのかを判断する基準は各開発者の意識に依存するため実運用は難しそうな印象です...

まとめ

詰まった箇所なども書きましたが、自分の担当していたアプリケーションでは概ね良好につかえていた印象です。

これから複数データベースを使おうと思ってる方の参考になれば幸いです。

最後に

明日は開発部SREの井上さんの記事です!お楽しみに。

qiita.com

adventar.org

また、delyではRailsエンジニアを絶賛募集中なので興味のある方は是非是非。

speakerdeck.com

www.wantedly.com

CXOとVPoEへのインタビュー記事もあります。

wevox.io

初めてPMっぽいことやって失敗した件

f:id:sakura818uuu:20191129091914p:plain:w200

はじめまして。

sakura818uuuです。

この前、社内で初めてPMっぽい動きをしたら盛大に失敗したので 反省すべきことや学んだことを書きます。


この記事はdely Advent Calendar 2019の12日目の記事です。

Qiita: https://qiita.com/advent-calendar/2019/dely
Adventar: https://adventar.org/calendars/4134

昨日はiOSエンジニアのnancyさんが「Combine と RxSwift を比較してみた - dely engineering blog」を書いてくれました!

では本題に戻ります。

はじめに

料理動画アプリ『クラシル』のある機能Aを改善したいと思っていました。

そして、初めてPMっぽい動きをしたら盛大に失敗したので
反省すべきことや学んだことを書きます。

今回はあるMTGの一幕をお届けします。

※本内容は社内限定で公開していたものを一部改変してお届けしています。

反省すべきとこ

【その1】 会議のゴールを決めきれていなかった

f:id:sakura818uuu:20191211121033p:plain:w200
会議が最終的にどこまでいきたいかを詰めきれていなくて議論が右往左往してしまいました。

<なぜ起こったか>
なぜ起こったかは2つの理由があると思います。
一つは 想定していたゴールとは別の方向に議論の流れがいってしまったこと です。

会議を行う前、以下の2つをゴール(必ずマストで決めたい)として考えていました。

①機能Aを改善する必要性の認識合わせをし、機能Aを改善する方向にもっていく
②改善に伴う開発スタイルを決定する

①は認識合わせもある程度うまくいきました。しかし、②の議論は想定していた流れとは違う方向になりました。
これは②を決める前に議論すべきことがあったからなのですが、そっちの正しい方向に参加メンバーがひっぱってくれて感謝してます。ありがとうございます。

もう一つの理由は PMをびびっていたこと です。
はじめてPMっぽい動きをするにあたりステークホルダーにずっと悩んでいました。
主催者側としては②を早く決めたかった理由が
「長期化する開発スタイルになる場合、 ステークホルダーも多くなるのは確実 。そうしたら情報の認知・共有も段違いにむずかしくなるし、それに加えてそれだけの人数をまとめてひっぱっていくことが自分にはできるのか
なるべくステークホルダーを最小限にとどめてごりごり改善していってまずはすすめていこう」
みたいな個人の理由が大きいです。
これはめちゃくちゃ反省していて、よくなかったなと思います。すいません。

ステークホルダーが多くなろうが機能Aをよくすること、それに集中することだけを考えればよかったです
自分のPMスキルを理由にしてこういう雑念を考えるのはよくないな、と学びました

<次はどうすれば解決する?>
・ゴールを機能Aをよくすることに集中する
・ステークホルダーの人数は考えない
・MTGのゴールのスコープを短くする ゴールが思いつかない場合はメンバーに相談するのもいいかもしれない (最後のやつは「解のない会議のゴールってどうやったら決めたらいいんですかね?」とXXXさんに聞いて教えてもらった手法の一つです)

【その2】 兼務しすぎて一杯一杯

f:id:sakura818uuu:20191211120811p:plain:w200

会議のファシリテーター・書記・議事録を一人で兼務したらすごく一杯一杯になりました。

<なぜ起こったか>
偶発的に起こってしまった、ではなく会議前から 意図的に兼務しようと思っていました 。なぜなら、自分が言い出しっぺで施策をはじめたわけで、他の人に会議を参加してもらって、その上なにかをお願いするのは図々しいと思ったからです。

<次はどうすれば解決する?>
議事録は 会議の音声を録音する ことで解決しそうです。
もしかしたら 書記は誰かに任せてもよいのかも しれません。次やるときは誰かにお願いしてみようかと思います。

よかったこと

【その1】 余裕をもってスケジュールをセッティングできた

参加者全員のスケジュールを確認して、余裕をもってMTGをセッティングできたことはよかったです。

6/19 XX終了後、参加者にオフラインで機能Aの改善MTGを開催したい旨を伝える
6/19 slackでメンションをつけて通知

f:id:sakura818uuu:20191129082301p:plain:w300
6/19 slackで関係者にメンションを付けてアジェンダを共有している図

7/1 前日にメンションをつけてリマインド
7/2 MTG開催

【その2】 事前にデータを用意できた

データがないことで議論が止まる/曖昧になることを避けたかったので事前に調査してQiitaを書きました。自分がデータサイエンスチームに所属していて出来ることの一つでもあったので、それを生かせたのはよかったかなと思います。

f:id:sakura818uuu:20191129083509p:plain:w200

まとめ

反省から学んだ、次PMになったときMTGで気をつけること3つ

- MTGのゴールのスコープを短くしてでもよいので適切なゴールを決めること

- 議事録は音声をとること

- ファシリテーターと書記の兼務は難しいので、書記はどなたかに協力してもらう




明日はデザイナーのredさんが「デザイン負債を返済する - クラシルのデザインの展望2020」というタイトルでアップ予定です!

最後に告知です、delyではエンジニアを絶賛募集中です🙌
ぜひお気軽にご連絡ください。

https://www.wantedly.com/projects/329047