google-ads-scripts について
google-ads-scriptsは、Google Ads Scriptsを作成する人向けのJavaScriptリファレンススキルです。AdsApp、GAQL、自動化パターン、Google Sheets連携を扱い、スクリプトの作成、デバッグ、改善方法を説明します。
主な機能
- 01AdsAppのオブジェクトモデルとセレクターの解説
- 02GAQLクエリパターンの提示
- 03アカウントとMCCの自動化を網羅
- 04Google Sheetsとメール連携の例
- 05エラー処理と安全な運用方法
活用シーン
- 01予算ペース監視スクリプトの作成
- 02検索語句から除外キーワードを抽出
- 03Sheetsで品質スコアを記録
- 04スクリプトのデバッグや実行改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cognyai/ claude-code-marketing-skills --skill google-ads-scriptsインストール後、Claude Code で/google-ads-scriptsを入力して読み込まれたか確認します。
詳細
Google Ads Scripts リファレンス
Google Ads Scripts の完全なリファレンスです。Google Ads 内での JavaScript による自動化、AdsApp オブジェクトモデル、セレクターとイテレーター、GAQL クエリ、実行可能なコードを含む一般的なスクリプトパターン、Google Sheets との連携、MCC の並列処理、ベストプラクティスを解説します。
ドキュメント全文:https://cogny.com/docs/google-ads-scripts
使用方法
/google-ads-scripts # Full overview
/google-ads-scripts budget pacing # Budget pacing monitor script
/google-ads-scripts negative keywords # Search term negative keyword miner
/google-ads-scripts quality score tracker # Quality score logging to Sheets
/google-ads-scripts MCC parallel # MCC parallel processing pattern
/google-ads-scripts selectors # Selector and iterator patterns
/google-ads-scripts GAQL # GAQL queries in scripts
/google-ads-scripts anomaly detection # Anomaly alerting script
指示
あなたは Google Ads Scripts のエキスパートです。このリファレンスを使って、ユーザーが Google Ads Scripts を作成、デバッグ、最適化できるよう支援してください。適切なエラー処理とベストプラクティスを備えた、完全で実行可能なコードを提示してください。
ユーザーから質問されたら、以下の該当するセクションを見つけ、すぐに使える JavaScript コードとともに、正確で実用的な回答を提示してください。
ユーザーが引数として特定のトピックを指定した場合は、その分野に焦点を当ててください。それ以外の場合は、機能と一般的なパターンの概要を提示してください。
重要な原則:
- アカウントを変更するスクリプトには、必ず
DRY_RUNフラグを含めます - 変更を加える前に、変更内容をログに記録します
- エラー処理には
try/catchを使用します - 複雑なクエリには GAQL(
AdsApp.search())を優先します - パフォーマンス向上のため、Google Sheets への書き込みはまとめて行います
- 実行時間の上限が30分(MCCでは60分)であることをユーザーに伝えます
概要
Google Ads Scripts を使うと、JavaScript で Google Ads をプログラムから操作できます。スクリプトは Google Ads のウェブインターフェース内で直接実行されるため、外部サーバー、API キー、OAuth は不要です。
主な機能:
- キャンペーン、広告グループ、広告、キーワード、アセットを読み取り、変更する
- GAQL(Google Ads Query Language)を使用してパフォーマンスデータを取得する
- ダッシュボードやログ記録のために Google Sheets と連携する
- MailApp でメール通知を送信する
- UrlFetchApp で HTTP リクエストを行う
- スケジュールに沿って実行する(毎時、毎日、毎週、毎月)
単一アカウントのスクリプトと MCC スクリプト:
| 機能 | 単一アカウント | MCC スクリプト |
|---|---|---|
| 対象範囲 | 1つのアカウント | MCC 配下のすべてのアカウント |
| エントリーポイント | main() | AdsManagerApp を使用する main() |
| 実行時間の上限 | 30分 | 60分 |
| 並列実行 | いいえ | はい、executeInParallel() |
言語: JavaScript ES5(一部 ES6)。let、const、アロー関数、テンプレートリテラルは使用できます。async/await、import/export、class は使用できません。
AdsApp オブジェクトモデル
AdsApp オブジェクトは、単一アカウントのすべての操作のルートです。
エンティティ階層
AdsApp
+-- campaigns()
| +-- adGroups()
| | +-- ads()
| | +-- keywords()
| | +-- audiences()
| +-- extensions()
+-- adGroups() (account-level shortcut)
+-- ads() (account-level shortcut)
+-- keywords() (account-level shortcut)
+-- negativeKeywords()
+-- shoppingCampaigns()
+-- videoCampaigns()
+-- labels()
+-- budgets()
+-- biddingStrategies()
よく使うエンティティメソッド
// Status
entity.isEnabled()
entity.isPaused()
entity.isRemoved()
entity.enable()
entity.pause()
entity.remove()
// Naming
entity.getName()
entity.setName('New Name')
// Stats (requires date range)
entity.getStatsFor('LAST_30_DAYS')
entity.getStatsFor('20250101', '20250131')
// Stats object methods
var stats = entity.getStatsFor('LAST_30_DAYS');
stats.getImpressions()
stats.getClicks()
stats.getCtr()
stats.getAverageCpc()
stats.getCost()
stats.getConversions()
stats.getConversionRate()
セレクターとイテレーター
すべてのエンティティコレクションは、セレクターとイテレーターのパターンを使用します。これは、あらゆるスクリプトにおける基本的なデータアクセスパターンです。
セレクターメソッド
var keywords = AdsApp.keywords()
.withCondition('Status = ENABLED')
.withCondition('CampaignStatus = ENABLED')
.withCondition('AdGroupStatus = ENABLED')
.withCondition('Ctr < 0.01')
.forDateRange('LAST_30_DAYS')
.orderBy('Impressions DESC')
.withLimit(100)
.get();
.withCondition(condition) — エンティティを絞り込みます。演算子:=、!=、>、<、>=、<=、CONTAINS、DOES_NOT_CONTAIN、STARTS_WITH、CONTAINS_IGNORE_CASE、REGEXP_MATCH、IN []。
.withCondition('Name CONTAINS "brand"')
.withCondition('Name REGEXP_MATCH "^(buy|shop|order).*"')
.withCondition('QualityScore > 5')
.withCondition('LabelNames CONTAINS_ANY ["Priority", "Monitor"]')
.withCondition('CampaignName IN ["Search - Brand", "Search - Generic"]')
.forDateRange(dateRange) — 指標に基づく条件では必須です。事前定義された期間:TODAY、YESTERDAY、LAST_7_DAYS、THIS_WEEK_SUN_TODAY、THIS_WEEK_MON_TODAY、LAST_WEEK、LAST_14_DAYS、LAST_30_DAYS、LAST_BUSINESS_WEEK、LAST_WEEK_SUN_SAT、THIS_MONTH、LAST_MONTH、ALL_TIME。
// Custom date range
.forDateRange('20250101', '20250131')
.orderBy(orderSpec) — 結果を並べ替えます(ASC/DESC)。
.withLimit(limit) — 結果数に上限を設定します。
イテレーターパターン
var iterator = AdsApp.keywords()
.withCondition('Status = ENABLED')
.forDateRange('LAST_7_DAYS')
.withCondition('Impressions > 100')
.get();
while (iterator.hasNext()) {
var keyword = iterator.next();
var stats = keyword.getStatsFor('LAST_7_DAYS');
Logger.log(keyword.getText() + ': ' + stats.getClicks() + ' clicks');
}
スクリプト内の GAQL
Google Ads Query Language を使用すると、スクリプト内から Google Ads API 全体に SQL のような方法でアクセスできます。
AdsApp.search(query)
JavaScript オブジェクトとして表された結果行のイテレーターを返します。
function main() {
var query = "SELECT " +
"campaign.name, " +
"campaign.status, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions " +
"FROM campaign " +
"WHERE campaign.status = 'ENABLED' " +
"AND segments.date DURING LAST_30_DAYS " +
"ORDER BY metrics.cost_micros DESC";
var results = AdsApp.search(query);
while (results.hasNext()) {
var row = results.next();
Logger.log(row.campaign.name +
' | Cost: $' + (row.metrics.costMicros / 1000000).toFixed(2) +
' | Conversions: ' + row.metrics.conversions);
}
}
AdsApp.report(query)
エクスポート機能を備えたレポートオブジェクトを返します。大量のデータや Sheets との連携に最適です。
var report = AdsApp.report(
"SELECT " +
"ad_group_criterion.keyword.text, " +
"ad_group_criterion.quality_info.quality_score, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.conversions " +
"FROM keyword_view " +
"WHERE campaign.status = 'ENABLED' " +
"AND segments.date DURING LAST_30_DAYS"
);
// Export directly to Google Sheets
var sheet = SpreadsheetApp.openByUrl('YOUR_SHEET_URL').getActiveSheet();
report.exportToSheet(sheet);
Search と Report の比較
| 機能 | AdsApp.search() | AdsApp.report() |
|---|---|---|
| 戻り値の型 | 行イテレーター | レポートオブジェクト |
| フィールドへのアクセス | row.campaign.name | 行イテレーターまたは exportToSheet() |
| Sheets へのエクスポート | 手動 | 組み込みの exportToSheet() |
| 最適な用途 | 行ごとの処理 | 一括エクスポート、ダッシュボード |
よく使うスクリプトパターン
1. パフォーマンスの低いキーワードを一時停止する
function main() {
var CONFIG = {
MIN_QUALITY_SCORE: 4,
MIN_CTR: 0.005,
MIN_IMPRESSIONS: 100,
DATE_RANGE: 'LAST_30_DAYS',
DRY_RUN: true
};
var query = "SELECT " +
"ad_group_criterion.keyword.text, " +
"ad_group_criterion.keyword.match_type, " +
"ad_group_criterion.quality_info.quality_score, " +
"ad_group.name, " +
"campaign.name, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.ctr, " +
"metrics.cost_micros, " +
"metrics.conversions " +
"FROM keyword_view " +
"WHERE campaign.status = 'ENABLED' " +
"AND ad_group.status = 'ENABLED' " +
"AND ad_group_criterion.status = 'ENABLED' " +
"AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
var pauseCount = 0;
while (results.hasNext()) {
var row = results.next();
var qs = row.adGroupCriterion.qualityInfo.qualityScore;
var ctr = row.metrics.ctr;
if (qs < CONFIG.MIN_QUALITY_SCORE || ctr < CONFIG.MIN_CTR) {
Logger.log('PAUSE: "' + row.adGroupCriterion.keyword.text + '"' +
' | QS: ' + qs + ' | CTR: ' + (ctr * 100).toFixed(2) + '%' +
' | Campaign: ' + row.campaign.name);
if (!CONFIG.DRY_RUN) {
var keywords = AdsApp.keywords()
.withCondition('Text = "' + row.adGroupCriterion.keyword.text + '"')
.withCondition('AdGroupName = "' + row.adGroup.name + '"')
.withCondition('CampaignName = "' + row.campaign.name + '"')
.get();
if (keywords.hasNext()) keywords.next().pause();
}
pauseCount++;
}
}
Logger.log('Total keywords to pause: ' + pauseCount);
if (CONFIG.DRY_RUN) Logger.log('DRY RUN — no changes applied.');
}
2. 予算ペース管理モニター
function main() {
var CONFIG = {
MONTHLY_BUDGET: 10000,
ALERT_THRESHOLD_OVER: 0.15,
ALERT_THRESHOLD_UNDER: 0.20,
ALERT_EMAIL: '[email protected]',
CAMPAIGN_LABEL: 'Budget-Monitor'
};
var today = new Date();
var daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
var dayOfMonth = today.getDate();
var expectedSpend = (CONFIG.MONTHLY_BUDGET / daysInMonth) * dayOfMonth;
var firstOfMonth = Utilities.formatDate(
new Date(today.getFullYear(), today.getMonth(), 1), 'UTC', 'yyyyMMdd');
var todayStr = Utilities.formatDate(today, 'UTC', 'yyyyMMdd');
var campaigns = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.CAMPAIGN_LABEL + '"]')
.forDateRange(firstOfMonth, todayStr)
.get();
var totalSpend = 0;
var campaignDetails = [];
while (campaigns.hasNext()) {
var campaign = campaigns.next();
var stats = campaign.getStatsFor(firstOfMonth, todayStr);
var spend = stats.getCost();
totalSpend += spend;
campaignDetails.push({
name: campaign.getName(),
spend: spend,
conversions: stats.getConversions()
});
}
var paceRatio = totalSpend / expectedSpend;
var projectedSpend = (totalSpend / dayOfMonth) * daysInMonth;
var status = 'ON PACE';
if (paceRatio > (1 + CONFIG.ALERT_THRESHOLD_OVER)) status = 'OVERSPENDING';
else if (paceRatio < (1 - CONFIG.ALERT_THRESHOLD_UNDER)) status = 'UNDERSPENDING';
Logger.log('Day ' + dayOfMonth + '/' + daysInMonth +
' | Expected: $' + expectedSpend.toFixed(2) +
' | Actual: $' + totalSpend.toFixed(2) +
' | Projected: $' + projectedSpend.toFixed(2) +
' | Status: ' + status);
if (status !== 'ON PACE') {
var body = 'Budget Pacing Alert\n\nStatus: ' + status +
'\nDay ' + dayOfMonth + ' of ' + daysInMonth +
'\nMonthly budget: $' + CONFIG.MONTHLY_BUDGET.toFixed(2) +
'\nExpected spend: $' + expectedSpend.toFixed(2) +
'\nActual spend: $' + totalSpend.toFixed(2) +
'\nProjected: $' + projectedSpend.toFixed(2);
MailApp.sendEmail(CONFIG.ALERT_EMAIL, '[Google Ads] Budget Alert: ' + status, body);
}
}
3. 検索語句からの除外キーワード抽出
function main() {
var CONFIG = {
MIN_COST: 50,
MAX_CONVERSIONS: 0,
MIN_CLICKS: 10,
DATE_RANGE: 'LAST_30_DAYS',
NEGATIVE_MATCH_TYPE: 'EXACT',
DRY_RUN: true,
LOG_SHEET_URL: ''
};
var query = "SELECT " +
"search_term_view.search_term, " +
"campaign.name, " +
"ad_group.name, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions " +
"FROM search_term_view " +
"WHERE campaign.status = 'ENABLED' " +
"AND metrics.cost_micros > " + (CONFIG.MIN_COST * 1000000) + " " +
"AND metrics.clicks > " + CONFIG.MIN_CLICKS + " " +
"AND metrics.conversions <= " + CONFIG.MAX_CONVERSIONS + " " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
var negatives = [];
while (results.hasNext()) {
var row = results.next();
var term = row.searchTermView.searchTerm;
var cost = row.metrics.costMicros / 1000000;
negatives.push({ term: term, campaign: row.campaign.name, cost: cost });
Logger.log('NEGATIVE: "' + term + '" | Cost: $' + cost.toFixed(2) +
' | Clicks: ' + row.metrics.clicks + ' | Campaign: ' + row.campaign.name);
if (!CONFIG.DRY_RUN) {
var campaigns = AdsApp.campaigns()
.withCondition('Name = "' + row.campaign.name + '"').get();
if (campaigns.hasNext()) {
var campaign = campaigns.next();
if (CONFIG.NEGATIVE_MATCH_TYPE === 'EXACT') {
campaign.createNegativeKeyword('[' + term + ']');
} else if (CONFIG.NEGATIVE_MATCH_TYPE === 'PHRASE') {
campaign.createNegativeKeyword('"' + term + '"');
} else {
campaign.createNegativeKeyword(term);
}
}
}
}
Logger.log('Total negative keyword candidates: ' + negatives.length);
}
4. 無効な URL のチェック
function main() {
var CONFIG = {
ALERT_EMAIL: '[email protected]',
PAUSE_BROKEN: false,
MIN_IMPRESSIONS: 10,
DATE_RANGE: 'LAST_7_DAYS'
};
var brokenUrls = [];
var ads = AdsApp.ads()
.withCondition('Status = ENABLED')
.withCondition('CampaignStatus = ENABLED')
.withCondition('AdGroupStatus = ENABLED')
.forDateRange(CONFIG.DATE_RANGE)
.withCondition('Impressions > ' + CONFIG.MIN_IMPRESSIONS)
.get();
while (ads.hasNext()) {
var ad = ads.next();
var url = ad.urls().getFinalUrl();
if (url) {
var status = checkUrl(url);
if (status !== 200) {
brokenUrls.push({
campaign: ad.getCampaign().getName(),
adGroup: ad.getAdGroup().getName(),
url: url,
httpStatus: status
});
Logger.log('BROKEN [' + status + ']: ' + url);
if (CONFIG.PAUSE_BROKEN) ad.pause();
}
}
}
if (brokenUrls.length > 0 && CONFIG.ALERT_EMAIL) {
var body = 'Broken URL Report\n\n';
for (var i = 0; i < brokenUrls.length; i++) {
body += 'HTTP ' + brokenUrls[i].httpStatus + ' | ' +
brokenUrls[i].campaign + ' | ' + brokenUrls[i].url + '\n';
}
MailApp.sendEmail(CONFIG.ALERT_EMAIL,
'[Google Ads] ' + brokenUrls.length + ' Broken URLs', body);
}
}
function checkUrl(url) {
try {
var response = UrlFetchApp.fetch(url, {
muteHttpExceptions: true,
followRedirects: true,
validateHttpsCertificates: false
});
return response.getResponseCode();
} catch (e) {
return 0;
}
}
5. 時間帯別の入札単価調整(デイパーティング)
function main() {
var CONFIG = {
DATE_RANGE: 'LAST_30_DAYS',
MAX_BID_MODIFIER: 0.30,
MIN_BID_MODIFIER: -0.50,
MIN_CONVERSIONS: 5
};
var query = "SELECT " +
"campaign.name, " +
"segments.hour, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.conversions, " +
"metrics.cost_micros " +
"FROM campaign " +
"WHERE campaign.status = 'ENABLED' " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
var hourlyData = {};
while (results.hasNext()) {
var row = results.next();
var name = row.campaign.name;
var hour = row.segments.hour;
if (!hourlyData[name]) hourlyData[name] = {};
if (!hourlyData[name][hour]) {
hourlyData[name][hour] = { clicks: 0, conversions: 0, cost: 0 };
}
hourlyData[name][hour].clicks += row.metrics.clicks;
hourlyData[name][hour].conversions += row.metrics.conversions;
hourlyData[name][hour].cost += row.metrics.costMicros / 1000000;
}
for (var campaignName in hourlyData) {
var hours = hourlyData[campaignName];
var totalConv = 0, totalClicks = 0;
for (var h in hours) {
totalConv += hours[h].conversions;
totalClicks += hours[h].clicks;
}
var avgCvr = totalClicks > 0 ? totalConv / totalClicks : 0;
Logger.log('\n' + campaignName + ' | Avg CVR: ' + (avgCvr * 100).toFixed(2) + '%');
for (var hour = 0; hour < 24; hour++) {
var data = hours[hour] || { clicks: 0, conversions: 0 };
var hourCvr = data.clicks > 0 ? data.conversions / data.clicks : 0;
var modifier = 0;
if (avgCvr > 0 && data.conversions >= CONFIG.MIN_CONVERSIONS) {
modifier = (hourCvr / avgCvr) - 1;
modifier = Math.max(CONFIG.MIN_BID_MODIFIER, Math.min(CONFIG.MAX_BID_MODIFIER, modifier));
}
Logger.log(' Hour ' + hour + ' | Conv: ' + data.conversions +
' | Modifier: ' + (modifier > 0 ? '+' : '') + (modifier * 100).toFixed(0) + '%');
}
}
}
6. 品質スコアの追跡
品質スコアの履歴を Google Sheets に記録します(Google Ads では品質スコアの履歴が標準で保持されません)。
function main() {
var CONFIG = {
SHEET_URL: 'YOUR_GOOGLE_SHEET_URL',
SHEET_NAME: 'QS History',
MIN_IMPRESSIONS: 50
};
var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL)
.getSheetByName(CONFIG.SHEET_NAME);
if (sheet.getLastRow() === 0) {
sheet.appendRow([
'Date', 'Campaign', 'Ad Group', 'Keyword', 'Match Type',
'Quality Score', 'Expected CTR', 'Ad Relevance', 'Landing Page Exp',
'Impressions', 'Clicks', 'Cost', 'Conversions'
]);
}
var today = Utilities.formatDate(new Date(), 'UTC', 'yyyy-MM-dd');
var query = "SELECT " +
"campaign.name, ad_group.name, " +
"ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, " +
"ad_group_criterion.quality_info.quality_score, " +
"ad_group_criterion.quality_info.search_predicted_ctr, " +
"ad_group_criterion.quality_info.creative_quality_score, " +
"ad_group_criterion.quality_info.post_click_quality_score, " +
"metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions " +
"FROM keyword_view " +
"WHERE campaign.status = 'ENABLED' " +
"AND ad_group.status = 'ENABLED' " +
"AND ad_group_criterion.status = 'ENABLED' " +
"AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
"AND segments.date DURING LAST_7_DAYS";
var results = AdsApp.search(query);
var rows = [];
while (results.hasNext()) {
var row = results.next();
var qi = row.adGroupCriterion.qualityInfo;
rows.push([
today, row.campaign.name, row.adGroup.name,
row.adGroupCriterion.keyword.text, row.adGroupCriterion.keyword.matchType,
qi.qualityScore || '', qi.searchPredictedCtr || '',
qi.creativeQualityScore || '', qi.postClickQualityScore || '',
row.metrics.impressions, row.metrics.clicks,
(row.metrics.costMicros / 1000000).toFixed(2), row.metrics.conversions
]);
}
if (rows.length > 0) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
}
Logger.log('Logged ' + rows.length + ' keywords');
}
7. 広告文のテスト(RSA のパフォーマンス)
function main() {
var CONFIG = {
MIN_IMPRESSIONS: 500,
DATE_RANGE: 'LAST_30_DAYS',
WINNER_CTR_LIFT: 0.20,
LOSER_CTR_DROP: -0.20,
ALERT_EMAIL: '[email protected]'
};
var query = "SELECT " +
"campaign.name, ad_group.name, ad_group_ad.ad.id, " +
"ad_group_ad.ad_strength, " +
"metrics.impressions, metrics.clicks, metrics.ctr, " +
"metrics.conversions, metrics.cost_micros " +
"FROM ad_group_ad " +
"WHERE campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' " +
"AND ad_group_ad.status = 'ENABLED' " +
"AND ad_group_ad.ad.type = 'RESPONSIVE_SEARCH_AD' " +
"AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
var results = AdsApp.search(query);
var adGroups = {};
while (results.hasNext()) {
var row = results.next();
var agKey = row.campaign.name + ' > ' + row.adGroup.name;
if (!adGroups[agKey]) adGroups[agKey] = [];
adGroups[agKey].push({
adId: row.adGroupAd.ad.id,
strength: row.adGroupAd.adStrength,
impressions: row.metrics.impressions,
clicks: row.metrics.clicks,
ctr: row.metrics.ctr,
conversions: row.metrics.conversions
});
}
var winners = [], losers = [];
for (var agKey in adGroups) {
var ads = adGroups[agKey];
if (ads.length < 2) continue;
var totalClicks = 0, totalImpr = 0;
for (var i = 0; i < ads.length; i++) {
totalClicks += ads[i].clicks;
totalImpr += ads[i].impressions;
}
var avgCtr = totalImpr > 0 ? totalClicks / totalImpr : 0;
for (var j = 0; j < ads.length; j++) {
var ctrLift = avgCtr > 0 ? (ads[j].ctr - avgCtr) / avgCtr : 0;
if (ctrLift >= CONFIG.WINNER_CTR_LIFT) winners.push({ adGroup: agKey, ad: ads[j], lift: ctrLift });
else if (ctrLift <= CONFIG.LOSER_CTR_DROP) losers.push({ adGroup: agKey, ad: ads[j], lift: ctrLift });
}
}
Logger.log('Winners: ' + winners.length + ' | Losers: ' + losers.length);
}
8. ROAS に基づく予算配分
function main() {
var CONFIG = {
TOTAL_DAILY_BUDGET: 500,
MIN_BUDGET: 20,
MAX_BUDGET: 200,
DATE_RANGE: 'LAST_14_DAYS',
CAMPAIGN_LABEL: 'Auto-Budget',
MIN_CONVERSIONS: 3,
DRY_RUN: true
};
var campaigns = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.CAMPAIGN_LABEL + '"]')
.forDateRange(CONFIG.DATE_RANGE)
.get();
var campaignData = [];
while (campaigns.hasNext()) {
var campaign = campaigns.next();
var stats = campaign.getStatsFor(CONFIG.DATE_RANGE);
var cost = stats.getCost();
var convValue = stats.getConversionValue();
campaignData.push({
campaign: campaign,
name: campaign.getName(),
cost: cost,
roas: cost > 0 ? convValue / cost : 0,
conversions: stats.getConversions(),
currentBudget: campaign.getBudget().getAmount()
});
}
campaignData.sort(function(a, b) { return b.roas - a.roas; });
var totalRoas = 0;
for (var i = 0; i < campaignData.length; i++) {
if (campaignData[i].conversions >= CONFIG.MIN_CONVERSIONS) totalRoas += campaignData[i].roas;
}
for (var j = 0; j < campaignData.length; j++) {
var c = campaignData[j];
var newBudget = (c.conversions >= CONFIG.MIN_CONVERSIONS && totalRoas > 0)
? (c.roas / totalRoas) * CONFIG.TOTAL_DAILY_BUDGET
: CONFIG.MIN_BUDGET;
newBudget = Math.max(CONFIG.MIN_BUDGET, Math.min(CONFIG.MAX_BUDGET, Math.round(newBudget * 100) / 100));
Logger.log(c.name + ' | ROAS: ' + c.roas.toFixed(2) +
' | Current: $' + c.currentBudget.toFixed(2) + ' | New: $' + newBudget.toFixed(2));
if (!CONFIG.DRY_RUN) c.campaign.getBudget().setAmount(newBudget);
}
if (CONFIG.DRY_RUN) Logger.log('DRY RUN — no budgets changed.');
}
9. 異常検知
function main() {
var CONFIG = {
LOOKBACK_DAYS: 14,
SPIKE_THRESHOLD: 2.0,
DROP_THRESHOLD: 0.5,
ALERT_EMAIL: '[email protected]',
METRICS: ['impressions', 'clicks', 'cost_micros', 'conversions']
};
var today = new Date();
var yesterday = new Date(today.getTime() - 86400000);
var lookbackStart = new Date(today.getTime() - (CONFIG.LOOKBACK_DAYS + 1) * 86400000);
var yesterdayStr = Utilities.formatDate(yesterday, 'UTC', 'yyyyMMdd');
var lookbackStr = Utilities.formatDate(lookbackStart, 'UTC', 'yyyyMMdd');
var query = "SELECT campaign.name, " +
"metrics.impressions, metrics.clicks, metrics.cost_micros, " +
"metrics.conversions, segments.date " +
"FROM campaign WHERE campaign.status = 'ENABLED' " +
"AND segments.date BETWEEN '" +
lookbackStr.substring(0, 4) + '-' + lookbackStr.substring(4, 6) + '-' + lookbackStr.substring(6, 8) +
"' AND '" +
yesterdayStr.substring(0, 4) + '-' + yesterdayStr.substring(4, 6) + '-' + yesterdayStr.substring(6, 8) + "'";
var results = AdsApp.search(query);
var campaignDays = {};
while (results.hasNext()) {
var row = results.next();
var name = row.campaign.name;
var date = row.segments.date;
if (!campaignDays[name]) campaignDays[name] = {};
campaignDays[name][date] = {
impressions: row.metrics.impressions,
clicks: row.metrics.clicks,
cost_micros: row.metrics.costMicros,
conversions: row.metrics.conversions
};
}
var alerts = [];
var yesterdayDate = Utilities.formatDate(yesterday, 'UTC', 'yyyy-MM-dd');
for (var campaignName in campaignDays) {
var days = campaignDays[campaignName];
var yesterdayData = days[yesterdayDate];
if (!yesterdayData) continue;
for (var m = 0; m < CONFIG.METRICS.length; m++) {
var metric = CONFIG.METRICS[m];
var sum = 0, count = 0;
for (var d in days) {
if (d !== yesterdayDate) { sum += days[d][metric] || 0; count++; }
}
if (count === 0) continue;
var avg = sum / count;
var current = yesterdayData[metric] || 0;
if (avg > 0) {
var ratio = current / avg;
if (ratio >= CONFIG.SPIKE_THRESHOLD || ratio <= CONFIG.DROP_THRESHOLD) {
alerts.push({
campaign: campaignName, metric: metric,
type: ratio >= CONFIG.SPIKE_THRESHOLD ? 'SPIKE' : 'DROP',
current: current, average: avg, ratio: ratio
});
}
}
}
}
if (alerts.length > 0) {
var body = 'Anomaly Report — ' + yesterdayDate + '\n\n';
for (var a = 0; a < alerts.length; a++) {
body += alerts[a].type + ': ' + alerts[a].campaign +
' | ' + alerts[a].metric + ' | ' + alerts[a].ratio.toFixed(2) + 'x avg\n';
}
MailApp.sendEmail(CONFIG.ALERT_EMAIL, '[Google Ads] ' + alerts.length + ' Anomalies', body);
}
}
10. ランディングページのパフォーマンス
function main() {
var CONFIG = {
DATE_RANGE: 'LAST_30_DAYS',
MIN_CLICKS: 50,
SHEET_URL: 'YOUR_GOOGLE_SHEET_URL',
SHEET_NAME: 'Landing Pages'
};
var query = "SELECT " +
"landing_page_view.unexpanded_final_url, " +
"metrics.clicks, metrics.impressions, " +
"metrics.cost_micros, metrics.conversions, " +
"metrics.conversions_value " +
"FROM landing_page_view " +
"WHERE segments.date DURING " + CONFIG.DATE_RANGE + " " +
"AND metrics.clicks > " + CONFIG.MIN_CLICKS + " " +
"ORDER BY metrics.clicks DESC";
var results = AdsApp.search(query);
var pages = [];
while (results.hasNext()) {
var row = results.next();
var cost = row.metrics.costMicros / 1000000;
var cvr = row.metrics.clicks > 0 ? row.metrics.conversions / row.metrics.clicks : 0;
var roas = cost > 0 ? row.metrics.conversionsValue / cost : 0;
pages.push([
row.landingPageView.unexpandedFinalUrl,
row.metrics.clicks, row.metrics.impressions,
cost.toFixed(2), row.metrics.conversions,
row.metrics.conversionsValue.toFixed(2),
(cvr * 100).toFixed(2) + '%', roas.toFixed(2)
]);
}
if (CONFIG.SHEET_URL && pages.length > 0) {
var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL)
.getSheetByName(CONFIG.SHEET_NAME);
sheet.clear();
sheet.appendRow(['URL', 'Clicks', 'Impressions', 'Cost', 'Conversions',
'Conv Value', 'CVR', 'ROAS']);
sheet.getRange(2, 1, pages.length, pages[0].length).setValues(pages);
Logger.log('Exported ' + pages.length + ' landing pages');
}
}
Google Sheets との連携
スプレッドシートを開く
var ss = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/SHEET_ID/edit');
var ss = SpreadsheetApp.openById('SHEET_ID');
var sheet = ss.getSheetByName('Data');
データの読み取り
var values = sheet.getRange('A1:D10').getValues(); // 2D array
var allData = sheet.getDataRange().getValues();
var value = sheet.getRange('B2').getValue(); // Single cell
データの書き込み
sheet.getRange('A1').setValue('Campaign Name');
sheet.appendRow(['2025-02-11', 'Brand', 150, 12]);
// Batch write (much faster than appendRow in a loop)
var data = [['Campaign', 'Clicks'], ['Brand', 150], ['Generic', 300]];
sheet.getRange(1, 1, data.length, data[0].length).setValues(data);
メールと通知
// Simple email
MailApp.sendEmail('[email protected]', 'Subject', 'Body text');
// HTML email
MailApp.sendEmail({
to: '[email protected]',
subject: '[Google Ads] Weekly Report',
htmlBody: '<h1>Report</h1><p>Details here.</p>'
});
スケジュール設定
ツールと設定 > 一括操作 > スクリプトで設定します。利用可能な頻度は、毎時、毎日、毎週、毎月です。
推奨スケジュール:
- 予算ペース管理:毎日または毎時
- 除外キーワードの抽出:毎週
- 品質スコアの追跡:毎日
- 異常検知:毎日(朝)
- 無効な URL のチェック:毎週
- ダッシュボード:毎日
エラー処理
function main() {
try {
processKeywords();
} catch (e) {
Logger.log('ERROR: ' + e.message + '\nStack: ' + e.stack);
MailApp.sendEmail('[email protected]',
'[Google Ads Script] Error', 'Error: ' + e.message);
}
}
実行時間の制限: 30 分(単一アカウント)、60 分(MCC)。適切に処理します。
var startTime = new Date().getTime();
var MAX_RUNTIME = 25 * 60 * 1000; // 25 min buffer
while (iterator.hasNext()) {
if (new Date().getTime() - startTime > MAX_RUNTIME) {
Logger.log('Approaching time limit. Stopping.');
break;
}
// Process...
}
MCC スクリプト
基本的な MCC スクリプト
function main() {
var accounts = AdsManagerApp.accounts()
.withCondition('Impressions > 0')
.forDateRange('LAST_7_DAYS')
.withLimit(50)
.get();
while (accounts.hasNext()) {
var account = accounts.next();
AdsManagerApp.select(account);
Logger.log('Processing: ' + account.getName());
// Now AdsApp refers to this account
}
}
executeInParallel() による並列処理
function main() {
var accounts = AdsManagerApp.accounts()
.withCondition('Impressions > 0')
.forDateRange('LAST_7_DAYS')
.get();
accounts.executeInParallel('processAccount', 'aggregateResults');
}
function processAccount() {
var account = AdsApp.currentAccount();
var result = { name: account.getName(), id: account.getCustomerId(), campaigns: [] };
var campaigns = AdsApp.campaigns()
.withCondition('Status = ENABLED')
.forDateRange('LAST_7_DAYS').get();
while (campaigns.hasNext()) {
var c = campaigns.next();
var s = c.getStatsFor('LAST_7_DAYS');
result.campaigns.push({
name: c.getName(), clicks: s.getClicks(),
cost: s.getCost(), conversions: s.getConversions()
});
}
return JSON.stringify(result); // Must return string
}
function aggregateResults(results) {
for (var i = 0; i < results.length; i++) {
if (results[i].getStatus() === 'OK') {
var data = JSON.parse(results[i].getReturnValue());
Logger.log(data.name + ': ' + data.campaigns.length + ' campaigns');
} else {
Logger.log('Error: ' + results[i].getError());
}
}
}
executeInParallel() のルール:
- アカウントごとの関数は文字列(
JSON.stringify())を返す必要があります - コールバックは
ExecutionResultオブジェクト(.getStatus()、.getReturnValue()、.getError()、.getCustomerId())の配列を受け取ります - 1 回の呼び出しにつき最大 50 アカウント
- 各アカウントの実行時間は最大 30 分
ベストプラクティス
- まずプレビューする — 実行前に必ず「プレビュー」をクリックします
- 変更前にログを記録する —
keyword.pause()の前にLogger.log('Pausing: ' + keyword.getText())を実行します - DRY_RUN フラグ — 変更を行うスクリプトには、すべて設定に
DRY_RUN: trueを含めます - 段階的に展開する — まず 1 つのキャンペーンで開始し、検証後に範囲を広げます
- エラー通知 — 失敗時にはメールを送信し、ログの確認だけに頼らないようにします
- ラベルを使用する — キャンペーン名をハードコードせず、ラベルを使ってスクリプトの対象を指定します
- Sheets への書き込みをまとめる — ループ内で
appendRow()を使わず、2 次元配列でsetValues()を使用します - スクリプトを文書化する — 目的、スケジュール、作成者、設定の説明を含むコメントブロックを追加します
制限事項
| 制限事項 | 詳細 |
|---|---|
| 実行時間 | 30 分(単一アカウント)、60 分(MCC) |
| async/await は非対応 | 同期実行のみ |
| ES6 のサポートは限定的 | import/export および class は使用できません |
| ディスプレイ/動画 API は非対応 | スクリプト経由のサポートは限定的 |
| Performance Max は非対応 | 読み取り専用(レポート機能は限定的) |
| URL フェッチ | 応答の上限は 50 MB、タイムアウトは 60 秒 |
| MailApp | 1 日あたり 100 件の宛先 |
| npm/モジュールは非対応 | 外部ライブラリをインポートできません |
| executeInParallel() | 1 回の呼び出しにつき最大 50 アカウント |
リソース
- Google Ads Scripts のドキュメント: https://developers.google.com/google-ads/scripts/docs/overview
- AdsApp リファレンス: https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp
- GAQL リファレンス: https://developers.google.com/google-ads/api/docs/query/overview
- スクリプトの例: https://developers.google.com/google-ads/scripts/docs/solutions
- Cogny の全ドキュメント: https://cogny.com/docs/google-ads-scripts