1. 「今週もレポート出すのか…」という営業・マーケあるあるから
「先週の流入、どうだった?」
「今月の目標に対して、進捗どう?」
営業やマーケの現場で、週次・月次の報告を求められる場面は、もはや日常茶飯事。
中には、Googleアナリティクス(GA4)を開いて、スプレッドシートに数値を貼って、グラフを作って、パワポに転記して…という一連のルーティンに疲弊している人も少なくありません。
ところが現実には
- 表は開かれず、PDFの1ページ目しか見られていない
- グラフは「なんかいい感じ」ならOK
- そもそも、上司は報告会にいない
といった「見られないレポートを作る」という矛盾が蔓延しています。
ダッシュボード見てくれれば済む話なのに…とは思うものの、報告を止めるわけにはいかない…
それでは、いっそのこと「人間が作らない仕組み」にしてしまおう!というのが、今回のアプローチです。
2. そんな無駄の多い定例業務はGeminiがうまくやってくれちゃう
まずは、実際のGemini Canvas出力を見てみてください。

GoogleのGeminiが、CSVファイルから自動生成してくれるこのインフォグラフィック、想像以上に仕上がっています。
- 自動でグラフ化されたトラフィック推移
- 見やすく整えられた配色と余白設計
- KPI別に切り分けられたセクション構造
- そしてある意味地雷になるかもしれない要約コメント
これらがたった1回のGemini Canvasへのアップロードで完結するわけです。
もちろん細かな編集はできますが、Slackにそのまま貼れるレベルの完成度で出てくるのは、地味に驚きます。
Looker Studioのような高度な設定も不要で、テンプレートを作るだけでなく、「資料そのもの」をGeminiが作ってくれる。
この時点で簡易報告系のグラフ作成に「もうスプシでごにょごにょせんでよいのでは?」感が漂い始めます。
3. GASでGA4→Geminiの連携を組むと、もう人間が書かなくてよくなる
実はこの仕組み、スプレッドシートすら使わずに、完全に自動で動かせる方法があります。
それが、Google Analytics Data APIを直接GASから呼び出す方法です。
本来GA4の定型レポートは「メール通知」や「PDFエクスポート」など人の手を必要とするものが多く、また一時的な受け皿とするスプレッドシートでもプラグインを活用する方法もあるのですが、Data APIを使えばGASから直接データを引っ張ることができます。
BigQueryやLooker StudioといったGoogle Cloud Platform系のサービスをこれまで使っていない、そこまでの膨大なデータ量を取り扱っていない規模の企業でも、大丈夫です。
また、GoogleWorkspaceでGeminiを活用している企業であれば、Gemini APIの無料枠の範囲内で収まる可能性が高く、Google Analytics Data APIも、今回のような週次、月次レベルのレポーティングであれば無料枠の範囲内で収まるはずです。
流れとしては、こんな感じです
- GASでGA4から直接データ取得(Google Analytics Data API)
→ GCPのAPIを有効化し、プロパティIDや指標を定義すればOK
→ 1日25,000件まで無料なので、週1〜2回の運用ではまず問題なし - GAS側でHTMLorCSVテンプレ生成
→ Canvasに投げやすい形に整形
→ 人気ページ/流入元など、営業・マーケで使いやすい視点で出力可能 - Gemini APIでCanvas生成+分析コメント追加
→ 「今週は自然検索が微増、全体流入は安定」など、文脈のある報告が自動でついてくる - GmailAppでチームに自動送信 or Slack通知
→ 件名も本文もテンプレ化しておけば、まさに報告しないレポートの完成
→ 毎週水曜の午前10時、自動で「前週+前々週の比較レポート」が届く世界観 - 以下のような工夫も実装済み
→ レポート期間の自動化:前週(日〜土)と前々週の2週分を毎週取得&比較
→ 重要指標の拡充:ユーザー数、人気ページ、参照元、セッション数など
→ 完全ノーエクセル運用:もうスプレッドシートは不要です
こうして、人間が「作る」「送る」「話す」といった工程を省き、データが自然に報告されていく仕組みが出来上がります。
言ってしまえば、Geminiが上司に代わって報告してくれる状態です。
4. GA4をGemini Camvasでインフォグラフィック化しメール送信するGAS
1. 準備するもの
Google Cloud Platform (GCP) プロジェクト:GCPコンソールで新しいプロジェクトを作成するか、既存のものを利用します。
GA4 プロパティID:レポートを取得したいGA4のプロパティID(URLのpの後の数字)を控えておきます。

