App Store Connect APIを使ってアプリのインストール数を取得する
つい最近個人開発アプリをだし、そのアプリのインストール数とかをapp storeのanalyticsで見ていたのですが、いちいち見に行くのが面倒くさかったり、Slack等と連携したかったので、アプリアナリティクスの数字をとってくれるいい感じのないかな〜と探したところ、Appleから公式のAPIが出ていたので、これを使ってみることにしました。
仕様としては、インストール数や課金登録数などを一日一回Slack等に通知してくれる、というものです。
久しぶりにこういうのを作ってみるということで、格安サーバーを探してみました。
以前はHerokuが個人開発者用の代表的サービスだったのですが、自分が業界離れている間に無料プランが終了していたようです。
ということで、他のサービスを探してみて、結果的に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の流れとしては、
- 自分の認証情報をリクエストしてJWT tokenを発行
- 発行したトークンでAPIリクエスト
- リクエスト成功したらインストール情報等が含まれたファイルが送られてくる。形式は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になります。
クエリパラメータを設定します。適宜自分の取得したいパラメータに変更してください
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からいい感じでできないかを今度は試してみるつもりです。
ご覧いただきありがとうございました。
Discussion