Skip to content

Instantly share code, notes, and snippets.

@elifkus
Last active February 25, 2021 12:32
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save elifkus/09cd63b3cfbf4e070ecc83b4a4358eaa to your computer and use it in GitHub Desktop.
Save elifkus/09cd63b3cfbf4e070ecc83b4a4358eaa to your computer and use it in GitHub Desktop.
Google Apps Script to retrieve data from Strava into a Spreadsheet.
/**
* There is a write-up of how to get this code to run. https://elifk.us/en/retrieving-your-strava-data-with-google-app-scripts/
*/
var CLIENT_ID = '<ClientId for the Strava App>';
var CLIENT_SECRET = '<Client Secret for the Strava App>';
var SPREADSHEET_NAME = "StravaData";
var SPREADSHEET_ID = "<Spreadsheet id for the Google Spreadsheet>";
var SHEET_NAME = "Sheet1";
var DEBUG = false;
//If you want to retrieve details such as 'description' you need to make the value of RETRIEVE_DETAILS = true;
var RETRIEVE_DETAILS = false;
var ONLY_WITH_HEARTRATE = true;
//If you want to retrieves only one type of activity, you should write the activity type below. Ex: var FILTER_BY_TYPE = "Ride";
var FILTER_BY_TYPE = "";
//Strava returns a lot of data. You can choose which fields you want to be returned.
//The field names should match the field names returned from getActivities method,
//which you can check http://developers.strava.com/docs/reference/#api-Activities-getLoggedInAthleteActivities on the right
//If you set RETRIEVE_DETAILS to true, you can specify the fields returned
//from http://developers.strava.com/docs/reference/#api-Activities-getActivityById as well. Again check the right part of the page
var DATA_FIELDS = ['start_date_local', 'max_heartrate', 'average_heartrate'];
//This is the sheet heading for the fields above. The number of items should match with DATA_FIELDS
var HEADING_FOR_DATA_FIELDS = ['Date', 'MaxHeartRate', 'AvgHeartRate'];
var HEADING_FOR_DATA_FIELDS = ['Date', 'MaxHeartRate', 'AvgHeartRate'];
//Strava changed its authentication mechanism. The old authentication method will stop working on Oct 15th, 2019
//for old oauth use "view_private", for new one use "activity:read_all" These include reading your private activities.
//If you want to use a different scope or several scopes check https://developers.strava.com/docs/oauth-updates/
//For new scopes, additional scopes can be added with a comma inbetween
//var STRAVA_API_SCOPE = "view_private";
var STRAVA_API_SCOPE = "activity:read_all";
/**
* Configures the service.
*/
function getService() {
return OAuth2.createService('Strava')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://www.strava.com/oauth/authorize')
.setTokenUrl('https://www.strava.com/oauth/token')
// Set the client ID and secret.
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
// Set the name of the callback function that should be invoked to complete
// the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
//Include private activities when retrieving activities.
.setScope(STRAVA_API_SCOPE)
}
/**
* Handles the OAuth callback.
*/
function authCallback(request) {
var service = getService();
var authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied');
}
}
function logAccessToken() {
var service = getService();
Logger.log("Access token: "+ service.getAccessToken());
}
/**
* Reset the authorization state, so that it can be re-tested.
*/
function reset() {
var service = getService();
service.reset();
}
/**
* Authorizes and makes a request to the API.
*/
function run() {
var service = getService();
if (service.hasAccess()) {
var url = 'https://www.strava.com/api/v3/athlete';
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
function retrieveData() {
//validate input
validateConfig()
//if sheet is empty retrieve all data
var service = getService();
if (service.hasAccess()) {
var sheet = getStravaSheet();
var unixTime = retrieveLastDate(sheet);
var url = 'https://www.strava.com/api/v3/athlete/activities?per_page=100&after=' + unixTime;
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
if (result.length == 0) {
Logger.log("No new data");
return;
}
if (RETRIEVE_DETAILS) {
retrieveAndInsertActivityDetailsForActivityList(result);
}
var data = convertData(result);
if (data.length == 0) {
Logger.log("No new data with the used filters (ONLY_WITH_HEARTRATE, FILTER_BY_TYPE)");
return;
}
insertData(sheet, data);
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
function validateConfig() {
if (DATA_FIELDS.length != HEADING_FOR_DATA_FIELDS.length) {
Logger.log("The length of DATA_FIELDS does not match with the length of HEADING_FOR_DATA_FIELDS. ");
Logger.log(DATA_FIELDS.length.toString()+" != "+HEADING_FOR_DATA_FIELDS.length.toString() + " Please fix.");
}
}
function retrieveAndInsertActivityDetailsForActivityList(activityListResult) {
var service = getService();
if (service.hasAccess()) {
var url = 'https://www.strava.com/api/v3/activities/';
var query = '?include_all_efforts=false';
for (var i = 0; i < activityListResult.length; i++) {
var activityURL = url + activityListResult[i]['id'] + query;
var response = UrlFetchApp.fetch(activityURL, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
extendObj(activityListResult[i], result);
}
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
function extendObj(obj1, obj2){
for (var key in obj2){
if(!obj1.hasOwnProperty(key)){
obj1[key] = obj2[key];
}
}
}
function retrieveLastDate(sheet) {
var lastRow = sheet.getLastRow();
var unixTime = 0;
if (lastRow > 0) {
var dateCell = sheet.getRange(lastRow, 1);
var dateString = dateCell.getValue();
var date = new Date((dateString || "").replace(/-/g,"/").replace(/[TZ]/g," "));
unixTime = date/1000;
}
return unixTime;
}
function convertData(result) {
var data = [];
for (var i = 0; i < result.length; i++) {
if ((!ONLY_WITH_HEARTRATE || result[i]["has_heartrate"]) && (FILTER_BY_TYPE.length == 0 || result[i]["type"] == FILTER_BY_TYPE)) {
var item = [];
for (var j = 0; j < DATA_FIELDS.length; j++) {
var field_name = DATA_FIELDS[j];
var single_data = result[i][field_name];
//distance is returned in meters, that's why I divide it into 1000 to have kms
if (field_name == 'distance') {
single_data = single_data/1000;
}
// if the data does not exist undefined is returned,
// I replace undefined with '-'
if (single_data == undefined) {
single_data = '-'
}
item.push(single_data);
}
data.push(item);
}
}
return data;
}
function getStravaSheet() {
var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = getOrCreateSheet(spreadsheet, SHEET_NAME);
return sheet;
}
function insertData(sheet, data) {
var header = HEADING_FOR_DATA_FIELDS;
ensureHeader(header, sheet);
var lastRow = sheet.getLastRow();
var range = sheet.getRange(lastRow+1,1,data.length,DATA_FIELDS.length);
range.setValues(data);
}
function ensureHeader(header, sheet) {
// Only add the header if sheet is empty
if (sheet.getLastRow() == 0) {
if (DEBUG) {
Logger.log('Sheet is empty, adding header.');
}
sheet.appendRow(header);
return true;
} else {
if (DEBUG) {
Logger.log('Sheet is not empty, not adding header.')
}
return false;
}
}
function getOrCreateSheet(spreadsheet, sheetName) {
var sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
if (DEBUG) Logger.log('Sheet "%s" does not exist, adding new one.', sheetName);
sheet = spreadsheet.insertSheet(sheetName)
}
return sheet;
}
@jorgeforero
Copy link

Hello Elif.

I'm trying to use this code in order to get info about my activities on Strava but I'm getting the error:
{"message":"Bad Request","errors":[{"resource":"Application","field":"redirect_uri","code":"invalid"}]}

The authorization callback Domain registered in Strava is:
https%3A%2F%2Fscript.google.com%2Fmacros%2Fd%2F1LjT4euzPxusz47ZqWZJfLUQkcrYzcsT4NyW7dS-WvXf6941ZmrK3AGin%2Fusercallback

The full error I'm getting is:
Request failed for https://www.strava.com/oauth/authorize?client_id=XXXXX&response_type=code&redirect_uri=https%3A%2F%2Fscript.google.com%2Fmacros%2Fd%2F1LjT4euzPxusz47ZqWZJfLUQkcrYzcsT4NyW7dS-WvXf6941ZmrK3AGin%2Fusercallback&scope=view_private&approval_prompt=auto returned code 400. Truncated server response: {"message":"Bad Request","errors":[{"resource":"Application","field":"redirect_uri","code":"invalid"}]} (use muteHttpExceptions option to examine full response) (line 28, file "Code")

where XXXX is my Client ID.

Could you help me to identify what I'm doing wrong? Any ideas?
Thank in advance,
Jorge.

@jorgeforero
Copy link

Hello Elif.
Finally I found my error. When I started to search about Strava + GAS, I found your code first. Days after, I found your post named "Retrieving your Strava Data..." and I understood the whole process in a better way. The solution: I replaced the redirect_uri with script.google.com and it worked!. Thanks. Pd: Maybe you could add the post's url in the code to obtain more explanation.
Jorge.

@elifkus
Copy link
Author

elifkus commented May 10, 2019

Hi Jorge,
I just saw your comments. I'm glad you could fix the problem.
I added a link to the post, as you suggested.
Thank you

@drew-wks
Copy link

Hi Elif,

Would you be willing to provide info on how to migrate the script now that Strava has only supports short-lived tokens?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment