Data Analytics

Build a Fitness App using IBM Watson Data Platform and 3rd Party Fitness APIs

Share this post:

Fitness App Steps

The core features comprising Watson Data Platform, Data Science Experience and Data Catalog on IBM Cloud, along with additional embedded AI services, including machine learning and deep learning, are now available in Watson Studio and Watson Knowledge Catalog. Get started for free at https://ibm.co/watsonstudio.

Taking a Stand Against a Sedentary Lifestyle

We all know that inactive lifestyles negatively impact our health. In her recent post, 8 Million Steps and Counting, Jane Rajah discussed how our knowledge of the health risks of desk jobs and a desire to motivate our coworkers inspired our team to build a Fitness App.

It took us just 5 days (and fewer than 2,000 lines of code) to build the app, using services and solutions available on the IBM Cloud. Once it was finished, we shared it with our coworkers across IBM, and today, employees in more than 15 countries are using our app to track daily steps and fitness goals.

In-app dashboards enable us to analyze and visualize the user activity data, including weight loss, overall steps, and leaderboards. We have even incorporated a third-party API to make recommendations (based on user data) about local fitness activities that they may enjoy.

Employees equipped with wearable technology reported an 8.5% increase in productivity and a 3.5% increase in job satisfaction, so we’re excited that our app is helping to keep our coworkers healthy, happy, and on the move!

 

Create Your Own Fitness App

In this post, I’ll share technical details and code samples to help you to create your very own Fitness App solution. If you want to further customize it or add specialized features, you can also go ahead and connect it to other services and APIs (like we did with the location mapping API).

Before we begin, we need to make sure your environment is set up and that you have the proper tools/keys. (But if you want to jump straight in, you can view the code on GitHub.)

 

Basic Fitness App Architecture

Fitness App Architecture

 

Let’s Get Started

Here are the services and solutions you’ll need to provision before we can begin:

 

Step 1: Bluemix

Create a Bluemix trial account.

The other services used to create the app will all be provisioned and managed within the Bluemix environment. We recommend that you use the standard plan to see ultimate performance benefits.

 

Step 2: Node.js

Once logged into your Bluemix account, provision the Node.js Cloud Foundry app.

This is how we’ll host your app.

 

Step 3: Cloudant

Next, provision the IBM Cloudant NoSQL database.

Cloudant will be used to store persistent data, such as user steps and relative weight.

 

Step 4: Data Science Experience

Then, provision the Data Science Experience (DSX).

DSX will analyze stored datasets and display the data in a graph format.

 

Step 5: Fitbit

Register your Fitbit application.

 

Setup your .env file

CLOUDANT_ACCOUNT=YOUR_CLOUDANT_URL
CLOUDANT_API_KEY=YOUR_CLOUDANT_API_KEY
CLOUDANT_PASSWORD=YOUR_CLOUDANT_PASSWORD
FITBIT_CLIENT_ID=YOUR_FITBIT_CLIENT_ID
FITBIT_CLIENT_SECRET=YOUR_FITBIT_CLIENT_SECRET
FITBIT_VERIFICATION_CODE=YOUR_FITBIT_SUBSCRIPTION_VERIFICATION_CODE

Register Fitbit Users

