booklista tech blog

booklista のエンジニアリングに関する情報を公開しています。

Datadog Synthetics Testsによる外形監視の自動化について

アイキャッチ

こんにちは。プロダクト開発部QAエンジニアの岡と申します。
弊社は「auブックパス (運営:KDDI株式会社)」と包括的なパートナーシップを結び、開発を行なっています。
こちらのサイトの監視に、Datadog Synthetics Tests(ブラウザーテスト)を導入して自動化したことについてお伝えします。

サイトの監視を自動化した結果、障害の検知と解決を迅速に行うことができるようになりました。

この記事で伝えたいこと

Datadog Synthetics Tests 導入理由

auブックパスは24時間365日営業しているため、ユーザーが本を購入できない、または読めないといった障害が発生することは許容できません。これまでサイトの監視を外部に委託していましたが、今後は障害の検知と解決を迅速に行うため、サイトの監視を内製化することにしました。また、内製化によるコスト削減も狙いのひとつです。システム構築コストも抑えるため、サイトの監視を簡単に実現できる有償ツールであるDatadog Synthetics Tests(ブラウザーテスト)を採用しました。

Datadog Synthetics Tests の概要

Datadog 公式URL

公式引用

Synthetic ブラウザテストを使用して、エンドツーエンドのテストで世界中の Web ページを顧客がどのように体験しているかを監視します。

Datadog Synthetics Testsには、公式サイトで紹介されている通り、ブラウザーテスト機能があります。レコーディング機能を使ってブラウザーの操作を記録し、ノーコードでブラウザーテストを作成できます。また、外形監視として障害の検知やサイトのレスポンスタイムの確認などを簡単に行うことができます。

ブラウザーテスト作成手順

まず、ブラウザーテスト作成手順について、「考慮したポイント」を交えて紹介します。

1. ブラウザーテストの作成開始

Datadogにアクセスし、ブラウザーテストを作成するためのダッシュボードに移動します。
Ux Monitoring > Synthetic Testsを選択し、画面右上の+New Testsをクリックして Browser Testをクリックします。

2. 設定

Set your test details

以下を設定します。

  • Starting URL:テスト対象のURL
  • Name:テスト名
  • Envrionment:環境(任意)
  • Additional Tags:タグ(任意)
    • 分類に使用
  • Advanced Options(任意)
    • Cookie設定、Proxyの設定、Basic認証などの設定が可能

Browsers & Devices

実行クライアント環境は「Laptop Large」「Tablet」「Mobile Small」「Chrome」「Firefox」「Edge」の組み合わせが可能です。

Select locations

アクセス元の設定をします。

  • 設定例:Tokyo (AWS)を選択

Define retry conditions

テスト失敗時の待機時間と再実行回数を設定します。

  • 設定例:テスト失敗後10000ミリ秒待機し2回リトライ

Define scheduling and alert conditions

テスト実行間隔の設定をします。

  • 設定例:テストを5分間隔で実行
  • Advancedでは詳細な時間設定が可能

アラートの閾値の設定をします。

  • 設定例:過去8分間に1か所でエラーが発生した場合、テストはアラートステータスになり通知が送信される

(ポイント) 実行コストとのバランスを考慮して、監視対象の重要度に応じて間隔を設定しました。参考:Datadog料金 (15ドル / 1,000テスト)

Configure the monitor for this test

障害検知時の通知方法を設定します。
通知方法としては、メール、Slack、PagerDuty、Webhookなど、豊富な選択肢があります。
※ここは通知作成方法で詳しく触れます。

設定が完了したらSave & Edit Recordingをクリックしてレコーディングに進みます。

4. レコーディングの開始

レコーディング開始前にChrome拡張機能「Datadog test recorder」をインストールする必要があります。

レコーディング画面では、 ノーコードで簡単にブラウザーテストが作成できます。
Start Recordingでレコーディングを開始します。右側に表示されたブラウザーでの操作が、左側に記録されていきます。

(ポイント) テストの対象は、外部連携している箇所など障害が発生しやすい箇所に絞って監視しています。 監視対象が増えると誤検知が増え、不要なアラートが上がってしまい、テストの実行コストも増えてしまいます。

(ポイント) 障害の発生頻度が高い箇所を中心にしてテストシナリオのステップ数を最小限に抑えました。
テストシナリオのステップ数が多いと、本来検出したい障害ではない箇所でテストが失敗して原因の特定が困難になります。そのため、障害の発生頻度が高い箇所にアサーション(検証)を配置し、ステップ数を絞りました。

5. 自動記録された内容を確認して調整

以下を必要に応じて調整します。

  • Step Name: シナリオのステップ名
  • Target Element: 操作対象
  • Click type: 操作対象のクリック方法
  • Advanced Options > User Specified Locator: ユーザー指定のロケーターが使用可能
    • 例えば、idやテキストを起点として要素を指定するためにXPathを使用するなど、柔軟な対応が可能
  • Wait for 60s before declaring this step as failed: 検証実行前にwait(待機ステップ)を挿入
  • Continue with test if this step fails: このステップが失敗したときにテストを続行するか選択

自動記録以外にも手動でアサーション(検証)ステップを追加できます。

(ポイント) テストステップ名を編集し、各ステップが何をテストしているのかを分かりやすくすることで、障害の発生箇所の特定が容易になります。

(ポイント) テストシナリオに分岐機能がないため、Continue with test if this step failsの設定を使用して分岐を実現しました。 例えば、特定の要素が表示された場合にのみアサーションを実施したい場合があります。シナリオステップのContinue with test if this step failsにチェックを入れることで、要素がある場合のみアサーションを実施するように設定することで分岐を実現しました。

(ポイント) 部分的にユーザー指定のロケーターを使用しました。
auブックパスのフロントエンドではReactを使用しています。Reactを使用する場合、デフォルトでは要素にIDが付与されないため、テキスト表示のない要素を特定したり、階層が変更された場合のテストメンテナンスが困難です。この課題に対処するため、開発者に対してテスト対象の要素にIDを付与するように依頼しました。付与されたIDを使用して、Advanced Options > User Specified Locatorでカスタムロケーターを指定しました。これにより、画面変更の際にもテストメンテナンスが容易になりました。

6. カスタムスクリプトの作成

✔︎Assertion > 下から2番目のTest custom JavaScript assertionを選択すると、JavaScriptの記述が可能になります。
クリックを繰り返す、サイト内に表示された値を変数に格納し後で検証に使用するなどの処理をJavaScriptで記述することにより、テストが拡張できます。
JavaScriptの記述以外にも、メールの受信テスト、DLファイルのテスト、サブテスト機能など機能が豊富です。

7. テストシナリオの実行

Run Test Nowで作成したシナリオを実行します。
テスト実行結果はダッシュボードで確認できます。結果をクリックして詳細を確認します。

  • Faild(失敗)の場合は作成したテストシナリオに問題が無いか、内容を見直す
  • 実行時の画面キャプチャーがあり、レスポンスタイムの確認が可能なため、テスト失敗の原因を特定できる

上記の場合、「モーダル画面が表示されてしまい要素のクリックができず、次の画面に遷移できなかった」がテスト失敗の原因です。そのためモーダル画面を閉じる処理を追加するとテストが成功します。

8. テストの修正

さきほど実行したテストが失敗しているので、「モーダル画面が表示された場合にクリック」というステップを追加します。

テストが成功したので、ブラウザーテストは完成です。

通知作成方法

つぎに、作成したブラウザーテストが障害を検知したときに通知する方法を紹介します。
電話通知設定と、Slack通知設定の2つを実施します。

電話通知設定

以前、弊社のテックブログ Amazon Connectを使った障害発生時の自動オンコール実現について でご紹介した方法を実施します。

