4種のGoogle系のAPIをNode.jsから実行する (GA, SpreadSheet, Big Query, GAM)

# はじめに

データの可視化を行う当たってGoogleの各種APIを触ってみましたが、最初は認証の仕方とか基本的な書き方とかでつまづく所もあるので、ハンズオンとして触り方をなるべくわかりやすく紹介したいと思います。
今回紹介するのは

- Google Analyticsのレポート
- Google SpreadSheet
- Big Query
- Google Ad Managerのレポート

のAPIになります。
サンプルコードは[こちら](https://github.com/hedrall/google-data-api-samples)に上げています。

# Google Analytics の レポートAPI

## 概要

- GAのAPIドキュメントを検索すると、Reporting API v4 と Core Reporting API が出てきますが、最新は Reporting API v4 です。
  - https://developers.google.com/analytics/devguides/reporting/core/v4?hl=ja
- ガイドをみると、Node.js用のクライアントは無い様なので、HTTP POSTリクエストを自分で生成する必要があります。
- 認証に関してはサービスアカウントを作る方法が固いと思います。[こちら](google-auth-library)からサービスアカウントを新規作成し、登録完了ページからクレデンシャルjsonをダウンロードしておいてください。
  - サンプルコードでは `credentials/ga.json` という名前で保存する想定です。
  - 参考: [サービス アカウント キーの作成](https://cloud.google.com/iam/docs/creating-managing-service-account-keys?hl=ja)
- サービスアカウントの権限でクエリーを実行するので、作成したサービスアカウントをGAにユーザとして追加し、レポートを実行するための権限を与えておいてください。
- 実際にAPIリクエストを行う際の認証はOAuthトークンを使用します。なにやら難しそうな感じですが、`google-auth-library`を使用すると簡単に取得ができる様になっています。
- クエリの書き方はこちらが参考になります。
  - https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet?hl=ja

## 実装

パッケージのインストール

```shell
npm i --save google-auth-library axios
```

コード

```js
// GAのAPIの利用例です

// トークンの取得のため
const { auth } = require( 'google-auth-library' );
// HTTPリクエストにはaxiosを使用します。
const axios  = require( 'axios' );
const path = require( 'path' );

( async () => {
  // --- start OAuthアクセストークンを手に入れる ---
  // credentialファイルを読み込む
  const credentialJson = require( path.resolve( `${__dirname}/../credentials/ga.json` ) );
  
  const client = auth.fromJSON( credentialJson );
  // 許可するスコープを定義
  client.scopes = ['https://www.googleapis.com/auth/analytics.readonly'];
  await client.authorize();
  const token =  client.credentials.access_token;
  // --- end OAuthアクセストークンを手に入れる ---
  
  // --- start Queryを作成する ---
  // ブラウザに対するページビューを取得する
  const query = {
    reportRequests: [
      {
        viewId: '{{ GAのビューIDを入力 }}', // todo
        // レポートの期間
        dateRanges: [ {
          startDate: '2020-04-01',
          endDate: '2020-04-02',
        } ],
        // ディメンジョン
        dimensions: [ { name: 'ga:browser' } ],
        // 指標
        metrics: [ { expression: 'ga:pageviews' }, ],
        // 指標のフィルターをかけたい場合は以下の様に記述する
        // dimensionFilterClauses: [
        //   {
        //     operator: 'OR',
        //     filters: [
        //       {
        //         dimensionName: 'ga:pagePath',
        //         operator: 'REGEXP', // or EXACT
        //         expressions: ['hoge']
        //       }
        //     ]
        //   }
        // ],
        
        // GAのレポートはコンソールからの操作と同様にサンプリングされる可能性があることに注意する
        samplingLevel: 'DEFAULT' 
      }
    ]
  };
  // --- end Queryを作成する ---

  // --- start リクエストを実行する ---
  const response = await axios.post(
    'https://analyticsreporting.googleapis.com/v4/reports:batchGet',
    query,
    {
      headers: {
        // アクセストークンを設定
        Authorization: `Bearer ${token}`
      }
    }
  );
  // --- start リクエストを実行する ---
  
  // 出力
  console.table( response.data.reports[0].data.rows.map( row => {
    return [ ...row.dimensions, ...row.metrics[0].values ];
  }) );
  
} )().catch( e => console.log( e ) );
```

## 補足

- クエリーでどんなディメンジョンや指標が使えるかは[Dimensions & Metrics Explorer](https://ga-dev-tools.appspot.com/dimensions-metrics-explorer/)を参考
- GAのレポートはコンソールからの操作と同様に結果がサンプリングされる可能性があることに注意してください。
    - https://support.google.com/analytics/answer/2637192?hl=ja

# BigQueryのクエリをAPIで実行

## 概要

- BigQueryのAPIにはNode.jsのクライアントライブラリがありますのでこちらを利用します。
    - https://googleapis.dev/nodejs/bigquery/latest/
- 自身のGCPアカウントのプロジェクトでAPIを有効化します。
    - こちらから有効化できます。https://console.cloud.google.com/apis/library/bigquery.googleapis.com?q=bigquery
    - 有効化したアカウントのアカウント名は下記コードに記載します。
- 認証はGAと同じくサービスアカウントで行うのがいいでしょう。サービスアカウントに`BigQuery ジョブユーザー`などのAPIが実行可能な権限をつけてください。
  - サンプルコードでは `credentials/bq.json` という名前で保存する想定です。
- [こちら](https://github.com/googleapis/nodejs-bigquery/blob/master/samples/query.js)のページにサンプルがあるので簡単に記述ができますが、サービスアカウントで認証を行う部分だけ以下のコードに追記しておきます。

## 実装

パッケージのインストール

```shell
npm i --save @google-cloud/bigquery
```

コード

```js
const path = require( 'path' );

( async () => {
  // クライアントを初期化
  const { BigQuery } = require( '@google-cloud/bigquery' );
  const bigquery = new BigQuery( {
    // 認証はクレデンシャルファイルのパスを渡すだけでいい
    keyFilename: path.resolve( `${__dirname}/../credentials/bq.json` ),
    projectId: '{{ プロジェクト名を入力 }}'
  } );

  // クエリを作成
  const query = `SELECT name
      FROM \`bigquery-public-data.usa_names.usa_1910_2013\`
      WHERE state = 'TX'
      LIMIT 100`;

  // 全てのオプションは -> https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query
  const options = {
    query: query,
    location: 'US',
  };

  // ジョブを実行
  const [job] = await bigquery.createQueryJob( options );

  // ジョブの完了を待つ
  const [rows] = await job.getQueryResults();

  // 結果を表示
  console.log( 'Rows:' );
  console.table( rows );
} )();
```

# Google Spread Sheet の API

## はじめに

- 今回はシートにデータを吐き出してみます。
- Google Sheets APIを使用します。
  - [Node.js Quickstart  \|  Sheets API  |  Google Developers](https://developers.google.com/sheets/api/quickstart/nodejs)
- Node.jsのクライアントがあるので、使用します。
- こちらも上記サンプルページの通りなのですが、認証の通し方を少し変えます。
- 自身のGCPのプロジェクトで Google Sheets APIを有効にします。
  - https://console.cloud.google.com/apis/library/sheets.googleapis.com?q=sheet
- マイドライブから操作したいSpreadSheetを任意のサービスアカウントに対して編集権限で共有します。
  - サンプルコードでは `credentials/sheets.json` という名前でサービスアカウントのクレデンシャルjsonを保存する想定です。

## 実装

## 実装

パッケージのインストール

```shell
npm i --save googleapis
```

コード


```js
const path = require( 'path' );
const { google } = require( 'googleapis' );

( async () => {
  // 認証する
  const authClient = await google.auth.getClient( {
    keyFile: path.resolve( `${__dirname}/../credentials/sheets.json` ),
    scopes: ['https://www.googleapis.com/auth/spreadsheets']
  } );
  const sheets = google.sheets( { version: 'v4', auth: authClient } );

  // 保存するデータ
  const cells = [
    [ 1, 2, 3 ],
    [ 4, 5, 6 ]
  ];

  // 書き込み
  const req = {
    // スプレッドシートのID, URLの一部
    // https://docs.google.com/spreadsheets/d/{{ここの部分}}/edit#gid=0
    spreadsheetId: '{{SPREAD_SHEET_ID}}', 
    range: `シート1!A:AZ`,
    valueInputOption: 'USER_ENTERED',
    resource: {
      values: cells
    }
  };
  await sheets.spreadsheets.values.append( req );
} )();
```

## 実装

# Google Ad Manager のレポートAPI

## はじめに

- 一番の曲者でした。
- Node.jsのクライアントライブラリは無いので、HTTPリクエストを自分で作る必要があります。
- クエリの方法ですが、SOAPという古いAPIの仕様が採用されているので、XMLでクエリを記述してPOSTのBODYに埋めこみます。
    - https://developers.google.com/ad-manager/api/soap_xml
- JSでXMLを扱うために `xml-js` というライブラリを使用します。クエリはXMLに変換するので、`xml-js`に合わせた形式で記述しています。
- 執筆時点で最新バージョンはv202002
- 認証は同様にサービスアカウントで行う。GAMの管理画面からGCPコンソールで作成したサービスアカウントを追加し、レポート用の権限をつけてください。
- サンプルコードでは `credentials/gam.json` にサービスアカウントのクレデンシャルが保存されている想定です。
- GAMはレポートの条件によって、APIからは実行できないレポート条件が存在します。API経由で保存済みのレポートのクエリを取得する機能がありますので、GAMの管理画面からサービスアカウントのユーザに「成り代わる」をして、レポートを保存しておくとAPI経由でクエリの内容とAPIに対応しているかを取得できます。（こちらはサンプルコードに書いていません。）

## レポート取得の流れ

こちらの通り
https://developers.google.com/ad-manager/api/reference/v202002/ReportService

1. `ReportService.runReportJob` でジョブを作成
2. `ReportService.getReportJob` でジョブの状態を取得し、完了するまでポーリングする
3. 完了後、`ReportService.getReportDownloadURL`を使用して、レポートのダウンロード用URLを取得する
4. ダウンロードする

## 実装

パッケージのインストール

```shell
npm i --save google-auth-library request axios xml-js
```

コード

```js
const path = require( 'path' );
const { auth } = require( 'google-auth-library' );
const axios = require( 'axios' );
const { js2xml, xml2js } = require( 'xml-js' );

// SOAPのベース
// soapenv:Body部分を色々変える
const soapJsonBase = {
  _declaration: {
    _attributes: {
      version: '1.0',
      encoding: 'UTF-8'
    }
  },
  'soapenv:Envelope': {
    _attributes: {
      'xmlns:soapenv': 'http://schemas.xmlsoap.org/soap/envelope/',
      'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
      'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance'
    },
    'soapenv:Header': {
      'ns1:RequestHeader': {
        _attributes: {
          'soapenv:actor': 'http://schemas.xmlsoap.org/soap/actor/next',
          'soapenv:mustUnderstand': '0',
          // APIのバージョンはここで指定
          'xmlns:ns1': `https://www.google.com/apis/ads/publisher/v202002`
        },
        'ns1:networkCode': {
          _text: '{{ GAMのネットワークIDを指定 }}'
        },
        'ns1:applicationName': {
          _text: '{{ GCPのプロジェクト名を指定 }}'
        }
      }
    },
    'soapenv:Body': undefined
  }
};
( async () => {
  // --- クエリの作成 ---
  // 日時と広告ユニットごとの合計のインプレッションを取得
  const query = {
    // ディメンジョン
    dimensions: [
      { _text: 'DATE' },
      { _text: 'AD_UNIT_NAME' }, // 広告ユニット
    ],
    adUnitView: { _text: 'FLAT' },
    // 指標
    columns: [
      // 全体系
      { _text: 'TOTAL_LINE_ITEM_LEVEL_IMPRESSIONS' }, // 合計のインプレッション
    ],
    // レポートの期間
    startDate: {
      year: { _text: '2020' },
      month: { _text: '4' },
      day: { _text: '1' }
    },
    endDate: {
      year: { _text: '2020' },
      month: { _text: '4' },
      day: { _text: '2' }
    },
    // 日時範囲の期間は「固定」
    dateRangeType: {
      _text: 'CUSTOM_DATE'
    },
    // フィルターをかける場合は以下の様に記述
    //statement: {
    //  query: {
    //    _text: ` where PARENT_AD_UNIT_ID = {{広告ユニットID}}`
    //  }
    //},
    // 管理画面とタイムゾーンを合わせる
    timeZoneType: {
      _text: 'PUBLISHER'
    }
  };
  // --- クエリの作成 ---

  // --- アクセストークンを取得 ---
  const keys = require( path.resolve( `${__dirname}/../credentials/gam.json` ) );
  const client = auth.fromJSON( keys );
  client.scopes = ['https://www.googleapis.com/auth/dfp', 'https://www.googleapis.com/auth/analytics.readonly'];
  await client.authorize();
  const token = client.credentials.access_token;
  // --- クエリの作成 ---
  
  // SOAPヘッダーに変換するオブジェクトを作成
  // xml-jsでXMLに変換する
  // 変換後↓
  // <runReportJob xmlns="https://www.google.com/apis/ads/publisher/v202002">
  //   <reportJob>
  //     <reportQuery> クエリー </reportQuery>
  //   </reportJob>
  // <runReportJob>
  let soapJson = { ...soapJsonBase };
  let body = {
    // ここで実行するコマンドを指定
    runReportJob: {
      // 中身はコマンドの引数
      _attributes: {
        xmlns: `https://www.google.com/apis/ads/publisher/v202002`
      },
      reportJob: {
        reportQuery: query
      }
    }
  };
  soapJson['soapenv:Envelope']['soapenv:Body'] = body;
  
  // APIリクエストを実行
  let soapXML = js2xml( soapJson, { compact: true } );
  let response = await axios.post(
      `https://ads.google.com/apis/ads/publisher/v202002/ReportService`,
      soapXML,
      { headers: { Authorization: `Bearer ${token}` } }
    );

  // レスポンスもXMLなのでjsonに変換する
  let data = xml2js( response.data, { compact: true } );
  const jobId = data['soap:Envelope']['soap:Body']['runReportJobResponse']['rval']['id']['_text'];
  console.log( 'ジョブID', jobId );
  
  // ジョブの完了をポーリングする
  const sleep = () => new Promise( resolve => setTimeout( () => resolve(), 1000 ) ); 
  while ( true ) {
    console.log( 'ポーリング' );
    soapJson = { ...soapJsonBase };
    body = {
      getReportJobStatus: {
        _attributes: {
          xmlns: `https://www.google.com/apis/ads/publisher/v202002`
        },
        reportJobId: { _text: jobId }
      }
    };
    soapJson['soapenv:Envelope']['soapenv:Body'] = body;
    // APIリクエスト実行
    soapXML = js2xml( soapJson, { compact: true } );
    response = await axios.post(
      `https://ads.google.com/apis/ads/publisher/v202002/ReportService`,
      soapXML,
      { headers: { Authorization: `Bearer ${token}` } }
    );
    data = xml2js( response.data, { compact: true } );
    // ステータスを取得
    const status =  data['soap:Envelope']['soap:Body']['getReportJobStatusResponse']['rval']['_text'];
    console.log( 'ステータス', status );
    if ( [ 'COMPLETED', 'FAILED' ].includes( status ) ) {
      console.log( '完了' );
      break;
    }
    await sleep();
  }
  
  // レポートのダウンロード用URLを取得する
  console.log( 'URLを取得' );
  soapJson = { ...soapJsonBase };
  body = {
    getReportDownloadURL: {
      _attributes: {
        xmlns: `https://www.google.com/apis/ads/publisher/v202002`
      },
      reportJobId: { _text: jobId },
      exportFormat: { _text: 'CSV_DUMP' },
    }
  };
  soapJson['soapenv:Envelope']['soapenv:Body'] = body;
  // APIリクエスト実行
  soapXML = js2xml( soapJson, { compact: true } );
  response = await axios.post(
    `https://ads.google.com/apis/ads/publisher/v202002/ReportService`,
    soapXML,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  data = xml2js( response.data, { compact: true } );
  const downloadURL = data['soap:Envelope']['soap:Body']['getReportDownloadURLResponse']['rval']['_text'];
  console.log( downloadURL );
  
  // ダウンロード
  const fs = require( 'fs' );
  const request = require( 'request' );
  await new Promise( ( resolve ) => {
    request( downloadURL )
      .pipe( fs.createWriteStream( 'report.csv.gz' ) )
      .on( 'close', async () => {
        resolve();
      } );
  } );
  
  // gzipなので、解答する
  const child_process = require( 'child_process' );
  await child_process.execSync( `gunzip -f ./report.csv.gz` );
  
} )().catch( e => console.log( e ) );
```

## 補足

試して見ると結構エラーする条件があると思います。特に多いのはフィルター条件です。例えば定義済みのkey-valueでレポートを作成したい場合があるかと思いますが、GAMの管理画面のレポートと違い文字列でkay-valueを指定することはできません。この場合CUSTOM_CRITERIAと言うディメンジョンをしてすると、CUSTOM_TARGETING_VALUE_IDと言うカラムが追加されるので、これでフィルターをかけます。何でフィルターできて、できないのかはドキュメントにほとんどのディメンジョンごとに記載されています。
使用可能なディメンジョン、指標、フィルター条件などはこちらを参考ください。
https://developers.google.com/ad-manager/api/reference/v202002/ReportService.ReportQuery

# 以上

参考になれば幸いです。