Google Apps ScriptでGoogle Analyticsのデータを取得する(GA4対応)【GAS】

2023年7月1日、Google Analyticsが大きく生まれ変わるにあたって、現在、Google Analytics 4への移行が推奨されています。これまでのトラッキングコードであるUA-XXXは利用できなくなり、G-XXXXという新しいコードで新しい解析手法でデータを取得することになります。

Google Apps Scriptではこれまで、Google Cloud Consoleで設定を行えば利用できましたが、今回の移行に合わせてGA4対応の「Google Analytics Data API」がリリースされました。GCPでの設定が不要でGASから簡単に統計データを取得する事が可能になっています。今回はGA4対応のスクリプトを利用してみたいと思います。

今回使用するスプレッドシート等

旧Google Analyticsとは初期の準備が大分ことなります。公式でもアナウンスされていますが、移行期限までに移行作業をしておかないと、ウェブアクセス解析が出来なくなるので、要注意です。

また今回は、WordPressで使用する事が前提です。プラグインはSimple SEO PackがGA4に対応しているので、導入しましょう。

※ただ、これまでの旧版と違い取得できるデータが随分と簡素になってしまったような。これから色々追加されると思うのですが。

事前準備

GA4へ移行する

既に移行の為のアナウンスがAnalyticsのページの上部に出ているのでそこから、移行作業を開始します。左サイドバーの管理⇒GA4設定アシスタントでも同じページを開く事が出来ますので、まずはそこから初めます。

  1. 新しい Google アナリティクス 4 プロパティを作成するのはじめにをクリックする
  2. ダイアログが出たら、「プロパティを作成」をクリックする
  3. GA4プロパティを確認をクリック
  4. アシスタントの設定がが開かれるので、表示されてるURLが正しいか確認(違っていたら、データストリームから変更可能
  5. ウェブタブをクリックして、ウェブストリームの詳細を開く
  6. 自分のサイトのレコードをクリックする
  7. 測定 IDが新しいトラッキングコードになるので、これをコピーしておく(これがWordPress用に必要)
  8. 続けて、上部のアナリティクスのロゴの隣の▼をクリックする
  9. GA4のプロパティの下に表示されてる数字(プロパティID)をコピーしておく(これがGAS用に必要)

図:GA4移行アシスタント

図:測定IDを取得する

図:プロパティIDも必要です

WordPressに設定

WordPressに予めプラグインとして、Simple SEO Packをインストールして有効化しておきます。

  1. WordPressの左サイドバーに出てる「SEO PACK」をクリックする
  2. 一般設定を開く
  3. Googleアナリティクスを開く
  4. トラッキングコードの種類をgtag.jsに変更する
  5. 「トラッキングID」または「測定ID」に前項で取得した測定IDを入力する
  6. 設定を保存するクリック

これで、しばらく放置しておけば、WordPressへのアクセスログがGA4として計測したデータがAnalyticsに表示されるようになります。

図:測定IDを登録する

GAS側の準備

スクリプトエディタを開き、以下の作業をするだけで利用できるようになっています。

  1. 左サイドバーの「サービス」の+アイコンをクリックする
  2. Google Analytics Data APIが表示されてるのでそれをクリックする(まだ、v1betaです)
  3. 追加をクリック
  4. AnalyticsDataから始まるメソッドとして呼び出す事が可能です。

サンプルコードは、Analytics Data Serviceに掲載されています。API自体の詳細はこちらに掲載されています。

図:サービスの追加が必要です

図:最初の1回目だけ認証が必要

ソースコード

リアルタイムレポートの取得

ポイントはメトリクスとディメンションの2つ。この2つに何が指定出来るのか?はGA4 Query Explorerにて調べる事が可能です。リアルタイムレポートやクロス集計も取れるようなので、今回は通常のレポートではなくリアルタイムレポートを取得してみます。

レスポンスはJSONで返ってくるので色々加工してからスプレッドシートに書き出しが必要です。

//GA4のプロパティID 
const propertyId = "ここにプロパティIDを入れる"

//GA4からデータを取得して、スプレッドシートに書き出す
function runReport() {
  //UIを取得
  let ui = SpreadsheetApp.getUi();

  //スプレッドシートを取得
  let ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");

  //Analyticsデータを取得する
  try{
    //メトリック(指標)を指定
    const metric = AnalyticsData.newMetric();
    metric.name = 'activeUsers';  //今回はアクティブユーザを指定

    //ディメンション(取得単位)を指定
    const dimension = AnalyticsData.newDimension();
    dimension.name = 'city';  //市町村単位を指定

    //期間を指定する(今日までを指定した)
    const dateRange = AnalyticsData.newDateRange();
    dateRange.startDate = '2022-05-25';
    dateRange.endDate = '2022-05-28';

    //GA4へリクエストを構築
    const request = AnalyticsData.newRunReportRequest();
    request.dimensions = [dimension];
    request.metrics = [metric];
    //request.dateRanges = dateRange;  //リアルタイムレポートの場合は除外する

    //レポートデータをリクエスト(リアルタイム)
    //runReportで通常のレポート取得
    const report = AnalyticsData.Properties.runRealtimeReport(request,'properties/' + propertyId);
    Logger.log(report.rows)

    //データが空ならば終了する
    if (!report.rows) {
      ui.alert('取得データが空っぽ!!');
      return;
    }

    //リアルタイムレポートのヘッダを取得する
    const dimensionHeaders = report.dimensionHeaders.map(
        (dimensionHeader) => {
          return dimensionHeader.name;
        });
    const metricHeaders = report.metricHeaders.map(
        (metricHeader) => {
          return metricHeader.name;
        });
    const headers = [...dimensionHeaders, ...metricHeaders];

    //ヘッダをシートの1行目に書き込む
    ss.appendRow(headers);

    //レポートデータを取得する
    const rows = report.rows.map((row) => {
      const dimensionValues = row.dimensionValues.map(
          (dimensionValue) => {
            return dimensionValue.value;
          });
      const metricValues = row.metricValues.map(
          (metricValues) => {
            return metricValues.value;
          });
      return [...dimensionValues, ...metricValues];
    });

    //レポートデータを2行目以降に一括で書き込む
    ss.getRange(2, 1, report.rows.length, headers.length).setValues(rows);

    //終了メッセージ
     ui.alert('データの取得が完了しました。')
  }catch(e){
    //エラーが発生した場合
    console.log("エラー発生:" + e.error)
  }
}
  • propertyIdに前項で取得したプロパティIDを入れておきます。
  • 今回はリアルタイムレポートなのでリクエストはAnalyticsData.Properties.runRealtimeReportで行います
  • メトリック、ディメンションを指定する(通常レポートの場合は期間も必要)
  • 返り値のJSONデータにヘッダとデータが含まれているのでそれをスプレッドシートに書き出しています。

図:metricsの離脱率(bounceRate)を指定中

図:リアルタイムデータを取得できた

トータルページビュー数の取得

概要

GA4で指定日付から今日までの間の個別のページやカスタム投稿タイプのページ、固定ページのページビュー数のみをざっくり取りたいなと思ってコードを作成しました。

しかし、GA4にはどうでもいいページのカウント数まで含まれてしまってるため、これをGASで除外しながら同時に自身のサイトで行った重大な変更により同一ページだけれどURLが異なるケースは、ページタイトルを基準に合算する必要があります。

これらの要因を考慮して最終的にスプレッドシートに出力する処理が今回の大きな目的になります。

フィルタリングルール

苦労するのは、そのままGA4のAPIに問い合わせてしまうと不必要なページの閲覧数データまで含めて取得してきてしまいます。例えば以下のようなページはカウント数を見ても仕方ないので除外したい。

  • not setで計測されてしまってるデータは不要
  • ページが見つかりませんページのデータのカウントは不要(404のページ)
  • 誰かの検索結果のページのカウント数データは不要
  • カテゴリページ、タグページ、アーカイブページのカウント数は不要
  • ページ送りやURLにfeedが含まれているページは不要
  • 日付アーカイブページについても不要

これらのデータについては取得してきてしまうので、GAS側で不要としてフィルタリングする必要があります。

その他の考慮要因

自分のサイトではWordPressで開始してAnalyticsで計測開始を始めてから、途中でGA4移行しています。これに加えて以下のようなページビュー数計測における重大な影響をおよぼす変更を加えています。

  • ページタイトルのサイト名との間に「|」記号をあとで追加している
  • URLにおいて日付が当初入っていたが、最近のパーマリンク変更にて現在はURLに日付が入っていない。

結果、同じページタイトルだけれども別々に集計されてしまってるので、これを統合するための処理をGASのコード内で行わせて、ページビュー数の合算処理を行っています。

WordPressのパーマリンクを短くするとどうなるのか検証してみた

ソースコード

グローバル設定

グローバル変数としてGA4プロパティや、取得開始日、書き出し先シート名、WordPressをインストールしたベースURLを準備しておきます。

// GA4プロパティID(数字のみ)
const GA4_PROPERTY_ID = 'ここにプロパティIDの数字を入れる'; // 例: '123456789'

// 集計開始日 (YYYY-MM-DD形式) - サイトの計測開始日など十分古い日付
const START_DATE = '2018-01-01'; 

// 書き出すシート名
const SHEET_NAME = 'GA4_PageViews';

// ベースURL
const BASE_URL = 'WordPressを入れたベースURL';    //WPを入れたフォルダが配下に別にある場合はその部分は不要。
メインのコード

前述していた自分のサイトにカスタマイズした余計なデータの除外をするための除外ルール用の関数を用意します(categorizePageType関数)。この関数を元に実行するメインの関数として、writeFilteredPageViewsToSheet関数を用意して実行します。

カスタム投稿タイプはURLから判断できるけれども、通常のページと固定ページは区別がつかないので、2タイプで分類します。そして、自身のサイトの前述記述の考慮すべき要因を元にページタイトルで合算する処理を加えて、ページビュー数を弾き出しています。

最終的にページビュー数でソートを掛けた配列をスプレッドシートに書き出しを洗替えで行っています。

図:希望する件数に最も近い結果を得られた

//サイトのURL構造(パス)とタイトルに基づいてページタイプを分類するヘルパー関数
function categorizePageType(decodedPath, pageTitle) {
  // 除外ルール(ページタイトルより)
  if (pageTitle === '(not set)') return '除外 (not set)';
  if (pageTitle.includes('ページが見つかりません')) return '除外 (404)';
  if (pageTitle.includes('検索:') || pageTitle.includes('検索結果:')) return '除外 (検索)';
  if (pageTitle.startsWith('タグ:') || pageTitle.startsWith('カテゴリ:')) return '除外 (アーカイブ)';
  
  const dateTitleRegex = /^\d{4}年(\d{1,2}月(\d{1,2}日)?)? /;
  if (dateTitleRegex.test(pageTitle)) return '除外 (日付アーカイブ)';

  // 除外ルール2 (デコード済みパスに基づく)
  if (decodedPath.includes('/category/')) return '除外 (カテゴリ)';
  if (decodedPath.includes('/tag/')) return '除外 (タグ)';
  if (decodedPath.includes('/page/')) return '除外 (ページ送り)';
  if (decodedPath.includes('?s=')) return '除外 (検索)';
  if (decodedPath.includes('/feed/')) return '除外 (Feed)';
  
  const dateArchiveRegex = /^\/wp\/\d{4}(\/\d{2}(\/\d{2})?)?\/?$/;
  if (dateArchiveRegex.test(decodedPath)) return '除外 (日付アーカイブ)';
  
  // 必要な「分類」ルール
  if (decodedPath.startsWith('/wp/info/')) return 'カスタム投稿 (info)';
  if (decodedPath.startsWith('/wp/')) return '記事・固定ページ';
  if (decodedPath === '/') return '記事・固定ページ';

  // 上記以外
  return 'その他・除外';
}


// GA4プロパティの「全期間」ページビュー数を取得し、タイトルからサイト名サフィックスを削除してから合算する
function writeFilteredPageViewsToSheet() {
  // スプレッドシート初期化
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(SHEET_NAME);
  sheet.getSheetByName(SHEET_NAME).getRange("A2:D").clearContent();

  // GA4 API リクエスト
  const propertyName = 'properties/' + GA4_PROPERTY_ID;
  const request = {
    property: propertyName,
    dateRanges: [{ startDate: START_DATE, endDate: 'today' }],
    dimensions: [{ name: 'pageTitle' }, { name: 'pagePath' }],
    metrics: [{ name: 'screenPageViews' }],
    limit: 10000, 
    orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }],
  };

  try {
    // GA4 Data API を実行
    const report = AnalyticsData.Properties.runReport(request, propertyName);

    if (!report.rows || report.rows.length === 0) {
      sheet.getRange(2, 1).setValue('データが見つかりませんでした。');
      return;
    }

    // 「全件合算」し、フィルタリング
    const aggregatedData = {}; // 合算用のオブジェクト
    const dateInPathRegex = /^(\/wp\/|\/wp\/info\/)\d{4}\/\d{2}\/\d{2}\//;

    // すべての行をループして「合算」
    report.rows.forEach((row) => {
      const pageTitle = row.dimensionValues[0].value;
      const pagePath = row.dimensionValues[1].value;
      const pageViews = Number(row.metricValues[0].value); 

      //サイト名をタイトルから削除する
      // | 🌴 officeの杜 🥥を削除 (長い方を先に)
      let cleanedTitle = pageTitle.replace(' | 🌴 officeの杜 🥥', '');
      // 🌴 officeの杜 🥥" を削除
      cleanedTitle = cleanedTitle.replace(' 🌴 officeの杜 🥥', '');
      // 前後の空白を削除
      cleanedTitle = cleanedTitle.trim();

      // URLのデコードを行う
      let decodedPath;
      try {
        decodedPath = decodeURIComponent(pagePath);
      } catch (e) {
        decodedPath = pagePath;
      }
      
      // 日付パス削除
      let normalizedPath = decodedPath.replace(dateInPathRegex, '$1');

      // 末尾スラッシュ削除
      if (normalizedPath !== '/' && normalizedPath.endsWith('/')) {
        normalizedPath = normalizedPath.slice(0, -1);
      }
      
      // データを合算
      if (aggregatedData[normalizedPath]) {
        // 既にキーが存在する場合
        const existingItem = aggregatedData[normalizedPath];
        existingItem.views += pageViews;
        
        // タイトルが(not set)で、クリーンなタイトルが(not set)でない場合に更新
        if (existingItem.title === '(not set)' && cleanedTitle !== '(not set)') {
          existingItem.title = cleanedTitle;
        }
        
      } else {
        // 新しいキーの場合
        aggregatedData[normalizedPath] = {
          title: cleanedTitle,
          views: pageViews,       // 合計PV
          path: normalizedPath
        };
      }
    });

    // 合算後のデータに対して「フィルタリング」を実行
    const desiredTypes = ['記事・固定ページ', 'カスタム投稿 (info)'];
    const filteredList = [];

    Object.keys(aggregatedData).forEach(pathKey => {
      const item = aggregatedData[pathKey];
      // categorizePageType には「クリーンなタイトル」が渡される
      const pageType = categorizePageType(item.path, item.title);
      
      if (desiredTypes.includes(pageType)) {
        item.type = pageType;
        filteredList.push(item);
      }
    });

    // スプレッドシート書き込み用の最終配列を作成
    const cleanedBaseUrl = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
    
    const dataForSheet = filteredList.map(item => {
      let finalPath = item.path;
      if (finalPath !== '/') {
        finalPath += '/'; // トップページ以外は / を付ける
      }
      const fullUrl = cleanedBaseUrl + finalPath;
      return [item.title, fullUrl, item.views, item.type];
    });
    
    // PV数で最終ソート
    dataForSheet.sort((a, b) => b[2] - a[2]);

    // スプレッドシートに一括書き込み
    if (dataForSheet.length > 0) {
      sheet.getRange(2, 1, dataForSheet.length, dataForSheet[0].length)
           .setValues(dataForSheet);
      console.log(`処理完了: GA4から ${report.rows.length} 件取得し、合算・フィルタリング後の ${dataForSheet.length} 件を書き出しました。`);
    } else {
      sheet.getRange(2, 1).setValue('フィルタリングの結果、対象となるデータがありませんでした。');
      console.log('フィルタリングの結果、対象となるデータがありませんでした。「categorizePageType」関数のルールを確認してください。');
    }

  } catch (e) {
    console.log('エラーが発生しました: ' + e.message);
    sheet.getRange(2, 1).setValue('エラー: ' + e.message);
  }
}

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)