Zenn
🚀

GASとGemini APIでGA4レポートとAI考察をChatworkに自動投稿する全手順

2025/04/01に公開

1. はじめに

「毎月のGA4レポート作成、正直めんどう...」
「数値を集計するだけじゃなく、そこから何が言えるのか考察もほしいけど、時間がない...」
「欲を言えば、まとめたレポートと考察をChatworkに自動で投稿してくれたら最高なのに!」

Webマーケターやサイト運営者なら、一度はこんな風に思ったことがあるのではないでしょうか? 日々の業務に追われる中で、定型的なレポート作成や分析に時間を取られるのは避けたいですよね。

この記事では、そんな悩みを解決するために、Google Apps Script (GAS) を使ってGoogle Analytics 4 (GA4) のデータを自動で集計し、さらにGoogleのAIであるGemini API(今回は gemini-1.5-flash を使用)にデータを分析させて考察を生成、最終的に整形されたレポートと考察をChatworkに自動投稿するシステムを構築した際の全手順と、その過程で遭遇したエラーや学びを共有します。

今回対象としたのは特定のWebサイト(例: 自社メディアサイト)のデータですが、設定を変更すれば他のサイトにも応用可能です。

私自身、GASや各種API連携は手探りで進めることも多く、「なんとなく動いてるな〜」という試行錯誤の連続でした。この記事では、同様にAPI連携や自動化に興味があるWeb担当者や、GASで業務効率化を図りたいと考えている方を主な対象に、具体的なコードと共に解説を進めます。前提として、多少のプログラミング(特にJavaScript)の知識があると理解がスムーズですが、コードはコピー&ペーストでもある程度動作するように記述していますので、ぜひ挑戦してみてください。

2. 対象読者

  • 毎月のGA4レポート作成・報告に時間がかかっているWeb担当者、マーケターの方
  • GA4のデータをAPI経由で取得・活用したい方
  • Gemini APIなどのLLM(大規模言語モデル)を具体的な業務に応用してみたい方
  • Chatwork APIを使った通知の自動化に興味がある方
  • Google Apps Script (GAS) を使った業務効率化に関心のある方
  • API連携やプログラミングで試行錯誤するのが好きな方

3. この記事を読むメリット

  • GASを使ってGA4 Data APIからデータを取得する方法がわかる。
  • GASからGemini APIを呼び出し、データに基づいたテキスト(考察・インサイト)を生成させる方法がわかる。
  • GASを使ってChatwork APIを呼び出し、整形されたメッセージを投稿する方法がわかる。
  • GA4レポート作成・分析・報告の一連の流れを自動化する具体的なスクリプトの全体像とコードが手に入る。
  • API連携時に遭遇しやすいエラー(認証、パラメータ指定など)とその解決プロセスを学べる。

4. 結論

この記事で紹介する方法を使えば、GASを中核としてGA4 Data API、Gemini API、Chatwork APIを連携させることで、指定したWebサイトのホスト名に基づいて前月のGA4データ(セッション、PV、前年同月比)と、そのデータに基づいたGeminiによる考察コメントを、整形されたメッセージとして自動でChatworkに投稿する仕組みを構築できます。

これにより、これまで手作業で行っていたデータ集計、レポート作成、簡単な分析、そして報告作業の大部分を自動化でき、大幅な時間短縮と、AIによる客観的な視点を取り入れたインサイトを手軽に入手することが可能になります。

5. 本文

それでは、具体的な構築手順を見ていきましょう。

a. システム概要

