🍎

App Store Connect APIを使ってアプリのインストール数を取得する

2023/11/06に公開

つい最近個人開発アプリをだし、そのアプリのインストール数とかをapp storeのanalyticsで見ていたのですが、いちいち見に行くのが面倒くさかったり、Slack等と連携したかったので、アプリアナリティクスの数字をとってくれるいい感じのないかな〜と探したところ、Appleから公式のAPIが出ていたので、これを使ってみることにしました。

仕様としては、インストール数や課金登録数などを一日一回Slack等に通知してくれる、というものです。

久しぶりにこういうのを作ってみるということで、格安サーバーを探してみました。
以前はHerokuが個人開発者用の代表的サービスだったのですが、自分が業界離れている間に無料プランが終了していたようです。

https://forest.watch.impress.co.jp/docs/news/1435422.html

ということで、他のサービスを探してみて、結果的にGCPが一番良さそう、ってことに落ち着きました。

デプロイ前に自分でテストしたかったので、有名なAPIテストツールであるPostmanを利用しました。
しかし、ちょっと問題があって結局Postmanは使いませんでした。(理由は後述)

今回のApp Store Connect APIはJWT認証を使ったAPIということでした。
JWT認証については生成AIのBardでこのように説明します。(JWT認証については自分で調べたので、内容自体は間違っていないことを確認済みです。)

JWTは、以下の3つの部分から構成されています。

  • ヘッダー
  • ペイロード
  • 署名

ヘッダーには、JWTの形式と署名アルゴリズムが記載されています。
ペイロードには、ユーザーのIDや権限などの情報が記載されています。
署名には、ヘッダーとペイロードを組み合わせて生成したハッシュ値が記載されています。
JWT認証では、まず、ユーザーが認証サーバーに対してログインを行います。認証サーバーでは、ユーザーのIDとパスワードを確認して、認証に成功した場合は、JWTを生成します。
ユーザーは、このJWTをAPIリクエストのヘッダーに指定して、APIサーバーに送信します。APIサーバーは、JWTの署名を検証して、有効なトークンであることを確認します。有効なトークンであれば、APIサーバーは、ユーザーの要求を処理します。
JWT認証のメリットは、以下のとおりです。

  • クライアント側で認証情報を保持する必要がないため、セキュリティが向上する
  • 認証サーバーからAPIサーバーに認証情報を渡す必要がないため、パフォーマンスが向上する
  • サーバーレス環境でも利用できる
    JWT認証のデメリットは、以下のとおりです。
  • 署名アルゴリズムの選択が重要である
  • 署名アルゴリズムが脆弱化した場合、トークンが改ざんされる可能性がある

JWT認証は、APIベースのアプリケーションでよく利用される認証方式です。セキュリティとパフォーマンスを両立できるため、多くの企業で採用されています。
(Bardより生成:prompt, JWT認証についてわかりやすく説明してください)

これをPostmanでやろうとしたのですが、JavaScriptしかできなく、requireでmoduleをimportができなかったので時間かかりそうだったため、ローカルでやることにしました。(Postmanを辞めた理由の伏線回収)

App Store Connect APIの流れとしては、

  1. 自分の認証情報をリクエストしてJWT tokenを発行
  2. 発行したトークンでAPIリクエスト
  3. リクエスト成功したらインストール情報等が含まれたファイルが送られてくる。形式はgzip

JWT tokenは最大20分有効なため、その有効期間であればJWT tokenを発行する必要はありません。しかし、今回はテストの段階だったので、都度トークンを生成しています。

それでは流れにそって具体的なコードとともに説明していきます。

1. 自分の認証情報をリクエストしてJWT tokenを発行

JWTトークンはheader, payload, signatureの3つから構成されています。
headerとpayloadは公開されており、この2つから秘密鍵を用いて、signatureを生成します。Generating Tokens for API Requests

App Store APIのheaderは3つのパラメーターがあります

headers = {
    "alg": "ES256",
    "kid":"Your Private Key ID ex. 2X9R4HXF34.",
    "typ": "JWT",
}

このkidを取得するために、App Store Connect APIのKeyを発行します。
App Store Connect→User and Access→Keys
ここでKeyを発行できます。発行する際にロールを選択するのですが、どうやらインストール数などはFinanceのロールを選択しないとダメらしいです。参考

ここのKEY IDのところがPrivate KeyのIDです。

