Skip to content

Instantly share code, notes, and snippets.

@PKBibi
Last active June 9, 2026 07:06
Show Gist options
  • Select an option

  • Save PKBibi/4503042b0ddef2c1bd036a66fab554b1 to your computer and use it in GitHub Desktop.

Select an option

Save PKBibi/4503042b0ddef2c1bd036a66fab554b1 to your computer and use it in GitHub Desktop.
TLD Auto-Exclusion Script - Google Ads Script | PPC Chief. For the background, read the blog post here: https://ppcchief.com/blog/gdn-exclusion-list
// TLD Auto-Excluder (fixed)
// Use safe builders for exclusions to avoid runtime "not a function" errors
var CONFIG = {
LOOKBACK_DAYS: 30,
ZERO_CONV_SPEND_THRESHOLD: 10,
MIN_CLICKS_THRESHOLD: 5,
ACCOUNT_LABEL: "",
AUDIT_SHEET_URL: "",
AUDIT_SHEET_NAME: "TLD Exclusion Log",
DRY_RUN: false,
ALERT_EMAIL: "",
};
var BAD_TLDS = [
".xyz",
".buzz",
".one",
".cloud",
".top",
".click",
".link",
".gdn",
".bid",
".win",
".download",
".stream",
".racing",
".date",
".faith",
".review",
".science",
".party",
".trade",
".accountant",
".cricket",
".loan",
".men",
".work",
".icu",
".fun",
".site",
".online",
".space",
".website",
".host",
".press",
".pw",
".tk",
".ml",
".ga",
".cf",
".gq",
".cam",
".rest",
".fit",
".kim",
".guru",
".ninja",
".rocks",
".live",
".world",
".today",
".zone",
];
var BAD_KEYWORDS = [
"torrent",
"pirate",
"crack",
"hack",
"cheat",
"keygen",
"warez",
"nulled",
"cracked",
"freevpn",
"freeproxy",
"freedownload",
"freestreaming",
"wallpaper",
"ringtone",
"screensaver",
"emoji",
"quiz",
"personality-test",
"lyrics",
"proxy",
"converter",
"convertfree",
"onlineconvert",
"flashgame",
"browsergame",
"playgame",
"freegame",
"earnmoney",
"makemoney",
"getpaid",
"cashback",
"rewardpoint",
"clickbait",
"viralstory",
"shocking",
"unbelievable",
"adultfriend",
"hookup",
"datenight",
"singlemeet",
"casinoonline",
"slotmachine",
"betfree",
"gamblingfree",
"spyware",
"malware",
"cleanpc",
"boostpc",
"fixpc",
"fakeid",
"buyfollower",
"buylikes",
];
function main() {
var selector = AdsManagerApp.accounts();
if (CONFIG.ACCOUNT_LABEL)
selector = selector.withCondition(
'LabelNames CONTAINS "' + CONFIG.ACCOUNT_LABEL + '"',
);
var childConfig = JSON.stringify({
lookbackDays: CONFIG.LOOKBACK_DAYS,
zeroConvSpendThreshold: CONFIG.ZERO_CONV_SPEND_THRESHOLD,
minClicksThreshold: CONFIG.MIN_CLICKS_THRESHOLD,
auditSheetUrl: CONFIG.AUDIT_SHEET_URL,
auditSheetName: CONFIG.AUDIT_SHEET_NAME,
dryRun: CONFIG.DRY_RUN,
badTlds: BAD_TLDS,
badKeywords: BAD_KEYWORDS,
});
selector.executeInParallel(
"processChildAccount",
"onAllComplete",
childConfig,
);
}
function processChildAccount(serializedConfig) {
var cfg = JSON.parse(serializedConfig);
var account = AdsApp.currentAccount();
var accountName = account.getName();
var accountId = account.getCustomerId();
var result = {
accountName: accountName,
accountId: accountId,
scanned: 0,
byTld: 0,
byKeyword: 0,
byPerformance: 0,
applied: 0,
alreadyExcluded: 0,
errors: [],
topExclusions: [],
};
var today = new Date();
var lookbackDate = new Date(
today.getTime() - cfg.lookbackDays * 24 * 60 * 60 * 1000,
);
var dateFrom = fmtDate(lookbackDate);
var dateTo = fmtDate(today);
var query =
'SELECT detail_placement_view.display_name, detail_placement_view.target_url, detail_placement_view.placement_type, campaign.name, campaign.id, campaign.advertising_channel_type, metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.conversions FROM detail_placement_view WHERE segments.date BETWEEN "' +
dateFrom +
'" AND "' +
dateTo +
'" AND campaign.advertising_channel_type IN ("DISPLAY", "VIDEO") AND campaign.status != "REMOVED" AND metrics.impressions > 0 ORDER BY metrics.cost_micros DESC';
var report;
try {
report = AdsApp.report(query);
} catch (e) {
try {
report = AdsApp.report(
"SELECT Criteria, DisplayName, CampaignName, CampaignId, Clicks, Impressions, Cost, Conversions FROM PLACEMENT_PERFORMANCE_REPORT WHERE Impressions > 0 DURING " +
dateFrom.replace(/-/g, "") +
"," +
dateTo.replace(/-/g, ""),
);
} catch (e2) {
result.errors.push("Report failed: " + e2.message);
return JSON.stringify(result);
}
}
var rows = report.rows();
var exclusionQueue = [];
var seenPlacements = {};
while (rows.hasNext()) {
var row = rows.next();
result.scanned++;
var placement = String(
row["detail_placement_view.target_url"] || row["Criteria"] || "",
)
.trim()
.toLowerCase();
var campaignId = row["campaign.id"] || row["CampaignId"];
var campaignName = row["campaign.name"] || row["CampaignName"];
var clicks = parseInt(row["metrics.clicks"] || row["Clicks"] || 0);
var costMicros = parseInt(row["metrics.cost_micros"] || 0);
var cost =
costMicros > 0 ? costMicros / 1000000 : parseFloat(row["Cost"] || 0);
var conversions = parseFloat(
row["metrics.conversions"] || row["Conversions"] || 0,
);
if (!placement || placement === "(not set)") continue;
var placementKey = placement + "|" + campaignId;
if (seenPlacements[placementKey]) continue;
var excludeReason = null;
for (var t = 0; t < cfg.badTlds.length; t++) {
if (endsWithTld(placement, cfg.badTlds[t])) {
excludeReason = "BAD_TLD: " + cfg.badTlds[t];
result.byTld++;
break;
}
}
if (!excludeReason) {
for (var k = 0; k < cfg.badKeywords.length; k++) {
if (placement.indexOf(cfg.badKeywords[k]) !== -1) {
excludeReason = "BAD_KEYWORD: " + cfg.badKeywords[k];
result.byKeyword++;
break;
}
}
}
if (
!excludeReason &&
clicks >= cfg.minClicksThreshold &&
cost >= cfg.zeroConvSpendThreshold &&
conversions === 0
) {
excludeReason =
"ZERO_CONV: " +
cost.toFixed(2) +
" spent, " +
clicks +
" clicks, 0 conv";
result.byPerformance++;
}
if (excludeReason) {
seenPlacements[placementKey] = true;
exclusionQueue.push({
placement: placement,
campaignId: campaignId,
campaignName: campaignName,
reason: excludeReason,
spend: cost,
clicks: clicks,
conversions: conversions,
});
}
}
// Apply exclusions safely
for (var i = 0; i < exclusionQueue.length; i++) {
var item = exclusionQueue[i];
try {
if (!cfg.dryRun) {
var campaign = AdsApp.campaigns().withIds([item.campaignId]).get();
var camp;
if (campaign.hasNext()) {
camp = campaign.next();
} else {
var videoCampaign = AdsApp.videoCampaigns().withIds([item.campaignId]).get();
if (videoCampaign.hasNext()) {
camp = videoCampaign.next();
}
}
if (camp) {
applyPlacementExclusionSafe(camp, item.placement);
} else {
result.errors.push(item.placement + ": campaign not found");
continue;
}
}
result.applied++;
} catch (e) {
if (e.message && e.message.indexOf("already exists") !== -1) {
result.alreadyExcluded++;
} else {
result.errors.push(item.placement + ": " + e.message);
}
}
}
result.topExclusions = exclusionQueue
.sort(function (a, b) {
return b.spend - a.spend;
})
.slice(0, 10)
.map(function (item) {
return (
item.placement + " | " + item.spend.toFixed(2) + " | " + item.reason
);
});
if (cfg.auditSheetUrl && exclusionQueue.length > 0) {
try {
var ss = SpreadsheetApp.openByUrl(cfg.auditSheetUrl);
var sheet = ss.getSheetByName(cfg.auditSheetName);
if (!sheet) {
sheet = ss.insertSheet(cfg.auditSheetName);
sheet.appendRow([
"Date",
"Account",
"Placement",
"Campaign",
"Reason",
"Spend",
"Clicks",
"Conversions",
]);
}
var now = new Date().toISOString();
for (var j = 0; j < exclusionQueue.length; j++) {
var row = exclusionQueue[j];
sheet.appendRow([
now,
accountName + " (" + accountId + ")",
row.placement,
row.campaignName,
row.reason,
row.spend.toFixed(2),
row.clicks,
row.conversions,
]);
}
} catch (e) {
result.errors.push("Audit log: " + e.message);
}
}
Logger.log(
"[" +
accountId +
"] " +
accountName +
": scanned " +
result.scanned +
", excluded " +
result.applied +
" (TLD:" +
result.byTld +
" KW:" +
result.byKeyword +
" PERF:" +
result.byPerformance +
")",
);
return JSON.stringify(result);
}
function onAllComplete(results) {
var totals = {
accounts: 0,
scanned: 0,
applied: 0,
byTld: 0,
byKeyword: 0,
byPerformance: 0,
alreadyExcluded: 0,
errors: 0,
};
var summaryLines = [];
for (var i = 0; i < results.length; i++) {
var r = JSON.parse(results[i].getReturnValue());
totals.accounts++;
totals.scanned += r.scanned;
totals.applied += r.applied;
totals.byTld += r.byTld;
totals.byKeyword += r.byKeyword;
totals.byPerformance += r.byPerformance;
totals.alreadyExcluded += r.alreadyExcluded;
totals.errors += r.errors.length;
if (r.applied > 0 || r.errors.length > 0)
summaryLines.push(
r.accountName +
" (" +
r.accountId +
"): +" +
r.applied +
" excluded, " +
r.alreadyExcluded +
" existing" +
(r.errors.length > 0 ? ", " + r.errors.length + " errors" : ""),
);
}
Logger.log("");
Logger.log("=== TLD AUTO-EXCLUSION COMPLETE ===");
Logger.log("Accounts: " + totals.accounts);
Logger.log("Placements scanned: " + totals.scanned);
Logger.log(
"Excluded: " +
totals.applied +
" (TLD:" +
totals.byTld +
" KW:" +
totals.byKeyword +
" PERF:" +
totals.byPerformance +
")",
);
Logger.log("Already excluded: " + totals.alreadyExcluded);
Logger.log("Errors: " + totals.errors);
if (summaryLines.length > 0) {
Logger.log("");
Logger.log("--- Per-account ---");
for (var j = 0; j < summaryLines.length; j++)
Logger.log(" " + summaryLines[j]);
}
if (CONFIG.ALERT_EMAIL && totals.applied > 0) {
var subject =
"TLD Auto-Exclusion: +" +
totals.applied +
" across " +
totals.accounts +
" accounts";
var body =
"Run: " +
new Date().toISOString() +
"\n\n" +
"Accounts: " +
totals.accounts +
"\n" +
"Scanned: " +
totals.scanned +
"\n" +
"Excluded: " +
totals.applied +
"\n" +
" By TLD: " +
totals.byTld +
"\n" +
" By keyword: " +
totals.byKeyword +
"\n" +
" By performance: " +
totals.byPerformance +
"\n" +
"Already excluded: " +
totals.alreadyExcluded +
"\n" +
"Errors: " +
totals.errors +
"\n\n" +
"Accounts with changes:\n" +
summaryLines.join("\n");
MailApp.sendEmail(CONFIG.ALERT_EMAIL, subject, body);
}
}
// helpers
function endsWithTld(placement, tld) {
var domain = placement
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "")
.replace(/:\d+$/, "");
if (domain.length <= tld.length) return false;
return domain.substring(domain.length - tld.length) === tld;
}
function fmtDate(date) {
var y = date.getFullYear();
var m = ("0" + (date.getMonth() + 1)).slice(-2);
var d = ("0" + date.getDate()).slice(-2);
return y + "-" + m + "-" + d;
}
/**
* Safely applies placement exclusion across Display or Video campaigns.
*/
function applyPlacementExclusionSafe(campaign, placement) {
var lower = String(placement || "").trim().toLowerCase();
// 1. Mobile app category (e.g. mobileappcategory::69500)
if (lower.indexOf("mobileappcategory::") === 0) {
var catId = parseInt(lower.split("::")[1], 10);
if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
var vt = campaign.videoTargeting();
if (vt.newMobileAppCategoryBuilder && typeof vt.newMobileAppCategoryBuilder === "function") {
vt.newMobileAppCategoryBuilder()
.withMobileAppCategoryId(catId)
.exclude();
return;
}
}
// For Display / other campaigns, mobile app categories are not supported via scripts at campaign level.
throw new Error("Mobile app category exclusions are not supported via script for Display/PMax campaigns at campaign level. Exclude them manually at the Account level under Content Suitability.");
}
// 2. Mobile application (e.g. com.example.app, or prefixed with com. or org.)
var isApp = (lower.indexOf("com.") === 0 || lower.indexOf("org.") === 0 || lower.indexOf("mobileapp::") === 0);
if (isApp) {
var appId = lower;
if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
var vt2 = campaign.videoTargeting();
if (vt2.newMobileApplicationBuilder && typeof vt2.newMobileApplicationBuilder === "function") {
vt2.newMobileApplicationBuilder()
.withAppId(appId)
.exclude();
return;
}
}
throw new Error("Mobile app exclusions are not supported via script for Display/PMax campaigns at campaign level. Exclude them manually at the Account level under Content Suitability.");
}
// 3. Website placements
// Try Display campaign builder first
if (campaign.display && typeof campaign.display === "function") {
var d = campaign.display();
if (d.newPlacementBuilder && typeof d.newPlacementBuilder === "function") {
d.newPlacementBuilder()
.withUrl(placement)
.exclude();
return;
}
}
// Try Video campaign builder
if (campaign.videoTargeting && typeof campaign.videoTargeting === "function") {
var vt3 = campaign.videoTargeting();
if (vt3.newPlacementBuilder && typeof vt3.newPlacementBuilder === "function") {
vt3.newPlacementBuilder()
.withUrl(placement)
.exclude();
return;
}
}
throw new Error("Campaign type not supported for placement exclusions via script.");
}
@PKBibi

PKBibi commented Jun 8, 2026

Copy link
Copy Markdown
Author

For the background, read the blog post here: https://ppcchief.com/blog/gdn-exclusion-list

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment