GASを利用してGA4とUAのイベント記録をスプレッドシートにしてgoogle Driveに保管した備忘録

小規模サービスのUA→GA4移行時に検証用として作ったもの。
もともと移行前に数値の変化を観測するためにGTM内で双方のアクセスを取っていた。
それを使って重複期間内の比較を行う。

本来は日々のアクセス記録をドライブに保存するために作っていたものの転用。
だいたいは既存の技術の組み合わせだけど備忘録として残しておく。

#### 注意点

件数が多くなるとエラーを吐くはず。
予想されるもので、アナリティクスの取得制限超過か、取得できているならスプレッドシートの書き込み制限超過、GASの実行時間超過か。

その場合は作業をより細かく分割すると良い。
アナリティクスからの取得で開始ページ数を指定してループで取得したり、スプレッドシートへの書き込みを分割して行ったり、作業時間計測を入れて中断・再開できるよう調整したり……。
そこまで来ると別の方法を試した方が良いかもしれないが。

- - -

### 前準備

* google ドライブに「保存用フォルダ」と実行用のgoogle apps scriptのファイルを作成する
* 保存用フォルダのIDを控えておく（フォルダに移動した際のurlで「folders/」以降の文字列）
* ga4、uaの各アカウントidを控えておく

### 手順（GAS上の作業）

#### 1. 左側のメニュー「ファイル」からファイルを追加して以下の内容を入れる

```ConstLabels.js
// 保存用フォルダのID
const SAVE_FOLDER_ID = 'xxxxxxxxxxxxxxxxxxxxxxxx';

// ga4のアカウントID
const GA4_ACCOUNT_VIEW_ID = 'xxxxxxxxxxx';

// UAのアカウントID
const UA_ACCOUNT_VIEW_ID = 'xxxxxxxxxxx';

// いつから集計を開始するか？ ※重複期間内であること(例：2023年4月1日)
const BASE_START_DATE = '2023-04-01';

// 何日単位で集計するか？(例：7日 = 一週間)
const INTERVAL_DAYS_COUNT = 14;
```

#### 2. 左側のメニュー「サービス」から「GoogleAnalyticsDataApi」「GoogleAnalyticsApi」を選択し追加する

#### 3. 左側のメニュー「ファイル」からファイルを追加して、GA4用の取得コードを書く。また、目的に合わせてフィルター設定もする

※内容的には公式のサンプルのが分かりやすいかも
　個人的な利便性と用途を理由に色々追加

今回はイベント「電話発信」を対象にフィルター設定（telEventReport()の関数）。

