0

I'm using a slightly modified version of this code snippet to get the last login time from a large number of users on a G Suite domain. Due to the large number of users, this code can't finish running in under 30 minutes and hits the execution time limit of 30 minutes and fails. I'm looking for a way to speed up the execution.

The application this runs on combines this data with a similar code snipped that reads values from the enterprise license manager API, which I expect will hit the same issue.

If it's not possible to make the code faster, I at least need to ensure it doesn't hit the execution time limit. I need to call this API for all users at least once for the application to work properly - it sorts a list of users by their last login time. The app is in App Maker using App Script on a G Suite Enterprise domain.

function getParameterValues(parameters) {
  return parameters.reduce(function(result, parameter) {
    var name = parameter.name;
    var value;
    if (parameter.intValue !== undefined) {
      value = parameter.intValue;
    } else if (parameter.stringValue !== undefined) {
      value = parameter.stringValue;
    } else if (parameter.datetimeValue !== undefined) {
      value = new Date(parameter.datetimeValue);
    } else if (parameter.boolValue !== undefined) {
      value = parameter.boolValue;
    }
    result[name] = value;
    return result;
  }, {});
}

function generateLoginActivityReport() {
  var today = new Date();
  var oneWeekAgo = new Date(today.getTime() - (7 * 24 * 60 * 60 * 1000));
  var timezone = Session.getScriptTimeZone();
  var date = Utilities.formatDate(oneWeekAgo, timezone, 'yyyy-MM-dd');

  var parameters = [
    'accounts:last_login_time',
    'drive:num_items_created'
  ];
  var rows = [];
  var pageToken;
  var page;
    do {
    page = AdminReports.UserUsageReport.get('all', date, {
      parameters: parameters.join(','),
      maxResults: 500,
      pageToken: pageToken,
    });

    var reports = page.usageReports;

    if (reports) {
      for (var i = 0; i < reports.length; i++) { 
        var report = reports[i];
        try {
          var parameterValues = getParameterValues(report.parameters);
          var row = [
            report.date,
            report.entity.userEmail,
            parameterValues['accounts:last_login_time'],
            //parameterValues['drive:num_items_created']
          ];
          rows.push(row);
          //var ar = app.models.ActivityReport.newRecord();
          //ar.LastLogin = parameterValues['accounts:last_login_time'];
          console.log(report.entity.userEmail);
          //ar.DocsAdded = 0; //getting this value is another issue but unrelated so it's set to 0 for now.
          //ar.Email = report.entity.userEmail.toString();
          //app.saveRecords([ar]);
        }
        catch(error) {
          console.error("Error: \n"+error);
        }
      }
    }
  } while (pageToken);
  }

And here's a sample execution:

[19-07-15 15:58:30:784 CDT] Starting execution
[19-07-15 15:58:30:796 CDT] Session.getScriptTimeZone() [0 seconds]
[19-07-15 15:58:30:797 CDT] Utilities.formatDate([Mon Jul 08 13:58:30 PDT 2019, America/Mexico_City, yyyy-MM-dd]) [0 seconds]
[19-07-15 15:58:32:202 CDT] console.log([user1@test.mavenwave.com, []]) [0.003 seconds]
[19-07-15 15:58:32:203 CDT] console.log([ad.test.admin@test.mavenwave.com, []]) [0 seconds]
[19-07-15 15:58:32:204 CDT] console.log([user3@test.mavenwave.com, []]) [0 seconds]
///more entries, roughly 195 total
[19-07-15 15:58:32:441 CDT] console.log([user4@test.mavenwave.com, []]) [0 seconds]
[19-07-15 15:58:32:441 CDT] console.log([user5@test.mavenwave.com, []]) [0 seconds]
[19-07-15 15:58:32:443 CDT] Execution succeeded [1.645 seconds total runtime]


Ian Hyzy
  • 481
  • 5
  • 26
  • Include your execution transcript so we can see what's taking the most time, this can be found in `View > Execution transcript`. – ross Jul 15 '19 at 20:51
  • @ross I updated the question with a sample of the execution log and I made sure the code can run standalone. I also have to note that the commented out section are App Maker specific functions that write to the database, but this log doesn't appear to be available in AppMaker. – Ian Hyzy Jul 15 '19 at 21:01
  • Even if you improve the execution time speed, if the number of users continue to increase, it will soon or later hit the same issue. Instead, you should consider using the solution described here https://stackoverflow.com/a/8608327/5983596. I use this solution and I'm good with it. – Morfinismo Jul 15 '19 at 21:17
  • I saw that post in my searching, but I wasn't able to figure out how to use the Activity Reports to get the right users each loop. You can get one user or all but not a list, and even if I could give it a list, I'm not sure how I would manage that. – Ian Hyzy Jul 15 '19 at 21:34
  • @IanHyzy How many users do you have in your domain? – Rafa Guillermo Jul 16 '19 at 08:54
  • @IanHyzy Rather than storing the list of users in a sheet could you no just store a pageToken and then on the next script execution start at the next page? – Rafa Guillermo Jul 16 '19 at 09:02
  • @RafaGuillermo There's tens of thousands. They aren't in a sheet, it's in an AppMaker SQL database. – Ian Hyzy Jul 17 '19 at 14:16
  • Do you have to do this in Apps Script? If you make the API calls in a standalone application rather than running a GAS project on the cloud you won't have the same execution time limits to worry about. – Rafa Guillermo Jul 22 '19 at 06:34
  • Hi @RafaGuillermo it's not a hard requirement but I was hoping to avoid building out all the auth and other things that you need when not using GAS - I would also still need to call it from GAS and it would be a lot of extra work. – Ian Hyzy Jul 22 '19 at 20:43

2 Answers2

0

Instead of getting the last login using the Reports API, would it be faster to just get it directly from a list of all user records? Use AdminDirectory.Users.list and retrieve the user.lastLoginTime property?

If you still need the number of Drive items (that you have commented out above), you might have to run that through a separate function, but you could do it based on some other criteria or starting with the most recently logged in users first.

Lynn Hoffman
  • 200
  • 11
  • I thought about doing this (getting the user list first, then getting login time on a per-page basis) but I'm also still working out how best to get the `lastLoginTime` for a given list of users, rather than one user or everyone. – Ian Hyzy Jul 17 '19 at 14:16
  • Even just pulling a list of all users in the domain exceeds the 30 minute execution time. – Ian Hyzy Jul 17 '19 at 20:45
  • There's a lot of users, yes. Unfortunately Google support will bounce tickets back regarding App Script unless there's direct evidence of a bug. This doesn't really qualify as a bug, unfortunately. – Ian Hyzy Jul 18 '19 at 17:03
  • If your accounts come through a G Suite reseller, they may be able to push it through on your behalf. Good luck! – Lynn Hoffman Jul 19 '19 at 19:39
0

I got around this by using App Maker's client scripting. The client script calls the server script, which gets 100 users' reports and then passes the page token back to the client script, which calls it again using the page token- this gets around the 30 minute time limit and continues the call in order.

function crunLoginActivityReport(page) {
  // this loops over every page token and gets the data and writes it to the DB.
  console.log("Running activity report for page: " + page);
  google.script.run.withSuccessHandler(function(result) {
    console.log("Got the following from the server, handing back to client runner" + result);
    if (result === null) {
      console.log("Result was null, stopping get");
      return; 
    } else {
    crunLoginActivityReport(result); }
  }).runLoginActivityReport(page);
  return;
}

function cinitLoginActivityReport() {
  google.script.run.withSuccessHandler(function(result) {
    crunLoginActivityReport(result);
    return;
  }).initLoginActivityReport();
  return;
}
Ian Hyzy
  • 481
  • 5
  • 26