Google Analytics4(GA4)のAPI対応

Google Analytics4のAPI利用方法を紹介します。

Ishiguro Suguru

Googleアナリティクスのバージョン変更に伴う対応をしたのでその時のメモです。

現在のGoogleアナリティクスは**ユニバーサルアナリティクス(UA)**と呼ばれるバージョンですが、 これが2023年7月1日から使えなくなるよと管理画面からも警告されていたので対応しました。

対応概要

ゴール

  • GA4のAPIを使用してWebサイトのアクセス数などの情報を取得する
  • 取得した情報はAWSのS3に保存する
  • S3に保存したデータをJavaScript(jQuery)でサイトに表示する

図で書くのは省略しますがAWSリソースはこんな流れのイメージです。 CloudWatchで定期的にLambdaを実行してGA4のAPIを呼び出しています。 APIで取得したデータはS3に保存します。

CloudWatch → Lambda → S3 → CloudFront → Webブラウザ参照 (JavaScript)

なおこの実装は運用しているまとめサイトの人気記事ランキングの表示に使っています。

漫画まとめ速報

作業概要

  1. GoogleアナリティクスでGA4プロパティを作成してWebサイトにタグを埋め込む →Googleタグマネージャーで対応。内容は省略。
  2. GCPでAPI有効化、権限設定
  3. AWS LambdaでGA4 API実行コード (python)を作成、デプロイ
  4. フロントエンド処理を作成 (JavaScript)

参考サイト

以下はOAuthを使用した例。今回は割愛。

1.GA4 API有効化手順

API利用にあたって今回はサービスアカウントを作成します。 以下の画面に従って設定を行っていきます。

GCP設定

GCPにアクセスして設定を実施。

Googleアナリティクス設定

GCPで作成したサービスアカウントを対象のGoogleアナリティクスに追加。 権限は閲覧でデータを参照できる。

2.AWSリソース設定(Lambda、CloudFormation)

  • CloudFormationでリソースは作成。
  • LambdaはDocker利用、実行コードはpython。

Lambda

srcディレクトリ配下に以下のファイルを置く。

  • app.py: メイン処理実行
  • credential.json: GCPでダウンロードした秘密鍵ファイル。名前を変更して利用。
  • Dockerfile: Docker定義
  • requirements.txt: 使用ライブラリ記述
  • SETTING.py: GA4のプロパティなど設定情報記述

app.py

"""Lambda実行ファイル

* Google Analytics4(GA4)対応のレポート取得コード
* SETTING.pyファイルにGA4のIDやレポート取得条件を定義

"""
import os
import json
import SETTING
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import BatchRunReportsRequest


def ga4_request(ga_id, requests):
    """GA4レポートのリクエスト実行

    Args:
        ga_id (str): GA4プロパティID
        requests (obj): GA4リクエスト
    Returns:
        google.analytics.data_v1beta.types.BatchRunReportsResponse:
            The batch response containing multiple reports.
    """
    client = BetaAnalyticsDataClient()

    # BatchRunReportsRequest: 複数リクエストを投げたいときに使用する
    # RunReportsRequest: リクエストが一つの場合はこれを使う
    request = BatchRunReportsRequest(
        property=f"properties/{ga_id}",
        requests=requests
    )

    # RunReportsRequestを使った場合はrun_reportを使用する
    res = client.batch_run_reports(request)

    return res


def parse_result(ga4_response):
    """GA4リクエストのレスポンスを加工

    GA4レポートデータの加工を行う。
    BatchRunReportsResponseを使って複数レポートを取得した場合の処理を記述。
    RunReportsResponseを使った場合はレスポンスの形式が少し異なるため注意。

    Args:
        ga4_response (google.analytics.data_v1beta.types.BatchRunReportsResponse):
            BatchRunReportsRequestで取得する結果を設定する。
    Returns:
        list of obj:
            レポートを格納したリスト。
            ディメンション名・メトリック名をつけた形式へ加工する。
    """
    result_all = []

    for i, report in enumerate(ga4_response.reports):
        result_row = []

        for row in report.rows:
            data = {}
            # 取得したレポートはディメンション名・メトリック名がないためkeyとして付与する
            for j, key in enumerate(report.dimension_headers):
                # ページタイトルに不要文字があるためここで削除しておく
                data[key.name] = row.dimension_values[j].value.replace(' - 漫画まとめ速報', '')
            for j, key in enumerate(report.metric_headers):
                data[key.name] = row.metric_values[j].value
            result_row.append(data)

        result_all.append(result_row)

    return result_all