次にpayloadです。
issは上の画像の発行者ID、iatはトークン生成時刻、expはトークン消滅時刻です。

payload = {
    "iss": "Your Issuer ID",
    "iat": int(time()),
    "exp": int(mktime(dt.timetuple())),
    "aud": "appstoreconnect-v1",
}

以上のheader,payloadを使って、signatureを生成します。
signatureを生成するために暗号鍵が必要です。KEYを発行したページからKEYのダウンロードをします。(ダウンロードは各KEYにつき1回のみなのでわかりやすいところに保存してください)

JWTトークンの生成には、Pythonのjwtモジュールを仕様します。
上記の一連の流れを書いたものが次です。

from datetime import datetime, timedelta
from time import time, mktime
import logging
import jwt

dt = datetime.now() + timedelta(minutes=19)

headers = {
    "alg": "ES256",
    "kid":"Your Private Key ID",
    "typ": "JWT",
}

payload = {
    "iss": "Your Issuer ID",
    "iat": int(time()),
    "exp": int(mktime(dt.timetuple())),
    "aud": "appstoreconnect-v1",
}

with open("AuthKey_PEIVATEKY.p8", "rb") as fh: // 適宜ファイル名を変更してください
    signing_key = fh.read()
    
gen_jwt = jwt.encode(payload, signing_key, algorithm="ES256", headers=headers)

print(f"[JWT] {gen_jwt}")

これを実行してみると、トークンが発行されます。

2. 発行したトークンでAPIリクエスト

APIのエンドポイントはhttps://api.appstoreconnect.apple.com/v1/salesReportsになります。

https://developer.apple.com/documentation/appstoreconnectapi/download_sales_and_trends_reports

クエリパラメータを設定します。適宜自分の取得したいパラメータに変更してください

params = {
    'filter[frequency]':'DAILY',
    'filter[reportDate]':'2023-10-22',
    'filter[reportSubType]': 'SUMMARY',
    'filter[reportType]': 'SALES',
    'filter[vendorNumber]': '87516456'
}

こちらのvendorNumberはApp Store ConnectのFinancialの左上に表示されています。

リクエストする際に気をつけるのは、インストールが0の日付の場合、404がレスポンスで返ってきてしまいます。なので、インストールがある日付を選択してください。

import urllib.request
import urllib.error

headers = {
  'Authorization': 'Bearer ' + gen_jwt,
  'Accept': '*/*'
}

api_url = 'https://api.appstoreconnect.apple.com/v1/salesReports'

# クエリパラメータ
params = {
    'filter[frequency]':'DAILY',
    'filter[reportDate]':'2023-10-22',
    'filter[reportSubType]': 'SUMMARY',
    'filter[reportType]': 'SALES',
    'filter[vendorNumber]': '87516456'
}

# APIリクエスト
req = urllib.request.Request('{}?{}'.format(api_url, urllib.parse.urlencode(params)), headers=headers)

3. リクエスト成功したらインストール情報等が含まれたファイルが送られてくる。形式はgzip

いよいよ最後の段階です。
リクエストが成功したらgzipでレスポンスが返ってきます。
今回はそれをjson形式で保存しました。

try:
    with urllib.request.urlopen(req) as res:
        # GZIPデータを解凍して保存
        with gzip.open(res, 'rb') as gzipped_file:
            output_file = "hoge.json"
            try:
                decompressed_data = gzipped_file.read()
                with open(output_file, 'wb') as output:
                    output.write(decompressed_data)
                print(f"データを保存しました: {output_file}")
            except Exception as e:
                print(f"データの解凍と保存に失敗しました: {str(e)}")
    code = 200
    
except (urllib.error.HTTPError, urllib.error.URLError) as e:
    logging.error("Error occurred in urllib.request.Request().\n code: " + str(e.code) + \
                    "\n reason: " + e.reason)
    code = e.code
    content = e.reason

成功すると次のようなレポートが取得できます。

これでインストール数などのデータを取得することができました。
ただ、個人的にはApp Analyticsのインプレッション数などを取得できなかったのが残念です。
また、整形したりインストールがない日付の場合は404がレスポンスになってしまう、という感じで使い勝手が悪そうなので、Firebase Analyticsからいい感じでできないかを今度は試してみるつもりです。

ご覧いただきありがとうございました。


参考サイト

GitHubで編集を提案

Discussion