事例集(通知系)

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 sendSlack() {
  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 sendMail() {
  if (FLAG_MAIL) {
    MailApp.sendEmail({
      to: MAIL_TO,
      subject: '【アカウントID:' + accountId + '】' + MAIL_TITLE,
      body: TEXT_MESSAGE_ARRAY.join('\n')
    });
  }
}


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,
  });
}