使用する要素は以下の4つです。

  • Datadog
  • AWS SNS
  • AWS Lambda
  • Amazon Connect

1. 初期設定

電話番号を取得するための申請などを行います。 参照:初期設定について

2. 架電設定

参照:架電用のAmazon Connectの問い合わせフロー(SNS, Datadog設定, Lambda)について

3. Datadog側の設定

2の最後ではモニターに対して設定する例がありますが、シナリオに設定もできます。
今回は、シナリオテストの失敗(Faild)を検知して電話の着信で知らせるため、以下のように設定をします。

  • ブラウザテストの設定 > Configure the monitor for this test > Notify your team

4.調整

障害検知の条件を調整します。
Datadog Monitors機能とalert conditionsを設定し、リトライでテストが成功した場合は即時復旧したとみなし、障害とはみなさないように調整しました。
これにより、不要な電話通知を防ぎつつ、実際の障害を正確に検知できています。

Slack通知設定

1. Slack側の設定

Datadog公式参照

2. Datadog側の設定

こちらも、シナリオテストの失敗(Faild)を検知してSlackに通知するため、以下のように設定をします。

  • ブラウザテストの設定 > Configure the monitor for this test > Notify your team

定期実行を開始

テストシナリオの「Resume Scheduling」で定期実行を開始します。 障害発生時にSlackと電話に通知されます。

運用手順

ブラウザーテストの作成と、障害の検知したときの通知方法の設定が完了したので、さいごに運用手順を紹介します。

1. 障害の発生

障害が発生すると、電話による通知があり、同時にSlackチャンネルにも通知が行われます。

2. 障害の特定

Slackのメッセージリンクからテスト失敗箇所へアクセスし、障害発生時のスクリーンショットやテストステップを確認できます。
テストシナリオ数とステップ数を厳選し、テストステップ名の編集により、各ステップが何をテストしているのかを分かりやすくすることにより、障害の発生箇所を迅速に特定できるようになりました。

3. 障害対応、復旧

障害の発生箇所が特定されたら、それに基づいて障害対応が行われます。 復旧した場合にも電話での通知とSlackへの通知が行われるように設定できます。(障害が発生している間もテストのスケジュール実行は継続されるため、復旧も即時検知されます)

4. 分析

電話通知やSlack通知により、障害発生から解消までの経過や復旧までの期間などをダッシュボードで確認できます。
これにより、障害の計測や原因分析が可能となり、障害の傾向やパフォーマンスの改善点を特定し、将来の問題を予防するための情報を得ることもできます。

さいごに

Datadog Synthetics Tests(ブラウザーテスト)を導入し、活用することで、障害の検知と解決を迅速に行うことができるようになりました。 弊社ではSynthetics Testに限らず、様々な方法でサイトの品質を担保しています。
お客様が快適にサイトをご利用いただけるよう、品質の向上を目指します。

NFTを入館証代わりにしてみた話

アイキャッチ

自己紹介

株式会社ブックリスタでエンジニアをしている城、椛澤、尾崎と申します。 私たちは日頃の主業務が異なりますので、それぞれ簡単に紹介させていただきます。

  • 城 :スマートフォンアプリ「YOMcoma」の開発
  • 椛澤:電子書籍ストアのプロジェクトマネージャー
  • 尾崎:電子書籍関連システムのプロジェクトマネージャー

活動概要

プロジェクト発足の経緯

「NFT」というワードについて、近年耳にする機会が増えています。 ブックリスタでは「エンジニアラボ」という取り組みを実施しており、 その中で「NFT」をテーマに1年間研究し、ブックリスタでの「NFT」の活用可能性を模索しました。

エンジニアラボとは

業務とは直接関係しない研究開発プロジェクトを行うことができる制度です。 有志で集まったメンバーが業務時間の最大20%を使い、1年間テーマに沿って研究開発することで、 自己研鑽を高めつつ、成果によってはサービス化に繋がることがあります。

  • 2023/1/30 公開の「コミックの類似性の算出とそのシステム構成について」でご紹介した「似た商品を探す」機能が、エンジニアラボの研究成果によってサービス化されました

NFTとは

NFTは非代替性トークン(Non-Fungible Token)の略称で、電子データに一意のトークンを付加することで 電子データに固有の価値をもたらすことができます。 画像、動画、音声はもちろん、チケットやコミュニティへの参加権など本来複製可能なデータに対して一意のアイテムとして扱うことができます。 トークン自体はブロックチェーン上に記録されるため、システム構成としてはブロックチェーンを中心としたシステム構成になります。

ftnft

活動内容

今回参加したメンバーは全員NFTおよびブロックチェーンの開発未経験者でした。 そのため、まずは座学による知識獲得から開始しましたが、実際に手を動かさないと得られない成果もあります。 ある程度の知識を獲得したところで、実装に向けてより具体的にサービスにアクセスしてきたユーザーが、特定のNFTを所有しているのかを確認する方法を調査しました。 調査結果をふまえてサービス案の検討をしていきました。

サービス案

「もしブックリスタがカントリークラブを始めたら」というテーマを設け、NFTで発行した会員証を保持しているユーザーだけが入場できるサービスと仮定して進めていきます。

アプリケーション概要

今回作成するアプリケーションのシステム構成イメージ図です。

システム構成

今回作成するアプリケーションはカントリークラブアプリと記載しました。

NFT Gardenを使ってPolygonEthreumのサイドチェーン)に独自のコントラクトを作成し、会員証に見立てたNFT(以降、会員証NFT)を複数発行しました。 PolygonやEthereumにおけるNFTとはERC721規格のトークンを指します。 ERC721のIF仕様はOpenZeppelinのドキュメントに記載があります。 これによるとownerOfを叩けば所有者のaddress、すなわちWalletIDを得られることがわかりました。 ではownerOfはどう叩くのか、調べるとRPCプロバイダーがブロックチェーンにリクエストを行いレスポンスを返してくれることが分かったので、今回はAnkrというRPCプロバイダーを使ってみます。

ユーザーを一意に特定するにはWeb3Authを使用します。 GoogleアカウントやSNS等で認証が行え、同じアカウントで認証すれば常に同じ一意の秘密鍵が得られます。 Web3Authで得た秘密鍵をPolygonで使用可能なEthereumAddressに変換し、WalletIDとして使用します。 EthereumAddressへの変換はハッシュ関数を使用するため、同じ秘密鍵を元にした場合は何度作成しても同じEthereumAddressが得られます。

実装

開発環境

プロジェクト作成

  1. Visual Studio Codeを開きshift + command + pFlutter new ProjectApplication の順に選択
  2. プロジェクトを配置するディレクトリを選択
  3. 任意のプロジェクト名を入力する

Web3Authでのウォレット認証

Web3AuthのFlutterSDKに実装手順の記載がありますが、それだけでは動かなかったので実際に動作確認した手順を記載します。

Web3AuthのSDKを入れる

  1. 作成したプロジェクト直下でflutter pub add web3auth_flutter
  2. Web3AuthのFlutterSDKをインストールflutter pub add web3auth_flutter
  3. web3dartをインストールflutter pub add web3dart
  4. ./android/app/build.gradletargetSdkVersion33に変更
  5. ./android/app/build.gradlecompileSdkVersion24に変更
  6. ./android/build.gradlerepositories {}maven { url "https://jitpack.io" }追加
  7. ./android/app/src/main/AndroidManifest.xml<manifest>直下に下記を追加
    • <uses-permission android:name="android.permission.INTERNET" />
  8. ./ios/Runner.xcworkpaceをXcodeで開きFile→Add Packagesを開く
  9. 右上の検索窓にhttps://github.com/web3auth/web3auth-swift-sdkを入れweb3auth-swift-sdkadd Package
  10. ./ios/Runner.xcodeproj/project.pbxprojIPHONEOS_DEPLOYMENT_TARGET3箇所を14.0に変更
  11. ./ios直下でpod install