```Ga4AnalyticsClass.js
class Ga4AnalyticsClass {
  constructor() {
    this.start_date = null;
    this.finish_date = null;
  }

  set startDate(str) {
    this.start_date = str;
  }

  set finishDate(str) {
    this.finish_date = str;
  }

  // 電話発信
  telEventReport() {
    const dimensions = [
      'pagePath',
      'pageTitle'
    ];

    const metrics = [
      'eventCount'
    ];

    // GA4からデータを取得する際のフィルタ条件を設定する
    // https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FilterExpression?hl=ja
    const filters = [
      {
        field_name : 'eventName',
        match_type : 'EXACT',
        value : '電話発信'
      }
    ];

    const report = this.runReport(dimensions, metrics, filters);

    return {
      headers: this.getHeaderRow(report),
      body: this.getBodyRows(report)
    }
  }

  getDimension(param) {
    const dimension = AnalyticsData.newDimension();
    dimension.name = param;
    return dimension;
  }

  getMetric(param) {
    const metric = AnalyticsData.newMetric();
    metric.name = param;
    return metric;
  }

  getFilterExpression(field_name, match_type, value) {
    const filter_expression = AnalyticsData.newFilterExpression();

    filter_expression.filter = AnalyticsData.newFilter();
    filter_expression.filter.fieldName = field_name;
    filter_expression.filter.stringFilter = AnalyticsData.newStringFilter();
    filter_expression.filter.stringFilter.value = value;
    filter_expression.filter.stringFilter.matchType = match_type;
    return filter_expression
  }

  runReport(dimensions, metrics, filters) {
    const date_range = AnalyticsData.newDateRange();
    date_range.startDate = this.start_date;
    date_range.endDate = this.finish_date;

    const request = AnalyticsData.newRunReportRequest();
    request.dimensions = dimensions.map( param => this.getDimension(param) );
    request.metrics = metrics.map( param => this.getMetric(param) );
    request.dateRanges = date_range;

    if(filters && filters.length > 0) {
      const dimension_filter = AnalyticsData.newFilterExpression();
      dimension_filter.andGroup =  AnalyticsData.newFilterExpressionList();
      dimension_filter.andGroup.expressions = filters.map( item => {
        return this.getFilterExpression(item.field_name, item.match_type, item.value);
      });
      request.dimensionFilter = dimension_filter;
    }

    return AnalyticsData.Properties.runReport(request, 'properties/' + GA4_ACCOUNT_VIEW_ID);
  }

  getHeaderRow(report) {
    const dimension_headers = report.dimensionHeaders.map(
      (dimension_header) => {
        return dimension_header.name;
      });
    const metric_headers = report.metricHeaders.map(
      (metric_header) => {
        return metric_header.name;
      });
    return [
      ...dimension_headers, 
      ...metric_headers
    ];
  }

  getBodyRows(report) {
    return report.rows.map((row) => {
      const dimension_values = row.dimensionValues.map(
        (dimension_value) => {
          return dimension_value.value;
        });
      const metric_values = row.metricValues.map(
        (metric_values) => {
          return metric_values.value;
        });
      return [
        ...dimension_values, 
        ...metric_values
      ];
    });
  }
}

```

#### 4. 左側のメニュー「ファイル」からファイルを追加して、Universal Analytics用の取得コードを書く。また、目的に合わせてフィルター設定もする

```UniversalAnalyticsClass.js
class UniversalAnalyticsClass {
  constructor() {
    this.start_date = null;
    this.finish_date = null;
  }

  set startDate(str) {
    this.start_date = str;
  }

  set finishDate(str) {
    this.finish_date = str;
  }

  telEventReport() {
    return Analytics.Data.Ga.get(
      'ga:' + UA_ACCOUNT_VIEW_ID, // ビューID
      this.start_date , // 開始日
      this.finish_date , // 終了日日
      'ga:totalEvents' ,
      {
        'dimensions': 'ga:pagePath, ga:pageTitle' ,
        'filters'   : 'ga:eventCategory=~(電話発信)' ,
        'sort': '-ga:totalEvents',
        'max-results': '200000'
      }
    );
  }
}

```

#### 5. 最後に実行用ファイルを追加する