今回構築するシステムの処理フローは以下の通りです。

  1. GASのトリガー実行: 設定したスケジュール(例: 毎月1日)になると、GASスクリプトが自動的に起動します。
  2. 日付計算: スクリプト内で、レポート対象期間(前月1日〜末日)と、比較対象期間(前年同月1日〜末日)を計算します。
  3. GA4データ取得: GASからGoogle Analytics Data API (runReportエンドポイント) を呼び出し、指定したGA4プロパティID、期間、指標(セッション、PV)、フィルタ(特定のホスト名)に基づいてデータを取得します。ここではGASのUrlFetchAppサービスとOAuth認証を使用します。
  4. データ抽出・整形: GA4 APIからの応答(JSON)を解析し、必要な数値(現在のセッション/PV、前年のセッション/PV)を抽出、前年同月比を計算します。
  5. Geminiによる考察生成: 抽出・整形した数値データを基に、分析依頼のプロンプト(指示文)を作成し、GASからGemini API (generateContentエンドポイント) を呼び出します。APIキー認証を使用します。
  6. 考察テキスト整形: Gemini APIからの応答(JSON)を解析し、生成された考察テキストを取得します。この際、Chatworkで絵文字化してしまう半角コロンなどを全角に置換します。
  7. Chatworkメッセージ作成: 取得したGA4データと整形済みの考察テキストを組み合わせ、Chatworkのメッセージ記法([info], [title]など)を使って整形し、指定のメンションを先頭に追加します。
  8. Chatwork投稿: GASからChatwork API (/rooms/{room_id}/messagesエンドポイント) を呼び出し、作成したメッセージを投稿します。APIトークン認証を使用します。
  9. エラーハンドリング: 各ステップでエラーが発生した場合、エラー内容をログに記録し、可能であればエラー通知をChatworkに投稿します。

システム概要図(イメージ)
(図はイメージです)

b. 事前準備

