From 427380fa638c767c373757357afd6d46e37c2280 Mon Sep 17 00:00:00 2001 From: ladiko Date: Wed, 20 Jun 2018 11:39:14 +0200 Subject: [PATCH 1/3] Adjustments for FB API v3.0 --- Code.gs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Code.gs b/Code.gs index 213ae29..d9caca4 100644 --- a/Code.gs +++ b/Code.gs @@ -1,6 +1,8 @@ /* Facebook Cost Data Upload in Google Analytics * Description: Uploads Facebook (API v2.11) Cost Data in Google Analytics. * @Ritwikga www.Digishuffle.com + * Updated: 20-06-2018 + * - API v3.0 * * Updated: 23-04-2018 * - Added SideBar @@ -133,7 +135,7 @@ return str function makeRequest(FB_AD_ACCOUNT_ID, FB_FIELDS, DATE_RANGE, start_date, end_date, SOURCE, MEDIUM, pos, limit) { // var fbRequest = getService(); - var requestEndpoint = "https://graph.facebook.com/v2.11/act_"+FB_AD_ACCOUNT_ID+"/insights?" + var requestEndpoint = "https://graph.facebook.com/v3.0/act_"+FB_AD_ACCOUNT_ID+"/insights?" var param = { 'level' : 'ad', 'fields': 'ad_id,'+FB_FIELDS, 'time_increment': '1', @@ -166,7 +168,7 @@ var parseData = JSON.parse(response) } // - var utms_endpoint = "https://graph.facebook.com/v2.11/act_"+FB_AD_ACCOUNT_ID+"/ads?fields=adcreatives%7Burl_tags%7D&limit="+5000 + var utms_endpoint = "https://graph.facebook.com/v3.0/act_"+FB_AD_ACCOUNT_ID+"/ads?fields=adcreatives%7Burl_tags%7D&limit="+5000 var utms_ads = UrlFetchApp.fetch(utms_endpoint, { @@ -332,7 +334,7 @@ function getService() { return OAuth2.createService('Facebook') // Set the endpoint URLs. .setAuthorizationBaseUrl('https://www.facebook.com/dialog/oauth') - .setTokenUrl('https://graph.facebook.com/v2.11/oauth/access_token') + .setTokenUrl('https://graph.facebook.com/v3.0/oauth/access_token') // Set the client ID and secret. .setClientId(CLIENT_ID) @@ -392,7 +394,7 @@ function preDefinedVariables(){ function adAccounts(){ var fbRequest = getService(); - var addaccounts_endpoint = "https://graph.facebook.com/v2.12/me?fields=adaccounts.limit(100)%7Bname,account_id%7D,email" + var addaccounts_endpoint = "https://graph.facebook.com/v3.0/me?fields=adaccounts.limit(100)%7Bname,account_id%7D,email" var adAccountInfo = UrlFetchApp.fetch(addaccounts_endpoint, { From 8337157b08992490bbd22d0fd9f36c469cfd8932 Mon Sep 17 00:00:00 2001 From: ladiko Date: Wed, 18 Jul 2018 17:41:38 +0200 Subject: [PATCH 2/3] Breakdowns added Breakdowns can separate data by country, gender, age or other properties --- Code.gs | 221 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 122 insertions(+), 99 deletions(-) diff --git a/Code.gs b/Code.gs index d9caca4..07e052e 100644 --- a/Code.gs +++ b/Code.gs @@ -1,6 +1,10 @@ -/* Facebook Cost Data Upload in Google Analytics +/* Facebook Cost Data Upload in Google Analytics * Description: Uploads Facebook (API v2.11) Cost Data in Google Analytics. * @Ritwikga www.Digishuffle.com + + * Updated: 18-07-2018 + * - Breakdowns function added to only upload part of the imported data + * * Updated: 20-06-2018 * - API v3.0 * @@ -20,13 +24,13 @@ * Updated: 10-10-2017 * - Facebook UTM Feature Added * - Access to Facebook Ad Level Data Parameters - * - Facebook Export Limit - Full Quota + * - Facebook Export Limit - Full Quota */ ///// Facebook Details /////// -var CLIENT_ID = ''; // Insert App ID +var CLIENT_ID = ''; // Insert App ID var CLIENT_SECRET = ''; // Insert App Secret @@ -34,19 +38,21 @@ var FB_AD_ACCOUNT_ID = '' //Ad Account Id var FB_FIELDS = ''; -// More FB_FIELDS at https://developers.facebook.com/docs/marketing-api/insights/fields/v2.10 +// More FB_FIELDS at https://developers.facebook.com/docs/marketing-api/insights/fields/v3.0 var pos = [2,1] //Spreadsheet Cell Position -var DATE_RANGE=''; //today, yesterday, this_month, last_month, this_quarter, etc +var DATE_RANGE=''; //today, yesterday, this_month, last_month, this_quarter, etc -//More DATE_RANGE at https://developers.facebook.com/docs/marketing-api/insights/parameters/v2.10 (date_preset paramteter) +//More DATE_RANGE at https://developers.facebook.com/docs/marketing-api/insights/parameters/v3.0 (date_preset paramteter) -//// To use below date range, make sure DATE_RANGE='' ///// +//// To use below date range, make sure DATE_RANGE='' ///// var start_date='2017-01-01'; // custom date range var end_date='2017-06-30'; - +// More BREAKDOWNS at https://developers.facebook.com/docs/marketing-api/insights/breakdowns/ +var BREAKDOWN_KEY = ''; // example: country +var BREAKDOWN_VALUE = ''; // example: US //// Facebook Ad URL UTMs values ///// var SOURCE = "facebook" // source, if not specified in Facebook Tracking URL Params utm_source @@ -60,15 +66,15 @@ var limit = 100; //Facebook Graph API Limit per request var ACCOUNT_ID = ""; //Account ID var PROPERTY_ID = ""; //Property ID var DATASET_ID = ""; //Data set upload ID - + //// CurrenyMultiplier //////// -var currenyMultiplier = 1; //Will Multipy 'Spend' Field. Use it for currency conversions. +var currenyMultiplier = 1; //Will Multiply 'Spend' Field. Use it for currency conversions. -//// Emailers //////////// +//// Emailers //////////// var isEmail = false // Will Send Email To Provide Status Of Upload (During Automation) var subject = '' // Enter Subject Line For Email Else It Fallback To "Facebook Data Upload To GA(ACCOUNTID)" @@ -100,8 +106,8 @@ sheet.clear() } function facebookData() -{ makeRequest(FB_AD_ACCOUNT_ID, FB_FIELDS, DATE_RANGE, start_date, end_date, SOURCE, MEDIUM, pos, limit) } - +{ makeRequest(FB_AD_ACCOUNT_ID, FB_FIELDS, DATE_RANGE, BREAKDOWN_KEY, BREAKDOWN_VALUE, start_date, end_date, SOURCE, MEDIUM, pos, limit) } + function uploadDataToGa() { uploadData(ACCOUNT_ID, PROPERTY_ID, DATASET_ID) } @@ -113,10 +119,10 @@ SpreadsheetApp.getUi().createMenu('Cost Data').addSubMenu(SpreadsheetApp.getUi() function fbAuth(){ - var UI=HtmlService.createTemplate("Click To Authorize
Authorized Successfully

} else {?> Not Authorized

}").evaluate() + var UI=HtmlService.createTemplate("Click To Authorize
Authorized Successfully

} else {?> Not Authorized

}").evaluate() SpreadsheetApp.getUi().showModalDialog(UI, "Facebook Authorization") - + } @@ -132,23 +138,25 @@ for (var key in param) { return str } -function makeRequest(FB_AD_ACCOUNT_ID, FB_FIELDS, DATE_RANGE, start_date, end_date, SOURCE, MEDIUM, pos, limit) { +function makeRequest(FB_AD_ACCOUNT_ID, FB_FIELDS, DATE_RANGE, BREAKDOWN_KEY, BREAKDOWN_VALUE, start_date, end_date, SOURCE, MEDIUM, pos, limit) { // - var fbRequest = getService(); + var fbRequest = getService(); var requestEndpoint = "https://graph.facebook.com/v3.0/act_"+FB_AD_ACCOUNT_ID+"/insights?" - var param = { 'level' : 'ad', + var param = { 'level' : 'ad', 'fields': 'ad_id,'+FB_FIELDS, 'time_increment': '1', 'limit' : limit } - + if (BREAKDOWN_KEY != "") { + param['breakdowns'] = BREAKDOWN_KEY; + } if(DATE_RANGE!="") { param['date_preset'] = DATE_RANGE ;} - else if(start_date!=""&&end_date!="") + else if(start_date!=""&&end_date!="") { param['time_range[since]']=start_date;param['time_range[until]']=end_date; dateRangeUsed=false} else { SpreadsheetApp.getUi().alert("Enter Correct Date Range!!")} - - var response = UrlFetchApp.fetch(requestEndpoint + jsonToQuery(param), + + var response = UrlFetchApp.fetch(requestEndpoint + jsonToQuery(param), { headers: { 'Authorization': 'Bearer ' + fbRequest.getAccessToken() @@ -162,38 +170,38 @@ var parseData = JSON.parse(response) if(parseData.hasOwnProperty('error')) { if(parseData.error.hasOwnProperty('error_user_title')) - {SpreadsheetApp.getUi().alert(parseData.error.error_user_title)} - else{SpreadsheetApp.getUi().alert(parseData.error.message)} + {SpreadsheetApp.getUi().alert("ParseError1: " + parseData.error.error_user_title)} + else{SpreadsheetApp.getUi().alert("ParseError2: " + parseData.error.message)} return } - + // var utms_endpoint = "https://graph.facebook.com/v3.0/act_"+FB_AD_ACCOUNT_ID+"/ads?fields=adcreatives%7Burl_tags%7D&limit="+5000 - - var utms_ads = UrlFetchApp.fetch(utms_endpoint, + + var utms_ads = UrlFetchApp.fetch(utms_endpoint, { headers: { 'Authorization': 'Bearer ' + fbRequest.getAccessToken() }, muteHttpExceptions : true - }) + }) var parsed_utms = JSON.parse(utms_ads) if(parsed_utms.hasOwnProperty('error')) { if(parsed_utms.error.hasOwnProperty('error_user_title')) - {SpreadsheetApp.getUi().alert(parsed_utms.error.error_user_title)} - else{SpreadsheetApp.getUi().alert(parsed_utms.error.message)} + {SpreadsheetApp.getUi().alert("ParseError3: " + parsed_utms.error.error_user_title)} + else{SpreadsheetApp.getUi().alert("ParseError4: " + parsed_utms.error.message)} return } - + var parsed_utms_data = nextTokenData(parsed_utms) - + // if(parseData.data.length>0) { - + var parseData = nextTokenData(parseData) var sheet= SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; @@ -203,28 +211,39 @@ var sheet= SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; if(sheet.getLastRow() > 0 && sheet.getLastColumn() > 0) {sheet.getRange(pos[0],pos[1],sheet.getLastRow(),sheet.getLastColumn()).clear();} - -sheet.getRange(pos[0], pos[1], parseData.data.length, Object.keys(parseData.data[0]).length).setValues(parser(parseData,parsed_utms_data,SOURCE,MEDIUM)) -try{ + +var Values = parser(parseData, parsed_utms_data, SOURCE, MEDIUM, BREAKDOWN_KEY, BREAKDOWN_VALUE) +if(Values.length == 0) +{ + // BREAKDOWNS removed all lines, so there is nothing to do + return; +} + +var secondObject = []; +for(var k in Values)secondObject.push(Values[k]); + +mySheet = sheet.getRange(pos[0], pos[1], secondObject.length, secondObject[0].length); +mySheet.setValues(secondObject); +try{ SpreadsheetApp.getUi().alert("REPORTS SUCCESS...!!!"); } catch (e) {return } - + } else {SpreadsheetApp.getUi().alert('No Facebook Data For The Applied Date Range'); return;} }; - + function nextTokenData(parseData) { var fbRequest = getService(); if(parseData.paging.next != undefined) -{ +{ var parsedata_pg = parseData; while (true) { - var response = UrlFetchApp.fetch(parsedata_pg.paging.next, + var response = UrlFetchApp.fetch(parsedata_pg.paging.next, { headers: { 'Authorization': 'Bearer ' + fbRequest.getAccessToken() @@ -232,11 +251,11 @@ if(parseData.paging.next != undefined) muteHttpExceptions : true }) - + parsedata_pg = JSON.parse(response) parseData.data = parseData.data.concat(parsedata_pg.data) - -if(parsedata_pg.paging.next == undefined) + +if(parsedata_pg.paging.next == undefined) { break;} } } @@ -249,17 +268,17 @@ function AdIds(id, parsed_utms_data) var data = parsed_utms_data -for (i in data.data) +for (i in data.data) { - + if (data.data[i].id == id) -{ +{ if (data.data[i].adcreatives.data[0].url_tags != undefined) { -var tags = data.data[i].adcreatives.data[0].url_tags -var ids_obj = {} +var tags = data.data[i].adcreatives.data[0].url_tags +var ids_obj = {} - ids_obj['id']=data.data[i].id + ids_obj['id']=data.data[i].id if (/utm_source=([^&]+)/i.exec(tags) != null) {ids_obj['source'] = /utm_source=([^&]+)/i.exec(tags)[1]} @@ -272,53 +291,57 @@ if (/utm_campaign=([^&]+)/i.exec(tags) != null) if (/utm_content=([^&]+)/i.exec(tags) != null) {ids_obj['content'] = /utm_content=([^&]+)/i.exec(tags)[1]} - + } else { return false } return ids_obj } } } -function parser(parseData,parsed_utms_data,SOURCE,MEDIUM) +function parser(parseData, parsed_utms_data, SOURCE, MEDIUM, BREAKDOWN_KEY, BREAKDOWN_VALUE) { - -var Data=parseData; + +var Data=parseData; var rw=[]; for (var i = 0; i < Data.data.length; i++) { + if (BREAKDOWN_KEY != "" && BREAKDOWN_VALUE != "" && Data.data[i][BREAKDOWN_KEY] != BREAKDOWN_VALUE) { + continue; + } rw[i]=[] var p = {} for (key in Data.data[i]) { - if (key == 'ad_id') { p = AdIds(Data.data[i][key],parsed_utms_data); continue;} - + if (key == 'ad_id') { p = AdIds(Data.data[i][key],parsed_utms_data); continue;} + if (p==undefined) { Logger.log("Ad ID Error"); break; } - if (key == 'campaign_name') - { if (p.campaign != undefined) - {rw[i].push(p.campaign);continue;} + if (key == 'campaign_name') + { if (p.campaign != undefined) + {rw[i].push(p.campaign);continue;} else { rw[i].push(Data.data[i][key].replace(/\,|\'|\"/g,'')); continue; } } - if (key == 'ad_name') - { - if (p.content != undefined) + if (key == 'ad_name') + { + if (p.content != undefined) { rw[i].push(p.content); continue; } else { rw[i].push(Data.data[i][key].replace(/\,|\'|\"/g,'')); continue; } } - + if(key == 'spend') { rw[i].push(Data.data[i][key]*currenyMultiplier); continue; } - + if(key=='date_stop') {continue;} if(key=='date_start') {rw[i].push(Data.data[i][key].toString().split('-').join(''));continue;} - + if (key == BREAKDOWN_KEY) {continue; } + rw[i].push(Data.data[i][key].replace(/\,|\'|\"/g,'')) } if (p.source !=undefined) { rw[i].push( p.source ) } else{ rw[i].push(SOURCE)} - + if (p.medium !=undefined) {rw[i].push( p.medium ) } else{rw[i].push(MEDIUM)} - } + } return rw } @@ -343,11 +366,11 @@ function getService() { // Set the name of the callback function that should be invoked to complete // the OAuth flow. .setCallbackFunction('authCallback') - - + + //Set Scope - .setScope('ads_read') - + .setScope('ads_read') + // Set the property store where authorized tokens should be persisted. .setPropertyStore(PropertiesService.getUserProperties()); @@ -356,7 +379,7 @@ function getService() { function authCallback(request) { var isAuthorized = getService().handleCallback(request); - + if (isAuthorized) { successUI(true) showBar() @@ -393,28 +416,28 @@ function preDefinedVariables(){ function adAccounts(){ - var fbRequest = getService(); - var addaccounts_endpoint = "https://graph.facebook.com/v3.0/me?fields=adaccounts.limit(100)%7Bname,account_id%7D,email" - - var adAccountInfo = UrlFetchApp.fetch(addaccounts_endpoint, + var fbRequest = getService(); + var addaccounts_endpoint = "https://graph.facebook.com/v3.0/me?fields=adaccounts.limit(100)%7Bname,account_id%7D,email" + + var adAccountInfo = UrlFetchApp.fetch(addaccounts_endpoint, { headers: { 'Authorization': 'Bearer ' + fbRequest.getAccessToken() }, muteHttpExceptions : true - }) - + }) + var parsedadAccountInfo = JSON.parse(adAccountInfo) if(parsedadAccountInfo.hasOwnProperty('error') || parsedadAccountInfo.adaccounts == undefined) - {return false} + {return false} else { var adAccountFB = nextTokenData(parsedadAccountInfo.adaccounts,100) var parsed_adurls = parsedadAccountInfo; parsed_adurls['adaccounts'] = adAccountFB - + } - - return { 'facebookAccountData':parsed_adurls.adaccounts.data } + + return { 'facebookAccountData':parsed_adurls.adaccounts.data } } @@ -422,15 +445,15 @@ function adAccounts(){ //// // -//Cost Data Upload Script - http://www.ryanpraski.com/google-analytics-cost-data-import-google-sheets-automated/ +//Cost Data Upload Script - http://www.ryanpraski.com/google-analytics-cost-data-import-google-sheets-automated/ // //// function uploadData(ACCOUNT_ID, PROPERTY_ID, DATASET_ID) { - var accountId = ACCOUNT_ID - var webPropertyId = PROPERTY_ID - var customDataSourceId = DATASET_ID + var accountId = ACCOUNT_ID + var webPropertyId = PROPERTY_ID + var customDataSourceId = DATASET_ID var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); var maxRows = ss.getLastRow(); var maxColumns = ss.getLastColumn(); @@ -440,51 +463,51 @@ function uploadData(ACCOUNT_ID, PROPERTY_ID, DATASET_ID) { } var newData = data.join("\n"); var blobData = Utilities.newBlob(newData, "application/octet-stream", "GA import data"); - + uploadStatus(accountId, webPropertyId, customDataSourceId, blobData) } function uploadStatus(accountId, webPropertyId, customDataSourceId, blobData){ try { - var upload = Analytics.Management.Uploads.uploadData(accountId, webPropertyId, customDataSourceId, blobData); + var upload = Analytics.Management.Uploads.uploadData(accountId, webPropertyId, customDataSourceId, blobData); SpreadsheetApp.getUi().alert("Data Has Been Sent To Google Analytics.!! Checking Errors..."); var uploadId = JSON.parse(upload) - var count = 0 + var count = 0 while(count < 5) {var status =Analytics.Management.Uploads.get(accountId, webPropertyId , customDataSourceId, uploadId.id ) status = JSON.parse(status) if(status['status'] == 'PENDING') {count++;Utilities.sleep(1000)} - else if(status['status'] == 'COMPLETED'){ + else if(status['status'] == 'COMPLETED'){ SpreadsheetApp.getUi().alert("SUCCESS.!! No Errors Found. Data Has Been Successfully Uploaded"); - sendEmail(isEmail,subject,"SUCCESS") + sendEmail(isEmail,subject,"SUCCESS") break; } else if(status['status'] == 'FAILED') { var error = "" for(var j=0;j"+ "

