アカウント変動通知

スクリプトの概要

本ページでは、アカウントにGoogleスプレッドシートで指定した条件に合致する変動があった場合に、その情報をGoogleスプレッドシートへ出力し、結果をメールまたはSlackで通知できるスクリプトをご紹介します。

チェック対象として指定できる指標は、インプレッション数/クリック数/コンバージョン数/コストの4種類です。
条件に合致する変動がない場合は通知されません。

本機能をご利用いただくことで、アカウント単位の配信実績でみてインプレッション数/クリック数/コンバージョン数/コストに変動があった場合はメールまたはSlackの通知で気付くことができるため、急な配信状況の変化にも対応しやすくなります。
※当スクリプトは検索広告・ディスプレイ広告共通版となっております。(設定されたアカウントが検索広告かディスプレイ広告かを自動で判定します)

スクリプト実行結果のイメージ

ご利用のイメージ

スプレッドシートで指定した過去N週間の同じ曜日の配信実績の平均と、スクリプト実行日の各値を比較します。
※本スクリプトで通知条件として参照する各指標の本日の値は、パフォーマンスレポートの「今日」のデータと同一のため、確定前の数値です。
ご利用の例 用途 通知条件 実行頻度
過去12週間の同じ曜日の配信実績の平均と比較して、スクリプトを実行した当日の「1時間前」の「インプレッション数が130%」以上になった場合にメール通知する 直近3か月の傾向と比較して、当日のインプレッションが急激に増えた場合に通知で気づけるため、設定の見直しがすぐに対応できる ■期間
・過去「12」週間
・当日の「1」時間前まで

■指標
・インプレッション数「130」%
1時間ごと
過去26週間の同じ曜日の配信実績の平均と比較して、スクリプトを実行した当日の「1時間前」の「クリック数が45%以下」または「コストが50%以下」になった場合に通知する 直近半年の傾向と比較して、当日のコストまたはクリック数が極端に減っている場合に通知で気づけるため、当日中に設定の見直しができる ■期間
・過去「26」週間
・当日の「1」時間前まで

■指標
・クリック数「45」%
・コスト「50」%
※指標の複数指定はOR条件となります
毎日 18:00

ご利用の流れ

1.Yahoo!広告スクリプトとGoogleアカウントを連携します
 詳しくはGoogleアカウントとの連携を確認してください。
 ※すでに連携済みの方は次のステップからご設定ください。

2.スプレッドシートのテンプレートを、以下のボタンからご自身のGoogle ドライブにコピーしてください。
  テンプレートをコピー
 コピーで作成したスプレッドシートのスプレッドシートIDを確認してメモしておいてください。スクリプト内の定数の設定で利用します。
 ※GoogleスプレッドシートIDの取得方法についてはこちらをご覧ください。

3. 2で作成したスプレッドシート内の「条件指定」シートにて、変動をチェックしたい指標の条件を指定してください。
 ※テンプレート初期値では、条件を指定するシートのシート名を「条件指定」、結果を出力するシートのシート名を「出力」としていますが、各シート名は任意でご変更可能です。

4. 管理画面上のスクリプト作成画面にて、後述のサンプルコードを設定してください。
 ※スクリプトの新規作成および編集の手順はこちらをご覧ください。

5. 管理画面上にてスクリプトの設定を完了後、必要に応じてスクリプトの実行頻度の設定をしてください。
 本スクリプトでは1時間ごとの設定が推奨です。
 ※スクリプトの実行頻度の設定手順はこちらをご覧ください。

サンプルコード内各定数のご説明

後述のサンプルコードにおける各定数の設定方法についてご説明いたします。

■スプレッドシートIDの指定例

次の例のように、' '(シングルクオーテーション)で囲んで指定してください。(スプレッドシートID:'12345abcde'の場合)
const SPREAD_SHEET_ID = '12345abcde'; 

■シート名の指定例

次の例のように、' '(シングルクオーテーション)で囲んで指定してください。(テンプレート初期値の場合:条件を指定するシートのシート名「条件指定」、結果を出力するシートのシート名「出力」)
const INPUT_SHEET_NAME = '条件指定';
const OUTPUT_SHEET_NAME = '出力';

■メール、Slackの指定例

メール、Slack設定についてをご確認ください。

サンプルコード

下記のスクリプトを、「コピー」ボタンを押してコピーし、スクリプトの入力画面に貼り付けてください。(このとき、灰色のコメント部分は消さずに残しておいてください)
「サンプルコード内各定数のご説明」またはスクリプト内に記載の利用方法に沿って、設定が必要な定数を設定してください。