def lambda_handler(event, context):
    """Lambda function"""

    # Google Analyticsアカウント設定
    os.environ[SETTING.GOOGLE_APPLICATION_CREDENTIALS] = SETTING.KEY_FILE_LOCATION

    # リクエスト(メトリック=PV)
    response = ga4_request(SETTING.GA_PROPERTY_ID, SETTING.REQUESTS_PV)
    # レポート加工
    result = parse_result(response)
    # S3へレポート結果登録
    SETTING.CLIENT.put_object(
        Body=json.dumps(result),
        Bucket=SETTING.S3_BUCKET_NAME,
        CacheControl=SETTING.CACHE_CONTROL,
        ContentType=SETTING.CONTENT_TYPE,
        Expires=SETTING.EXPIRES,
        Key=SETTING.SAVE_FILE_PV
    )

    # リクエスト(メトリック=ユーザー数)
    response = ga4_request(SETTING.GA_PROPERTY_ID, SETTING.REQUESTS_USERS)
    # レポート加工
    result = parse_result(response)
    # S3へレポート結果登録
    SETTING.CLIENT.put_object(
        Body=json.dumps(result),
        Bucket=SETTING.S3_BUCKET_NAME,
        CacheControl=SETTING.CACHE_CONTROL,
        ContentType=SETTING.CONTENT_TYPE,
        Expires=SETTING.EXPIRES,
        Key=SETTING.SAVE_FILE_USERS
    )

    return

SETTING.py

データ取得条件はJSONで記述従ったが変換処理に手間がかかるため、API仕様そのままで記述している。 ちなみにサービスアカウントではなく、OAuth認証もしくはGCPのCLIを使用すればJSON形式でリクエストを そのまま食わせられるが調査に時間がかかるため断念…

途中まで調べたので気が向いたら記事を書く予定。

"""設定ファイル

* Google Analytics4(GA4)およびAWS設定値を定義
* GA4レポートデータの保存先もここで定義しておく

"""
import boto3
import os
from datetime import datetime
from google.analytics.data_v1beta.types import (
    DateRange,
    Dimension,
    Filter,
    FilterExpression,
    FilterExpressionList,
    Metric,
    OrderBy,
    RunReportRequest
)

# =====================
# AWS
# =====================
CLIENT = boto3.client("s3")
# データ保存先のS3バケット名はtemplate.yamlで定義
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME")

# S3ファイル保存時のメタデータ
CACHE_CONTROL = "public, max-age=0"
CONTENT_TYPE = "application/json"
EXPIRES = datetime(1990, 1, 1)

# S3保存先 <環境に合わせて変更する>
SAVE_FILE_PV = "ga4/manga/report_pv.json"
SAVE_FILE_USERS = "ga4/manga/report_users.json"

# =====================
# GA4
# =====================
# GA4プロパティID
GA_PROPERTY_ID = "<環境に合わせて変更する>"

# ダウンロードしてきた認証情報のファイル名
KEY_FILE_LOCATION = "credential.json"
# 認証設定
GOOGLE_APPLICATION_CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS"

# リクエスト時に使用するレポートの取得条件。
# date_rangesに複数期間を指定してレポートを取得することはできるが、
# レスポンスのデータ加工に手間がかかるためここでは期間をわけて変数を用意。
REQUEST_PV_POST_30DAYS_AGO = RunReportRequest(
    dimensions=[
        Dimension(name="pagePath"),
        Dimension(name="pageTitle"),
    ],
    metrics=[
        Metric(name="screenPageViews"),
    ],
    date_ranges=[
        DateRange(start_date="30daysAgo", end_date="today"),
    ],
    dimension_filter=FilterExpression(
        filter=Filter(
            field_name="pagePath",
            string_filter=Filter.StringFilter(
                value="/post/",
                match_type=Filter.StringFilter.MatchType.CONTAINS,
                case_sensitive=False
            ),
        )
    ),
    order_bys=[
        OrderBy(desc=True, metric=OrderBy.MetricOrderBy(metric_name="screenPageViews"))
    ],
    limit=30
)

