Growthのオペレーションツールの歩み 〜ダッシュボード編〜

Merpay Advent Calendar 2021 の 15 日目は 2 本立てで、2 本目の記事をメルペイ Growth Platform Team のバックエンドエンジニアの ksoichiro がお送りします。

この記事は、Growthのオペレーションツールの歩み 〜クーポン編〜に続く「ダッシュボード編」です。

Growth Platform Team は、メルペイの Growth に関わるプロダクトを開発・運用するチームです。"Growthのオペレーションツールの歩み 〜クーポン編〜" でもご紹介したように、Growth Platform Team では複数のプロダクトを開発・運用しています。それぞれシステムのオペレーションが必要な業務がありますが、この記事では「ダッシュボード」でのオペレーションに関してご紹介します。

メルペイダッシュボードにおける出し分け

メルペイでは、メルカリアプリにおける「メルペイ」タブのことを「メルペイダッシュボード」と呼んでいます (以下、単に「ダッシュボード」と記載します)。ダッシュボードには、決済のための UI 以外にもお客さまのための情報を掲載する領域が複数存在します。

これらは、メルペイにおけるアクションをお客さまに促すもの、キャンペーンなどをお知らせするもの、その他様々な機能への導線など、メルペイの機能への入り口として幅広い情報を提供する役割を持ち、それぞれ適切なお客さまに適切な内容を表示しなければなりません。コンテンツを表示するためには文言や URL といったコンテンツそのものの入稿 (設定) のオペレーションが必要になりますが、表示対象のお客さまを指定する、いわゆる「出し分け」についても検討する必要があります。

この出し分け条件の種類は固定ではなく、新機能のリリースや各種のキャンペーンの実施により追加したり変更したりする必要があります。更新の頻度が低く時間に余裕がある場合は都度開発すれば対応できますが、キャンペーンなどの施策の実施が決定してから開始までの期間が短くスピードが求められる場合や、他のプロジェクトに開発リソースを割かなくてはならず対応できない場合もあります。

一方で、これまでダッシュボードは複数回のリニューアルを実施してきており、これらの情報を掲載する領域の仕様も変わりやすかったということもあって、入稿のためのシステムを開発するのは避けてきたという事情もあります。

こうした状況を踏まえて過去の施策・開発を分析しながら、ある程度の変化が発生することを前提としながらも開発が不要になるような仕組みを模索してきました。

非エンジニア職種の方が簡単に入稿できるようにする外部のサービスもあると思いますが、いくつかの理由から内製しており、今回はその一部を説明します。

バナーツールの活用

チーム外の関係者からもスピーディに入稿できる必要があり、かつ出し分けが比較的簡単に済む (安全に設定できる) 領域については、以前社内で開発された「バナーツール」を利用しています。初期開発からかなり時間は経っていますが、まだまだ活躍しています。バナーツールそのものの詳細な説明は割愛しますが、以下の資料で詳しく説明されていますので、ご興味のある方はご参照ください。

このツールの特徴は以下の通りです。

  • 入稿用の管理画面と、入稿内容取得用の API がある
  • 簡単に用途 (テナント) を増やすことができる
  • バナー設定の項目は JSON Schema で定義され、開発なしでカスタマイズできる
  • Go をベースとした条件式評価の仕組みがある

ツール側では表示内容、表示条件を保存し、利用側では表示条件となる変数をリクエストパラメータとして API に与えることで、ツール側がフィルタリングして適切な内容を返すようになっています。

例:

  • Bool(A) == true → 変数 A が true のセグメントのお客さまのみ表示する
  • Bool(A) == true && Int(B) > 0 → 変数 A が true かつ変数 B が 1 以上のセグメントのお客さまのみ表示する

バナーツールで利用する変数を定義するのはバナーツール API の呼び出し側の役割です。 リクエストパラメータとして定めた変数に値を割り当ててバナーツールの API にリクエストします。

イメージ:

https://[バナーツールAPIのURL]/?A=true&B=100

また、レイテンシなどいくつかの理由から、お客さまからのリクエストの都度バナーツールの API を呼び出すことはせずに呼び出し側でレスポンスをキャッシュしてリクエストを間引く必要があります。

これらのことから、メルカリアプリから直接バナーツールへリクエストするのではなくバックエンドのサービスを仲介させています。

以前から利用してきた部分においては、レイテンシを低く抑えるため以下のような割り切りの仕様としてきました。

  • バナーツールへのリクエストは非同期にし、初回のリクエストはコンテンツの表示を諦める
  • 同じ条件でのリクエストでキャッシュがある場合はキャッシュを返す

以下の図のイメージです。

バナーツールの利用イメージ

複雑な条件への対応

簡単な出し分けで済む場合にはこれで十分ですが、ダッシュボードの中でもお客さまに対してより適切なアクションを提案することを意図した領域「Next Action Support Area」(通称 N.A.S.A. (ナサ, 以下「N.A.S.A.」と記載します)では出し分けの条件が複雑になります。少し前の内容にはなりますが、この N.A.S.A. のデザイン思想についてご興味のある方は以下の記事もご覧ください。

複雑性をシンプルに。メルペイTOP画面に込めたデザインの思想。|Mercari Design|note

N.A.S.A. においてもバナーツールを活用することを考えましたが、対象者を限定するための複雑な出し分けが必要になるため、同じ方式を適用しようとすると以下の問題が生じます。

  • リクエストのパターンごとにキャッシュを構築するため、変数の組み合わせや変数の値の種類が多いとキャッシュのパターンが増大し、キャッシュにヒットしにくくなる
  • 結果としてバナーツールへのリクエストが増え、コンテンツが表示されない回数も増える

複雑な条件であっても最終的にはいくつかのコンテンツを表示するか表示しないかという簡単な条件に集約されますから、いくつかの単純な条件を合成した変数、例えば「キャンペーン A の対象者であるかどうか」のような単純なものを定義すれば短期的には解決できます。ただし、この方法では都度開発が必要になりますし、基本的にその施策だけでの使い捨てになってしまいます。

また N.A.S.A. においては、単に入稿されたものをすべて表示するわけではなく、最適なものを1つだけ選択して表示しなければならないという制限もあります。さらにこの「最適なものを1つ」という条件にも例外があり、重要なアクションを促さなければならない特定の状況においては複数の内容を表示する場合もあります。表示内容についても、固定の文言ではなく動的に構成するものも存在します。このため、単純にバナーツールの入稿内容をプロキシするだけでは実現できません。

この問題は以下の対策により解決しました。

  1. 利用側で表示条件を評価するようにした
  2. 評価された結果をさらに1〜2件に選別するようにした
  3. 移行が難しいものはビルトインのコンテンツとして選択できるようにした

それぞれ簡単にご説明します。

1. 利用側で表示条件を評価するようにした

前述の問題は、バナーツール側で表示条件を評価しフィルタリングしようとすることによる問題です。そこで、バナーツールの利用側で表示条件を評価することを考えました。

バナーツールでは、JSON Schema に準拠した JSON によって簡単にカスタムの設定項目を増やすことができます。メルカリアプリで表示するための情報に加え、ここに追加の条件設定用の項目を増やしました。バナーツールの入稿画面にある既存の表示条件設定項目には常に表示される設定をしておきます。

バナーツールの条件評価の部品を抽出して利用側に組み込むことで、利用側でフィルタリングができるようになりました。構成は以下の図のイメージです。

変更後の構成

2. 評価された結果をさらに1〜2件に選別するようにした

N.A.S.A. において重要なのは、最適なものを1件表示するという仕様の扱いです。基本的にはフィルタリングされたものを優先度順に並び替えて先頭の1件を取得すれば良いのですが、前述の通り例外が存在します。

これは他の入稿に依存して表示有無・表示件数が変わるというというものですが、バナーツールには相互に作用するような設定は存在しません。これも、各設定を特定する ID と、それを参照して挙動を変えるかどうかを設定する設定項目を増やすことで対応しました。

1.と2.のいずれも、バナーツールにサポートされていない機能を利用側で拡張しているので、当然ながら入稿ミスのリスクを伴います。API レイヤでの評価時に入稿ミスがあるとわかったものは非表示にしログ監視でアラートをあげるような対応としていますが、今後改善が必要な課題です。

3. 移行が難しいものはビルトインのコンテンツとして選択できるようにした

過去の実績の分析において、表示条件が特殊で他で利用する可能性が低い、メッセージが動的に変わるため単純には入稿できない、プロダクト仕様としてメッセージを変更させたくない、などの例外の扱いに悩みました。これらに対応するように開発することもできますが、メリットが少ないと判断し、バナーツール上の設定項目としては「ビルトイン」のコンテンツとして選択できるようにしました。

ビルトインのコンテンツは変更できないし、最終的にバナーツールの利用側でフィルタリングするのだからバナーツールで入稿する必要もないのでは?という疑問もあるとは思いますが、入稿する担当者の視点では今お客さまに表示されている可能性のあるものはすべてバナーツール上に登録されているという状態がわかりやすく望ましいと考え、このようにしています。

システムへの影響の抑制

出し分け条件に必要なデータはダッシュボードを表示する API のレイヤで収集して変数として設定する必要があります。このデータには、データベースから定期的に集計して表示条件として利用するものもあれば、他のマイクロサービスの API から取得するものもあります。

バナーツールで入稿する場合、表示条件として利用できる変数が重要になりますが、入稿内容によってバックエンドのサービスに急激に負荷がかかりサービス提供に影響が出るようなことは避けなければなりません。

他のマイクロサービスの API にアクセスしなければ利用できない変数については、知識を持つバックエンドエンジニアが環境変数の設定を変更しなければ利用できないようにしています。また、必要に応じてリクエストに条件をつけることもできるようになっています。

以下のような JSON 形式の環境変数で、依存する API ごとに設定を記述します。

{
    "dependency_a": [
        {
            "start_time": "2021-06-01T00:00:00+09:00",
            "end_time": "2021-08-01T00:00:00+09:00",
            "condition": "true",
            "params": ["202106_campaign"]
        },
        {
            "start_time": "2021-08-01T00:00:00+09:00",
            "end_time": "2021-10-01T00:00:00+09:00",
            "condition": "Bool(VariableA) == true",
            "params": ["202108_campaign"]
        }
    ],
    "dependency_b": ...
}

設定を適用する期間 start_timeend_time のほか、前述のリクエスト条件として condition という項目で同じ形式の条件式を記述できるようにしています。 リクエストする際にパラメータが必要な場合のために params という設定も用意しています。

ダッシュボードを表示する API の中では、可能な限り並列で依存先の API を呼び出して結果を統合するようにしていますが、その API 間でも依存関係のあるものもあります。このため、API の並列呼び出しがいくつか直列になるような構成となっています。前述の設定における condition の項目で利用できる変数は、この依存関係に従いリクエスト時点までに値が準備できているものに限られます。以下はソースコードのイメージです。

g, ctx = errgroup.WithContext(parentCtx)
…
if ok, params := dashboardcondition.ShouldSendRequest(
    // 依存 A の設定を使ってリクエスト要否を判定
    &dashboardDependencyConditions.DependencyA,
    ...
); ok {
    g.Go(func() error {
        // Dependency A の呼び出し
    })
}
if err := g.Wait(); err != nil {
    return nil, err
}
// 並列でリクエストするが、依存関係のある場合は並列呼び出しを直列で複数回呼び出す部分もある
if ok, params := dashboardcondition.ShouldSendRequest(
    &dashboardDependencyConditions.DependencyB,
    // Dependency A から取得した結果を Dependency B のリクエスト条件として利用することもできる
    userConditions.Get(),
); ok {
    g.Go(func() error {
        // Dependency B の呼び出し
    })
}

ビルトイン関数の追加

表示条件式では、型(変数名) == 値 のように変数の値を何かと比較するという形式が基本になっています。しかしこれだけでは複雑な表現をすることができません。バナーツールではビルトインの関数を実装して条件式の中で利用できるようになっているので、そこに必要な関数を追加することで表現力を高めています。

例: クーポン A の利用を促すコンテンツの表示条件

before: クーポン A が利用できるお客さまかどうかを示す変数を都度実装

Bool(IsTargetOfCouponA) == true

after: "要素を含む"ことを調べる関数を追加し、お客さまが利用できるクーポンのリストとして実装

contains(String(AvailableCoupons), "クーポンAのID")) == true

