TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

587

この記事は BASE Advent Calendar 2021 の21日目の記事です。 devblog.thebase.in はじめに Payment Devグループの山本( @msysyamamoto )です。 この記事は私たちのグループで行っている読書会の紹介になります。これから読書会を開催しようとしている方や、いまの読書会を改善したいと思っている方の参考になれば幸いです。 こんな感じで行っています 参加者 基本的にはグループのメンバは全員参加になっています(もちろん、業務が忙しい場合などは欠席することは可能です)。現在は14名のメンバが参加しています。 グループの読書会となってはいますが、別のグループからの参加も可能だったりします。 体制 グループのメンバの中の一人が読書会のまとめ役となります。まとめ役は立候補制になっています。もし、立候補する方がいなかった場合は、グループのマネージャからの指名になります。 スケジュール 毎週1回、同じ曜日同じ時間に開催しています。時間は1回あたり1時間となっています。いつも同じ時間に開催することでリズムが生まれ、読書会を続けていきやすくなるのではないかと個人的には思っています。 場所 Zoom で行っています。 読書会は録画をしており、参加できなかった方は後でどのような内容だったかを確認できます。 選書の方法 まとめ役に委ねています。 多くの場合、まとめ役がいくつか本をピックアップし、参加者がそれに投票し読む本を決定します。ピックアップされた本以外で読みたい本があれば、まとめ役でなくとも選択肢に本を追加することができます。 まとめ役に委ねていますので、まとめ役が読書会で読みたい本があれば投票を経ずとも読む本を決定できたりもします。 参考までに今まで読書会で読んできた本を紹介します。2021年12月現在は『DNSがよくわかる教科書』を読み進めているところです。 リーダブルコード オブジェクト指向設計実践ガイド Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 テスト駆動開発 失敗から学ぶRDBの正しい歩き方 Web API: The Good Parts 決済サービスとキャッシュレス社会の本質 DNSがよくわかる教科書 本の難易度やページ数にもよるのですが、今までの実績からして大体1ヶ月から2ヶ月で1冊の本を読み終えています。 会の進め方 事前にやっておくこと 読む本が決定した時点で、読む範囲の決定と、その範囲は誰が発表者になるかを決定しておきます。発表者は1回の会で5人となっています。発表者はランダムで決定されるのではなく、参加者全員が同じくらいの回数発表者になるように決めています。 発表者は会が始まる前まで該当範囲を読み、気づき・疑問を弊社でドキュメントサービスとして利用しているKibelaに書いておきます。要約は書きません。 発表者でない方も事前に該当範囲を読んでおきます。 その会の司会者を決めます。司会者はボットによってランダムに決定されます(まとめ役 ≠ 司会者 となっています。ランダムなのでまとめ役が司会者になる場合もありますが)。 読書会本番 発表者がKibelaに記述した気づき・疑問を説明し、それに対してディスカッションして行きます。発表者は5人ですが、この進め方で毎回ちょうどよい時間に終わるか、少しオーバーするくらいの時間に収まっています。 以上が私たちのグループで開催している読書会の現在時点でのやり方になります。私が読書会に参加するようになったのは2021年7月なのですがその時とのやり方と現在のやり方を比べると、改善されている部分があります。 例えば発表者の決定方法ですが、以前のやり方はランダムで決定していたのですが、連続で発表者になったりする人が出たりするのでよくないよね、ということで今の方法に改善されました。 今のやり方がベストだとは思い込まずに、改善できそうなところはどんどん改善していき、よりよい読書会にできるといいと思っています。 よかったこと この記事の締めくくりとして、私個人の感想にはなるのですが、読書会でよかったと思うことを紹介します。 自分では読まなさそうな本を読むことができる 具体的に言いますと『決済サービスとキャッシュレス社会の本質』がそれにあたります。正直なところ、読書会で読む本のとしてピックアップされるまでその本を知りませんでしたが、決済に関わる仕事をしている身として、興味深く読み進めることができました。 自分とアンテナの張り方が違う人が集まることで、自分の知らなかった本を読むこと・知ることができるので、知識の幅を広げるのにいいと思いました。 コミュニケーションの場になる 私たちのグループは全員が同じプロジェクトを進めているわけではありません。同じプロジェクトに参加しているメンバとはよくコミュニケーションを取るのですが、別のプロジェクトに参加しているメンバとはそうではありません。 また、新型コロナウィルスの影響によりリモートで仕事をする機会が増え、グループのメンバと顔を合わせる機会が減ってきています。 それゆえ、同じグループの中でもお互いのことをよく知らないということがあるのですが、読書会がお互いのことを知るよい場所になっていると思います。
この記事は BASEアドベントカレンダー 21日目の記事です。 まえがき BASE BANK株式会社でエンジニア兼Engineering Program Managerをやっている 松雪( @applepine1125 ) と 永野( @glassmonekey ) です。 BASE BANKでは組織の拡大に伴って表出した課題を解決するために、プロダクトのデリバリー、クオリティに責任を持つEngineering Program Manager(以下EPM)という役割を導入しています。 今回はまだ馴染みのないであろうEPMについてと導入の背景、具体的な働きざまについてご紹介します。 Engineering Program Manager(EPM) とはなにか そもそもEPMという役割自体が日本で馴染みのないものだと思います。 Apple や Amazon 、 Meta(旧FaceBook) 社などで正式なポジションとして存在しており、ざっくり各社のjob descriptionから要素を抽出してみると、主に以 下のような役割を担っていることがわかります。 開発のリードや開発プロセスの改善 設定したリリーススケジュールへの責任とプロダクトのクオリティの担保 プロジェクト、プロダクトの技術的な阻害要因の排除 ステークホルダーと適切なコミュニケーションを取り、自身での意思決定や意思決定のエスカレーションの実施 Product Manager(以下PdM)や外部のステークホルダーと開発チームの間に入り、適切に開発のサイクルを回していくために必要な役割です。 組織によってはこれらの役割をProject Manager(以下PjM)、Engineering Manager(以下EM)、Tech Lead(以下TL)といった役割が担っていたり、もしくは名も無き仕事として誰かが対応しているのではないでしょうか。 PjM、TL、EMとEPMの違い PjM、TL、EMといった、一見EPMと近い役割がすでに存在する中でEPMという役割が新たに生まれたのはなぜでしょうか、PjM、TL、EMとEPMとでは何が違うのでしょうか。 それぞれ重なる部分もありますが、役割としては微妙に異なります。それぞれ見比べてみましょう。 PjMとEPM PjMはPJの完遂に責任を持つ役割です。 PJの要件定義や進捗管理、メンバー集めや、場合によっては予算を獲得してくるなど、PJ完遂のために様々な業務を行います。 EPMとの一番の違いは、技術的なバックグラウンドがなくてもPjMの役割を担える点です。 PjM自身が技術的なバックグラウンドを持っていたり、メンバー内にそういった判断材料を揃えることができるエンジニアがいると開発に関する意思決定がしやすく比較的円滑にPJが進行します。 しかしそういったコミュニケーションや意思決定ができないとPjMはただエンジニアが提示する開発スケジュールを飲み込むしか無く、根本原因にテコ入れできずにスケジュール遅延が発生するいわゆるデスマーチへと突入していってしまいます。 こうならないようにPjMの意思決定を補佐し、協力してプロダクトのリリースを行うEPMのような存在が重要になってきます。 組織によってはPjMを開発面から補佐する役割はTLが担うというケースも多いかもしれません。 TLとEPM 組織によってTLに期待する役割は様々だと思いますが、どの組織でも割と共通しているのはコード品質や設計、アーキテクトの面でチームや組織に影響を及ぼす存在であることかと思います。 EPMでもそういった側面を求められる場面はありますが、そこを主戦場とするメンバーがすでにいるのであればそのメンバーに役割を委譲しつつ、連携しながらプロダクトのデリバリーへの責務を全うするのがEPMの大きな特徴です。 EMとEPM EMは組織によってTL以上に多様な役割を持っています。 参考に、広木氏が提唱する マネジメントの4象限と強め/弱めのEMの定義 と照らし合わせてみると、 主にPeople, Team Managementを行う弱めのEMとEPMは補完関係になるのではないでしょうか。 採用や評価、日々の1on1なども含め、仕事を通した各メンバーの成長やチーム作りと向きあうEMと、メンバーそれぞれのスキルを最大限に生かしてプロダクトのデリバリーとクオリティを担保するEPMとでは、その役割の中で向き合う要素は似ているものの最終的な責任が異なります。 この両方を一人で担うのはスイッチングコストがとても高く難しい仕事となってしまいます。 下図のように、強めのEMがいれば弱めのEM、EPM含め様々なマネージャ業を一手に引き受けてもらうことはできますが、そういったスーパーマンはそうそういないため、弱めのEMとEPMそれぞれで責務を分割してフォーカスするのは有効な打ち手なのではないでしょうか。 なぜEPMが必要なのか ではそのようなEPMはどのようなケースで必要になるのでしょうか? 一つ考えられるケースとしては「目的別組織」のマネジメントの場合ではないでしょうか。 目的別組織とEPM 組織形態としてよく比較されるのが"機能別組織(職能別組織)"と"目的別組織(職能横断型組織)"です。 機能別組織の場合、PJ毎に各チームからメンバーが集まり、PJが終わると解散、各々別のPJへ・・・というのがよく見る流れではないでしょうか。 こういった組織運営を行っている場合、わざわざEPMなどを配置せずとも、PjMやEM、TLがEPMの役割も担うことでとりあえず目の前のPJを完遂させる事はできると思います。 しかし目的別組織になると、自分が所属するチームが受け持つ機能、プロダクト、領域と継続的に向き合えるようになります。すると開発プロセスやステークホルダーとの関わり方の改善も半永久で継続的に行う必要があったり、チームの経験を形式知として蓄積しやすくなるため、それらの活動を主な責務とするEPMを配置することは合理的ではないでしょうか。 特に特定の領域と継続的に向き合い価値提供していくためには、その領域の深いドメイン知識も必要になってきます。仮に開発プロセスやコミュニケーション力が高かったとしても、深いドメイン知識がないと適切なエスカレーションや開発スコープの変更判断などができないためです。 BASE BANKがEPMを据えた背景 ここからは具体例の一つとしてBASE BANKがEPMを据えた理由とBASE BANKでのEPMの働きざまについてもう少し深掘ってみます。 BASE BANKはショップオーナーが売上を立てたあとの活動に関わる機能やプロダクトを受け持っているチームです。 またBASE BANK社を設立した2018年はまだBASE社は目的別組織ではなかったため、BASE BANKはある意味BASE内での実験的な目的別組織(チーム)としてチームを成長させてきました。 そういった事も踏まえ、メンバーが増え受け持つプロダクトも増える中で、上記のようなEPMとしての責務を持つ役割が必要となったのは必然とも言えました。 BASE BANK黎明期 BASE BANKの立ち上げ期はざっくりエンジニアとPMが存在しており、明確に役割を細分化しなくても開発を回していける規模でした。 メンバーが増え、よりチームらしく エンジニアメンバーがjoinし、人数が少しづつ増え、受け持つ機能やプロダクトも増えててくると、ある程度意識して開発プロセスや採用、評価を回していく必要が出てきました。 そこでTeam Managementを行うEM、TLといった役割が生まれ、PMと協力してTeam Management、People Managementを行うようになりました。 このときはまだ、各機能の開発の運営はTLやPMが並行して受け持つ形になっていました。 ピザ2枚 を分け合う以上の規模に 更にメンバーの採用が進み、関わるプロダクトもYELL BANK、BASEカード、振込申請などさらに増え、各プロダクトのグロースにも本格的に着手しなければならなくなりました。 すると、それまで並行して複数プロダクトの開発プロセスを主導していたEMやPdMが本来の責務である組織全体の事業推進やTeam、People Managementに注力せざるを得なくなり、各プロダクトの開発プロセスの主導やPdM、各ステークホルダーとの橋渡し役として開発を主導するEPMという役割が生まれました。 ちなみに、BASE BANKのエンジニアは各々の強みを活かしつつ開発の全てのライフサイクルに関わる フルサイクルエンジニア としての能動的な働きを求められます。 そのため、EPMは主導といってもマイクロマネジメントをするわけではなく、開発チームとしての目的やビジョンを掲げつつ各メンバーと協力して日々の運営や改善をしたり、メンバーがより能動的に動けるようなお膳立てをします。 それぞれのプロダクトごとのEPMとしての役割 では、EPMのプロダクトごとの具体例を紹介します。 現時点では、主にBASE BANKとしては BASEカード と YELL BANK がメインのプロダクトです。 決済のドメイン知識を持つ松雪( @applepine1125 )がBASEカード、 元々YELL BANKの開発をしていた永野( @glassmonekey )がそれぞれEPMとして開発を主導しており、それぞれのEPMの役割について紹介します。 BASEカードにおけるEPM BASEカード のEPMとして、現在以下のような仕事を行っています。 - スプリントの運営 - QAの仕込み - 社内外のステークホルダーとのコミュニケーション - 関係各所へ決済の知識やBASEカードの仕様をインプット - 意思決定のための技術面または仕様面(特にカード決済の知識)の判断材料の提供、場合によっては自身で意思決定 https://cp.thebase.in/basecard cp.thebase.in BASEカードはショップの出金に直接関わる機能のため、内部統制などを責務とする Corporate Solutions Engineer(CSE) やリスクマネジメントチームとの関わりが多く、日頃から密にコミュニケーションを取っています。 また、機能実現のために外部企業が提供しているAPIを利用しており、仕様面の調整や、精算上の証跡を用意するためにBASEの経理との連携も密に行う必要があります。 BASEカードの場合、リアルカードの発行など直近の大まかな開発マイルストーンが割と明確であることから、 実装したい各機能のスケジュール上での優先度をPdMと議論、調整し、開発をすすめる上で発生する技術的な阻害要因を排除すること がEPMとして重要になります。 YELL BANKにおけるEPM 簡単にYELL BANKというサービスを紹介しておくと、BASEを活用されてるショップオーナーの皆様へ簡単に資金調達を提供するサービスです。 thebase.in 最近はスタートアップ企業の隆盛もあり、VCなどから資金調達をすることも馴染み深くなりましたが、BASEのようなロングテールの規模感だとまだまだ未知の市場です。 そのため、YELL BANKの開発では 少しずつ小さな仮説検証すること を重きにおいて開発サイクルを回しています。 特に機能をデリバリーするときはできる限りユーザーストーリー作成し、機能と価値をセットで考えられるように気をつけています。特に以下を意識してメンバーと一緒に作ります。 不確実性はどこになるあるのか? 仮説検証したい内容はどこか? 機能を横断的に作る メンバーと一緒に作ることで、メンバー全員がゴールを意識すること、EPM以外もオーナーシップを持って開発をすることに繋がると考えています。 またバックエンド・フロントエンドで区切るのではなく、横断的に作ることで手戻りはできる限り少なくします。 ユーザーストーリーの分割に関しては アジャイル開発におけるユーザーストーリー分割実践 〜画面リニューアルの裏側〜 をご覧ください。 devblog.thebase.in また、仮説検証を実践するにあたって作っては放置ではいけません。そこでPdM/PMMがPDCAを回しやすくするためのデータ基盤の整備もしています。 過去の実践した事例に関しては Google Apps Script× BigQuery × Googleスプレッドシート × データポータルで簡易CRMを作ってみた や Lookerでショップのサービス活用カルテを作成した話 をご覧ください。 devblog.thebase.in devblog.thebase.in BASE BANKとしてのEPMのこれから 一言にEPMと言っても、プロダクトの特性によって役割や重点の置き方が変わってきます。 今の役割の定義は来年には変わってることも全然あるでしょう。正直模索中です。 現在のEPMはプロダクトのデリバリーやクオリティにフォーカスした役割です。 ただ、プロダクトの成功を目指すためにはそれを支えるメンバーの中長期的な成長も重要な要素です。 そのため今後の可能性としてはメンバーに協力してもらうだけでなく、それを各メンバーの評価として還元するためにPeople Managementも担うようになる可能性も大いにあるでしょう。 おわりに これから更にプロダクトのデリバリー速度やクオリティを高めていくには、現メンバーの成長だけでなく、より多くのメンバーの力が必要です。 BASE BANK自体や各プロダクトへの興味でもEPMへの興味だけでもなんでもいいのでぜひ気軽にお話しましょう! open.talentio.com
この記事は BASE Advent Calendar 2021 の20日目の記事です。 はじめに こんにちは。Owners Experience Backend Group の杉浦です。主にサーバーサイドのアプリケーションの実装をしています。 エンジニアにとって、外部企業から提供されるAPIやCDN(Content Delivery Network)といった『外部サービス』をどう扱うかは悩ましい問題です。 特にシステムの設計段階において「外部サービスをどうやって内部システムに組み込むのか?」という方針は、その後のアプリケーションの生産性に大きく影響します。 仮に、密結合に設計してしまうと「外部サービス」という不確実性に影響されやすくなるため、好ましい状況とはいえません。どのように疎結合を実現するのか?という設計が、外部サービスから不確実性をハンドリングする生命線になります。 そこで、この記事では、2021年11月にリリースを行なった「Akamai Image Video Manager の導入プロジェクト」を例に、外部サービスの不確実性を最小化する「設計戦略」の実践方法を提示します。 本稿は3章立てになっています。 第1章:外部サービスの不確実性という問題 第2章:設計戦略のプランニング 第3章:設計戦略を実行するチームマネジメント 第1章では、外部サービスの不確実性について考察しています。内部サービスがミクロな不確実性への対処が主軸になるのに対して、外部サービスの不確実性がよりマクロな範囲、特に外部企業の経営方針の影響を受けやすいという点に言及しました。 第2章では、不確実性を最小化するための設計戦略を提示します。疎結合な設計はもちろん、外部企業の経営状況を読み解くために財務状況(PL/BS)を踏まえる必要性を提示しました。 第3章では、Akamai Image Video Manager の導入プロジェクトを例に、設計戦略を遂行するためのチームマネジメントについて言及しています。外部サービスの導入・移行プロジェクトには技術的な知識が不可欠なことから、エンジニアである筆者がチームを取りまとめた実践例をとなります。 第1章:外部サービスの不確実性という問題 良い設計をするための1つの重要な論点として「不確実性の最小化」があります。 ここで重要なのが、内部のシステムに対する「不確実性」と、外部のシステムに対する「不確実性」は、それぞれ「似て非なる存在」ということです。 内部システムにおける不確実性は、社内のエンジニアでなんとか対応できる範囲が多いと言えます。あくまで、社内における不確実性に対峙するため、その対処も社内で完結しやすいためです。 一方、外部システムは「存在そのものが不確実性に満ちている」といえます。期待したレスポンスが返却されるか?というミクロな不確実性に加えて、それとは別次元のマクロな不確実性を伴います。 例えば、外部システムが「3年後にもサービスを継続している」という保証はどこにもありません。加えて「値上げをしない」という確証も(契約に入れない限りは)どこにもないのです。 つまり、外部システムの不確実性は、干渉できない範囲に及ぶため、これらの不確実性を考慮した設計を考えることは容易ではないといえます。 第2章:設計戦略のプランニング なぜ戦略なのか? 外部サービスの不確実性に対処するためには、単なるアプリケーションの設計はもちろんですが、その大前提となる「設計戦略」が鍵を握ります。 あえて「戦略」という仰々しい単語を使う理由は、その影響力の時間軸が長いからです。戦略を間違えると、リリース直後は問題が発生しなかったとしても、数年後に苦労します。出発点である戦略を間違えた場合、改修コストへの投資が必要になりますが、こうなると当然、ROIも低下するため、経営的にも好ましくありません。 つまり、方向性を間違えることは将来にわたってビジネスに好ましくない影響を与えることを意味するため、設計の上位概念である「設計戦略」が必要になるということです。 取り替え可能性を考慮して設計する 「外部サービスの取り替え可能性」を考慮した設計こそが、不確実性の最小化にあたって有効な方針となります。 取り替えが可能であれば、別の代替サービスへの移行や、代替機能の開発(内製化)が容易になります。そして、仮に、外部サービスで「値上げ」や「サービス停止」といった不確実な事態が発生した場合に、影響を最小限に抑える最後の砦になります。 ただし、この設計方針は、競合製品が存在する「コモディティーな市場」でしか取り得ない選択肢になります。 逆に、代替が存在しない「独占的なサービス」を利用する場合は、価格決定権は外部サービスの側にあります。さらに、代替サービスの登場は考えにくいため、取り替え可能な状態にする必然性に乏しいといえます。 この意味で、外部サービスの利用を前提とした設計戦略を考える上では、外部サービスのビジネス上の競争環境や、経営状況の見極めが必要になります。 このうち、外部サービスの経営状況は、財務の分析によって明らかにできます。米国企業であれば10K(Security Report)、日本企業であれば有価証券報告書が、そのサービスの置かれた競争環境を示す材料になります。 今回のプロジェクトではAkamai(証券コード:NASDAQ: AKAM)が提供するサービス「Akamai Image Video Manager」の利用を前提としたため、Akamaiが公表した2021年の10Kについて、ざっくりと目を通しました。 https://www.ir.akamai.com/sec-filings 責務を1つのリポジトリに集約する 今回の「Akamai Image Video Manager の導入プロジェクト」では、画像配信基盤を「取り替え可能な状態」にするという設計戦略を採用しました。 具体的な実現方法は、外部サービスを利用した画像配信に関する責務を、1箇所のリポジトリに集約するということです。 従来のBASEにおいては、複数のリポジトリに画像配信の外部サービスのコードが点在しており、責務分離の観点であまり好ましい状態ではありませんでした。 そこで、画像配信に関する実装上の責務を1つのリポジトリに集約することによって、疎結合の実現を目論みました。 第3章:設計戦略を実行するチームマネジメント エンジニア駆動のチームマネジメント 筆者の普段のBASEでの仕事は、コードを読み書きすることなので、プロジェクト関連のマネジメントには関与しません。 ですが、今回の「Akamai Image Video Manager の導入プロジェクト」においては、プロジェクトチームを取りまとめました。 その理由は、このプロジェクトを回すためには、エンジニアリングの専門的な知識が必要だったからです。 一般的なBASEの社内プロジェクトであれば、プロダクトマネージャの元に、複数のエンジニアが参画して、1つの機能開発に携わることが多いといえます。 ですが、このプロジェクトにおいては、画像ファイルの基本的な知識に加えて、キャッシュやCDN、BASEのリポジトリ構成を理解する必要がありました。そこで、普段はコードを書いている筆者が、チームを取りまとめることになったのです。 なお、このプロジェクトでは、筆者は実装を行なっていません(ただしプルリクのレビューは行なっています)。 これは社内事情が影響しており、当初は取りまとめと実装を両立して行う予定だったものの、急遽実装が必要になった別のプロジェクトに追加アサインされたため、時間の都合上、Akamaiの移行プロジェクトは「取りまとめ役」に徹することになったからです。※ ※急ぎの実装とは「Facebookドメイン認証」というプロジェクトで、これに関してもテックブログを書いています。 →「ROI(投資利益率)を意識したエンジニアリング」 答えのない意思決定に対峙する チームの取りまとめにあたって、実装の担当箇所の振り分けと、局所的な意思決定の2つを行いました。 1つ目の実装担当箇所の振り分けとは、(1)改修範囲の調査と見積もりを行い、(2)チームの各メンバーに実装箇所を割りあて、(3)リリースに至る手順を交通整理する、ということです。 2つ目の局所的な意思決定とは、リリースにあたって実装以外で決めなければならないことについて、社内の担当者と調整したうえで決定することです。今回は「ショップに対するコミュニケーション」と「APIの互換性」という2点で、意思決定を行う必要がありました。 「ショップに対するコミュニケーション」とは、BASEの顧客であるショップへの通知のことを意味します。画像配信基盤の移行にあたって、画像表示の仕様が微妙に変化することが避けられなかったため、利用者の方にその内容を事前にお伝えすることが必要でした。 この際に、どのような方法(メールなのか?管理画面なのか?)でお伝えするのかを判断する必要があり、プロダクトマネージャの方の大きな助けを借りつつ方針を決めています。 「APIの互換性」とは、BASEが外部に公開しているAPI(BASE-API)への影響を加味することです。 Akamaiへの移行によって、BASEが提供するAPIの構造には変化はないものの、画像関連の「パス」が変更されるため「潜在的に影響あるかもしれない」という懸念がありました。 従来の画像パスとの互換性を残すか?それとも残さないか?というのは非常に難しい問題で、BASE社内の各方面の責任者の方と相談しつつ、方針の決定に至りました。 これらの経過を経て、2021年11月ごろまでに「Akamai Image Video Manager の導入プロジェクト」は無事に終了しました。 協力していただいた社内の皆様、チームの方々に大感謝しております。 終わりに 筆者の個人的な見解として、インターネット業界では、2020年代を通じて外部システムが提供する機能のボリュームは増え続け、業界全体でAPIなどを通じた外部サービスとの連携が1つの主戦場になると見ています。 近い将来、エンジニアは「外部サービスの不確実性をいかに最小化するか」という問題に、より一層、真剣に対峙する必要が出てくるでしょう。 その際に、このテックブログの内容が少しでも参考になれば幸いです。 最後にいつもの宣伝です。BASEでは各職種で採用を強化しておりますので、お気軽にお声がけください! https://open.talentio.com/r/1/c/binc/homes/4380
この記事は BASE Advent Calendar 2021 の19日目の記事です。 BASE BANK株式会社でエンジニアをしている若野( @sam8helloworld )です。 私が普段見ているサービスではBASEの他のアプリケーションや外部サービスのページから遷移してくることがよくあります。 さらにその時々でユーザに提供する情報や振る舞いを変えたくなることもあります。 今回はそういったケースの中でも 外部サービスの特定のページから遷移した時のみAPIを叩き、それ以外のページ更新、戻る、進む、他ページからの遷移の場合はAPIを叩かない という仕様を実現するために私が行った調査検証が記事の内容となります。 仕様を簡単に図で表すと以下のようになります。 アプリケーションの構成の整理 今回の実装を行うに当たって前提となるアプリケーションは以下の3つの登場人物で成り立っています。 SPAの起点になるindex.htmlを配信しているバックエンドサーバ 特定のディレクトリ配下(e.g. /shop_admin/xxx)のURLにアクセスしてきた時に、SPAの起点になるindex.htmlをレスポンスとして返すことが責務です。 BFFサーバ SPAから呼ばれて表示要件に応じたjsonをレスポンスとして返すことが責務です。サービスのビジネスロジックはここに集約されている場合が多いです。 SPA BFFサーバにサービスのビジネスロジックを委譲しており、画面の表示やユーザのインタラクションを扱うことが主な責務です。 どこにロジックを持つか問題 さて登場人物がわかったところで、今度はその中でも上記の 外部サービスの特定のページから遷移した時のみAPIを叩き、それ以外のページ更新、戻る、進む、他ページからの遷移の場合はAPIを叩かない というロジックを持つべきなのは誰なのか?ということが問題になります。 今回のケースでは以下の要件を満たすことができるかが鍵になります。 ページ遷移してきた時に処理を実行できる 直前のページのURLを取得できる 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない では登場人物を1つずつ検証していきます。 SPAの起点になるindex.htmlを配信しているバックエンドサーバ 結論から言うと全ての要件を満たすことはできませんでした。(厳密に言えばやろうとすればできるけど遠回り感すごい。) [x] ページ遷移してきた時に処理を実行できる [x] 直前のページのURLを取得できる [ ] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない まず1つ目の要件の「ページ遷移してきた時に処理を実行できる」に関しては、外部サービスからページ遷移してくる都合上必ずバックエンドサーバのControllerでSPAの起点になるindex.htmlを返す処理を行うので以下のようにページ遷移時にAPIを叩くことも可能です。 public class XXController { // 省略 public function index() { // ページ遷移してきた時の処理を行う // $api->call(); $this->render('SPAのindex.html'); } } 次に2つ目の要件の「直前のページのURLを取得できる」に関しては、以下のようにリファラを参照することでブラウザセッション内の直前のページのURLを取得できます。 public class XXController { // 省略 public function index() { $referer = $_SERVER['HTTP_REFERER']; if ($referer === '特定のURL') { // ページ遷移してきた時の処理を行う } $this->render('SPAのindex.html'); } } ※ 注意点: <meta name="referrer" content="no-referrer"> タグがあるなど、リファラを送信しない設定になっているページのURLは取得できません。 最後に3つ目の「戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない」に関しては、以下の3パターンを場合のサーバサイドの状態を考える必要があります。 外部ページから戻ってきた時 外部ページから進んできた時 ページを更新した時 外部ページから戻ってきた時 例えば以下のように a.example.com -> b.example.com -> c.example.com とリンクを押して遷移して、戻るボタンで b.example.com に戻ってきた場合を考えます。 ブラウザの進むボタン(history forward)や戻るボタン(history back)はリファラの状態も元に戻してしまいます。なので、 c.example.com から遷移してきたにも関わらずバックエンドでリファラを参照するとあたかも a.example.com から遷移してきたように見えてしまうのです。 developer.mozilla.org html.spec.whatwg.org 外部ページから進んできた時 「外部ページから戻ってきた時」とは違って確かに直前のページの意味合いはあっていますが、今度はバックエンドでリンクを踏んだページ遷移と区別がつかないので要件を満たせません。 ページを更新した時 ページを更新した場合も「外部ページから進んで来た時」と同様にリファラはそのままですが、バックエンドではリンクを経由したページ遷移と区別がつかないので要件を満たせません。 ※ セッションやCookieを使えばバックエンドで状態を持てるのでページの更新や2回目以降の遷移かどうかが判断できますが、後述の方法の方がシンプルなので採用していません。 BFFサーバ そもそものSPAから呼ばれてjsonを返すことが責務なので、BFFサーバに画面遷移の状態を判別させるのはお門違いです。 [ ] ページ遷移してきた時に処理を実行できる [ ] 直前のページのURLを取得できる [ ] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない SPA SPAではなんとか3つの要件全てを満たすことができました。 [x] ページ遷移してきた時に処理を実行できる [x] 直前のページのURLを取得できる [x] 戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない 1つ目の要件の「ページ遷移してきた時に処理を実行できる」と3つ目の要件の「戻るや進むボタン(ブラウザのHistoryAPI)、ページ更新の時は処理を実行しない」に関しては、ブラウザのNavigation Timing APIを使うことで要件を満たせます。 Navigation Timing APIではバックエンドで判別できなかった戻る・進む・更新・ページ遷移をそれぞれ状態として取得できるので、ページ遷移の時だけ処理を行うということが可能になります。 developer.mozilla.org ただ注意が必要なことは、バックエンドと違ってSPAではページ遷移という概念が普通のリンクを踏んだページ遷移とvue-routerなどのjsで制御されたルーティングによるページ遷移の2つあるということです。 2つ目の要件の「 直前のページのURLを取得できる」に関しては、 document.referrer がバックエンドのリファラと同じ役割を果たすので要件を満たせます。 mounted() { const referrer = document.referrer if (referrer === '特定のURL') { // ページ遷移してきた時の処理を行う } } Navigation Timing API さて、Navigation Timing APIを使えば要件を満たせると言いましたが、Navigation Timing APIとはどういうものなのでしょうか? Navigation Timing APIの MDN Web Docs を参照すると主に以下の使い方をするAPIであると書かれています。 Collecting timing information(タイミング情報の収集) Determining navigation type(ナビゲーションタイプの決定) さらにDetermining navigation typeでは以下のことが判別できると書いてあります。 Was this a load or a reload? (ロードかリロードか) Was this a navigation or a move forward or backward through history? (ページ遷移かhistory backかhistory forwardか) How many (if any) redirects were required in order to complete the navigation? (遷移が完了するまでに何回りダイレクトしたか) これは上述したクリアすべき要件にピッタリハマりそうです。 Navigation Timing APIを使ってナビゲーションタイプを判別する方法 ナビゲーションタイプを判別する方法は2つあります。 1つは window.performance.navigation.type を参照する方法です。 if ( window . performance . navigation . type === window . performance . navigation . TYPE_NAVIGATE ) { console . log ( 'ページ遷移' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_RELOAD ) { console . log ( 'ページ更新' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_BACK_FORWARD ) { console . log ( '戻る・進む' ) } if ( window . performance . navigation . type === window . performance . navigation . TYPE_RESERVED ) { console . log ( 'その他' ) } developer.mozilla.org 2つ目の方法はPerformanceNavigationTimingインタフェースを使用する方法です。 const entries = window . performance . getEntriesByType ( 'navigation' ) for ( const entry of entries ) { if ( entry . type === 'navigate' ) { console . log ( 'ページ遷移' ) } if ( entry . type === 'reload' ) { console . log ( 'ページ更新' ) } if ( entry . type === 'back_forward' ) { console . log ( '戻る・進む' ) } if ( entry . type === 'prerender' ) { console . log ( 'その他' ) } } developer.mozilla.org 2つの判別方法の使い分け 2021年12月現在、主要ブラウザで window.performance.navigation.type を利用することはできますが、参照することは非推奨となっています。 代替の方法として2つ目のPerformanceNavigationTimingインタフェースを利用することが推奨されている状況です。 ただし、PerformanceNavigationTimingはiosのsafariが今のところサポートしていない状況です。 const types = window . PerformanceObserver . supportedEntryTypes if ( types . includes ( 'navigation' )) { // PerformanceNavigationTimingインタフェースに対応 } 上記の supportedEntryTypes でPerformanceNavigationTimingのサポート状況は確認できます。 navigation という文字列を含む配列を返す場合は対応済みなのでPerformanceNavigationTimingインタフェースを使った実装を。 navigation という文字列を含まない配列を返す場合は window.performance.navigation.type を使った実装を行うのが良さそうです。 developer.mozilla.org Navigation Timing APIを使った判別はvue-routerを利用した遷移にも使える? Navigation Timing APIにおける「ページ遷移」「戻る・進む」はあくまでも document オブジェクトが初期化・再構築されるような処理に対して遷移に対して判定されるようです。 www.w3.org html.spec.whatwg.org VueをはじめとしたSPAのルーティングでは document オブジェクトを初期化するのではなく、一部のDOMを更新する仕組みになっているので「ページ遷移」「戻る・進む」は判定できませんでした😢 ただ、今回処理を行いたいのはVueのルーティングではなく外部ページからリダイレクトされてきた時なので要件は満たせるというわけです。 document.referrerと組み合わせて「特定ページからの初めての遷移を判定する」をコード isFromCertainPage() { // safari on ios か確認 if (! window .PerformanceObserver.supportedEntryTypes. includes ( 'navigation' )) { // ページ遷移か確認 if ( window . performance .navigation. type !== window . performance .navigation.TYPE_NAVIGATE) { return false } } // ページ遷移か確認 const entries = window . performance .getEntriesByType( 'navigation' ) for ( const entry of entries) { if (entry. type !== 'navigate' ) { return false } } const expectedReferer = '特定のURL' let referrer = document . referrer . replace ( /\?.*$/ , '' ) // safariのバージョンによってreferrerのトレイリングスラッシュの有無が分かれるので、一律トレイリングスラッシュをつける // https://trac.webkit.org/changeset/280342/webkit/ referrer = referrer. slice (- 1 ) !== '/' ? referrer + '/' : referrer if (!referrer. startsWith (expectedReferer)) { return false } // SPA内での回遊判定 if ( this .$store. state .alreadyVisited) { return false } return true } ※ SPAのルーティングの遷移に関しては現状判定するAPIがないので、状態を持つようにしています。 終わりに Navigation Timing API自体は複雑ではないので、すぐに実装に入れました。ただ、SPAではなぜNavigation Timing APIでページ遷移が判定できないのか?ということを深掘りしていくとブラウザAPIの仕組みが垣間見えて結構面白かったです。 あとは今回みたいにブラウザのAPIを利用したコードのテスト書こうとしたときに、jsdomだとモックだらけになっちゃうし実ブラウザを使ったIntegrationテストにするにもコストかかるしで塩梅が難しいと感じました。ここら辺のテスト詳しい方がいたらぜひ @sam8helloworld まで🙇‍♂️ また、もしBASEBANKに興味のある方は @sam8helloworld や下記のリンクまで open.talentio.com 明日はOwners Experience Backend Groupの杉浦さんです。
この記事は BASE Advent Calendar 2021 の19日目の記事です。 こんにちは、BASE株式会社でバックエンドエンジニアをしている小川です。 私たちのチームでは10月からレスポンス改善PJとして、BASEのレスポンスが遅い処理を改善する施策を行なってきました。 その中で下記の例のようなクエリが重い処理として挙がり、そのクエリではGROUP BYが使われていたのですがDISTINCTを使った方が早くなるのではという意見がありました。 SELECT c1 FROM t1 WHERE c1 > const GROUP BY c1; そこで、DISTINCTとGROUP BYでどちらが早いのかをネット上で調べたところ、DISTINCTの方が早い説やGROUP BYの方が早い説、どちらも変わらない説などいろんな説があり、実際のところどうなんだろうと思ったことがきっかけで検証してみることにしました。 DISTINCTとGROUP BYどちらを使った方が良いか検証した結果をご紹介したいと思います。 DISTINCTとGROUP BYの違いについて DISTINCTとGROUP BYは重複行をまとめるという目的で使われると思いますが、それぞれどんな違いがあるのでしょうか。 DISTINCTについて 射影を行う段階で重複を排除 GROUP BYについて 指定された列名でレコードのグループ化を行う。 グループ化したレコードは、集約関数を用いることで集計することができる。 つまり、 DISTINCTは、重複を排除した結果を出力する場合に使用 GROUP BYは、重複を排除した結果に対して集計処理を行いたい場合に使用 という違いがあります。 今回例に挙げたクエリはGROUP BYを使用していましたがグループ化した後、集計処理をせずに重複行を削除するためだけに使われています。 こういった重複行を削除するためだけなら、用途にあったDISTINCTを使うべきだと思います。 次に肝心なDISTINCTとGROUP BYどちらが早いのか見ていきます。 DISTINCTとGROUP BYどちらの方が早い 下記のようなDISTINCTとGROUP BYを使用したクエリを使って、クエリキャッシュを無効にしてそれぞれの実行計画と実行速度を検証してみました。 SELECT DISTINCT c1 FROM t1 WHERE c1 > const; SELECT c1 FROM t1 WHERE c1 > const GROUP BY c1; ※c1にはインデックスが貼ってある状態 検証環境 MySQL5.6 検証結果としましては、 実行計画は全く同じで実行速度もほぼ同じ結果となりました。 MYSQL-DISTINCTの最適化 In most cases, a DISTINCT clause can be considered as a special case of GROUP BY. For example, the following two queries are equivalent: Due to this equivalence, the optimizations applicable to GROUP BY queries can be also applied to queries with a DISTINCT clause. Thus, for more details on the optimization possibilities for DISTINCT queries, see Section 8.2.1.14, “GROUP BY Optimization”. こちらのURLのMYSQLのドキュメントにも記載されている通り、オプティマイザによって最適化された結果、内部では同じ処理になり性能は同じということが分かります。 実行速度も検索結果も同じということであれば、DISTINCTとGROUP BYは使用用途によって使い分けてください。 おわりに DISTINCTとGROUP BYについての違いや、速度に関して紹介させていただきましたが参考になったら幸いです。 今回の調査ではレスポンスの改善にはなりませんでしたが、ほんの些細なことでも調べて、検証することによって新たな発見があると思います。 実際に私も検証するまでは、GROUP BYよりも重複行を削除するだけの処理のDISTINCTの方が早いと思っていました。 ぜひ気になったことがあれば検証してみてください! 明日のアドベントカレンダーはyusugiuraさんとShoTakeuchiさんです!お楽しみに!
バックエンドエンジニアの @cureseven です。レスポンス改善プロジェクトという名前で、BASEのレスポンス速度を早くするために10月より動いてきました。 経緯に触れた後、レスポンス改善をどう進めてきたかとおこなった施策を紹介します。 レスポンス改善プロジェクトの経緯 始めにプロジェクトが立ちあがった経緯を少しお話します。 BASEはリリースして9年ほど立ちました。その間、さまざまな機能が追加されたり、ありがたいことに多くのユーザーさんに使っていただいています。その結果、DBに保存しているデータ量が増えました。データ量が多いことによりSelectの実行時間が重くなるなどが起こっていました。 ユーザの方から複数件画面の重さについてのお問い合わせをいただいており、今回プロジェクトとして改善することになりました。 重いリクエストの特定 それではここから、どう進めてきたかとおこなった施策を紹介します。 BASEではNew Relic Oneを導入しており、リクエストごとのレスポンス速度の統計を見たり、Transactionの詳細を見ることができます。私たちはNew Relic APMで重いリクエストを突き止め、施策を実施しました。 New Relic APM Transactionsのページで、 Most time consuming(実行回数 × 平均実行時間) Slowest average responce time(平均実行時間) を見比べながら、平均実行時間が遅すぎるものやたくさんの人が利用していて不便に感じそうなものを中心に見ていくことにしました。 Most time consuming順にエンドポイントをsortしている 今回は購入者が触れる画面ではなく、オーナーさんの作業がサクサクできることにフォーカスして、オーナーさんの使う管理画面の重いものから見ていきました。 最も重い画面についてはある程度見通しがついていましたが、Most time consumingについては予測を立てることが難しいです。New Relic APMを見ることでどんなAPIが重いのかを把握することができました。根拠となる数字を持って優先度を決め施策を実施していくことで、効率よくオーナーさんの問題にアプローチできました。 また、フロントエンドで時間がかかっているかどうかの検証にはBrowserの機能を利用できます。これから使っていくつもりです。 dashboardによる重いリクエストの可視化 オーナーさんからのお問い合わせを受けるより先に社内で重いリクエストがあったことを知るために、Transactionのdashboardを作成し監視することにしました。 dashboardでは注文数の多いshopをいくつかピックして、オーナーさんの使う管理画面に限定したTransactionの95パーセンタイルの重さが目立つリクエストを表示しました。 alert飛ばす設定 dashboardを作成しましたが、張り付いて監視しないでいいように、alertを飛ばす設定をしました。 今回は購入者が触れる画面, オーナーさんの使う管理画面両方において、それぞれにTransaction timeの閾値をセットし、リクエストごとに95パーセンタイルが閾値を超えたときにslack通知するようにしました。 設定に役立ったリンクです。 slackのhookリンクの生成 alertの設定 alert設定のパラメータの説明 通知頻度の設定 レスポンス改善施策の実施 New Relic APMのTransaction traceという機能で、Transactionの詳細を見ることができます。どのメソッドのどのクエリが重いかまで断定できます。 私たちは重いリクエストのTransaction traceを見ることでボトルネックを把握し、リファクタ方針を考えました。リファクタ施策の具体例をいくつか紹介します。 indexを貼った 重いクエリを発見したとき、explainして実行計画がどのようなものかを確認しました。indexが適切に使われていないクエリを発見したので、indexを貼る対応を実施しました。 不要な処理を消した 機能のリニューアルや機能を廃止する中でいらなくなった処理が残ってしまうことがあります。今回、そんな処理がTransaction timeを圧迫してしまっていました。不要な処理を削除することでレスポンスが改善しました。 クエリ改善 今回よく目にしたのが、レコードの存在の有無だけ分かればいい時に、集計したり大量にデータを取ってきたりしているというものでした。レコードの存在有無確認は最初の1件だけとれば良いので、集計処理などをやめることで、クエリ実行時間を短縮できました。 また、画面描画毎に一つずつのデータを取ってきて集計するのではなく、バッチで集計したデータを格納しているテーブルを利用するようにする修正を実施しました。こちらの修正では、95パーセンタイルで15秒ほどかかっていたリクエストが、1秒以下で返ってくるようになりました。 また、注文数をカウントする処理では、画面上は99以上になると「99+」という表示になるところがあります。大量に注文があるshopのクエリがとても重くなってしまっていたため、サブクエリを使って100件以上は数えないようにしました。 バッチ実行の負荷軽減 New Relic APMのTransaction traceは、1分ごとに、基準値より重かった最大Transaction timeの詳細を残しています。 取れたTransaction traceを眺めていると、どうやら15分ごとに基準値に引っかかっていることがわかりました。詳細を見ると、15分おきに出ているTransaction traceではdb接続に決まって6秒ほどかかっていることがわかりました。 原因は15分間隔で実行されるバッチにて大量レコードのtruncateおよびinsertのSQLを実行していることでした。一旦メモリにもった上でループ処理することで急激な負荷を抑えることで、15分おきに出ていた6秒のdb接続時間は解消されました。 dashboardを見ているだけではこのような傾向を読み解くことはできませんでした。Transactionの99パーセンタイル、95パーセンタイル、50パーセンタイルを見比べたり、Transaction traceの出力時間を見たりすることで見えてくるものもありました。 プロジェクトを通して 3ヶ月間対応を行なってきた結果、画面ロードが遅いことに関してお問い合わせいただいていたshopさまから改善されたねの声をいただきました。しかしながらまだまだ遅い画面はあるため、来年以降も改善にむけて動いていこうと思っています。 最後に、レスポンス改善プロジェクトを通して感じたことを記して終わろうと思います。 機能リニューアルなどのタイミングで適切に不要な処理を消すのはもちろんですが、今回のプロジェクトのようにたまにリファクタをするための期間を設けてそのような処理がないか見直すのは大事だなと思いました。大掃除の季節ですし、ソースコードの見直しを実施するいいタイミングなのかもしれません。可読性も上がります。 機能開発のタイミングで貼ったindexが、データが増えることにより効かなくなるときがあります。今回の施策で劇的に効果が上がったのはindexの付与でした。こちらもリリース後年月が経ってから改めて見直すのが良いなと思いました。 あまりにもレスポンスが遅いと素晴らしい機能も使い物にならないので、仕様を少し変更して大幅に改善するものであれば、変更することも検討してみるのがいいなと思いました。 これからも使っていけるサービスであるために、さまざまな実装方法を検討する必要があると改めて感じました。
この記事は BASEアドベントカレンダー2021 18日目の記事です。 UIデザイナーの Yoshioka です。コードベースのデザインツールとして個人的に気になっている UXPin Merge を試してみました。 UXPin Mergeとは UXPinはベクターベースのデザインツールとは違い、HTML/CSSのコードで定義されていることをベースにしているデザインツールです。 UXPin Merge はReactやStorybookのコンポーネントを取り込み、実装されているものと同じコンポーネントを元にデザインができる機能です。 試してみる 今回は簡単にローカルでつないでみました。 ReactComponentsやStorybookを読み込むことができます。 UXPin MergeのReactComponents、Storybookでできることが違います。(詳しくは後述します) また、storybookの場合にはコンポーネントの配置に若干のラグが発生します。 立ち上げる 該当のReactComponentsリポジトリから、ローカルでExperimentalModeを立ち上げます。 これにより、URLが生成されるのでコード変更してどうUXPinで反映されるのか確認することができます。 配置していく コンポーネントがこのようにリスト表示されるので、これをドラッグ&ドロップして配置していきます。 簡単なデザインですが配置してみました。 できること 各コンポーネントをクリックするとコンポーネント定義されているpropsが表示されます。 今回はReactComponents連携したので、コンポーネントをネストすることができます。 (storybook連携ではできない様です) ネストすることのメリットは、下記の様にレイアウトコンポーネントを作成し、その中にコンポーネントを入れ込むことができます。 ネスト内のコンポーネントを削除することもできます。 今回はStackというレイアウトコンポーネントの中に、SelectとButtonを内包しています。 StackのpropsでDirectionを持っているので、レイアウトを切り替えることができます。 レイアウトコンポーネントのMarginなど覚えておくのも大変なので便利です。 できないこと Figmaなどのデザインツールと違ってコンポーネントのDetachはできません。 もちろんテキストや図形など配置できますが、コンポーネント内に配置するなどはできません。 このあたりはプロダクトやデザイナーの好みによりそうですが、自由にデザインデータを作れないことにあまりネガティブな印象はありませんでした。 また、Figmaの様にテキストをアートボード上で編集することはできず、props上で入力する必要があります。 プロトタイプについて まだ上記の様にコンポーネント配置しただけですが、この状態でどのようなプロトタイプが作成されるのかみてみます。 Interactionにはなにも設定していませんが、このようにプロトタイプが作成されます。 Figmaでも似た表現を行うことはできますが、実装が元になっていることでコンポーネントライブラリと差分なくプロトタイプ確認できるのはメリットかなと思います。 デザインツールで起きがちな問題 ここでたまに聞くデザインツールで起きがちな問題を洗ってみます。 このコンポーネントない、などのデザインデータと実装のギャップなどどう解決してよいかわからない沼が出てきます。 コードベースだと解決できるかもしれない 簡単に触った程度ですが、ここまででコードベースのデザインデータにした時のメリットは デザイナー・エンジニアがコンポーネントに対し同じ基準でデザインできることにあります。 これまでの企画・デザイン・コーディングといったフローから、デザイナーとエンジニアが共同でプロトタイプを作ることができることで、既に共通の認識があるのでコミュニケーションコストを抑え開発スピードをあげることができそうです。 現状ではデザイナーがコードへの理解を示すこと、開発環境作りやGitの連携にかなりエンジニアに工数を割いてもらう必要もありますが、検討してみる価値はあります。 来年以降もコードベースのデザインツールを注目していきたいとおもいます!
この記事はBASEアドベントカレンダー2021 17日目の記事です。 はじめに DataStrategyチームの杉です。 ショッピングアプリPay IDではさまざまなショップでの商品購入が可能です。 "探す"タブにはおすすめ機能がついており、利用者にあった商品やショップのレコメンドを行なっています。 おすすめ商品の掲載例 おすすめの商品ではさまざまなアルゴリズムを並行に運用しており、その中のひとつとしてAmazon Personalizeを利用しています。 このアルゴリズムの計算は今まで1日に1回のbatch処理で行なっていました。 しかし、閲覧や購入のログをリアルタイムに利用することでよりマッチしたおすすめ商品を掲載することができるのではという想いでevent trackerを用いたリアルタイムに変化をするレコメンドに挑戦をしました。 この記事では、event trackerをどう実装したかをメインにお伝えしたいと思います。 Amazon Personalizeとは Amazon Personalizeについては以前に@pigooosukeさんが発表をしているので運用しているおすすめの全体の構成やAmazon Personalizeの利用注意点などについてはこちらで見ることができます。 https://speakerdeck.com/pigooosuke/aws-personalize-recsys (現在の構成と異なる部分もあります) Amazon Personalizeを使用することでメンテナンスコストを下げることや精度の高いおすすめを提供することができています。 event trackerについて Amazon Personalizeではevent trakcerを使うことでリアルタイムなイベントを反映させた結果の推論結果を取得することができます。 作成、eventの送信はどちらもboto3で行うことが可能です。 ①event trackerの作成 personalize = boto3.client(service_name= 'personalize' , region_name=AWS_REGION) response = personalize.create_event_tracker( name=EVENT_TRACKER_NAME, datasetGroupArn=dataset_group_arn ) ②eventが発生するたびにput eventを行う personalize_runtime = boto3.client( service_name= 'personalize-runtime' , region_name=AWS_REGION) personalize_events.put_events( trackingId=tracking_id, userId=userId, sessionId=sessionId, eventList=[{ 'sentAt' : timestamp, 'eventType' : 'view' , 'itemId' : '12345' }, { 'sentAt' : timestamp, 'eventType' : 'view' , 'itemId' : '6789' }] ) eventの送信はuser_id単位で送ります。 また、session_idを用いることでその時点では未知なユーザに対してもデータを溜めることが可能です。 推論への影響はすぐに反映され、eventの影響を確認することができます。 event内容によっては必ずしも推論結果に影響するわけではないので、put eventをしても変わらないこともあるということには注意が必要です。 event trackerを導入後の構成 eventとして溜めていくデータとしては2種類あります。 DBに記録された購入情報など 行動ログ これらをリアルタイムにevent trackerに流し込んでいくために以下の構成にしました。 メインのworkerは2台あります ①Kafka->SQS DBの更新イベントと行動ログをKafkaで取得し、SQSへ随時eventを送ります。 ここではRDSに接続をし必要情報の紐付けなども行なっています。 ②SQS->put_event SQSからAmazon Personalizeのevent trakcerへput eventをするような動きをしています。 SQSを挟むことで急激なトラフィックなどが来てもevent trackerに流し込む部分の負荷が高くならないようにしました。 event trackerの注意点 event trackerの検証を行なっているうえで、注意点に出会いました。 いくつかありましたが、その中でも2つご紹介させていただきます。 1. 未知商品に対してはeventを追加しても影響がない データセットには以下の3種類あります。 Item User User-item interaction このItemに入っていない商品は推論結果として出現しません。 そのため、未知商品をeventとして追加をしても推論結果が変わることや推論結果として新しく商品が登場することはありませんでした。 ここで未知商品を反映させるためには商品登録時にPutItemsをする必要があります。 2. "interactions"データセットimportだけではput eventの内容は消えない 今までdailyで計算をする際にデータセットを毎日importすることで上書きをしていました。 これはデータセットの肥大化を防ぐことを目的として行なっておりました。 put eventで追加したデータは"interactions"データセットとして追加されていきます。 そのため、今回リアルタイムに追加をしたeventの内容と、毎日の集計しているデータセットの中身が被ってしまう問題が発生しました。 "interactions"データセットを削除することでevent trakcerに追加した中身を消すことができます。 しかし、"interactions"データセットを削除するためにはevent trackerの削除やfilterの削除も必要です。 そのため、実際に試したところ、約20~30分filterが不在になることとなりました。 (この時間は今回試したデータセットの大きさの場合であり、データセットの大きさなどで変化します。) filterではすでに購入した商品などを除くような処理をいれているため、filterがないことはよくないと判断をし、最終的には"interactions"データセットは初期load以降はevent trackerで補うように変更をし毎日の作り直しをやめました。 さいごに 今回、リアルタイムな閲覧情報などをAmazon Personazlieのevent trackerで導入することで変化するレコメンドについてご紹介をしました。 また、ご紹介した機能は検証中のため、現在全ユーザに公開されている機能ではありません。 これからももっとよりよい精度と新しい発見を提供できるようにレコメンドに磨きをかけていきたいと思います。 最後までお読みいただきありがとうございました。明日はkikuchiさんとcuresevenです!
この記事は New Relic Advent Calendar 2021 16 日目の記事です。 遅刻しちゃいました。本当は発表の裏側的な話にしようかなと思ったのですが、先日のイベントが素晴らしかったので、参加レポートにしました。 はじめに こんにちは!! BASE BANK 株式会社 Dev Division にて Software Developer をしている永野( @glassmonkey )です。 普段はバックエンドエンジニアとして、Go/Python/PHP を主に書いてたりします。 昨日行われた NRUG (New Relic User Group) Vol.1 に参加し、社内での活用事例として「初心者でも簡単に扱えるNew Relic」というコンセプトで発表させていただきました。 nrug.connpass.com 前回のvol.0 では同僚の清水( @budougumi0617 )による参加レポートもあるので合わせてご確認ください。 Goを New Relic で扱いやすくするOSSの紹介もあるので、ぜひ御覧ください。 devblog.thebase.in NRUG (New Relic User Group)とは NRUG(New Relic User Group)はNew Relicを活用するユーザーの集いです。 Slackもあるので、興味のある方はぜひご参加ください。一緒にわいわいしましょう。 招待リンクは こちら です。 発表した内容 内容は私達 BASE BANKチーム が運用しているAPIを New Relic を使って改善したという事例です。 裏コンセプトとして、 NRQL をまともに書けない私でもNew Relicを活用し、数字として繋がるような改善をすることができたというのがあったりはします。 別の切り口でのパフォーマンス改善話を別チームの @cureseven さんから、記事公開の予定なのでそちらも楽しみに!! 公開は18日予定です。 感想 New Relic 三昧の1日で非常に楽しかったです。 特に中の人に直接質問できたのは大変良い機会でした。 リリースノートを眺めるのも楽しいですが、肉声でおすすめの機能が聞けることはイベントの参加の醍醐味だと感じました。 特に CodeStream は分散トレースからログだけでなく、普段使ってるIDEでコードジャンプが実現する素晴らしい機能で感動しました!! 活用していきたいなと思いました。JetBrains系、VS Code、VisualStudioに対応してるので普段使いには困らなさそうです。 www.codestream.com ぜひ一緒にオブザーバビリティをやっていきたい人いたら一緒に働きましょう!! open.talentio.com open.talentio.com
CTOの川口 ( id:dmnlk ) です。 BASE株式会社は本日、 The PHP Foundation への寄付を行いました。 The PHP Foundationとはなんぞや、というのはインフィニットループ様の記事を読むのがとてもわかりやすいのでそちらをご覧ください。 www.infiniteloop.co.jp BASEで動いている多くのコードはPHPであり、そのPHPの継続的な開発を支援するのは当然であると考え今回寄付をしました。 opencollective.com とりあえず今年分はまとめて行いましたが、来年以降も継続的に行っていく予定です。 この記事を見た、PHPを利用している企業の皆様も一度ご検討ください。 余談ですが、OpenCollectiveを初めて使いましたが組織の妥当性検証ができないのが気になりましたが、騙って寄付するメリットもよくわからないですし気にしないこととしました。
この記事は BASE Advent Calendar 2021 の16日目の記事です。 こんにちは。ProductManagementグループに所属している坂東( @naoto bando )です。 2021年はメンバーが10名を超える2つのPJを同時進行する機会があり、色々と考えることや学ぶことの多い年でした。 特にコロナ禍の真っ只中だったということもあり、リモート下におけるコミュニケーションはこれまで以上に気を使う面が多くありました。 本稿ではそういったコロナ禍におけるコミュニケーションの中のランチ会(懇親会)のさいに工夫をしたことを書かせていただきます。 読まれた方が工夫されている点などがありましたら、twitterなどで教えていただけると嬉しいです。 ※これはランチ会後の状態です。楽しそうな雰囲気になりました。 大人数のリモートランチの難しさ リモートであってもなくても、PJにおけるランチ会など懇親会の目的は、 PJメンバーが一定のコミュニケーションをとり、お互いの理解度を深めることにあるかと思います。 そのため、企画者の最低限の役目は「みんなが安心に快適に話せる」環境を作ることです。 オフラインや少人数リモートは楽 これらの状態はオフラインであれば、対面する人やそばにいる人などの2~4名ほどの小さなノードになって対話が進むため、放っておいてもおおよそなんとかなります。 また、だいたいのケースで企画者は全体的に声が聞こえ目が届く状況にあるので、席をシャッフルをするなど、状況を見ながら対応を決めていくことができます。 つまり、場所を提供すればなんとかなります。 オンラインでも3〜4名ほどであれば、比較的みんなが満足に話せる様に感じます。 大人数リモートの罠 しかしながら、オンラインでかつ10名を超えるような規模感になると、次のようなことが問題になります。 声がかぶると対話できないため喋れない 少数の人が喋り続け、会話の輪に入れない 急に話をふられて焦る そのため、話すのが好きな人が話し続ける感じになりがちかなぁ。と感じています。 こうなってくると気持ちも離れ、仕事のことを考えはじめ、いっそうコンテキストがわからなくなっていきます。(まさに私がこの状態になりがち。) これはもともとの目的からすると大変よろしくない状況です。 Remo Conference などを用いれば小さなノードにすることもできますが 別部屋の雰囲気などがわからないので、企画者としては怖いものがあります。 どこかの部屋が静まりかえっていたりしたらと思うと、怖くないですか・・・? どうしたか ここまでのことから、問題は参加者がしゃべる機会やタイミングを失うことで、場への参加を諦めてしまうことにあると言えそうです。 そこで、この問題の解消方法として、以下を満たせばランチ会の目的を果たせると考えました。 それぞれが話す時間を強制的に作る 話のお題をある程度予想できるようにしておく(場のコンテキストから置いてけぼりにならないようにする) また、PJ内でのランチ会は継続的におこなわれるため、適度なランダム性と拡張性を持てるようツールを用いておこないました。 ここからは NETA というツールを使ったランチ会と、自前のLT会ぽいランチ会について紹介していきます。 それぞれに一長一短がありました。 参考になれば幸いです。 NETA を使ってみた NETA はAID-DCC Inc.さんが作成されたオンライン上のコミュニケーションツールです。 このツールは事前に「話すテーマ」と「話す人」を設定しておくと、ランダムで「話すテーマ」と「話す人」を指定してくれます。 プリセットの話すテーマ集も準備されています。 「オンライン飲み」や「アイスブレイク」「合コン」「SONTAKU」(?)などのテーマを選択することで、参加者の名前を記入するだけで開始できます。 実際にやったこと 以下の理由で事前にPJメンバーに対し「他のPJメンバーに聞きたいこと」をアンケートで募集しました。 プリセットの質問は少々PJの雰囲気に合わないかなと感じがし PJメンバーの対話感を出したかった こうすることで、局所的に質問者と返答社の擬似的な対話を作ろうとしました。 PJメンバーへの呼びかけ Googleフォームで簡単に「他のPJメンバーに聞きたいこと」を収集するフォームを作成。 集めた質問集。 「さいきん一番痛かったこと辛かったこと(物理・精神どちらでも可)を教えて下さい」?🤔 質問にも個性がでておもしろいですね。 集計した質問週と参加者をNETAに質問を記入すれば準備完了です。 実際に動かしてみるとこんな感じです。動きがあるので楽しいですね。 やってみて 適度にランダム感があり、楽しく過ごすことが出来ました。 質問にも返答にも個性が出るので、PJメンバーの意外な一面などを見ることができました。 よく出る質問や、よく当たる人などが出てくるため、そういったハプニングも含めて場を楽しめます。 ただ、指名される人はランダム抽出のみであるため、ランチ会も後半になると指名されない人がでてきます。 そのため、最後はルーレットを回して話すテーマを決め、回答者はファシリテータが指名する形になります。 なので、ファシリテータは状況に気を使いながら進行をする必要がありました。 NETAの特徴 何が当たるか、誰が当たるかわからないランダム性を楽しみたいとき すでにPJ内の心理的安全性が保たれているとき 参加者がアドリブに強いとき こんなときはNETAが良さそうです。 ランダムであることの面白さを生かすのであれば、NETAはオンライン飲みなど、よりカジュアルな場面の方が活きるかもしれません。 逆に、どうしても話す人が中心になってインタラクションが生まれにくかったり、 ビジュアル的な説明ができないので、トーク力が出てしまうと言った点が気になりました。 こういったことを活かして、次のような形式を試してみました。 LT会形式にしてみた LT会は(説明するまでもないかもしれないですが)1人の持ち時間が数分の短いプレゼン会です。 気負わず気軽に発表してみましょう。といったものです。 この形式にのっかり、先に質問と書き込むスペースをわたしておき、あとは適当に画像とか用いて質問に答えてもらいました。 事前に準備できて、かつ画像やリンクで表現できるため、口下手であっても情報量を補えるだろうという考えです。 実際にやったこと 事前に準備が必要なため、1週間くらい前にこんな感じでメンバーに依頼。 (この時期疲れがピークだったようで、毎日何かしら数字をミスっていました。。。) メンバーに渡したのは、質問がいくつも書かれた白紙の MIRO 。 この質問リストの中から答えやすいものや、話せるネタがあるものをいくつか選んで書いてもらうようにしました。 参加者全員が自身の土俵で話をしてもらいたかったためです。 質問リストの作成には 100の質問ひろば というサイトが役に立ったような立たなかったような感じでした。 質問ネタに困ったら使ってみてください。 実際に書き込んでもらう際は、全体の流れがわかるようにこんな感じにしてました。 右側のFrameには事前に名前が記入されていて、各自そこに書き込みます。 中央付近のShuffle部分は発表順を決めるために利用しました。 付箋に参加者の名前を書いておき、発表前にMiroの Totally Random というアドオンを使ってシャッフルします。 多少はランダム性があったほうが楽しい気がするんですよね。 やってみて まず、全体としては、発表形式かつ発表順が事前にわかっているため「飯を食う時間の確保」「気持ち的な準備」ができるので進行が非常に楽でした。 発表者としては、事前に画像を用いて書き込めること、答える質問も自由に選定できるため、自分の言いたいことだけを言える楽さがありました。 また、Miroの機能でスタンプで発表にリアクションできるため、よりインタラクティブな場が生じたように感じます。 LT会形式の特徴 参加者それぞれの認識が浅いとき 安定して進行させたいとき こんなときはLT会形式が良さそうです。 進行にランダム性を廃している点と、自己紹介になっている点で、より「人を知ること」にフォーカスした形式となりました。 PJ始まりたてで、自己紹介などからやる必要がある際には、こういった形式のほうがより解像度高くその人を知ることができて良さそうです。 逆に、すでに心理的安全性が保たれていて、話しやすい状況になっている場合、 この形式では対話は生まれにくいので、機会としてはもったいないかもしれません。 まとめ コロナ禍においてPJのあり方や企業への勤め方も大きく変わりました。 また、働き方も多様になり、弊社でもリモート前提で入社される方も増えてきています。 そうすると、対面であっても対面でなくても、情報や心理的の格差がなくパフォーマンスを発揮できるチームビルビルディングが必要性が増していくでしょう。 この時代の流れは今後も続くと思うので、多様性を維持して働きやすい環境というのは、どの会社でも直面している課題なのではないかと思います。 この記事をここまで読んでいただいたみなさまは、きっといろいろな工夫をされていると思うので、 「こんな工夫してるよ!」というのがあればぜひお話を聞きしたいです。 @naoto bando まで気軽にお声がけください。 以上です。明日のアドベントカレンダーはDataStrategyチームの杉さんの記事です。お楽しみですね!
はじめに この記事はBASE Advent Calendar 2021の15日目の記事です。 BASE株式会社 Owners Experience Frontend チームのパンダ( @Panda_Program )です。 2021年の5月に入社してから、アサインされるプロジェクトの仕事以外に社内 UI コンポーネントライブラリ「BBQ」のメンテナンスに取り組んでいました。 その中でも特に Storybook 周りの整理をする過程、Storybook の v5 から v6 へのバージョンアップとその自動化のプロセスを以下の記事にまとめました。 Vue2 + Storybook v5 のコンポーネントを v6 向けに書き換える TypeScript Compiler API で40の Storybook コンポーネントを storiesOf から CSF(Component Story Format)に置換した 本記事はこの続きにあたります。 BASE の社内 UI コンポーネントライブラリ開発において、ここまでは Storybook というツールをしっかり使うということを目的としていました。 しかし、ここからは Storybook の力を引き出して開発者にその恩恵を感じて貰い、DX を向上させるところまで進めたいと考えています。 本記事では、現状の BBQ の課題を解決しつつ、Storybook をフル活用するために Chromatic の利用を検討し、社内で議論して実際に使った後、導入に至った過程を紹介します。 このため、本記事は「BBQ の課題 → 課題の解決策の検討 → ツールの選定 → ツール導入後の開発フローの変更の紹介」という構成を取ります。なお、Chromatic のインストール方法や具体的な使い方は本記事では扱わず、 別の記事で紹介しています。 社内UIコンポーネントライブラリ「BBQ」の課題 BBQ とは何か BBQ とは、BASE で利用している社内用の UI コンポーネントライブラリです。 Vue2 と TypeScript、scss で記述しており、Storybook で表示確認をしています。Web アプリケーションの BASE とは独立したレポジトリで管理、運用されています。 一例を挙げると、商品が未登録のときに表示される「EmptyBox」というコンポーネントは、この BBQ の中に作られており、import で呼び出すだけで利用できるので各所で再利用が可能です。 商品が購入されていないことを表すUI Storybook ではこのように記述されています。 const Template = (args, { argTypes } ) => ( { components: { EmptyBox } , props: Object .keys(argTypes), render(h) { return ( <div> <section> <h1>・ボタンあり</h1> { /* empty box */ } <bbq-empty-box size= {this .size } icon= {this .icon } > <div slot= "text" > <p><span>まだ商品が登録されていません</span></p> <p><span>商品を登録してあなたのネットショップを作りましょう</span></p> </div> <bbq-button type= "submit" slot= "action" width= {this .buttonSize } icon= "plus" >商品を登録する</bbq-button> </bbq-empty-box> </section> <section style= { { marginTop: '30px' } } > <h1>・ボタンなし</h1> { /* empty box */ } <bbq-empty-box size= {this .size } icon= {this .icon } > <div slot= "text" > <p><span>商品が見つかりません</span></p> </div> </bbq-empty-box> </section> </div> ) } , } ); このように、 BASE のデザインを体現した再利用可能なコンポーネントライブラリが BBQ です。 他にも Button や Modal、Calendar などの多数のコンポーネントがあります。コンポーネントの粒度は Atomic Design の Atoms から Molecules 相当に絞られています。 BBQ の直近の課題 さて、BBQ の直近の課題は大きく以下の2点です。 既存のコンポーネントを改修した際に発生する DOM、CSS に起因する表示崩れを自動で検知できないこと 依存モジュールのバージョンアップに時間がかかること BBQ にはテストがありません。 ビジュアルリグレッションテストやコンポーネントテストがあれば、上記の2点は解決できるはずです。以前はスナップショットテストがあったそうですが、私が入社した時には既に CI 上でもローカルでも動かされていませんでした。 テストがないと、開発者は動いているコードの改修に対して萎縮してしまいます。 自分がバグを仕込まないか戦々恐々としながらコードを書き換えて何度も動作確認をし、最後は勇気を振り絞ってリリースをします。 もしそれでバグが出てしまったらロールバックをしなければなりません。その時間と手間に加えて、原因分析、事象の共有、再発防止策を考えるなど多くのことに時間が割かれてしまいます。 テストがある場合は、ローカルや CI でテストが落ちていればそれを修正するだけでいいのにもかかわらず。 また、共通コンポーネント集はどの企業にとっても素早い開発速度を支える重要なツールであり、デザインや開発のコストを大幅に減らし、ユーザーにリリースを届けるスピードを加速させる大事な資産です。 その資産を守り、健全に育てるためにはやはりテストの導入が必要だと考えました。 そこで、まずは何らかのテストを導入することを BBQ のメンテチームのメンバーに相談しました。 BBQ に対するテスト方法、ツールの選定 BBQ にはメンテナンスチームがあります。メンバーは自分を含めた5人のフロントエンドエンジニアで、プロジェクトの合間にボランティアでメンテナンスをしています。 ライブラリのバージョンアップや、コンポーネント全体の見直しで発見した issue の対応などを行なっています(12月からはアドベントカレンダー 6日目のアクセシビリティの記事 を作成した @rry が加わって6人になりました)。 上記の BBQ の課題をメンテチームに共有し、E2E や VRT(ビジュアルリグレッションテスト)、コンポーネントテストやユニットテストなど色々なテスト方法がある中で、 BBQ に最適なテストは何か議論をしました。 みんなで議論した結果、VRT が BBQ の課題を解決する手段として最も良いと結論づけました。 議論の中で挙げられた検討事項は以下のようなものです。 E2E テストはそもそも今回の課題に見合っていないとして除外。 BBQ はコンポーネントのライブラリ集なのでページをまたぐ画面遷移やバックエンドまたはモックも必要になるようなログインといった粒度の大きなテストは不要だから ユニットテストも除外。BBQ は Vue.js を クラスコンポーネントで記述しており、state を利用したロジックがほとんど。 このため、ロジックをユニットテストに切り出しにくいから コンポーネントテストは、少し議論したけどやはり却下。 BBQ のライフサイクルを考えると、たくさんコンポーネントを追加・改修していくフェーズは終了しており、開発自体は活発ではない。 全てのコンポーネントに対してテストを書くコストは大きい一方で、既存のコンポーネントは数年間運用してバグが出ていないことが確認されており、既存のコンポーネントに対する新しい機能の追加も今はほとんどないため VRT(ビジュアルリグレッションテスト) はちょうど良い。 現在は CSS の修正や、ロジックにあまり関係しない DOM のちょっとした修正がメインであり、開発者は表示崩れの有無を主に確認したいため。 スクリーンショットを撮って差分を表示するだけで実現できるため、コンポーネントテストより記述は少なくなる 以上が VRT を採用した理由です。 E2E やユニットテストを今は採用しなかった理由も記述しました。議論の中では最初に候補から外されたくらいメンバー間で不採用の理由は自明でしたが、本記事は意思決定の過程を紹介する記事なのであえて書き残しておきました。 コンポーネントテストは私から提案しました。BBQ は Input や Modal といったエラー状態や開閉状態を持つコンポーネントが存在するので、それらのテストもでき、ロジックが壊れていないことを担保できると考えたからです。 しかし、メンバーと議論して BBQ に対する理解を深めていく過程で、開発のホットな箇所をこそ守るべきだと考えを改め、VRT が適切だと判断しました。 幸い私は Storybook の VRT ツールである Chromatic の活用経験があったため、BBQ の課題は VRT で解決できるであろうことが予想できました。 課題の解決手段から考えるのではなく、テスト対象のアプリケーションの性質とその課題を考えることが重要だねとチームで確認しあいました。 そして VRT を導入しようとチームで決めた後は、 VRT を実施するツールの検討に入りました。 なお、これは将来に渡ってコンポーネントテストやユニットテストなどを導入しないということではありません。 実際、VRT の導入と並行して、BBQ にバグが出たときの再発防止策としてコンポーネントテストが導入され、過去にバグがあった一部のコンポーネントでデグレが起こらないようにテストが記述されました。 今回は BBQ 全体に関わるテストの導入という文脈であることをご理解頂ければと思います。 VRT を実施するツール。reg-suit と Chromatic VRT とは何か そもそも VRT(ビジュアルリグレッションテスト)とは何でしょうか。 システム開発の文脈でリグレッションという単語は「新規開発に伴って、既存の機能が正常に動作しなくなること」を指します。これはデグレとも呼ばれます。 リグレッションテストは、「コードを書き換えても以前の機能が正常に動作することを担保するテスト」です。それを視覚の面からテストしたものがビジュアルリグレッションテストです。 具体的には、画面(ブラウザ)にページやコンポーネントを表示してスクリーンショットを撮影し、現行の UI と改修後の UI を比較して差分の有無を検出することで実現されます。 reg-suit と Chromatic この VRT を実施するツールとして有名なのは reg-suit です(「レグスイート」と呼んでる人もいますが、suit の発音は「スーツ」です)。 reg-suit について調べた結果、今回は以下の理由から採用を見送りました。 reg-suit は画像の差分を解析して diff を表示してくれるツールであるため、画像は自分たちで別途準備する必要があるため reg-suit には便利な plugin が揃っているが、plugin の選定をしたり、設定ファイルを書いたり、S3 のバケットを準備したりする必要があり、Chromatic と比較すると準備量が多いため 先程記述したように自分は Chromatic の利用経験があったため、コマンドを一行実行するだけで Storybook をビルドして publish できる体験が忘れられませんでした。 しかも全てのストーリーに対してコミットごとの差分を解析、表示してくれる Chromatic は、導入するのも捨てるのも楽だと考えていました。 特に、Chromatic は Storybook のメンテナーが作成したツールであり、BBQ は Storybook で表示確認をしている点も考慮すると、今回は Storybook のエコシステムに乗った方が良いという判断をしました。 reg-suit が悪いわけではなく、こちらも上記のテスト方法の検討結果と同様に BBQ というアプリケーションに対しては Chromatic の方が適切だと判断しただけです。 なお、 Chromatic の具体的な使い方については別の記事をご参照ください。 以上のような意思決定の過程と結論をマネージャーに共有したところ、まずは無料プランで導入し、 BBQ のメンテナンスチームの開発メンバーが Chromatic を使った開発フローを体験した上で、良し悪しを判断しようということになりました。 そこで、実際に Chromatic を一部のブランチに適用していく準備を始めました。 Chromatic を効果的に活用するための準備 ストーリーの数を増やす Chromatic を BBQ に導入する前に、コンポーネントのストーリーのバリエーションを増やしました。 デザイン崩れを検知するためには、コンポーネントの様々なバリエーションを予め表示しておき、カバー範囲を広げないと VRT の効果が半減するからです。 コンポーネントを改修した際に、特定の Props を与えたときにだけ表示される状態の考慮が漏れていて、気づかないところでバグが発生している可能性もあります。 そのようなバグを未然に防ぐために、様々な Props を与えたコンポーネントのストーリーを作成して管理下に置くことが理想です。 具体的には、以下に挙げる Search コンポーネントのように、Storybook の CSF(Component Story Format)の機能を利用して、 Props を部分的に変えて一つのコンポーネントのバリエーションを網羅するようにしました。 import { action } from "@storybook/addon-actions" ; import { Story } from "@storybook/vue" ; import Vue, { VNode } from "vue" ; import README from "./README.md" ; import Search from "./Search.vue" ; export default { title: "Elements/Search/Search(Vue)" , parameters: { notes: { README } , } , } ; type Props = { name: string placeholder: string keyword: string disabled: boolean } const Template: Story<Props> = (args, { argTypes } ) => Vue.extend( { components: { Search } , props: Object .keys(argTypes), render(h) { const { name, placeholder, keyword, disabled } = this .$props return ( <div style= {{ padding: "40px" }} > <bbq-search name= { name } placeholder= { placeholder } disabled= { disabled } vModel= { keyword } onSearch= { (val: any) => this .search(val) } onChange= {this .change } onInput= {this .input } /> </div> ) as VNode } , methods: { input(val: string) { action( 'input' )(val) } , search(val: string) { action( 'search' )(val) } , change(val: string) { action( 'change' )(val) this .$props.keyword = val } , } , } ); export const Default = Template.bind( {} ) Default.args = { name: 'search' , placeholder: '商品名・説明から検索' , keyword: '' , disabled: false , } ; export const NoPlaceholder = Template.bind( {} ) NoPlaceholder.args = { ...Default.args, placeholder: '' , } ; export const WithKeyword = Template.bind( {} ) WithKeyword.args = { ...Default.args, keyword: 'おいしい肉' , } ; export const Disabled = Template.bind( {} ) Disabled.args = { ...Default.args, disabled: true , } ; さまざまな Props を渡す もちろん Search 以外にも Button や Input のような全てのコンポーネントに対してこのような変更を加えています。 Props のバリエーションを増やすことで、結果的にストーリーの数が当初の79から148に増えました。 GitHub Actions でコミットを push するたびにビルドする Chromatic を導入しても常に活用しなければ意味がありません。 そこで、ブランチごとに、またコミットの push のたびに Chromatic をビルドするために GitHub Actions(GHA) を活用しました。 Chromatic 用の GHA のテンプレートは公式で用意されています。 このため、開発者としてやるべきことはこのテンプレートを使うことと、ビルド用のトークンを GitHub レポジトリのシークレットに埋め込むことだけした。これで各ブランチで Chromatic をビルドする準備ができました。 Chromatic の効用をチームで体感する dependabot が作成するブランチに適用する 便利なツールを導入した後はチームで運用し、ツールに対して知見をチームで溜めていくことが次のステップです。 BBQ のチームメンバーが Chromatic を体験してみるにあたり適切な粒度のタスクを探したところ、モジュールのバージョンアップデートがちょうどいいように思われました。 従来、 dependabot によってバージョンアップデートのブランチが作成されたときは、担当しますと手を上げた開発者のローカル環境で手による動作確認がされていました。 具体的には、 Storybook をローカルで立ち上げて全部のコンポーネントを表示、チェックして、以前のバージョンと比較して問題ないことを確認していました。 しかし、この手法は確認に時間がかかる上に、「ビルドは通ってるし目視確認はしたけど本当にバグは出ていないと自信を持ってまでは言えない」という雰囲気がメンテチーム内にありました。 このため、心理的な負担もありモジュールのバージョンアップデートの対応は即座になされるわけではありませんでした。 ただし、脆弱性のあるパッケージのバージョンが上がっていないなどクリティカルな問題があるというわけではなく、dependabot があるパッケージのバージョンアップのブランチを作ってみんな気づいているけど、2〜3週間放置されている間に次のバージョンがリリースされたというような状況です。 パッケージのバージョンアップデートの理想状態は、CI でテストとビルドをしてエラーが出たらアップデートをやめ、CI が Fail しなければ master マージするという運用です。 テストがある場合は人がアドホックに動作確認をする必要がなくなります。しかし、BBQ にはテストがないためそれができていなかったのです。 差分がないから安心してリリースできる この課題は Chromatic で一定程度解決できると予想していました。 BBQ は UI コンポーネント集のため、ロジックに関するライブラリはほとんど入っていません(date-fns くらい)。依存ライブラリは Webpack の loader や eslint、stylelint といったビルド時に必要なツールくらいです(このため、パッケージのアップデートが遅れてもクリティカルな問題にはならなかったのです)。 dependabot が作成したブランチの CI を実行し、Chromatic で Storybook をビルド、表示するようにしました。 Chromatic 上ではブランチの baseline と呼ばれる時点のスクリーンショットとの差分が解析されます ( 詳しくはブランチとベースラインの解説は公式ドキュメントをご覧ください。 )。 テストと同様に、差分があれば Chromatic 上で目視チェックをし、差分がなければ問題ないとして master マージをすれば良いのです。 Chromatic が Storybook をビルドして URL を発行してくれるため、わざわざローカルで Storybook を立ち上げる手間が省けます。 また、差分があるコンポーネントは Chromatic がピックアップしており、差分のがある箇所もハイライトをつけてくれているため、抜け漏れがありません。 これは動作確認の時間の大きな節約になる上に、開発者としても不安なくパッケージアップデートを取り込んだ新バージョンの BBQ リリースできます。 実際にいくつかのパッケージのバージョンを上げましたが、以前と違ってとても楽になりました。 人間の目では気づかない差分もカバーしてくれる Chromatic の試用期間中に、アイコンに変更があるという差分が検知されました。 しかし、元のアイコンと新しいアイコンを見比べてみても全く同じように見えます。エンジニアメンバーが手を加えた覚えはなく、Chromatic のご検知かと疑いながら念のためデザイナーのチームに確認してみました。 すると、デザイナーから「特定の環境下で表示がおかしかったので、少し前に SVG の不要なパスを削除した。見た目には変更がない」とコメントがあり、それはまさにこの差分のことでした。言われてみると、確かに以前のアイコンの線が少し太いように見えます。 Chromatic が検知したアイコンの差分 今回は拡大しているのでなんとなく差が分かりますが、Storybook 上のアイコンは 14px という小さいサイズで表示されているため、目で見ても差分が分かりませんでした。 Chromatic はこんなところまで検知してくれるのかという驚きで、BBQ メンテチームが盛り上がると共に Chromatic に対する信頼感が増しました。 VRT がある開発フロー 開発フローに Chromatic を組み込む Chromatic は開発フローの中で、コードレビューのフェーズで活用しています。 コードと表示に変更があった場合、その表示の変更は意図したものであるかを確認するためのレビューです。コードレビューの中に、デザイン面の差分のレビューを組み込むイメージです。 Chromatic は GitHub と連携可能であるため、PR の画面からビルドの状態を知ることが可能です。下記の画像では「UI Review」と「UI Tests」に緑のチェックマークがついていますが、もし表示に差分があるときは黄色の丸いマークが表示されます。 GitHub の PR の Chromatic の状態のフィードバック Chromatic のサイト上で差分のあるコンポーネントの画像がピックアップされているので、レビュワーはそのコンポーネントに対するレビューをします。 気になる点があれば Chromatic 上でコメントを記入し、問題なければ Chromatic 上で Accept をします。 上記の画像に緑のチェックマークがついているのは、そもそも差分がない場合か、全ての差分に対してレビュワーが Accept をした場合のどちらかです。この画像では差分があったけど Accept しているため、「UI Tests」の説明に「4つの変更が基準として Accept された」と書かれています。 「レビュワーの責任範囲はコードレビューまでで、動作確認や表示確認はブランチの開発者の責務」と考える方もいますが、BBQ では Chromatic の導入に伴い表示確認もレビュワーにお願いすることになります。 ただし、Chromatic が差分を予め検出しているため、レビュワーの負担が大きく増えるようなことはありません。 むしろ、レビュワーが表示の変更点を網羅的に知れるという知識の標準化に役立つ上に、見た目の変更を伝えるために前後比較のスクリーンショットを撮って PR に貼り付けるというレビュイーの手間も省けます。 これが VRT を組み込んだ開発フローです。 Chromatic に対する BBQ メンテチームのメンバーの意見 BBQ メンテチームのみんなに Chromatic を一通り触ってもらった後、マネージャーに Chromatic を触った感想を報告する会が開かれました。そこでは以下の声を聞くことができました。 手元で Storybook を立ち上げていたが、その必要がなくなった 変更点をまとめて Chromatic で見れる 変更があったコンポーネント数が表示される 差分が全て記録されているので安心感がある 人の目でわからない差分を検知してくれた アイコンが変更されていた もちろん懸念点も少しありました。例えば「Chromatic 上のコメントが PR や Slack に通知できず、メールでの通知だけである上に、URL がコメントへのリンクになっていない」というものです。ただ、懸念点がどれもツール側の話なのでこちら側でできることはなく、とりあえず改善要望を出しておきました。 以前から折を見て議論を重ねており、マネージャーにも逐次共有して意見を聞いてきたため、BBQ に VRT を導入することに対する反対や Chromatic ではない別のツールが良いなどの声は出なかったためホッとしました。 以上が、BASE の BBQ という共通 UI コンポーネントライブラリのレポジトリに Chromatic を導入するという意思決定の過程でした。 終わりに フレームワークやライブラリ導入の技術選定に関する記事は枚挙にいとまがないですが、外部ツールを選定するためのプロセスや意思決定に関する記事はあまり見ないなと思い、本記事を書いてみました。 Chromatic の導入を例に挙げていますが、以下の点を意識して読んで貰うとより一般的な視点が得られると思います。 適切なツールを選ぶためには、まず現在開発しているアプリケーションの性質をつぶさに観察すること 社内で使用するツールはマネージャーと二人三脚で選定すること。ツールの調査や社内に使い方を広めるのは現場のエンジニアの役割であるが、導入可否の最終決定者はマネージャーであるため ツールを使い続けるためにチームからフィードバックを受けること。一人が特定のツールの導入を陣頭指揮してもいいが、ツールを使うのはその人だけではないため なお、 Chromatic の具体的な使い方については別の記事で紹介します。 Chromatic の導入や使ってみたいツールの導入に役立てば幸いです。 明日の記事は坂東さんの「10人以上のPJでリモートランチ会をするときの工夫したこと」です。
この記事は BASE Advent Calendar 2021 の14日目の記事です。 こんにちは。UIデザイナーのノムラ( @nomjic )です。2021年の初め頃にデザインリサーチPJを開始して、3〜4名のメンバーでここ一年間、定性リサーチにトライしてまいりました。その内容を本記事に書いていきたいと思います。 デザイン業務の傍らで実施した活動につき、リサーチ内容の深度・精度はいささか低めです。すでにがっつりリサーチに取り組んでいる方よりも、「 定性リサーチ活動を始めようとしている人、始めたいと思っている人 」にとって参考になる記事にできたらいいなぁと思いつつ、書かせていただきます。 デザインリサーチとは 我々の行っている活動を「デザインリサーチ」と呼んでいますが、 定性リサーチ、UXリサーチ、ユーザーリサーチ 、といった言葉に読み替えていただいても問題ありません。ゆくゆくはプロトタイピングやユーザビリティテストも取り入れていきたいと思いますが、現状では「 ユーザーにインタビュー調査をしていろいろ実態を探っています 」という一言でほぼ言い尽くせるリサーチ活動です。 以前に書いたブログ記事 で私の思うところの「デザインリサーチとは」を述べていますので、ご興味お有りでしたら併せてお読みいただけると幸いです。 今年実施した内容・概要 4つのリサーチを行いました。1回のリサーチで数名〜10名弱の方からお話を聞いています。 インタビュワーや書記役は主にPJメンバーが行っていますが、他の社内メンバーに参加してもらうこともあります。 「まずはユーザーとデザイナー(およびPM、エンジニア)が対話する場を作ろう」という形で動き出し、得られた知見を蓄積しつつ、なるべく多くのメンバーを巻き込んで社内に定性リサーチ文化を醸成する、ということを目指してきました。 実施した4つのリサーチについて、以下、概要を述べます。 リサーチその1 「まずは話を聞こう」 リサーチ内容 2021年 2月にインタビューをし、3月にかけて分析をしました。9ショップ・10人のBASEショップオーナーさんから話を聞いています。アパレル系、雑貨系、食品系などさまざまなジャンルのショップから話をお聞きしました。 ※ インタビュー設計や分析の流れは、 以前の記事 に書いたデザインリサーチワークショップの内容に沿っています。 所感 特に仮説等を立てずに「とにかく話を聞いてみよう」と始めたリサーチだったため、得られた成果は少々散漫というか、どう活用するかをイメージしづらいものでした。 とは言え、何はともあれ 「ユーザーに連絡して日程調整し、話を聞かせてもらって分析し、社内に報告する」という一連の流れ をデザイナー主導で行えたというだけでも、一つの大きな成果であったと思っています。 リサーチその2 「属性ベースでセグメント切って深掘り」 リサーチ内容 2021年 5月にインタビューをし、6月にかけて分析をしました。7ショップ・9人のアパレル系BASEショップオーナーさんから話を聞いています。 所感 「インタビュー対象の絞り込み(アパレル限定)」「質問項目の具体性アップ」を行うことで、1Qで得られた成果に比べてより具体的・実用的な情報を得られました。 基本的には「その1」と同じ流れでリサーチを行なったため、だいぶスムーズに一連の流れをこなせています。また、この回ではリサーチPJの外部に協力者を募り、他職種メンバーも巻き込んでいます。(インタビュワーや書記を行ってもらいました。) リサーチその3 「退会したユーザーとも話してみよう」 リサーチ内容 「その2」と並行して、2021年 6月にインタビューと分析を行なっています。BASEから別のECサービスへ移転した3名のショップオーナーさんからお話をお聞きしました。 このリサーチではいくつか仮説を立てて臨みました。結果、かなり具体的で、開発に役立ちそうな情報を得ることができています。 所感 BASEにマッチしなかったユーザーからも意見を集めたい、という想いから実施した「退会したユーザーへのインタビュー」だったのですが、実際に話を聞けた3名は「一度BASEから移転したが、今はBASEに戻ろうとしている」という人たちでした。非常に参考になるお話をたくさん聞けたものの、いささか偏りの大きいリサーチになってしまいました。 リサーチその4 「関心ごとベースでセグメント切って深掘り」 リサーチ内容 2021年8月にショップオーナーさんの関心ごとを問うアンケートを行ったのち、アンケート回答者を対象に 9月にインタビューをし、10月・11月にかけて分析をしました。アンケート結果から「集客・広告に強い関心を持っている」と解釈できる9ショップ・9人のBASEショップオーナーさんから話を聞いています。 所感 このリサーチでは、「傾向」と「課題」の2つの観点で、得られた情報をまとめています。 リサーチも4回目ということで、インタビューおよび分析はだいぶスムーズになってきました。反面、「得られた結果が開発に役立つか?」という面ではまだ不十分である感は否めません。 「その2」同様、この回でもリサーチPJ外からの参加者を募っており、より多くの社内メンバーに定性リサーチを経験してもらえました。 定性リサーチ結果を、どう開発に役立てるか リサーチ結果・知見の社内シェア この一年間で行なったリサーチで得られた情報(議事録、録画データ、インサイト)はスプレッドシート上に一覧表の形でまとめて、社内でシェアしています。また、上記「その1〜4」のリサーチごとに報告会を行うことで、口頭による社内へのシェアも行なっています。 リサーチその2、その4ではリサーチPJ外のメンバーにもインタビューに参加してもらっており、直接的に定性リサーチを体験してもらう形で、ユーザー実態に触れてもらっています。 このように、 シート(ドキュメント)に情報を蓄積する 報告会を実施して知見・成果をシェアする 直接参加して体験もらう という3つのルートで組織内にアウトプットしているのですが、この3つの中で 「直接参加」が圧倒的に意義が大きい(学びが深い) というのが、リサーチPJを率いている身としての本音だったりします。 ドキュメントや報告会では全然伝わらないナマの実態というものがあり、それはプロダクト開発へのマインドにも大きく影響する、と思っています。 (これってライブイベントみたいだなぁ、と思って書いた note記事 などもありますので、ご興味あったらお読みいただけたら幸いです。) 「直接的な体験」と、「蓄積したドキュメント」のバランス 直接参加してもらうのが一番学びが深いのは明らかなのですが、現実に「社内メンバーみんなに参加してもらう」というのは流石に無理があります。(その状態を目指したい、という想いはあるのですが) どうしても参加者が特定のメンバーに限られてしまいます。 組織で活動する以上、その活動の成果は蓄積して「新規メンバーでもアクセスできるように」「当時の担当者が不在でも活かせるように」しなくてはいけません。 いわゆる「属人化を避ける」というやつですね。 なので、直接参加してもらって体験してもらうだけでなく、ドキュメントや映像のような、後から第三者がアクセスできる形で蓄積していかなくてはいけません。 落とし所としては、以下の2つを交互に行うのが良さそうかなと思っています。 実態探索型リサーチ なるべく社内メンバーを広く巻き込みつつ、ユーザーの実態を探るインタビューを行う。 直接的にプロダクト改善に寄与する情報を得ることよりも、「ユーザーを 知ること 」「ユーザーと 対話すること 」に重きを置く。 仮説検証型リサーチ 具体的な仮説を立てて、インタビューにより仮説を検証する。 ユーザーを知ることよりも、プロダクト改善につながる情報を得ることを重視し、ドキュメントとして社内で共有した際に 共感 や 納得感 を呼び起こせる、プロダクト開発メンバーの行動につながりやすいアウトプットを目指す。 そもそも、リサーチ活動の在り方として 現在のデザインリサーチPJは、開発PJや企画PJとは独立した形で、ユーザー実態情報の収集とリサーチノウハウの蓄積を行っています。 この状態では、プロダクトの直接的な改善には繋がりにくく、そのためプロダクトを良くすることに心燃やしているBASE社員たちの関心を集めづらいです。リサーチ活動への参加を促したりリサーチ成果の共有を行っても、期待したほどの反響は得られません。 開発PJや企画PJの内容を鑑みて、それらを評価・検証するような動きをするとか、それらPJの発足の手前段階で関わっていくなど、リサーチ活動の在り方そのものを考えるべきでは?という想いもあり、検討をしているところです。 まとめ いろいろ書きましたが、今の活動形態での良き成果を目指しつつ、活動形態や体制の在り方を検討(MGRと相談)して、より質の高いリサーチ活動を行っていきたいと考えております。 以上です!最後までお読みいただいた方、ありがとうございます。 明日のアドベントカレンダーはフロントエンドエンジニアのkushibikiさんの記事です。お楽しみに!
この記事は BASE アドベントカレンダー 13 日目の記事です。 はじめに こんにちは。 BASE BANK 株式会社 Dev Division にて Software Developer をしている永野( @glassmonekey )です。 普段はバックエンドエンジニアとして、Go/Python/PHP を主に書いてたりします。 最近はチームの分析基盤づくりとかもやってたりします。 そのことについて先日書いたりもしたので、もし良かったらご確認ください。 devblog.thebase.in 私達のチームでは、「BASE」でショップを運営しているショップオーナーが簡単に資金調達をできる「 YELL BANK 」というサービスの開発・運営しています。 thebase.in あるプロジェクトで、ショップオーナーへのコミュニケーション手段に HTML メール を新しく作ることになりました。その際に HTML メール のコーディングも自分たちで行う必要がありました。 今回は その際に行った HTML メール 制作を MJML を使うと楽にできたのでその紹介になります。 HTMLメールの事情 大前提として、 HTML メール を使うとリッチな内容でコミュニケーションを取ることができます。 昨今のメーラーだと基本的には対応しているので使いたくなります。 弊社でも HTML メール の事例で 4月に購入完了メールをリニューアル しました。 では早速 HTML メール を作りましょうと言っても、なかなかそうは行きません。 なぜなら一般に、 HTML メール のマークアップと Webページ のマークアップと毛色が違うからです。 特に、 HTML メール のマークアップは 基本的なCSS はインライン 、 どのような環境でも安定しているテーブルレイアウト が基本となる事情があります。メーラーそれぞれの描画任せが要因だったりするようです。 参考: How to Create HTML Emails Using the Table Element [+ Templates] では HTML メール でどのタグが利用できるのでしょうか。 それに関しては大体は以下のサイトで簡単に調べることができます。 www.caniemail.com 例えば、「BASE」ならぬ <base>タグ だと以下のような対応状況のようです。緑は完全に対応、黄色は部分的に対応、赤は未対応です。 ちなみに対応状況は手動で調べられてるようです。とはいえ最初に見る状況としては大変助かっています。 Because every test is done manually, some features might not have been tested on every email client. Can I email… Email Client Support Scoreboard また製作者の方である @HTeuMeuLeu さんの記事の1つの Outlook is not an Email Client からは各社メーラへの対応状況の複雑さの苦労が垣間見えます。 以上のことから基本的には HTML メール のコーディングは避けたほうが無難です。無理してコーディングする必要がなければ、 Send Grid などの SasS を使うことが良いでしょう。 しかし、コーディングしないといけないときもあるでしょう。そのときに便利なものが MJML となります。 MJMLとは MJML とはレスポンシブな HTML メール を作ることのできるフレームワークです。 mailjet社 が開発しています。 だいたいのことは ドキュメント に載っているので参照ください。 最初は オンラインエディタ で試しながら始めてみるのがおすすめです。 これを使うと複雑なテーブルタグを書かずとも、簡単に HTML メール を書くことができます。 具体例を見てみましょう。 mjml.io 例) < mjml > < mj- body > < mj- section > < mj-column > < mj-image width = "100px" src = "https://mjml.io/assets/img/logo-small.png" ></ mj-image > < mj-divider border - color = "#F45E43" ></ mj-divider > < mj-text font- size = "20px" color = "#F45E43" font-family= "helvetica" > Hello World </ mj-text > </ mj-column > </ mj- section > </ mj- body > </ mjml > 見た目はこのようになります。 ちなみに、実際の HTML はこのような形で出力されています。 MJML で書くと大分簡略化されていることがわかります。 <!doctype html> < html xmlns= "http://www.w3.org/1999/xhtml" xmlns:v= "urn:schemas-microsoft-com:vml" xmlns:o= "urn:schemas-microsoft-com:office:office" > < head > < title > </ title > <!--[if !mso]><!--> < meta http-equiv = "X-UA-Compatible" content = "IE=edge" > <!--<![endif]--> < meta http-equiv = "Content-Type" content = "text/html; charset=UTF-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1" > < style type = "text/css" > #outlook a { padding : 0 ; } body { margin : 0 ; padding : 0 ; -webkit- text-size-adjust : 100% ; -ms- text-size-adjust : 100% ; } table , td { border-collapse : collapse ; mso- table -lspace: 0pt ; mso- table -rspace: 0pt ; } img { border : 0 ; height : auto ; line-height : 100% ; outline : none ; text-decoration : none ; -ms- interpolation-mode : bicubic ; } p { display : block ; margin : 13px 0 ; } </ style > <!--[if mso]> <noscript> <xml> <o:OfficeDocumentSettings> <o:AllowPNG/> <o:PixelsPerInch>96</o:PixelsPerInch> </o:OfficeDocumentSettings> </xml> </noscript> <![endif]--> <!--[if lte mso 11]> <style type="text/css"> .mj-outlook-group-fix { width:100% !important; } </style> <![endif]--> < style type = "text/css" > @media only screen and ( min-width : 480px ) { .mj-column-per-100 { width : 100% !important ; max-width : 100% ; } } </ style > < style media = "screen and (min-width:480px)" > .moz-text-html .mj-column-per-100 { width : 100% !important ; max-width : 100% ; } </ style > < style type = "text/css" > @media only screen and ( max-width : 480px ) { table .mj-full-width-mobile { width : 100% !important ; } td .mj-full-width-mobile { width : auto !important ; } } </ style > </ head > < body style = "word-spacing:normal;" > < div style = "" > <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> < div style = "margin:0px auto;max-width:600px;" > < table align = "center" border = "0" cellpadding = "0" cellspacing = "0" role = "presentation" style = "width:100%;" > < tbody > < tr > < td style = "direction:ltr;font-size:0px;padding:20px 0;text-align:center;" > <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--> < div class = "mj-column-per-100 mj-outlook-group-fix" style = "font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;" > < table border = "0" cellpadding = "0" cellspacing = "0" role = "presentation" style = "vertical-align:top;" width = "100%" > < tbody > < tr > < td align = "center" style = "font-size:0px;padding:10px 25px;word-break:break-word;" > < table border = "0" cellpadding = "0" cellspacing = "0" role = "presentation" style = "border-collapse:collapse;border-spacing:0px;" > < tbody > < tr > < td style = "width:100px;" > < img height = "auto" src = "/assets/img/logo-small.png" style = "border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width = "100" /> </ td > </ tr > </ tbody > </ table > </ td > </ tr > < tr > < td align = "center" style = "font-size:0px;padding:10px 25px;word-break:break-word;" > < p style = "border-top:solid 4px #F45E43;font-size:1px;margin:0px auto;width:100%;" > </ p > <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #F45E43;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" ><tr><td style="height:0;line-height:0;">   </td></tr></table><![endif]--> </ td > </ tr > < tr > < td align = "left" style = "font-size:0px;padding:10px 25px;word-break:break-word;" > < div style = "font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#F45E43;" > Hello World </ div > </ td > </ tr > </ tbody > </ table > </ div > <!--[if mso | IE]></td></tr></table><![endif]--> </ td > </ tr > </ tbody > </ table > </ div > <!--[if mso | IE]></td></tr></table><![endif]--> </ div > </ body > </ html > MJML入門 では実査に、 MJML を触ってみましょう。 プロジェクト構成 最初にメールを作成するためのプロジェクトを準備しましょう。 今回説明するディレクトリ構成は以下としました。 ├── dist ├── mjml │ ├── section │ └── template ├── package.json └── yarn.lock 各ディレクトリの責務は以下の形です。 今回、メールのコンポーネントは再利用 する/しない ぐらいの区別しかしていません。 MJML によるメール作成の知見が社内で成熟してくれば、変わる可能性はあります。 dist …生成される html を出力します。 mjml … 生成するための MJML ファイル群を置きます。 section… Header や Footer など再利用するコンポーネント用 mjml ファイル群を置く所 template ... 1 メールのバリエーションと対応した mjml ファイル群を置く所 インストール 基本的には npm で mjml-cli を入れたら終わりです。 $ npm install mjml package.json には npm run build などで build できるように、以下を追記しておきます。 " scripts ": { " build ": " mjml ./mjml/template/*.mjml -o ./dist " } 基本構成 基本的には ドキュメント を見つつ、 テンプレート を見て、参考にさせてもらうのが近道です。 基本的には HTML と同じ用なセマンティクスを持っているのでそれに従って記載するといいでしょう。 ローカルで開発時は VS codeの拡張 を入れると previewで確認しつつ編集できるので便利です。 基本構成例 < mjml > < mj- head > < mj-attributes > < mj-text color = "red" /> < mj-class name = "content" color = "black" /> </ mj-attributes > </ mj- head > < mj- body > < mj- section > < mj-column > < mj-text > Hello, World(Red) </ mj-text > < mj-text mj- class = "content" > Hello, World(Black) </ mj-text > </ mj-column > </ mj- section > </ mj- body > </ mjml > 基本的なマークアップの規則としては、いわゆるグリッドレイアウトになります。 メールは複数のセクションから構成されますが、基本的な 1 つのセクションの構成は mj-section > mj-column > コンテンツ要素 から成り立ちます。 主に利用するタグは以下の流れになります。 mj-section … 基本的なコンテンツの区切りを定義します。 mj-column … 複数使うことでモバイルでは 1 列、PC では 2 列といったレスポンシブを実現します。 コンテンツ要素 mj-text … テキスト表示 mj-image … 画像表示 などなど 共通の装飾について 文字色などの装飾は個別の要素に指定すれば変更できます。 < mj-text color = "red" > 赤色 </ mj-text > しかし、余白や文字サイズなどは統一感出したいでしょう。その場合は mj-class を使って予め用意しておくことで同時に複数の要素に共通的な装飾を施すことも可能です。 準備は以下のように mj-attribute に class 名を用意しておきます。 < mj- head > < mj-attributes > < mj-class name = "content" font- size = "14px" font-family= "Arial" line- height = "24px" padding= "0" /> < mj-class name = "annoation" font- size = "12px" font-family= "Arial" line- height = "18px" padding= "0" /> </ mj-attributes > </ mj- head > 利用自体は簡単で mj-class を指定するだけです。 < mj-text mj- class = "content" > 半角スペースで複数のクラスの適用もできます。 < mj-text mj- class = "A B" > ファイル分割について ある程度メールの種類が増えてくると、ヘッダー部分などの共通部分を分離したくなりますよね。 それには、 mj-include を使うことで実現します。 たとえば共通の分割線パーツコンポーネントを section/common/divider.mjml で用意してたとします。 < mj- section > < mj-column > < mj-divider border - width = "4px" border - color = "#F0F1F4" padding= "0" /> </ mj-column > </ mj- section > 呼び出し側としては以下のように相対パスで記述するだけで完了です。 略… < mj-include path= "../section/common/divider.mjml" /> 基本的にはセクション単位で扱うことが多いはずなので、共通のパーツも section 単位で作っておくと再利用がしやすいです。 パーツ分割における注意事項としては、パーツ側のコンポーネント編集時には VS Code の preview が動作しないことが挙げられます。 おそらく preview には mj-body などの要素が必要だが、パーツ側コンポーネントにはそれらが含まれていなからだとは思われます。 なので、最初から分離しようとせずにコーディングがある程度完了してから分離したほうが作業効率は良いです。 余談 余談ですが私自身は MJML のことは知らず、同僚の @sam8helloworld が同僚となる前に教えてくれたという個人的エピソードもあります。大変助かりました。 htmlメールのいい感じのフレームワークとかない? sendgridで素直に作れ?はい。 — エターナル・フィールド (@glassmonekey) 2021年5月6日 HTMLメールはご存知かもですが仕様が独特かつHTMLのルールも古いので人がコーディングするのはただただコスト高いっすね それでもコーディングベースで細かくやりたいならMJML, SaaSで基本ノーコードでやりたいなら無難にsendgrid, mailchimp ,b-dashってところですかね — さむ (@sam8helloworld) 2021年5月6日 おわりに MJML を紹介しましたがいかがだったでしょうか? HTML メール は令和となった現代においてもなかなか難しいので、ぜひ皆さんも MJML をご活用ください。 無論実際のメーラー上の確認は最終的には必要ですが、手元でプレビュー見つつローカルで大半の作業を完結できるので、開発体験は最高でしたし何より工数の削減にも繋がり大変重宝しました。 最後に宣伝ですが、まだまだやれてないことがたくさんあるので、一緒にプロダクトを成長させていく仲間を募集中です。 open.talentio.com 明日は 2021 年のデザインリサーチ振り返りです。楽しみですね〜
この記事はBASE Advent Calendar 2021の12日目の記事です devblog.thebase.in ごあいさつ はじめましての人ははじめまして、こんにちは!フロントエンドエンジニアのがっちゃん( @gatchan0807 )です。 この記事は Denoの公式Docsを読んでみた前編 の続きで、前回得た知識を使って実際にDeno + GitHub Codespaces環境でSlack Botを開発してみよう!の回になります。 もしDenoについて詳しく知りたい方は前回の記事からご覧いただけますと幸いです!🙏 どんなSlack Botを作るのか まず完成品のイメージを共有してから実際に実装する内容に触れていきます。 少し話は脱線するのですが、BASEではコミュニケーションツールにSlackを使っており、その中で独自のSlack Appを作成してChatOpsを実現しています。 主だったところで言うと、STGとは別に複数用意されている検証環境のデプロイや起動、特定の開発中ブランチの適用などをエンジニアはもちろん、PMの方々も日常的に行っています。 下記はあくまでイメージですが、下記のようなコマンド的なテキストをBotにメンションして、デプロイを行ったりしています。 @dev-bot deploy repository inspection1 feat/something-branch-name 詳しくは、以前投稿されたこちらの記事をご覧ください。 BASEにおけるSlack活用術を大公開!〜Slackで始める業務改善〜 - BASEプロダクトチームブログ そういった環境があるため、Slack Botを使ったコマンドの実行に一定のリテラシがある前提で下記のようなサブコマンド付きのSlack Botを作成していきます。 作る予定のBotとそのサブコマンドたち 今回作成するのはご覧の通り、登録されたメンバーの一覧を色んなパターンでランダムにごにょごにょするBotです! @random-bot [command] [group] [member-name] ~~~~~~~ 便利コマンド系 ~~~~~~~ @random-bot (help|h) 👉 ヘルプの表示 @random-bot ping 👉 起動確認 ~~~~~~~ データ登録・削除系 ~~~~~~~ @random-bot create [group-name] 👉 グループ(メンバーを追加する枠)の作成 @random-bot (disband|delete-group|remove-group) [group-name] 👉 グループ(メンバーを追加する枠)の解散 @random-bot list [group-name] 👉 グループ内のメンバー一覧の表示 @random-bot add [group-name] [member-name] 👉 グループにメンバーの追加(メンバー名はテキストもメンションもどちらも登録可能です) @random-bot (delete|remove) [group-name] [member-name] 👉 グループからメンバーの削除 ~~~~~~~ シャッフル実行系 ~~~~~~~ @random-bot random-sort [group-name] 👉 グループ内のメンバー一覧をシャッフルして並び替え @random-bot pick [group-name] [number] 👉 グループ内から指定の人数をランダムでピックアップする @random-bot separate [group-name] [divide-count] [merge-option] 👉 グループ内のメンバーを指定の人数ごとにグループ分けする BASEのSlackで下記のようなユースケースを観測していたので、それらのニーズにマッチしたシャッフルのパターンをいくつか用意しています。 デイリーの今日の一言でのランダム化のニーズ 自分が今所属しているプロジェクトチームでは、デイリーで今日の一言とその日やっていることを共有する時間を取ってコミュニケーションと進捗共有を行っています。 その発表順が固定化されないように、 スッキリす の運勢ランキングが一番良い人からやってみたり、 順番ぎめ.com を使ってランダムに並び替えたりしてみていました。 @random-bot random-sort [group-name] はこのユースケースを満たすために実装していきます。 peer1on1でのランダム化のニーズ 続いて、こちらも自分が所属しているフロントエンドのチームではpeer 1on1という制度を運用しており、ランダムに選ばれた2〜3人で雑談をする時間を週に1回取っています。 peer 1on1の詳しい解説はこちらも過去投稿されたこちらの記事に譲りますが、現在もGASで作られたスプレッドシート上で管理されています。 そのシャッフル、本当にシャッフルですか?何気ない落とし穴にハマった話 - BASEプロダクトチームブログ @random-bot separate [group-name] [per-number] はこのユースケースを満たすために実装していきます。 読書会の司会選出でのランダム化のニーズ 最後に、別のチームで定期的に行われている読書会の司会選出のニーズです。 @random-bot pick [group-name] [number] はこのニーズを満たすために実装していきます。 こちらは標準搭載されている Slackのカスタムレスポンス機能 を利用して、(Slackbotのレスポンスを複数パターン登録して)ランダム機能が実現されています。(これを初めて見た時、正直めっちゃ賢いな…!って思いました) ただ、この方法だと想定外のチャンネルで反応してしまうことがあるので、指定するキーワードには注意ですね⚠ 以上のようなニーズがあったので、今回のSlack Bot作る題材にちょうど良いや!と思ったのが作ろうと思った理由です。 さらに、Slackのリマインダー機能と組み合わせて、決まった時間に自動的にシャッフルした結果を出してくれるようになれば手間も減るので良さそうです🙆 実際にDeno環境でSlack Botを作っていく 前回の記事の最後で、Slack社が発表した Denoで提供されるSlack Appの次世代開発プラットフォーム のベータプラットフォームに参加できればそちらで開発する!と言っていたのですが、残念ながらベータプラットフォームへの参加は叶わなかったので、有志がNode.js用Slackライブラリを Deno向けに書き換えて提供してくれているライブラリ の方を利用していきます。 前回の記事がほとんどの項目をStep by Stepで書いたためにめちゃ長記事になってしまったのを反省したので、詳細は公式ドキュメントに譲り、ハマりどころとポイントをピックアップして書いていくことにします! また、今回のコードの全容は こちらのGitHub に公開しているので、もしご興味あればご覧ください👀 コミットメッセージ込みで試行錯誤の履歴が残っていますし、記事公開後もちょこちょこリファクタしたりする予定です(PRやIssueも歓迎です🙆) 実際に作ってみた所感 実際に作ってみてどうだったのよ?という感想を見に来られている方もいらっしゃるかと思うので、まず最初にそちらを書いておきます。 DenoでのSlack Botの開発はロジック部分は基本的にはNode.js上の開発体験とほぼ変わらない。でも、周辺知識の入れ直しで大変。 特に https://esm.sh や https://skypack.dev などの挙動を読み取りながらライブラリバージョンを指定するの辛かった。 まだまだエコシステムもライブラリも発展途上のため、詰まった時に情報がなくライブラリの中を読みに行って自力でデバッグになるのは大変。 逆にライブラリたちがURL管理なのでライブラリを読みに行く労力は少し減っている部分はある。 Deno Deployはまだまだベータ版。すぐに対応できなさそうな問題にぶち当たったので代替としてCloud Runを使った。 今回根幹として使っていたライブラリがDeno Deploy実行環境から提供されていないAPIを使っていて死ぬという事があった😢 GitHub Codespaces上でSlack Botを開発するのは、体験が良い面もあれば微妙な面もあった。 良い面👍: ポートフォワーディング機能で簡単にHTTPSのURLを公開できるので、SlackのWebHookやEvents APIの指定がngrokとか用意しなくていいのめちゃ楽。 良い面👍: 手元のPCのVSCodeからも、iPadのブラウザ上からも同じ環境を見れるので出先でちょこっと直しやすかった。 微妙な面🤔: ポートフォワーディングのタイムラグが体感1分ぐらいあるので、再起動時とかはSlack Bot側の反応が全くなくなってどこまでデータが届いているのか分からずにちょくちょく集中力が切れる。 微妙な面🤔: Denoのライブラリキャッシュ取得後の反映に微妙にラグがあって何度かファイルの読み込み直しやVSCode/ブラウザの再起動が必要だった。 今回公式ドキュメントから仕入れた知識のみで開発することで、開発自体は問題なく行えるものの所々辛い部分を見てきたので、次回以降は aleph.js や packup 、 Velociraptor 等のDeno周辺ツール群も組み合わせて実装を進めてみたいなと思いました🙆 当初の予定と実際のギャップ 合わせて、実際に作ってみてわかった、初期の自分の中での期待とギャップがあった部分(主に詰まった部分)もまとめていきます。 Deno Deployはimport-map機能にまだ対応していなかった。 Deno Deployで提供されていないAPIを使っているライブラリがあるとデプロイ時に???となる。 利用したライブラリの型情報が古く、 --no-check と一部 any を使ってしまった。 Deno Deployはimport-map機能にまだ対応していなかった これは先に調べておけば…。という話ではあるのですが、現時点ではDeno Deployは import-map には対応しておらず、基本的に deps.ts のパターンで実装する必要があります。 https://deno.com/deploy/docs/runtime-api#future-support ある程度、実装を進めたタイミングでデプロイしようとしてこれに気づいたため、完全に「にゃーん」となりました🐱 今更気づいた。Deno Deployやとimport_mapまだ対応されてへんやーん https://t.co/Nh2rcuXhXM — がっちゃん@えんじにゃ⛺ (@gatchan0807) 2021年12月8日 とはいえ、そこまで労力はかからないのでササッと deps.ts パターンに書き換えたのですが、再度デプロイしたところ次の問題が襲ってきました。 Deno Deployで提供されていないAPIを使っているライブラリがあるとデプロイ時に???となる ※あくまで状況証拠を並べてこれが原因っぽい。という推理のもと書いている点だけご容赦ください🙇 先程の import-map 問題を対応して再度アプリケーションをデプロイしてアクセスしてみたところ、相変わらず502を吐いてページが見れませんでした。 ログを見ると上記のようなエラーを吐いてアプリケーションの起動に失敗していました。 該当ファイルの該当行 を見にいってみたものの、エラーメッセージの reading 'deno' とは異なるデータを参照しているので違いそう…。 該当の 'deno' プロパティはファイルの中だと ${Deno.version.deno} しか使われていないけど、発生行数違うな…? と思いながら、色々調べて改めて https://deno.com/deploy/docs/runtime-api を見たところ、 It's different from Deno but aims to have similar APIs where applicable. の一文を見つけました👀 提供されている同様のAPIリストを見た感じ Deno.version の名前はない(あくまでDenoを模したランタイムなので提供されていない?)のと、上記の発生行が違う疑問は、実行時にコメントを削除されたファイルだとしたら計算が合う点から、 slack_bolt ライブラリの依存先の slack_web_api ライブラリ内の参照が問題で落ちているのだ。と自分の中で結論づけました。 今回利用している slack_bolt ライブラリは今回のアプリケーションの根幹に置いていたものだったこともあって、ある程度実装した上でこれを外して再度実装し直すのは時間がかかりすぎるし、別のライブラリでうまくいく保証は無い。と考え、 結果としてはDeno Deployを利用するのを諦める形で意思決定しました 。 利用したライブラリの型情報が古く、 --no-check と一部 any を使ってしまった 今回利用した slack_bolt には一部依存ライブラリ側での型情報の融通が出来ていない(多分一部のライブラリのバージョンアップ忘れが原因)バグがあったため、 --no-check のフラグを付けて実行し、型情報を無視するようにました( ここ 見る限りv1.17.0からは --no-check=remote が使えて外部ライブラリだけ型チェック無視になるみたいなので、Dockerfileの指定ではすでにこっそりこれを指定して使ってみてる👀) また、最終更新が10ヶ月前のライブラリだったため、現在のレスポンスには実際に返却されているデータが型情報に載っていなかったため、一度 any 化し、 as で型をつけ直すという対応を(型拡張するのをめんどくさがって)行いました。 https://github.com/gatchan0807/deno-slack-random-bot/blob/e6a655c74e25f8e765988e0f9c1f2ac1231737ad/src/app.ts#L21-L24 この辺りは次世代化が発表されたこのタイミングというのもあって、「既存の非公式ライブラリをメンテナンスするよりもSlack公式の次世代開発プラットフォームが全体に公開されるのを待つほうがコスパが良い」という思いになりPR作成は見送りましたが、本来であれば積極的にPR出して行きたいものですね👀 ざっくりとした実装の流れ 最後に、Slack Botが実際に動くようになるまでに行った対応の流れを紹介しておきます。 実際に動いた!の図 Slack Boltの公式チュートリアルを参考に、Slack Appを作成開始する slack.dev 基本的には各ステップのインポート部分の書き方を読み替えてステップを進めていく形で問題ないです🙆 Slackのワークスペースに関しては、権限があれば本番のワークスペースでやっても問題ないです。 ただ、自分はいきなり本番のワークスペースで試すのが怖いのと、管理者画面にどんな設定項目があるのかわからないと困ることがたまにあるので個人のワークスペースを遊び場として作成して、そこで実行できるかチェックしたりしています。 Codespaces環境内に環境変数を指定する 上記の公式チュートリアルの中で、環境変数にSlackのトークンを設定する作業がありますが、exportするだけではせっかく設定した環境変数がCodespacesが再起動されるたびに吹っ飛んでしまうので、提供されているCodespaces secretsの機能を使って環境変数を設定しましょう。 Codespaces の暗号化されたシークレットを管理する - GitHub Docs 下記のように設定できていればOKです🙆 そして、設定した環境変数を適用するにはCodespaceの再起動が必要なので、下記の通りVSCodeコマンドパレットを使って停止→起動を行いましょう。 Using the Visual Studio Code Command Palette in Codespaces - GitHub Docs 再起動後、 $ export | grep SLACK_BOT_TOKEN / $ export | grep SLACK_SIGNING_SECRET を叩いてきちんと環境変数に設定されていることを確認しましょう。 その後、指定のファイルを deno run してみて、問題なく起動できればOKです🙆 Events APIを使ってメンション&サブコマンドの命令を受けられるようにする 上記のチュートリアルを通して、Codespaces上で起動している環境に「リクエストを受け取る→Slackにレスポンスを返す」ことが確認できたので、 app.message メソッドを使って、メンションとサブコマンドの認識ができるようにしていきます。 細かい説明は省いてしまいますが、 app.message は第一引数に正規表現を設定することで正規表現に当てはまるメッセージを検出した場合に、第二引数の関数を呼び出される仕組みになっています。(いわばRouterやEventListnerのようなもの) そして、その関数の引数には各種リクエスト時に受け取ったtimestampやテキストの内容などと、レスポンス用の say 関数が詰まったオブジェクトが渡ってくるので、それらを利用してメンションから受け取ったデータの保存やメンションへの返事などを実装する形になります。 今回利用しているDeno用Bolt SDKの更新が10ヶ月前で止まっているためか、実際に返却されるオブジェクトと event に設定されている型情報が一致していなかったため、一度雑にany化してしまうというTypeScript的には禁忌なことをやってしまっていますが、本来は型情報を拡張して適用するようにしましょう🙏(時間があれば対応します…) import { SubCommandPattern } from "./subcommands.ts"; /** ~~ 省略 ~~ **/ // グループの入れ物の作成コマンド app.message(SubCommandPattern.create, async ({ event, say }) => { const _anyEvent = event as any; const text = _anyEvent.text as string; const user = _anyEvent.user as string; const [_botName, _subcommand, groupName] = text.split(" "); console.log("[INFO] Create: ", _anyEvent.text); await say(`<@${user}> 【 ${groupName} 】グループの作成が完了しました🎉`); }); /** ~~ 省略 ~~ **/ この対応の全貌は↓のコミットに https://github.com/gatchan0807/deno-slack-random-bot/commit/7e27039539013946be6663244ea4dd2762fe5b8d データストアを構築する ここまででSlack Botがコマンドを受け取って返信できるのを確認できたので、続いて作りたいコマンドを実現するために必要なデータの置き場所をつくります。 今回は本番では Deno Deploy Cloud Runを使う関係上、データストアを外に持つ必要があり、個人開発で使った経験があるFirebase(Firestore)を利用して行こうと思います。 Deno Deployの公式ドキュメントに似たような処理の書き方のチュートリアルがあるのですが、FirebaseのSDKバージョンがv8の頃の記法のまま(namespace形式)なので、基本的にはFirebaseの公式ドキュメントを参考にしながら、データ永続化処理を実装していきます。 firebase.google.com この辺り を参考にしながら、Firestoreの環境を構築し、Firebase Configを取得して、Slackのトークンと同じように環境変数に設定しちゃいましょう。 ※ FIREBASE_CONFIG は読み込み時に JSON.parse(Deno.env.get("FIREBASE_CONFIG")) のような形でparseされるので、JSONの記法に合わせてプロパティ名をダブルクォーテーションで囲むのと、改行の削除を行ったうえで環境変数に設定しています。 ライブラリを deps.ts に追加する export { App } from "https://deno.land/x/slack_bolt@1.0.0/mod.ts"; export { initializeApp } from "https://cdn.skypack.dev/@firebase/app@v0.7.10?dts"; export * from "https://cdn.skypack.dev/@firebase/firestore@v3.4.0?dts"; Skypackを利用する場合、 ?dts を設定しないと返ってくるライブラリの型情報がなくなってしまうので、忘れずに設定しましょう。(流石にFirebaseのSDKで型情報がないのは辛い) データ永続化の処理を実装する /** ~~ 省略 ~~ **/ console.log("[INFO] Execute add command:", _anyEvent.text); const docRef = doc(db, "groups", groupName); const docSnap = await getDoc(docRef); if (!docSnap.exists()) { console.info(`[INFO] The specified group name does not found.`); await say(`<@${user}> 【${groupName}】グループは登録リストに見つかりませんでした!`); return; } const groupRef = doc(collection(db, "groups"), groupName); const usersRef = collection(groupRef, "users"); await addDoc(usersRef, { userName: targetUserName, timestamp: timestamp, }); await say( `<@${user}> "${groupName}"グループに【${targetUserName}】を追加しました! Welcome.👏👏👏`, ); /** ~~ 省略 ~~ **/ この対応の全貌は↓のコミットに https://github.com/gatchan0807/deno-slack-random-bot/commit/8281e1a8b1cda83f3828fdc4347001c8c0d8495d ランダム処理を実装する 初期時点では、ランダム処理は雑にコピってきた arr.sort(() => Math.random() - 0.5); を使って実装していましたが、下記の記事で触れられている通り、ランダムといいつつある程度の偏りが発生してしまいます。 devblog.thebase.in そのため、記事に習ってフィッシャーイェーツのアルゴリズムを拝借して実装し直しました🙆 一通り実装できたら、デプロイする Deno Deployにデプロイするプランは残念ながら頓挫してしまったので、代わりにDockerに詰め込んでしまって、GCP上のCloud Runにデプロイするプランに切り替えました。 実際に処理を詰め込んだDockerfileは↓です https://github.com/gatchan0807/deno-slack-random-bot/blob/main/Dockerfile Cloud Runについての詳細な説明は こちらの紹介ページ に譲りますが、今回のユースケースで利用量であればデイリーの無料枠で事足りるレベルなので、Firestoreの無料枠とも相まって基本無料でSlack Botを作成できる見込みです。 - 200 万リクエスト(1 か月あたり) - 360,000 GB 秒のメモリ、180,000 vCPU 秒のコンピューティング時間 - 1 GB の北米からの下り(外向き)ネットワーク(1 か月あたり) cloud.google.com GitHubからの自動デプロイ設定はGUIからポチポチと タイトルのとおりですが、Dockerfileをリポジトリに置いた上で、Cloud Runの設定(もしくはCloud Buildの設定)でGitHubにCloud Build Appのインストールを行えばほぼ自動でGitHubへのPushをトリガーにした自動デプロイ環境を用意できます。 cloud.google.com Cloud Runの環境変数設定を忘れずに 実行環境の環境変数設定もGUIから実施できるので、設定を忘れずに。 忘れてしまうと 指定のポートでアプリケーション起動してないよ 的なエラーが発生してデプロイ失敗してしまいます🙏 cloud.google.com デプロイが完了したら、再度Slack Botの設定画面に行ってRequest URLをCloud Runのサービスに紐付いたURLに書き換えてしまいましょう🙆 Verifiedになれば、 @random-bot くんのバックエンドがCloud Runの本番環境に切り替わり、いつでもBotを利用できるようになっているはずです! これにてSlack Bot完成! まとめ もし前回のDeno基礎知識 + 環境構築編からここまでお付き合い頂いた方がいらっしゃれば、長々とお読みいただいて本当にありがとうございました!! Denoに対しての興味から調査を開始し、Slack Botの作成まで行なって記事にしてきましたが、久しぶりに知らないエコシステムをガッツリ触るということをやったので、とても楽しかったです🙆 全体的な感想としては、2年たったとはいえまだまだDenoも発展途上なんだな!という印象を受け、これから色々な進化を見せてくれるのを追うのが楽しみになりました👀 先にも上げたaleph.jsやpackupなどもこれから触りつつ、Node.jsエコシステムに比べてまだまだ参入余地が沢山あるDenoのコミュニティに機会があれば貢献していきたいなと思いました! 皆さまもぜひ、言語はJavaScript/TypeScriptで馴染みがあるけど、エコシステムは新しいが故にどことなく不思議な感覚になるDenoの世界に飛び込んでいただければと思います! 明日のアドベントカレンダーはBASE BANKのアプリケーションエンジニアの @glassmonekey さんの記事です!お楽しみに!
BASEアドベントカレンダー2021 11日目の記事です。 BASEアドベントカレンダー2021 DataStrategyチームの齋藤( @pigooosuke )です。 DataStrategyチームでは機械学習のモデルや集計結果をAPI経由で配信することが多く、社内の他チームと連携する際にも、どういうリクエストパラメータが存在し、どういうレスポンスが期待されるのかを共有しています。 弊チームでは、AWSのAPI Gatewayを利用しているので、API Gatewayの機能として提供されているドキュメントパーツを利用して共通フォーマットのAPI仕様書を管理しています。 markdown記法によるドキュメント管理に比べ、各APIのリクエストパラメーターやレスポンスモデルの視認性の改善や更新漏れが減るようになりました。 今回は、どのようにAPI仕様書を管理しているのかを紹介したいと思います。 *現在利用している Redoc のデモ画像 Redocのデモ画像 API Gateway ドキュメントパーツ API Gatewayでは各APIリソースに紐づくレスポンスモデル、クエリパラメータ、リクエストヘッダーなどに対して、ドキュメントを設定することができます。かつ、設定されたドキュメントを全て結合して、OpenAPI 3もしくはSwagger形式のjson/yamlを出力することが出来ます。 AWS デベロッパーガイド: API Gateway での API ドキュメントの表現 例えば、 /ysaito/recommend という「おすすめコンテンツを配信するAPI(GET method)」を提供していると仮定して、 このAPIでは、パラメータとして ユーザーID と 最大取得件数 を指定できるとします。 その場合、 /ysaito/recommend に対するドキュメント /ysaito/recommend に含まれるクエリパラメータ ユーザーID(user_id) に対するドキュメント /ysaito/recommend に含まれるクエリパラメータ 最大取得件数(max_size) に対するドキュメント をそれぞれ個別に管理することが出来ます。 今回のケースでは、コンソール画面上では以下のように登録されます。 合わせて、APIレスポンスに関しても、 API Gatewayのモデル管理でモデル登録&APIに紐付けしておくことで出力ファイルにレスポンスモデルを含めることが可能です。もちろん200以外のステータスコードの登録も可能です。 またこのドキュメントのリリースバージョンは、API Gateway本体のデプロイと同様にバージョン管理されているので、必要に応じて、過去のリリースバージョンにロールバックなども容易に可能になっています。 概略 全体のフローとして以下の図の通りです。 API GatewayにAPI定義をTerraformを利用して登録する ビルド用のrepositoryがAPI GatewayからOpenAPI/swagger仕様のAPI定義ファイルを取得する RedocというOpenAPI/swaggerファイルをHTMLファイルに変換するツールでビルドする S3にアップロードし、ドキュメント公開用サーバー上で公開する 処理詳細 API GatewayにAPI定義をTerraformを利用して登録する Document PartsはTerraformで管理することができるため、APIを作成する場合に合わせて登録するようにしています。 Terraform: Resource: aws_api_gateway_documentation_part ビルド用のrepositoryがAPI GatewayからOpenAPI/swagger仕様のAPI定義ファイルを取得する ビルド用のRepositoryのGitHub Actionsでは、AWS CLIにてAPI Gatewayからのドキュメント取得を以下のコマンドで取得しています。 aws apigateway get-export --parameters extensions='apigateway' --rest-api-id `<API ID>` --stage-name prod --export-type oas30 --accepts application/yaml `<出力先path>` RedocというOpenAPI/swaggerファイルをHTMLファイルに変換するツールでビルドする OpenAPI/swaggerファイルをHTML変換するツールはいくつか公開されているのですが、 視認性の観点で Redoc を採用しています。 おわりに 今回は、API仕様書の管理について紹介してみました。 ドキュメントの管理は、「昔の情報から更新されてない」「誰が管理するの?」といった長年の課題ではありますが、 API Gatewayの開発と連動して、自動でドキュメント発行できる仕組みを運用することで負荷軽減に繋がりました。 明日のアドベントカレンダーはフロントエンドエンジニアのがっちゃん( @gatchan0807 )の記事です。お楽しみに!
BASE BANKでエンジニアをしている @budougumi0617 です。 この記事はBASE Advent Calendar 2021 10日目の記事…ではなく、New Relic Advent Calendar 2021 10日目の記事です。 qiita.com TL;DR ソフトウェア開発チームのパフォーマンスを示す 4 つの指標がある https://www.devops-research.com/quickcheck.html デプロイの頻度 変更のリードタイム 変更障害率 サービス復元時間 New Relic Oneを使って「デプロイの頻度」を計測してみた Event APIとNRQLでデプロイ回数をNew Relic Oneのダッシュボードに図示できる https://docs.newrelic.com/docs/data-apis/ingest-apis/introduction-event-api ダッシュボード 自分たちチームのパフォーマンスはどれくらいなのか? 2009年に「10 deploys per day」というタイトルと共にDevOpsというキーワードが産まれて 1 から10年以上が経ちました。 10+ Deploys Per Day: Dev and Ops Cooperation at Flickr from John Allspaw www.slideshare.net 市場へサービスのリリースを繰り返し、より短時間で多くのフィードバックループを回すことがソフトウェア開発チームの生産性向上の鍵なようです。 この事実を具体的な指標にしたのが DevOps Research and Assessment(DORA)チーム の研究から導き出されたソフトウェア開発チームのパフォーマンスを示す 4 つの指標です( Google Cloudブログより引用 )。 デプロイの頻度 - 組織による正常な本番環境へのリリースの頻度 変更のリードタイム - commit から本番環境稼働までの所要時間 変更障害率 - デプロイが原因で本番環境で障害が発生する割合(%) サービス復元時間 - 組織が本番環境での障害から回復するのにかかる時間 LeanとDevOpsの科学[Accelerate] や クイックチェックツール では具体的なパフォーマンスレベルも示されています。 performance ( 表はさきほど同様 Google Cloudブログより引用 ) しかし、「 ではあなたのチームはどのレベルなの? 」と聞かれると「多分先週は毎日デプロイしてたはず…」なんて曖昧な回答しかできない状態でした。すでに他社ではこの質問に答えるためのアプローチ方法がいろいろ取られています。 生産性を可視化したい! / SUZURI's four keys https://speakerdeck.com/udzura/suzuris-four-keys-ce85acb4-bb98-4b6c-b6d8-0e62b5253872 普段「推測するな、計測せよ」「可観測性!!Observability!!O11y!!」なんて言っているのにこれではいけない…ということでチームの各指標をちゃんと計測しようと思いました。本記事ではまずは手始めにデプロイの頻度を計測した方法の紹介です。 デプロイ頻度を集計したい デプロイ頻度を計測するには様々なツールがあると思いますが、我々はNew Relicを使ってデプロイ頻度を集計することにしました。 New Relicを使ってデプロイ頻度を集計する理由は次のとおりです。 New Relicで集計している他のメトリクス情報と組み合わせて別の情報を引き出せる可能性がある 新たなツールを導入するよりも日々使うツールは少ないほうがよい 同様に確認が必要なダッシュボードは少ないほうがよい NRQLの便利さとグラフの作りやすさ サービスをデプロイしたという事実を集計する New Relicには Deployment Marker という他のパフォーマンス情報とアプリケーションのデプロイを関連付ける機能があります。 docs.newrelic.com qiita.com Deployment Marker を利用して Deployment イベントを記録すると、APMのグラフにデプロイタイミングを示す線がプロットされます。 New Relic One上でデプロイタイミングを記録しておくことでデプロイがメトリクスにどのような影響を及ぼしたのか解析できます。 Deploymentが一緒に描画されたメトリクス 最初はこの機能を使って記録した Deployment イベントをNRQLで集計すればよいと考えていました。 しかし、当時(そして2021年12月現在も、) Deployment はNRQLによって集計できるイベントはありませんでした。 デプロイ頻度を可視化するには集計してグラフ化する必要があります。そこで我々はデプロイのたびに独自のカスタムイベントを発行することにしました。 Event APIを利用した独自イベントの発行 New RelicのNRQLで集計できる type (NRQLの FROM に指定できるデータ)は Log や Transaction など多岐に渡ります。 そしてNRQLはユーザーが独自に定義した type のデータを集計することも可能です。独自定義のイベント( type )を作れるのがEvent APIです。 docs.newrelic.com Event APIを実行するために必要なもの Event APIを利用するために必要な情報は次の2点です。 License Key Account ID New Relic Oneを利用している場合、Event APIはLicense Keyを確認するだけで実行できます( 以前はInsert Keyが必要だったはずなのですが、2021年12月現在は非推奨 )。 docs.newrelic.com Licence Key自体は次のURLをクリックすると遷移できる「API Keys」の中から確認することができます。 同ページを見れば自分たち組織のアカウントIDも確認できるはずです。 one.newrelic.com Event APIの実行方法 Event APIの基本的な利用方法は先ほど記述したLicense Keyを利用して任意のJSONを curl コマンドで POST するだけです。 docs.newrelic.com 細かい仕様はありますが、任意のキー/バリューを含めることで記録する情報を自由に付与することができます。 今回は次のようなJSONフォーマットで独自イベントを発行することにします。 [ { " eventType ":" Deploy ", " deploy_user ":" ${DEPLOY_USER} ", " service ":" ${SEPLOY_SERVICE_NAME} " } ] たとえひとつのイベントだけでも配列で定義しないとうまくいきません。 時刻情報はAPI実行時の timestamp が自動的に含まれるので自前の定義は不要です。 「誰がデプロイしたのか?」も情報として含めていますが、これを集計すると謎の力学が発生してしまうのであくまで参考値として記録しておくだけにしておきます。 CircleCI上でEvent APIを実行する 上記のJSON定義をNew Relicのエンドポイントに送信すればイベントの記録が始まります。 我々のチームはCircleCIを使ってサービスをデプロイしているので、CircleCIのデプロイ用ワークフローの最後のステップにEvent APIを実行するようにします。 各サービスの .circleci/config.yml の中に次のjobを定義しました。 record-deployment : executor : name : aws-cli/default # curlコマンドが使えれば何でもよい steps : - setenv_prd # 環境変数のセットアップ - run : name : Send Deploy event to New Relic command : | curl -i \ -X POST 'https://insights-collector.newrelic.com/v1/accounts/${NEW_RELIC_ACOUNT_ID}/events' \ -H "X-Insert-Key:${NEW_RELIC_LICENSE_KEY}" \ -H "Content-Type: application/json" \ -d \ "[ { \" eventType \" : \" Deploy \" , \" deploy_user \" : \" ${CIRCLE_USERNAME} \" , \" service \" : \" ${CIRCLE_PROJECT_REPONAME} \" } \ ]" NEW_RELIC_ACOUNT_ID と NEW_RELIC_LICENSE_KEY という環境変数を設定する必要があります。 CIRCLE_USERNAME 、 CIRCLE_PROJECT_REPONAME はCircleCI実行時に自動的に設定される値です。 circleci.com あとはこのjobをデプロイフローの最後に設定しておきます。 デプロイフローの実行完了(デプロイ成功)のタイミングで Deploy というイベントが記録されます。 test-build-deploy : jobs : - test : filters : branches : only : main - build-push : requires : - test # .... - deploy-prd : requires : - approve-deploy-prd - record-deployment : requires : - deploy-prd Event APIを利用した独自イベントの集計 データが取得できるようになったならば後は集計してダッシュボードに掲載します。 New Relic Oneを開いたらQuery Builderに移動して Deploy イベントを集計していきます。 良いグラフができたら右下の「Add to Dashboard」ボタンを使ってダッシュボードにグラフを追加していきます。 参考までに我々が作成したグラフとNRQLのクエリを紹介します。 ダッシュボード 月間デイリーデプロイ数 SELECT count (*) FROM Deploy TIMESERIES 1 day FACET service SINCE 30 days ago 月間デイリーデプロイ数 まずは過去30日の日毎のデプロイ数です。サービス別にカウントしています。 NRQLは独自イベントに付与している独自属性を使って FACET を書くこともできます。 今週のデプロイ総数(先週比較) SELECT count (*) FROM Deploy SINCE this week COMPARE WITH 1 week ago ウィークリーデプロイ数 次に先週比較の数値です。 COMPARE WITH を使うと比較対象との比率まで簡単に示してくれます。 先週と比べて今週のデプロイ頻度は低いのか?気づかないうちにチームのパフォーマンスがブレてていないか?を確認したくて作りました。 週間デプロイ数 最後は直近4週間のデプロイ総数です。 SELECT count (*) FROM Deploy FACET weekOf(timestamp) UNTIL monday SINCE 5 week ago 週別デプロイ数 こちらもコンスタントにデプロイできているか?を確認するために作りました。 終わりに 本記事では「LeanとDevOpsの科学」などで示されているソフトウェア開発チームのパフォーマンスを示す4 つの指標のうち、デプロイ頻度をNew Relicを使って確認する方法を紹介しました。いくつか実際に利用している集計クエリも紹介しました。「こんな見方もできるのは?」「私たちのチームはこういう点をみている」とコメントある方は教えていただけると幸いです。 ソフトウェア開発チームのパフォーマンスの4つの指標ではデプロイ頻度の他に3つのキーが定義されています(再度 Google Cloudブログより引用 )。 デプロイの頻度 - 組織による正常な本番環境へのリリースの頻度 変更のリードタイム - commit から本番環境稼働までの所要時間 変更障害率 - デプロイが原因で本番環境で障害が発生する割合(%) サービス復元時間 - 組織が本番環境での障害から回復するのにかかる時間 デプロイ頻度以外については現状まだ計測できていません。 その他のキーについても可視化した際は本開発者ブログで共有させていただきます。 また、New Relic Oneでは最近サービスレベル目標(SLO)を設定、サービスレベル指標(SLI)、Error budgetを管理できるService Level Management機能もPublic betaになりました。 来年は4キーメトリクスの他に、SLIについても可視化していければなと思います。 docs.newrelic.com そして BASE Advent Calendar 2021 10日目の記事も書いたのでよかったら読んでみてください。 devblog.thebase.in 参考 LeanとDevOpsの科学[Accelerate] DevOpsの起源とOpsを巡る対立 エリート DevOps チームであることを Four Keys プロジェクトで確認する | Google Cloud Blog State of DevOps Report 2021を日本語で解説 https://puppet.com/resources/report/2021-state-of-devops-report https://twitter.com/t_wada/status/1420652243330605060 生産性を可視化したい! / SUZURI's four keys NewsPicksにCTOとして入社して1年でDX Criteriaを大幅改善した話 https://ubiteku.oinker.me/2015/07/01/devops%E3%81%AE%E8%B5%B7%E6%BA%90%E3%81%A8ops%E3%82%92%E5%B7%A1%E3%82%8B%E5%AF%BE%E7%AB%8B/ ↩
BASEアドベントカレンダー2021 10日目の記事です。 BASEアドベントカレンダー2021 10日目 BASE BANKでエンジニアをしている @budougumi0617 です。 マイグレーションファイルが含まれたPull Request(PR)が作られたとき、自動更新したER図をPRに追加するGitHub Actionsを作りました。 本記事では紹介するGitHub Actionsを利用すると次のようなメリットが得られます。 マイグレーションファイルをPRに出すだけでPRに更新されたER図が追加される 開発者は面倒なER図の更新作業から開放される レビューアはマイグレーションファイルを含んだPRをER図を見ながらレビューできるようになる プロジェクト関係者は常にメインブランチのマイグレーションファイルの状態と一致したER図を確認できる サンプルPR 自動生成したER図 TL;DR ER図はあれば便利だけれど運用しているとメンテが大変 k1LoW/tbls をGitHub Actionsで動かすとマイグレーションファイルのPull Request(PR)に自動更新したER図を追加できる RDBMSのスキーマをマイグレーションツールで管理しているのが前提条件 マイグレーションファイルの追加PRでマイグレーション後のER図を確認しながらレビューできる マイグレーションファイルを書くだけでER図をメンテする必要がなくなる サンプルリポジトリはこちらです。 github.com GitHub ActionsのYAMLを見たい方はこちらを参照してください。 https://github.com/budougumi0617/sample_tbls_actions/blob/v0.0.1/.github/workflows/tbls.yml k1LoW/tbls で自動生成しているER図はこのディレクトリにあります。 https://github.com/budougumi0617/sample_tbls_actions/tree/v0.0.1/schema/dbdoc マイグレーションファイルを含んだPRに対してGitHub ActionsでER図を更新しているPRのサンプルはこちらです。 https://github.com/budougumi0617/sample_tbls_actions/pull/2 ER図の必要性 Webアプリケーションを開発・運用しているならば、大抵の場合RDBMSを利用していると思います。 私のチームではRDBMSのテーブル定義を rubenv/sql-migrate によるマイグレーションによって管理しています。 github.com マイグレーションによるテーブル管理を行なっているとき問題になるのがER図の管理です。 マイグレーションファイルとER図を両方利用しているとどうやってER図を最新の状態と一致させるのかが運用上の課題になります。新規開発サービスだったり機能追加が活発なサービスの背後にあるRDBMSは毎週のようにマイグレーションが実施されます。このようなRDBMSの状態を手動でER図に反映し続けるのはかなりの労力が必要です。 また、マイグレーションファイルのレビューをするときはDDLとして正しいかだけではなく、DBの設計として妥当であるかもレビューする必要があります。そのためには他のカラム、他のテーブルを含めてマイグレーション後の姿を俯瞰的に確認する必要があります。 ER図を運用する時の課題 前述したとおりER図はあったほうが良いですがER図を作り維持し続けるには様々な課題があります。 本番環境のテーブルの状態とER図がかならずマッチしている保証がない マイグレーションファイルが真か?ER図が真か? ER図が古いだけなのか?それともマイグレーションが意図通り行われなかったのか? マイグレーションファイルを書くのが先か、ER図を書くのが先か 俯瞰的にレビューするならばER図がほしい オレは ADD COLUMN したいだけなんだ… マイグレーションをする側からすると少しDDLを書くだけで済むはずなのにマイグレーション結果に影響しないER図の修正まで気を使うのは大変… 単純なカラム追加だけならばよいですが、外部キー制約付きのテーブル追加のようなDDLを書く場合ER図の更新は非常に煩わしいものでした。 k1LoW/tblsという救世主 メンテフリーなER図を実現するための救世主が github.com/k1LoW/tbls です。 github.com qiita.com tbls のいいところそれだけで2記事くらい書けるので省略しますが、シンプルに設定した接続先のDBのスキーマ情報を(デフォルトでは)MarkdownとSVGファイルに出力してくれます。 自動生成したER図 CI-Friendly とREADMEにかかれている通りシングルバイナリで svg ファイルまで生成してくれる点もとてもありがたいです。これ系のツールでありがちな「まずは Graphviz をインストールします」ということもないのでCI上でも簡単に実行できます。 しかし、 tbls は「データベースに接続してER図を自動生成するツール」です。 つまりマイグレーションを当てた状態のデータベースを用意しないと tbls を使うことができません。 マイグレーションの妥当性を確認する際もER図を見たいのに、マイグレーションしないとER図が生成できません。 そこでマイグレーションファイルを含んだPRが作られるたびにActions上でRDBMSを起動するようにActionsを書きました。 マイグレーションを含んだPRでER図を自動更新するGitHub Actions 今回作成したGitHub Actionsの全容は以下のとおりです。 https://github.com/budougumi0617/sample_tbls_actions/blob/v0.0.1/.github/workflows/tbls.yml name : update er graph on : pull_request : paths : - schema/sample/** - schema/tbls.yml jobs : tbls : name : generate-and-push runs-on : ubuntu-latest services : mysql : image : mysql:5.7 options : --health-cmd "mysqladmin ping -h localhost" --health-interval 20s --health-timeout 10s --health-retries 10 ports : - 3306:3306 env : MYSQL_ALLOW_EMPTY_PASSWORD : yes MYSQL_DATABASE : sample MYSQL_USER : sample MYSQL_PASSWORD : sample steps : - name : Checkout uses : actions/checkout@v2 with : ref : ${{ github.event.pull_request.head.ref }} token : ${{ secrets.PERSONAL_ACCESS_TOKEN }} - name : Setup go uses : actions/setup-go@v2 with : go-version : '^1.17.1' - name : Execute migration run : | go get -u github.com/rubenv/sql-migrate/sql-migrate make up ENV=ci working-directory : ./schema - name : Execute tbls run : | curl -sL https://git.io/use-tbls > use-tbls.tmp && . ./use-tbls.tmp && rm ./use-tbls.tmp tbls doc -f working-directory : ./schema # tbls実行後、差分有りもしくは新規ファイルの数をカウントする - name : Count uncommit files id : check_diff run : | git status --porcelain | wc -l file_count=$(git status --porcelain | wc -l) echo "::set-output name=file_count::$file_count" working-directory : ./schema - name : Commit ER graph # 更新したER図をPRにコミットする if : ${{ steps.check_diff.outputs.file_count != '0' }} run : | git config user.name github-actions git config user.email github-actions@github.com git add . git commit -m "generate er graphs from actions" git push working-directory : ./schema # PRへ自動コミットしたらPRにコメントしておく - name : Report commit on pull request if : ${{ steps.check_diff.outputs.file_count != '0' }} uses : actions/github-script@v4 with : script : | github.issues.createComment({ issue_number : context.issue.number, owner : context.repo.owner, repo : context.repo.repo, body : 'Actions committed new ER files🤖' }) Actionsの主な流れは次のとおりです。 PRにマイグレーションファイルの変更が含まれていた場合実行する 事前にMySQLを起動しておく マイグレーションファイルを実行する tbls コマンドを実行してER図を生成する もし既存のER図と差分があったら、PRに対してその差分をコミットする PRにマイグレーションファイルを更新した旨をコメントする ひとつひとつ該当するYAMLの抜粋と一緒に解説していきます。 PRにマイグレーションファイルの変更が含まれていた場合実行する GitHub Actionsは特定のディレクトリに更新があったときのみ起動する設定が可能です。 私たちのリポジトリはアプリケーションコードと一緒にマイグレーションファイルがコミットされていますが、この設定によりアプリケーションコードを変更するだけのPRで意味もなくCIが実行されることを防ぎます。 on : pull_request : paths : - schema/sample/** - schema/tbls.yml 事前にMySQLを起動しておく GitHub Actionsはワークフロー中にサービスコンテナという名前でDBなどのDockerコンテナを起動しておく事ができます。 docs.github.com 今回は次のような宣言でMySQLを起動しておきます。 mysql : image : mysql:5.7 options : --health-cmd "mysqladmin ping -h localhost" --health-interval 20s --health-timeout 10s --health-retries 10 ports : - 3306:3306 env : MYSQL_ALLOW_EMPTY_PASSWORD : yes MYSQL_DATABASE : sample MYSQL_USER : sample MYSQL_PASSWORD : sample コードをクローンしてGitHubの設定をする コードをクローンしてGitの設定も行ないます。ここで設定したGitの設定は後半の更新したER図のコミット・pushにも利用されます。 - name : Checkout uses : actions/checkout@v2 with : ref : ${{ github.event.pull_request.head.ref }} token : ${{ secrets.PERSONAL_ACCESS_TOKEN }} マイグレーションファイルとアプリケーションコードを同じリポジトリに入れている場合、Actionsで利用するトークンはデフォルトのトークンではなく、払い出したトークンを利用したほうがよいです。 これはデフォルトのトークンでPRに対してコミットをpushすると、そのコミットに対しては(無限ループ事故の防止のため)Actionsが起動しないためです。 docs.github.com 本記事で紹介しているActionsは起動しなくても問題ありません。しかし、アプリケーションコードとマイグレーションファイルを一緒にPRで変更していた場合ActionsからのpushでActionsが起動しないことになります。 ただ起動しないだけならばいいのですが、 CIのチェックステータスも消えるため、他のActionsでfailしていた状態も消えます 。 PRのBranch Protection RuleでCIのステータスを利用しているならば必ずトークンを払い出して再度Actionsが実行されるようにしておきます。 マイグレーションファイルを実行する 先ほど示したMySQLに対してマイグレーションを実行しておきます。 これでER図を生成したいDBがGitHub Actions上に構築されます。 - name : Checkout uses : actions/checkout@v2 with : ref : ${{ github.event.pull_request.head.ref }} token : ${{ secrets.PERSONAL_ACCESS_TOKEN }} - name : Setup go uses : actions/setup-go@v2 with : go-version : '^1.17.1' - name : Execute migration run : | go get -u github.com/rubenv/sql-migrate/sql-migrate make up ENV=ci working-directory : ./schema make up ENV=ci コマンドは次のMakefileの設定と、 dbconfig.yml で成り立っています。 https://github.com/budougumi0617/sample_tbls_actions/blob/v0.0.1/schema/Makefile ENV := "local" up: ## Apply migration files sql-migrate up -env= $(ENV) -config= dbconfig.yml https://github.com/budougumi0617/sample_tbls_actions/blob/v0.0.1/schema/dbconfig.yml ci : dialect : mysql datasource : root@tcp(127.0.0.1:3306)/sample?parseTime= true dir : sample tbls コマンドを実行してER図を生成する tbls をインストールして実行します。CIフレンドリー過ぎてやることはこれだけです。あっけないですね… - name : Execute tbls run : | curl -sL https://git.io/use-tbls > use-tbls.tmp && . ./use-tbls.tmp && rm ./use-tbls.tmp tbls doc -f working-directory : ./schema tblsの設定自体も接続情報と sql-migrate が利用しているマイグレーション管理用のテーブルを除外するようにしているくらいです。 https://github.com/budougumi0617/sample_tbls_actions/blob/v0.0.1/schema/tbls.yml dsn : mysql://root@127.0.0.1:3306/sample?parseTime= true er : comment : true distance : 9 exclude : - gorp_migrations もし既存のER図と差分があったら、PRに対してその差分をコミットする 先ほど実行した tbls によってER図のファイルに何らかの変化があった場合、gitのステータスに差分が出ます。 一つでも差分を見つけたらコミットを作り、PRのブランチに対してpushします。 このようにしておくことで空コミットが作られたり、Actionsが無限にループ実行することありません。 - name : Count uncommit files id : check_diff run : | git status --porcelain | wc -l file_count=$(git status --porcelain | wc -l) echo "::set-output name=file_count::$file_count" working-directory : ./schema - name : Commit ER graph # 更新したER図をPRにコミットする if : ${{ steps.check_diff.outputs.file_count != '0' }} run : | git config user.name github-actions git config user.email github-actions@github.com git add . git commit -m "generate er graphs from actions" git push working-directory : ./schema PRにマイグレーションファイルを更新した旨をコメントする 最後にPRにER図を更新した旨を書くコメントを残して終わります。 - name : Report commit on pull request if : ${{ steps.check_diff.outputs.file_count != '0' }} uses : actions/github-script@v4 with : script : | github.issues.createComment({ issue_number : context.issue.number, owner : context.repo.owner, repo : context.repo.repo, body : 'Actions committed new ER files🤖' }) あとは PERSONAL_ACCESS_TOKEN と言う名前でGitHubパーソナルアクセストークンを保存しておけば次のPRよりER図が自動生成されるようになります。 自動生成したER図が追加されたサンプルPR 上記のGitHub Actionsを使って更新されたPRが次になります。 github.com このPRではマイグレーションファイルが一つ追加されています。 PRに対してGitHub Actionsが実行された結果、更新されたER図が追加されています。 サンプルPR 開発者は何もせずマイグレーション後のER図をPRに追加できました。 レビューアは更新されたER図を確認することで、表で示されたテーブル構成や図示されたリレーションを見ながらレビューすることができます。 https://github.com/budougumi0617/sample_tbls_actions/blob/migrate-card/schema/dbdoc/cards.md このPRが妥当ならばマイグレーションファイルと更新されたER図が main ブランチにマージされます。 終わりに 以上のGitHub Actionsにより、我々の開発では以下の開発者体験が実現できました。 GitHub上でER図をいつでもテーブル定義を確認できる ER図とマイグレーション結果が一致していることを保証する 開発者はER図を書く必要がない マイグレーション後のER図を確認しながらマイグレーションファイルのPRをレビューできる 明日は @pigooosuke さんです!
BASE Advent Calendar 2021 9日目の記事です。 フリーランスのフロントエンドエンジニア 坪内です。 BASE のお手伝いをさせていただくようになって 1ヶ月が経ち、色々見えるようになってきた中で最も気になっていた点の 1つが、 「 HMR されていない 」 でした。 BASE の Web フロントエンドは webpack でビルドされているのですが、 ローカルの開発環境が Docker 上で動いている事もあってか、残念ながら HMR はされていない状況でした。 「Docker 環境だから無理?」 「“ webpack HMR docker ” とかでググると色々出てくるし出来そうな?」 さっそく取り掛かってみました。 HMR とは? 一応、HMR の説明をしておきます。 https://webpack.js.org/guides/hot-module-replacement/ H ot M odule R eplacement の略です。 js モジュールの変更を、ブラウザをリロードすること無しに動的に差し替えて反映する webpack の機能の事を指します。 歴史は割と古く、webpack 1系の頃から試験的に導入されていました。 似たものとして、ファイルの変更を検知してブラウザを自動でリロードしてくれる liveReload があります。 仕組み HMR はおおよそ以下の流れで実現されています。 webpack-dev-server が HMR 用のコードを含めて js を返す WebSocket に接続 webpack がファイルの変更を検知したら差分ビルド 変更があったことを WebScoket で通知 変更差分情報を取得 変更のあったモジュールを取得して動的に置き換え 置き換えができない場合は liveReload される 構成 一般的な SPA の場合、起動した webpack-dev-server にブラウザからアクセスし、API などへのアクセスは必要に応じてリバースプロキシするという構成を取ります。 起動した webpack-dev-server ( http://localhost:3000 など)にブラウザでアクセス webpack でビルドされた html を返却 webpack でビルドされた js を返却 webpack で管理していないリソースは backend へリバースプロキシ 当初 BASE のサービスは CakePHP を使用しているので、上記と同様に前段に webpack-dev-server を配置して CakePHP へリバースプロキシする必要があると思っていました。 また、ローカル Docker 環境は架空のドメイン(ここでは仮に *.base.test )上で動くようになっているので、webpack-dev-server へもその架空ドメインとしてアクセス出来るようにしてあげる必要があると考え、Docker 環境内に webpack-dev-server を建てました。 これはこれで動くので間違いではないのですが、 Docker上で動かすので単純に重くなる Docker上で webpack-dev-server を使うかどうかを切り替えるのが面倒そう node-gyp によるネイティブパッケージに依存していると、Docker上で yarn install する必要がある 内部のライブラリへの yarn link が厳しそう などの問題があり、色々と調整や解決が大変そうでした。 助言 そんな中、ある助言をもらいました。 アセットだけホスト側の dev-server に繋げないんだろうか ❗❗❗ リバースプロキシしなければならないと思い込んでしまっていましたが、 CDN から js や css を取得して動かせるわけなので、webpack-dev-server を CDN のように扱うこともできるのではないか? 助言後 webpack-dev-server はホスト側( http://localhost:3081/ )で起動する CakePHP で js をロードしている箇所の URL を http://localhost:3081/ に向ける 以上。 js の向き先を変えるだけで良くなったので、docker-compose には影響を及ぼさず、とてもシンプルな仕組みになりました! webpack の設定 最終的な webpack の devServer 設定は以下です。 devServer: { allowedHosts: [ '.base.test' ] , client: { overlay: true , webSocketURL: 'ws://localhost:3081/ws' , } , headers: { 'Access-Control-Allow-Origin' : '*' , } , port: 3081, proxy: { '*' : 'http://localhost:8081/' , } , static : false , watchFiles: [ 'app/**/*' ] , } , 考慮した点 CORS を解決する Docker 環境 https://*.base.test から見ると、 http://localhost:3081 はクロスオリジンなアクセスになるので、 devServer.headers として CORS ヘッダーを返す必要があります。 また、webpack-dev-server へのアクセスを許可するドメインを devServer.allowedHosts に追加する必要がありました。 devServer: { allowedHosts: [ '.base.test' ] , headers: { 'Access-Control-Allow-Origin' : '*' , } , } , HMR の WebSocket 接続先や差分取得先などを webpack-dev-server に向ける html は https://*.base.test/ から返されており、そのままだと HMR 用の WebSocket などもそちらを向いてしまうので、 devServer.client.webSocketURL で ws://localhost:3081/ws へ向けます。 devServer: { client: { webSocketURL: 'ws://localhost:3081/ws' , } , } また、 webpack-manifest-plugin を使用している関係で、 output.publicPath を指定していたのですが、 output: { publicPath: '/' , } , これが差分の取得先に使われているので、webpack-dev-server で起動している場合は localhost:3081 へ向くようにする必要がありました。 output: { publicPath: env.WEBPACK_SERVE ? 'http://localhost:3081/' : '/' , } , webpack 管理外の js への対処 webpack でビルドされていない古い js が残っており、そのままだとそれらへのアクセスが 404 になってしまうので、それ用に devServer.proxy で CakePHP へリバースプロキシしています。 devServer: { proxy: { '*' : 'http://localhost:8081/' , } , } , また、それらを含め CakePHP 側に変更があった際に liveReload されるよう、 devServer.watchFiles を設定しています。 devServer: { watchFiles: [ 'app/**/*' ] , } , CakePHP で js の向き先を変える HtmlHelper を使用して、 <? = $ this -> Html -> script ( 'foo' ) ?> のように出力していれば、 App.jsBaseUrl を設定する事で向き先を変えることができます。 <?php Configure :: write ( 'App.jsBaseUrl' , 'http://localhost:3081/js/' ) ; これをローカルでのみ有効になる Config ファイルに記述して、必要に応じて切り替えるようにしています。 おわりに Docker 環境でもシンプルな形で webpack-dev-server で HMR する事ができました! この形で HMR できるとなると、 Proxyman や Charles などの proxy と組み合わせてみたりなど色々応用もできそうですね。 明日は @budougumi0617 さんです!
Chromatic とは Chromatic とは、Storybook のメンテナーが作成している Storybook 用のツールです。Storybook をビルドして公開したり、ストーリーごとのスクリーンショットを撮影し、差分を比較してくれる機能を備えています。 Chromatic を使うことにより、UI の予期せぬ変更を事前に検知することができます。本記事では Chromatic の導入、活用方法をご紹介します。 なお、BASE 社では社内の UI コンポーネントライブラリである BBQ で Chromatic を導入、活用しています。その経緯はアドベントカレンダー15日目に公開する記事でご紹介します。 Chromatic をプロジェクトに導入する サンプルプロジェクトを作成する 今回は Storybook 公式で用意されている サンプルプロジェクト を利用します。 プロジェクト作成にあたり、以下のコマンドを実行します。 $ npx degit chromaui/intro-storybook-vue-template taskbox $ cd taskbox $ yarn これで taskbox の作成が完了しました。 次に以下のコマンドを実行して Storybook を立ち上げ、どのようなコンポーネントがあるか確認します。 $ yarn storybook Storybook の画面 Button や Header、Page といったコンポーネントが存在することが確認できました。 サンプルプロジェクトに Chromatic を導入する 次に、Chromatic にログインします。「Add project」ボタンから新規プロジェクトを追加できます。 Chromatic の管理画面 GitHub レポジトリかプロジェクト作成のどちらかを選択します。今回は右側のプロジェクト作成を選択しました。 プロジェクト作成用のボタンが二つ並んでいる すると、Chromatic 用のトークンが発行されます。 Chromatic のコマンドとトークンが表示されている 画面に表示されているコマンドを taskbox ディレクトリで実行します。 $ npx chromatic --project-token=your-token すると、以下のエラーが表示されます。 $ npx chromatic --project-token=your-token Chromatic CLI v6.1.0 https://www.chromatic.com/docs/cli ✔ Authenticated with Chromatic → Using project token '********da59' ✖ Retrieving git information → Chromatic only works from inside a Git repository. プロジェクトを git 管理下に置いていないため Publish に失敗します。このため、適当なレポジトリを作成して push しましょう。 レポジトリに push した後、再度 chromatic コマンドを実行します。 $ npx chromatic --project-token=your-token Chromatic CLI v6.1.0 https://www.chromatic.com/docs/cli ✔ Authenticated with Chromatic → Using project token '********da59' ✔ Retrieved git information → Commit 'b6eb197' on branch 'main'; no parent commits found ✔ Collected Storybook metadata → Storybook 6.4.0 for Vue3; supported addons found: Actions, Essentials, Links ✔ Storybook built in 22 seconds → View build log at /Users/panda/playground/vue/test/taskbox/build-storybook.log ✔ Publish complete in 14 seconds → View your Storybook at https://61aef69049f6bb003ac71914-qocfdyaegr.chromatic.com ✔ Started build 1 → Continue setup at https://www.chromatic.com/setup?appId=61aef69049f6bb003ac71914 ✔ Build 1 auto-accepted → Tested 9 stories across 4 components; captured 8 snapshots in 9 seconds ✔ Build passed. Welcome to Chromatic! We found 4 components with 9 stories and took 8 snapshots. ℹ Please continue setup at https://www.chromatic.com/setup?appId=61aef69049f6bb003ac71914 ログの中で表示されている https://61aef69049f6bb003ac71914-qocfdyaegr.chromatic.com/?path=/story/example-introduction--page という URL にアクセスすると、Storybook を閲覧できます(プロジェクトを削除したため、現在は表示されません)。 Chromatic のトップページに taskbox プロジェクトが追加されていることが確認できます。 Chromatic のプロジェクト一覧 taskbox プロジェクトを見る taskbox プロジェクトをクリックすると、次の画面に遷移します。 Chromatic の Build ページ メニューの内容は以下の通りです。 Builds:過去のビルド履歴を閲覧できる Library:Storybook のストーリー単位でコンポーネントを表示できる Manage:Slack 連携や URL からアクセスできる Storybook の公開範囲設定、コラボレーション相手の選択などができる メニューの中から Builds を開いてみましょう。 Builds の中を覗く Builds をクリックします。すると、以下の画面が表示されます。 コンポーネント一覧 Storybook のファイル単位ではなく、ストーリー単位でコンポーネントが区切られています。 一番上の Button をクリックします。すると、Button コンポーネントのキャプチャが撮影されていることがわかります。 ボタンコンポーネントのスクリーンショット また、右下に DOM が表示されていることがわかります。こちらは Header コンポーネントです。 ヘッダーコンポーネントのコードが表示されている 差分を追加する 次に、差分確認をします。Header コンポーネントと Page コンポーネントのコードを変更します。 変更点は以下の3つです。 Header で右上のボタンの位置を入れ替え Page のタイトルの Storybook を Chromatic に変更 Page の文章の一部を削除 変更前 変更後 変更前の Page コンポーネント Page コンポーネント変更後 この変更を加えた後、再度 Chromatic でビルドします。 $ npx chromatic --project-token=your-token すると、ビルド2 が追加されました。 Build 一覧 差分をレビューする ビルド2 を表示すると、変更があるコンポーネントがピックアップされています。 Build 2 のコンポーネント一覧 一番下の Page の「Logged Out」を表示してみます。すると、コンポーネントの Build1 と Build2 の見た目とコードの差分がハイライトされています。 単体 比較 Page コンポーネントの差分ハイライト Page コンポーネントの変更前と変更後が比較されている もし差分が意図通りであれば、右上の Accept ボタンをクリックします。そうでなければ Deny をクリックして、コードを変更した人に再度確認して貰います。 変更点を Accept するボタンが表示されている Accept をするのはコードを改修した人ではなく、別の人にレビュワーとしてクリックしてもらうのが望ましいです。BASE の BBQ の開発フローでも、コードレビューをするレビュワーが Accept や Deny をすることにしています。 全てのコンポーネントを Accept すると Build2 の表示が緑になります。 Build 2 のコンポーネントの差分が全て緑色になっている GitHub Actions で Chromatic を実行する Chromatic は GitHub Actions で実行できます。実際、 公式で yml の書き方が紹介されています。 # .github/workflows/chromatic.yml # Workflow name name : 'Chromatic' # Event for the workflow on : push # List of jobs jobs : chromatic-deployment : # Operating System runs-on : ubuntu-latest # Job steps steps : - uses : actions/checkout@v1 - name : Install dependencies run : yarn - name : Publish to Chromatic uses : chromaui/action@v1 # Chromatic GitHub Action options with : token : ${{ secrets.GITHUB_TOKEN }} projectToken : ${{ secrets.CHROMATIC_PROJECT_TOKEN }} CHROMATIC_PROJECT_TOKEN は、冒頭で取得したトークンのことです。このトークンをレポジトリの Secrets に設定すると、GitHub Actions から読み取ることが可能です。 Chromatic を CI に組み込むことで、PR から Chromatic のステータスをチェックすることができます。 GitHub の CI の結果一覧 差分がなかったり、あるいは全て Approve された場合には緑のチェックマークがつきます。差分がある場合は、黄色い丸のマークがつきます。 これで Chromatic をレビューの工程に組み込めました。 Chromatic のその他の機能 Chromatic は差分を表示する画面にコメントできたり、ビルド結果を Slack に通知したり、コメントをメール通知可能です(コメントの Slack 通知は未対応)。 また、プランをアップグレードすると、Chrome だけではなく Firefox、IE11 でのスクリーンショットを撮影可能になります。BASE では Starter プランに加入しています。 Chromatic の価格プラン一覧 また、Chromatic 上の Storybook に対して GitHub 認証を設定できるので、万が一 URL が漏れて外部の人が覗いてしまうということも防止できます。 おわりに Chromatic はセットアップや GitHub 連携がとても簡単です。無料プランもあるので、 Storybook を活用している会社・組織・チームであればぜひ活用を検討してみてはいかがでしょうか。