- Property ID: "+PROPERTY_ID} - else{ message = "

Data Import Has Been Failed. Here are some errors


Errors: "+status+"
" + else{ message = "

Data Import Has Been Failed. Here are some errors


Errors: "+status+"
" } MailApp.sendEmail({ 'to':Session.getActiveUser().getEmail(), From 1c59624b011184fa01327212cca0119daaa34bf3 Mon Sep 17 00:00:00 2001 From: ladiko Date: Thu, 19 Jul 2018 16:36:10 +0200 Subject: [PATCH 3/3] Always clear the table In case of 'No Facebook Data For The Applied Date Range', the old data haven't been cleared from the table and possible wrong data would be uploaded to Google Analytics if uploadDataToGa() is called. --- Code.gs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Code.gs b/Code.gs index 07e052e..201ad1b 100644 --- a/Code.gs +++ b/Code.gs @@ -198,10 +198,6 @@ var parseData = JSON.parse(response) var parsed_utms_data = nextTokenData(parsed_utms) // - -if(parseData.data.length>0) -{ - var parseData = nextTokenData(parseData) var sheet= SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; @@ -212,6 +208,9 @@ var sheet= SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; if(sheet.getLastRow() > 0 && sheet.getLastColumn() > 0) {sheet.getRange(pos[0],pos[1],sheet.getLastRow(),sheet.getLastColumn()).clear();} +if(parseData.data.length>0) +{ + var Values = parser(parseData, parsed_utms_data, SOURCE, MEDIUM, BREAKDOWN_KEY, BREAKDOWN_VALUE) if(Values.length == 0) {