Web3AuthのDeveloper Dashboardに登録

Web3AuthのDeveloper Dashboardにプロジェクトを作成しスキーマを登録することで、認証完了後にFlutterアプリ側で認証情報を得る事ができます。

画面イメージ

  1. Developer Dashboardにサインアップ
  2. サイドバーのPlug and playを開く(画像①)
  3. Create Projectで任意プロジェクト名を入力しTestnetプロジェクトを作成(画像②)
  4. 作成したプロジェクトを選択しAdd a new whitelist URLにAndroid用とiOS用のURLを登録(画像③)
    1. Android: {SCHEME}://{YOUR_APP_PACKAGE_NAME}
      • {SCHEME}は任意文字列
      • {YOUR_APP_PACKAGE_NAME}はAndroidManifest.xmlのpackageに記載されたもの
    2. iOS: {bundleId}://openlogin
      1. {bundleId}は任意文字列
  5. ClientIDはコピーしておく(画像④)
  6. Flutterにスキーマを登録 (SCHEMEとYOUR_APP_PACKAGE_NAMEはwhitelist URLに登録した内容へ置換)
    1. ./android/app/src/main/AndroidManifest.xml<intent-filter>直下に下記を追記
      • <data android:scheme="{SCHEME}" android:host="{YOUR_APP_PACKAGE_NAME}" android:path="/auth" />
    2. ./ios/Runner/Info.plist4行目付近の<dict>と書かれた直後に下記を追加 <key>FlutterDeepLinkingEnabled</key> <true/> <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLName</key> <string>web3auth</string> <key>CFBundleURLSchemes</key> <array> <string>{bundleId}</string> // Web3Auth dashboard で入力した内容に置換 </array> </dict> </array>

実処理を実装する

./lib/main.dartをを下記の通り修正しflutter runで実行してみましょう。

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:web3auth_flutter/enums.dart';
import 'package:web3auth_flutter/input.dart';
import 'package:web3auth_flutter/web3auth_flutter.dart';
import 'package:web3dart/credentials.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    // 省略
}

class MyHomePage extends StatefulWidget {
    // 省略
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();