REQUEST_PV_POST_7DAYS_AGO = RunReportRequest(
    dimensions=[
        Dimension(name="pagePath"),
        Dimension(name="pageTitle"),
    ],
    metrics=[
        Metric(name="screenPageViews"),
    ],
    date_ranges=[
        DateRange(start_date="7daysAgo", end_date="today"),
    ],
    dimension_filter=FilterExpression(
        filter=Filter(
            field_name="pagePath",
            string_filter=Filter.StringFilter(
                value="/post/",
                match_type=Filter.StringFilter.MatchType.CONTAINS,
                case_sensitive=False
            ),
        )
    ),
    order_bys=[
        OrderBy(desc=True, metric=OrderBy.MetricOrderBy(metric_name="screenPageViews"))
    ],
    limit=30
)

REQUEST_PV_POST_1DAYS_AGO = RunReportRequest(
    dimensions=[
        Dimension(name="pagePath"),
        Dimension(name="pageTitle"),
    ],
    metrics=[
        Metric(name="screenPageViews"),
    ],
    date_ranges=[
        DateRange(start_date="1daysAgo", end_date="today"),
    ],
    dimension_filter=FilterExpression(
        filter=Filter(
            field_name="pagePath",
            string_filter=Filter.StringFilter(
                value="/post/",
                match_type=Filter.StringFilter.MatchType.CONTAINS,
                case_sensitive=False
            ),
        )
    ),
    order_bys=[
        OrderBy(desc=True, metric=OrderBy.MetricOrderBy(metric_name="screenPageViews"))
    ],
    limit=30
)

REQUEST_PV_TAGS_7DAYS_AGO = RunReportRequest(
    dimensions=[
        Dimension(name="pageTitle"),
    ],
    metrics=[
        Metric(name="screenPageViews"),
    ],
    date_ranges=[
        DateRange(start_date="7daysAgo", end_date="today"),
    ],
    dimension_filter=FilterExpression(
        and_group=FilterExpressionList(
            expressions=[
                FilterExpression(
                    filter=Filter(
                        field_name="pagePath",
                        string_filter=Filter.StringFilter(
                            value="/tags/",
                            match_type=Filter.StringFilter.MatchType.CONTAINS,
                            case_sensitive=False
                        ),
                    )
                ),
                FilterExpression(
                    not_expression=FilterExpression(
                        filter=Filter(
                            field_name="pageTitle",
                            string_filter=Filter.StringFilter(
                                value="Tags - 漫画まとめ速報",
                                match_type=Filter.StringFilter.MatchType.CONTAINS,
                                case_sensitive=False
                            ),
                        )
                    )
                ),
            ]
        )
    ),
    order_bys=[
        OrderBy(desc=True, metric=OrderBy.MetricOrderBy(metric_name="screenPageViews"))
    ],
    limit=30
)

REQUEST_USERS_SOURCE = RunReportRequest(
    dimensions=[
        Dimension(name="sessionSource"),
    ],
    metrics=[
        Metric(name="activeUsers"),
    ],
    date_ranges=[
        DateRange(start_date="7daysAgo", end_date="today"),
    ],
    order_bys=[
        OrderBy(desc=True, metric=OrderBy.MetricOrderBy(metric_name="activeUsers"))
    ],
    limit=100
)

REQUEST_USERS_CLICK = RunReportRequest(
    dimensions=[
        Dimension(name="linkDomain"),
    ],
    metrics=[
        Metric(name="activeUsers"),
    ],
    date_ranges=[
        DateRange(start_date="7daysAgo", end_date="today"),
    ],
    order_bys=[
        OrderBy(desc=True, metric=OrderBy.MetricOrderBy(metric_name="activeUsers"))
    ],
    limit=100
)