課題と今後の展望

これらの対策によって、マーケティング施策の実施の都度必要となっていた開発が削減され、リードタイムも短くなりつつあります。大掛かりにやろうとせず、既存のツールを工夫して使うことにより低コストで改善する取り組みは今のところ成功していますが、まだまだ課題はあります。

  • 同時に実施される施策の優先順位の整理や表示条件の調整に手間がかかる
  • バリデーションが不十分なため設定には知識が必要になる
  • 柔軟なアクセス権の制御ができない
  • …etc.

今後、プロダクトとしての方向性を捉えつつマイクロサービスの構築などによってこれらを解決していきたいと考えています。

おわりに

メルペイの Growth を担う Growth Platform Team におけるオペレーションのツール化の状況について、この記事ではダッシュボードの部分についてご紹介しました。少しでも参考になることがあれば幸いです。

同じチームの yukinasu さんによる Merpay Advent Calendar 2021 3日目の記事「メルペイのキャンペーンを支えるフィルタリング機能」と似ている部分もあると思いますが、最初から一体で開発していたわけではなく、徐々に組織の形を変えながら今の Growth Platform Team になっています。提供する機能や今に至るまでの経緯は異なっていても、短絡的に管理画面を構築するような理想形にこだわりすぎることなく、その都度目的や要件を見極めながら仕組みを作ってきている点は共通していると思います。

オペレーションのツール化というと作業の効率を良くすることをイメージしがちですが、Growth においては、様々な部署・職種が関わりながら短期間で複数の施策を実現していこうとすることでオペレーションのミスによるインシデントのリスクも高まるため、安全面での考慮も重要です。発生してしまったインシデントは振り返り、改善策を仕組みに反映させながら前に進んでいます。

メルペイでは、今回ご紹介したような仕組みを作る取り組みにご興味のある方、ミッション・バリューに共感できるバックエンドエンジニアを募集しています。一緒に働ける仲間をお待ちしております。

ソフトウェアエンジニア (Backend) [Merpay]

明日の Merpay Advent Calendar 2021 は、SRE の foostan さんが担当です。引き続きお楽しみください。