Gemini APIキー:Google AI for DevelopersからAPIキーを取得します。
2. GCPでAPIを有効化する
- GCPコンソールで、準備したプロジェクトを選択します。
- 左上のナビゲーションメニューから「APIとサービス」>「ライブラリ」を選択します。
- 検索バーに「Google Analytics Data API」と入力し、表示されたAPIをクリックして「有効にする」ボタンを押します。

3. Google Apps Scriptプロジェクトの作成と設定
- Google Apps Scriptのダッシュボードにアクセスし、「新しいプロジェクト」を作成します。
- GCPプロジェクトの紐付け
- エディタの左側メニューから「プロジェクトの設定」(歯車アイコン)をクリックします。
- 一番下にある「Google Cloud Platform(GCP)プロジェクト」セクションで、「プロジェクトを変更」をクリックします。
- 準備したGCPプロジェクトのプロジェクト番号を入力し、「プロジェクトを設定」をクリックします。(プロジェクト番号はGCPのダッシュボードで確認できます)


- GAS側でAPIサービスを有効化
- エディタの左側メニュー「サービス」の横にある「+」アイコンをクリックします。
- リストから「Google Analytics Data API」を選択し、「追加」ボタンをクリックします。

4. コードのコピー&ペースト
- デフォルトで表示されているコードをすべて削除し、以下の内容を貼り付けます。
// --- 設定項目 ---
// このセクションの値を、ご自身の環境に合わせて変更してください。
// ---------------------------------------------------------------
// 1. GA4のプロパティID (例: '123456789')
// GA4の管理画面 > 「プロパティ設定」で確認できる数字です。
const GA4_PROPERTY_ID = 'YOUR_GA4_PROPERTY_ID';
// 2. レポートメールの送信先メールアドレス
const RECIPIENT_EMAIL = 'recipient@example.com'; // 例: 'boss@your-company.com, team@your-company.com'
// 3. レポートメールの件名
const EMAIL_SUBJECT = '【週次レポート】ウェブサイト パフォーマンス分析';
// 4. [オプション] レポートに含めたい主要なコンバージョンイベント名
// GA4で設定しているコンバージョンイベントの名前を文字列で指定します。
const CONVERSION_EVENT_NAMES = ['purchase', 'generate_lead'];
// --- メインの実行関数 ---
// この関数をトリガーで定期実行することで、全体の処理が自動的に開始されます。
// ---------------------------------------------------------------
function main() {
try {
// 1. レポート対象となる期間(前週と前々週)を自動で計算します。
const { currentPeriod, previousPeriod } = calculateReportPeriods();
// 2. GA4 Data APIを呼び出して、2期間分のレポートデータを取得します。
const currentData = fetchGa4Data(currentPeriod.startDate, currentPeriod.endDate);
const previousData = fetchGa4Data(previousPeriod.startDate, previousPeriod.endDate);
// 3. 取得したデータを基に、Gemini APIへ分析を依頼します。
const geminiAnalysis = getGeminiAnalysis(currentData, previousData, currentPeriod.label, previousPeriod.label);
// 4. Geminiの分析結果を含む、メール配信用HTMLレポートを生成します。
const htmlBody = generateReportHtml(currentData, previousData, currentPeriod.label, previousPeriod.label, geminiAnalysis);
// 5. 完成したHTMLレポートを指定されたメールアドレスに送信します。
sendReportEmail(RECIPIENT_EMAIL, EMAIL_SUBJECT, htmlBody);
Logger.log('レポートの生成と送信が正常に完了しました。');
} catch (error) {
// 途中でエラーが発生した場合、ログに詳細を記録します。
Logger.log('エラーが発生しました: ' + error.toString() + ' Stack: ' + error.stack);
}
}
// --- 日付処理関数 ---
// ---------------------------------------------------------------
/**
* レポート対象となる2つの期間(前週と前々週)を計算します。
* このスクリプトは「毎週水曜に実行」を想定しているため、実行日を基準に
* 「直前の日曜〜土曜」と「その前の日曜〜土曜」を自動で算出します。
* @return {{currentPeriod: Object, previousPeriod: Object}} 開始日・終了日・ラベルを含むオブジェクト
*/
function calculateReportPeriods() {
const today = new Date();
const dayOfWeek = today.getDay(); // 曜日を数字で取得 (日曜=0, 月曜=1, ..., 土曜=6)
// 実行日を基準に、一番最近の土曜日がいつだったかを計算
const lastSaturday = new Date(today);
lastSaturday.setDate(today.getDate() - dayOfWeek - 1);
// 前週の開始日(土曜日から6日前なので日曜日)を計算
const lastSunday = new Date(lastSaturday);
lastSunday.setDate(lastSaturday.getDate() - 6);
// 前々週の終了日(前週の日曜日の1日前)
const saturdayBeforeLast = new Date(lastSunday);
saturdayBeforeLast.setDate(lastSunday.getDate() - 1);
// 前々週の開始日(その土曜日から6日前)
const sundayBeforeLast = new Date(saturdayBeforeLast);
sundayBeforeLast.setDate(saturdayBeforeLast.getDate() - 6);
// APIで使える 'YYYY-MM-DD' 形式の文字列に変換
const formatDate = (date) => date.toISOString().slice(0, 10);
return {
currentPeriod: { // 現行期間(前週)
startDate: formatDate(lastSunday),
endDate: formatDate(lastSaturday),
label: `${formatDate(lastSunday)} - ${formatDate(lastSaturday)}`
},
previousPeriod: { // 比較期間(前々週)
startDate: formatDate(sundayBeforeLast),
endDate: formatDate(saturdayBeforeLast),
label: `${formatDate(sundayBeforeLast)} - ${formatDate(saturdayBeforeLast)}`
}
};
}
// --- GA4 Data API 連携関数 ---
// ---------------------------------------------------------------
/**
* GA4 Data APIを呼び出して、指定された期間のレポートデータを取得します。
* ここで「どんな指標を」「どんな切り口で」取得するかを定義します。
* @param {string} startDate - 開始日 (YYYY-MM-DD)
* @param {string} endDate - 終了日 (YYYY-MM-DD)
* @return {Object} - チャネル別レポートとページ別レポートを含むオブジェクト
*/
function fetchGa4Data(startDate, endDate) {
const propertyName = `properties/${GA4_PROPERTY_ID}`;
// 取得したいレポートの内容を複数定義
const requests = [
// レポート1: チャネル別レポート
{
dateRanges: [{ startDate, endDate }],
dimensions: [{ name: 'sessionDefaultChannelGroup' }], // 分析の切り口(ディメンション)
metrics: [{ name: 'sessions' }, { name: 'engagedSessions' }, { name: 'totalUsers' }, { name: 'newUsers' }], // 取得したい数値(メトリクス)
orderBys: [{ metric: { metricName: 'sessions' }, desc: true }], // セッション数の多い順に並べる
limit: 10 // 上位10件に絞る
},
// レポート2: ページ別(人気コンテンツ)レポート
{
dateRanges: [{ startDate, endDate }],
dimensions: [{ name: 'pageTitle' }, { name: 'pagePath' }],
metrics: [{ name: 'screenPageViews' }, { name: 'userEngagementDuration' }, { name: 'conversions' }],
orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }], // 閲覧数の多い順に並べる
limit: 10
}
];
// 定義したリクエストをまとめてAPIに送信
const response = AnalyticsData.Properties.batchRunReports({ requests: requests }, propertyName);
// 取得したデータを扱いやすい形に整形して返す
return {
channelReport: parseReport(response.reports[0]),
pageReport: parseReport(response.reports[1])
};
}
/**
* GA4 Data APIからのレスポンスを、扱いやすいオブジェクトの配列に変換します。
* @param {Object} report - APIレスポンスのレポート部分
* @return {Array<Object>} - 整形後のデータ配列
*/
function parseReport(report) {
const header = [...(report.dimensionHeaders || []).map(h => h.name), ...(report.metricHeaders || []).map(h => h.name)];
if (!report.rows) return [];
return report.rows.map(row => {
const rowData = {};
[...(row.dimensionValues || []), ...(row.metricValues || [])].forEach((value, i) => {
rowData[header[i]] = value.value;
});
return rowData;
});
}
// --- Gemini API 連携関数 ---
// ---------------------------------------------------------------
/**
* Gemini APIを呼び出し、GA4のデータに基づいた分析コメントを生成させます。
* @param {Object} currentData - 現行期間のデータ
* @param {Object} previousData - 比較期間のデータ
* @param {string} currentPeriodLabel - 現行期間のラベル
* @param {string} previousPeriodLabel - 比較期間のラベル
* @return {string} Geminiが生成したMarkdown形式のテキスト
*/
function getGeminiAnalysis(currentData, previousData, currentPeriodLabel, previousPeriodLabel) {
const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
if (!GEMINI_API_KEY) throw new Error('Gemini APIキーが設定されていません。スクリプトプロパティを確認してください。');
const prompt = createPromptForGemini(currentData, previousData, currentPeriodLabel, previousPeriodLabel);
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }] };
const options = { method: 'post', contentType: 'application/json', payload: JSON.stringify(payload), muteHttpExceptions: true };
const response = UrlFetchApp.fetch(url, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode === 200) {
const result = JSON.parse(responseBody);
return result.candidates[0].content.parts[0].text;
} else {
Logger.log(`Gemini API Error: ${responseCode} - ${responseBody}`);
return "AIによる分析の生成中にエラーが発生しました。";
}
}
/**
* Geminiに送信するためのプロンプト(指示文)を動的に生成します。
* ここで渡す情報の質が、AIの分析コメントの質を決めます。
*/
function createPromptForGemini(currentData, previousData, currentPeriodLabel, previousPeriodLabel) {
let prompt = `あなたは優秀なWebマーケティングアナリストです。以下のウェブサイトのトラフィックデータ比較に基づいて、プロフェッショナルな視点から「総合評価」「注目すべき変化」「推奨アクション」を日本語のマークダウン形式でまとめてください。\n\n`;
prompt += `## 比較期間\n- **現行期間:** ${currentPeriodLabel}\n- **比較期間:** ${previousPeriodLabel}\n\n`;
const kpiCurrent = currentData.channelReport.reduce((acc, row) => ({
sessions: acc.sessions + parseInt(row.sessions || 0),
users: acc.users + parseInt(row.totalUsers || 0)
}), { sessions: 0, users: 0 });
const kpiPrevious = previousData.channelReport.reduce((acc, row) => ({
sessions: acc.sessions + parseInt(row.sessions || 0),
users: acc.users + parseInt(row.totalUsers || 0)
}), { sessions: 0, users: 0 });
prompt += `## 全体サマリー (KPI)\n`;
prompt += `- **合計セッション数:** ${kpiPrevious.sessions.toLocaleString()} → ${kpiCurrent.sessions.toLocaleString()}\n`;
prompt += `- **合計ユーザー数:** ${kpiPrevious.users.toLocaleString()} → ${kpiCurrent.users.toLocaleString()}\n\n`;
prompt += `## チャネル別データ\n${JSON.stringify(currentData.channelReport)}\n\n`;
prompt += `## 人気コンテンツデータ\n${JSON.stringify(currentData.pageReport)}\n\n`;
prompt += `上記データから、特に重要なポイントを抽出して分析してください。`;
return prompt;
}
// --- HTMLレポート生成 & メール送信関数 ---
// ---------------------------------------------------------------
/**
* GAS側でHTMLを完全に組み立て、メールで表示できるレポートを生成します。
* @returns {string} 完成したHTML文字列
*/
function generateReportHtml(currentData, previousData, currentPeriodLabel, previousPeriodLabel, geminiAnalysis) {
const template = HtmlService.createTemplateFromFile('template');
const allChannels = [...new Set([...currentData.channelReport.map(d => d.sessionDefaultChannelGroup), ...previousData.channelReport.map(d => d.sessionDefaultChannelGroup)])];
const mergedChannelData = allChannels.map(channel => {
const currentItem = currentData.channelReport.find(d => d.sessionDefaultChannelGroup === channel) || {};
const previousItem = previousData.channelReport.find(d => d.sessionDefaultChannelGroup === channel) || {};
return {
channel,
sessions_curr: parseInt(currentItem.sessions || 0),
sessions_prev: parseInt(previousItem.sessions || 0),
users_curr: parseInt(currentItem.totalUsers || 0),
users_prev: parseInt(previousItem.totalUsers || 0),
};
}).sort((a, b) => b.sessions_curr - a.sessions_curr);
// テンプレートファイル(template.html)に渡す変数を設定します。
template.currentPeriodLabel = currentPeriodLabel;
template.previousPeriodLabel = previousPeriodLabel;
template.geminiAnalysisHtml = markdownToHtml(geminiAnalysis);
template.sessionsChartHtml = createBarChartHtml(mergedChannelData, 'sessions');
template.usersChartHtml = createBarChartHtml(mergedChannelData, 'users');
template.pageReportTableHtml = createPageReportTableHtml(currentData.pageReport);
return template.evaluate().getContent();
}
/**
* 生成したレポートを指定された宛先にメールで送信します。
*/
function sendReportEmail(recipient, subject, htmlBody) {
if (!recipient) {
Logger.log('送信先メールアドレスが設定されていません。');
return;
}
GmailApp.sendEmail(recipient, subject, "", { htmlBody: htmlBody });
}
// --- HTML生成ヘルパー関数 ---
// 以下の関数群は、レポートの各パーツ(グラフや表)をHTML文字列として生成するための補助的な関数です。
/**
* 簡易的なHTMLバーチャートを生成します。
*/
function createBarChartHtml(data, key) {
const max_prev = Math.max(...data.map(d => d[key + '_prev']));
const max_curr = Math.max(...data.map(d => d[key + '_curr']));
const maxValue = Math.max(max_prev, max_curr, 1); // ゼロ除算を避けるために最低でも1を確保
let html = '<table style="width: 100%; border-collapse: collapse;">';
data.forEach(d => {
const prev_val = d[key + '_prev'];
const curr_val = d[key + '_curr'];
const prev_percent = (prev_val / maxValue) * 100;
const curr_percent = (curr_val / maxValue) * 100;
html += `
<tr>
<td style="width: 120px; padding: 4px 8px 4px 0; font-size: 12px; color: #334155; vertical-align: top; line-height: 1.5;">${d.channel}</td>
<td style="padding: 4px 0;">
<div style="font-size: 11px; color: #64748b; margin-bottom: 2px;">前: ${prev_val.toLocaleString()}</div>
<div style="background-color: #f1f5f9; border-radius: 4px; width: 100%;">
<div style="width: ${prev_percent}%; background-color: #94a3b8; height: 8px; border-radius: 4px;"></div>
</div>
<div style="font-size: 11px; color: #1e293b; margin-top: 4px; margin-bottom: 2px;">現: ${curr_val.toLocaleString()}</div>
<div style="background-color: #f1f5f9; border-radius: 4px; width: 100%;">
<div style="width: ${curr_percent}%; background-color: #4f46e5; height: 8px; border-radius: 4px;"></div>
</div>
</td>
</tr>
`;
});
html += '</table>';
return html;
}
/**
* 人気コンテンツのHTMLテーブルの行を生成します。
*/
function createPageReportTableHtml(data) {
if (!data || data.length === 0) return '<tr><td colspan="4" style="padding: 8px 4px; font-size: 12px;">データがありません。</td></tr>';
let html = '';
data.forEach(row => {
html += `
<tr style="border-bottom: 1px solid #e5e7eb;">
<td style="width: 55%; padding: 8px 4px; font-size: 12px; word-wrap: break-word; word-break: break-all;">${row.pageTitle}</td>
<td style="width: 15%; padding: 8px 4px; text-align: right; font-size: 12px;">${parseInt(row.screenPageViews || 0).toLocaleString()}</td>
<td style="width: 20%; padding: 8px 4px; text-align: right; font-size: 12px;">${parseFloat(row.userEngagementDuration || 0).toFixed(1)}秒</td>
<td style="width: 10%; padding: 8px 4px; text-align: right; font-size: 12px;">${parseInt(row.conversions || 0).toLocaleString()}</td>
</tr>
`;
});
return html;
}
/**
* Geminiが生成したMarkdownテキストを、メールで表示できるHTMLに変換します。
*/
function markdownToHtml(text) {
if (!text) return '';
const lines = text.split('\n');
let html = '';
lines.forEach(line => {
if (line.trim() === '') {
return;
}
// 見出し (###, ##)
if (line.startsWith('### ')) {
html += `<h3 style="font-size: 16px; font-weight: 600; margin-top: 16px; margin-bottom: 8px; padding-bottom: 4px; border-bottom: 1px solid #e5e7eb;">${line.substring(4)}</h3>`;
} else if (line.startsWith('## ')) {
html += `<h2 style="font-size: 18px; font-weight: 600; margin-top: 24px; margin-bottom: 16px;">${line.substring(3)}</h2>`;
}
// 箇条書き (* で始まる行)
else if (line.startsWith('* ')) {
const content = line.substring(2).replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
html += `<p style="margin: 4px 0 4px 20px; text-indent: -10px;">• ${content}</p>`;
}
// 番号付きリスト (1. で始まり、* の入れ子を含む可能性のある行)
else if (line.match(/^\d+\. /)) {
const parts = line.replace(/^\d+\. /, '').split(/\s\*\s/);
const mainPoint = parts[0].replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
html += `<p style="margin: 10px 0 5px 0;"><strong>${mainPoint}</strong></p>`;
if (parts.length > 1) {
for (let i = 1; i < parts.length; i++) {
const subPoint = parts[i].replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
html += `<p style="margin: 3px 0 3px 20px; text-indent: -10px;">• ${subPoint}</p>`;
}
}
}
// 通常の段落 (太字の見出しを含む可能性)
else {
const p = line.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
html += `<p style="margin: 12px 0 8px 0;">${p}</p>`;
}
});
return html;
}
- エディタの左側にある「ファイル」の横の「+」アイコンをクリックし、「HTML」を選択します。
- ファイル名を「template」として作成します。
- 作成された内容をすべて削除し、以下のtemplate.htmlの内容を貼り付けます。
// ===============================================================
// ファイル名: template.html
// ===============================================================
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ウェブサイト パフォーマンス分析レポート</title>
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f8fafc; color: #0f172a; margin: 0; padding: 0;">
<div style="max-width: 680px; margin: 0 auto; padding: 24px;">
<header>
<h1 style="font-size: 24px; font-weight: 700; margin: 0;">ウェブサイト パフォーマンス分析レポート</h1>
<p style="color: #475569; margin-top: 4px; line-height: 1.6;">期間: <span style="font-weight: 600;"><?!= currentPeriodLabel ?></span> vs <span style="font-weight: 600;"><?!= previousPeriodLabel ?></span></p>
</header>
<div style="margin-top: 24px; background-color: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);">
<h2 style="font-size: 18px; font-weight: 600; margin-top: 0; margin-bottom: 16px;">✨ AIによる分析サマリー</h2>
<div style="line-height: 1.6; font-size: 14px; word-break: break-word;"><?!= geminiAnalysisHtml ?></div>
</div>
<div style="margin-top: 24px; background-color: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);">
<h2 style="font-size: 18px; font-weight: 600; margin-top: 0; margin-bottom: 16px;">チャネル別セッション数</h2>
<div><?!= sessionsChartHtml ?></div>
</div>
<div style="margin-top: 24px; background-color: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);">
<h2 style="font-size: 18px; font-weight: 600; margin-top: 0; margin-bottom: 16px;">チャネル別ユーザー数</h2>
<div><?!= usersChartHtml ?></div>
</div>
<div style="margin-top: 24px; background-color: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);">
<h2 style="font-size: 18px; font-weight: 600; margin-top: 0; margin-bottom: 16px;">人気コンテンツ トップ10</h2>
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead>
<tr>
<th style="width: 55%; text-align: left; padding: 8px; background-color: #f1f5f9; font-size: 12px; border-bottom: 1px solid #e5e7eb;">ページタイトル</th>
<th style="width: 15%; text-align: right; padding: 8px; background-color: #f1f5f9; font-size: 12px; border-bottom: 1px solid #e5e7eb;">閲覧数</th>
<th style="width: 20%; text-align: right; padding: 8px; background-color: #f1f5f9; font-size: 12px; border-bottom: 1px solid #e5e7eb;">合計エンゲージメント時間</th>
<th style="width: 10%; text-align: right; padding: 8px; background-color: #f1f5f9; font-size: 12px; border-bottom: 1px solid #e5e7eb;">CV数</th>
</tr>
</thead>
<tbody>
<?!= pageReportTableHtml ?>
</tbody>
</table>
</div>
<footer style="text-align: center; font-size: 12px; color: #475569; margin-top: 32px;">
<p>このレポートはGoogle Apps ScriptとGemini APIによって自動生成されました。</p>
</footer>
</div>
</body>
</html>
4. スクリプトの設定変更
スクリプトコードの上部にある「— 設定項目 —」セクションを、ご自身の環境に合わせて編集します。
- GA4_PROPERTY_ID
- RECIPIENT_EMAIL
- EMAIL_SUBJECT
- CONVERSION_EVENT_NAMES (任意)
5. Gemini APIキーの安全な保存
- Google AI for DevelopersでAPIキーを取得します。
- Google Analytics Data APIを有効にしたGCPプロジェクトと同じプロジェクトを選択してAPIキーを作成するのが最もスムーズです。