    Uri redirectUrl;
    if (Platform.isAndroid) {
      redirectUrl = Uri.parse('{SCHEME}://{HOST}/auth');    // Web3Auth dashboard でホワイトリスト登録したURLに変更
    } else if (Platform.isIOS) {
      redirectUrl = Uri.parse('{bundleId}://openlogin');       // Web3Auth dashboard でホワイトリスト登録したURLに変更 
    } else {
      throw UnKnownException('Unknown platform');
    }
    await Web3AuthFlutter.init(Web3AuthOptions(
        clientId: "XXXXXXX",       // コピーしておいたClientID
        network: Network.testnet,
        redirectUrl: redirectUrl));
  }

  Future<void> login() async {
    final result = await Web3AuthFlutter.login(LoginParams(
      loginProvider: Provider.google,
      mfaLevel: MFALevel.DEFAULT,
    ));
    final credentials = EthPrivateKey.fromHex(result.privKey ?? "");
    print(result.userInfo);
    print('authenticated WalletID: ${credentials.address.hex}');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(),
      floatingActionButton: FloatingActionButton(
        onPressed: login,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

画面に表示されたFABを押し、自身のGoogleアカウントで認証すると、Flutterのログに自身のGoogleアカウント情報やWalletIDが表示されました。

簡単な解説です。 StatefulWidgetの生成時に呼ばれるinitState()Web3AuthFlutter.init()を呼び出すことで初期化しています。 初期化時のパラメーターにredirectUrlを渡していますが、これはDeveloper Dashboardでホワイトリスト登録したURLと同じ文字列です。 ホワイトリスト登録したURLと同じURLが指定されているのにリダイレクトされない場合は下記を確認してみてください。

  • iOS: ./ios/Runner/Info.plistに設定したスキーマ
  • Android: ./android/app/src/main/AndroidManifest.xmlに設定したスキーマ

会員証NFTの保持確認

会員証NFTを持っている人だけが入場できるサービスを作るので、ユーザーが会員証NFTを持っているかを確認する処理を実装する必要があります。

Web3Auth認証で得たユーザー情報から、所有するNFTの一覧が取得できるのでは…と少し期待しましたができないようです。 ここでは前述のRPCプロバイダーAnkrを使用して、Polygonから会員証NFT所有者のWalletIDを取得していきます。 PolygonのNFTであるERC721規格のトークンに関する情報を得るには、RPCを介してコントラクト関数が記述されたABIというJSON形式のインターフェースを使用します。

複数存在する会員証NFTの所有者が1つでも認証ユーザーのWalletIDと一致すれば、会員証NFTの所有者であると証明できます。

実装

認証時に作成したlogin()を下記に変更して実行してみます。

  Future<void> login() async {
    final result = await Web3AuthFlutter.login(LoginParams(
      loginProvider: Provider.google,
      mfaLevel: MFALevel.DEFAULT,
    ));

    // 認証したユーザーのWalletIDを取得
    final credentials = EthPrivateKey.fromHex(result.privKey ?? "");
    print('authenticated WalletID: ${credentials.address.hex}');
    
    // Polygon上に生成したコントラクトアドレス
    const contractAddress = "0x{contractAddress}";

    final client = Web3Client("https://rpc.ankr.com/polygon", http.Client());

    // ERC721のabi
    // https://gist.github.com/olegabr/45d659bec5f068eb9d82af4d3f712a23
    const erc721abi =
        '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"operator","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"owner","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"_approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}]';

    final contract = DeployedContract(
        ContractAbi.fromJson(erc721abi, "erc721abi"),
        EthereumAddress.fromHex(contractAddress));

    int nftTokenIndex = 0;
    bool isOwner = false;
    while (true) {
      try {
        final ownerOfResponse = await client.call(
            contract: contract,
            function: contract.function('ownerOf'),
            params: [
              BigInt.from(nftTokenIndex),   // intだとエラーになるので注意
            ]);
        final ownerWalletID = ownerOfResponse.first.toString();
        if (ownerWalletID == credentials.address.hex) {
          isOwner = true;
          break;
        }
        nftTokenIndex++;
      } catch (e) {
        break;
      }
    }
    print("isOwner: $isOwner");
  }

ユーザーに会員証NFTを転送する前後で実行するとログのisOwnerの値が意図通りに変わる事が確認できました。

今回はWeb3Client()のパラメータにPolygon向けRPCのURLが設定されていますが、別のチェーンを使用する場合こちらに各エンドポイントが記載されていますので参照ください。

その後のwhileブロック内では下記箇所でownerOf()を実行してNFTの所有者を取得し、認証したWalletIDのcredentials.address.hexと一致するかをみています。 今回で一番大事なところです。

final ownerOfResponse = await client.call(
    contract: contract,
    function: contract.function('ownerOf'),
    params: [
      BigInt.from(nftTokenIndex),
    ]);
if (ownerWalletID == credentials.address.hex) {

最後に

ブロックチェーンを取り巻く環境がめまぐるしく変化していること、筆者らの知見が浅かったことにより、こんなにコンパクトな実装にはふさわしくないであろう時間がかかりました。 時間がかかった大きな要因は、下記3点が大きかったと感じています。

  • 開発目線で調べると、「コントラクト」とは何を指すのか、ERC721のIFを叩くには、など調べることが多いが当初検索するキーワードがわからなかった
  • 情報が古い記事を見て実装してうまく動かないこと
  • 公式のページであってもリンク切れがあるなど、正しい仕様が不明なケースも存在した

この記事の内容が、これからアプリ開発をされる方の何かしらのヒントになれば幸いです。

auブックパスのストア開発におけるスクラム

アイキャッチ

株式会社ブックリスタ プロダクト開発部の開発・運用を担当している片山と申します。

今回は、弊社が包括的なパートナーシップを結び開発を行なっている「auブックパス (運営:KDDI株式会社)」のストア開発におけるスクラムについて記載させていただきます。

本記事はauブックパスのストア開発でのスクラムについての説明であり、純粋なスクラムとしては正しくない内容が含まれている可能性がありますので、ご注意ください。

また、本記事は筆者が開発者であるため、開発者が参加するイベントのみに絞り込んで記載しており、開発者が参加しないイベントについては記載しておりません。

開発に集中できる仕組み

auブックパスのストア開発におけるスクラムは、開発者が開発に集中できる仕組みとなっています。

以下は、1スプリントの開発者の大まかな時間割です。

時間割

図を見ていただいて分かる通り、開発者は自分の時間の大部分を開発に使用することが出来ます。

また、スクラムとしては当然なのですが、開発者は2週間のスプリントで実施する作業をスプリントプランニングで明確にします。作業の優先度も必ず決めます。そのため、自分が何をすればよいか迷うことはありません。

メンバー紹介

スクラムチームのメンバーは以下のようになっております。(通常のスクラムとは違うメンバー構成です)

  • プロダクトマネージャー
    • マーケティング戦略に則ったプロダクト改善の立案
    • プロジェクトマネージャーと連携した各ステークスホルダーとの調整
    • リリース後の効果検証のリード
  • プロジェクトマネージャー
  • 開発者
    • ブロダクトゴールを実現するたための開発(設計・実装・テスト)
    • プロダクトの運用
  • インフラ担当者
    • AWSにおけるインフラの設計・構築
    • プロダクトの運用
  • QA担当者
    • テストの計画・実施
    • 開発者に対する品質面のレビュー
  • スクラムマスター
    • MTGのファシリテート
    • プロセスの監視・改善

スクラムの内容

朝会

毎朝10時30分から30分間だけ朝会を実施します。 朝会にはプロジェクトマネージャー、開発者、インフラ担当者、QA担当者、スクラムマスターが参加します。

プロジェクトマネージャーを除く参加者全員が前日やったこと、本日やること、今問題になっていることを報告し、情報共有します。

報告はだいたいいつも15分程度で完了し、残り15分は雑談をしています。趣味の話など仕事には関係のない会話が多いです。auブックパスのチームは全員がリモートで仕事していることが多いため、朝会の雑談は、チームワークを維持するための貴重な時間となっています。

スプリントプランニング

スプリントプランニングは、次回スプリントで各担当者が実施する作業を決める MTG です。 プロジェクトマネージャー、開発者、インフラ担当者、QA担当者、スクラムマスターが参加します。

スプリントプランニングを始める前までに、以下の作業を実施しておきます。

  • バックログの優先順位付
    • チケット管理ツール Jira を使用
    • プロジェクトを要求管理用と開発用に分けており、プロダクトマネージャーが要求管理用プロジェクトで付けた優先順位を元に、プロジェクトマネージャーが開発用プロジェクトの優先順位を決める
  • ストーリーポイントの算定
    • プランニングポーカーを用いて、開発者全員で見積もる

auブックパスのスプリントプランニングは2回に分けて実施します。スプリントを開始する4日前の月曜日にスプリントプランニング1を実施します。2日前の木曜日にスプリントプランニング2を実施します。

スプリントプランニング1では、優先度の高い順番にチケットの担当を決めます。チケットの優先度が付いていることは非常に重要で、開発者は自分が担当している未完了のチケットの中で一番優先度が高いチケットを集中して作業できるようになります。

また、チケットの担当者の割り当ては、できる限り希望者を優先して割り当てるようにしています。自分が希望したチケットを作業する方が、誰かが決めたチケットの作業をするよりもモチベーションが上がります。

スプリントプランニング1と2の間で開発者は、自分が担当となったチケットをサブタスクに分割し、作業時間を見積もります。ここで重要なのは、各チケットに対するサブタスクの洗い出しになります。出来る限り粒度の細かいサブタスクに分割することで、見積もりが正確になり、かつ、作業進捗も分かりやすくなります。

スプリントプランニング2では、各担当者のチケットにおける見積もり時間を確認し、次回スプリントで実施可能かどうかを判断します。見積もり時間が多い場合は、チケットを分割したり、チケットをバックログへ戻すなどして調整します。見積もり時間が少ない場合は、バックログにあるチケットを新たに割り当てます。

開発

ある程度の規模の開発であれば、設計、テスト仕様書作成、実装、テスト実施の順番に作業します。1スプリントで収まらない開発である場合は、それぞれの工程を別々のスプリントで実施することもあります。

設計

設計書は ConfluenceGoogle Sheets で記載することが多いです。

書き方は決めておらず、各開発者が好きな書き方で設計書を書いています。

そして、設計書は必ず他メンバーがレビューします。レビューすることで、担当者が気づかない問題を実装前に気づくことがよくあり、手戻りを防ぐことが出来ています。

テスト仕様書作成

テスト仕様書を実装前に記載することで、実装するプログラムのインプットとアウトプットを明確化します。

設計書と同様にテスト仕様書もレビューすることで、手戻りを防ぐことが出来ています。

以前は、テスト内容が十分でなく、不具合をリリースしてしまうことがありました。そこで、どの画面・機能で何を確認するべきかのテスト観点を資料化することで、全ての開発者が十分な品質を保証できるテスト仕様書を作成できるようになりました。

実装

テストファースト

可能な限りテストファーストで、Unit テストを実装するルールとしています。ただ、UI 部分など Unit テストが十分に実装できていない箇所があり、今後の課題となっています。

Linter、Formatter

Linter にて事前に分かる問題は自動で検出するようにし、Formatter にてコードのスタイルは自動で統一するようにしています。

CI/CD

GitHub でプルリクを作成したときに、 ビルド、Unit テスト、Linter を実行し、問題がない場合のみマージできるようにしています。これがあるおかげで、コードレビューの手間がかなり削減されています。

GitHub のプルリクをマージすると、AWS CodePipeline にて、自動で開発環境・ステージング環境にリリースされ、ステークホルダーがすぐに確認できるようになっています。

コードレビュー

GitHub のプルリクでは、最低一人の reviewer から approve が出ないとマージ出来ないルールとしています。

reviewer の選択は、GitHub の Code review assignment を使用し、各開発者が均等にレビュー担当となります。これにより、実装内容がバランスよく情報共有されています。

テスト実施

事前に作成しておいたテスト仕様書に従ってテストを実施します。

開発規模がある程度大きい場合は、開発者がテストを実施した後に、QA 担当者がさらにテストを実施することで、品質を保証するようにしています。

リリース

リリースは master ブランチにマージして、 AWS CodePipeline にて承認ボタンをクリックするだけで出来るように単純化されています。

ただし、一部のサービスは Jenkins を使用したリリースとなっております。Jenkins でも画面からいくつかの項目を入力して実行するだけなので、難しくはありません。

スプリントレトロスペクティブ

スプリントレトロスペクティブは終了したスプリントの反省会のようなものです。 スプリントレトロスペクティブには、プロジェクトマネージャー、開発者、インフラ担当者、QA担当者、スクラムマスターが参加します。

スプリントレトロスペクティブでは以下について、話し合います。

  • 各自の振り返り
  • ベロシティの推移の確認
  • 次回スプリントの改善目標

各自の振り返りでは、各自が個人、相互作用、プロセス、ツールに関して検査し、うまくいったこと・うまくいかなかったことを発表します。そして、うまくいかなかったことに関しては改善点をみんなで考え、うまくいったことに関してはみんなが真似できるように情報共有します。

ベロシティの推移の確認では、過去6回のスプリントと今回のスプリントのコミットメント1と完了2比較し、今回のベロシティが過去に比べてどうだったかを確認します。ベロシティの結果が過去と大きく異なる場合は、原因と対策を話合います。

スプリントレトロスペクティブの最後に、次回スプリントの改善目標をまとめます。

ナレッジ共有会

ナレッジ共有会はスクラムには関係のないイベントです。

参加は任意参加として、時間に余裕のあるメンバーのみが参加します。

開発者各自がauブックパスサービスに関わる知識、もしくは、システム開発に関わる知識を共有するための MTG です。

例えば、最近のナレッジ共有会では、以下のような知識を共有しました。

  • RDS のバックアップ
  • RDS の認証方法
  • Lambda の Layer
  • OpenVPN
  • トイルについて
  • jest の spyOn
  • 冪等性について
  • 書籍「はじめて学ぶソフトウェアのテスト技法」
  • auブックパスのアクセスログについて

用意する資料は箇条書きの簡単な文章程度で、資料作成など事前準備に時間をかけることはありません。

まとめ

以上は、2023年3月末時点のauブックパスのストア開発におけるスクラムです。

auブックパスのストア開発の今の体制が出来て2年半が経過しました。2年半の間に色々試行錯誤することで、記載したような開発方法となりました。この方法は、開発者中心の方法となっており、開発者が開発に集中出来る環境であると自負しています。


  1. スプリントプランニングで決めたストーリーポイントの合計。
  2. スプリントで完了にできたストーリーポイントの合計。

DatadogのRUM Session Replay費用削減について

アイキャッチ

こんにちは。プロダクト開発部でクラウドインフラエンジニアとして業務を行っている高澤です。

今回は、Datadogの料金削減の一環として行った、「RUM Session Replay の費用削減」についてお伝えします。

費用削減の効果を先に書くと、RUM Session Replayの料金がほぼ0ドルになりました。

この記事で伝えたいこと

Datadog の概要

Datadogとは、Datadog社が提供する監視・モニタリングのSaaSサービスです。

各リソースやクラウド(AWS,GCP,Azureなど)からメトリクス・ログなどを取り込み・連携し、モニタリングできるサービスです。

その他できることは多岐に渡ります。

Datadog の具体的な使用例

Datadogを利用し、現在以下をメインで行っています。

  • AWSのメトリクス連携をし、監視・通知
  • リソース監視・通知
  • ログを取り込み、可視化・検知
  • ダッシュボードを使用し、リソース状況やKPIを把握
  • SLO監視
  • Syntheticsテストの実施・監視・通知
  • アラート・オンコールの集約

Datadog費用削減に至る背景と見直しポイントについて

Datadogは上記機能を提供してくれて大変便利なサービスなのですが、データの取り込み量が増えるに従い、料金が増えていきます。

料金の削減が課題となり、プラン見直しも含め、Datadog営業担当の方と相談する機会を設けていただきました。

相談の結果、個々のサービスについて、最適なプラン変更などを実施していただき料金を抑える目処は付きました。

しかし「On-Demand RUM Session Replay」という請求項目については、「利用する・しない」を設定するものではありませんでした。

データを送っているとその分課金される種類の機能であり、呼び出し側でデータ送信の設定をしないといけない、と判明しました。

RUM Session Replayという機能について

この「RUM Session Replay」は、Datadogの請求から確認できる項目名です。

「RUM Session Replay」の具体的な機能としてはどのようなものでしょう、ということをまず確認しました。

を確認し、ポイントは以下です。

Datadogのセッションリプレイは、レビュー、分析、トラブルシューティングのために、ユーザーのWebブラウジング体験をキャプチャして視覚的に再生することができます。DatadogのエラートラッキングやAPMトレースによってフラグが立てられたユーザーセッションを再生することで、UXの問題をより迅速に特定し、修正することができます。

つまり、ユーザーのブラウズ操作(ユーザーセッション)をリプレイし、エラー発生や遅延を再現させ、問題解決に役立てる機能のようです。

RUM Session Replayの費用削減設定について

機能の利用有無についてチーム内確認

上記の機能説明にて「〜のようです。」という表現で書いている通り、このデータは意図して送信していたわけではなく、デフォルト設定のまま運用した結果、Datadogに送信されていたデータでした。

念の為、チーム内にて「RUM Session Replay」のデータを使った機能を利用している人がいないかを確認し、特にいないことが確認できたため、データ送信を止めることになりました。

対応方法についての確認

セッションリプレイ機能を無効にするドキュメントを確認しました。

上記ドキュメントで記述されている対応方法としては、以下です。

セッションの記録を停止するには、startSessionReplayRecording() を削除し、sessionReplaySampleRate を 0 に設定します。 これにより、リプレイを含む Browser RUM & セッションリプレイプランのデータ収集が停止します。

上記対応を実施していきます。

対応時の細かな問題

現在この機能のために使用している @datadog/browser-rum パッケージのバージョンは 3.11.0でした。

該当バージョンの場合、取りうる設定値の中に sessionReplaySampleRate という値がありませんでした。

そのため、以下パッケージのドキュメントを確認しました。

sessionReplaySampleRate の以前の値が、 replaySampleRate であったと読み取れます。

replaySampleRate については非推奨の値ですが、今回の対応ではパッケージのアップデートは見送ることとし、replaySampleRateを0としてみます。

設定及びデプロイ・設定効果確認

replaySampleRate を0に修正してデプロイします。

その結果を見るためには、Datadogにログインします。

左側メニューから「 UX Monitoring → Sessions & Replay 」を辿り、Real User Monitoring画面へ遷移します。

以下スクリーンショットのように Session Replay available にチェックを入れ、結果を見ます。

スクリーンショット

こちらで確認した結果、データが検索で出なくなっていることを確認できたため、対応完了となりました。

費用削減の結果

はじめに書いた内容の繰り返しになりますが、対応の結果、RUM Session Replayの料金がほぼ0ドルになりました。

まとめ

Datadogの料金削減ポイント・注意点を合わせてまとめると以下となります。

デフォルトのままだと意図していないデータが自動で送られ、料金高騰に繋がりますので、随時見直していきましょう。

  • 対応方法についてはドキュメントに記載されていることが多いので確認する
  • どうしてもわからない場合はDatadogサポートへ問い合わせる

機能のローンチ直後は無料であっても、有料になることが散見されますので、料金削減の観点からはなるべく明示的にデータを送らない設定にすることが有効です。

  • まず利用してみてから見極める
  • Datadogからのお知らせはよく確認し、料金改定・有料化のお知らせの場合は注意し対応を検討する

以上です。

YouTube Data API の Quota 上限アップ申請で注意すべき 3 つのこと

アイキャッチ

株式会社ブックリスタ プロダクト開発部の酒井です。

去年の秋頃、推し活アプリ「Oshibana」の新機能として「YouTube ウィジェット」を開発しました。
YouTube ウィジェットでは、ユーザーが登録中の YouTube チャンネル一覧から選択したチャンネルの最新動画および配信情報をウィジェット上に表示できます。
自分が推している YouTuber やゲーム実況者、芸能人、アイドルなどの新着動画や Live 配信を見逃さずに視聴できるので、非常に便利です。
ぜひとも使ってみてください。

YouTubeウィジェット YouTubeウィジェット



概要

ユーザーが登録中の YouTube チャンネル一覧は 「YouTube Data API」 の「Subscriptions: list」を利用して取得しています。
しかし、YouTube Data API では 1 日の API 最大使用量(Quota)が決まっています。
Quota は単純に API を呼び出した回数ではなく、呼び出し回数にコストをかけた数値になります。
API ごとにかかるコストが決まっており、例えば YouTube チャンネルの情報を取得する「channels:list」は 1 コストかかり、YouTube 動画を検索する「search:list」は 100 コストかかります。
デフォルトは 10000 であるため、その日中に「search:list」が 100 回呼ばれたら、次の 101 回目以降はエラーとなり、データが取得できなくなります。
API 実行は全ユーザーのリクエストが合計してカウントされるため、ユーザーが多ければ例え 1 コストだったとしても Quota が 10000 では心許ない数値となります。
※特に Oshibana では iOS のウィジェット上から定期的に API を実行するので、すぐに上限を超えてしまいます。

公式ドキュメント

YouTube Data API の概要 - クォータの使用量
https://developers.google.com/youtube/v3/getting-started?hl=ja#quota

YouTube Data API (v3) - Quota Calculator
https://developers.google.com/youtube/v3/determine_quota_cost

上限を上げるためには、YouTube に申請し、アプリが問題無いものであるか審査を受けて承認される必要があります。
申請内容やアプリ自体に問題があると申請が却下(リジェクト)されてしまい、以降はメールで直接 YouTube とやり取りすることになります。
相手はアメリカの企業であるため、返信メールの文面は全て英語で記述する必要があります。
基本的には Google 翻訳や DeepL 翻訳で大体伝わる内容になると思いますが、翻訳ミスにより内容が正しく伝わらない可能性もあるので、念のため文面に問題がないかある程度は確認しておいた方が良いです。

この記事では、申請を承認してもらうための注意点をできる限りまとめていきますので、これから申請を送る方たちの参考になれば幸いです。

なお、本記事の対象は iOS アプリ(ネイティブアプリ)が前提となります。
Android アプリ、Web アプリは対処法が異なる可能性がありますので、ご注意ください。



前提

この記事では、Google Cloud Platform(GCP)にて以下の作業が完了していることを前提としています。

  • プロジェクトの作成
  • YouTube Data API の有効化、API キーの取得
  • OAuth 同意画面の設定
  • Google OAuth 認証制限の解除



申請方法

申請は以下のページから行います。

YouTube API サービス - 監査と割り当て増加フォーム
https://support.google.com/youtube/contact/yt_api_form

アプリの開発組織の情報、アプリ本体(API クライアント)の情報、利用ユーザー数、API の使用方法などの基本情報を入力し、割り当てリクエストフォームの欄にて、追加割り当て量(デフォルトの 10000 から増やしたい数)、追加したい理由、追加しないとアプリで使えなくなる機能などを入力し、送信ボタンをクリックすれば申請が行われます。

申請画面は日本語でも表示できますが、内容は英語で送った方が良いです。

上限を無制限にはできないので、追加割り当て量は必要量をしっかりと計算し、申請する必要があります。
例えば Oshibana では、申請時点での API リクエストの実測値(アクティブユーザー数、画面の表示数、ウィジェット配置数など)から今後の成長予測を行い、将来の API リクエスト数を割り出し、それにコストをかけた数値を追加割り当て量として申請しています。

追加理由や上限突破で使用不可になる機能についても細かく記述しました。
追加理由については、上記の追加割り当て量の算出方法も含めてしっかりと明記した方が伝わりやすいです。



注意すべき 3 つのこと

1.デモ動画の作り方

Quota 上限アップの申請後、YouTube から「Quota の上限増加申請の理由と申請内容の根拠、および追加した Quota がどのようにアプリで利用されるのかを英語で解説した動画を準備せよ」という内容のメールが届くことがあります。
おそらく申請フォームに入力された内容だけでは判断がつかないため、実際にアプリを操作して、どのように API が呼び出されているのか、なぜ多量のリクエストが発生するのかを解説して欲しいということだと思われます。

動画は iPhone の画面録画機能を使って撮影し、YouTube へ投稿後、字幕機能を使って英語の字幕を付けました。
公開設定は「限定公開」にしており、動画の URL を返信メールで伝えています。

Oshibana のデモ動画では以下の内容を撮影しました。

  • Google OAuth 認証の手順
  • YouTube ウィジェットの作成方法およびホーム画面への配置方法
    → この時、各画面やウィジェットで実行されている API の種類や実行頻度について字幕で解説しています。
  • YouTube ウィジェットの削除方法
  • Google OAuth 認証の解除手順

特に 以下の 2 つは重要なポイントであるため、字幕でも個別に解説を入れています。

API スコープについて

API スコープとは、GCP の OAuth 同意画面で登録する「OAuth 認証の際にアプリのユーザーに許可を求める権限の範囲設定」のことです。
YouTube Data API の Subscriptions:list は「機密性の高いスコープ」に該当するため、API スコープの登録が必要となります。

「.../auth/youtube」のスコープは登録や更新等、表示以外にできることが多くなるため、「YouTube API サービス利用規約」に引っかかってリジェクトされるリスクが高くなります。
よって、API の取得結果を表示するだけなら「.../auth/youtube.readonly」で登録するのがオススメです。

GCP APIスコープ

Oshibana のデモ動画では、Google OAuth 認証の際に表示される YouTube アクセス許可ページで YouTube API が readonly のスコープであることを確認する内容を撮影しています。

アラートのメッセージが「View your videos and playlists (動画とプレイリストの表示)」となっていれば、readonly のスコープで登録されています。

ここでスコープが readonly であることをアピールすれば、YouTube からポリシー違反を指摘されるリスクが減ります。

APIスコープ APIスコープ

別件ですが、OAuth 同意画面は英語で表示されている必要があるので、表示言語を「English(United States)」に変更し、言語を切り替えている様子も撮影する必要があります。

言語変更 言語変更

API の実行方法について

YouTube API サービス利用規約 では「ユーザーの認証と認可」に関する規約があり、認証における制約やトークンの失効に関する内容が定められています。
アプリが規約を満たしていると判断されなければ、リジェクトされてしまいます。

例えば Oshibana では、「Subscriptions: list」を実行するために Google アカウントで OAuth 認証し、トークンを発行しています。
このトークンが OAuth 認証の解除処理で失効され、API が実行できなくなっていることを示す必要があります。
よって、デモ動画で Google OAuth 認証の解除手順を撮影し、トークンが失効されたことで API が実行できなくなった旨を字幕で説明しています。

ただし、API 実行に OAuth 認証を使っていない場合はその限りではありません。
Oshibana では、ホームに配置した YouTube ウィジェット上で「PlaylistItems: list」「Videos: list」「Channels: list」の API を実行していますが、これらは「非機密スコープ」の API であるため、OAuth のトークンを必要とせず API キーで実行しています。
よって、ウィジェットは OAuth 認証の解除後も API を実行し続けますが、認証不要で動く API であるためポリシーに違反してないことを字幕で説明しています。

認証の解除後も API が動いているので規約違反だと認識されないよう、API の実行箇所ごとに認証が必要なのかどうかをしっかりと解説するのがポイントです。


2.プライバシーポリシーの記載内容

以下の内容がアプリのプライバシーポリシーに記載されていないとリジェクトされます。

  • YouTube の利用規約への言及
  • アプリが YouTube API Services を使用していることの明記
  • Google のプライバシーポリシーへのリンク
  • Google のセキュリティ設定ページでアプリからのアクセス権が取り消せることについての言及

よって、Oshibana では以下のような内容をプライバシーポリシーに記載しています。

YouTube API サービスの利用
本アプリでは、動画情報などを取得するために YouTube API サービスを利用しています。
YouTube API サービスは、Google 社のプライバシーポリシー、YouTube の利用規約に基づいて提供されています。
YouTube の利用規約、YouTube API サービス利用規約、Google プライバシーポリシーについては以下をご覧ください。
Google プライバシーポリシー( https://www.google.com/intl/ja/policies/privacy/
YouTube( https://www.youtube.com/t/terms
YouTube API サービス利用規約( https://developers.google.com/youtube/terms/api-services-terms-of-service

また、ユーザーは、Google セキュリティ設定ページ( https://security.google.com/settings/security/permissions )から本アプリケーションのアクセス権を削除することで、本アプリケーションによるユーザーの Google アカウントへの接続を無効にすることができます。
ただしこれらの場合、本アプリケーションの一部の機能が使用できなくなる、または一部のページが正しく表示されなくなる場合があることをあらかじめご了承ください。

なお、プライバシーポリシーを修正した場合、本番環境にリリースしてから YouTube に返信してください。
ユーザーがプライバシーポリシーを確認できる状態になっている必要があるらしく、プライバシーポリシーの原文をファイルで送ったり、開発環境に掲載したものを共有するなどは許容されませんでした。
社内の法務確認などで時間を要する場合は早めの対応が必要です。


3.返信メールの文面

前述の通り、申請がリジェクトされ YouTube からメールが届いた場合は、指摘を解消後、英語でメールを返信する必要があります。
指摘の内容に対して、修正結果や対応方法などが明確に伝わるよう工夫すると、やり取りが少なくなります。

例えば Oshibana では、以下のような内容でメールを返信しています。

デモ動画について

指摘内容について解説した箇所を動画の再生時間を記載するなどで伝わりやすくしました。

[Response to Policy D Violations]
The application has been modified.
The points we would like to ask you to reconfirm are explained in the video.
Please turn on subtitles to confirm.
https://www.youtube.com/watch?v=XXXXXXXXXX

  • The scope of the YouTube API used was changed to readonly. (2:51〜)
  • The application connects to the YouTube API with an OAuth token, but the acquired user data is used only for display and is not stored in the DB. (3:52〜)
  • The iOS widget also calls the YouTube API, but only the YouTube API that can be executed in a non-confidential scope. The API key is then used to access the YouTube API. (5:14〜)
  • We added a button to disconnect YouTube account. (7:00〜)
    →This is in response to your point "Add an option to disconnect your YouTube account".
  • After disconnecting your YouTube account, you can switch to another YouTube account when reassociating. (8:53〜)

プライバシーポリシーについて

プライバシーポリシーは日本語で掲載しているため、英語に翻訳した状態でキャプチャを取り、追記した部分を赤枠で囲み、修正結果をわかりやすくしました。
公開されているプライバシーポリシーが日本語でも、ここで英訳した内容を伝えることができれば問題ありません。

[Response to Policy A Violations]
We have updated our Privacy Policy regarding Policy A.
https://oshibana.fun/privacy_policy.html

The red framed area in the following screenshot is the content added in this revision.

プライバシーポリシー



やってみた感想

海外の企業を相手にメールでやり取りするのは非常に大変でしたが、YouTube の字幕の付け方を覚えられたり、YouTube API サービス利用規約の内容に詳しくなったりと、申請以外にも色々なノウハウを得ることができたと思っています。
記事では全てを記載していませんが、YouTube とは実に半年近くやり取りをしており、特に後半は YouTube から指摘されたポリシー違反がなかなか解消できず、苦労しました。
やり取りが長くなると、実は過去に返答していた内容が誤って伝わっていたり、説明不足のままやり取りが進んでしまっているケースがあるので、その時は一度最初から振り返ってみるのも良いかもしれません。

長くなりましたが、本記事が皆さんの申請作業の役に立てば幸いです。

Reader StoreのPMのお仕事

アイキャッチ

自己紹介

はじめまして。プロダクト開発部の森本です。
現在、「Reader Store(運営:株式会社ソニー・ミュージックエンタテインメント)」のPMを担当しております。

PMと言っても、会社によって業務内容が多岐にわたるのでReader StoreのPMはこんなことしていますというのをイメージしていただければと思い記事にしました。

業務内容

最近実施した新機能開発の案件に沿ってお話しします。

課題抽出

2020年~2021年にかけて有名人気コミックがシリーズ完結を迎えました。
これまでReader Storeで上記シリーズを購入していたお客様は、シリーズの完結をきっかけにストアから離れてしまう可能性があります。
BIツールを使い、実際の数値を確認し、仮説を検証していきます。実際の数値を見ても、完結シリーズを購入していたお客様の数パーセントが、シリーズ完結以降、新しい商品をストアで購入していないことがわかりました。

新しく読みたいコミックがないお客様に対し、新しい出会いを提供し、ストアを継続して利用してもらいたいと考えます。
そんななかで、選んだコミックに似た商品を表示する機能(以後、「似た商品を探す」機能)を提供したいという要望が上がったので、この機能の導入について検討を進めました。

まず、「似た商品を探す」機能を導入することでどんな効果・インパクトがあるかを試算します。
ストアには新機能と同じように、タグを用いて商品を探す機能があるため、タグ機能と同じくらいの売上が見込めるのではないかと仮定しました。

また、「似た商品を探す」機能は、作品同士の似てる要素をグラフ化するなど独自性を持たせることで、お客様に楽しんでいただく機能です。その結果、利用者の拡大や利用頻度向上にも繋がるはずだと考えました。

要件整理

「似た商品を探す」機能の要件を整理していきます。

  • シリーズを指定し、似た商品を検索
  • 似た商品結果を表示するページを新設
  • コミックジャンルに限定
  • 書誌詳細、シリーズ詳細ページに結果ページへの導線を追加

新設ページの画面構成要素については、既存のデータの持ち方や既存コードを見て実現可能なものを判断しながらラフ案(ワイヤーフレーム)を作成しました。

また、このタイミングでやらないことも明確にしておきます。
今回はスモールスタートとして、改善を進めていく前提にし、コミック以外のジャンルは見送ることや、似た商品結果の利用範囲を書誌詳細やシリーズ詳細ページからの導線に限定することなどを事前に決めておきました。

各部との調整

デザイン検討・調整

要件整理で作成したラフ案をもとに、デザイナーへデザイン作成を依頼します。
今回の案件では、似ている要素を視覚的にわかりやすくするためにどうするかをポイントにしていたので、デザイン案をいくつか作成していただきました。

似た商品を探す・デザイン検討案

上記のキャプチャは一例ですが、A案のチャートの方がB案と比べて似た商品が並んだときにぱっと見て似ている要素がわかりやすいです。
ですが、検索元商品との比較がしづらいなどの課題にも気付きました。検索元商品の要素もチャート化することにより、視覚で比較できるようにする工夫を盛り込みました。

似た商品を探す・確定デザイン このようにデザイン案を見ながら、細かく関係者と議論して調整しました。

ビジネス部門との調整

ビジネス部門に向けて、開発目的や、変更内容・決まったデザインなどを共有します。
新しい機能のリリースになるため、リリース時のユーザー周知や露出の方法なども関係者を巻き込んで相談しながら決めていきました。

開発共有

開発者へ要件を説明します。
開発者が詳細設計をしていく中で、要件では明記されていなかった細かな仕様の詳細を詰めたり、開発スケジュールの確認をしたりします。

開発サポート

進捗状況や開発を進める中で出てきた課題を確認します。
必要があれば、仕様を再調整するなど、開発がスムーズに進むよう尽力します。

効果測定

「似た商品を探す」機能の効果が期待通りに出ているかリリース後に効果測定しました。
BIツールやGAの数値を見ていくつかの気付きがありました。
「似た商品を探す」機能は、他の既存機能と比較して、CVRや離脱率などでは良好な数値を出していましたが、PV数が想定していたより良くないことがわかりました。
新機能のユーザー周知が足りていないことが原因だと考え、改善するために他チームにも協力してもらい、PUSHやメルマガ配信を実施しました。さらにTOPページの露出追加を行い、多くのお客様に知っていただこうと画策中です。
このように、効果が良くない場合は改善できる方法を考え、できることから進めていきます。

案件以外の業務内容

問い合わせ対応

運用チームやCSからの問い合わせ対応を行います。
コードやデータを見て、状況確認します。

システム対応が必要な場合は、対応の進め方を検討したり、関係部門との調整したりします。

障害対応

システム障害が発生したときには、開発メンバーの音頭をとって障害対応を進めます。

開発プロセス改善

開発が迅速に進むよう、開発ルールの整備など開発プロセス改善も進めます。

全体を通して

やりがい

新しい機能をリリースして終わりではなく、効果検証をして改善を進めていくことができます。単発ではなく、一貫して関わることができます、もっとこうしたいということを実現するチャンスが多いです。

リリースした機能に対して、社内評判が良かったり、感謝の声掛けをしてもらえるとやって良かったなと感じます。
また、ユーザーからの良い感想などを見ることでも嬉しい気持ちになります。

大変なところ

さまざまな部署やチームに説明する上で、知りたいポイントがそれぞれ違います。そのため、それぞれが理解しやすいように伝えないといけないです。資料作成など時間がかかってしまい、つらいです。

やりたいことがたくさんあり、行列待ちになっているので、できないことも出てきてしまいます。優先順位を上手く決めることややらないことを判断するのは難しいです。

まとめ

Reader StoreのPMは業務が幅広く、私自身まだまだできないことも多いですが、その分やりがいがあり、成長できる仕事です。
これからも日々の業務を頑張っていきます。

最後まで読んでいただきありがとうございました。

コミックの類似性の算出とそのシステム構成について

アイキャッチ

はじめまして。プラットフォームソリューション部の山口です。
今回は、昨年2022年末にReader Store(運営:株式会社ソニー・ミュージックエンタテインメント)にてリリースされた「似た商品を探す」の裏側について紹介します。
(現在は、コミックのみで利用できる機能です。)

機能

まずは、「似た商品を探す」機能について簡単に紹介します。
簡単には、「○□ぽい本」、「□○みたいなテイストの本」 を見つけることができる機能と考えています。
現在は、あるコミックの要素を算出して、その要素と近しい作品たちが一覧となって表示されます。

例えば、幕末を舞台にしたバトルマンガであれば、そのマンガは「幕末」「バトル」「刀剣」などの要素が強いだろうと算出します。
そして、それらの要素の傾向を持っているマンガを算出しています。

このあたりは実際に、Reader Store にて好きな、 もしくは興味のあるマンガを検索し、 「似た商品を探す」を使っていただけるとわかりやすいですので
よければ一度利用してみてください。

類似性とシステム

さて、ここからはよりシステム的な面での機能説明や裏側のことについて紹介していきます。
次のような区分にわけて説明します。

  • 要素の抽出と類似性について
  • 検索方法とシステム構成

要素の抽出と類似性について

「似た」と一口に言っても、どう似ているのか、どの程度似ているのかなど気になることがあります。
その点をシステム的にどう算出し、どう表現するのかをまずは説明します。
本を構成する要素は多分にありますが、どんな本であるかがわかる1つの要素として「あらすじ」や「説明文」があります。
今回は、「説明文」に絞って説明します。

説明文を1つとっても使われている言葉・単語や表現は多岐に渡ります。
我々はまずWord2Vecなどの機械学習アルゴリズムを利用して、この表現方法を学習し、ベクトル化することを行いました。

例えば、次のように説明文を利用して、ベクトルを算出しました。

  • マンガA ならば (0.1, 0.2, 0.3)
  • マンガB ならば (0.3, 0.1, 0.6)

この時点でも、このベクトルを利用することで、似た作品の算出はできますが、「何故似たのか」「どこが似ているのか」がわかりません。

そのため、「バトル」「刀剣」などの要素についても同様にベクトル化を行い、
本ごとにどの要素と類似しているのか(その要素を持っているのか)を算出できないかと考えました。
「バトル」「刀剣」などの要素のベクトル化は、本の説明文のベクトル化と同様の手法で算出しました。

例えば、次のようにです。

  • バトル ならば (0.0, 0.0, 0.9)
  • 刀 剣 ならば (0.3, 0.1, 0.0)

これで、本のベクトルと要素のベクトルが算出されました。
次に、「本ごとにどの要素と類似しているのか」を算出します。
この算出にはいろいろな手法があります。
例えば、ユークリッド距離やCosin類似度などです。
今回は、簡単にL1ノルムを利用して、「マンガA」と「バトル」との距離を出してみます。

バトル 刀剣
マンガA 0.9 0.6
マンガB 0.7 0.6

例えば、マンガAは「バトル」が0.9、「刀剣」が0.6とバトルが強い作品だろうと算出します。
また、マンガAとマンガBは「バトル」の要素を強くもち、「刀剣」については同程度触れている作品だろうと予測できます。

結果、マンガAの似た作品として「バトル」の要素を強くもち「刀剣」要素もあるマンガBを算出します。

実際には他の計算手法の利用やスコアのBoostingなどを行っていますが、
このようなステップで、本の要素の抽出と類似性の算出をしています。

検索方法とシステム構成

本のベクトル化やその計算方法について説明しましたが、ここでは実際のシステムに落とし込んだ構成などについて紹介します。

主だったものとして以下を利用しています。

  • AWS Lambda: 本のベクトル化などの算出に利用
  • AWS ECS: APIサーバーとして
  • OpenSearch: 書籍間の類似度の計算と絞り込みのため

今回は、運用上の観点からAWSのマネージドサービスを優先的に選択しています。
簡単に、どのような利用をしているか説明します。

簡易な構成図

本のベクトル化

まず、本のベクトルの処理は、日々多くの書籍を扱う関係上、AWS Lambdaを利用しています。
AWS Lambdaの同時実行を利用することで、数分間に数万件のベクトル化を行っています。
実際には、以下の3フェーズにわけて、処理できるようにしています。

  1. 説明文の形態素解析
  2. 説明文のベクトル化
  3. 要素との類似度の算出とDBへの挿入

また、これらの処理結果は都度S3に保存することで、
途中の処理で失敗しても、中断箇所からやり直せるようにしています。

類似作品の検索

次に、ベクトル化した本同士の類似度の計算には、OpenSearchを利用しました。
OpenSearchの持っているKNN近傍検索によって、類似度の計算をしています。
さらに、ビジネス要件にそった絞り込みに関しても、OpenSearchの検索機能を利用しています。
そのため、一度のリクエストでビジネス要件にそった類似作品の抽出を行えるようにしています。

検索API

最後に、いままでの機能を外部に提供するためのAPIについて説明します。
利用状況に応じたスケールなどの運用上の観点から、AWS ECSを利用しています。
スケールに関しては、ECS キャパシティープロバイダーとEC2 Auto ScaringGroupを利用しています。
これらを利用することで、タスクの負荷状況に応じてタスクのスケールイン/スケールアウトから、
EC2インスタンスのスケールイン/スケールアウトまでを自動で行えます。
これにより、必要なタスクの増減に伴った、インスタンスの増減に繊細な注意を払う必要がなくなりました。
おかげで、一見するとシンプルな構成にできたのではないかと考えています。

最後に

本機能は、よりお客様が本と出会える機会を増やせないかという考えを元に開発が進められました。
未熟な部分もありますが、「お、こんな本があったのか」などの本との出会いの一助となれれば幸いです。