0

I am trying to export my Pipedrive data to a Google Sheet, in particular to make the link between two of my queries. So I first wrote this script:

function GetPipedriveDeals2() {
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheets = ss.getSheets();
  let sheet = ss.getActiveSheet();

   //the way the url is build next step is to iterate between the end because api only allows a fixed number of calls (100) this way i can slowly fill the sheet.
  let url    = "https://laptop.pipedrive.com/v1/products:(id)?start=";
  let limit  = "&limit=500";
  //let filter = "&filter_id=64";
  let pipeline = 1; // put a pipeline id specific to your PipeDrive setup 
  let start  = 1;
  //let end  = start+50;
  let token  = "&api_token=XXXXXXXXXXXXXXX";
  let response = UrlFetchApp.fetch(url+start+limit+token); //
  let dataAll = JSON.parse(response.getContentText()); 
  let dataSet = dataAll;
  //let prices = prices;
  //create array where the data should be put
  let rows = [], data;
  for (let i = 0; i < dataSet.data.length; i++) {
  data = dataSet.data[i];
    rows.push([data.id,
               GetPipedriveDeals4(data.id)
               ]);
  }

  Logger.log( 'function2' ,JSON.stringify(rows,null,8) );   // Log transformed data

  return rows;
}

// Standard functions to call the spreadsheet sheet and activesheet
function GetPipedriveDeals4(idNew) {
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheets = ss.getSheets();
  let sheet = ss.getActiveSheet();

   //the way the url is build next step is to iterate between the end because api only allows a fixed number of calls (100) this way i can slowly fill the sheet.
  let url    = "https://laptop.pipedrive.com/v1/products/"+idNew+"/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
  let limit  = "&limit=500";
  //let filter = "&filter_id=64";
  let pipeline = 1; // put a pipeline id specific to your PipeDrive setup 
  let start  = 1;
  //let end  = start+50;
  let token  = "&api_token=XXXXXXXXXXXXXXXXX"
  

  let response = UrlFetchApp.fetch(url+start+limit+token); //
  let dataAll = JSON.parse(response.getContentText()); 
  let dataSet = dataAll;
   //Logger.log(dataSet)
  //let prices = prices;
  //create array where the data should be put
  let rows = [], data;
  if(dataSet.data === null )return
  else {
    for (let i = 0; i < dataSet.data.length; i++) {
      data = dataSet.data[i];
      let idNew = data.id; 
      rows.push([data.id, data['d93b458adf4bf84fefb6dbce477fe77cdf9de675']]);
    }
  
  Logger.log( 'function4', JSON.stringify(rows,null,2) );   // Log transformed data
  return rows;
  }
}

But it is not optimized at all and takes about 60 seconds to run, and google script executes the custom functions only for 30 seconds... With help, I had this second function:

function getPipedriveDeals(apiRequestLimit){
  //Make the initial request to get the ids you need for the details.
  var idsListRequest = "https://laptop.pipedrive.com/v1/products:(id)?start=";
  var start  = 0;
  var limit  = "&limit="+apiRequestLimit;
  var token  = "&api_token=XXXXXXXXXXX";
  var response = UrlFetchApp.fetch(idsListRequest+start+limit+token);
  var data = JSON.parse(response.getContentText()).data;
  
  //For every id in the response, construct a url (the detail url) and push to a list of requests
  var requests = [];
  data.forEach(function(product){
    var productDetailUrl = "https://laptop.pipedrive.com/v1/products/"+product.id+"/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
    requests.push(productDetailUrl+start+limit+token)
  })
  
  //With the list of detail request urls, make one call to UrlFetchApp.fetchAll(requests)
  var allResponses = UrlFetchApp.fetchAll(requests);
 // logger.log(allResponses);
  return allResponses; 
}

But this time it's the opposite. I reach my request limit imposed by Pipedrive: https://pipedrive.readme.io/docs/core-api-concepts-rate-limiting (80 requests in 2 sec).

I confess I have no more idea I thought of putting OAuth2 in my script to increase my query limit, but it seems really long and complicated I'm not at all in my field.

In summary, I would just like to have a script that doesn't execute requests too fast but without exceeding the 30 seconds imposed by Google Apps Script.

---------------------EDIT---TEST---FOREACH80-------------------------------------

 function getPipedriveProducts(){
  //Make the initial request to get the ids you need for the details.
  var idsListRequest = "https://laptop.pipedrive.com/v1/products:(id)?start=";
  var start  = 0;
  var limit  = "&limit=500";
  var token  = "&api_token=XXXXXXXXXXXXXXXXXXX";
  var response = UrlFetchApp.fetch(idsListRequest+start+limit+token);
  var data = JSON.parse(response.getContentText()).data;
  
  //For every id in the response, construct a url (the detail url) and push to a list of requests
   const batch = new Set;
  let requests = [];
  data.forEach(function(product){
    var productDetailUrl = "https://laptop.pipedrive.com/v1/products/" + product.id + "/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
    requests.push(productDetailUrl+start+limit+token);
    if(requests.length === 79) {
      batch.add(requests);
      requests = [];
    }
  })
  const allResponses = [...batch].flatMap(requests => {
    Utilities.sleep(2000);
    return UrlFetchApp.fetchAll(requests);
   Logger.log(allResponses) 
  });
}

enter image description here

