Skip to content
This repository has been archived by the owner on Oct 14, 2020. It is now read-only.

Add Android feedback collection endpoint #111

Merged
merged 3 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions bin/remove-feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node

const assert = require('assert')
const moment = require('moment')

const MongoClient = require('mongodb').MongoClient
const mongoURL = process.env.MLAB_URI
if (!mongoURL) throw new Error('MLAB_URI must be set in environment')

const FEEDBACK_COLLECTION = process.env.FEEDBACK_COLLECTION || 'feedback'

const args = require('yargs')
.describe('id', 'remove a single item by id')
.describe('interval', 'remove all feedback created before this interval')
.argv

const main = async (args) => {
MongoClient.connect(mongoURL, (err, client) => {
const db = client.db()

const collection = db.collection(FEEDBACK_COLLECTION);
if (args.id) {
collection.deleteOne({ id : args.id }, (err, result) => {
assert.equal(err, null)
if (result.result.n === 0) {
console.log(`Feedback with id ${args.id} not found`)
} else {
console.log(`Feedback with id ${args.id} removed`)
}
client.close()
})
} else if (args.interval) {
let [num, period] = args.interval.split(' ')
num = parseInt(num)
assert(typeof(period) === 'string')
const targetYMD = moment().subtract(num, period).format('YYYY-MM-DD')
console.log(`This will remove all feedback before ${targetYMD}. Run again with -f to accept`)
if (targetYMD && args.f) {
console.log(`removing all feedback before ${targetYMD}`)
collection.deleteMany({ ymd: { $lt: targetYMD }}, (err, result) => {
assert.equal(err, null)
console.log(`${result.result.n} feedback records removed`)
client.close()
})
} else {
client.close()
}
} else {
client.close()
}
})
}

main(args)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"start": "./node_modules/.bin/babel-node src/index.js",
"lint": "standard",
"verify": "node tools/verify.js",
"test": "S3_DOWNLOAD_KEY=1 S3_DOWNLOAD_SECRET=1 BEHIND_FASTLY=1 tap test/*.js",
"test": "S3_DOWNLOAD_KEY=1 S3_DOWNLOAD_SECRET=1 BEHIND_FASTLY=1 API_KEYS=a,b,c tap test/*.js",
"test-win": "set BEHIND_FASTLY=1 && tap test/*.js"
},
"author": "Brave",
Expand Down
88 changes: 88 additions & 0 deletions src/controllers/feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const Joi = require('joi')
const Boom = require('boom')
const moment = require('moment')
const storage = require('../storage')
const uuid = require('uuid/v4')
const verification = require('../verification')

const FEEDBACK_COLLECTION = process.env.FEEDBACK_COLLECTION || 'feedback'

// feedback validator
const validator = {
payload: {
selection: Joi.any().allow('yes', 'no').required(),
platform: Joi.string().required(),
os_version: Joi.string().required(),
phone_make: Joi.string().required(),
phone_model: Joi.string().required(),
phone_arch: Joi.string().required(),
app_version: Joi.string().required(),
user_feedback: Joi.string().max(1024).optional(),
api_key: Joi.string().required(),
}
}

// build object to be stored from feedback
const buildStorageObject = (payload) => {
return {
id: uuid(),
ts: (new Date()).getTime(),
ymd: moment().format('YYYY-MM-DD'),
selection: payload.selection,
platform: payload.platform,
os_version: payload.os_version,
phone_make: payload.phone_make,
phone_model: payload.phone_model,
phone_arch: payload.phone_arch,
version: payload.app_version,
user_feedback: payload.user_feedback,
}
}

// build return result object
const successResult = (id) => {
return {
id: id,
status: 'ok',
ts: (new Date()).getTime()
}
}

exports.setup = (runtime) => {
const routes = []

routes.push({
method: 'POST',
path: '/1/feedback',
config: {
description: '* Record feedback',
handler: async (request, reply) => {
try {
// phase 2 - to be implemented - rate limit on IP address

// verify API key
if (!verification.isValidAPIKey(request.payload.api_key)) {
return reply(Boom.notAcceptable('invalid api key'))
}

// build event
const storageObject = buildStorageObject(request.payload)

// abstract storage mechanism
await storage.storeObjectOrEvent(runtime, FEEDBACK_COLLECTION, storageObject)

// return success
return reply(successResult(storageObject.id))
} catch (e) {
return reply(Boom.badImplementation(e.toString()))
}
},
validate: validator
}
})

return routes
}

exports.buildStorageObject = buildStorageObject
exports.successResult = successResult
5 changes: 4 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ mq.setup((senders) => {
// webcompat collection routes
let webcompatRoutes = require('./controllers/webcompat').setup(runtime, releases)

// feedback collection routes
let feedbackRoutes = require('./controllers/feedback').setup(runtime, releases)

let server = null

// Output request headers to aid in osx crash storage issue
Expand Down Expand Up @@ -119,7 +122,7 @@ mq.setup((senders) => {
server.route(
[
common.root
].concat(releaseRoutes, extensionRoutes, crashes, monitoring, androidRoutes, iosRoutes, braveCoreRoutes, promoProxy, installerEventsCollectionRoutes, webcompatRoutes)
].concat(releaseRoutes, extensionRoutes, crashes, monitoring, androidRoutes, iosRoutes, braveCoreRoutes, promoProxy, installerEventsCollectionRoutes, webcompatRoutes, feedbackRoutes)
)

server.start((err) => {
Expand Down
15 changes: 14 additions & 1 deletion src/verification.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const _ = require('underscore')
const moment = require('moment')

// verification libraries
Expand All @@ -12,6 +13,13 @@ const verifiers = [
linuxCore.variousVersions,
]

const API_KEYS = _.object(
(process.env.API_KEYS || '')
.split(',')
.map((k) => { return k.trim() })
.map((k) => { return [k, true] })
)

// public function to determine is a request should be verified, and if so,
// if the usage ping is valid (by iterating over a set of verifiers)
const isUsagePingValid = (request, usage, apiKeys = [], tlsSignatures = []) => {
Expand All @@ -33,7 +41,12 @@ const writeFilteredUsagePing = (mg, usage, cb) => {
filteredCollection.insertOne(usage, cb)
}

const isValidAPIKey = (k) => {
return !!API_KEYS[k]
}

module.exports = {
isUsagePingValid,
writeFilteredUsagePing
writeFilteredUsagePing,
isValidAPIKey,
}
30 changes: 30 additions & 0 deletions test/feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const tap = require('tap')
const moment = require('moment')

const feedback = require('../src/controllers/feedback')
const verification = require('../src/verification')

tap.test('feedback', (t) => {
let results = feedback.buildStorageObject({
selection: 'no',
platform: 'androidbrowser',
os_version: 'os',
phone_make: 'make',
phone_model: 'model',
phone_arch: 'arch',
app_version: '1.2.3',
user_feedback: 'feedback'
})
t.equal(results.platform, 'androidbrowser', 'platform captured')
t.ok(results.ts, 'timestamp inserted')
t.ok(results.ymd, 'ymd inserted')
t.ok(results.id, 'id inserted')

t.equal(feedback.successResult('1').status, 'ok', 'ok result well formed')
t.ok(feedback.successResult('1').id, 'ok result has id')

t.ok(verification.isValidAPIKey('a'), 'verification key found')
t.notok(verification.isValidAPIKey('z'), 'verification key not found')

t.done()
})