タイミーのブログ - TECH PLAY

TECH PLAY

タイミー

タイミー の技術ブログ

308

タイミーの新谷、江田、酒井、正徳です。 Kaigi on Rails 2023 が10月27日、28日の2日間で開催されました。タイミーは去年に引き続きKaigi on Railsのスポンサーをさせていただきました。 また、タイミーには世界中で開催されている全ての技術カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。詳しくは以下をご覧ください。 productpr.timee.co.jp この制度を使ってオフラインでは4名のエンジニアが参加しました。 参加して聞いたセッションのうち印象に残ったいくつかをピックアップしてご紹介します。 生きた Rails アプリケーションへの Delegated Types の導入 docs.google.com Rails 6.1から導入されたDelegated Typeに関する発表でした。 デプロイ履歴を管理する機能において、AWSのECSやLambdaなどのサービスごとに固有で設定したいデータと、各デプロイ共通のデータ管理をする上で単一テーブル継承(Single Table Inheritance: STI)ではなくDelegated Typeが活用されていました。 タイミーでは一部のテーブル設計でSTIが活用されています。ただ、すべてのサブクラスのすべての属性を持つ単一のテーブルが作られ、nullが避けられない等のデメリットはよくある話だと思いまして、発表の中でも触れられていました。 一方Delegated Typeではスーパークラスの共通データ、サブクラスの固有データを格納するテーブルがそれぞれ作成されます。ポリモーフィック関連をベースにした委譲による実装がなされ、個別データを持つ側のクラスからアソシエーション(今回の発表では has_one)で辿れる仕組みでRails開発者としても直感的に使える仕組みだと感じました。 また各種Gem側での対応も順調に進められており、特にFactoryBotで詰まることなくデータ生成が出来るのは開発・運用の観点でも心理的ハードルが下がる話でした。 そもそもRails 6.1からActiveRecordに導入された機能で、まだ実際に稼働中のプロダクトでの実例もあまり聞いたことが無かったので具体例と共にその特徴を知ることができ、有益な時間になりました。実際に導入する際にはまたこの発表を参考にさせていただこうと思います。 (@edy2xx) 生きた Rails アプリケーションへの delegated types の導入 by mozamimy - Kaigi on Rails 2023 やさしいActiveRecordのDB接続のしくみ speakerdeck.com MySQLへの接続確立するまでのActiveRecord内部の動きを、findメソッドをデバッグ実行した時のスタックトレースを参考に、接続に関する主要なクラスの役割を紹介しながら、内部処理を学ぶセッションでした。 ActiveRecordを用いるとdatabase.ymlに必要な情報を載せるだけでDBにアクセスしデータ取得や操作を行うことができるためあまり意識をすることがありませんでした。しかし、このセッションを通して改めてどのようにDBへの接続を確立・管理するかや、クラスごとの責務を理解できた気がしました。 後半の知見でも触れられていた事ですが、この内部処理を理解することで、接続プールはDBの接続情報を保持したインスタンスを持つため(端折っています)、フェイルオーバーなどでDBホストの変更を行っても、変更以前の接続情報を用いてしまう可能性があります。そのような可能性があることを知っておく事は運用において大事ですし、そのような事態になった時や、そうならないための打ち手を常に持っておく事も重要です。 最後に軽く触れられていたバリデーションクエリの他にも、Railsの再起動等を通して再接続を試みたり、一時的にコネクションプールをやめて都度接続にしたり、クエリ実行時にエラーが発生した場合にエラー内容によってリトライをかけるなど色々あるので改めて理解を深めようと思いました。 (@pokohide) やさしいActiveRecordのDB接続のしくみ by kubo - Kaigi on Rails 2023 Simplicity on Rails - RDB, REST and Ruby speakerdeck.com 偶発的な複雑性が少ない状態をシンプルと定義した上でテーブル設計・API設計・クラス設計の観点でどんなことを意識すべきかを紹介するセッションでした。 テーブル設計:リソースエンティティだけでなくイベントエンティティを見出してhas_many through 関連を作ること。 API設計:テーブルとリソースは似ていることもあるが同じではないので、テーブルに対応しない Controller を作ることは問題ないので気にしなくていいこと。 クラス設計:Controller の create アクションは INSERT INTO 以上のことをしても良いこと。Controller が複雑になってきたと感じたら FormObject の利用を検討すること。 事業としてはイベントエンティティがコンバージョンポイント(お金に変換できるデータという理解をしました)であるという話は確かになと思いました。タイミーのコンバージョンポイントは働くワーカーさんが求人に申し込むマッチングなのですが、ここに対応するモデルは UserOffering という User モデルと Offering モデルの多対多を管理するリソースエンティティとしての命名になっています。様々なものに紐づいているモデルなので変更を加えるのは大変ですが、長期的には手を加えていきたいと思いました。 イベントエンティティをモデルとして表現するという話は texta.fm でも耳にしていて、今回の発表を聞いて更に理解を深めたいと思ったので texta.fm での話の元になっていたパーフェクト Ruby on Rails を読んでみようと思います。 (@euglena1215) Simplicity on Rails - RDB, REST and Ruby by MOROHASHI Kyosuke - Kaigi on Rails 2023 Railsの型ファイル自動生成における課題と解決 speakerdeck.com Railsアプリに型を導入するときに起きるいくつかの課題と、それらの課題に対する解決策を紹介するセッションでした。 課題: Railsアプリに手軽に型を導入したい -> 解決: orthoses-rails 課題: アプリケーションコードの型が無い -> 解決: rb prototype 課題: YARDを活用したい -> 解決: orthoses-yard 課題: YARDのシンタックスをチェックしたい -> 解決: rubocop-yard 弊社でも型(RBS)を活用しているため、課題に共感しながら聞いていました。 Orthoses は使用していませんが、いくつかの解決策は弊社の開発環境でも活用できそうなので、Orthoses への乗り換えも含めて検討・参考にしたいと思いました。 また、発表の後に3Fの休憩部屋(ROOM 0)で今後の型に関する話をすることができて、個人的に型に対するモチベーションが上がる1日になりました。 今後は型(RBS)に関するOSSにも少しずつコントリビュートしていきたいと思います。 (@sinsoku) Railsの型ファイル自動生成における課題と解決 by Yuki Kurihara - Kaigi on Rails 2023 管理機能アーキテクチャパターンの考察と実践 speakerdeck.com 後追いで作ることになりがちな管理機能を Frontend, Backend のソースコードを置き場を元にいくつかのアーキテクチャパターンに分類しそれぞれのpros/consをまとめたのちに、B/43ではどのアーキテクチャを選択したのかと選択した理由・振り返りを共有するセッションでした。 タイミーは ActiveAdmin を用いてモノリシックコードベースに管理機能を密結合させています。ActiveAdmin を使う上で色々と大変な面はあるのですが、なんだかんだ ActiveAdmin は便利ということで使い続けています。とはいえ、過去・現在に最適なアーキテクチャが未来も最適である保証はないので、今後の管理機能の在り方を考える上で非常に参考になりました。 また、管理機能のバックエンドとエンドユーザー向け機能のバックエンドを同じにするのも同意です。管理機能の中には本来は機能として提供すべきなものが提供されていないことから運用に負がたまって社内管理機能に機能追加の力学が働き、結果として管理機能が肥大化していくというのはあるあるだと思っています。バックエンドを分けると管理機能に閉じた変更の方が容易になり、管理機能の肥大化を加速させる要因になりうると感じました。 (@euglena1215) 管理機能アーキテクチャパターンの考察と実践 by ohbarye - Kaigi on Rails 2023 オフライン参加楽しかったですね。2日間で多くの企業のエンジニアとも交流が出来ました。 今年は抽選に外れたのでブース出展できなかったのですが、来年はブース出展したいと思います。来年はブースでまた会いましょう!
はじめに こんにちはokodoonです タイミーのデータ基盤に対してデータモデリングを始めてしばらく経ったので、現状の全体構成を紹介したいと思います 全体構成 弊社のBigQueryは以下の4層にレイヤリングされています それぞれの役割は以下のような切り分けになっています レイヤー名 役割 データレイク層 複数ソースシステムのデータを未加工の状態でBigQueryにロードする宛先 dbt snapshotによるソースの履歴化 ステージング層 複数ソースシステムのデータを共通した処理でクレンジングする層 DWH層 ソースシステムのデータ形式を分析に適した形に変換する層 ディメンショナルモデリング/ログテーブルをイベント単位に分割/その他便利テーブル作成 データマート層 特定用途に対して1:1で作成されたテーブル群を格納する層 ダッシュボード用テーブル/Looker用テーブル/GoogleSheets用テーブル/外部サービス連携用テーブル 図で表すとこんな感じです DWH層, データマート層の切り分けについて 「DWHとデータマートとは?どう切り分けるか?」と100人に聞いたら100通りの答えが返ってきそうなこちらの定義 弊社内ではデータ基盤チーム内で議論した結果、以下のような切り分けをしています - DWH: ステージングデータを分析ニーズに合わせて加工したもの - データマート: 特定ニーズに対して1:1で作成されたテーブル なので ディメンショナルモデリングなどの分析用データへ変換したテーブル クエリを発行する際に、結果の出力が重くなってしまうテーブルを軽量化したもの などがDWH層に格納されているテーブル群で 特定ダッシュボード用に作られたテーブル Looker探索環境用に作られたwide_table 外部サービス連携用に成形されたテーブル などが現在データマート層に格納されているテーブル群になります ※「ダッシュボードに1:1で対応するマートを作る方針なのかあ」と思われるかもですが、こちらは現在推進中のdbt exposureを用いたアウトプット管理を見据えているためです(またこの辺りは別の機会にアウトプットします!) 各層の説明を少しだけ深掘り データレイク層 この層でdbt snapshotを活用した履歴化を実行しています 下流のステージング層/DWH層などで履歴化をしていない理由としては、何かしらの加工がされたあとのデータを履歴化する場合、加工処理に何かしらのバグや想定漏れがあった場合のロールバックが大変なためです 分析に使用する場合は以降の層でクレンジング/モデリングされたデータのみでいいため、基本的に分析ユーザーはデータレイク層へのアクセス権限を保持していません ステージング層 以下のような処理を加えて、生データをクレンジングしています データ型の変更と統一 パーティショニング/クラスタリング データマスキング 品質担保のテスト uniqueテスト Nullテスト 外部キーテスト ユーザーが3NF形式のデータに対してクエリを書く場合は、品質が保証されたステージング層のテーブルに対してクエリを実行してもらうような運用にしています DWH層 DWH層内は以下のような区分が作られています DWH層内区分 役割 dbtのフォルダパス events ログテーブルを分析要件にあるイベントごとに分割整理して軽くしたテーブル群 models/dwh/events component ディメンショナルモデリングのデザインパターンに当てはまらないが便利な部品のようなテーブル群 models/dwh/component pre_dimension 同一属性単位にドメイン情報を結合 ドメイン要件に従ったフラグ生成などの複雑な事前処理 models/dwh/dimension_modeling/pre_dimension dimension SCD Type2カラムの生成( https://en.wikipedia.org/wiki/Slowly_changing_dimension#Type_2:_add_new_row ) サロゲートキーの生成 models/dwh/dimension_modeling/dimension fact ビジネス指標の作成 関連Dimensionのサロゲートキーの反映 models/dwh/dimension_modeling/fact conformed_fact 複数プロセスを跨いだよく使われるfactの組み合わせをconformed dimensionで結合 複数プロセスを跨いだビジネス指標の作成 models/dwh/dimension_modeling/conformed_fact 弊社Dimensional Modelingについて 弊社Dimensional Modelingについてざっくり解説します SCD Type 6の採用 dimensionテーブルは「履歴化されたテーブル(Type2)」と「最新情報を持つテーブル(Type1)」に分けることなく、SCD Type1~3の全てを一つのテーブルで管理する方針としています こうすることでFactに登録するサロゲートキーが少なくなり、分析時のクエリと思考がスリムになると考えています 採用しているFactテーブルのパターン Transaction Fact + Factless Factを接合したFactテーブル Transaction FactテーブルとFactless Factテーブルを別々に作るのではなく、ビジネスプロセス単位でまとめてくっつけています 例えば「買い物プロセス」を仮定した場合に、Transaction Factにあたる 合計売上 とFactless Factにあたる 決済回数 が同一の買い物Factテーブルにあるイメージです Snapshot Factテーブル Transaction FactやFactless Factではうまく表現できないような指標(etc. ユーザー数全体の推移, 銀行残高の推移, )を日,月単位の粒度でスナップショットして保持します 例えば日毎のユーザーdimensionのサロゲートキーを保持するようなSnapshot Factテーブルを作成することで、日単位のユーザー数の推移をDimensionテーブルで絞り込み可能な状態で追えるようになります FatなFactテーブル Factテーブルは分析ニーズのあるビジネスプロセス単位で作られると思いますが、そのビジネスプロセスで発生する指標を全て一つのFactテーブルで完結して出力できるような方針をとっています 買い物の決済プロセスのFactテーブルを仮定すると、 決済Factの決済金額 * 決済Dimensionの消費税率 = 消費税金額 みたいにFactとDimensionを組み合わせて新しい指標を出力するのではなく、決済Factテーブルに 決済金額 , 消費税金額 を二つとも持っておくような形です こうすることで「Factテーブルに定義されている指標をDimensionテーブルで絞り込むだけでいい」「Factテーブルにない指標があればFactテーブルに足せばいい」とクエリ作成時の思考が軽くなると考えています Factテーブル上に社内の指標を全て整理して保持している状態です データマート層 データマート層内区分 役割 dbtのフォルダパス wide_table ディメンショナルモデリング済みテーブルをJOIN済みの状態にしたテーブル群 Lookerの探索環境に1:1で対応している models/marts/wide_table dashboard LookerStudioダッシュボード, Spreadsheetのコネクテッドシートに1:1で対応しているテーブル群 models/marts/dashboard その他 他サービスに連携していたり、特定プロジェクトのために一時的に作られるテーブル群 models/marts/{{その他}} Lookerとの接続について LookerのUIを見てみると、dimensionとmeasureが選択できるような、ディメンショナルモデリングを意識したものになっています そこで弊社ではこの画像のようにLookerのデザインに乗っかる形で、Lookerの接続先をディメンショナルモデリング済みのwide_tableとしており、スター内のFactテーブルの指標をmeasureとして、Factに接続するDimensionテーブル内の要素をDimensionテーブルごとに分割して表示しています こうすることで 商品の生産国が中国の決済金額を決済方法ごとに出力したい というユーザーの思考があった時に 商品の生産国だから => 商品Dimensionの生産国を選択 決済方法だから => 決済Dimensionの決済方法を選択 出力したい指標は決済金額だから => Factの決済金額を選択 とLooker上のユーザーの行動に繋がると思っており、 ユーザーの思考 , 実際に走るクエリ , Lookerの探索環境のUI ができるだけ一致するようなデザインになっています こうすることでLookerへのフィードバックがモデリングをより便利にすることに繋がり モデリングへのフィードバックがLookerをより便利にすることに繋がります ダッシュボード用 / スプレッドシート用のデータマートについて 基本的にマートはView形式で作成します ( 補足:Viewテーブルとは ) Viewとして作成することで無駄なビルドやコスト増加を避ける方針です マート化する判断基準は「正確性と安全性を保守すべき対象かどうか」であり、チーム内で定めた基準に従ってマート化を実施してアウトプットをコード管理します ダッシュボード用 / スプレッドシート用のデータマートをdbtで作成するのでdbt上でリネージュ管理が行えるようになり、破壊的な変更が上流で発生した場合に保守対象のダッシュボードへの影響を抑えることができます 作成したマートに対応するdbt exposureを宣言することでデータオーナー管理やアウトプット先管理も行えるようにする予定です 将来構想 数ヶ月以内に実現したい将来 dbt exposureによるオーナーを含めたアウトプットの管理 主要ビジネスプロセスの全モデル化 1年以内とかにはやりたい将来 サンドボックス層の作成 ポリシータグを用いた、メンバーのアクセス可能情報レベルに応じたクエリ範囲の制限 まとめ・今後の課題 弊社は組織拡大のスピードがとても早く、データ活用者数もとても多いので クエリ作成者間で出力される指標値がブレる。ダッシュボードやLookerの値とアドホッククエリの値がブレる。そもそも定義がブレている などの問題を防ぎつつ 簡便にあらゆる指標値がクエリできる ように、ディメンショナルモデリングを主軸としたデータモデリングを推進してきました 現在は作成したモデル数はかなりのものになり、それに従ってLooker環境もリッチになってきたのですが、アナリストが作るアドホッククエリやダッシュボード作成クエリにまだまだ採用してもらえていない課題感があります モデリング済みテーブルを使ってクエリを書くのはDWH開発に参加してくれているアナリストメンバーに集中していて、他アナリストメンバーにモデリングなどの技術的内容とDWHの開発内容などをうまく同期できていない状態です モデリング済みテーブルを使用することで 指標が全社定義と常に一致する Dimensionの履歴を考慮した指標出力が可能になる クエリ文量が1/5とかになる クエリパフォーマンスが向上するケースもある などメリットも大きいと思うので、モデリング手法の理解を助けるアウトプットやワークショップなど様々なHowを通して、アドホックなクエリに採用してもらってもっとフィードバックのサイクルが高速化するようにしていきたいです! We’re Hiring タイミーのデータ統括部では、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp
イベント概要 2023年8月2日に「What’s “Next” JS Meetup」と題してバベル社のuhyoさんをお招きしてNext.jsに関する勉強会を開催しました。 その中でタイミーフロントエンドエンジニアのいーふとさん(@redshoga)の講演をイベントレポートにまとめてお届けします。 今回のスピーカー Server Actionsの概要 ※ 2023年8月2日のイベント開催時点での情報であり、将来的に変わる、変わっている可能性があります。 Server Actionsはざっくり説明するとサーバーで動作するコードをあたかもクライアント上に記述できる機能です。コード下部のLikeボタンがクリックされると、propsに渡されているincreateが呼び出され、その中でPrismaのclientが動作します。 クライアントで動作 レンダリング結果 サーバーで動作 送信データ 一つのファイルだけでなく別ファイルで書くことも可能で左側がクライアントで動作、右側がサーバーで動作します。それぞれに”use client” “use server”というディレクティブを書く必要があります。 レンダリング結果を見ると普通のフォームとして動作することがわかります。右側の”createPost” というのが普通のTypeScriptをインポートしているにも関わらず通常のformとして動いてくれる不思議な機能です。 送信データとしては普通のフォームデータです。内部ではACTION_IDがよしなに付与され、良い感じに動作します。 既存のフルスタックTSのアプローチ ここまでがServer Actionsを踏まえた未来のTypeScriptをイメージした内容だったのですが、現状のフルスタックTypeScriptにはどんなアプローチがあるのかを見ていきます。 よくあるのがスキーマの活用です。概念としてはOpen APIやGraphQLとも一緒だと思います。スキーマAPIの定義ファイルからTypeScriptの型生成を行う。よく見る代表例としてはopenapi-typescriptなどですね。タイミーではopenapi2aspidaというライブラリを使っています。aspidaというライブラリがあってそれに変換してくれるものですね。当然、定義ファイルが必要になりますが、自動生成される仕組みにしてしまえば楽なのかなと思います。 他の例としてはtRPCが今、流行っているのかなと思っています。TypeScriptに特化したRPCでインターフェースを提供するライブラリという解釈です。Next.js、Express、FastifyにインターフェースとしてつけてあげるとあたかもRPCのような感じで書くことが可能になります。フロント側の対応もしっかりしてReactのhookを勝手に提供してくれる優秀なライブラリだったりします。 シリアライズもsuperjsonが用いられており、MapやSetといったオブジェクトをそのまま送ることが出来ます。 クライアントサイド レンダリング結果 サーバーサイド 送信データ こちらが実際に書いた例です。 クライアントサイド側にapi.example.hello.useMutationというのがありますが、これがサーバーサイド側のコードを書くと勝手に生成されているような感覚で書くことができる開発者体験のよいライブラリになっています。 シリアライザとしてはsuperjsonが用いられており、内部ではよしなにwrapされているので使用感としてRPCっぽいといった感じのライブラリになります。 ここまでServer Actionsについてと今までのフルスタックTypeScriptをどうやって書いていたのか、という話をしてきましたが、比較するとServer Actionsは圧倒的に手軽です。”use server”と書くだけで動くので手軽です。 シリアライズの方式も微妙に違っていて、レスポンスで返ってくるやつを色々実験しているのですが、JSXとかも実は送れたりします。 Next.js特化で共通のインターフェースみたいな概念ではないので今のところはServer Actionsを書いて、それをNativeAppから呼ぶみたいなことは想定されていないと思います。revalidatePathの様な所謂、Next.jsのキャッシュサービスと併用して使ってねみたいなサンプルコードとかも散見されるので、インターフェースを提供するというよりはやはりNext.jsに特化したRPCといった印象です。 Server Actionsの活用例 Server Actionsを知っている人は似たイメージを持っていると思いますが、Vercel盛り盛りのフルスタック構成ですね。 サーバーのデータベースを触る際はServer ActionsとかAPI Routesとかで書いて、クライアントコードは普通にReactで書くと。Vercelは最近様々なサーバーレスストレージサービスを出しているのでそれらを活用して永続化していきます。 あとはKey-Valueストアとかもあるのでそれらでキャッシングをしてあげるといった感じになります。 もう一つはBFF as Server Actionsのようなイメージですね。BFF as Server Actionsという言葉自体は私が作った造語なのですが、先程の構成ではネイティブアプリケーションからAPIを叩けないですよね。 それをAPIだけVercelから外に出して、VercelのServer Actionsから叩いてあげる、といったことも可能ではないかなと思っています。あとは単純にServer ActionsにすることでJavaScriptが動かない環境でもフォームとタグが動くのでそういったプログレッシブエンハンスメントを主とした活用もあるのではないかなと思います。 まとめ 今回はSever Actionsこんな感じですよ、ということをお伝えしました。とはいえまだアルファ版なのでServer ActionsちゃんとGAされてほしいなと期待を持っています。便利なので使っていきたいという気持ちですね。最近のNext.jsはフルスタック寄りに様々な機能を提供してくれているんですけど、Railsのような規約(レール)がある訳ではないのでその議論は活発になっていくのでないかなというのを個人的に思っています。 ご清聴ありがとうございました。 uhyoさんの発表や弊社のもう一人の西浦さんの発表が気になる方は以下の動画を是非ご覧ください。 www.youtube.com 少しでも興味を持っていただいた方は是非こちらからカジュアルにお話しましょう! product-recruit.timee.co.jp
内容 こんにちは! スクラム マスターのShinoP ( @marupopu )です! この記事は、 スクラムフェス仙台2023 で登壇した事によるふりかえり記事になります。 実際の発表内容はこちら speakerdeck.com ※後で動画も公開されると思いますので、ぜひ見てください! なぜ登壇したか? 初めに、なぜ登壇する事となったか?ですが、時は今年の1月に行われた RSGT でコンフォートゾーンから抜け出す体験をしたのがきっかけでした。 具体的なコンフォートゾーンから抜け出す体験というのは、RSGT初参加で、初 OST のホストを務めた事です。 今までは外部の勉強会に参加しても、話を聞くだけで自分の中で解釈をして納得をして、わかった気になって終わっていました。 また、自分は参加する人で、登壇している人たちはすごい人というような印象を持っていました。 そんな中、RSGTならではの空気感や、 OST のホストをやる人も多く存在し、遠方から参加していることもあり、参加するからには効果を最大限にしたいという思いがあり、勢いのまま OST のホストに申し込んだ結果、多くの方が集まって頂き5チーム程度に分裂して話し合う規模になりました。 その時に、自分が思っているような疑問でも共感や気になってくれる人は案外多いのかもしれない…と認識できました。 その後、 タイミー主催の勉強会 での登壇経験をして、共同で発表してくださった Coincheck の方々や、参加者の方とScrumについて語り合う体験が楽しいと感じました。 www.youtube.com 自社主催の勉強会での登壇が楽しいと感じたので、さらに課外活動などにも興味が湧きました。 なぜ、 スクラム フェス仙台か? さて、なぜ スクラム フェス仙台を選択したのか?というと、私自身が 岩手県 盛岡在住ということもあり、仙台も割と近いので現地参加しやすかったのと、4年前に東京から岩手に移住してくるほど東北が好きで、東北という括りで盛り上げたかったのが理由になります! あと、去年も参加したかったのですが、現地チケット取れずに断念した記憶があるのでリベンジも込めて。 雰囲気はこんな感じ スクフェス 仙台間もなく始まるサメー🦈 pic.twitter.com/nVx7xLKmtK — スクラム フェス仙台 (@scrumsendai) 2023年8月25日 初プロポーザルと文豪制度 もちろん、初プロポーザルでした! Scrum Fest Sendai 2023 - SMって兼任?専任?結局どうなの? 〜持続可能なチームになる為の鍵〜 | ConfEngine - Conference Platform どのように書いたら良いかわからなかったので、他の方の書き方などを参考に、自分の伝えたいことを書いてみました。 また、採択頂けたタイミングで資料作成などを行うにあたり、弊社のTDE10の中の人つの”文豪制度”を利用しました。 productpr.timee.co.jp 壁打ちをしてもらいながら資料を作成し、本当に自分が伝えたいことを 言語化 するのを手伝って頂き、本当に助かりました。 また、壁打ちを行なっていく過程で、最初に出したプロポーザルのアウトラインがずれていたりもしたのに気が付いたので、何を伝えたいのか?から正しく伝えられるような 言語化 、資料作成までお世話になりました。 登壇者での参加は何がよかったか? RSGTなどもそうでしたが、登壇者でない時の参加と登壇者としての参加の違いを感じたので書いていこうと思います。 まず、気持ちの問題かもしれないのですが、”圧倒的喋りかけやすさ”があるような気がしました。 何人かに、タイミーのしのぴーさんですよね?って話しかけられて嬉しかったです😆 めちゃめちゃ目立つ登壇者用TシャツもGOOD! また、認定 スクラム マスター研修やクローズドなイベントでの交流のあった方々にも声をかけて頂いたりして、登壇者 = 参加することが確定しているなどのイメージなので、話しかけて頂いたのかな?と推測しています。 あとは、同じ登壇者同士で話せたことも良かったと思います。 初めての登壇者同士で話したりして、緊張しますね…とかその時の心境をシェアできたのはとても良かったです。 その他よかった点 こちらは登壇者でなくとも スクラム フェス仙台2023の体験がよかった点を書いていきます。 まず、なんと言っても” クラフトビール ”が美味しかった🍻🍻 (ビールを飲みながらの OST 最高でした!!!) Day1で少し飲みすぎて、Day2に酒焼けで声が少し枯れてしまったのは反省😂 ノベルティ のシャークレくんグラス 来週8/25,26は スクフェス 仙台! ノベルティ を製サメしてます🦈オンサイトの参加の方はぜひ現地で乾杯🍻しましょ〜 #scrumsendai pic.twitter.com/s7BZjrHJBZ — スクラム フェス仙台 (@scrumsendai) 2023年8月15日 また、その場でしか体験できないワークショップ体験や飲み会、昼ご飯時に話されるディープな悩みの相談会なども現地参加ならではの楽しみ方だなぁと思いました。 あとは、会場が複数の階に分かれていたこともあって、ギャザリング感がめっちゃ味わえました。 1階では、メイントラックと品川トラックとオンライントラックがモニターで映されていたのですが、その空間で話している人もいればモニターを見て観戦している人もいたり自由な感じでとても雰囲気が良かったです。 この場作りという観点で、 スクラム フェス仙台2023の運営メンバーが考え抜いたレイアウトだと思うと、すごいと感じておりました。 さらに、スタッフの方々も楽しんでいる&参加者も全員協力的だったと思うので、やはり場を作り上げていくのは全員でやるのだなぁと再認識できました。 反省点 めちゃめちゃ楽しかったのですが、ふりかえっていて、いくつか反省点が見えてきました。 それは、登壇を意識するあまり、他のセッションが頭に入ってこなかった…という点です。 Day1でお酒をたくさん飲んだのも緊張を紛らわすためかもしれません…(酒焼けの言い訳) そして、お気付きでしょうか…現地でのオリジナル写真がほとんどない事に… 精神的に一杯一杯で写真を撮るのも忘れていました… また、Day1ではしゃぎ過ぎて、登壇が終わった後の無気力感と 疲労 が半端なかったです。 (まさに スクラム フェス仙台2022のシャークレくんのツイートみたいなイメージ🤣) それだけ、この登壇にかけてきたという事にしておきましょう😅 初めての社外カンファレンスの登壇終わって、めっちゃ脱力してる 燃え尽きた… — ShinoP/しのぴー@ スクラム マスター (@marupopu) 2023年8月26日 #scrumsendai スクラム フェス仙台のすべての日程を終了しました。 2日間本当にありがとうございました! pic.twitter.com/RPuVsa3NRo — スクラム フェス仙台 (@scrumsendai) 2022年8月27日 今後について 今回、 スクラム フェス仙台に登壇者として参加して、色々な気付きが得られました。 その中で、 カイゼン したい点としては、登壇経験を積み重ねてさらに楽しみたい!ということです。 今回は初参加、初プロポーザル、外部イベントでの初登壇という事でめちゃめちゃ緊張したのですが、楽しみの方が上回る体験となりました。 しかし、もっと楽しむためには登壇に慣れる事により心に余裕を持ってさらに楽しめるのでは?と感じました! (あと、全力で楽しむために体力もつけたい!) 他には、今回の スクラム フェス2023の運営側の方々が光る場面がいくつも目撃したので、自分でもこのような場作りができるようになりたいと強く思いました。 元々、なぜ スクラム フェス仙台か?にも書いた通り、東北を盛り上げたいという気持ちがあります。現在、北東北(青森・岩手・秋田)に スクラム や アジャイル の勉強会はほとんど開催されていないですし、コミュニティも私の観測範囲では見当たらないように見えます。 (コンパスで見ると「 アジャイル ・ スクラム 」のワードで勉強会が一つもない) ですので、個人的なNextActionとして岩手で アジャイル / スクラム コミュニティを生み出す活動なども今後やっていきたいと考えています!! 最後に、こんな素晴らしい場作りをして頂いた運営の方々を初め、関わってくださった全員に改めて感謝をお伝えして、ふりかえりの記事とさせて頂きます!! ありがとうございました!そして、おつサメでした!! スクフェス 仙台2023!おつサメ〜🦈最高に楽しい2日を皆さんと作れて最高嬉しいシャー  #scrumsendai pic.twitter.com/jtNhSak5sz — スクラム フェス仙台 (@scrumsendai) 2023年8月27日
イベント概要 2023年8月24日にGaudiy社との共催で「 不確実性を乗りこなす強いプロダクトマネジメント組織のデザイン 」と題してプロダクトマネジメント組織に関する勉強会を開催しました。 不確実性の高いマーケットに対して、高い成果を出せるチームにしていきたいPdMやPOの方、組織設計からプロダクトのアウトカムを高めたいと考えているEMの方などに特にお勧めの勉強会でしたのでイベントの中から高石さんによる「『わからない』に立ち向かい続け成果を生み出す、実験志向なチームの育み方」の講演レポートをお送りします。 今回のスピーカー 「『わからない』に立ち向かい続け成果を生み出す、実験志向なチームの育み方」 不確実性の高いゴールに付き物の「わからない」 プロダクト開発ではゴールを設定します。タイミーではスクラムを採用しているチームが多いのでプロダクトゴールと表現されます。 RetentionRateの10%向上といった定量的なゴールもあれば、ユーザーストーリーの形式で特定属性のユーザーが○○を実現できるようになるといったゴールもあると思います。近年はこの様に何かしらのゴールに向かってイテレーティブな開発をすることが一般的になってきました。 ここで付き物なのが「わからない」ということです。ゴールが定まってもそこにどうやれば辿り着けるのかは「わからない」に満ちています。PdMやPOの方で「わからない」のフィードバックを貰ったことがない人はあまりいないのではないでしょうか? でもこの「わからない」ことってネガティブなことなのでしょうか? 人間にとって「わからない」ものは本能的に身構えてしまったり、気持ち悪さを感じることが多いと思います。一方でこの「わからない」気持ちは「わかりたい」という行動への原動力でもあります。「わからない」を放置しておくとモチベーションの低下に繋がってしまいますが、適切に解消が出来ればプロダクトの成果に繋がります。 「わからない」を「わかる」にする力がプロダクトの成果を生む 普段、仕事で強く意識することは少ないですが日常業務の中でも「わからない」ことが沢山あります。 こういった様々な疑問がある中で皆さんは無意識に解消する行動を取っていると思います。インタビューをしてみたり、A/Bテストを実施したり、言語化こそされていなくてもわかる様にするアクションを取っているのではないでしょうか。 その様な行動で「わからない」を一つずつ「わかる」に変えていくことで不確実性の高いゴールに対しても道筋が見えてきます。ゴールの達成方法がわかるようになってくるのでプロダクトの成功にまた一歩近づくことが出来ます。 逆に「わからない」を解決出来ないとBIGBANGリリースに繋がったり、出したは良いけどその後の計測が上手く出来ずに成功したかも失敗したかも判断出来ないといったことが発生します。それだけならまだしも「わからない」を解決出来ず、不確実性を許容出来ないチームになってしまうと簡単なゴールしか設定できなくなってしまうリスクもあります。 基本的にどのプロダクトも新規性が高くまだ世の中に無いものを生み出しているわけですから、文化として「わからない」を許容し、それを解消しつづける組織力をいかに創れるのかが企業の競争力に直結すると思います。 タイミーのケースから見る、チームの実験を支える環境づくり ユーザビリティテストやA/Bテストなどについてはよくまとまった書籍がたくさんありますし、皆さんも勉強されていることと思います。しかし、座学として知識を習得しても、それらを実際にチームとして行動に移すまでには辿り着けないことの方が多いのではないでしょうか? そこでタイミーでは「チームが実験を行ったり情報収集する、心理・物理のハードルを極限まで取り除く」ということを行っています。 チーム専属のアナリスト チーム専属になる前までは分析チームに依頼をして、回答を待つ形式でした。この場合、分析チームが忙しい場合だと答えが分かるまで1〜2週間掛かってきます。このようなことになると既にスプリントの1週間を超えてしまいます。依頼側としてもそんなに時間がかかるなら別のやり方を模索しよう、となり依頼することそのものを諦めてしまいがちです。 しかし、スクラムチームに1人専任でプロダクトアナリストが所属する様になったことで物事の大小を問わず、迅速な解決ができる様になり、定量面の仮説検証速度が向上しました。 ユーザーインタビューのハードルを極限まで落とす「 Interview as a Service 」 タイミーではユーザーと話したいと思ったらとにかくすぐに話せるようになっています。 Googleカレンダーにパブリックな仕組みがあってユーザーインタビューの予定を作ると自動で人がアサインされ、議事録が用意されて、インタビューアーのリクルーティングまで完結する仕組みがプロダクトマーケティング部によって運用されています。 このプロセスでは最短で明日、中央値でも2、3日後には望んだセグメントの人とお話が出来ます。 これによりスプリントの中に収まってくるので事前にリサーチして準備をしよう、ではなくスプリントの中で解くといったケースまで出てきたのも面白かったポイントです。 ここまで環境を整えていくと勝手に実験が増えていきます。実験をしよう、しようと思っているうちは中々実験が進みませんが、阻害する要因を全て取り除いていくと皆も実験したい気持ちはそもそもあるので自然と実験が増えます。 実験に特化したフレームワークの採用 『LeanUX』の中で紹介されているLeanUXCanvasを採用しています。 1〜8までのボックスがあるのですが、特に7が面白く「一番はじめに学習しないといけないこと」を明示しています。今回の言葉でいうと「わからないものの中でもっとも早くわかる様にしないといけないこと」ですね。「わからない」ことの存在を事前にフレームワークで宣言をしてその解消の重要性も説いていることがユニークに感じています。 取り組みのとある共通点 どのケースも全く違う取り組みに見えて、共通点があります。 ① ゴール達成の過程に生まれる「わからない」を「わかる」にする力をつけること ② チームを取り巻く、組織や環境面からアプローチしていること 「わからない」を爆速で楽にわかるようにするといった観点でチームや組織を組成すれば高い不確実性も許容して実験を重ねて解決できる強いチームが生まれます。 また「わからない」を解決するだけであれば手段はさまざまありますが、組織や環境面からのアプローチはレバレッジが効きます。例えばインタビューの話であれば開発チームに限らず、営業やマーケティングチームが使ったりしていて、全社に波及しています。 まとめ まとめると今日は不確実性という言葉は広いので「わからない」と置き換えましたが、この「わからない」ことをネガティブに受け取って欲しくないと思っています。「わからない」ことも当然ですが、すぐ「わかる」ことよりも「わからない」に対して挑戦して考えていく方が生産的でプロダクトの組織としてもより成果をあげれるのかなと思います。 実験や仮説検証でも個人だけの頑張りではなくチームとして取り組めることが多くあります。プロダクトのチームは元より実験をしたいので妨げる要因を取り除いていくことで自然と実験する文化が育まれ、不確実性に強いチームになっていきます。タイトルに繋がっていきますが皆で「わからない」に立ち向かい続け成果を生み出す、実験志向なチームを育てていきましょう。 充実のパネルディスカッションは本編動画から! www.youtube.com 実験文化のある組織をいいなと思った方は是非お話しましょう product-recruit.timee.co.jp
こんにちは! タイミーのデータアナリストの@takahideです。 今回は、メンバーが行っている「勉強会」を紹介させていただきます。 ご紹介する内容は以下になります。 ・勉強会ではどんなことをやっているの? ・勉強会を通じた業務の広がりとは? 勉強会について 勉強会の具体例 背景 勉強会で用いた本 勉強会の進め方 勉強会を通して 効果検証の講習会 まとめ We’re Hiring! 勉強会について まずは、勉強会に関して簡単に紹介させてください。 タイミーでは、有志のメンバーが勉強会を立ち上げる文化が根付いています。 その中でも、データアナリストの勉強会は大きく2種類あります。 一つは、「会計」「プロジェクトマネジメント」などテーマをもとにした「一話完結型」のもの。 もう一つは、課題本を決めて輪読する「読書会型」で、今回はこちらをご紹介します。 勉強会の例 「読書会型」の勉強会の一番の目的は「分析力の向上」です。 勉強会を通じて、分析手法に関する共通言語が浸透することで、分析スキルの底上げだけでなく、 分析プロセスや結果に関するコミュニケーションが円滑になりました。 また、他部署の方と勉強会を行うことで、複数部署間のコミュニケーションもスムーズになりました。 プロダクトチームと行なった勉強会の感想 今回は「効果検証」に関する勉強会を一つの事例として紹介できたらと思います。 勉強会の具体例 背景 効果検証の勉強会は、データアナリストの検証手法の習得を目的に始まりました。 タイミーの組織が大きくなるにつれて、検証が必要な施策の件数も増加傾向にあります。 また、マーケティング施策のように、ABテストでの検証が難しいものも増えており、手法の共通認識を作るニーズが高まっていました。 勉強会で用いた本 勉強会では、『効果検証入門〜正しい比較のための因果推論/計量経済学の基礎』(安井 翔太)を課題図書として用いました。 傾向スコア、差の差分法(DID)など効果検証の手法が網羅的に説明されているのと、コードが豊富なのが良かったです。 勉強会の進め方 勉強会は、週に一度、担当者がgoogle collabを用いて発表を行い、他メンバーから質疑応答を行いながら進めています。 コードを確認しながら進めることで理解が深まりやすくなっています。 勉強会を通して 大きく3つの活動に繋がりました。 一つ目は、当初の目的だった「効果検証手法に関する共通言語の促進」です。 特に、検証手法を用いる際の注意点を共通認識として持つことができました。 例えば、回帰不連続デザイン(RDD)を利用する際には、「分析対象が介入に関する状態を操作できない」ことが求められますが、こうした条件が満たされない場合は、逆に、集積分析(bunching analysis)のような操作を前提にデザインされた手法を試すなどです。 二つ目は、勉強会をきっかけに、データアナリスト内でpythonを推進する活動に繋がったことです。 勉強会はpythonで実行しましたが、勉強会の内容を踏まえてタイミーの実データを用いた検証を行う際に、pythonへの慣れの有無がボトルネックになっていることが分かりました。 そこで、現在は、pythonを通常の分析業務で自然に扱えるための学習パッケージの構築を進めています。 三つ目は、全社的な講習会に繋がったことです。 こちらに関して次に詳細をお伝えできたらと思います。 効果検証の講習会 冒頭でもお伝えしましたが、現在タイミーは組織拡大に伴い、効果検証のニーズが増加しています。 そこで、今回の勉強会の内容と、過去の分析ナレッジを踏まえて、全社的な効果検証の講習会を実施しました。 効果検証の目的から始まり、交絡因子など、効果検証で注意するべき内容をお伝えしました。 また、効果検証を進める際に、データアナリストと事業部がどのように連携すると良いかをお話しました。 まとめ データアナリストの勉強会のご紹介いかがだったでしょうか。 改めてにはなりますが、データアナリストが勉強会を行うメリットに「分析手法の習得」と「共通言語の促進」があると思います。 特に後者は、勉強会のナレッジを他部署の方に展開するなど、コミュニケーションの円滑化に繋がっていると考えます。 タイミーは事業、組織ともに拡大傾向にありまして、データアナリストが活躍する機会が多く存在しています。 これからもデータアナリスト発信で意思決定に役立つ情報を展開することで事業の成長に貢献したいと考えています。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! カジュアル面談 も行っていますので、少しでも興味がありましたら、気軽にご連絡ください。
こんにちは☀️ タイミーでアナリストとアナリティクスエンジニアしてますokodoonです 今回の記事はdbt CloudでPull Requestを作るときに、レビュー負荷が高くなってしまっていた問題を解消できるように、コンパイル済みのSQLをPR上にコメントするような仕組みを作成したことについての紹介です。 もし同じような課題感を抱えている方がいらっしゃれば、参考にしていただければ幸いです 課題感 今回選択した解決策 背景/前提 実装概要 各ステップの説明 PRの情報をもとにprofiles.ymlの動的生成 コンパイル処理の実施 PR上にコメント どんなふうに動くかみてみる 結果 We’re Hiring! 課題感 弊社のデータ基盤ではDWH層DataMart層は「分析用に加工されたデータを扱う層」として定義しています。 各種ドメインに依存した集計や変換のロジックが含まれるため、この層のモデリングに関しては基盤開発側のレビューのみでなくアナリスト観点でのレビューも必須となります。 ですが、分析ドメイン観点でのレビューが必要な場合に、純粋なSQLではないdbtモデルがアナリストレビューの障壁になることが多いです。 またアナリスト以外であっても「このmacroがどのようにコンパイルされるか」を把握するのは少し面倒だったりします 今回選択した解決策 そこでdbtモデルをコンパイルしたSQLファイルをPullRequest上にコメントするような仕組みを考えました。 この実装によって先ほど挙げた課題感が以下図のように解決されることを期待して開発しました 背景/前提 1. 開発環境について 弊社ではdbtを活用したデータ基盤の開発を行っており、 dbtモデル開発をdbt Cloud上の統合環境にて実施するような流れになっています。 2. CIジョブについて CI用のdbt CloudのJobは RUN ON PULLREQUEST で実行されて、以下のようなコマンドが実行されています。 dbt build --select state:modified+ mainブランチとの差分があるdbtモデルがBigQuery上にビルドされている状態です(本来CIで走るdbtコマンドをレイヤーごとに分割していますが、ここでは簡略化しています) 3. CI環境のスキーマ名とcustom_schemas.yml戦略 dbt Cloudで設定できるCI Jobではtarget schema名の命名が dbt_cloud_pr_<job_id>_<pr_id> となっており、連携レポジトリのPR番号に対して動的に作成されます。 (DBT_JOB_SCHEMA_OVERRIDEでこの命名規則の上書きもできますが、PR単位でCIテーブルが作成されて欲しいので、デフォルトの命名規則にしたがっています。) 参考: https://docs.getdbt.com/docs/deploy/continuous-integration#how-ci-works また、弊社では開発環境とCI環境のスキーマ名が {{ターゲットスキーマ名}}_{{カスタムスキーマ名}} になるように custom_schemas.sql で定義してあります。 そのため、CI環境のテーブルは dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} の命名規則で作成されたデータセット名以下に作成されている状態です。 実装概要 作成したYAMLはこちら(クリックで展開) name : DBT Compile and Comment on PR on : pull_request : types : [ opened, synchronize, reopened ] jobs : dbt_compile : runs-on : ubuntu-latest permissions : contents : read pull-requests : write id-token : write env : DBT_ENVIRONMENT : dev steps : - name : Check out repository code uses : actions/checkout@v3 with : fetch-depth : 0 ref : ${{ github.event.pull_request.head.ref }} - name : Fetch base ref run : git fetch origin ${{ github.event.pull_request.base.ref }} - name : Set Up Auth uses : "google-github-actions/auth@v1" with : token_format : "access_token" workload_identity_provider : "hogehogehoge" service_account : "hogehogehoge@hogehoge.iam.gserviceaccount.com" - name : Set up Cloud SDK uses : google-github-actions/setup-gcloud@v1 - name : Set up Python uses : actions/setup-python@v3 with : python-version : 3.11 - name : Install dependencies run : | python -m pip install --upgrade pip pip install dbt-core dbt-bigquery - name : Generate profiles.yml run : | chmod +x ci/compile_sql_comment/generate_profile.sh ci/compile_sql_comment/generate_profile.sh ${{ github.event.pull_request.number }} - name : Compile DBT id : compile run : | dbt deps dbt compile --profiles-dir . --target dev --profile timee-dwh compiled_sqls="" files=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.head.ref }} | grep '\.sql$' || true ) if [ -n "$files" ] ; then for file in $files; do compiled_file_path=$(find target/compiled -name $(basename $file)) echo "Compiled file path: $compiled_file_path" if [ -n "$compiled_file_path" ] ; then compiled_sql=$(cat "$compiled_file_path" ) compiled_sqls="${compiled_sqls}<details><summary>${file}</summary>\n\n\`\`\`\n${compiled_sql}\n\`\`\`\n\n</details>" fi done fi printf "%b" "$compiled_sqls" > compiled_sqls.txt - name : Comment on PR uses : actions/github-script@v6 with : script : | const fs = require('fs'); const output = fs.readFileSync('compiled_sqls.txt', 'utf8' ); const issue_number = context.payload.pull_request.number; const owner = context.repo.owner; const repo = context.repo.repo; async function processComments() { const comments = await github.rest.issues.listComments( { owner : owner, repo : repo, issue_number : issue_number, } ); const dbtComment = comments.data.find(comment => comment.body.startsWith('DBT Compile Result')); const body = ` DBT Compile Result : \n$ { output } `; if (dbtComment) { await github.rest.issues.updateComment( { owner : owner, repo : repo, comment_id : dbtComment.id, body : body } ); } else { await github.rest.issues.createComment( { owner : owner, repo : repo, issue_number : issue_number, body : body } ); } } processComments(); actionsの流れを説明するとこうなります。 PRブランチにチェックアウトして、PRブランチとmainの差分を確認するためにfetch workload_identity_providerを用いたBigQueryへの認証 必要パッケージのインストールと使用するパッケージの宣言 PRの情報をもとにprofiles.ymlの動的生成 コンパイル処理の実施 dbt compileの実行 mainとの差分があるファイルだけを抽出 差分ファイルのcompile結果を文字列化 PR上にcompile結果がなければdbt compile結果を新規コメント。既にcompile結果がコメントされていたらコメントをupdate 各ステップの説明 説明が必要そうなステップの説明をしていきます PRの情報をもとにprofiles.ymlの動的生成 上で述べた通り、CI環境のテーブルは dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} の命名規則で作成されたデータセットに配置されています。 dbt compileの出力結果のデータセット名が dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} になるように、デフォルトスキーマ名を動的に宣言するためのprofiles.ymlを作成するシャルスクリプトを作成しております #!/bin/bash set -e cat << EOF > profiles.yml timee-ci-dwh: target: ci outputs: dev: type: bigquery method: oauth project: ci_env_project schema: dbt_cloud_pr_39703_ $1 execution_project: ci_env_project threads: 1 EOF 参考: https://docs.getdbt.com/docs/core/connect-data-platform/bigquery-setup#oauth-via-gcloud これによりdbt compileの出力結果が、このターゲットスキーマ名を参照したスキーマになります。 コンパイル処理の実施 run : | dbt deps dbt compile --profiles-dir . --target ci --profile timee-ci-dwh compiled_sqls="" files=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.head.ref }} | grep '\.sql$' || true ) if [ -n "$files" ] ; then for file in $files; do compiled_file_path=$(find target/compiled -name $(basename $file)) echo "Compiled file path: $compiled_file_path" if [ -n "$compiled_file_path" ] ; then compiled_sql=$(cat "$compiled_file_path" ) compiled_sqls="${compiled_sqls}<details><summary>${file}</summary>\n\n\`\`\`\n${compiled_sql}\n\`\`\`\n\n</details>" fi done fi printf "%b" "$compiled_sqls" > compiled_sqls.txt dbt compileをPRブランチで実行 git diffで差分があった.sqlファイル名をdbt compileの実行結果であるcompile済みsqlファイルが格納される target/compiled/ 以下でfindを実行してfileパスを取得 fileパスのcat結果を折りたたみタグ内に格納して文字列に追加 って流れの処理にしています。 このような処理にすることでSQLコンパイル結果のPRコメントを必要分だけコメントできる形としました。また、折りたたみタグに格納することでPullRequestの可視性を損なわないようにしました。 PR上にコメント - name : Comment on PR uses : actions/github-script@v6 with : script : | const fs = require('fs'); const output = fs.readFileSync('compiled_sqls.txt', 'utf8' ); const issue_number = context.payload.pull_request.number; const owner = context.repo.owner; const repo = context.repo.repo; async function processComments() { const comments = await github.rest.issues.listComments( { owner : owner, repo : repo, issue_number : issue_number, } ); const dbtComment = comments.data.find(comment => comment.body.startsWith('DBT Compile Result')); const body = `DBT Compile Result:\n${output}`; if (dbtComment) { await github.rest.issues.updateComment( { owner : owner, repo : repo, comment_id : dbtComment.id, body : body } ); } else { await github.rest.issues.createComment( { owner : owner, repo : repo, issue_number : issue_number, body : body } ); } } PR上のコメントの一覧を取得して、既に DBT Compile Result: で始まるコメントがPR上に存在するのであれば、そのコメントのアップデート、存在しないのであれば新しくコメントをする。 という処理をしています。 条件分岐が発生する処理を簡便に記載したかったのでinlineでjavascriptを記載できる github-script を使用して記述しています これによってactionが走るたびにコメントが新規でされるのではなく、一つのコメントが上書きされ続けるような処理となり、PullRequestの可視性を損なわないようにしました。 どんなふうに動くかみてみる こんなモデルをテストで作ってみました {{ config( schema = ' sample_schema ' )}} SELECT ' hogehoge ' AS name , 30 AS amount UNION ALL SELECT ' fugafuga ' AS name , 50 AS amount {{ config( schema = ' sample_schema ' )}} SELECT SUM (amount) AS sum_amount FROM {{ ref( ' sample_model1 ' ) }} AS sample_model1 以下のようにPullRequest上に変更内容が折り畳まれた状態でコメントされて、ref関数のコンパイル結果がCI環境のデータセットになっていることを確認できます 次にsample_model1.sqlを以下のように修正して再度pushしてみます {{ config( schema = ' sample_schema ' )}} SELECT ' hogehoge ' AS name , 30 AS amount UNION ALL SELECT ' fugafuga ' AS name , 50 AS amount UNION ALL SELECT ' blabla ' AS name , 100 AS amount 以下のように既存のcommentがeditされて、修正後の内容でコンパイルした結果で上書きされていることが確認できます 結果 弊社のデータ基盤は four keys計測による開発ヘルススコアの計測 を行っており、今回の仕組みのリリース前後で開発リードタイムを計測してみましたが、目立った影響は出ていませんでした😢 レビューを円滑にできる環境を整っていないことが課題ではなく、他業務との兼ね合いでDWH開発のレビューに充てることができる時間がそもそも少なそうだったり、レビューに必要なドメインのインプットが足りていないことがボトルネックになっていそうだなという発見にも繋がったので、そこはプラスに捉えています メンバーの声を聞いていると便利なことは間違いないらしいので、レビューコストの低減による持続的なトイル削減に将来的には繋がっていけばいいなと思ってます🙏 使えそうだな。試してみようかなと思っていただけたら幸いです! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp
こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の菊地です。 今回は、タイミーがBIツールとして導入しているLookerでの、 H3 を使用した可視化をするための取り組みを紹介したいと思います! H3とは H3 とは、Uber社が開発しているグリッドシステムで、オープンソースとして提供されています。 H3 では、位置情報に紐づいたイベントを階層的な六角形の領域にバケット化することができ、バケット化された単位でデータの集計が可能になります。 タイミーでは、サービスを提供する各都市の需給を測定するために,六角形単位で集計したデータを可視化するなど、様々な場面での分析に活用しており、例えば以下のような可視化を行なっております(数値はランダム値です)。 H3 についての詳細は、以下のページが参考になるかと思います。 https://h3geo.org/ https://github.com/uber/h3 https://www.uber.com/en-JP/blog/h3/ 前提条件 LookerのデータソースはGoogle Cloud BigQueryとします。 可視化に必要な各種ファイルの生成にはPythonを用いており、使用しているPythonバージョンと、依存ライブラリのバージョンについては、以下で動作確認を行っています。 Pythonバージョン: 3.11.4 依存ライブラリ geojson==3.0.1 h3==3.7.6 numpy==1.25.2 pandas==2.0.3 shapely==2.0.1 topojson==1.5 また、大変簡略化した例ですが、データソースのBigQueryプロジェクト・データセット・テーブルは、以下の想定とします。 プロジェクト名: sample-project データセット名: sample-dataset テーブル sales : 売上データを保持しているテーブル sales_at : 売上の日時 amount : 売上 place_id : places テーブルの外部キー places : 位置情報(緯度・経度)を保持しているテーブル places テーブルと sales テーブルには1:nのリレーションが存在 erDiagram places ||--|{ sales: "1:n" places { INTEGER id FLOAT latitude FLOAT longitude } sales { INTEGER id DATETIME sales_at INTEGER amount     INTEGER place_id } Lookerでの可視化を行うための手順 今回は以下の手順に従って、上記saleテーブルの売上をH3六角形にバケット化し、Looker上で可視化します。 緯度経度情報を保持しているBigQueryテーブルにH3六角形IDを付与し、別テーブルとして保存 TopoJsonファイルの作成 作成したTopoJsonファイルをLookerに追加 Lookerのmodelファイルにmap_viewフィールドを追加 Lookerのviewファイルにdimensionを追加 Lookerのmodelファイルにexploreを追加 Lookerでの可視化 1. 緯度経度情報を保持しているBigQueryテーブルにH3六角形IDを付与し、別テーブルとして保存 集計の際に使用する「緯度経度情報を保持しているBigQueryテーブル」(ここでは places テーブル)に対して、H3六角形ID(以下H3 hex idと記載)を付与し、別テーブルとして保存しておきます。ここでは h3_places テーブルとして保存しています。 下記は、 places テーブルをpandas.DataFrameとして読み込み、H3六角形解像度0~15までのH3 hex idを付与し、テーブルとして書き出すコードの例です。 H3六角形解像度は値が大きくなるにつれて、小さな六角形(=解像度が上げる)になり、詳細については下記ドキュメントが参考になるかと思います。 https://h3geo.org/docs/core-library/restable/ import h3 import pandas as pd class BigQueryClient : def __init__ (self): ... def read_table_as_dataframe (self, table_id: str ) -> pd.DataFrame: """BigQueryテーブルをpandas.DataFrameとして読み込む処理""" ... def write_table_from_dataframe (self, df: pd.DataFrame, table_id: str ) -> None : """pandas.DataFrameをBigQueryテーブルを書き込む処理""" ... def make_h3_hex_ids (df: pd.DataFrame) -> pd.DataFrame: _df = df.copy() for resolution in range ( 16 ): # 緯度・経度情報を元に、H3 hex idを付与 _df[f 'h3_hex_id_res_{resolution}' ] = df.apply( lambda x: h3.geo_to_h3(x[ 'latitude' ], x[ 'longitude' ], resolution), axis= 1 ) return _df if __name__ == '__main__' : ... bq_client = BigQueryClient() # 緯度(latitude)、経度(longitude)を保持しているBigQueryテーブルをDataFrameとして読み込む df = bq_client.read_table_as_dataframe( 'sample-project.sample-dataset.places' ) # H3 hex idを付与する h3_df = make_h3_hex_ids(df) h3_df.rename(columns= dict ( id = 'place_id' ), inplace= True ) # H3 hex idを付与したDataframeをBigQueryテーブルとして書き込み bq_client.write_table_from_dataframe(df=h3_df, 'sample-project.sample-dataset.h3_places' ) 例として、以下のような緯度経度を保持しているサンプルデータに、H3 hex idを付与した場合、以下のような結果になります。 import numpy as np np.random.seed( 42 ) tokyo_latitude = 35.6762 tokyo_longitude = 139.6503 df = pd.DataFrame( [[i, np.random.normal(tokyo_latitude, 0.3 ), np.random.normal(tokyo_longitude, 0.3 )] for i in range ( 1 , 11 )], columns=[ 'place_id' , 'latitude' , 'longitude' ] ) h3_df = make_h3_hex_ids(df) h3_df.head( 10 ) 2. TopoJsonファイルの作成 「1. 緯度経度情報を保持しているBigQueryテーブルにH3 hex idを付与する」でH3 hex idを付与したDataFrameを元に、TopoJsonファイルを作成します。 TopoJsonの詳細についてはこちらの「 topojson 」GitHubリポジトリを参照してください。 下記は、TopoJsonファイルを作成するコード例です。 処理の内容としては、GeoJson形式を経由して、TopoJsonに変換し、ファイルとして出力をしています。 TopoJsonファイルは、 H3解像度 別に作成しています。 from pathlib import Path import geojson import h3 import pandas as pd from shapely import geometry import topojson class H3ToGeojson : @ staticmethod def get_h3_geojson_features (h3_hex_ids: list [ str ]) -> list [geojson.Feature]: polygons = h3.h3_set_to_multi_polygon(h3_hex_ids, geo_json= True ) features = [geojson.Feature(geometry=geometry.Polygon(polygon[ 0 ]), properties= dict (h3_hex_id=h3_hex_id)) for polygon, h3_hex_id in zip (polygons, h3_hex_ids)] return features def get_h3_geojson_feature_collection_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str ) -> geojson.FeatureCollection: assert df.columns.isin([h3_hex_id_column]).any(), f 'column `{h3_hex_id_column}` is not exists.' unique_h3_hex_ids = df[h3_hex_id_column].unique().tolist() geojson_features = self.get_h3_geojson_features(unique_h3_hex_ids) feature_collection = geojson.FeatureCollection(geojson_features) return feature_collection class H3ToTopojson : def __init__ (self): self.h3_to_geojson = H3ToGeojson() def get_h3_topojson_topology_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str ) -> topojson.Topology: feature_collection = self.h3_to_geojson.get_h3_geojson_feature_collection_from_dataframe( df, h3_hex_id_column=h3_hex_id_column ) return topojson.Topology(feature_collection, prequantize= False ) def make_h3_topojson_file_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str , save_file_path: Path) -> None : topojson_topology = self.get_h3_topojson_topology_from_dataframe(df=df, h3_hex_id_column=h3_hex_id_column) topojson_topology.to_json(save_file_path) if __name__ == '__main__' : ... h3_to_topojson = H3ToTopojson() save_dir = Path( 'topojson' ) save_dir.mkdir(exist_ok= True ) for resolution in range ( 0 , 16 ): h3_hex_id_column = f 'h3_hex_id_res_{resolution}' h3_to_topojson.make_h3_topojson_file_from_dataframe(df=h3_df, h3_hex_id_column=h3_hex_id_column, save_file_path=save_dir / f '{h3_hex_id_column}.json' ) 例として、先ほど作成したサンプルデータに対して、 resolution=4 を指定してTopoJsonファイルとして書き出す処理は以下のようになります。 h3_to_topojson = H3ToTopojson() h3_to_topojson.make_h3_topojson_file_from_dataframe(h3_df, resolution= 4 ) TopoJsonファイルの中身は以下のようになります。 { " type ":" Topology "," objects ": { " data ": { " geometries ": [{ " properties ": { " h3_hex_id ":" 842f5a3ffffffff " } ," type ":" Polygon "," arcs ": [[ -5 , -2 , 0 ]] ," id ":" feature_0 " } , { " properties ": { " h3_hex_id ":" 842f5bdffffffff " } ," type ":" Polygon "," arcs ": [[ 1 , -4 , 2 ]] ," id ":" feature_1 " } , { " properties ": { " h3_hex_id ":" 842f5abffffffff " } ," type ":" Polygon "," arcs ": [[ 3 , 4 , 5 ]] ," id ":" feature_2 " }] ," type ":" GeometryCollection " }} ," bbox ": [ 139.198358 , 35.267135 , 140.126313 , 36.103519 ] ," arcs ": [[[ 139.44526 , 35.765969 ] , [ 139.458427 , 36.000295 ] , [ 139.695196 , 36.103519 ] , [ 139.918545 , 35.971536 ] , [ 139.903854 , 35.7366 ]] , [[ 139.44526 , 35.765969 ] , [ 139.667342 , 35.634256 ]] , [[ 139.653549 , 35.399825 ] , [ 139.419179 , 35.297723 ] , [ 139.198358 , 35.429167 ] , [ 139.21065 , 35.662982 ] , [ 139.44526 , 35.765969 ]] , [[ 139.653549 , 35.399825 ] , [ 139.667342 , 35.634256 ]] , [[ 139.667342 , 35.634256 ] , [ 139.903854 , 35.7366 ]] , [[ 139.903854 , 35.7366 ] , [ 140.126313 , 35.603627 ] , [ 140.111006 , 35.368594 ] , [ 139.874758 , 35.267135 ] , [ 139.653549 , 35.399825 ]]]} 3. 作成したTopoJsonファイルをLookerに追加 LookerのFileBrowserを開いて、先ほど作成したTopoJsonファイルを追加します。 追加後、適切なフォルダにファイルを移動します。ここでは maps/h3 フォルダにTopoJsonファイルを移動します。 ├── maps    └── h3    ├── h3_hex_id_res_0.topojson    ├── h3_hex_id_res_1.topojson    ├── h3_hex_id_res_2.topojson ...    └── h3_hex_id_res_15.topojson 4. Lookerのmodelファイルにmap_viewフィールドを追加 下記のようにmap_layerを設定します。map_layerは H3解像度 別に設定しています。 property_key は「2. TopoJsonファイルの作成」で使用している H3ToGeojson.get_h3_geojson_features メソッド内の geojson.Feature の引数で設定している properties のkey名である h3_hex_id を指定しています。 map_layer: h3_hex_id_res_0 { file : "/ maps / h3 /h3_hex_id_res_0. topojson " format : topojson property_key: " h3_hex_id " } map_layer: h3_hex_id_res_1 { file : "/ maps / h3 /h3_hex_id_res_1. topojson " format : topojson property_key: " h3_hex_id " } map_layer: h3_hex_id_res_2 { file : "/ maps / h3 /h3_hex_id_res_2. topojson " format : topojson property_key: " h3_hex_id " } ... map_layer: h3_hex_id_res_15 { file : "/ maps / h3 /h3_hex_id_res_15. topojson " format : topojson property_key: " h3_hex_id " } 5 . Lookerのviewファイルにdimensionを追加 map_layer_nameは、「4. Lookerのmodelファイルにmap_viewフィールドを追記」で作成した、map_layer名を指定します。 dimensionは H3解像度 別に設定しています。 view : h3_places { sql_table_name: ` sample - project . sample - dataset .h3_places` ;; dimension : h3_hex_id_res_0 { group_label: " H3 " group_item_label: " H3 解像度0の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_0 ;; map_layer_name: h3_hex_id_res_0 } dimension : h3_hex_id_res_1 { group_label: " H3 " group_item_label: " H3 解像度1の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_1 ;; map_layer_name: h3_hex_id_res_1 } dimension : h3_hex_id_res_2 { group_label: " H3 " group_item_label: " H3 解像度2の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_2 ;; map_layer_name: h3_hex_id_res_2 } ... dimension : h3_hex_id_res_15 { group_label: " H3 " group_item_label: " H3 解像度15の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_15 ;; map_layer_name: h3_hex_id_res_15 } } 6 Lookerのmodelファイルにexploreを追加 下記のようにexploreを追加します。 explore : sales { label : " sales " ... join : h3_places { view_label: " place " type : inner sql_on: $ { sales .place_id } = $ { h3_places.place_id } ;; relationship : many_to_one } } 7. Lookerでの可視化 作成したexploreでマップでの可視化を行うと、地図上にH3六角形メッシュが表示され、メッシュ毎にバケット化された集計値を色で表現することができます。 下記は東京近郊のデータを H3解像度 7のdimensionを使用して可視化した例です(数値はランダム値です)。 今回作成した H3解像度 dimensionを変更することで、目的に合わせて六角形メッシュの大きさを変更して可視化を行うことが可能です。 まとめ 今回は、Uber社がオープンソースとして提供している H3 を使用して、Looker上で可視化を行う方法について解説しました。 タイミーでは今回紹介したLookerでの可視化以外にも、機械学習の特徴量作成時に使用するなど、様々な場面で H3 を活用しています。 今後も地理情報を活かした分析をする際に活用していきたいと考えています。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう! mermaid.initialize({startOnLoad: true});
はじめに マッチング領域、ワーキングリレーションチームの @Juju_62q です。 タイミーでは有志のメンバーが集まって1年半ほど前から輪読会を行っています。 現在5冊の書籍を読み終わっています。 ブログのタイトルにもありますが、今回 エンタープライズ アプリケーション アーキテクチャパターン (略称PoEAA)の輪読を実施しました。 書籍選定には以下のような狙いを置いていました。 複雑な ドメイン をソフトウェアで扱うための術を身につける アクティブレコードパターンにより詳しくなり、 Rails を上手く扱えるようになる 書籍に出てくる デザインパターン をきっかけとしてタイミーの アーキテクチャ について議論し、実験のきっかけを作る。 輪読会の方法 週1でmiroを利用して実施しています。 輪読会は参加者の「わからなかったこと」、「話してみたいこと」を掘り下げるような形で実施しています。 また、特別取り上げることは少ないですが、個人の感想や勉強になった場所を書けるようになっています。 「わからなかったこと」では参加者が書籍を読んでわからなかったことについて話します。 これは少し理解が曖昧な部分について、みんなで解説を行い理解を深めている様子です。 「話してみたいこと」ではタイミーの話や、ソフトウェアエンジニアリング一般で用途や使い分けについて話しています。 下記は「重ロック」の概念をタイミーのマッチングロジックに適用できないか話している様子です。 感想はそれぞれが自由に書いています。 輪読会では「参加者が少しでも参加前より理解できること」を目的に以下のような心構えを置いています。 本輪読会は読んだ人の疑問や議論したいことに基づいて意見交換をする形をとっています。したがって、参加者の協力がなければ良いものになることはありません。チームの学びを最大化するためにもあなたの思ったことを教えてください。 発言の正しさや凄さに関わらず、疑問、自分の意見を発言することに対して常にポジティブです。 ファシリテーター 及び参加者の皆さんはどんな意見も歓迎し、発言したことにまず感謝をするように努めてください。 特に今回は解釈が難しい書籍だったと思うので、「わからない」が主張しやすいことは大事だったと思っています。今後も参加者の「わからない」を大事にして運用していきたいと考えています。 学びや感想 岡野 Web Application Frameworkがよくできているというのを強く感じることができる書籍でした。 書籍の中に出てくる デザインパターン や考え方はタイミーでも実際に活かせることが多く、具体のテーブルやモデルについて議論ができるのはとても良い体験だったと思っています。 ActiveRecord と ドメイン モデルの関係とかの話が2002年の段階で整理されていたのは本当にすごいです。 難波 タイミーのバックエンドは Ruby on Rails を使っているので、この本を読むことでActive Recordの複雑さや大変さについて認識を新たにすることができたのはとても良かったです。書籍が発行された時代を感じるような課題意識や制約も多々ありますが抽象的に捉えることで現代でも参考になる部分はたくさんあったので、こういった過去の名著を一通り読んでみるのはオススメです。それにしてもLockは考えることが多くて本当に大変。 須貝 いつかは読みたいと思いながらなかなか読めずにいたPoEAAにチャレンジできて本当に良かったです。機会を作ってくれた輪読会主催メンバーに感謝しています。 書籍の中で紹介されているパターンが開発者間での共通言語となり「これは重オフラ インロック では?」といった会話がSlack上で発生することもありました。自分としてはまだまだ理解できていない部分もたくさんあるので今後も読み返していきたいと思います。 texta.fmの最初のほうのエピソードを聴くと Ruby on Rails との関係性も含めてさらに理解が深まるので本当におすすめです。自分はかなり助けられました。 終わりに PoEAAは2002年とそれなりに古い書籍ではありますが、考え方は現代にも通じる良い本です。 ここに書かれている設計パターンは Rails でもたくさん実装されています。 複雑なビジネスを扱う上で強力な武器になると思うので、みなさんぜひ一度読んでみてください。
こんにちは、Androidエンジニアのはる( @ haru2036 )とシャム( @arus4869 )です! 私たちは2023年6月8-9日にアメリカ合衆国サンフランシスコで開催された droidcon San Francisco 2023 に参加してきました。 前回のイベント報告編に引き続き、実際のセッションを紹介していきたいと思います。 tech.timee.co.jp 特に気になったセッション(haru編) Mobile Feature Flags and Experiments at Uber はじめに取り上げるのはUberのMahesh HadaさんとArun Babu A S PさんによるUberでのFeature Flag運用に関するセッションです。 Uberでは大量のFeature FlagやExperimentalなFeatureを管理するために独自のFeature Flag自動生成などの取り組みを行っているそうで、そういったFlagのFetchをするタイミングもサービスの性質上「大きく位置情報が変化したとき」など特殊なものがあるということでした(日本から渡航してきたばかりの私の端末上でもそれをトリガーとしたFlagのFetchが走ってたんだなあ、と謎の実感を持ちながら聞いていました)。 また、そういったFlagによって問題が起きた時にできる限り早くロールバックするためにFCMを用いて緊急ロールバック用メッセージを送信しているという話を聞いた時はなかなか衝撃を受けました。 タイミーでもFirebase Remote Configなどを使ったFeature Flag管理を行っているのですが、それとは桁外れに大規模で即時性の高い管理が行われていてさすがだなと思いました。 上記の緊急ロールバックの話など、世界で展開している大きなサービスならではの手法を知ることができたセッションでした。 Unlocking the Power of Shaders in Android with AGSL and Jetpack Compose 次に取り上げるのはRikin MarfatiaさんのAGSLをJetpack Composeで使うセッションです。 AGSLはAndroid上で利用できるシェーダ言語で、GPUを使用して画面を描画することができます。 私は趣味でUnityなどでシェーダを扱っていたのですが、それとよく似た記法のAGSLを用いることで視覚的にリッチなUIを実現することができるそうでした。 実際にデモとして紹介されていたUIはボタンが電球のように光ったり、写真ギャラリーの切り替え時に色収差を発生させるなどのとても派手な表現でしたが、それをCompose上で簡単に実現できることに感動しました。 AGSLについてはこちら↓ developer.android.com Reimagining text fields in Compose 最後に取り上げるのはGoogleのZach KlippensteinさんによるJetpack ComposeにおけるTextFieldの成り立ちとこれからについてのセッションです。 多くのプロダクトと同様に、MVPから始めてユーザースタディを繰り返しながら開発していったそうなのですが、これまた多くのプロダクトと同様に現在では技術的負債も貯まってきてしまっているそうです。 そこで、それを解消すべく大きくAPIを変更したBasicTextField v2を開発しているそうです。ユーザースタディ参加者も募集中だよ!とのことです。参加してみたいなあ。 GoogleでのCompose開発の舞台裏と、TextFieldの未来を同時に知ることができる一粒で二度美味しいセッションでした。 speakerdeck.com 特に気になったセッション(syam編) 次にsyamが特に気になったセッションを紹介していきます! Navigating the Unknown: Tips for Efficiently Learning a New Codebase ADAM GREENBERGさんによるセッションを紹介します。 新しいコードベースを理解し、その知識を活用するための具体的な手法と戦略について深く掘り下げたセッションでした。 ADAM GREENBERGさん は、自身の経験を基に、コードベースの理解、ドキュメンテーションの作成、そしてその知識の共有という3つの主要なステップを中心にお話ししていました。 以下にセッションで触れた3つの重要なステップについて触れます。 1. コードベースの理解 新しいプロジェクトや既存のコードベースに取り組む開発者にとって、コードベースの理解は非常に重要なステップである。コードベースの理解を促進させるためにはアーキテクチャ図の作成やデバッグツールの使用などが重要であると話していました。 2. ドキュメンテーションの作成 ドキュメンテーションをすることは、学んだこと振り返ったりを他の人々と共有するための重要なステップであると説いています。アーキテクチャ図の作成、重要な機能の説明、特定のコードスニペットの説明など必要に応じてドキュメンテーションする必要があると話していました。 3. 知識の共有 他の開発者が自分の知識を利用し、コードベースをより効果的に利用するための重要なステップです。このステップがチーム全体の生産性と効率を向上させ、個々の開発者が自身の理解を深めるのに役立つと述べました。 このセッションは、新しいコードベースを効果的に理解し、その知識を活用・共有するために大事なポイントをお話ししていました。 ちょうど僕たちのチームもオンボーディングや共通認知をとるためのREADMEを作成していることもあり、当たり前のことかもしれませんが、改めて聞くことができたのでこのセッションは有用でした。 Find your way with GoogleMap() {} 次は、 BRIAN GARDNERさんによるセッションを紹介します。 このセッションでは、その可能性を具体的に示すために、マップの表示、マーカーの追加、そしてクラスタリングといった基本的な機能がどのように簡単に実装できるのかを学びました。 特に印象的だったのは、大量のマーカーを効率的に表示するためのクラスタリング機能です。マーカーの数が増えると、地図が混雑し、ユーザーが特定のマーカーを見つけるのが難しくなります。しかし、クラスタリングを使用することで、近接するマーカーを一つのクラスタとして表示でき、地図の見やすさとパフォーマンスを向上させることができるとのことです。 また、このセッションでは、具体的な実装方法を確認するためのソースコードも提供されました。 JetpackComposeのGoogleMapを使用する際にいくつか注意点も述べていました。 クラスタリングについての注意点として、MarkerをClustering内で使用しないようにとの警告がありこれは、 IllegalStateException を引き起こす可能性があるそうです。 また、パフォーマンスを向上のために MapsInitializer.initialize(context, MapsInitializer.Renderer.LATEST) を使用して、最新のレンダラーを利用することを推奨していました。 さらに、Paparazziテストで、特にプレビューモードでの早期リターンを利用して、レイアウトを保持するためのBoxを返すと良いとのことでした。 ANR問題についても触れられていましたが、具体的な解決策は聞き取れなかったので、調べてみようかなと思います。 ちょうどGoogleMap周りをコンポーズ化している時だったので、とても有り難かったです。 セッションスライド speakerdeck.com ソースコード github.com Animating content changes with Jetpack Compose 最後に KINNERA PRIYA PUTTIさんによるセッションを紹介します! Jetpack Compose for Desktopを使ってスライドを作っており、随所にアニメーションが動くすごいプレゼンテーションでした! KINNERA PRIYA PUTTIさん は自身の実践を踏まえ、UIアニメーションをより魅力的にするための各種アニメーションテクニックについて語ってくれました。 以下にセッションで紹介された注目すべきテクニックについて触れていきます。 1. CrossFadeによるコンテンツ間の切り替え 新旧の画面や要素間のトランジションを滑らかにするCrossFadeは、ユーザーがアプリの使用中に感じる違和感を軽減するための非常に重要な要素です。彼は、これがいかに自然なユーザー体験を生むかについて語っていました。 2. animateContentSizeによるコンテンツサイズのアニメーション化 コンテンツのサイズを変更する必要がある場合、animateContentSizeを用いるとその変更がなめらかになります。これは、ユーザーが自由にコンテンツのサイズを調整できるようにするための重要なステップであると彼は指摘していました。 3. 各種UI要素に対するアニメーションの追加 リスト、詳細画面、ナビゲーションドロワー、ボトムシート、ダイアログなどのUI要素にアニメーションを追加することで、ユーザーフレンドリーなUIを実現する方法について具体的に語られました。 このプレゼンテーションは、Jetpack Composeを活用して、ユーザーにとって使いやすく、魅力的なUIアニメーションを創出するための重要なポイントを提供していました。 我々のチームもUI開発におけるアニメーションの活用に取り組んでおり、このような新たな視点やアイデアを得られることは非常に有意義でした。 まとめ 今回はセッションの内容をいくつか抜粋して紹介させて頂きました。 もしより詳しい内容や海外カンファレンスの様子に興味を持っていただけたら、カジュアル面談でお話しすることもできますので是非一度お話ししましょう! product-recruit.timee.co.jp
はじめに こんにちは、Androidエンジニアのはる( @ haru2036 )とシャム( @arus4869 )です! 私たちは2023年6月8~9日にアメリカ合衆国サンフランシスコで開催された droidcon San Francisco 2023 に参加してきました。 タイミーでは KaigiPass というカンファレンス参加補助制度があり、その海外参加第一号として私たちが参加してきた形になります。 私たちが開発・運営している タイミー は、ワーカーさんが利用するためにモバイルアプリが必要不可欠なサービスとなっており、その開発に関する知見を広く得るために参加してきました。 テック企業が集まるサンフランシスコでのカンファレンスは内容もそうですが情報の新鮮度といういう意味でも良い刺激になり、とても有意義な経験をすることができました。 会場のようす 会場となったカリフォルニア大学サンフランシスコ校は、医学分野が主になっているそうで、カンファレンスセンターのすぐ隣にリハビリセンターのようなものがあったりしました。そう言った意味ではソフトウェアのカンファレンス会場としては結構異色なのではないでしょうか。 また、国内のカンファレンスと比べてスポンサーの層が結構違うという感想を抱きました。 国内カンファレンスでは多くの場合Androidプラットフォーム上でサービスを展開している事業会社がスポンサーをしているケースが多いのですが、それと比較するとSentry, Bitrise, DataDogなどほとんどの会社が事業会社に対して開発をサポートするサービスを提供している会社だったのが興味深かったです。 https://twitter.com/arus4869/status/1666885385597898752?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1666885385597898752%7Ctwgr%5Eb44440b405611dde3ca583c63950f1096c7d8b21%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fwww.notion.so%2Ftimee%2Fd5096f7c9c0647b2a036b7393917e7b7 My first time abroad was #dcsf23 and it was really good. Thanks to the very helpful sessions and kind people! pic.twitter.com/MhtTpYF2ms — はる (@haru2036) 2023年6月10日 実際にブース出展している様々な会社のノベルティをいただきました。 droidcon sanfransiscoTシャツは入場者特典でみんなもらえるみたいです! すごく靴下が多い印象でした。 どこの会社のステッカーも良かったのですが、droidconステッカーが中でも嬉しかったです また、会場では一日を通して食事や飲み物が提供されていたのですが、全て屋外で提供されており参加者の憩いの場となっていました。私(はる)も朝は顔くらいのサイズがあるクロワッサンをかじりながらコーヒーを飲んでいました(笑) ランチの時間の屋外では見知らぬ人でも活発に話しかけて盛り上がっているところも多く、私も初めて会う方とお話しすることが多かったです。 実際のセッションの内容については後日投稿するセッション内容編をご覧ください! tech.timee.co.jp 初めての海外カンファレンスに参加してみて 今回初めて海外カンファレンスに参加してみた私たちでしたが、印象的だったのは先述のスポンサーの違いだけではありません。セッション自体の内容も、全世界でサービスを提供しているUberのような企業による実際の実装の解説や、Androidというプラットフォーム自体を提供しているGoogle自身によるフレームワーク自体についての解説、今後の予定の発表など、一次情報に直接触れている感覚がとても強いイベントでした。 また、日本でも「この中で⚪︎⚪︎使ってる方どのくらいいらっしゃいますかね?」と質問する発表者の方は多いですが、今回のイベントではさらに多くのスピーカーがそう言った質問を投げかけたりしていました。 また、結構な確率でジョークを挟んでくるスピーカーが多く、この辺りの話術は自分が発表する際にも見習いたいな、と感じました。 また、今回のトレンドとして多くセッションがあったのはJetpack Composeに関する話題で、やはり世界中のAndroid開発者が興味を持っているトピックなんだなと感じました。 おまけ せっかくサンフランシスコに行ったので、近くにあるいくつかのとても行きたかった場所に行ってきました! Computer History Museum (マウンテンビュー) 興奮のあまり建物の写真を撮り忘れました…… コンピュータに関わる人なら人生で一度はぜひ行ってもらいたいComputer History Museum。念願かなっていくことができました。そろばんや計算尺などの道具から始まり、コンピュータ科学の父と呼ばれているアラン・チューリングが解読したエニグマ暗号機の実機や、GUIを初めて搭載したコンピュータであるXerox Alto、iPhoneのご先祖様と言えなくもないNewton MessagePad, そして2021年に発売されたばかりのAI用超巨大プロセッサ、Cerebras Wafer Scale Engine 2などコンピュータの始まりから現在に至るまでのエポックメイキングなコンピュータたちが多数収蔵されていました。 周りを少し散策するだけでGoogleのロゴが入った建物だらけのエリアに迷い込んでしまうので、これぞシリコンバレー!という感じの場所でした。 Twitter X本社 当時は、この看板が外れるとは思いませんでしたが、Twitterの看板が外れる前に見ることができてよかったです! まとめ 今回の海外カンファレンスへの参加では、帰国してすぐにコードベースの改善に活かせる情報から、これからのAndroidを取り巻く情報までの幅広い知見たちだけではなく、実際にその場にいることによって得られる肌感覚のようなものや海外の開発者との新しい出会いなど、絶対にその場にいないと得られないものをたくさん持ち帰ってくることができたと思います。 また、タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非一度お話ししましょう! product-recruit.timee.co.jp
こんにちは、マッチング領域でバックエンドエンジニアをしているぽこひで ( @pokohide ) です。 前回はRails edgeでCIを回し始めた話を紹介しました。 tech.timee.co.jp 今回は、実際に弊社でCIをRails edgeで回し始めた事で見つけたエラーの例を紹介していきます。記事公開時点(2023年7月)のバージョンは下記の通りです。 $ ruby -v ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [aarch64-linux] $ rails -v Rails 7.0.6 ActiveRecord::DangerousAttributeError object_id is defined by Active Record このエラーに関する参考記事はこちらです。 euglena1215.hatenablog.jp 一部のモデルで object_id というカラム名を定義していたため以下のようなエラーが発生し、そのレコードを生成しているテストが軒並み落ちました。 ActiveRecord::DangerousAttributeError: object_id is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name. # /usr/local/bundle/gems/factory_bot-6.2.1/lib/factory_bot/decorator/new_constructor.rb:9:in `new' # /usr/local/bundle/gems/factory_bot-6.2.1/lib/factory_bot/decorator.rb:16:in `send' ... Objectクラスで、オーバーライドされると予期せぬ影響を与えうるメソッド名と同じ名前を利用できなくなったからでした。 object_id に限らず dup , freeze , hash , class , clone なども利用できなくなるのでそういったカラム名を使わないようにする必要があります。 このエラーは object_id を object_identifier とリネームする事で対応しました。 Rails7.0.5.1からの create_association の挙動変化によるエラー 巷で話題のcreate_associationの挙動変更によるエラーです。既存レコードが存在する場合にユニーク制約によりバリデーションエラーが発生するといってテストケースがありましたが、そちらのテストが落ちました。Rails edgeを回し始めた頃はRails v7.0.4だったため、このエラーに遭遇しました。 Rails v7.0.5.1から create_association の挙動が「別々のトランザクションでinsertしてからdeleteする」から「同一トランザクションでdeleteしてからinsertする」に変わりました。この影響により、DBのレコードに依存するバリデーション(例: validates_uniquness_of )が効かなくなりました。この挙動変化に関する内容はこちらをご参考ください。 blog.willnet.in parser gemを利用してcreate_associationの呼び出し箇所を調査し、影響範囲を一つずつ確認、必要な箇所に条件文を追加するなどして対応しました。 既にリリースされている変更ではありますが、Rails edgeでCIを回し始めたことで早期に問題を発見でき、create_association削除する前に検証を挟む新しいオプションの提案をrails/railsへのPR *1 を通して行うなどしました。 NoMethodError: undefined method `reading_role' for ActiveRecord::Base:Class Rails7.1から ActiveRecord::Base.reading_role がなくなるため、この記述を行っていた箇所のテストが落ちました。 irb(main): 002 : 0 > ActiveRecord :: Base .reading_role { " severity " : " WARN " , " message " : " DEPRECATION WARNING: ActiveRecord::Base.reading_role is deprecated and will be removed in Rails 7.1. \n Use `ActiveRecord.reading_role` instead. \n (called from xxxxx) " } => :reading irb(main): 003 : 0 > ActiveRecord :: Base .writing_role { " severity " : " WARN " , " message " : " DEPRECATION WARNING: ActiveRecord::Base.writing_role is deprecated and will be removed in Rails 7.1. \n Use `ActiveRecord.writing_role` instead. \n (called from xxxxx) " } => :writing この問題に関しては元々出ていたWarningの内容に従い、 ActiveRecord.reading_role を代わりに使う事で対応しました。 partialsにインスタンス変数をlocalsとして渡す挙動が削除されたことによるエラー 弊社では請求書(PDF)を生成するために ActionController::Base#render_to_string を用いてHTML文字列を取得しています。その処理の中でpartialsにインスタンス変数をlocalsとして渡していましたが、そこでエラーが発生しました。 今までWarningが出ていた内容ではありますが、Rails7.1からインスタンス変数をlocalsで渡せなくなったためです *2 。 インスタンス変数ではなくローカル変数として渡すことで対応を予定しています。 before_type_castの返り値の型が変わったことによるエラー # create_table :posts, force: true do |t| # t.integer :foo, default: 1, null: false # end class Post < ActiveRecord :: Base enum foo : { x : 1 , y : 2 } end Integer型でEnumを定義しているカラム foo を持つモデルのレコードに対して record.foo_before_type_cast で参照すると、元々の環境ではInteger型で返っていたものが、Rails edge環境下ではString型で返るようになったため落ちているテストケースを見つけました。 record = Post .new( foo : :x ) # 検証当時の環境(Rails v7.0.6) record.foo_before_type_cast => 1 # Rails edge record.foo_before_type_cast => " 1 " 今回のエラーを再現するコード *3 を用意し、 git bisect *4 を利用して二分探索で挙動が変わったをコミットを調査しました。その後のコミットやPRを追ったところ、rails v7.1からDBの型で値を取得する *_for_database が追加されていることに気づいたため、その実装をバックポートして使うように修正することで対応を行いました。 # frozen_string_literal: true case Rails :: VERSION :: STRING when /^ 7 \. 0 / # 以下の定義を読み込むために何もしない。 when /^ 7 \. 1 / # v7.1 には含まれているため読み込まない。 return else # v7.2 以降で削除し忘れないように例外を投げる。 raise ( ' Consider removing this patch ' ) end module ActiveRecord module AttributeMethods module BeforeTypeCast # refs: https://github.com/rails/rails/pull/46283 def read_attribute_for_database (attr_name) name = attr_name.to_s name = self .class.attribute_aliases[name] || name attribute_for_database(name) end end end end 最後に 今回は、Rails edgeでCIを回すことによって見つけた将来動かなくなるコードの早期発見やその対応、原因についての簡単な解説を行いました。また紹介しきれていないですが、エラーだけでなくRails 7.2からの廃止予定を知らせるWarningもいくつか確認できました。 Rails edge導入当初は112個のテストケースが落ちましたが、徐々に対応を行なっていき落ちるテストケースは22件にまで減りました。こういった活動を継続することでRubyやRailsのコミュニティの進化に追随できるので引き続き頑張っていこうと思います。 タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非お話ししましょう! product-recruit.timee.co.jp *1 : https://github.com/rails/rails/pull/48643 *2 : https://github.com/rails/rails/commit/8241178723d02123734a1efd01c12b9fda2f4fea *3 : https://gist.github.com/pokohide/b310ea180e7de0467360c96debbb8363 *4 : https://git-scm.com/docs/git-bisect
こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の小関です。 今回はDSグループがMLパイプライン構築時に活用しているVertex AI Pipelinesを効率的に開発するための取り組みを紹介したいと思います! Vertex AI Pipelinesとは Vertex AI Pipelines とは、Google Cloudが提供しているMLパイプラインをサーバーレスに構築・実行できるサービスです。 Vertex AI Pipelinesを活用することで、下記のようなデータをBigQeuryから取得し、特徴量の作成・データセットの分割後、モデルを学習するようなML パイプラインが比較的容易に構築できます。 Vertex AI Pipelinesで構築したMLパイプラインのサンプル Vertex AI Pipelinesの活用事例と挙げられた改善点 タイミーのDSグループでは、下記のようなML パイプラインをVetex AI Pipelinesで開発・運用しています。 ワーカーに対して、おすすめの募集を出力するパイプライン クライアントの離脱を予測するパイプライン 各種KPIを予測するパイプライン ML パイプラインを構築していく上で、以下のような改善点が挙げられていました。 パイプラインのリポジトリのディレクトリ構成や、CI/CDを共通化したい Google Cloud上での処理を共通化し、より使いやすい形で処理を呼び出せるようにしたい 各パイプラインに必ず入れ込む必要があるKubeflow Pipelines *1 の記述を共通化したい Vertex AI Pipelinesを効率的に開発するための取り組み 挙げられた課題に対して、DSグループでは以下の3つの取り組みを行なっています。 1. Vertex AI Pipelines開発用のテンプレートリポジトリの構築 cookiecutter を使用して、パイプライン開発に関わるディレクトリや、CI/CDに用いるyaml, shell scriptを生成してくれるテンプレートを作成しました。パイプラインの開発開始時にこのテンプレートを利用しています。 下記のようにcookiecutterコマンドでプロジェクトを生成し、プロジェクト名・プロジェクトの説明・Pythonのバージョン・作成者を入力することで、開発用のテンプレートが生成されます。 $ cookiecutter [開発用のテンプレートリポジトリのパス] > project_name [project_name]: > project_description []: > python_version [3.10.1]: > author [timee-dev]: # Vertex AI Pipelines開発用のテンプレート . ├── .github │   ├── PULL_REQUEST_TEMPLATE.md │   └── workflows │   ├── CI/CDのyamlファイル ├── .gitignore ├── Makefile ├── README.md ├── pyproject.toml ├── src │   └── pipeline_template │   ├── components │   │   └── component │   │   ├── パイプラインを構成するコンポーネントのソースコードとDockerfile │   ├── pipelines │   │   ├── パイプラインをコンパイル、実行するためのソースコード │   ├── pyproject.toml └── tests ├── テストコード 2. Google Cloudの処理を集約した社内ライブラリの構築 Google CloudのPythonライブラリ をラップして、BigQueryでのクエリ実行・クエリ結果のDataFrame化・テーブルの書き込みや、Cloud StorageにおけるファイルのI/O処理などを行える社内ライブラリを構築しています。こちらの社内ライブラリは、DSグループ全体で保守・運用を行なっており、バージョン管理とデプロイの自動化をした上で、Artifact Registryにプライベートパッケージとして置いて利用しています *2 。この社内ライブラリに関しては、Vertex AI Pipelinesに限らずVertex AI WorkbenchやCloud Runなど、他のGoogle Cloudのサービスでの実装でも活用されています。 簡単な利用例として、SQLファイルのクエリを実行して、 pd.DataFrame として取得する処理を紹介します。 # 社内ライブラリからBigQuery関連の処理をまとめているクラスをimport from [社内ライブラリ名].bigquery import BigQueryClient # project_idにGoogle Cloudのプロジェクト名、sql_dirにSQL fileを格納しているディレクトリ名を指定 bq_client = BigQueryClient(project_id=PROJECT_ID, sql_dir=SQL_DIR) # test.sql内でJinjaテンプレートで定義されているパラメーターをquery_paramsで受け取り、クエリの実行結果をpd.DataFrameとして受け取る test_df = bq_client.read_gbq_by_file( file_name= 'test.sql' , query_params= dict (loading_start_date=LOADING_START_DATE, loading_end_date=LOADING_END_DATE), ) -- SQL_DIR/test.sql DECLARE LOADING_START_DATE DEFAULT ' {{ loading_start_date }} ' ; DECLARE LOADING_END_DATE DEFAULT ' {{ loading_end_date }} ' ; SELECT * FROM `project_id.dataset_id.table_id` WHERE event_data BETWEEN LOADING_START_DATE AND LOADING_END_DATE 3. Kubeflow Pipelinesにおいて共通化できる処理を集約した社内ライブラリの構築 コンポーネント間のアーティファクトの受け渡し・Cloud StargeへのI/O処理や、yamlで定義されたコンポーネントの情報を取得してくる処理 *3 などを行える社内ライブラリを構築しています。こちらもDSグループ全体で保守・運用を行なっており、バージョン管理とデプロイの自動化をした上で、Artifact Registryにプライベートパッケージとして置いて利用しています。 利用例として、学習データを受け取り、それを特徴量とターゲットに分割するコンポーネントにおけるアーティファクトの受け渡し・Cloud StargeへのI/O処理の実装を紹介します。 # pipeline_name/components/component_name/src/main.py from dataclasses import dataclass import pandas as pd from [社内ライブラリ名].artifacts import Artifacts from [社内ライブラリ名].io import df_to_pickle @ dataclass class ComponentArguments : train_dataset_path: str @ dataclass class OutputDestinations : x_train_path: str y_train_path: str def main (args: ComponentArguments) -> pd.DataFrame: train_dataset = pd.read_pickle(args.train_dataset_path) x_cols = [ 'x1' , 'x2' ] y_col = [ 'y' ] X_train, y_train = train_dataset[x_cols], train_dataset[y_col] return X_train, y_train if __name__ == '__main__' : # アーティファクトのパスを取得 artifacts = Artifacts.from_args(ComponentArguments, OutputDestinations) # インプットとなるアーティファクトのパスを受け取り、main関数を実行 X_train, y_train = main(artifacts.component_arguments) # パイプラインのアーティファクトを管理するCloud Storageのバケットへpickle形式でX_train, y_trainを書き込む df_to_pickle(artifacts.output_destinations.x_train_path, X_train) df_to_pickle(artifacts.output_destinations.y_train_path, y_train) 取り組みから感じたメリット 上で挙げた取り組みを通じて、グループ全体で感じている主なメリットを紹介していきます。 開発のスピードが上がる。特にパイプラインのテンプレートが用意されている事で、開発の初動が大幅に速くなりました。 テンプレートやライブラリを通して、ファイル構成や処理に共通知があるので、メンバー間でのレビューがしやすくなっている。 個別に開発した処理を社内ライブラリに追加していくことで、グループ全体の資産として蓄積している。 属人化されているコードが減っていくので、新規メンバーのキャッチアップがし易くなる。 今回紹介した取り組み以外にも、MLパイプラインのソースコードのモノリポ化などDSグループでは常に社内のMLOps基盤を強固にしていく活動を続けています! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp *1 : Kubeflow Pipelines SDK で実装したパイプラインをVertex AI Pipelinesで動作させています *2 : Artifact RegistryのプライベートパッケージをPoetryで扱う方法は こちら が参考になります *3 : Kubeflow Pipelinesにおけるパイプラインの定義ファイルで使用します
DREグループの 石井 です。 先日といってももうしばらく前ですが、Techmeeというイベントで生産性についてトイルの計測をしてそれを一定に抑える取り組みをしているという話をさせて頂きました 。 https://timeedev.connpass.com/event/275750/ www.youtube.com これはDREグループ内の生産性を維持する=一定以上のアウトプットが出せる状態を作り出すための取り組みでした。 しかし、生産性といえば、古くはコード量で計測されていたような(最も、コード量は良い指標ではありませんが)ものや今で言えばfourkeysといったようなもので、どれだけアウトプットに繋がっているかが大切になります。 私の所属するDREグループでは、主に色々なデータソースからデータを抽出し、便利に分析しやすい状態にすることに責任を持つと同時に、データ基盤の運用も担当しています。 そのため、私達のグループはもとより、他の分析者の生産性を上げることに責任を持っていかないといけません。 今回はDREグループで始まったfourkeysによる生産性の計測と今後の展望について書かせていただこうと思います。 前提としてfourkeys 今回の計測に当たってはfourkeysをベースとして考えることにしました。 これについては書籍や色々な発表資料などを漁り、グループ内で勉強会などを通してある程度共通理解を得て我々の計りたいものに対してまずはこれでいってみよう、という合意が得られたためです。 我々が考える生産性については後述しますが、前提としてのfourkeysは以下の本や資料などを読んでいただけると理解が進むかと思います。 State of DevOps 2021 https://services.google.com/fh/files/misc/state-of-devops-2021.pdf (2022もありますが、フォーム入力が必要なためパブリックに公開されているこちらを貼っています)  エリート DevOps チームであることを Four Keys プロジェクトで確認する https://cloud.google.com/blog/ja/products/gcp/using-the-four-keys-to-measure-your-devops-performance 継続的デリバリーのソフトウェア工学 https://bookplus.nikkei.com/atcl/catalog/22/12/01/00531/ DREグループの考える生産性の定義 我々DREグループは社内向けのデータ基盤を提供するグループです。 つまり、生産性計測の対象が1つのチームに閉じるとは限らず、何なら複数グループの活動が全て終わって1つの機能提供が完了するというケースも多くあります。 例えば、新規データをつなぎ込みモデリングした上でダッシュボードを提供しようとするケースでは、DREグループだけでなく、モデリングを担当するBIグループが絡んできます。このとき、DREグループが担当するステージング処理までが完了したとしても価値が提供できていると言えるでしょうか?そうではなく、データの取り込み、ステージング処理、モデリング、ダッシュボード化のすべてが完了して価値を提供できていると言えると考えています。 そのため、開発はもちろんですが、その2グループの連携がスムーズに流れることも重要な要素であり、その点も改善の対象となってくるはずです。例えば、もしドキュメントが足りなくてBIグループがモデリングに入れないのであればそれをスムーズに提供するべきだし、依頼内容が曖昧でヒアリングに時間がかかりすぎるならそこも改善対象かもしれません。 つまり、スループットとしては絵にすると以下の全体で計測・改善を行う必要があると考えています。 また、単に速度だけを追求してしまうと障害を起こしまくる仕組みになってしまいかねません。 そのため、同時に安定性の指標として障害も計測する必要があるのですが、データ基盤の場合シンプルにデプロイによる失敗を計測すればよいかというとそういうことでもなく、どうしてもデータソース側の変更などにより「何もしてないのに壊れた」ということが発生することも多くあります。 そのあたりを考慮して実装を考えていく必要があります。 実装 実装は以下の様になりました。 基本的にDORAが公開してくれている実装を使い、Github上の営みをBigQuery上に収集しています。 デプロイ周りについては、 ほとんどのリポジトリはmain merge時にデプロイ自動化されているので、マージをデプロイの代替指標として採用する 障害は現在Notion上のポストモーテムDBで管理できているのでそこから引っ張ってくる ということにしています。 現時点では通常のfourkeysを取得できるようにしていますが、今後は上述した通りのリードタイムをNotion上にあるバックログなどと連携させて取得していく予定です。 考察と今後について 現時点ではほぼ素のfourkeysが取得できた状態です。 最初に計りたかったものからするとやや乖離がある状態ですが、それでもいくつかの示唆を得ることができました。 例えば、dbtのリポジトリでDREグループのリリース速度は十分早いのですが、データのモデリングを行うBIグループはかなりの時間がかかっておりなにか課題がありそうなことが特定できました。 これについてはヒアリングを行っており、実際にメンバー間でモデリングについての習熟度や前提条件の理解などの差があるために、レビューで大きな時間がかかっていることがわかりました。 また、当たり前ですがDREグループだとしても意図して大きいPRを出すとマージまでの時間が圧倒的に伸びていることがわかります。 どちらも担当ベースでは理解のあることと思いますが、具体的な数値として見えてくると議論の俎上にも上がりやすく、解決に向けて動きやすくなるのでシンプルなfour keysでも意味のある可視化なのではないかと思います。 ただ、やはりこれでは最初に書いたような課題は見えてこない部分はあるので、少しずつアップデートしていき、エンドツーエンドのfourkeysを測れるようにしてより生産性の高いデータ基盤を実現する礎にしていければと思っており、そのあたりはまたブログ等で発表できればと思っています。 おわりに 我々DREグループはデータ基盤を提供する裏方の部門ではありますが、データを使った業務の生産性を最大化するために業務に取り組んでいます。 こういった話に興味がある、少しでも気になった方はお気軽に カジュアル面談 に申し込みいただけると嬉しいです。
こんにちは、マッチング領域でバックエンドエンジニアをしているぽこひで ( @pokohide ) です。 冷やし中華はじめました的なタイトルですね。分かります。 今回はタイミーが本番運用しているRailsアプリケーションに対してRails edgeでCIを回すようになった話を紹介します。翌週には「〜見つけたエラー編(仮)〜」と題して、実際に弊社で見つけたエラーの例を紹介していきます。記事公開時点(2023年7月)のバージョンは下記の通りです。 $ ruby -v ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [aarch64-linux] $ rails -v Rails 7.0.6 弊社ではRubyもRailsも積極的に最新バージョンにあげる活動をしています。今回の記事はRailsに関してですが、Rubyのアップグレードも同様に行っています。過去にはRuby3.2にし、YJITを有効化にした記事を公開しているので興味があれば、ご一読ください。 tech.timee.co.jp Rails edgeとは? ChatGPTに聞いて楽をしてみました。 この記事ではChatGPTと同様に rails/rails のmainブランチを指すこととします。 Rails edgeは、安定版ではなく開発版なので以下のような特徴があります。 将来のリリースで利用可能になるかもしれない新機能が含まれている 正式リリースされていない既知のバグ修正が含まれている 安定版ではなく、まだ評価されていない機能やバグが含まれている可能性があるため、本番環境で使用する際には注意が必要です。GitHubでは、毎週本番環境で使うRailsをedgeにアップデートしているそうです *1 。 なぜRails edgeでCIを回すのか タイミーでは、RubyやRailsのコミュニティの進化に継続的に追随することで、高速化や機能追加などの恩恵を受け、ユーザーに最大限の価値を提供していきたいと考えています。 Rails edgeでは、将来のリリースで利用可能になる新機能を事前に検査できるため、潜在的な影響を確認することができます。また、バグや互換性の問題を早期に発見し、解決策を見つけることも可能です。その他にも、最新の機能や修正に関して何かあれば、安定版リリース前に異議申し立てをしやすいといったメリットもあります。 これらの利点を考慮し、Rails edgeでCIを回すことにしました。 Rails edgeでCIを回す rails以外のgemはそのまま利用したいため、既存のGemfileを読み込み、railsのみを rails/rails に上書きすることでRails edge用のGemfileを用意します。 # frozen_string_literal: true # 既存のGemfileを読み込む eval_gemfile File .expand_path( ' ../Gemfile ' , __dir__ ) # 上書きしたい依存関係を削除する dependencies.delete_if { |d| d.name == ' rails ' } # rails/railsのメインブランチに上書きする gem ' rails ' , branch : ' main ' , github : ' rails/rails ' 使用するgemのバージョンが変更されたので専用のlockファイルを用意する必要があります。 BUNDLE_GEMFILE を指定する事で指定したGemfileを使用できます。 $ BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install これによりRails edgeと他のGemとの依存関係を定義したファイルの用意が完了します。次にこれらのファイルを利用してCIでテストを実行する準備をしていきます。実際の設定ファイルを簡略化して紹介しています。なお、弊社ではCIツールとしてCircleCIを利用しています。 version : 2.1 orbs : ruby : circleci/ruby@2.0.1 jobs : test : parameters : gemfile : type : string default : Gemfile environment : BUNDLE_GEMFILE : << parameters.gemfile >> BUNDLE_PATH_RELATIVE_TO_CWD : true docker : - image : cimg/ruby:3.2.2 steps : - checkout - ruby/install-deps : key : gems-<< parameters.gemfile >> gemfile : << parameters.gemfile >> - run : name : Test command : bundle exec rspec workflows : version : 2 workflow : jobs : - test : filters : branches : only : - master - /^rails_edge.*$/ matrix : parameters : gemfile : - "gemfiles/rails_edge.gemfile" 今回はmasterブランチと rails_edge.* ブランチでのみ実行するようにしました。 Orb - circleci/ruby で定義されるコマンドである install-deps ではgemfileパラメータを渡すと指定したGemfileを利用してbundle installを実行してくれるため、その機構を利用しています。 また、 BUNDLE_PATH_RELATIVE_TO_CWD を利用することで、Gemfileではなくカレントディレクトリに対して相対的にパスを指定するのでGemの競合を回避でき、キャッシュを有効活用できます。 Rails edgeで落ちるテストに印をつける 後出しですが今回の目的は「 Rails edgeでCIを回す 」ことであり、全てのテストを修正することではありませんでした。そこで、今回は落ちたテストをpendingすることにしました。 skipではなくpendingを採用したのは、mainブランチの追従によりテストが成功する可能性があるためです。skipはテストの成功・失敗に限らずテストを実行しませんが、pendingはテストの失敗時のみ保留にするため修正に気づくことができます。 便宜上、これから出るであろうRails7.1で動かないこととそれまでに直すぞという意味合いも込めて以下のようなメッセージを表示するようにしています。ここは適当に変えていただくのが良いかと思います。 # frozen_string_literal: true module PendingIfRailsEdge def pending_if_rails_edge ' PENDING: Rails 7.1で動くように直す ' if Rails :: VERSION :: STRING .start_with?( ' 7.1 ' ) end end RSpec .configure do |config| config.extend PendingIfRailsEdge end 以下のように使えます。こうすることでテストコードをgrepした時に落ちているコードをすぐに見つけることができるの便利でもあります。 context ' foo ' , pending : pending_if_rails_edge do ... end it ' bar ' , pending : pending_if_rails_edge do ... end これでRails edgeでCIを回すことができるようになりました。 Rails edge用のlockファイルを追従させる masterブランチと rails_edge.* ブランチでRails edgeでCIを回す運用を開始してから、featureブランチでGemの追加や削除を行ったが rails_edge.gemfile の更新を忘れてmasterブランチにマージしたため、masterブランチでRails edgeでのCIがfailするケースが発生しました。 本番環境に影響がないとはいえmasterブランチのCIがfailしているのはモヤモヤすることや、せっかく導入した仕組みが形骸化してしまうため、masterブランチマージ前に気づける仕組みを導入しました。 具体的には BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install を実行して差分が発生すれば失敗するステップを追加し、featureブランチでも実行するようにしました。 jobs : check_outdated_gemfile : parameters : gemfile : type : string docker : - image : cimg/ruby:3.2.2 steps : - checkout - ruby/install-deps : key : gems-<< parameters.gemfile >> gemfile : << parameters.gemfile >> - run : name : re-bundle install command : bundle install - run : name : check file changes command : | if [ `git diff --name-only << parameters.gemfile >>.lock` ] ; then echo 'Please run `BUNDLE_GEMFILE=<< parameters.gemfile >> bundle install`' exit 1 else exit 0 fi 最後に 今回はRails edgeでCIを回し始めた背景や導入する方法について紹介しました。 今回の取り組みを通して、将来のRailsアップグレードにおいて遭遇するであろうバグに早期に気づき、迅速な対応ができるようになりました。まだGitHubのように *1 、 Rails edgeを毎週取り込める状態ではないですが、引き続き頑張っていこうと思います。 次回は、Rails edgeでCI回し始めたことで見つけた問題を紹介していきます。記事公開時には公式Twitter ( @TimeeDev ) でアナウンスしていくのでフォローしていただけると嬉しいです。 また、タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非お話ししましょう! product-recruit.timee.co.jp *1 : https://github.blog/2023-04-06-building-github-with-ruby-and-rails
こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の 小栗 です。 今回は、DSグループのメンバーにおすすめの本を聞いてみたのでご紹介します! *1 おすすめ本を通して、DSグループの雰囲気や、業務で活用するスキル・知識について、みなさんに伝わればいいなと考えています。 データサイエンス(DS)グループの紹介 本題に入る前に、軽くDSグループの紹介をさせてください。 DSグループは、タイミーの事業成長をデータ・アルゴリズムで支援することを目的としています。 例えば、以下のような業務を日々行なっています。 Google Cloudを利用した機械学習パイプライン&基盤の開発・運用 ユーザーへ仕事を推薦するレコメンドエンジンの開発 営業活動を支援する予測モデルの開発 ビジネス施策の効果検証 現在、専任・兼任・業務委託のメンバーで構成されており、今後も規模を拡大する予定です。 Web企業、コンサル企業、AIベンチャーなどで、データサイエンティスト/機械学習エンジニアとして経験を積んできたメンバーが在籍しています。 特に、機械学習システム設計、機械学習モデル実装、効果検証などに強みがあるメンバーが揃っています。 DSグループおすすめ本の紹介 それでは、DSグループの各メンバーに聞いた「心からおすすめできる本」を、推薦メンバーからのコメントを載せつつ紹介します。 ジャンルとしては、機械学習、因果推論、開発、組織に関する書籍を取り上げます。 機械学習のおすすめ本 Rによる統計的学習入門 Rによる 統計的学習入門 作者: Gareth James , Daniela Witten , Trevor Hastie , Robert Tibshirani 朝倉書店 Amazon メンバーからのコメント↓ カステラ本として有名な 『統計的学習の基礎』 を手掛けた著者が書いた、機械学習の入門書。 『統計的学習の基礎』は良書ではあるものの内容・分量がヘビーなので、実務家に一番最初におすすめしたいのはコレかなと思ってます。 説明の平易さが適切であり、個人的に入門書の中で一番わかりやすかったです。 ベイズ推論による機械学習入門 機械学習スタートアップシリーズ ベイズ推論による機械学習入門 作者: 須山敦志 講談社 Amazon メンバーからのコメント↓ ベイズ推論による学習・予測を丁寧に数式レベルで追うことができる良書です。 ギブスサンプリングや変分推論を理論から理解したい方におすすめしたいです。 自然言語処理の基礎 IT Text 自然言語処理の基礎 作者: 岡﨑直観 , 荒瀬由紀 , 鈴木潤 , 鶴岡慶雅 , 宮尾祐介 オーム社 Amazon メンバーからのコメント↓ 自然言語処理の基礎から始まり、近年の深層学習ベースの手法まで丁寧に解説した本。 Transformer、BERT、GPTなど、大規模言語モデルの興隆を支える新しい技術の解説もあり、和書としては貴重です。 推薦システム実践入門 推薦システム実践入門 ―仕事で使える導入ガイド 作者: 風間 正弘 , 飯塚 洸二郎 , 松村 優也 オライリージャパン Amazon メンバーからのコメント↓ 推薦システムについて、アルゴリズムの解説に留まらず、企画段階やデザインまで幅広く扱った実践的な一冊です。 本書を教材にDSグループで勉強会を開くなど、実務で重宝しました! 因果推論のおすすめ本 効果検証入門 効果検証入門〜正しい比較のための因果推論/計量経済学の基礎 作者: 安井 翔太 技術評論社 Amazon メンバーからのコメント↓ RCTだけでなく、観察データを用いた因果推論の手法をわかりやすく解説した、実務家向けの一冊。 DSグループではビジネス施策の効果検証を行うことも多く、仕事をする上で基礎になっています。 DSグループと同じ部署に属するBIグループも勉強会で本書を使っていたので、一緒にわいわい学びました。 統計的因果推論の理論と実装 統計的因果推論の理論と実装 Wonderful R 作者: 高橋将宜 , 石田基広 , 市川太祐 , 高橋康介 , 高柳慎一 , 福島真太朗 , 松浦健太郎 共立出版 Amazon メンバーからのコメント↓ 図表を交えた解説がわかりやすい、理論と実装のバランスが良いなど、非常に洗練された書籍です。 『効果検証入門』では深く説明されなかった手法も解説されており、併せて読むのがおすすめです。 開発系のおすすめ本 ロバストPython ロバストPython ―クリーンで保守しやすいコードを書く 作者: Patrick Viafore オーム社 Amazon メンバーからのコメント↓ Pythonで保守しやすい、堅牢なコードを書くための情報がよくまとまっています。 機械学習モデリングやデータ処理を含む実験コードを本番環境に乗せていく際に、本書や 『リーダブルコード』 に立ち返って開発やレビューすることを意識しています。 単体テストの考え方/使い方 単体テストの考え方/使い方 作者: Vladimir Khorikov マイナビ出版 Amazon メンバーからのコメント↓ 単体テストの考え方を網羅的かつ深くまとめた書籍。 DSグループでは(アドホック分析のコード以外は)テストを実装することにしているため、価値のあるテストケースをつくるため手元に置いている一冊です。 データマネジメントが30分でわかる本 データマネジメントが30分でわかる本 作者: ゆずたそ , はせりょ , 株式会社風音屋 Amazon メンバーからのコメント↓ 中身も見た目もヘビーすぎる DMBOK を独自に要約してまとめた本。 DMBOKの本質が簡潔にまとめられており、(大半のケースでは)こちらを参照することで問題を解決できる気がしています。 ソフトウェア見積り 人月の暗黙知を解き明かす ソフトウェア見積り 人月の暗黙知を解き明かす 作者: スティーブ マコネル 日経BP Amazon メンバーからのコメント↓ 有名な「不確実性コーン(プロジェクトの進行に伴って不確実性が減少することを表した図)」の初出本。 ソフトウェア開発における見積もりに関して、本質的な視点を提供してくれます。 開発に関わるすべての人におすすめできる書籍です。 組織系のおすすめ本 エンジニアリング組織論への招待 エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング 作者: 広木 大地 技術評論社 Amazon メンバーからのコメント↓ エンジニアリング組織における課題の解決方法についてまとめられた本。 アジャイル的な思考や、不確実性の考え方はエンジニアだけでなくデータサイエンティストにとっても重要なので、おすすめです。 恐れのない組織 恐れのない組織――「心理的安全性」が学習・イノベーション・成長をもたらす 作者: エイミー・C・エドモンドソン , 村瀬俊朗 英治出版 Amazon メンバーからのコメント↓ 「心理的安全性」の提唱者であるエドモンドソン博士が、心理的安全性が組織にもたらす影響についてまとめた本。 関連書籍は今たくさんあるのですが、これ一冊読めば心理的安全性のコアがわかり、応用が効くと思っています。 データ統括部では 心理的安全性勉強会 を開催するなど、文化の浸透をはかっています。 We’re Hiring! タイミーのデータ統括部では、こういったおすすめ本の情報が日々Slackに飛び交っていたり、勉強会を定期的に行なっていたりします。 学習意欲や好奇心のある人にとって嬉しい環境ではないかと、(手前味噌ながら)いちメンバーとして思っています。 タイミーでは、データサイエンティストやエンジニアをはじめ、一緒に働くメンバーを募集しています! product-recruit.timee.co.jp *1 : 先日、データ統括部メンバーのおすすめ本も note で紹介しましたので、興味があればご一読ください。
はじめに 初めまして、タイミーのDREチーム(Data Reliability Engineering Team)でエンジニアをしてます、筑紫です。 今回DREチームで実施した合宿ついてご紹介させて頂こうと思います。 DREのカルチャーを少しでも知って頂けたら嬉しいです。 DREチームについて紹介 DREチームでは、社内の様々データを集約し、クレンジングを行い、社内外で利活用できる形で提供するためのデータ基盤プロダクトの開発・運用を行なっております。 データ基盤の詳細については、ProductOwner(以降POと記載)の土川の記事をご参照ください。 tech.timee.co.jp 今年の4月に私を含め2人入社したことでメンバーの入れ変えもあり、データ基盤の開発体制が新しくなりました。 メンバーが大きく変わったこともあり、開発を進める上で今までのデータ基盤の歴史的背景や方向性の理解にメンバー間で差分があることが課題になっていました。 DREチームのMissionと合宿の目的 DREではチームとしてのMissionを定めています。 ここでいうMissionとは、ビジネスシーンでよく用いられる組織やチームのMVV(Mission・Vision・Value)のMissionで、組織やチームの存在意義、果たすべき使命を指します。 Missionを定めて、チームとしての明確な目標をチームの共通言語にすることで、チームの役割や責任範囲を明確化し、成果の向上や成功につながるデータ基盤プロダクトの開発を効率的に進めることができます。 また、Missionは外部とのコミュニケーションをスムーズにし、チームのモチベーションを高める要素ともなります。 元々のMissionは以下の通りです。 『信頼性の高いデータ基盤を整備し、活用のための環境を提供する』 ただ、Missionを定義した頃から時間も経っていること、またチーム構成が大きく変わったこともあり、アップデートしたい機運が高まっていました。 今のチームでMissionを再検討することで、上述の課題も含め解消できるのではという話になり、DREチームのMissionを決めるワークショップを開催することになりました。 合宿の内容 普段はリモートワークが多いチームですが、合宿は1日だけ東京オフィスに集まり、会議室を貸し切って実施することになりました。 Missionを決めるにあたって、まずは、DREチームの存在意義についてメンバーそれぞれポストイットで意見を出し合い、それを議論しながらクラスタリングしました。 その結果を基に、Missionの方向性を導き出す形で進めました。 ただ、ポストイットの結果からMissionという形で、抽象度高いフレーズを抽出することに苦戦しました。 重要視する要素については、メンバー間で認識が概ね揃っていたものの最終的にMissionという形でどう表現するかに難儀しました。 議論中で、元々のMissionに入っていた、”信頼性”というワードを採用することになりました。 タイミーのDREチームにおける“信頼性”とは、スピード、品質、安定性、ユーザビリティを総合的に表現したものであり、ユーザがデータを利活用する上で、DREチームではこれらの要素を特に重視しています。 特にスピードについては、データが生成されてから活用されるまでの時間を短縮していきたいというPOの思いがあり、また、これからリアルタイム性を求められる要求に対応していくためにも温度感の高い指標になってきています。 また、高い”信頼性”を達成するための手段として、DataOpsという観点を導入することになりました。 DataOpsは、ガートナー社が提唱している概念で、データパイプラインの構築、自動化、監視、デプロイメントの迅速化、データ品質の向上などを重視することで、組織のデータ管理者と利用者の間の連携促進し、データの収集、処理、分析、展開のプロセスを効率化するためのプラクティスです。 このプラクティスを用いて、”信頼性”あるデータ基盤を構築していくことをMissionとすることになりました。 その後議論が進み、最終的に以下のMissionに決まりました。 『DataOpsを実現し、信頼性の高いデータ基盤プロダクトを提供する』 まとめ 日頃このようなチームの方向性などを深く話を機会が少ないので、とても貴重な時間を過ごせました。 チーム内でMissionを定めることができ、同じ方向を向いてプロダクト開発を進めていけそうで、メンバー間での満足度も高く、良かったと思います。 また、それ以上にチームで議論しながら、データ基盤プロダクトの構想や方向性をPO+開発メンバー間で共有できたという、その過程にとても価値がある会だったと思います。 今回定めたMissionを持ってデータ基盤プロダクトの成長を加速させていきたいと思ってます。 最後に、タイミーではエンジニア・データサイエンティストを初め、様々な職種のメンバーを募集してます! product-recruit.timee.co.jp
こんにちは。2023年1月に株式会社タイミーに入社したバックエンドエンジニアの id:euglena1215 です。 RubyKaigi 2023 がとうとう明日に迫ってきました。楽しみですね。 タイミーは RubyKaigi で初めてブース出展を行います。至らぬ点もあるかと思いますが、RubyKaigi を一緒に盛り上げていければと思っています!どうぞよろしくお願いします。 今回はタイミーが本番運用している Rails アプリケーションに対して Ruby 3.2.2 へのアップデートと YJIT の有効化を行い、パフォーマンスが大きく改善したことを紹介します。 RubyKaigi で「Ruby 3.2+YJIT 本番運用カンパニーです」と言いたいので粛々と進めている — Shintani Teppei (@euglena1215) 2023年4月19日 前提 タイミーを支えるバックエンドの Web API は多くのケースで Ruby の実行よりも DB がボトルネックの一般的な Rails アプリケーションです。JSON への serialize は active_model_serializers を利用しています。 今回の集計では API リクエストへのパフォーマンス影響のみを集計し、Sidekiq, Rake タスクといった非同期で実行される処理は集計の対象外としています。 今回は Ruby 3.1.2 から Ruby 3.2.2 へのアップデートと YJIT 有効化を同時に行いパフォーマンスの変化を確認しました。そのため、パフォーマンスの変化には Ruby のバージョンアップによる最適化と YJIT 有効化による最適化の両方の影響があると考えられますがご容赦ください。 結果 以下のグラフは API リクエスト全体のレスポンスタイムの 50-percentile です。アップデート前後でレスポンスタイムが 約10%高速化されている ことが確認できました。 API リクエスト全体のレスポンスタイムの 50-percentile リクエスト全体としては大きく高速化されていることが確認できました。それでは、レスポンスが遅く、時間当たりのリクエスト数が多いアプリケーションの負荷の多く占めるエンドポイントではどうでしょうか? そこで、タイミーの Web API のうち2番目に合計の処理時間 *1 が長いエンドポイントへのパフォーマンス影響を確認しました。 *2 以下のグラフは2番目に合計の処理時間が長いエンドポイントのレスポンスタイムの 50-percentile です。Ruby アップデートの数日前に GW 中の負荷対策のためのスケールアウトを実施したことで変化が少し分かりにくくなっていますが、Ruby 3.1.2 から Ruby 3.2.2+YJIT にしたことによって 10%以上高速化されている ことが確認できました。 2番目に合計の処理時間が長いエンドポイントのレスポンスタイムの 50-percentile 元々十分に高速なエンドポイントだけでなく、アプリケーション負荷の多くを占めていたエンドポイントのパフォーマンスも改善されていることが分かります。Ruby 3.2 アップデート+YJIT 有効化はパフォーマンスチューニングへの十分実用的な打ち手と言えるのではないでしょうか。 まとめ Ruby 3.1.2 から Ruby 3.2.2 へのアップデートと YJIT を有効にしたことでリクエスト全体のレスポンスタイムの 50-percentile が約10%高速化されました。また、アプリケーション負荷の多くを占めていたエンドポイントも同様に10%以上高速化されていることを確認できました。 Ruby 3.2’s YJIT is Production-Ready でも YJIT によって Shopify が 5~10% 高速化されたと記されていることから、「Ruby 3.2 の YJIT は一般的な Rails アプリケーションを 10%程度高速化させる」と考えて良いのではないかと思っています。 これほどの高速化に尽力していただいた Ruby コミッターのみなさん、本当にありがとうございました。 余談ではありますが、自社で YJIT を有効化したことによって YJIT という技術がより自分ごとになり、内部で何が行われているのかをきちんと理解したいと思うようになりました。 RubyKaigi 2023 で理解を深めようと思います。 宣伝 冒頭で説明したように、タイミーは RubyKaigi 2023 でブース出展を行います。今回の記事ではパフォーマンス改善の結果のみ紹介しましたが、タイミーでのこういった技術改善における取り組み方など話したいことは色々あるので、ぜひブースでお話しさせてください! また、RubyKaigi 後 5/16(火)にはスポンサー振り返り会を Qiita さん、Wantedly さんと実施予定です。 「自分の会社も RubyKaigi スポンサーをしてほしいと思っているエンジニア」をターゲットにしたちょっとニッチな会ですが、もしかして自分ターゲットかも…?と思う方はぜひご参加ください! wantedly.connpass.com *1 : 合計の処理時間 = 平均レスポンスタイム x hits数 *2 : 最も合計の処理時間が長いエンドポイントはGWの繁閑の影響でレスポンスタイムに変化が生じ、比較が困難であったため除外しています。
こんにちは。2022年11月に株式会社タイミーに入社した sinsoku です。 最近は GitHub Actionsの YAML を書く機会が多く、 YAML も複雑化してきました。 しかし、日常的に YAML を触っている職人以外にはパッと読めないことも多いので、社内の方々が読めるように GitHub Actionsの YAML の書き方をまとめたいと思います。 目次 三項演算子 環境変数(env) 変数(outputs) 関数(workflow_call) 関数 + 配列(dynamic matrix) GitHub CLIの活用 まとめ 三項演算子 GitHub Actions には 三項演算子 がないため、代わりに論理 演算子 を使います。 - steps : - run : echo "${{ (github.ref == 'refs/heads/main' && 'production') || 'staging' }}" 参考: Expressions 環境変数 (env) 環境変数 を使いたい場合は env で定義します。 env : TIMEE_CEO : ryo TIMEE_CTO : kameike jobs : job-env : runs-on : ubuntu-latest steps : - run : echo $TIMEE_CEO # この実装だと置換後の文字列 `echo kameike` を実行する - run : echo ${{ env.TIMEE_CTO }} ただし、 env で env の値を指定するとエラーになるケースがあるので注意してください。 env : DEPLOY_ENV : ${{ (github.ref == 'refs/heads/main' && 'production' ) || 'staging' }} # Unrecognized named-value: 'env'. Located at position 1 within expression: env.DEPLOY_ENV == 'production' IS_PROD : ${{ env.DEPLOY_ENV == 'production' }} jobs.<job_id>.env でも同様のエラーが出ます。 jobs : job-error : runs-on : ubuntu-latest # Unrecognized named-value: 'env'. Located at position 1 within expression: env.DEPLOY_ENV == 'production' IS_PROD : ${{ env.DEPLOY_ENV == 'production' }} jobs.<job_id>.steps[*].env であればエラーになりませんが、同じ階層の env の値は参照できません。 env : TIMEE_CTO : kameike jobs : job-env : runs-on : ubuntu-latest steps : # この実装だと `echo "true, foo, -bar"` を実行する - run : echo "${{ env.IS_KAMEIKE }}, ${{ env.FOO }}, ${{ env.BAR }}" env : IS_KAMEIKE : ${{ env.TIMEE_CTO == 'kameike' }} FOO : foo BAR : ${{ env.FOO }}-bar 参考: Workflow syntax for GitHub Actions 変数(outputs) 汎用的な名前の 環境変数 を定義すると、何かの CLI コマンドに影響する可能性があります。 これを避けるために、outputs で変数を定義することもできます。 steps : - id : var run : | echo "x=foo" >> "$GITHUB_OUTPUT" echo "y=bar" >> "$GITHUB_OUTPUT" # この実装だと `echo "foo, bar"` を実行する - run : echo "${{ steps.var.outputs.x }}, ${{ steps.var.outputs.y }}" outputs は env と違い、 bash の処理結果を変数に定義することができます。 steps : - uses : actions/checkout@v3 - id : var run : | echo "terraform-version=`cat .terraform-version`" >> "$GITHUB_OUTPUT" # この実装だと `echo "1.4.4"` を実行する - run : echo "${{ steps.var.outputs.terraform-version }}" 参考: Defining outputs for jobs 関数(workflow_call) ワークフローの一部を別のファイルに定義し、関数のように呼び出すことができます。 # .github/workflows/_say.yml name : say on : workflow_call : inputs : name : required : true type : string jobs : hello : runs-on : ubuntu-latest steps : - run : echo "Hello, ${{ inputs.name }}." bye : needs : hello runs-on : ubuntu-latest steps : - run : echo "Bye, ${{ inputs.name }}." 使い方は以下の通りです。 jobs : job-say : uses : ./.github/workflows/_say.yml with : name : kameike 参考: Reusing workflows 関数 + 配列(dynamic matrix) workflows_call の入力には真偽値、数字、文字列しか使えません。 The value of this parameter is a string specifying the data type of the input. This must be one of: boolean, number, or string. 引用: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callinputsinput_idtype しかし、少し工夫することで配列を扱うことができます。 # .github/workflows/_say_multi.yml name : say_multi on : workflow_call : inputs : names : required : true type : string jobs : matrix : runs-on : ubuntu-latest outputs : names : ${{ steps.set-matrix.outputs.names }} steps : - id : set-matrix run : | names=`echo '${{ inputs.names }}' | jq -csR 'split("\\n") | map(select(. != ""))' ` echo "names=$names" >> $GITHUB_OUTPUT hello : needs : matrix runs-on : ubuntu-latest strategy : matrix : name : ${{ fromJSON(needs.matrix.outputs.names) }} steps : - run : echo "Hello, ${{ matrix.name }}." 使い方は以下の通りです。 job-say-multi : uses : ./.github/workflows/_say_multi.yml with : names : | ryo kameike GitHub CLI の活用 GitHub API を使うことで、チェックアウトせずにファイル名を取得することができます。 job-gh-matrix : runs-on : ubuntu-latest outputs : files : ${{ steps.set-matrix.outputs.files }} steps : - id : set-matrix run : echo "files=`gh api '/repos/{owner}/{repo}/contents/.github/workflows?ref=${{ github.sha }}' --jq '[.[].name]'`" >> $GITHUB_OUTPUT env : GH_REPO : ${{ github.repository }} GH_TOKEN : ${{ github.token }} job-gh-echo : needs : job-gh-matrix runs-on : ubuntu-latest strategy : matrix : file : ${{ fromJSON(needs.job-gh-matrix.outputs.files) }} steps : - run : echo "${{ matrix.file }}" 画像のようにファイル名の一覧で並列にJobを実行できます。 まとめ YAML むずかしいですね。 この記事が読んだ方の参考になれば幸いです。 また、弊社の GitHub Actionsやデプロイフローについて気になることがあれば、フッターの採用ページのURLから面談の申し込みをどうぞ!
こんにちは、フロントエンドエンジニアの樫福です。 タイミーのフロントエンドの開発に関わるエンジニアの人数が増えてきました。大人数で開発しながら品質を高い状態に保つには、品質に対する共通認識を作ることが大切です。 このたび、チームでフロイントエンドの 単体テスト についての勉強会を開催しました。 testing-library というフロントエンドのテストに使うライブラリを例に挙げ、具体的な手法よりも、テストを実装する前に抑えておきたい思想についてフォーカスしました。 フロントエンドでテストしたい項目 フロントエンドの単体テストを難しくする要因 testing-library を使って壊れにくいテストを作る方法 要素を見つけるクエリ ユーザの動作をシミュレーションする user-event 要素の状態を検査する jest-dom 実際にテストを書いてみる テストが書けないケース 運用するときの注意点 単体テストを作る目的を明確にする アクセシビリティのガイドラインを定める おわりに フロントエンドでテストしたい項目 フロントエンドでテストしたい項目には次のようなものがあると思います。 ユーティリティ関数や ビジネスロジック が正しく実装されていること 意図しない見た目の変更が起こらないこと ユーザの動作に対して期待した正しい応答が返ってくること ユーティリティ関数や ビジネスロジック は関数やクラスとして切り出し、それらに対しての 単体テスト を書くことになります。基本的には入力に対する出力を確認する出力値ベーステストでテストします。クラスで実装する場合にはアクションに伴って変更した状態を検査する状態ベーステストも併せて使います。これらはフロントエンド以外でも扱うトピックです。 意図しない見た目の変更が起こらないことをテストする場合は Visual Regression Test (VRT) を使います。 reg-suit などのツールを使って、変更ごとの画像のキャプチャを撮って比較します。 ユーザがボタンをクリックする、などの動作のテストは 単体テスト で実装することができます。 testing-library というライブラリを使ってユーザの動作をシミュレーションすることで、 UI コンポーネント の挙動を検査します。画面上の表示に対しては状態ベーステストを、 API にリク エス トを送ることのテストはコミュニケーションベーステストを実装することになります。 テストトロフィーとそれぞれのテストで検査できること *1 勉強会では 単体テスト におけるフロントエンド特有の難しさについて扱いたいので、3つ目のような「ユーザの画面上に表示された要素に対する動作を起点とするようなテスト」に注目しました。 フロントエンドの 単体テスト を難しくする要因 一般的なソフトウェアと比較してフロントエンドにおいて特筆すべき点に次のようなものがあります。 ユーザが要素を取得する ユーザが(マウスやキーボードの操作などの)アクションを実行する 要素が変化したことを検査する ユーザのアクションなどに応じて API リク エス トをする フロントエンドのテストが壊れやすかったり理解するのが難しくなったりする場合、とくに、要素の取得をシミュレートすることに躓いていることが多いように感じています。 testing-library を使って壊れにくいテストを作る方法 testing-library はフロントエンドのテストに使うツール群です。 testing-library には次のような基本方針があります。 The more your tests resemble the way your software is used, the more confidence they can give you. testing-library.com そのソフトウェアが実際に使われる姿に似ているほど、テストの信頼性が高くなります。 testing-library は、ソフトウェアの使われ方に似たテストを作れるような機能を提供してくれています。ライブラリを使ってテストを実装するときには、この考え方に則ってテストを書くように心がけましょう。 以下では、 testing-library が提供している機能を3つ紹介します。 要素を見つけるクエリ 一つ目は、ページに表示されている要素を見つけるクエリです。要素に対して動作をする場合も、要素の状態を検査する場合も、まずは要素を見つけることから始まります。 testing-library.com クエリには優先度があり、なるべく優先度が高いものを使うことが推奨されています。たとえば、 getByRole は優先度が高く、 getByPlaceholderText は優先度が低いです。これらはどのように決められているのでしょうか。 たとえば、ラベルが『生年月日』で プレースホルダ ーが "2023-03-27" であるような入力要素を見つけるクエリを作ってみます。次の2種類の実装のいずれも想定通りに動きました。 // 1. ラベルが『生年月日』である入力要素を取得する screen.getByRole ( "textbox" , { name: "生年月日" } ); // 2. プレースホルダーが "2023-03-27" であるような入力要素を見つける screen.getByPlaceholderText ( "2023-03-27" ); 1, のクエリは「ラベルが『生年月日』である入力要素」を取得しています。2. のクエリは「 プレースホルダ ーが "2023-03-27" であるような入力要素」を取得しています。ユーザがアプリケーションを使用する際、おそらく 1. のクエリのような考え方で要素を認識するでしょう。 getByRole が getByPlaceholderText よりも優先度が高い理由は、このようなユーザの考え方を反映しやすい傾向にあるからです。 必ずしも優先度が高いクエリを使うことが良いわけではないですが、なるべくユーザの考え方を反映させて要素を取得するクエリを書くことはよいテストを作りに欠かせないと思います。 ユーザの動作をシミュレーションする user-event ユーザの動作をシミュレーションするために user-event というライブラリを提供しています。 testing-library.com 特定の要素をクリックしたり、キーボードで入力したりをシミュレーションする際は次のように実装します。 // ユーザオブジェクトの生成 const user = userEvent.setup (); // ボタンをクリックする await user.click ( screen.getByRole ( "button" )); // 入力要素に「こんにちは」と入力する await user. type( screen.getByRole ( "textbox" ), "こんにちは" ); user.click を実行すると、実際に要素をクリックしたときと同じようにイベントが発火します。これによって、実装者はボタンをクリックしたときに裏側でどのような挙動が取られるかを気にすることなくテストを実装することができます。 同じようにユーザの動作をシミュレーションする方法に、同じく testing-library の fire-event を使った実装があります。こちらは、ユーザの動作そのものではなく DOM のイベントを発火させる機能を持っています。 user-event と fireEvent ではどちらを使うべきでしょうか。 ユーザがアプリケーションを使う場合、「このボタン要素の click イベントを発火させよう」と考えるのではなく「このボタン要素をクリックしよう」と思って使うはずです。したがって、テストをソフトウェアが使われる姿に似せるという観点において、 user-event を使うことが推奨されています。ただし、 user-event が再現できていないブラウザの挙動もいくつか存在します。そのような挙動に対してテストを書きたいときには fireEvent を使うといいでしょう *2 。 要素の状態を検査する jest-dom 要素の状態を検査する機能として、 jest-dom というライブラリが提供されています。 testing-library.com jest のマッチャーを追加して、確認(AAA パターン *3 における Assert)をしやすくしてくれます。 提供している関数の一覧 を見るとよくわかりますが、直感的に状態を確認できるようになっています。 たとえば、要素がフォーカスされていることをテストする場合は次のように実装します。要素がどういう状態のときにフォーカスされているかという実装の詳細には立ち入らず、要素がフォーカスされていることをユーザが認識するのと同じように、テストが実装されていることがわかります。 const inputElement = screen.getByRole ( "textbox" ); expect ( inputElement ) .toHaveFocus (); 実際にテストを書いてみる 具体的なテストの例を見てみましょう。次のような UI コンポーネント に対してテストを書いてみます。 この コンポーネント は次のような仕様を満たします。 ラベル『ユーザ名』『ニックネーム』『生年月日』の入力要素がある 入力して『送信』ボタンをクリックすると、 props で渡す submit 関数が呼ばれる。入力値はその引数として与えられる 『ユーザ名』『生年月日』は必須項目である。空文字列のまま『送信』ボタンをクリックすると submit が呼ばれず、アラートメッセージが表示される 次のような二つのテストを実装します。 すべての入力要素にデータを入力し『送信』ボタンをクリックすると、データが送信されること 『ユーザ名』だけ空文字列にして『送信』ボタンをクリックすると、データが送信されずにアラートメッセージが表示されること まずは、すべての入力要素に値を入力し、 submit 関数が呼び出されていることを確認するテストを実装してみます。 test ( "入力したデータが送信される" , async () => { // 準備フェーズ const mockSubmit = jest.fn (); render (< UserForm submit = { mockSubmit } / >); const user = userEvent.setup (); // 実行フェーズ await user. type( screen.getByRole ( "textbox" , { name: "ユーザ名" } ), "太郎" ); await user. type( screen.getByRole ( "textbox" , { name: "ニックネーム" } ), "たろちゃん" ); await user. type( screen.getByRole ( "textbox" , { name: "生年月日" } ), "2023-03-27" ); await user.click ( screen.getByRole ( "button" , { name: "送信" } )); // 確認フェーズ expect ( mockSubmit ) .toBeCalledWith ( { name: "太郎" , nickname: "たろちゃん" , "birthday" : "2023-03-16" } ); } ); 実行フェーズの await user.type(screen.getByRole("textbox", { name: "ユーザ名" }), "太郎"); は、「アクセシブルな名前が『ユーザ名』であるような入力要素に "太郎" と入力する」という意味です。 確認フェーズでは、 submit が呼ばれていることと、その引数として渡されるオブジェクトを検査しています。 次に、『ユーザ名』の入力要素に値を入力せずに送信ボタンをクリックした場合のテストを実装します。このとき、 submit 関数が呼び出されず、アラートメッセージが表示されることを確認したいです。 test ( "ユーザ名の入力がないと、データが送信されない" , async () => { // 準備フェーズ const mockSubmit = jest.fn (); render (< UserForm submit = { mockSubmit } / >); const user = userEvent.setup (); // 実行フェーズ await user. type( screen.getByRole ( "textbox" , { name: "ニックネーム" } ), "たろちゃん" ); await user. type( screen.getByRole ( "textbox" , { name: "生年月日" } ), "2023-03-27" ); await user.click ( screen.getByRole ( "button" , { name: "送信" } )); // 確認フェーズ expect ( mockSubmit ) .not.toBeCalled (); const alertTextBox = await screen.queryByRole ( "alert" ); expect ( alertTextBox ) .toHaveTextContent ( "ユーザ名が入力されていません。" ); } ); 前のテストと比べて、実行フェーズにおけるはアクセシブルな名前が『ユーザ名』である入力要素への入力がなくなりました。 確認フェーズでは、 submit が呼ばれなくなったことを確認しています。また、アラートが表示され、そのテキストについても検査しています。 いずれのテストも、ユーザがアプリケーションを使う際の使用方法や状態を観測する方法がそのままテストに反映されているのがわかると思います *4 。 このようなテストは、要素の順番が入れ替わったり一部の属性が変わったりしても影響を受けにくく、人にとって理解もしやすいです。なるべくシンプルなテストを実装できるように、ぜひ testing-library の提供している API を眺めて使い方を考えてみてください。 テストが書けないケース もし、テストを書いた UI が次のような マークアップ で実装されているとどうでしょうか。 < p > ユーザ名 </ p > < input id = "name" /> < p > ニックネーム </ p > < input id = "nickname" /> < p > 生年月日 </ p > < input id = "birthday" /> < div > 送信 </ div > この状態だと先ほどのテストを実行することはできなくなります。たとえば、『ユーザ名』という文字列は id="name" である入力要素のアクセシブルな名前として認識されませんし、『送信』と書かれている要素はボタンとして認識されません。 この実装は、テスト容易性が低いという以前にマシンリーダビリティが低い状態にあります。マシンリーダビリティとは、機械にとってのコンテンツの読み取りやすさの度合いです。フロントエンドのテストは、機械がコンテンツを読み取ったり操作をしたりしてソフトウェアの動作をシミュレーションするという性質上、マシンリーダビリティが高いほうがテスト容易性が高くなる傾向にある。 フロントエンドのテストの導入の前に、マシンリーダビリティ(ひいては アクセシビリティ )の向上を目指すとよいと思います。先ほどの実装は、たとえば、次のように修正することでマシンリーダビリティもテスト容易性も高まります。 < label for = "name" > ユーザ名 </ label > < input id = "name" /> < label for = "nickname" > ニックネーム </ label > < input id = "nickname" /> < label for = "birthday" > 生年月日 </ label > < input id = "birthday" /> < button > 送信 </ button > 運用するときの注意点 テストはソフトウェアの品質の向上になくてはならないですが、それ自体がソフトウェアの品質を向上させる魔法ではありません。 ソフトウェアの品質を上げられるようなテストを実装するために、次のようなことを事前に決めておくとよいです。 単体テスト を作る目的を明確にする バリデーションを含む入力フォームの コンポーネント 、特定の API を叩く コンポーネント 、他のページへの導線がある コンポーネント などなど、多種多様な コンポーネント ごとに必要になるテストは異なります。 単体テスト を作る目的は、ソフトウェアの退行に対する保護や リファクタリング への耐性を与えて、ソフトウェア開発プロジェクトの成長を持続可能にすることです *5 。しかし、何をもってして「持続可能である」と主張するかは人によって変わります。 どれだけのシナリオをカバーするテストを実装するか ソフトウェアがあるシナリオで仕様通りに動作することを検査するにはどのようなテストがよいか どういう状態のとき、"よいテストである"といえるか これらは、チームの思想や対象となるソフトウェアによって回答は様々でしょう。 カバレッジ を上げることを目的としてテストが大量に実装しても、これらを軽視してしまうと技術的負債になってもったいなんです。テストに取り組む前に、テストを実装する目的をしっかり考えられるとよいテストの実装ができると思います。もちろん、これらのことはフロントエンド以外のテストでも同じです。 アクセシビリティ の ガイドライン を定める 前述のとおり、マシンリーダビリティが高いほど testing-library を使ったテストが実装しやすくなります。 testing-library を使ったテストの品質を高めるためには、 アクセシビリティ の向上が必須と言ってよいでしょう *6 。 ただし、テストの品質の向上のためだけに アクセシビリティ の向上を目指すと、本当にユーザによってアクセシブルなソフトウェアになるとは限りません。 アクセシビリティ を向上させる目的がわからなくなってしまっては本末転倒です。 たとえば、 img タグに alt 属性を付与すると、 getByAltText を使って要素を取得することができます。一方、 aria-label を付与すると、 getByRole を使ったクエリで要素を取得することができるようになります。テストだけを考えると getByRole のほうがクエリの優先度が高いので aria-label 属性を付与するほうがよいように感じます。しかし、 alt と aria-label では挙動がことなり、一概に aria-label を使うことがよいとは言えません *7 。テストはあくまで内部品質の向上のために実装されるもので、内部品質の向上ために外部品質を棄損するのは避けたほうがよいです。 alt 属性を使った実装のほうがユーザ体験がよくなると判断したなら、 testing-library のクエリの優先度は無視して実装をするべきです。 まずはテストを気にせずに、 アクセシビリティ の ガイドライン を定めるのがよいでしょう。そして、制定された ガイドライン をもとにした実装に対するテストの実装方法について検討します。 もし、 ガイドライン に沿った実装ではテストを実装しづらいと感じるならば、ユーザへの影響がない範囲で ガイドライン を改定するのがよいと思います。 おわりに テストはユーザ体験に影響を与えませんが、開発者体験の向上に大きく寄与します。せっかくテストを作るのだから、より効果的なテストを書いて開発者体験を向上させたいです。 今回はフロントエンドの 単体テスト という観点でよいテストの書き方について考えましたが、 VRT や E2E テストでは、また違った観点が必要になります。様々なテストを使いこなすまでの道のりは長いですが、少しづつ改善していけるように努力していきたいです。 *1 : 画像引用: https://testingjavascript.com *2 : https://ph-fritsche.github.io/blog/post/why-userevent *3 : 単体テスト の考え方/使い方 p57-58 *4 : コミュニケーションベーステストはその性質上、ユーザが知覚するままのテストにはならないです。submit が呼ばれることの検査はコミュニケーションベーステストです。 *5 : 単体テスト の考え方/使い方 p6-10 *6 : https://logmi.jp/tech/articles/328087#s3 *7 : https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html#images-that-convey-information