/*このソースコードは MIT License のもとで提供されています。
https://ads-developers.yahoo.co.jp/ja/ads-script/post/30418913.html 
■スクリプト内容
アカウントに、スプレッドシートで指定した条件に合致する変動があった場合にメール/Slack通知します。
■利用方法
1.当スクリプトを、検索広告またはディスプレイ広告のアカウントに設定してください。
2.定数を以下のように設定してください。
■定数
・SPREAD_SHEET_ID   //スプレットシートIDを指定
・INPUT_SHEET_NAME  //条件指定シート名
・OUTPUT_SHEET_NAME //出力シート名
・FLAG_MAIL         //結果をメール送信するならtrue、しないならfalse
・MAIL_TO           //メール送信先のYahoo! BusinessID
・MAIL_TITLE        //メールタイトル
・FLAG_SLACK        //結果をSlack送信するならtrue、しないならfalse
・URL_FETCH_APP     //SlackのWebhook URL
■制限事項
 */
//設定が必要な定数
const SPREAD_SHEET_ID = '';
const INPUT_SHEET_NAME = '条件指定';
const OUTPUT_SHEET_NAME = '出力';
const FLAG_MAIL = false;
const MAIL_TO = ['Yahoo! JAPAN Business ID'];
let MAIL_TITLE = 'アカウント変動検出';
const FLAG_SLACK = false;
const URL_FETCH_APP = '';
//設定が不要な定数(変更するとエラーになります)
const accountId = AdsUtilities.getCurrentAccountId();
const productType = AdsUtilities.getProductType();
let TEXT_MESSAGE_ARRAY = [];
const SPREAD_SHEET_URL = 'https://docs.google.com/spreadsheets/d/' + SPREAD_SHEET_ID;
const SS_PERIOD_RANGE_STR = 'B3';
const SS_DATA_FROM_HOURS_AGO = 'B4';
const SS_METRICS_RANGE_STR = 'B6:C9';
const SS_OUTPUT_RANGE_STR = 'A3:D6';
const WEEKDAYS = ["土曜日", "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日"];
const MILLIS_PER_DAY = 1000 * 60 * 60 * 24;
const REPORT_FIELDS = ['DAY', 'HOUR_OF_DAY', 'IMPS', 'CLICKS', 'CONVERSIONS', 'COST'];
const TODAY = new Date();
let globalTargetTime = 0;//取得データの時間はグローバル変数にする
const METRICS = {//IdxがSSの各RangeのRowIdxと対応
  impression: "インプレッション数",
  click: "クリック数",
  conversion: "コンバージョン数",
  cost: "コスト",
};
function main() {
  try {
    //スプシから値取得
    const ss = validateAndGetSpreadsheet(SPREAD_SHEET_ID);
    const inputSh = validateAndGetSheet(ss, INPUT_SHEET_NAME);
    const { ssWeekCnt, ssHoursAgo, metricThresholds } = getDataFromSS(inputSh);
    // N時間前の時刻を計算
    const now = Utilities.formatDate(TODAY, 'Asia/Tokyo', 'H');
    globalTargetTime = Number(now) - ssHoursAgo;
    // N時間前の時刻が1時より前の場合、ログを出力して終了
    if (globalTargetTime <= 1) {
      Logger.log('N時間前の時刻が1時より前です。処理を終了します。');
      return;
    }
    //計算する
    monitorAccount(metricThresholds, ssWeekCnt, ss);
    //超えてたらアラート
    if (TEXT_MESSAGE_ARRAY.length > 0) {
      validateAndSendMail();
      validateAndSendSlack();
    }
  } catch (error) {
    //エラーの時もアラート
    MAIL_TITLE = '!!エラー!!' + MAIL_TITLE;
    logAndMessage('!!エラーが発生しています!!詳細は管理画面のログをご確認ください。\n' + error);
    validateAndSendMail();
    validateAndSendSlack();
    throw error;
  }
}
function getDataFromSS(inputSh) {
  const ssWeekCnt = Number(inputSh.getRange(SS_PERIOD_RANGE_STR).getValues()[0][0]);//DDLなのでチェックはしない
  const ssHoursAgo = getSSHour(inputSh.getRange(SS_DATA_FROM_HOURS_AGO).getValues()[0][0]);
  const ssMetrics = inputSh.getRange(SS_METRICS_RANGE_STR).getValues();
  const metricThresholds = {};
  Object.keys(METRICS).forEach((key, index) => {
    metricThresholds[key + "Threshold"] = getMetrics(ssMetrics[index]);
  });
  return { ssWeekCnt: ssWeekCnt, ssHoursAgo: ssHoursAgo, metricThresholds: metricThresholds };
}
function getSSHour(ssValue) {
  if (ssValue.trim() == '' || isNaN(Number(ssValue))) {
    throw new Error('時間は必ず数字で指定してください。');
  }
  return Number(ssValue.trim());
}
function getMetrics(row) {
  const value = row[1].trim();
  if (isNaN(Number(value))) {
    throw new Error(row[0] + 'の値は数値で入力してください');
  }
  // %から少数に変換
  return Number(value) / 100;
}
function getConditionForReport(ssWeekCnt) {
  //時間フィルタ
  const hourFieldName = productType == 'SEARCH' ? 'HOUR_OF_DAY' : 'HOUR';//検索とディスプレイで項目名が違う
  REPORT_FIELDS[1] = hourFieldName;
  const hourFilter = {
    field: hourFieldName,
    filterOperator: 'LESS_THAN_EQUALS',
    values: [globalTargetTime]
  };
  //期間
  const todayCondition = createCondition(0);
  const pastCondition = createCondition(ssWeekCnt);
  return { todayCondition: todayCondition, pastCondition: pastCondition, hourFilter: hourFilter };
}
function createCondition(ssWeekCnt) {
  let startDate = new Date(TODAY); // TODAYの値をコピー
  let endDate = new Date(TODAY); // TODAYの値をコピー
  //本日はそのまま使う
  if (ssWeekCnt > 0) {
    //過去分
    const daysAgo = ssWeekCnt * 7;
    startDate.setDate(startDate.getDate() - daysAgo);
    endDate.setDate(TODAY.getDate() - 1);//本日は入れない
  }
  return {
    StartDate: Utilities.formatDate(startDate, 'Asia/Tokyo', 'yyyyMMdd'),
    EndDate: Utilities.formatDate(endDate, 'Asia/Tokyo', 'yyyyMMdd')
  };
}
function monitorAccount(metricThresholds, ssWeekCnt, ss) {
  //レポート用条件取得
  const conditionForReport = getConditionForReport(ssWeekCnt);
  //今日の情報を取得
  let todayReportRows = getReport(conditionForReport.todayCondition, conditionForReport.hourFilter);
  const todayComparisonData = createComparisonData(todayReportRows, 1);//レポートで取った値はそのまま使うので1
  //比較期間の情報を取得
  let pastReportRows = getReport(conditionForReport.pastCondition, conditionForReport.hourFilter)
  const pastComparisonData = createComparisonData(pastReportRows, ssWeekCnt);
  const outputData = compareAndCreateOutputData(todayComparisonData, pastComparisonData, metricThresholds);
  //「前回チェック日」
  const nowStr = Utilities.formatDate(TODAY, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm');
  let outputSh = validateAndGetSheet(ss, OUTPUT_SHEET_NAME);
  outputSh.getRange('B1').clear();
  outputSh.getRange('B1').setValues([[nowStr]]);
  if (outputData.length > 0) {
    //データ部
    outputSh.getRange(SS_OUTPUT_RANGE_STR).clear();
    outputSh.getRange(SS_OUTPUT_RANGE_STR).setValues(outputData);
  }
}
function getReport(todayCondition, hourFilter) {
  let report = null;
  const operand = {
    fields: REPORT_FIELDS,
    reportDateRangeType: 'CUSTOM_DATE',
    dateRange: {
      startDate: todayCondition.StartDate,
      endDate: todayCondition.EndDate
    },
    filters: [
      hourFilter
    ],
    reportSkipReportSummary: 'TRUE',//集計行を消しておく
    reportSkipColumnHeader: 'TRUE'//ヘッダはskip
  };
  Logger.log(JSON.stringify(operand));//条件をログ出力
  if (productType == 'SEARCH') {
    operand.reportType = 'ACCOUNT';//検索の場合はreportType必須
    report = AdsUtilities.getSearchReport(operand).reports[0].rows;
  } else if (productType == 'DISPLAY') {
    report = AdsUtilities.getDisplayReport(operand).reports[0].rows;
  } else {
    throw new Error('当スクリプトは検索広告またはディスプレイ広告のアカウントに設定してください。');
  }
  return report;
}
function createComparisonData(reportRows, ssWeekCnt) {
  let reportMetrics = {};
  Object.keys(METRICS).forEach((key) => {
    reportMetrics[key] = 0;//初期化
  });
  if (reportRows.length == 0) {
    Logger.log('レポートが0行でした');
    return reportMetrics;
  }
  const todayDayOfWeek = getWeekday(TODAY.getFullYear(), TODAY.getMonth() + 1, TODAY.getDate());
  for (let i = 0; i < reportRows.length; i++) {
    const row = reportRows[i];
    const [year, month, day] = row[0].split('-').map(Number);// 日付文字列を/で分けて代入
    const reportDayOfWeek = getWeekday(year, month, day);
    //曜日によるフィルタ
    if (reportDayOfWeek == todayDayOfWeek) {
      //集計するのはここでは係数系のみでOK
      Object.keys(METRICS).forEach((key, idx) => {
        reportMetrics[key] += parseFloat(row[2 + idx]);//列Idxは+2
      });
    }
  }
  //最後に週数で割る
  Object.keys(METRICS).forEach((key, idx) => {
    reportMetrics[key] = parseFloat((reportMetrics[key] / ssWeekCnt).toFixed(2));//列Idxは+2
  });
  return reportMetrics;
}
// Zellerの公式による曜日計算(毎回new Dateによる速度低下除け)
function getWeekday(year, month, day) {
  // 1月と2月を前年の13月と14月として扱う必要がある
  if (month < 3) {
    month += 12;
    year -= 1;
  }
  const K = year % 100; // 世紀の年(下2桁)
  const J = Math.floor(year / 100); // 世紀の数(上2桁)
  const h = (day + Math.floor(13 * (month + 1) / 5) + K + Math.floor(K / 4) + Math.floor(J / 4) + 5 * J) % 7;
  return WEEKDAYS[h];
}
function compareAndCreateOutputData(todayComparisonData, pastComparisonData, metricThresholds) {
  let outputData = [];
  for (const [key, value] of Object.entries(METRICS)) {
    const thresholdKey = key + "Threshold";
    if (metricThresholds[thresholdKey]) {
      const todayValue = todayComparisonData[key];
      const pastValue = pastComparisonData[key];
      const expectedValue = (pastValue * metricThresholds[thresholdKey]).toFixed(2);
      outputData.push([value, todayValue, pastValue, expectedValue]);
      if (todayValue > expectedValue) {
        //今日の値が高いとNG
        const alertMessage = value + 'が指定した値より高くなっています。' + globalTargetTime + ' 時時点の値:' +
          todayValue + '(' + expectedValue + '超)';
        logAndMessage(alertMessage);
      } else {
        //通知対象ではなくてもログには出力する
        Logger.log(value + ':' + globalTargetTime + ' 時時点の値:' + todayValue + '(' + expectedValue + '超)');
      }
    } else {
      outputData.push([value, '', '', '']);//間に指定がない行が入るとずれるので空行追加
    }
  }
  return outputData;
}
//ログ、メールなどに流すテキスト
function logAndMessage(text) {
  Logger.log(text);
  TEXT_MESSAGE_ARRAY.push(text);
}
function validateAndGetSpreadsheet() {
  if (SPREAD_SHEET_ID === 'SPREAD_SHEET_ID' || SPREAD_SHEET_ID === '') {
    throw new Error('スプレッドシートIDを設定してください。');
  }
  try {
    return SpreadsheetApp.openById(SPREAD_SHEET_ID);
  } catch (e) {
    throw new Error('スプレッドシートを開くことが出来ませんでした。スプレッドシートIDまたはスプレッドシートの権限が正しいか確認してください。' + e);
  }
}
function validateAndGetSheet(ss, sheetName) {
  if (sheetName === '') {
    throw new Error('シート名を設定してください。');
  }
  const sh = ss.getSheetByName(sheetName);
  if (sh === null) {
    throw new Error('シートが開けませんでした。シート名を確認してください。');
  }
  return sh;
}
function validateAndSendMail() {
  if (FLAG_MAIL) {
    if (MAIL_TO.length < 1 || !MAIL_TO.every(str => typeof str === 'string' && /^[a-zA-Z0-9]+$/.test(str))) {
      throw new Error('Yahoo! JAPANビジネスIDを設定してください。メール送信前までに実行された処理は完了しています。');
    }
    MailApp.sendEmail({
      to: MAIL_TO,
      subject: '【アカウントID:' + accountId + '】' + MAIL_TITLE,
      body: TEXT_MESSAGE_ARRAY.join('\n'),
    });
  }
}
function validateAndSendSlack() {
  if (FLAG_SLACK) {
    if (URL_FETCH_APP === 'SLACK_WEBHOOK_URL' || URL_FETCH_APP === '') {
      throw new Error('SlackのWebhook URLを設定してください。Slack送信前までに実行された処理は完了しています。');
    }
    UrlFetchApp.fetch(URL_FETCH_APP, {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify({
        text: TEXT_MESSAGE_ARRAY.join('\n'),
      }),
    });
  }
}

結果確認

アカウントに、スプレッドシートで指定した条件に合致する変動があった場合に、スプレッドシートに結果が出力されていれば成功です。