スクリプトを作成する前に、以下の準備が必要です。

  1. Google Cloud Platform (GCP) プロジェクト:
    • GASスクリプトに紐づくGCPプロジェクトを用意し、Google Analytics Data API を有効化します。
  2. Gemini API:
    • Google AI Studio や GCP Vertex AI などで Gemini API を有効化し、APIキーを取得します。(gemini-1.5-flash など利用したいモデルを確認)
  3. Chatwork API:
    • ChatworkのAPI設定画面から APIトークン を発行します。
    • 投稿したい ルームID を確認します。(例: 123456789
  4. Google Analytics 4 (GA4):
    • レポート対象の GA4 プロパティ ID を確認します(例: properties/123456789)。
    • レポート対象サイトを特定するための ホスト名(例: your-target-site.example.com)を確認します。
  5. Google Apps Script (GAS) プロジェクト:
    • 新しいGASプロジェクトを作成します。
    • タイムゾーン設定: プロジェクト設定でタイムゾーンを「(GMT+09:00) 東京」などに設定します。
    • appsscript.json の編集:
      • マニフェストファイルを表示する設定にし、以下の oauthScopes を設定します。
        {
          "timeZone": "Asia/Tokyo",
          "dependencies": {},
          "exceptionLogging": "STACKDRIVER",
          "runtimeVersion": "V8",
          "oauthScopes": [
            "[https://www.googleapis.com/auth/script.external_request](https://www.googleapis.com/auth/script.external_request)",
            "[https://www.googleapis.com/auth/analytics.readonly](https://www.googleapis.com/auth/analytics.readonly)",
            "[https://www.googleapis.com/auth/script.scriptapp](https://www.googleapis.com/auth/script.scriptapp)"
          ]
        }
        
    • (強く推奨) スクリプトプロパティの設定:
      • 取得したChatwork APIトークンとGemini APIキーは、コード内に直接記述せず、「プロジェクトの設定」>「スクリプト プロパティ」に保存します。
      • CHATWORK_API_TOKEN, GEMINI_API_KEY という名前でそれぞれの値を保存します。

c. GAS スクリプト解説

以下に、スクリプトの主要な関数について解説します。(完全なコードは最後に掲載します)

定数と設定

スクリプト冒頭で設定値を定数として定義します。APIキーなどはスクリプトプロパティから読み込むことを前提とします。

// 定数設定 (抜粋)
const GA4_PROPERTY_ID = 'properties/YOUR_GA4_PROPERTY_ID'; // ★あなたのGA4プロパティIDに置き換え
const CHATWORK_API_TOKEN = PropertiesService.getScriptProperties().getProperty('CHATWORK_API_TOKEN') || 'YOUR_CHATWORK_API_TOKEN_PLACEHOLDER'; // スクリプトプロパティ推奨
const CHATWORK_ROOM_ID = 'YOUR_CHATWORK_ROOM_ID'; // ★あなたのChatworkルームIDに置き換え
const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY') || 'YOUR_GEMINI_API_KEY_PLACEHOLDER'; // スクリプトプロパティ推奨
const GEMINI_API_ENDPOINT = '[https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=](https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=)' + GEMINI_API_KEY; // モデル名は適宜変更
const TARGET_HOSTNAME = 'your-target-site.example.com'; // ★レポート対象のホスト名に置き換え
const MENTION_PREFIX = '[To:123456789]担当者さん\n'; // ★メンション先AIDと表示名に置き換え (不要なら空 '')
const CHATWORK_ERROR_MENTION_AID = '123456789'; // ★エラー時メンション先AID (上記と同じか、管理者など。不要なら空 '')

メイン関数 (reportBunkenMagazineToChatwork)

全体の処理フローを制御します。日付計算、各API呼び出し、エラーハンドリングを行います。

function reportBunkenMagazineToChatwork() { // 関数名は適宜変更推奨
  try {
    // ... (日付計算) ...
    // ... (デバッグログ) ...
    // 2. GA4データ取得
    const siteData = fetchGa4Data(formattedStartDate, /* ... */); // 関数名変更例
    // 3. Geminiで考察生成 & 整形
    const insightsRaw = generateInsights(siteData, /* ... */); // 関数名変更例
    const insights = insightsRaw.replaceAll(': ', ':');
    // 4. メッセージ作成
    const message = formatChatMessage(siteData, insights, /* ... */); // 関数名変更例
    // 5. Chatwork投稿
    postToChatwork(message);
    Logger.log('メイン処理 正常終了');
  } catch (error) {
    // ... (エラーハンドリング) ...
  }
}

GA4データ取得関数 (WorkspaceGa4Data)

UrlFetchApp で GA4 Data API を呼び出します。認証、リクエストボディ(指標、期間、ホスト名フィルタ)、応答解析(rowsから指標抽出、YoY計算)を行います。

function fetchGa4Data(startDate, endDate, prevStartDate, prevEndDate) { // 関数名変更例
  // ... (引数チェック) ...
  // ... (metrics, dateRanges, dimensionFilter (TARGET_HOSTNAMEを使用) 設定) ...
  const requestBody = { dateRanges, metrics, dimensionFilter };
  // ... (UrlFetchAppでAPI呼び出し、応答解析、YoY計算) ...
  return { currentSessions, /* ... */ };
}

Gemini考察生成関数 (generateInsights)

UrlFetchApp で Gemini API を呼び出します。APIキー認証、プロンプト作成(GA4データと具体的な分析指示を含む)、応答解析を行います。

function generateInsights(siteData, startDate, endDate) { // 関数名変更例
  const prompt = `
ウェブサイト「${TARGET_HOSTNAME}」のアクセスデータ分析をお願いします。
# 分析対象データ
* 報告期間: ${startDate}${endDate}
* 期間中セッション数: ${siteData.currentSessions.toLocaleString()} (...)
* ... (他のデータ) ...
# 分析依頼
... (具体的な指示) ...
`;
  // ... (UrlFetchAppでAPI呼び出し、応答解析) ...
  return generatedText;
}

Chatworkメッセージ作成関数 (formatChatMessage)

取得データと考察をChatwork記法で整形します。メンションを先頭に追加します。

function formatChatMessage(siteData, insights, startDate, endDate) { // 関数名変更例
  let message = MENTION_PREFIX; // メンションから開始
  // ... (日付フォーマット) ...
  message += `[info][title]【GA4 定期レポート:対象サイト】[/title]`; // タイトルを一般化
  message += `【報告対象期間】...(${TARGET_HOSTNAME})\n`;
  message += `セッション:${siteData.currentSessions.toLocaleString()}...\n`;
  // ... (PV、区切り線、考察パート) ...
  message += `${insights}\n`;
  message += `[/info]`;
  return message;
}

Chatwork投稿関数 (postToChatwork)

UrlFetchApp で Chatwork API を呼び出します。APIトークン認証、リクエストボディ(body, self_unread)、contentTypeself_unread: '0' の指定がポイントです。

function postToChatwork(message) {
  const url = `https://api.chatwork.com/v2/rooms/${CHATWORK_ROOM_ID}/messages`;
  const options = {
    method: 'post',
    headers: { 'X-ChatWorkToken': CHATWORK_API_TOKEN },
    contentType: 'application/x-www-form-urlencoded',
    payload: {
      body: message,
      self_unread: '0' // 文字列 '0' で false を指定
    },
    muteHttpExceptions: true
  };
  // ... (UrlFetchAppでAPI呼び出し、応答チェック) ...
}

d. 自動実行の設定

GASの「トリガー」メニューから、reportBunkenMagazineToChatwork (または変更した関数名) を「時間主導型」で設定します(例: 月ベースタイマーの毎月1日)。

e. 注意点とハマりどころ(今回の学び)

  • OAuthスコープ: appsscript.jsonでの設定と承認が必須。
  • APIパラメータ: GA4のdimensionsdateRangeはNG。Chatworkのself_unreadは boolean falseではなく文字列 '0'が必要だった。API仕様は細部まで確認、試行錯誤が必要。
  • GAS実行関数: エディタ上部のプルダウンで正しいメイン関数を選択して実行すること。
  • Geminiプロンプト: AIの出力品質はプロンプト次第。具体的かつ明確な指示が必要。
  • デバッグ: Logger.logは問題解決の友。変数やAPI応答をこまめに確認。
  • 機密情報管理: APIキー/トークンは必ずスクリプトプロパティで管理する。

6. まとめ

今回は、GASを中心に複数のAPIを連携させ、GA4レポート作成からAI考察、Chatwork通知までを完全自動化する手順を紹介しました。

この仕組みで定型業務から解放され、より本質的な分析や施策立案に時間を使えるようになります。AIの活用で新たな気づきも得られるかもしれません。

紹介したコードをベースに、あなたの分析したい指標やサイト、考察してほしい観点に合わせてカスタマイズしてみてください。最後までお読みいただき、ありがとうございました。

7. 参考:完全なスクリプトコード(公開用・ダミー値)

/**
 * GA4データを集計し、Gemini APIで考察を加えてChatworkに自動投稿するスクリプト
 * 公開用サンプルバージョン
 */

// --- 定数設定 ---
// ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
// ★ 以下のプレースホルダーをあなたの環境に合わせて設定してください ★
// ★ APIキー/トークンはスクリプトプロパティでの管理を強く推奨します ★
// ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
const GA4_PROPERTY_ID = 'properties/YOUR_GA4_PROPERTY_ID'; // 例: properties/123456789
const CHATWORK_API_TOKEN = PropertiesService.getScriptProperties().getProperty('CHATWORK_API_TOKEN') || 'YOUR_CHATWORK_API_TOKEN_PLACEHOLDER';
const CHATWORK_ROOM_ID = 'YOUR_CHATWORK_ROOM_ID';         // 例: 123456789
const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY') || 'YOUR_GEMINI_API_KEY_PLACEHOLDER';
const GEMINI_API_ENDPOINT = '[https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=](https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=)' + GEMINI_API_KEY; // モデル名は適宜変更
const TARGET_HOSTNAME = 'your-target-site.example.com';     // 例: [www.example.com](https://www.example.com)
const MENTION_PREFIX = '[To:123456789]担当者さん\n';       // 例: [To:アカウントID]表示名\n (不要なら空 '')
const CHATWORK_ERROR_MENTION_AID = '123456789';         // 例: エラー通知先アカウントID (不要なら空 '')

/**
 * メイン実行関数:指定した関数のレポートを作成しChatworkに投稿する
 */
function createAndPostReport() { // 関数名をより汎用的に変更
  try {
    // 1. 日付計算 (前月と比較対象の前年同月)
    const today = new Date();
    let targetYear = today.getFullYear();
    let targetMonthIndex = today.getMonth() - 1;
    if (targetMonthIndex < 0) { targetMonthIndex = 11; targetYear--; }
    const startDate = new Date(targetYear, targetMonthIndex, 1);
    const endDate = new Date(targetYear, targetMonthIndex + 1, 0);
    const prevYearStartDate = new Date(targetYear - 1, targetMonthIndex, 1);
    const prevYearEndDate = new Date(targetYear - 1, targetMonthIndex + 1, 0);

    const scriptTimeZone = Session.getScriptTimeZone();
    Logger.log(`Using Timezone for Formatting: ${scriptTimeZone}`);

    const formattedStartDate = Utilities.formatDate(startDate, scriptTimeZone, 'yyyy-MM-dd');
    const formattedEndDate = Utilities.formatDate(endDate, scriptTimeZone, 'yyyy-MM-dd');
    const formattedPrevYearStartDate = Utilities.formatDate(prevYearStartDate, scriptTimeZone, 'yyyy-MM-dd');
    const formattedPrevYearEndDate = Utilities.formatDate(prevYearEndDate, scriptTimeZone, 'yyyy-MM-dd');

    Logger.log(`期間設定完了: Current=${formattedStartDate}~${formattedEndDate}, Previous=${formattedPrevYearStartDate}~${formattedPrevYearEndDate}`);
    Logger.log(`Calling fetchGa4Data with: ${formattedStartDate}, ${formattedEndDate}, ${formattedPrevYearStartDate}, ${formattedPrevYearEndDate}`);

    // 2. GA4データ取得・集計
    const siteData = fetchGa4Data(formattedStartDate, formattedEndDate, formattedPrevYearStartDate, formattedPrevYearEndDate);
    Logger.log(`取得データ: ${JSON.stringify(siteData)}`);

    // 3. Geminiで考察生成 & 整形
    const insightsRaw = generateInsights(siteData, formattedStartDate, formattedEndDate);
    const insights = insightsRaw.replaceAll(': ', ':'); // Chatwork絵文字対策
    Logger.log(`生成インサイト(整形後): ${insights}`);

    // 4. Chatworkメッセージ作成
    const message = formatChatMessage(siteData, insights, formattedStartDate, formattedEndDate);

    // 5. Chatwork投稿
    postToChatwork(message);
    Logger.log('メイン処理 正常終了');

  } catch (error) {
    Logger.log('Error in main function: ' + error.toString() + '\n' + error.stack);
    try {
      let errorMessage = `GA4レポート自動投稿でエラーが発生しました。\nError: ${error.toString()}`;
      if (CHATWORK_ERROR_MENTION_AID) errorMessage = `[To:${CHATWORK_ERROR_MENTION_AID}]\n${errorMessage}`;
      else if (MENTION_PREFIX) errorMessage = MENTION_PREFIX + errorMessage; // エラーも通常宛先に通知する場合
      postToChatwork(errorMessage);
    } catch (e) {
      Logger.log('Failed to post error message to Chatwork: ' + e.toString() + '\nOriginal Error: ' + error.toString());
    }
  }
}

/**
 * GA4 Data APIから指定期間のデータを取得・集計する
 * @param {string} startDate - 開始日 (YYYY-MM-DD)
 * @param {string} endDate - 終了日 (YYYY-MM-DD)
 * @param {string} prevStartDate - 前年同期間の開始日 (YYYY-MM-DD)
 * @param {string} prevEndDate - 前年同期間の終了日 (YYYY-MM-DD)
 * @return {object} 集計データオブジェクト
 */
function fetchGa4Data(startDate, endDate, prevStartDate, prevEndDate) {
  if (!startDate || !endDate || !prevStartDate || !prevEndDate || typeof startDate !== 'string' || typeof endDate !== 'string' || typeof prevStartDate !== 'string' || typeof prevEndDate !== 'string') {
     throw new Error(`WorkspaceGa4Data に無効な日付: Start=${startDate}, End=${endDate}, PrevStart=${prevStartDate}, PrevEnd=${prevEndDate}`);
  }
  const metrics = [ { name: 'sessions' }, { name: 'screenPageViews' }];
  const dateRanges = [ { startDate: startDate, endDate: endDate, name: 'current' }, { startDate: prevStartDate, endDate: prevEndDate, name: 'previous' } ];
  const dimensionFilter = { filter: { fieldName: 'hostName', stringFilter: { matchType: 'EXACT', value: TARGET_HOSTNAME }}};
  const requestBody = { dateRanges: dateRanges, metrics: metrics, dimensionFilter: dimensionFilter };
  const options = { method: 'post', contentType: 'application/json', headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() }, payload: JSON.stringify(requestBody), muteHttpExceptions: true };
  const apiUrl = `https://analyticsdata.googleapis.com/v1beta/${GA4_PROPERTY_ID}:runReport`;

  Logger.log(`Calling GA4 API: ${apiUrl}`);
  Logger.log(`Request Body: ${JSON.stringify(requestBody)}`);

  const response = UrlFetchApp.fetch(apiUrl, options);
  const responseCode = response.getResponseCode();
  const responseBody = response.getContentText();

  if (responseCode !== 200) {
    Logger.log(`GA4 API Error: ${responseCode} - ${responseBody}`);
    throw new Error(`GA4 API取得失敗 Status: ${responseCode}, Body: ${responseBody}`);
  }

  const report = JSON.parse(responseBody);
  Logger.log(`GA4 API Response: ${JSON.stringify(report)}`);

  let currentSessions = 0, currentPv = 0, prevSessions = 0, prevPv = 0;
  const metricHeaders = report.metricHeaders;

  if (report.dimensionHeaders && report.dimensionHeaders[0].name === 'dateRange' && report.rows && report.rows.length > 0) {
      Logger.log("Extracting data from 'rows' using implicit dateRange dimension.");
      report.rows.forEach(row => {
          const dateRangeValue = row.dimensionValues[0].value;
          const sessionValue = parseInt(row.metricValues[0].value || '0');
          const pvValue = parseInt(row.metricValues[1].value || '0');
          if (dateRangeValue === 'current') { currentSessions = sessionValue; currentPv = pvValue; Logger.log(`Found Current Data: Sessions=${currentSessions}, PV=${currentPv}`); }
          else if (dateRangeValue === 'previous') { prevSessions = sessionValue; prevPv = pvValue; Logger.log(`Found Previous Data: Sessions=${prevSessions}, PV=${prevPv}`); }
      });
  } else if (report.rowCount === 0) { Logger.log("GA4 Report: No data found."); }
  else { Logger.log("GA4 Report: Response structure unexpected."); }

  Logger.log(`Parsed Values: Current(Sess:${currentSessions}, PV:${currentPv}), Previous(Sess:${prevSessions}, PV:${prevPv})`);

  const sessionYoY = prevSessions > 0 ? (currentSessions / prevSessions) * 100 : (currentSessions > 0 ? Infinity : 0);
  const pvYoY = prevPv > 0 ? (currentPv / prevPv) * 100 : (currentPv > 0 ? Infinity : 0);
  const formatYoY = (y) => (y === Infinity ? "N/A (前年ゼロ)" : isNaN(y) ? "N/A" : y.toFixed(1) + '%');

  return {
    currentSessions: currentSessions, currentPv: currentPv,
    prevSessions: prevSessions, prevPv: prevPv,
    sessionYoY: sessionYoY, pvYoY: pvYoY,
    sessionYoYFormatted: formatYoY(sessionYoY), pvYoYFormatted: formatYoY(pvYoY)
  };
}

/**
 * Gemini APIを使用してデータに関する考察を生成する
 * @param {object} siteData - GA4から取得・集計したデータ
 * @param {string} startDate - 開始日 (YYYY-MM-DD)
 * @param {string} endDate - 終了日 (YYYY-MM-DD)
 * @return {string} 生成された考察テキスト
 */
function generateInsights(siteData, startDate, endDate) {
  const prompt = `
ウェブサイト「${TARGET_HOSTNAME}」のアクセスデータ分析をお願いします。

# 分析対象データ
* 報告期間: ${startDate}${endDate}
* 期間中セッション数: ${siteData.currentSessions.toLocaleString()} (昨年同期間比: ${siteData.sessionYoYFormatted})
* 期間中PV数: ${siteData.currentPv.toLocaleString()} (昨年同期間比: ${siteData.pvYoYFormatted})
* (参考) 昨年同期間セッション数: ${siteData.prevSessions.toLocaleString()}
* (参考) 昨年同期間PV数: ${siteData.prevPv.toLocaleString()}

# 分析依頼
上記データに基づき、以下の点を明確かつ具体的に分析し、箇条書きで記述してください。

1.  **現状サマリー:**
    * 期間中のアクセス状況(セッション数、PV数)と、昨年比較での増減傾向を簡潔にまとめてください。

2.  **特筆すべき点:**
    * 昨年比較で特に大きな変化(増加・減少)が見られる指標はありますか? その変化率と具体的な数値を挙げてください。
    * その変化の要因として考えられること(仮説)を推測してください。(例:特定のキャンペーン、SEO変動、記事の影響、季節要因など)

3.  **課題・懸念点:**
    * もしアクセス数が減少している場合、考えられる具体的な問題点は何ですか?
    * アクセス数が伸びている場合でも、潜在的な課題や今後注意すべき点はありますか?

4.  **考察・次のアクションへのヒント:**
    * 今回のデータから、どのようなインサイトが得られますか?(ユーザー行動の変化、コンテンツの評価など)
    * 今後、アクセス改善のために試すべき施策や、さらに詳しく分析すべき点があれば提案してください。
`;
  const payload = { contents: [{ parts: [{ text: prompt }] }] };
  const options = { method: 'post', contentType: 'application/json', payload: JSON.stringify(payload), muteHttpExceptions: true };

  Logger.log(`Calling Gemini API: ${GEMINI_API_ENDPOINT}`);

  const response = UrlFetchApp.fetch(GEMINI_API_ENDPOINT, options);
  const responseCode = response.getResponseCode();
  const responseBody = response.getContentText();

  if (responseCode === 200) {
    const jsonResponse = JSON.parse(responseBody);
    if (jsonResponse.candidates && jsonResponse.candidates[0]?.content?.parts?.[0]?.text) {
        return jsonResponse.candidates[0].content.parts[0].text.trim();
    } else {
        Logger.log(`Gemini API response format unexpected: ${responseBody}`);
        return `インサイトの生成に失敗しました(レスポンス形式エラー)。`;
    }
  } else {
    Logger.log(`Gemini API Error: ${responseCode} - ${responseBody}`);
    return `インサイトの生成に失敗しました(APIエラーコード: ${responseCode})。`;
  }
}