# 各リクエストを配列に格納。
# BatchRunReportsRequestで一度にリクエストできる上限は5個までのため注意。
REQUESTS_PV = [
    REQUEST_PV_POST_1DAYS_AGO,
    REQUEST_PV_POST_7DAYS_AGO,
    REQUEST_PV_POST_30DAYS_AGO,
    REQUEST_PV_TAGS_7DAYS_AGO,
]

REQUESTS_USERS = [
    REQUEST_USERS_SOURCE,
    REQUEST_USERS_CLICK,
]

requirements.txt

google-analytics-data
google-api-python-client
boto3

Docerfile

FROM public.ecr.aws/lambda/python:3.9

# 使用するファイルを追記
COPY app.py SETTING.py requirements.txt credential.json ./

RUN python3.9 -m pip install -r requirements.txt -t .

# Command can be overwritten by providing a different command in the template directly.
CMD ["app.lambda_handler"]

CloudFormation

以下のコードでリソースを作成。 PyCharmでデプロイ。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  python3.9

  Lambda and EventBridge SAM Template for getGA4Report

Globals:
  Function:
    Timeout: 3
    Environment:
      Variables:
        TZ: Asia/Tokyo
        S3_BUCKET_NAME: <S3バケット名を記入>

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: GetGA4ReportLambdaRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: "/"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AmazonS3FullAccess

  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: GetGA4ReportLambda
      Role: !GetAtt LambdaRole.Arn
      Timeout: 30
      MemorySize: 512
      PackageType: Image
      Architectures:
        - x86_64
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./src
      DockerTag: python3.9-v1

  Rule:
    Type: AWS::Events::Rule
    Properties:
      Description: Scheduled Rule
      Name: GetGA4ReportLambdaRule
      # 30分ごとに実行
      ScheduleExpression: 'cron(0/30 * * * ? *)'
      State: ENABLED
      Targets:
        - Arn: !GetAtt LambdaFunction.Arn
          Id: lambda

  FunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref LambdaFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt Rule.Arn

Outputs:
  LambdaFunction:
    Description: "Lambda Function ARN"
    Value: !GetAtt LambdaFunction.Arn
  LambdaRole:
    Description: "Implicit IAM Role created for function"
    Value: !GetAtt LambdaRole.Arn

CloudFront

GA4のアクセス情報をS3に保存するが、CloudFront経由で参照できるように設定。 詳細は割愛。

3.フロントエンド処理 (JavaScript)

S3データを取得して画面に表示。 jQueryを使用。

// 人気記事ランキングのリンク生成
$(function () {
  // console.log("_____rank.js: load");

  // 人気記事の最大表示数
  let rankCount = 10;
  // JSONファイル取得先
  let fileUrl = "/ga4/manga/report_pv.json";

  $.getJSON(fileUrl)
    .done(function (response) {
      // console.log("_____rank.js: ajax success", response);

      let count = 0;
      response.forEach(function (data) {
        // olタグを生成
        let ol = $("<ol>").addClass("popular__list");

        // 取得データは期間別に配列形式で格納されている。
        // postページのデータが格納されているが、最終値はtagページのデータ。
        // [0]: today~1day ago
        // [1]: today~7days ago
        // [2]: today~30day ago
        // [3]: today~7day ago(tag page)
        if (count < 3) {
          // 挿入先のIDを生成
          let id = "#tab-content-0" + count;
          count = count + 1;

          for (let i = 0; i < rankCount; i++) {
            let title = data[i].pageTitle;
            let url = data[i].pagePath;

            // liタグを生成してaタグ追加
            let li = $("<li>")
              .addClass("popular__item")
              .append(`<a href="${url}">${title}</a>`);
            // olに生成したliタグを追加
            ol.append(li);
          }

          // 指定したIDに生成したリンクを挿入
          $(id).html(ol);
        }
      });
    })
    .fail(function (e) {
      console.log("_____rank.js: ajax fail", e);
    });
});

最後に

Google Analyticsで今までAPIを使っていた方は、そこまで使い方変わっていないので時間はかからないと思います。

comments powered by Disqus