In order for us to pull data from Fitbit, we first need to setup a callback endpoint for users to register with our application and a subscription to notify when users data needs to be updated. You can view the Fitbit’s documentation at Fitbit OAuth. Once you’ve created your SDK for Node.js app, you should have a FQDN from Bluemix. (https://<unique_hostname>.mybluemix.net).

Next, we will need to create an express server with an endpoint to handle the OAuth callback from Fitbit and subscription notifications.

app.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import express from "express";
import bodyParser from "body-parser";
import path from "path";
import cfenv from "cfenv";
import { logger } from "./logger";

import home from "./controllers/home";
import fitbit from "./controllers/fitbit";
import registered from "./controllers/registered";

let app = new express();

// Middleware to handle application/json and applicat/x-www-form-urlencoded
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.set("views", path.join(__dirname, "public"));

app.set("view engine","ejs");

app.use(express.static(__dirname + "/public"));
// Middleware routes
app.use("/", home);
app.use("/fitbit", fitbit);
app.use("/registered", registered);

const appEnv = cfenv.getAppEnv();
const PORT = appEnv.port || 3000;


// start server on the specified port and binding host
app.listen(PORT, "0.0.0.0", function() {
    logger.log("info","server starting on " + appEnv.url);
});

 

Add Persistence Using Cloudant

When a user registers with our application, we want to make sure we are persisting their access_token and refresh_token so we can pull the user’s data later on. To do this, we will use Cloudant on IBM Bluemix. But before we can write code to insert data into Cloudant, we need to create the databases.

Launch the Cloudant dashboard and create a weight, steps, and users database. (If you are unsure on how to create a database, you can view the documentation here.) You will also want to create an index to optimize query lookup, which can also be found in the documentation. We will now create a wrapper around the Cloudant client to handle persisting steps, weights, and user’s data into our Cloudant database.

cloudant.js

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    import cloudant from "cloudant";
    import Promise from "bluebird";

    export default class Cloudant {
        /**
         * Creates a cloudant db connection.
         * @constructor
        */
        constructor() {
            this.cloudant = new cloudant({ account: process.env.CLOUDANT_ACCOUNT, key: process.env.CLOUDANT_API_KEY, password: process.env.CLOUDANT_PASSWORD });
            this.mass_db = this.cloudant.db.use("weight");
            this.user_db = this.cloudant.db.use("users");
            this.steps_db = this.cloudant.db.use("steps");
        }

        /**
         * Returns all users registered.
         * @returns {Array} All users registered in the db.
         */
        getAllUsers() {
            return new Promise((resolve,reject) => {
                this.user_db.list({ include_docs:true }, (err, data) => {
                    if(err) reject(err);

                    resolve(data.rows);
                });
            });
        }

        /**
         * Register user into database.
         * @param {Object} doc A formatted doc for cloudant to insert a user
         * @returns {Object} Response from cloudant.
         */
        addUser(doc) {
            return new Promise((resolve,reject) => {
                this.user_db.insert(doc, (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }

        /**
         * Retrieves a user from the database.
         * @param {Object} query A formatted query for cloudant to retrieve a user.
         * @returns {Object} Response from cloudant.
         */
        getUser(query) {
            return new Promise((resolve,reject) => {
                this.user_db.find(query, (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }

        /**
         * Retrieves a user from the database.
         * @param {Object} user A user that you want to delete from user's db
         * @returns {Object} Response from cloudant.
         */
        deleteUser(user) {
            return new Promise((resolve,reject) => {
                this.user_db.destroy(user["_id"], user["_rev"], (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }


        /**
         * Insert steps into db.
         * @param {Object} doc A formatted doc for cloudant to insert steps for a given user
         * @returns {Object} Response from cloudant.
         */
        insertSteps(doc) {
            return new Promise((resolve,reject) => {
                this.steps_db.insert(doc, (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }

        /**
         * Delete steps from steps db.
         * @param {Object} doc A formatted doc for cloudant to insert steps for a given user
         * @returns {Object} Response from cloudant.
         */
        deleteSteps(doc) {
            return new Promise((resolve,reject) => {
                this.steps_db.destroy(doc["_id"], doc["_rev"], (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }

        /**
         * Insert steps in bulk into db.
         * @param {Array} docs An array of formatted doc for cloudant to insert steps for a given user
         * @returns {Object} Response from cloudant.
         */
        insertBulkSteps(docs) {
            return new Promise((resolve,reject) => {
                this.steps_db.bulk({ docs }, (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }
        /**
         * Insert weight in bulk into db.
         * @param {Array} docs An array of formatted doc for cloudant to insert weight for a given user
         * @returns {Object} Response from cloudant.
         */
        insertBulkWeight(docs) {
            return new Promise((resolve,reject) => {
                this.mass_db.bulk({ docs }, (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }


        /**
         * Returns steps for a given user.
         * @param {Object} query A formatted query for cloudant to get steps for a given user
         * @returns {Array} Returns all steps for a given user.
         */
        getSteps(query) {
            return new Promise((resolve,reject) => {
                this.steps_db.find(query, (err, data) => {
                    if(err) reject(err);
                    data = (data ? data.docs : []);
                    resolve(data);
                });
            });
        }

        /**
         * Insert weight into db.
         * @param {Object} doc A formatted doc for cloudant to insert weight for a given user
         * @returns {Object} Response from cloudant.
         */
        insertMass(doc) {
            return new Promise((resolve,reject) => {
                this.mass_db.insert(doc, (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }

        /**
         * Returns weight for a given user.
         * @param {Object} query A formatted query for cloudant to get weight for a given user
         * @returns {Array} Returns all weight for a given user.
         */
        getMass(query) {
            return new Promise((resolve,reject) => {
                this.mass_db.find(query, (err, data) => {
                    if(err) reject(err);

                    data = (data ? data.docs : []);
                    resolve(data);
                });
            });
        }

    /**
         * Delete weight doc from database
         * @param {Object} doc A formatted doc for cloudant to insert steps for a given user
         * @returns {Object} Response from cloudant.
         */
        deleteMass(doc) {
            return new Promise((resolve,reject) => {
                this.mass_db.destroy(doc["_id"], doc["_rev"], (err, data) => {
                    if(err) reject(err);

                    resolve(data);
                });
            });
        }
    }

 

Pull the Data

Next, we need to identify the type of data we want to gather. Fitbit’s API documentation showcases the different types of data that are available to you. For now, we will be collecting steps, weight and profile information. Now that we have our Cloudant library created we can start calling Fitbit’s API for the user’s steps and mass. Once we get that data, we can process the data and leverage the wrapper we created above to persist the data into Cloudant.

fitbit.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import Cloudant from "./cloudant.js";
import request from "request-promise";
import async from "async";
import { logger } from "./logger";

const MAX_RETRY = 5;
const FITBIT_CLIENT_ID = process.env.FITBIT_CLIENT_ID;
const FITBIT_CLIENT_SECRET = process.env.FITBIT_CLIENT_SECRET;
const FITBIT_CLIENT_SECRET_64 = new Buffer(`${FITBIT_CLIENT_ID}:${FITBIT_CLIENT_SECRET}`).toString("base64");
const db = new Cloudant();
const rp = request.defaults({
    resolveWithFullResponse: true, // A boolean to set whether the promise should be resolved with the full response or just the response body.
    simple: false, // A boolean to set whether status codes other than 2xx should also reject the promise.
    transform: (body, response, resolveWithFullResponse) =>  { // eslint-disable-line
        return  { statusCode: response.statusCode, headers: response.headers , data: JSON.parse(body) } ;
    }
});

/**
 * Represents a FitBit
 */
export default class FitBit {

    /**
     * Get user's steps for the day.
     * @param {string} task The task that fitbit subscription provides.
     * @param {string} user The user's doc from cloudant.
     * @returns {Object} The daily steps for the requested user.
     */
    async getSteps(task, user) {
        try {
            let results = await rp({
                url: `https://api.fitbit.com/1/user/${task.ownerId}/activities/steps/date/${task.date}/${task.date}.json`,
                headers: {
                    "Authorization": `Bearer  ${user.access_token}`
                },
            });

            if(results.statusCode == 200) {
                return results.data["activities-steps"];
            } else if (results.statusCode == 401) {
                let refresh_status = await this.refreshTokens(user);
                return { statusCode: 401, error: refresh_status.data.errors };
            }
        } catch(err) {
            logger.log("error",`Error occured getting steps from fitbit api: ${JSON.stringify(err,null,4)}`);
        }
    }

    /**
     * Get user's mass for the day.
     * @param {string} task The task that fitbit subscription provides.
     * @param {string} user The user's doc from cloudant.
     * @returns {Object} The daily body_mass for the requested user.
     */
    async getMass(task, user) {
        try {
            let results = await rp({
                url: `https://api.fitbit.com/1/user/${task.ownerId}/body/log/weight/date/${task.date}/${task.date}.json`,
                headers: {
                    "Authorization": `Bearer  ${user.access_token}`
                },
            });

            if(results.statusCode == 200) {
                return results.data["weight"];
            } else if (results.statusCode == 401) {
                let refresh_status = await this.refreshTokens(user);
                return { statusCode: 401, error: refresh_status.data.errors };
            }
        } catch(err) {
            logger.log("error",`Error occured getting mass from fitbit api: ${JSON.stringify(err,null,4)}`);
        }
    }

    /**
     *  Refresh a user's accessn token and save to the database.
     * @param {Object} user A user object contains refresh_token.
     * @returns {void}
     */
    async refreshTokens(user) {

        try {
            let refreshed_ua_token = await rp({
                uri: "https://api.fitbit.com/oauth2/token",
                method: "POST",
                headers: {
                    "Authorization": `Basic ${FITBIT_CLIENT_SECRET_64}`,
                    "Content-Type": "application/x-www-form-urlencoded"
                },
                form: {"grant_type": "refresh_token", "refresh_token": user.refresh_token}
            });

            if (refreshed_ua_token.statusCode != 200) {
                return refreshed_ua_token;
            }

            // Parse response and update user's doc
            user.refresh_token = refreshed_ua_token.data.refresh_token;
            user.access_token = refreshed_ua_token.data.access_token;

            // Insert updated user doc into cloudant.
            let updated_user = await db.addUser(user);
            if(!updated_user.ok) {
                logger.log("error","There was an issue updating a user's doc in cloudant..");
            } else {
                logger.log("info", `Refreshed user ${user.fitbit_id}'s access_token and refresh_token.'`);
            }
            return refreshed_ua_token;

        } catch(err) {
            logger.log("error", `Error refreshing tokens for user ${JSON.stringify(err,null,4)}`);
        }
    }

    /**
     * Revoke a users access and subscription.
     * @param {Object} user The user which needs to be revoked.
     * @returns {void}
     */
    async revokeAccess(user) {
        try {
            await request({
                url: `https://api.fitbit.com/1/user/-/apiSubscriptions/${user.fitbit_id}.json`,
                method: "DELETE",
                headers: {
                    "Authorization": `Bearer ${user.access_token}`
                }
            });

            await request({
                url: "https://api.fitbit.com/oauth2/revoke",
                headers: {
                    "Authorization": `Basic ${FITBIT_CLIENT_SECRET_64}`
                },
                qs: { "token": user.access_token }
            });

            logger.log("info", `Subscription and user's access has been deleted for user ${user.fitbit_id}`);
        } catch(err) {
            logger.log("error",`Error occured revoking access from fitbit api: ${JSON.stringify(err,null,4)}`);
        }
    }

    /**
     * Process weight for the day
     * @param {string} task A notification that a user's weight has changed.
     * @param {function} cb A callback for when the task is done
     * @returns {void}
     */
    async processMass(task, cb) {
        let user = await db.getUser({ "selector" : { "fitbit_id": task.ownerId } });
        user = user.docs[0];
        let user_mass = await this.getMass(task, user);

        // Check to see if fitbit api returned 400 or 401
        // If retries has reached the maximum attempts then drop from queue and
        // post error to console. Otherwise send task to the end of the queue and
        // increase retry attempts
        if (user_mass.statusCode == 400 || user_mass.statusCode == 401) {
            if (task.retry < MAX_RETRY) {
                task.retry++;
                queue.push(task, (err, message) => {
                    if (err) logger.log("error",`There was an error processing the task: ${JSON.stringify(err, "", 4)}`);
                    if (message) logger.log("info",message);
                });
                return cb(null, `Retry #${task.retry}: ${task.collectionType} pull for user ${task.ownerId} on ${task.date}.`);
            } else {
                return cb({
                    statusCode: user_mass.statusCode,
                    message: `User ${task.ownerId} has hit the ${MAX_RETRY} max attempts..`,
                    error: user_mass.error
                },
                null);
            }

        } else if (user_mass.length == 0){
            return cb(null,`Fitbit API has returned no data for ${task.ownerId} user on ${task.date}.`);
        }

        user_mass = user_mass[0];

        // Check to see if weight exists for that day.
        let massExists = await db.getMass({ "selector": { "fitbit_id": task.ownerId, "date": task.date} });
        if (massExists.length > 0) {
            massExists = massExists[0];
            if(massExists["body_mass"] != user_mass.weight) {
                massExists["body_mass"] = user_mass.weight;
                await db.insertMass(massExists);
            }
        } else {
            await db.insertMass({
                body_mass: user_mass.weight,
                name: user.name,
                fitbit_id: task.ownerId,
                date: task.date
            });
        }
        return cb(null, `Successfully updated mass for Fitbit user: ${task.ownerId} on ${task.date}`);
    }

    /**
     * Process steps for the day
     * @param {string} task A notification that a user's steps has changed.
     * @param {function} cb A callback for when the task is done
     * @returns {void}
     */
    async processSteps(task, cb) {
        // Gets user's data from cloudant
        let user = await db.getUser({ "selector" : { "fitbit_id": task.ownerId } }).catch(e => logger.log("error", `Error getting user: ${e, "", 4}`));
        user = user.docs[0];

        // Call Fitbit's api to process task for a given user.
        let user_steps = await this.getSteps(task, user);

        // Check to see if fitbit api returned 400 or 401
        // If retries has reached the maximum attempts then drop from queue and
        // post error to console. Otherwise send task to the end of the queue and
        // increase retry attempts.
        if (user_steps.statusCode == 400 || user_steps.statusCode == 401) {
            if (task.retry < MAX_RETRY) {
                task.retry++;
                queue.push(task, (err, message) => {
                    if (err) logger.log("error",`There was an error processing the task: ${JSON.stringify(err, "", 4)}`);
                    if (message) logger.log("info",message);
                });
                return cb(null, `Retry #${task.retry}: ${task.collectionType} pull for user ${task.ownerId} on ${task.date}`);
            } else {
                return cb({
                    statusCode: user_steps.statusCode,
                    message: `User ${task.ownerId} has hit the ${MAX_RETRY} max attempts..`,
                    error: user_steps.error
                },
                null);
            }

        }
        user_steps = user_steps[0];

        // Check to see if steps exist for that day.
        let stepsExists = await db.getSteps({ "selector": { "fitbit_id": task.ownerId, "date": task.date} });
        if (stepsExists.length > 0) {
            stepsExists = stepsExists[0];
            if(stepsExists["steps"] != user_steps.value) {
                stepsExists["steps"] = user_steps.value;
                await db.insertSteps(stepsExists);
            }
        } else {
            await db.insertSteps({
                steps: user_steps.value,
                name:  user.name,
                fitbit_id: task.ownerId,
                date: task.date
            });
        }
        return cb(null, `Successfully updated steps for Fitbit user: ${task.ownerId} on ${task.date}`);
    }
}

const fb = new FitBit();
// Create queue to handle notifications
export const queue = async.queue(function(task, cb) {
    task.retry = task.retry || 0;
    // Exponentially increase timeout based on retry attempts.
    let timeout = 3000 * Math.pow(2, task.retry);
    logger.log("debug", `Sleeping for ${timeout / 1000} seconds`);
    setTimeout(function() {
        if( task.collectionType == "body") {
            fb.processMass(task, cb);
        } else if (task.collectionType == "activities") {
            fb.processSteps(task,cb);
        } else{
            cb();
        }
    },timeout);
}, 1);

queue.drain = (err) => {
    if(err) logger.log("error"`Emptying queue failed - ${err, "", 4}`);
    logger.log("debug","Successfully emptied queue..");
};

 

 

Setting up routes

Now that we have our code to handle pulling the data from Fitbit’s API and persisting into Cloudant, we need a route to handle notification of updates and user registration. When a user registers they will subscribe our application to steps and weight updates in almost real time.

controllers/fitbit.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { Router } from "express";
import url from "url";
import request from "request-promise";
import cfenv from "cfenv";
import Cloudant from "../cloudant.js";
import { logger } from "../logger";
import { queue } from "../fitbit";

const db = new Cloudant();
const app = new Router();
const appEnv = cfenv.getAppEnv();
const FITBIT_CLIENT_ID = process.env.FITBIT_CLIENT_ID;
const FITBIT_CLIENT_SECRET = process.env.FITBIT_CLIENT_SECRET;
const FITBIT_CLIENT_SECRET_64 = new Buffer(`${FITBIT_CLIENT_ID}:${FITBIT_CLIENT_SECRET}`).toString("base64");
const FITBIT_CB_ENDPOINT = `${appEnv.url}/fitbit/callback`;
const VERIFICATION_CODE = process.env.FITBIT_VERIFICATION_CODE;


app.get("/callback", async (req,res) => {
    // Parse URL for authorization code.
    const fullUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
    const authorize_code = url.parse(fullUrl, true, true).query.code;

    try {

        let user_authorization = await request({
            uri: "https://api.fitbit.com/oauth2/token",
            method: "POST",
            headers: {
                "Authorization": "Basic "+ FITBIT_CLIENT_SECRET_64,
                "Content-Type": "application/x-www-form-urlencoded"
            },
            form: {"grant_type": "authorization_code", "client_id": FITBIT_CLIENT_ID, "redirect_uri": FITBIT_CB_ENDPOINT, "code": authorize_code}
        });

        user_authorization = JSON.parse(user_authorization);
        const access_token = user_authorization.access_token ;
        const expires_in_var = user_authorization.expires_in;
        const refresh_token = user_authorization.refresh_token;
        const scope_var = user_authorization.scope;
        const token_type_var = user_authorization.token_type;
        const fitbit_id = user_authorization.user_id;


        // Get users profile information: https://dev.fitbit.com/docs/user/
        let profile_request = await request({
            uri: `https://api.fitbit.com/1/user/${fitbit_id}/profile.json`,
            method: "GET",
            headers: {
                "Authorization": `Bearer ${access_token}`
            }
        });
        profile_request = JSON.parse(profile_request);


        // Add steps subscription for the user to notify us when steps changes.
        await request({
            uri: `https://api.fitbit.com/1/user/-/activities/apiSubscriptions/${fitbit_id}.json`,
            method: "POST",
            headers: {
                "Authorization": `Bearer ${access_token}`
            }
        }).catch(e => e);

        // Add weight subscription for the user to notify us when weight changes.
        await request({
            uri: `https://api.fitbit.com/1/user/-/body/apiSubscriptions/${fitbit_id}.json`,
            method: "POST",
            headers: {
                "Authorization": `Bearer ${access_token}`
            }
        }).catch(e => e);

        // Check user to see if they exist, if so update info.
        let user_exist = await db.getUser({
            "selector": {
                name: profile_request.user.fullName,
                fitbit_id: fitbit_id
            }
        });
        let results;
        if(user_exist.docs.length > 0 ){
            // User exists already just update their access_token and refresh_token
            user_exist = user_exist.docs[0];
            user_exist.access_token = access_token;
            user_exist.refresh_token = refresh_token;
            results = await db.addUser(user_exist).catch(err => logger.log("error",`Error updating existing user: ${JSON.stringify(err,null,4)}`));
        } else {
            // No user exists, just register a new user.
            results = await db.addUser({
                name: profile_request.user.fullName || "UNKNOWN",
                age: profile_request.user.age || "UNKNOWN",
                gender: profile_request.user.gender || "UNKNOWN",
                fitbit_id,
                access_token,
                expires_in: expires_in_var,
                token_type:token_type_var,
                scope: scope_var,
                refresh_token: refresh_token,
                registered_on: new Date()
            });
        }
        // Redirect to index page.
        if(!results.ok) {
            logger.log("error",`Error adding user to database: ${JSON.stringify(results,null,4)}`);
            res.redirect("/");
        } else {
            logger.log("info","Registered new user into database.");
            res.redirect("/registered");
        }

    } catch(err) {
        logger.log("error",`Error: ${JSON.stringify(err,null,4)}`);
    }
});

app.route("/notify")
    // Subscription verification endpoint
    .get((req,res) => {
        let code = req.query.verify || "";
        logger.info(JSON.stringify(req.body));
        if (code == VERIFICATION_CODE && code != "") {
            res.sendStatus(204);
        } else if (code != VERIFICATION_CODE && code != "") {
            res.sendStatus(404);
        }
    })
    // Handles the actual notifications.
    .post((req, res) => {
        if (req.is("application/json")) {
            let notifications = req.body;

            // Fitbit subscription expects a 204 response within 3 seconds.
            res.sendStatus(204);

            // Push notifications to a queue to be handled.
            queue.push(notifications, (err, message) => {
                if (err) logger.log("error",`There was an error pushing to the queue: ${JSON.stringify(err, "", 4)}`);
                if (message) logger.log("info",message);
            });
        } else {
            res.sendStatus(400);
        }

    });
export default app;

 

Logging data to console

You might of noticed that in some of the code snippets we are importing a Logger and using it to print debug statements to the console. We have used the Winston npm to achieve this, which looks something like this:

logger.js

1
2
3
4
5
6
7
8
import winston from "winston";

export const logger = new (winston.Logger)({
    level: "verbose",
    transports: [
        new (winston.transports.Console)(),
    ]
});

 

Supporting ES7 Features

You might of noticed some of the new ES7 keywords, async/await. In order for us to deploy this on Bluemix, we need to tell them to use the Node.js 8.1 runtime, to do this insert this snippet into your package.json:

1
2
3
4
5
  "engines": {
      "node": "8.1.0",
      "npm": "5.0.3"
  }

 

Deploy to Bluemix!

Now that we have finished developing locally, we can deploy this application to Bluemix using the CF CLI. First ensure that you have a valid manifest.yml file in the root directory that will look something like this:

manifest.yml


1
2
3
4
5
applications:
- name: YOUR_UNIQUE_APPLICATION_NAME 
  command: node server.js
  random-route: false
  memory: 300M

 

Then run the following commands to deploy your application to Bluemix:

cf api https://api.ng.bluemix.net
cf login
cf push -f manifest.yml

 

Explore and Visualize the Data with Data Science Experience

Now that we have an application that users can sign up for, it’s time to use Data Science Experience to analyze and derive some insights from your users’ data. Duplicate the Jupyter Notebook here to explore and play around with it.

 

Enhance Your App

You just learned how to create a solution that allows you to ingest data, persist it to a data store, and analyze the data using a notebook in the Data Science Experience. You now have an application that will record and track your users’ steps and showcase who among them is getting the most activity each day!

Those are the basics, but there are many parts of the application that can be further expanded to include robust additional features.

 

  • You can use a service like IBM Streaming Analytics to sift through large amounts of incoming data and locate targeted results, such as user activity. You could then use that personalized activity data to build a user activity recommendation engine based on browser location data, which could suggest places to run, hike, or cycle.
  • Or, you can use Watson Machine Learning to create a model that will predict competition winners, which you can deploy and use inside of your Node.js application.

 

What other ways could you enhance your Fitness Application?

Let us know in the comments, and happy coding!

More Data Analytics stories
March 27, 2019

Db2 Warehouse Flex Comes to Amazon Web Services (AWS)

In a strategic escalation in our approach to cloud data warehousing, we’re bringing Db2 Warehouse Flex to Amazon Web Services as a fully managed, scalable, and elastic cloud data warehouse.

Continue reading

March 12, 2019

Expanding Data Warehouse Capabilities for the IBM Hybrid Data Management Platform

The IBM Hybrid Data Management Platform is expanding capabilities with both the Flex and Hybrid Flex plans. These two types of warehousing solutions will help you optimize your hybrid cloud architectures in terms of both performance and cost-savings

Continue reading

March 5, 2019

Deprecation of Apache Spark (Lite Plan)

We’d like to inform you about the deprecation of the Apache Spark (Lite plan) service. The Lite plan of this service will be retired on June 28, 2019. Please note that the Enterprise plan has already been deprecated.

Continue reading