Yahoo!広告 スクリプト | Developer Center
English事例集(通知系)
・Q.動的ディスプレイの商品リストで、前日にアップロードエラーが発生した際に通知したいです・Q.キャンペーン/広告グループに紐づくクイックリンク本数が指定した本数を超える場合に、スプレッドシートに出力してメールで通知したいです
Q.動的ディスプレイの商品リストで、前日にアップロードエラーが発生した際に通知したいです
A.以下のスクリプトで実現できます。
動作確認バージョン:v202406
コードサンプル ここをクリックすると折り畳みます。
/*このソースコードは MIT License のもとで提供されています。
https://ads-developers.yahoo.co.jp/ja/ads-script/post/30418913.html
■スクリプト内容
前日アップロードでエラー数が0以上の商品リストがある場合、メールまたはSlackで通知する。
■利用方法
1.当スクリプトを、ディスプレイ広告のアカウントに設定してください。
2.定数を以下のように設定してください。
3.実行頻度を1日1回(時間は任意)に設定してください。
■定数
・FLAG_MAIL //結果をメール送信するならtrue、しないならfalse
・MAIL_TO //メール送信先のYahoo! BusinessID
・MAIL_TITLE //メールタイトル
・FLAG_SLACK //結果をSlack送信するならtrue、しないならfalse
・URL_FETCH_APP //SlackのWebhook URL
・EVERYTIME_ALERT //true:毎回(アップロードエラーがなくても)通知、false:アップロードエラーがある場合のみ通知
*/
//設定が必要な定数
const FLAG_MAIL = false;
const MAIL_TO = ['Yahoo! JAPAN Business ID'];
let MAIL_TITLE = '商品リストのエラー通知';
const FLAG_SLACK = false;
const URL_FETCH_APP = 'SLACK_WEBHOOK_URL';
const EVERYTIME_ALERT = false;
//設定が不要な定数(変更するとエラーになります)
const accountId = AdsUtilities.getCurrentAccountId();
let TEXT_MESSAGE_ARRAY = [];
const UPLOAD_STATUS_ERROR_OBJ = {
'FILE_FORMAT_ERROR': 'アップロードファイルに不備あり',
'SYSTEM_ERROR': 'システムエラー',
'NETWORK_ERROR': 'ネットワークエラー',
'FILE_NOT_FOUND_ERROR': '対象ファイルなし',
'FILE_SIZE_OVER_ERROR': 'ファイルサイズ上限を超過',
'AUTH_ERROR': '認証エラー',
'UPLOAD_COUNT_OVER_ERROR': 'アップロード回数上限を超過',
};
function main() {
try {
checkConstants();
const totalErrCnt = getFeedDataError();
if (totalErrCnt > 0 || EVERYTIME_ALERT) {
//エラーがある場合、または毎回通知の場合は、エラーがなくても通知
sendMailAndSlack();
}
} catch (error) {
MAIL_TITLE = '!!エラー発生!!' + MAIL_TITLE;
logAndMessage('!!エラーが発生しました!!詳細は管理画面のログをご確認ください。' + error);
sendMailAndSlack();
throw error;
}
}
//当要件では最初にチェックしないと肝心な時にエラーに気づけないので
function checkConstants() {
let errorMsg = '';
if (FLAG_SLACK && (URL_FETCH_APP === 'SLACK_WEBHOOK_URL' || URL_FETCH_APP === '')) {
errorMsg += 'URL_FETCH_APPにはSlackのWebhook URLを設定してください。';
}
if (FLAG_MAIL && (MAIL_TO.length < 1 || !MAIL_TO.every(str => typeof str === 'string' && /^[a-zA-Z0-9]+$/.test(str)))) {
errorMsg += 'MAIL_TOにはYahoo! JAPANビジネスIDを設定してください。';
}
if (errorMsg != '') {
throw new Error(errorMsg);
}
}
function getFeedDataError() {
//前日
let date = new Date();
date.setDate(date.getDate() - 1);
let yesterday = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyyMMdd');
//データ取得
let feedMaster = Display.FeedDataService.get({
accountId: accountId,
fileUploadDateRange: {
endDate: yesterday,
startDate: yesterday
},
}).rval;
if (feedMaster.totalNumEntries == 0) {
//前日分がない場合
logAndMessage('前日アップロードされた商品リストはありませんでした');
return 0;
}
//判定
let errCnt = 0;
let uploadErrCnt = 0;
for (let i = 0; i < feedMaster.values.length; i++) {
const feedData = feedMaster.values[i].feedData;
if (UPLOAD_STATUS_ERROR_OBJ.hasOwnProperty(feedData.fileUploadStatus)) {
//エラーステータスの場合
uploadErrCnt++;
}
if (feedData.errorCount > 0) {
//アップロード自体は成功しているが一部エラーの場合
errCnt++;
}
}
//ログ出力
const totalErrCnt = errCnt + uploadErrCnt;
if (totalErrCnt > 0) {
logAndMessage('前日アップロードのエラーが' + totalErrCnt + '件あります(アップロード失敗:'
+ uploadErrCnt + '件、アップロード成功したが一部エラーあり' + errCnt + '件)');
} else {
logAndMessage('前日アップロードでエラーが発生した商品リストはありませんでした');
}
return totalErrCnt;
}
//ログ、メールなどに流すテキスト
function logAndMessage(text) {
Logger.log(text);
TEXT_MESSAGE_ARRAY.push(text);
}
//メール・Slack部品
function sendMailAndSlack() {
sendMail();
sendSlack();
}
function sendMail() {
if (FLAG_SLACK) {
UrlFetchApp.fetch(URL_FETCH_APP, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({
text:
'【アカウントID:' + accountId + '】' + MAIL_TITLE + '\n' +//Slackもタイトルがあった方がいい
TEXT_MESSAGE_ARRAY.join('\n'),
}),
});
}
}
function sendSlack() {
if (FLAG_MAIL) {
MailApp.sendEmail({
to: MAIL_TO,
subject: '【アカウントID:' + accountId + '】' + MAIL_TITLE,
body: TEXT_MESSAGE_ARRAY.join('\n')
});
}
}
Q.キャンペーン/広告グループに紐づくクイックリンク本数が指定した本数を超える場合に、スプレッドシートに出力してメールで通知したいです
A.以下のスクリプトで実現できます。本数部分は任意に設定いただけます。
動作可能プロダクト:MCC(本スクリプトは検索広告のアカウントのみ)
動作確認バージョン:v202402
コードサンプル ここをクリックすると折り畳みます。
//キャンペーン/広告グループに紐づくクイックリンク本数が指定した LIMIT_COUNT 本を超える場合に、スプレッドシートに出力してメールで通知します。
//設定が必要な定数
const SPREAD_SHEET_ID = 'スプレッドシートID'; //★書き出すスプレッドシートID
const SHEET_NAME_CMP = 'キャンペーンクイックリンク'; //★キャンペーンのクイックリンクを書き出すスプレッドシートのシート名
const SHEET_NAME_ADG = '広告グループクイックリンク'; //★広告グループのクイックリンクを書き出すスプレッドシートのシート名
const MAIL_TO = ['Yahoo! BusinessID'];
const MAIL_TITLE = 'クイックリンク超過通知';
const LIMIT_COUNT = 16; //通知対象本数
//設定が不要な定数
const accountId = AdsUtilities.getCurrentAccountId();
const MESSAGE_TEXT = 'クイックリンクオンの上限が' + LIMIT_COUNT + '本を超過しています。スプレッドシートをご確認ください。\n'
+ 'https://docs.google.com/spreadsheets/d/' + SPREAD_SHEET_ID;
const ENTITY_TYPE = {
CAMPAIGN: 'CAMPAIGN',
ADGROUP: 'ADGROUP'
}
const DAYS_OF_WEEK_FORLOG = {
"MONDAY": "月曜日",
"TUESDAY": "火曜日",
"WEDNESDAY": "水曜日",
"THURSDAY": "木曜日",
"FRIDAY": "金曜日",
"SATURDAY": "土曜日",
"SUNDAY": "日曜日"
};
const MINUTES_FORLOG = {
"ZERO": "00",
"FIFTEEN": "15",
"THIRTY": "30",
"FORTY_FIVE": "45"
};
const CMP_HEADER = ['キャンペーン名', 'クイックリンクテキスト', 'クイックリンク説明文1',
'クイックリンク説明文2', '開始日', '終了日', '曜日・時間帯', '最終リンク先URL', 'スマートフォン向けURL',
'トラッキングURL', 'カスタムパラメータ', 'キャンペーンID', '広告表示アセットID'];
const ADG_HEADER = ['キャンペーン名', '広告グループ名', 'クイックリンクテキスト',
'クイックリンク説明文1', 'クイックリンク説明文2', '開始日', '終了日', '曜日・時間帯',
'最終リンク先URL', 'スマートフォン向けURL', 'トラッキングURL', 'カスタムパラメータ', 'キャンペーンID', '広告グループID', '広告表示アセットID'];
function main() {
try {
const assetMap = getAssetMap();
//キャンペーン
const cmpQuickLinkCnt = outputQuickLink(ENTITY_TYPE.CAMPAIGN, assetMap, SHEET_NAME_CMP);
//広告グループ
const adgQuickLinkCnt = outputQuickLink(ENTITY_TYPE.ADGROUP, assetMap, SHEET_NAME_ADG);
if (cmpQuickLinkCnt + adgQuickLinkCnt > 0) {
sendEmail(MESSAGE_TEXT);
}
} catch (error) {
sendEmail('!!エラーが発生しました!!管理画面のログをご確認ください\n' + error);
throw error;
}
}
function getAssetMap() {
const assets = Search.AssetService.get({
accountId: accountId,
types: ['QUICKLINK'],
numberResults: 2000 //MAX持ってくる
}).rval;
let aseetMap = new Map();
if (assets.totalNumEntries == 0) {
throw new Error('クイックリンクが1件も登録されていません');
}
for (let i = 0; i < assets.values.length; i++) {
const asset = assets.values[i].asset;
aseetMap.set(asset.assetId, asset);
}
return aseetMap;
}
function outputQuickLink(entityType, assetMap, sheetName) {
const sh = SpreadsheetApp.openById(SPREAD_SHEET_ID).getSheetByName(sheetName);
clearSpreadSheet(entityType, sh);//最初にクリアしてしまう
const over16AssetsArr = getQuickLinkOverLimit(entityType);
if (over16AssetsArr.length == 0) {
Logger.log('クイックリンクが' + LIMIT_COUNT + '本を超過している' + entityType + 'はありませんでした。');
return 0;
}
const ssOutputData = createSSOutputData(entityType, over16AssetsArr, assetMap);
sh.getRange('A1').setValues(ssOutputData);
Logger.log(entityType + 'に紐づくクイックリンクを' + ssOutputData.length + '件出力しました。');
return ssOutputData.length;
}
function clearSpreadSheet(entityType, sh) {
let lastRow = sh.getLastRow();
if (lastRow < 2) {
//初回の時はクリア不要
return;
}
if (entityType == ENTITY_TYPE.CAMPAIGN) {
sh.getRange('A2:M' + lastRow).clear();
} else {
sh.getRange('A2:O' + lastRow).clear();
}
}
//配信オンのクイックリンクが LIMIT_COUNT 件以上ある{キャンペーンID:アセットID}の配列を取得
function getQuickLinkOverLimit(entityType) {
let assetService;
let idKey;
let assetProp;
let assetsObj = {}; // key: エンティティID、value: アセットIDの配列
if (entityType == ENTITY_TYPE.CAMPAIGN) {
assetService = Search.CampaignAssetService;
idKey = 'campaignId';
assetProp = 'campaignAsset';
} else {
assetService = Search.AdGroupAssetService;
idKey = 'adGroupId';
assetProp = 'adGroupAsset';
}
const assets = assetService.get({
accountId: accountId,
types: ['QUICKLINK'],
userStatuses: ['ACTIVE']
}).rval;
if (assets.totalNumEntries == 0) {
return [];
}
for (let i = 0; i < assets.values.length; i++) {
const asset = assets.values[i][assetProp];
const { assetId } = asset;
const entityId = asset[idKey];
if (!assetsObj[entityId]) {
assetsObj[entityId] = [];
}
assetsObj[entityId].push(assetId);
}
let over16AssetsArr = [];
for (const entityId in assetsObj) {
if (assetsObj[entityId].length >= LIMIT_COUNT) {
over16AssetsArr.push({
[idKey]: entityId,
assetIds: assetsObj[entityId]
});
}
}
return over16AssetsArr;
}
function createSSOutputData(entityType, over16AssetsArr, assetMap) {
let idKey;
let entityIds = [];
let ssOutputData = [];
if (entityType == ENTITY_TYPE.CAMPAIGN) {
idKey = 'campaignId';
entityIds = over16AssetsArr.map(item => item.campaignId);
ssOutputData.push(CMP_HEADER);
} else {
idKey = 'adGroupId';
entityIds = over16AssetsArr.map(item => item.adGroupId);
ssOutputData.push(ADG_HEADER);
}
const entityMasterMap = getEntityMasterMap(entityType, entityIds);
for (let i = 0; i < over16AssetsArr.length; i++) {
const entityId = Number(over16AssetsArr[i][idKey]);
let entityNameObj;
if (entityMasterMap.has(entityId)) {
entityNameObj = entityMasterMap.get(entityId);
} else {
//ないはずないが何のため
Logger.log(idKey + ':' + entityId + ' のマスタが取得できませんでした。');
}
const assetArr = over16AssetsArr[i].assetIds;
for (let j = 0; j < assetArr.length; j++) {
const assetId = assetArr[j];
if (assetMap.has(assetId)) {
const assetObj = assetMap.get(assetId);
const row = createSSRow(entityType, entityId, entityNameObj, assetObj);
ssOutputData.push(row);
} else {
Logger.log(idKey + ':' + entityId + ' に紐づいているアセットID ' + assetId + ' のアセットがマスタに存在しません。');
}
}
}
return ssOutputData;
}
function getEntityMasterMap(entityType, entityIds) {
let entityMasterMap = new Map();
if (entityType == ENTITY_TYPE.CAMPAIGN) {
const campaigns = Search.CampaignService.get({
accountId: accountId,
campaignIds: entityIds,
numberResults: 10000,
}).rval;
for (let i = 0; i < campaigns.values.length; i++) {
const { campaignId, campaignName } = campaigns.values[i].campaign;
entityMasterMap.set(campaignId, { campaignName: campaignName });
}
} else {
const adGroups = Search.AdGroupService.get({
accountId: accountId,
adGroupIds: entityIds,
numberResults: 10000,
}).rval;
for (let i = 0; i < adGroups.values.length; i++) {
const { adGroupId, adGroupName, campaignName, campaignId } = adGroups.values[i].adGroup;
entityMasterMap.set(adGroupId, { adGroupName: adGroupName, campaignName: campaignName, campaignId: campaignId });
}
}
return entityMasterMap;
}
function createSSRow(entityType, entityId, entityNameObj, assetObj) {
if (entityType == ENTITY_TYPE.CAMPAIGN) {
return [
entityNameObj.campaignName,
assetObj.assetData.quickLinkAsset.linkText,
assetObj.assetData.quickLinkAsset.description1,
assetObj.assetData.quickLinkAsset.description2,
getDateString(assetObj.assetData.quickLinkAsset.startDate),
getDateString(assetObj.assetData.quickLinkAsset.endDate),
getScheduleString(assetObj.assetData.quickLinkAsset.schedules),//曜日・時間帯
assetObj.finalUrl,
assetObj.smartphoneFinalUrl == null ? '(空白)' : assetObj.smartphoneFinalUrl,
assetObj.trackingUrl == null ? '(空白)' : assetObj.trackingUrl,
getCustomParamString(assetObj.customParameters),
entityId,
assetObj.assetId,
''//N列(配信設定オフ変更)クリア用
];
} else {
return [
entityNameObj.campaignName,
entityNameObj.adGroupName,
assetObj.assetData.quickLinkAsset.linkText,
assetObj.assetData.quickLinkAsset.description1,
assetObj.assetData.quickLinkAsset.description2,
getDateString(assetObj.assetData.quickLinkAsset.startDate),
getDateString(assetObj.assetData.quickLinkAsset.endDate),
getScheduleString(assetObj.assetData.quickLinkAsset.schedules),
assetObj.finalUrl,
assetObj.smartphoneFinalUrl == null ? '(空白)' : assetObj.smartphoneFinalUrl,
assetObj.trackingUrl == null ? '(空白)' : assetObj.trackingUrl,
getCustomParamString(assetObj.customParameters),
entityNameObj.campaignId,
entityId,
assetObj.assetId,
''//P列(配信設定オフ変更)クリア用
];
}
}
function getDateString(yyyymmdd) {
if (yyyymmdd == null) {
return '(空白)';
} else {
const year = yyyymmdd.substring(0, 4);
const month = yyyymmdd.substring(4, 6);
const day = yyyymmdd.substring(6, 8);
return `${year}/${month}/${day}`;
}
}
function getScheduleString(schedules) {
if (schedules == null) {
return '(空白)'
}
let outputString = '';
for (let i = 0; i < schedules.length; i++) {
const schedule = schedules[i];
const { dayOfWeek, startHour, startMinute, endHour, endMinute } = schedule;
const dayOfWeekJapanese = DAYS_OF_WEEK_FORLOG[dayOfWeek];
const startMinuteReadable = MINUTES_FORLOG[startMinute];
const endMinuteReadable = MINUTES_FORLOG[endMinute];
outputString += `${dayOfWeekJapanese}: ${startHour}:${startMinuteReadable}~${endHour}:${endMinuteReadable}\n`;//管理画面と表示をそろえる
}
return outputString;
}
function getCustomParamString(customParameters) {
if (customParameters == null) {
return '(空白)';
}
let outputString = '';
for (let i = 0; i < customParameters.parameters.length; i++) {
const parameter = customParameters.parameters[i];
outputString += `{_${parameter.key}} = ${parameter.value}\n`;//管理画面と表示をそろえる
}
return outputString;
}
function sendEmail(text) {
MailApp.sendEmail({
to: MAIL_TO,
subject: MAIL_TITLE,
body: text,
});
}