/**
 * Chatwork投稿用にメッセージを整形する
 * @param {object} siteData - GA4から取得・集計したデータ
 * @param {string} insights - Geminiが生成した考察テキスト (コロン置換済み)
 * @param {string} startDate - 開始日 (YYYY-MM-DD)
 * @param {string} endDate - 終了日 (YYYY-MM-DD)
 * @return {string} 整形済みメッセージ文字列
 */
function formatChatMessage(siteData, insights, startDate, endDate) {
  let message = MENTION_PREFIX; // メンションで開始
  const formattedStartDateJp = Utilities.formatDate(new Date(startDate), Session.getScriptTimeZone(), 'M月d日');
  const formattedEndDateJp = Utilities.formatDate(new Date(endDate), Session.getScriptTimeZone(), 'M月d日');

  message += `[info][title]【GA4 定期レポート:対象サイト】[/title]`; // タイトルを一般化
  message += `【報告対象期間】${formattedStartDateJp}${formattedEndDateJp}${startDate}${endDate})\n`;
  message += `■アクセス推移 (${TARGET_HOSTNAME})\n`;
  message += `セッション:${siteData.currentSessions.toLocaleString()}(昨年同月比: ${siteData.sessionYoYFormatted})\n`;
  message += `PV:${siteData.currentPv.toLocaleString()}(昨年同月比:${siteData.pvYoYFormatted})\n`;
  message += `[hr]`;
  message += `■考察・インサイト (AIによる分析)\n`;
  message += `${insights}\n`; // 整形済みインサイト
  message += `[/info]`;
  return message;
}

/**
 * Chatwork APIを呼び出してメッセージを投稿する
 * @param {string} message - 投稿するメッセージ本文
 */
function postToChatwork(message) {
  const url = `https://api.chatwork.com/v2/rooms/${CHATWORK_ROOM_ID}/messages`;
  const options = {
    method: 'post',
    headers: { 'X-ChatWorkToken': CHATWORK_API_TOKEN },
    contentType: 'application/x-www-form-urlencoded',
    payload: {
      body: message,
      self_unread: '0' // 文字列 '0' で false を指定
    },
    muteHttpExceptions: true
  };

  Logger.log(`Posting to Chatwork Room ID: ${CHATWORK_ROOM_ID}`);
  Logger.log(`Chatwork Request Options: ${JSON.stringify(options)}`);

  const response = UrlFetchApp.fetch(url, options);
  const responseCode = response.getResponseCode();
  const responseBody = response.getContentText();

  if (responseCode === 200) {
    Logger.log('Message posted to Chatwork successfully.');
  } else {
    Logger.log(`Error posting to Chatwork: ${responseCode} - ${responseBody}`);
    Logger.log(`Failed Chatwork Payload: ${JSON.stringify(options.payload)}`);
    // エラーをスローしてメイン関数でキャッチさせる
    throw new Error(`Chatwork投稿失敗 Status: ${responseCode}, Body: ${responseBody}`);
  }
}

// --- (おまけ) スクリプトプロパティ設定用関数 ---
/*
function setScriptProperties() {
  // ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
  // ★ ここにあなたの Chatwork API Token と Gemini API Key を入力 ★
  // ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
  const chatworkToken = 'YOUR_CHATWORK_API_TOKEN_HERE';
  const geminiKey = 'YOUR_GEMINI_API_KEY_HERE';

  PropertiesService.getScriptProperties().setProperties({
    'CHATWORK_API_TOKEN': chatworkToken,
    'GEMINI_API_KEY': geminiKey
  }, true); // true: 既存のプロパティを全て削除してから設定
  Logger.log('Script properties set. PLEASE REMOVE YOUR KEYS FROM THE CODE NOW.');
}
*/

/*
// --- (おまけ) スクリプトプロパティ読み込みテスト用関数 ---
function testReadProperties() {
  const token = PropertiesService.getScriptProperties().getProperty('CHATWORK_API_TOKEN');
  const key = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  Logger.log(`Token from Properties: ${token ? token.substring(0, 5) + '...' : 'null or not set'}`);
  Logger.log(`Key from Properties: ${key ? key.substring(0, 5) + '...' : 'null or not set'}`);
}
*/

Discussion

ログインするとコメントできます