```run.js
function run() {
  // スプレッドシート保存先
  const save_folder = DriveApp.getFolderById(SAVE_FOLDER_ID);

  // 日付指定（集計開始日と終了日）
  const start_date = new Date(BASE_START_DATE);
  start_date.setHours(0);
  start_date.setMinutes(0);
  start_date.setSeconds(0);
  const end_date = new Date();
  end_date.setHours(0);
  end_date.setMinutes(0);
  end_date.setSeconds(0);

  // 何日単位で集計するか？
  const interval_time = INTERVAL_DAYS_COUNT * 24 * 60 * 60 * 1000;
 
  // 期間の配列を作る
  const ranges_count = Math.round((( end_date.getTime() - start_date.getTime() ) / interval_time) - 0.5);
  const ranges = [...Array(ranges_count).keys()].map(num => {
    return {
      start : new Date(
        start_date.getFullYear(), 
        start_date.getMonth(), 
        start_date.getDate() + (num * INTERVAL_DAYS_COUNT), 
        0, 0, 0
      ),
      end : new Date(
        start_date.getFullYear(), 
        start_date.getMonth(), 
        start_date.getDate() + ((num + 1) * INTERVAL_DAYS_COUNT), 
        0, 0, -1
      ),
    }
  });

  // 期間で処理をループ
  ranges.forEach( dates => {
    const temp_start_date = Utilities.formatDate(dates.start, 'JST', 'yyyy-MM-dd');
    const temp_finish_date = Utilities.formatDate(dates.end, 'JST', 'yyyy-MM-dd');

    const temp_file_name = 'イベント比較__' + temp_start_date + '_' + temp_finish_date;

    const temp_file_exists = save_folder.getFilesByName(temp_file_name).hasNext();
    if (!temp_file_exists) {
      (() => {
        const file = SpreadsheetApp.create(temp_file_name);
        DriveApp.getFileById(file.getId()).moveTo(save_folder);
      })();
    }
    const temp_file = save_folder.getFilesByName(temp_file_name).next();

    // スプレッドシートのid取得
    const ss_file_id = temp_file.getId();
    const ss_file = SpreadsheetApp.openById(ss_file_id);

    // 記録用シートの取得・あるいは作成
    const current_tel_sheet = (() => {
      const sheet_name = '電話発信';
      const temp_sheet = ss_file.getSheetByName(sheet_name);
      const result_sheet = temp_sheet ?? ss_file.insertSheet(sheet_name);
      if(!temp_sheet) {
        ss_file.moveActiveSheet(1);
      }
      return result_sheet;
    })();

    try {
      // 最初にそれぞれに日付をセットする
      const ga4_analytics = new Ga4AnalyticsClass;
      ga4_analytics.startDate = temp_start_date;
      ga4_analytics.finishDate = temp_finish_date;

      const ua_analytics = new UniversalAnalyticsClass;
      ua_analytics.startDate = temp_start_date;
      ua_analytics.finishDate = temp_finish_date;

      // レポートの取得と挿入形式の作成
      const ga4_tel_report = ga4_analytics.telEventReport();
      const ga4_tel_report__result = [
        // header
        ga4_tel_report.headers,
        // body
        ...ga4_tel_report.body, 
        // 集計行
        [
          '集計', 
          ...getCalcRow(ga4_tel_report.body).splice(1)
        ]
      ];
      
      const ua_tel_report = ua_analytics.telEventReport();
      const ua_tel_report__result = [
        // header
        [
          ...ua_tel_report.query.dimensions.split(','),
          ...ua_tel_report.query.metrics
        ],
        // body
        ...ua_tel_report.rows,
        // 集計行
        [
          '集計', 
          ...getCalcRow(ua_tel_report.rows).splice(1)
        ]
      ]

      // 一旦シートをキレイにした後にデータ挿入
      current_tel_sheet.clear();
      current_tel_sheet.getRange(1, 1).setValue('Google Analytics 4');
      current_tel_sheet.getRange(2, 1, ga4_tel_report__result.length, ga4_tel_report__result[0].length).setValues(ga4_tel_report__result);
      current_tel_sheet.getRange(1, ga4_tel_report__result[0].length + 2).setValue('Universal Analytics 4');
      current_tel_sheet.getRange(2, ga4_tel_report__result[0].length + 2, ua_tel_report__result.length, ua_tel_report__result[0].length).setValues(ua_tel_report__result);
    } catch (e) {
      console.log('Failed with error: %s', e.error);
    }
  });
}

// helper. 集計行の作成
function getCalcRow(rows) {
  const base_arr = Array.from(new Array(rows[0].length), x => 0)

  return rows.reduce((result, row) => {
    row.forEach((column, index) => {
      const temp_value = Number(column);
      if(!isNumber(temp_value)){
        return;
      }
      result[index] += temp_value;
    })
    return result;
  }, base_arr)
}

// helper. 数値型のチェック
function isNumber(value) {
  return typeof value === 'number' && isFinite(value);
};
```

あとはGAS上で関数runを実行すれば、googleドライブ上の保存フォルダにスプレッドシートが追加される。
各classの「telEventReport」内にある「dimensions」「metrics」を増やせば情報を増やすこともできる。