diff --git a/Code.gs b/Code.gs index abc1004..ab964e5 100644 --- a/Code.gs +++ b/Code.gs @@ -1,65 +1,113 @@ function getConfig(request) { var service = getService(); - var response = JSON.parse(UrlFetchApp.fetch("https://graph.facebook.com/v2.10/me/accounts", { + + // Constructing the URL for Ad Account Insights API request + var url = 'https://graph.facebook.com/v19.0/me/adaccounts?fields=name,id'; + + // Fetching ad account data from Facebook API + var response = UrlFetchApp.fetch(url, { + method: 'GET', headers: { Authorization: 'Bearer ' + service.getAccessToken() } - })); + }); + + // Parsing the response to extract relevant data + var adAccountData = JSON.parse(response.getContentText()); + + // Creating the config object with options for Ad Account ID var config = { configParams: [ { type: "SELECT_SINGLE", - name: "pageID", - displayName: "Page ID", - helpText: "Please select the Page ID for which you would like to retrieve the Statistics.", + name: "adAccountID", + displayName: "Ad Account ID", + helpText: "Please select the Ad Account ID for which you would like to retrieve the Statistics.", options: [] } ], dateRangeRequired: true }; - response.data.forEach(function(field) { + + // Populating the options for Ad Account ID based on the ad account data + adAccountData.data.forEach(function(account) { config.configParams[0].options.push({ - label: field.name, - value: field.id + label: account.name, + value: account.id }); - }) + }); + + // Return the config object return config; -}; +} var facebookSchema = [ { - name: 'timestamp', - label: 'Timestamp', + name: 'date_start', + label: 'Date start', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } }, { - name: 'timestampWeek', - label: 'Timestamp Week', + name: 'date_end', + label: 'Date end', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } }, { - name: 'timestampMonth', - label: 'Timestamp Month', + name: 'campaign_name', + label: 'Campaign Name', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } }, { - name: 'likes', - label: 'Likes Total', + name: 'clicks', + label: 'Clicks', dataType: 'NUMBER', semantics: { conceptType: 'METRIC' } }, + { + name: 'cpm', + label: 'CPM', + dataType: 'NUMBER', + semantics: { + conceptType: 'METRIC' + } + }, + { + name: 'ctr', + label: 'CTR', + dataType: 'NUMBER', + semantics: { + conceptType: 'METRIC' + } + }, + { + name: 'cpc', + label: 'CPC', + dataType: 'NUMBER', + semantics: { + conceptType: 'METRIC' + } + }, + { + name: 'frequency', + label: 'FREQUENCY', + dataType: 'NUMBER', + semantics: { + conceptType: 'METRIC' + } + }, + { name: 'impressions_daily', label: 'Impressions', @@ -69,149 +117,146 @@ var facebookSchema = [ } }, { - name: 'engagements_daily', - label: 'Page Post Engagements', + name: 'purchases', + label: 'Purchase', + dataType: 'NUMBER', + semantics: { + conceptType: 'METRIC' + } + },{ + name: 'spend', + label: 'Spend', dataType: 'NUMBER', semantics: { conceptType: 'METRIC' } } + ]; + function getSchema(request) { return {schema: facebookSchema}; }; + function getData(request) { var service = getService(); - + + // Helper function to adjust dates function dateDelta(dObj, num) { - if (isNaN(num)) { - var dateStart = new Date(dObj); - } else { - var dateStart = new Date(dObj); - var dateStart = new Date(dateStart.setDate(dateStart.getDate() + num)); + var dateStart = new Date(dObj); + if (!isNaN(num)) { + dateStart.setDate(dateStart.getDate() + num); } var dd = dateStart.getDate(); - var mm = dateStart.getMonth()+1; //January is 0! - + var mm = dateStart.getMonth() + 1; // January is 0 var yyyy = dateStart.getFullYear(); - if(dd<10){ - dd='0'+dd; - } - if(mm<10){ - mm='0'+mm; - } - var dateStart = yyyy + "-" + mm + "-" + dd; - return dateStart; - } - - var gStartDate = new Date(request.dateRange.startDate); - var gStartDate = new Date(dateDelta(gStartDate, -1)); - var gEndDate = new Date(request.dateRange.endDate); - var gEndDate = new Date(dateDelta(gEndDate, +1)); - var gRange = Math.ceil(Math.abs(gEndDate - gStartDate) / (1000 * 3600 * 24)); - var gBatches = Math.ceil(gRange / 92); - - if (gBatches < 2) { - var batch = [{"method": "GET", "relative_url": request.configParams.pageID + "/insights/page_fans,page_impressions,page_post_engagements?since=" + dateDelta(gStartDate) + "&until=" + dateDelta(gEndDate)}]; - //console.log(batch); - } else { - batch = []; - var iterRanges = gRange / gBatches; - - for (i = 0; i < gBatches; i++) { - var iterStart = dateDelta(gStartDate, (iterRanges * i)); - if (i == (gBatches - 1)) { - var iterEnd = dateDelta(gEndDate); - } else { - var iterEnd = dateDelta(gStartDate, (iterRanges * (i + 1)) + 1); - } - batch.push({"method": "GET", "relative_url": request.configParams.pageID + "/insights/page_fans,page_impressions,page_post_engagements?since=" + iterStart + "&until=" + iterEnd}) - } - //console.log(batch); + return yyyy + "-" + (mm < 10 ? '0' + mm : mm) + "-" + (dd < 10 ? '0' + dd : dd); } - - // Fetch the data with UrlFetchApp - var url = "https://graph.facebook.com?include_headers=false&batch=" + encodeURIComponent(JSON.stringify(batch)) - - var response = JSON.parse(UrlFetchApp.fetch(url, { - method: 'POST', - headers: { - Authorization: 'Bearer ' + service.getAccessToken() - } + + // Encode parameters + var actionAttributionWindows = encodeURIComponent(JSON.stringify(['7d_click'])); //change attribution parameter here. If you prefer standard meta attribution use ['7d_click','1d_view'] + var fields = encodeURIComponent('impressions,ctr,cpc,frequency,cpm,clicks,spend,campaign_id,campaign_name,adset_name,action_values'); + var filtering = encodeURIComponent(JSON.stringify([{ field: "action_type", operator: "IN", value: ["purchase"] }])); + var timeRange = encodeURIComponent(JSON.stringify({ + since: dateDelta(request.dateRange.startDate), + until: dateDelta(request.dateRange.endDate) })); - - // Prepare the schema for the fields requested. + + var baseUrl = `https://graph.facebook.com/v21.0/${request.configParams.adAccountID}/insights/`; + var url = `${baseUrl}?action_attribution_windows=${actionAttributionWindows}&level=adset&fields=${fields}&filtering=${filtering}&time_range=${timeRange}&limit=5000`; + var dataSchema = []; + var data = []; + + // Populate the schema based on requested fields request.fields.forEach(function(field) { - for (var i = 0; i < facebookSchema.length; i++) { - if (facebookSchema[i].name === field.name) { - dataSchema.push(facebookSchema[i]); - break; + facebookSchema.forEach(function(schemaField) { + if (schemaField.name === field.name) { + dataSchema.push(schemaField); } - } + }); }); - var data = []; - - // Prepare the tabular - // console.log("Response: %s", response); -// response.data[0].values.forEach(function(day, i) { - response.forEach(function(resp) { - var resp = JSON.parse(resp.body); - resp.data[0].values.forEach(function(day, i){ - var values = []; - dataSchema.forEach(function(field) { - switch(field.name) { - case 'timestamp': - var fbTime = day.end_time; - var myTime = fbTime.substring(0,4) + fbTime.substring(5,7) + fbTime.substring(8,10); - values.push(myTime); - break; - case 'timestampWeek': - var myTime = new Date(day.end_time); - var startTime = new Date(myTime.getFullYear(), 00, 01); - var deltaTime = Math.abs(myTime - startTime); - var weekTime = ("0" + Math.ceil((deltaTime / (1000 * 3600 * 24)) / 7)).slice(-2); - values.push(myTime.getFullYear() + weekTime); - break; - case 'timestampMonth': - var fbTime = day.end_time; - var myTime = fbTime.substring(0,4) + fbTime.substring(5,7); - values.push(myTime); - break; - case 'likes': - values.push(day.value); - break; - case 'impressions_daily': - values.push(resp.data[1].values[i].value); - break; - case 'engagements_daily': - values.push(resp.data[4].values[i].value); - break; - default: - values.push(''); - } - }); - data.push({ - values: values - }); + + // Loop to handle pagination + while (url) { + console.info("Fetching data from URL: ", url); // Log current request URL + var response = UrlFetchApp.fetch(url, { + method: 'GET', + headers: { + Authorization: 'Bearer ' + service.getAccessToken() + } + }); + + var jsonResponse = JSON.parse(response.getContentText()); + console.info("API Response: ", JSON.stringify(jsonResponse)); // Log API response + + // Process data from the current page + jsonResponse.data.forEach(function(entry) { + var values = []; + dataSchema.forEach(function(field) { + switch (field.name) { + case 'date_start': + values.push(entry.date_start); + break; + case 'date_end': + values.push(entry.date_stop); + break; + case 'campaign_name': + values.push(entry.campaign_name); + break; + case 'clicks': + values.push(parseInt(entry.clicks)); + break; + case 'cpm': + values.push(parseFloat(entry.cpm)); + break; + case 'ctr': + values.push(parseFloat(entry.ctr)); + break; + case 'cpc': + values.push(parseFloat(entry.cpc)); + break; + case 'frequency': + values.push(parseFloat(entry.frequency)); + break; + case 'impressions_daily': + values.push(parseInt(entry.impressions)); + break; + case 'spend': + values.push(parseFloat(entry.spend)); + break; + case 'purchases': + var purchaseValue = entry.action_values?.find(a => a.action_type === 'purchase')?.['7d_click'] || ''; // change to (a => a.action_type === 'purchase')?.value || ''; for standard 7d_click, 1d_view attribution + values.push(parseFloat(purchaseValue) || ''); + break; + default: + values.push(''); + } }); + data.push({ values: values }); }); - //console.log("Data Schema: %s", dataSchema); - //console.log("Data: %s", data); + // Log current batch data + console.info("Processed Data Batch: ", JSON.stringify(data)); - - // Return the tabular data for the given request. + // Check for next page + url = jsonResponse.paging?.next || null; + console.info("Next Page URL: ", url); // Log next page URL + } + + // Return the aggregated data + console.info("Final Data Schema: ", JSON.stringify(dataSchema)); + console.info("Final Data Rows: ", JSON.stringify(data)); return { schema: dataSchema, rows: data }; -}; +} function isAdminUser() { - if (Session.getEffectiveUser().getEmail() == "#########@gmail.com") { + if (Session.getEffectiveUser().getEmail() == "xxxxx@xxxxxxx.xxx") { return true; } } diff --git a/Facebook_OAuth2.gs b/Facebook_OAuth2.gs index 27c5d7c..5e06e1f 100644 --- a/Facebook_OAuth2.gs +++ b/Facebook_OAuth2.gs @@ -15,12 +15,21 @@ function runFBAuth(e) { var service = getService(); var html = ''; if (service.hasAccess()) { - var url = 'https://graph.facebook.com/v2.10/me'; + var url = 'https://graph.facebook.com/v19.0/me'; var response = UrlFetchApp.fetch(url, { headers: { 'Authorization': 'Bearer ' + service.getAccessToken() - } + }, + muteHttpExceptions: true // Add this option to examine full response + }); + if (response.getResponseCode() == 401) { + var responseBody = response.getContentText(); + Logger.log('Full response body: ' + responseBody); + // Handle error or troubleshoot based on the response +} else { + // Parse response and map data as usual +} var result = JSON.parse(response.getContentText()); Logger.log(JSON.stringify(result, null, 2)); } else { @@ -33,7 +42,7 @@ function runFBAuth(e) { /** * Reset the authorization state, so that it can be re-tested. */ -function reset() { +function resetAuth() { var service = getService(); service.reset(); } @@ -44,8 +53,8 @@ function reset() { function getService() { return OAuth2.createService('Facebook') // Set the endpoint URLs. - .setAuthorizationBaseUrl('https://www.facebook.com/dialog/oauth?scope=manage_pages,read_insights') - .setTokenUrl('https://graph.facebook.com/v2.10/oauth/access_token') + .setAuthorizationBaseUrl('https://www.facebook.com/dialog/oauth?scope=public_profile,ads_read,read_insights') + .setTokenUrl('https://graph.facebook.com/v19.0/oauth/access_token') // Set the client ID and secret. .setClientId(CLIENT_ID) @@ -72,6 +81,11 @@ function authCallback(request) { } } +function logRedirectUri() { + var service = getService(); + Logger.log(service.getRedirectUri()); +} + function get3PAuthorizationUrls() { var service = getService(); if (service == null) { diff --git a/README.md b/README.md index a675835..97cd054 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ # Facebook_Connector **Google Data Studio Community Connector for Facebook Insights.** +Based on @halsandr Facebook connector. +Works with OAut2. Make sure to grant marketing Api access to your Facebook app as you will need ads_read and read_insights permissions. + +Implemented Api pagination in the latest release. +Updated to work with Insight API 21.0 + +ATTENTION: The script is set so to get purchase values from the 7d_click attribution, ignoring sales coming from 1d_view. +If you wish to get standard 7d_click, 1d_view check the comments in code.gs and change accordingly. + +------------ [![Facebook Connector](https://img.shields.io/github/tag/halsandr/Facebook_Connector.svg)](https://github.com/halsandr/Facebook_Connector) Follow the setup from [here](https://github.com/googlesamples/apps-script-oauth2) to get the OAuth2 working, dont forget to fill in the `CLIENT_ID` and `CLIENT_SECRET` at the top of `Facebook_OAuth2.gs`. diff --git a/appsscript.json b/appsscript.json index d5d7e41..bbf1115 100644 --- a/appsscript.json +++ b/appsscript.json @@ -1,15 +1,19 @@ { - "timeZone": "Europe/London", + "timeZone": "Europe/Rome", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", "dependencies": { - "libraries": [{ - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "22" - }] + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43" + } + ] }, "webapp": { - "access": "MYSELF", - "executeAs": "USER_DEPLOYING" + "executeAs": "USER_DEPLOYING", + "access": "MYSELF" }, "dataStudio": { "name": "Facebook Connector", @@ -19,4 +23,4 @@ "supportUrl": "http://shuttlefi.sh/", "description": "This connector uses the Facebook Graph API to retrieve Analytics from Facebook." } -} \ No newline at end of file +}