allanetu
  • 1
  • 3
  • 1
    When you use `fetchAll`, the requests are issued "concurrently", therefore if you have 80 or more products, you will hit the request limit. There are several standard approaches to this: 1. chunking requests - make 79 requests, `sleep` 2s, repeat for the next 79, etc. 2. [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) - as soon as `fetchAll` fails due to rate-limiting, `sleep` 2s and continue, if not enough - sleep more, and more, and more until you are able to make the request. Re: OAuth - there is a [library for that](https://github.com/gsuitedevs/apps-script-oauth2) – Oleg Valter is with Ukraine Jul 09 '20 at 16:14
  • Honestly, I had a thought about making 79 request and make a sleep but I've waste my entire days trying to do that and also trying to implement Oauth I had already saw the library but to be honest I don't understand it well, could you write a piece of code for the request ? I am sorry to ask that but I don't know how to do it and this work is very important for my small company... – allanetu Jul 09 '20 at 16:47
  • 1
    Why do you want to do this in a custom function? Why not a menu or a button? – TheMaster Jul 09 '20 at 16:58
  • in fact I try to do it with a button the first time with my function who was taking 60sec but when I executed script at the opening of the sheet no data was displayed, I don't know if I was doing something wrong but I didn't find anything about that so I thought it was not possible – allanetu Jul 09 '20 at 17:33
  • 1
    @AlbanM - if you want a specific code piece tailored to your situation, please hire a freelancer that will do it for a modest fee. Although we can offer general solutions that will be useful for other users as well (see TheMaster's answer as the closest to your situation) – Oleg Valter is with Ukraine Jul 09 '20 at 18:15

2 Answers2

1
  • Create Set of 80 requests each

  • Execute each set value using fetchAll

  const batch = new Set;
  let requests = [];
  data.forEach(function(product){
    var productDetailUrl = "https://example.com";
    requests.push(productDetailUrl+start+limit+token);
    if(requests.length === 80) {
      batch.add(requests);
      requests = [];
    }
  })
  const allResponses = [...batch].flatMap(requests => {
    Utilities.sleep(2000);
    return UrlFetchApp.fetchAll(requests);
  });
TheMaster
  • 45,448
  • 6
  • 62
  • 85
  • Thank you for your example, it's helping a lot ! But I just tried it and I don't know why it return nothing, I will put a picture of the log and how I integrate your code with what I had. Thanks again for your time – allanetu Jul 09 '20 at 18:16
  • 1
    @AlbanM you're not logging the responses. Logger.log(allResponses) – TheMaster Jul 10 '20 at 02:58
  • Sorry I just see I didn't write in my previous edit, but I try it and it's seem's to not work at all, even when I try to call the function ```=getPipedriveProducts()``` it return me blank cells.. Thanks for your help again – allanetu Jul 10 '20 at 09:36
  • @AlbanM *it return me blank cells..* because you're not ``return``ing anything from the function. Kindly take a basic js course(eg: w3schools), if you value my time. It'll barely take 5 hours of your time. – TheMaster Jul 10 '20 at 11:31
1

Chunking

One of the most important concepts in working with APIs is chunking as you need to avoid rate-limiting, accommodate request scheduling, parallelize CPU-heavy calculations, etc. There are countless ways to split an array in chunks (see half a hundred answers in this canonical Q&A just for JavaScript).

Here is a small configurable utility tailored to the situation where one wants to split a flat array into an array of arrays of a certain size/pattern (which is usually the case with request chunking):

/**
 * @typedef {object} ChunkifyConfig
 * @property {number} [size]
 * @property {number[]} [limits]
 * 
 * @summary splits an array into chunks
 * @param {any[]} source 
 * @param {ChunkifyConfig}
 * @returns {any[][]}
 */
const chunkify = (source, {
  limits = [],
  size
} = {}) => {

  const output = [];

  if (size) {
    const {
      length
    } = source;

    const maxNumChunks = Math.ceil((length || 1) / size);
    let numChunksLeft = maxNumChunks;

    while (numChunksLeft) {
      const chunksProcessed = maxNumChunks - numChunksLeft;
      const elemsProcessed = chunksProcessed * size;
      output.push(source.slice(elemsProcessed, elemsProcessed + size));
      numChunksLeft--;
    }

    return output;
  }

  const {
    length
  } = limits;

  if (!length) {
    return [Object.assign([], source)];
  }

  let lastSlicedElem = 0;

  limits.forEach((limit, i) => {
    const limitPosition = lastSlicedElem + limit;
    output[i] = source.slice(lastSlicedElem, limitPosition);
    lastSlicedElem = limitPosition;
  });

  const lastChunk = source.slice(lastSlicedElem);
  lastChunk.length && output.push(lastChunk);

  return output;
};

const sourceLimited = [1, 1, 2, 2, 2, 3];
const outputLimited = chunkify(sourceLimited, { limits: [2, 1] });
console.log({ source : sourceLimited, output : outputLimited });

const sourceSized = ["ES5", "ES6", "ES7", "ES8", "ES9"];
const outputSized = chunkify(sourceSized, { size: 2 });
console.log({ source : sourceSized, output : outputSized });

From there, the only thing you need is to traverse the array while waiting for each chunk to complete to make it applicable to your situation. Please beware that requests can fail for any number of reasons - you should persist last successfully processed chunk.