/**
* Global CPC cap script (Google Ads Scripts)
*
* What this script does:
* 1) Keyword layer: normalize keyword max CPC to TARGET_CPC
* 2) Ad group layer: normalize ad group max CPC to TARGET_CPC
* 3) Campaign layer: monitor only. Report campaigns whose realized avg CPC
* is still above CAP_THRESHOLD in the chosen date range.
*
* Important:
* - In Google Ads Scripts, campaign avg CPC is a performance metric, not a
* generic "campaign max CPC" knob that can be safely normalized the same way
* as keyword / ad group bids in this account structure.
* - This script therefore writes on keyword + ad group, and reports on campaign.
*/
const CONFIG = {
TARGET_CPC: 0.09,
CAP_THRESHOLD: 0.13,
DRY_RUN: false,
ONLY_ENABLED: false,
REQUIRE_CLICKS: false,
CLICK_DATE_RANGE: 'LAST_30_DAYS',
RUN_KEYWORD_CAP: true,
RUN_ADGROUP_CAP: true,
RUN_CAMPAIGN_MONITOR: true,
KEYWORD_SELECTOR_LIMIT: 200000,
ADGROUP_SELECTOR_LIMIT: 200000,
CAMPAIGN_SELECTOR_LIMIT: 50000,
KEYWORD_MAX_UPDATES: 20000,
ADGROUP_MAX_UPDATES: 20000,
CAMPAIGN_MONITOR_DATE_RANGE: 'LAST_7_DAYS',
CAMPAIGN_MONITOR_TOP_N: 100,
LOG_SAMPLE_LIMIT: 50,
LOG_ERROR_LIMIT: 100,
};
function shouldNormalizeBid(oldCpc) {
if (oldCpc === null || oldCpc === undefined || isNaN(oldCpc)) return true;
return Math.abs(Number(oldCpc) - CONFIG.TARGET_CPC) > 1e-9;
}
function isEnabledEntity(entity) {
return !CONFIG.ONLY_ENABLED || entity.isEnabled();
}
function isManualCpcCampaign(campaign) {
const strategy = String(
campaign && campaign.getBiddingStrategyType
? campaign.getBiddingStrategyType()
: ''
);
return !strategy || strategy === 'MANUAL_CPC';
}
function safeMessage(error) {
if (!error) return 'Unknown error';
if (error.message) return String(error.message);
return String(error);
}
function pushSample(samples, line) {
if (samples.length < CONFIG.LOG_SAMPLE_LIMIT) {
samples.push(line);
}
}
function pushError(samples, line) {
if (samples.length < CONFIG.LOG_ERROR_LIMIT) {
samples.push(line);
}
}
function formatCpc(value) {
if (value === null || value === undefined || isNaN(value)) return 'null';
return Number(value).toFixed(3);
}
function logLines(title, lines) {
if (!lines.length) return;
Logger.log(title);
for (var i = 0; i < lines.length; i++) {
Logger.log(lines[i]);
}
}
function runKeywordCap() {
const stats = {
layer: 'keyword',
scanned: 0,
wouldUpdate: 0,
updated: 0,
skippedDisabled: 0,
skippedNonManualStrategy: 0,
skippedNoBid: 0,
skippedNoRuleMatch: 0,
errors: 0,
};
const samples = [];
const errors = [];
let selector = AdsApp.keywords();
if (CONFIG.REQUIRE_CLICKS) {
selector = selector
.forDateRange(CONFIG.CLICK_DATE_RANGE)
.withCondition('metrics.clicks > 0');
}
const it = selector.withLimit(CONFIG.KEYWORD_SELECTOR_LIMIT).get();
while (it.hasNext()) {
const keyword = it.next();
stats.scanned++;
const campaign = keyword.getCampaign();
const adGroup = keyword.getAdGroup();
if (!isEnabledEntity(campaign) || !isEnabledEntity(adGroup) || !isEnabledEntity(keyword)) {
stats.skippedDisabled++;
continue;
}
if (!isManualCpcCampaign(campaign)) {
stats.skippedNonManualStrategy++;
continue;
}
const oldCpc = keyword.bidding().getCpc();
if (!shouldNormalizeBid(oldCpc)) {
stats.skippedNoRuleMatch++;
continue;
}
stats.wouldUpdate++;
const descriptor =
campaign.getName() +
' | ' +
adGroup.getName() +
' | ' +
keyword.getText() +
' | ' +
formatCpc(oldCpc) +
' -> ' +
formatCpc(CONFIG.TARGET_CPC);
pushSample(samples, descriptor);
if (CONFIG.DRY_RUN) continue;
if (stats.updated >= CONFIG.KEYWORD_MAX_UPDATES) {
Logger.log('Keyword layer reached KEYWORD_MAX_UPDATES, stop this run.');
break;
}
try {
keyword.bidding().setCpc(CONFIG.TARGET_CPC);
stats.updated++;
} catch (e) {
stats.errors++;
pushError(errors, descriptor + ' | ERROR: ' + safeMessage(e));
}
}
Logger.log('=== Keyword cap summary ===');
Logger.log(JSON.stringify(stats));
logLines('--- keyword sample updates ---', samples);
logLines('--- keyword errors ---', errors);
return stats;
}
function runAdGroupCap() {
const stats = {
layer: 'adgroup',
scanned: 0,
wouldUpdate: 0,
updated: 0,
skippedDisabled: 0,
skippedNonManualStrategy: 0,
skippedNoBid: 0,
skippedNoRuleMatch: 0,
errors: 0,
};
const samples = [];
const errors = [];
let selector = AdsApp.adGroups();
if (CONFIG.REQUIRE_CLICKS) {
selector = selector
.forDateRange(CONFIG.CLICK_DATE_RANGE)
.withCondition('metrics.clicks > 0');
}
const it = selector.withLimit(CONFIG.ADGROUP_SELECTOR_LIMIT).get();
while (it.hasNext()) {
const adGroup = it.next();
stats.scanned++;
const campaign = adGroup.getCampaign();
if (!isEnabledEntity(campaign) || !isEnabledEntity(adGroup)) {
stats.skippedDisabled++;
continue;
}
if (!isManualCpcCampaign(campaign)) {
stats.skippedNonManualStrategy++;
continue;
}
const oldCpc = adGroup.bidding().getCpc();
if (!shouldNormalizeBid(oldCpc)) {
stats.skippedNoRuleMatch++;
continue;
}
stats.wouldUpdate++;
const descriptor =
campaign.getName() +
' | ' +
adGroup.getName() +
' | ' +
formatCpc(oldCpc) +
' -> ' +
formatCpc(CONFIG.TARGET_CPC);
pushSample(samples, descriptor);
if (CONFIG.DRY_RUN) continue;
if (stats.updated >= CONFIG.ADGROUP_MAX_UPDATES) {
Logger.log('Ad group layer reached ADGROUP_MAX_UPDATES, stop this run.');
break;
}
try {
adGroup.bidding().setCpc(CONFIG.TARGET_CPC);
stats.updated++;
} catch (e) {
stats.errors++;
pushError(errors, descriptor + ' | ERROR: ' + safeMessage(e));
}
}
Logger.log('=== Ad group cap summary ===');
Logger.log(JSON.stringify(stats));
logLines('--- ad group sample updates ---', samples);
logLines('--- ad group errors ---', errors);
return stats;
}
function runCampaignMonitor() {
const stats = {
layer: 'campaign-monitor',
scanned: 0,
withClicks: 0,
aboveCap: 0,
};
const hotCampaigns = [];
const it = AdsApp.campaigns()
.withLimit(CONFIG.CAMPAIGN_SELECTOR_LIMIT)
.get();
while (it.hasNext()) {
const campaign = it.next();
stats.scanned++;
if (!isEnabledEntity(campaign)) {
continue;
}
const perf = campaign.getStatsFor(CONFIG.CAMPAIGN_MONITOR_DATE_RANGE);
const clicks = perf.getClicks();
if (!clicks) {
continue;
}
stats.withClicks++;
const avgCpc = perf.getAverageCpc();
if (!avgCpc || avgCpc <= CONFIG.CAP_THRESHOLD + 1e-9) {
continue;
}
stats.aboveCap++;
hotCampaigns.push({
name: campaign.getName(),
clicks: clicks,
avgCpc: avgCpc,
cost: perf.getCost(),
conversions: perf.getConversions(),
strategy: String(
campaign.getBiddingStrategyType ? campaign.getBiddingStrategyType() : ''
),
});
}
hotCampaigns.sort(function (a, b) {
if (b.cost !== a.cost) return b.cost - a.cost;
return b.avgCpc - a.avgCpc;
});
Logger.log('=== Campaign monitor summary ===');
Logger.log(
JSON.stringify({
stats: stats,
dateRange: CONFIG.CAMPAIGN_MONITOR_DATE_RANGE,
capThreshold: CONFIG.CAP_THRESHOLD,
})
);
const limit = Math.min(CONFIG.CAMPAIGN_MONITOR_TOP_N, hotCampaigns.length);
for (var i = 0; i < limit; i++) {
const item = hotCampaigns[i];
Logger.log(
item.name +
' | clicks=' +
item.clicks +
' | avgCpc=' +
formatCpc(item.avgCpc) +
' | cost=' +
formatCpc(item.cost) +
' | conv=' +
item.conversions +
' | strategy=' +
item.strategy
);
}
return stats;
}
function main() {
Logger.log('=== Global CPC cap run start ===');
Logger.log(
JSON.stringify({
mode: 'normalize-to-target-cpc',
targetCpc: CONFIG.TARGET_CPC,
capThreshold: CONFIG.CAP_THRESHOLD,
dryRun: CONFIG.DRY_RUN,
onlyEnabled: CONFIG.ONLY_ENABLED,
requireClicks: CONFIG.REQUIRE_CLICKS,
clickDateRange: CONFIG.CLICK_DATE_RANGE,
runKeywordCap: CONFIG.RUN_KEYWORD_CAP,
runAdGroupCap: CONFIG.RUN_ADGROUP_CAP,
runCampaignMonitor: CONFIG.RUN_CAMPAIGN_MONITOR,
campaignMonitorDateRange: CONFIG.CAMPAIGN_MONITOR_DATE_RANGE,
})
);
if (CONFIG.RUN_KEYWORD_CAP) {
runKeywordCap();
}
if (CONFIG.RUN_ADGROUP_CAP) {
runAdGroupCap();
}
if (CONFIG.RUN_CAMPAIGN_MONITOR) {
runCampaignMonitor();
}
Logger.log('=== Global CPC cap run end ===');
}
本文档为站内渲染。原始文件本地路径:saas/source/knowledge-world/Knowledge-World-项目-Practice-Google-Ads-ads-data-script-cpc-edc921.txt(仅本地保留,不入库不部署)