Data Analytics
Build a Fitness App using IBM Watson Data Platform and 3rd Party Fitness APIs
September 11, 2017 | Written by: Sean Sabour
Categorized: Data Analytics | Data Science | How-tos | Internet of Things
Share this post:
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
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!
Build a Container Image Inside a Kubernetes Cluster and Push it to IBM Cloud Container Registry
We're going to show you how to build a source into a container image from a Dockerfile inside a Kubernetes cluster and push the image to IBM Cloud Container Registry with Google's Kaniko tool.
Simplify and Automate Deployments Using GitOps with IBM Multicloud Manager 3.1.2
Use Argo CD, a GitOps continuous delivery tool for Kubernetes, and IBM Multicloud Manager to achieve declarative and automated deployment of applications to multiple Kubernetes clusters.
Solving Business Problems with Splunk on IBM Cloud Kubernetes Service
In this tutorial, we will install Splunk Connect for Kubernetes into an existing Splunk instance. Splunk Connect for Kubernetes provides a way to import and search your Kubernetes logging, object, and metrics data in Splunk.