APIキーをコードに直接書き込むのは危険なため、以下の手順で安全に保存します。
- スクリプトエディタの左側メニューから「プロジェクトの設定」(歯車アイコン)をクリックします。
- 「スクリプト プロパティ」セクションで、「スクリプト プロパティを追加」をクリックします。
- プロパティに「GEMINI_API_KEY」値に取得した自身のAPIキーを入力し、「スクリプト プロパティを保存」をクリックします。

6. 定期実行トリガーの設定
- スクリプトエディタの左側メニューから「トリガー」(目覚まし時計アイコン)をクリックします。
- 右下の「トリガーを追加」ボタンをクリックします。
- 以下の通り設定し、「保存」をクリックします。
- 実行する関数を選択:main
- イベントのソースを選択:時間主導型
- 時間ベースのトリガーのタイプを選択:週タイマー
- 曜日を選択/日を選択:毎週水曜日
- 時刻を選択:午前10時

7. 初回実行と承認
設定後、一度手動で実行して動作を確認することをおすすめします。
スクリプトエディタの上部で、実行する関数がmainになっていることを確認し、「実行」ボタンをクリックします。
初回実行時には、スクリプトがGoogleアナリティクスのデータやGmailの送信になどを行うための承認を求められます。
画面の指示に従って許可してください。
これで、設定したスケジュールに合わせて、AIによる分析コメント付きのインフォグラフィックレポートが自動でメール送信されるようになります。
5. AIコメントが勝手に状況を語り出すメリットと罠
Gemini Canvasには、AIがデータを読んでコメントを生成する機能があります。
たとえば
「先週比で全体トラフィックが4%減少。SEO経由は安定、広告流入に依存している傾向が見られます」
これ、営業報告でもそのまま読めるクオリティなのですが、調子の良いときは「頼れるアシスタント」に、調子が悪いときは「余計なこと言うやつ」に見えてしまうのが、ちょっとした落とし穴です。
とくに数字が落ち込んだ週など「言い訳もまだ準備できてないのに、全部言うなよ…!」という気持ちになるかもしれません。
とはいえ、感情を交えず淡々と事実を語るAIは、ある意味もっとも冷静な報告者でもありますので、このあたりは臨機応変に活用しましょう。
6. この仕組みは、中小企業の現場にちょうどいい
本格的なダッシュボードを作りこむなら、Looker StudioやTableauのようなBIツールが王道です。
でも、初期設計や権限設定の難しさ、修正時のコストを考えると、中小企業や小規模チームにはオーバースペックになりがちでしょう。
その点、今回のようなGAS+Gemini構成は
- Excel+メール運用からの脱却の第一歩としてちょうどよく
- 可視化の手間を減らしつつ、共有まで含めた運用ができる
- NotebookLMなど他ツールとの連携でさらに発展させられる余地がある
という意味で、背伸びしない自動化として非常にバランスのいい選択肢だと感じています。
少し茶化した感じの記事になってしまいましたが、先日のメール返信文や議事録のフォーマット・共有手法の統一と並んで、基礎レポートの粒度が整うということは、AIを本格的に活用するうえで不可欠な段取りなのです。
7. レポート業務は「やらない」が最強の選択肢
生成AIの導入で、「もっと良いレポートを作る」方向に力を注ぎがちですが、そもそも「作らない・送らない・説明しない」を実現できれば、それがもっとも価値のある改善かもしれません。
GA4 → GAS → Gemini Canvas → Gmail
この連携によって、人間が手を動かす前に「レポート」が届く仕組みが作れます。
手慣れた方であれば、受け皿となるGmailをSlackに変えたり、Gmail内でGeminiを動かし要約だけを読み取るといった、さらに改善されたフローにすることも可能です。
結局のところ、営業やマーケの現場では、レポートによる報告・確認よりも、結果に対して論理的な改善策を事前に打っているのかどうかが重視されます。
今回の手法の後で具体的にAIと壁打ちをすると思うのですが、その手前の工数は可能な限り削っていきましょう。