Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
feat(metrics): add flow events for email sent and clicked"
Browse files Browse the repository at this point in the history
Closes #1511
  • Loading branch information
seanmonstar committed Oct 24, 2016
1 parent 2f03ce5 commit d903b6c
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 29 deletions.
51 changes: 29 additions & 22 deletions lib/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ module.exports = function (
redirectTo: redirectTo,
resume: resume,
acceptLanguage: request.app.acceptLanguage
})
}).then(() => request.emitMetricsEvent('email.verification.sent'))
}
}

Expand Down Expand Up @@ -774,7 +774,7 @@ module.exports = function (
return getGeoData(ip)
.then(
function (geoData) {
mailer.sendVerifyLoginEmail(
return mailer.sendVerifyLoginEmail(
emailRecord,
tokenVerificationId,
userAgent.call({
Expand All @@ -789,6 +789,7 @@ module.exports = function (
)
}
)
.then(() => request.emitMetricsEvent('email.confirmation.sent'))
}
}

Expand Down Expand Up @@ -1402,21 +1403,21 @@ module.exports = function (
payload: {
service: isA.string().max(16).alphanum().optional(),
redirectTo: validators.redirectTo(config.smtp.redirectDomain).optional(),
resume: isA.string().max(2048).optional()
resume: isA.string().max(2048).optional(),
metricsContext: METRICS_CONTEXT_SCHEMA
}
}
},
handler: function (request, reply) {
log.begin('Account.RecoveryEmailResend', request)
var sessionToken = request.auth.credentials
var service = request.payload.service || request.query.service

// Choose which type of email and code to resend
var code, func
const sessionToken = request.auth.credentials
const service = request.payload.service || request.query.service
if (sessionToken.emailVerified && sessionToken.tokenVerified) {
return reply({})
}

// Choose which type of email and code to resend
let code, func, event
if (sessionToken.tokenVerificationId) {
code = sessionToken.tokenVerificationId
} else {
Expand All @@ -1425,8 +1426,10 @@ module.exports = function (

if (!sessionToken.emailVerified) {
func = mailer.sendVerifyCode
event = 'verification'
} else {
func = mailer.sendVerifyLoginEmail
event = 'confirmation'
}

return customs.check(
Expand All @@ -1445,10 +1448,9 @@ module.exports = function (
acceptLanguage: request.app.acceptLanguage
}, request.headers['user-agent'], log)
))
.then(() => request.emitMetricsEvent(`email.${event}.resent`))
.done(
function () {
reply({})
},
() => reply({}),
reply
)
}
Expand All @@ -1467,29 +1469,34 @@ module.exports = function (
}
},
handler: function (request, reply) {
var uidHex = request.payload.uid
var uid = Buffer(uidHex, 'hex')
var code = Buffer(request.payload.code, 'hex')
var service = request.payload.service || request.query.service
var reminder = request.payload.reminder || request.query.reminder
const uidHex = request.payload.uid
const uid = Buffer(uidHex, 'hex')
const code = Buffer(request.payload.code, 'hex')
const service = request.payload.service || request.query.service
const reminder = request.payload.reminder || request.query.reminder

log.begin('Account.RecoveryEmailVerify', request)

// verify_code because we don't know what type this is yet, but
// we want to record right away before anything could fail, so
// we can see in a flow that a user tried to verify, even if it
// failed right away.
request.emitMetricsEvent('email.verify_code.clicked')

db.account(uid)
.then(
function (account) {
(account) => {
// This endpoint is not authenticated, so we need to look up
// the target email address before we can check it with customs.
return customs.check(request, account.email, 'recoveryEmailVerifyCode')
.then(
function () {
return account
}
() => account
)
}
)
.then(
function (account) {
var isAccountVerification = butil.buffersAreEqual(code, account.emailCode)
(account) => {
let isAccountVerification = butil.buffersAreEqual(code, account.emailCode)

/**
* Logic for account and token verification
Expand Down
108 changes: 101 additions & 7 deletions test/local/account_routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,90 @@ test('/recovery_email/status', function (t) {
})
})

test('/recovery_email/resend_code', t => {
t.plan(2)
const config = {
signinConfirmation: {}
}
const mockDB = mocks.mockDB()
const mockLog = mocks.mockLog()
mockLog.flowEvent = sinon.spy(() => {
return P.resolve()
})
const mockMailer = mocks.mockMailer()
const mockMetricsContext = mocks.mockMetricsContext({
gather: sinon.spy(function (data) {
return P.resolve(this.payload && this.payload.metricsContext)
})
})
const accountRoutes = makeRoutes({
config: config,
db: mockDB,
log: mockLog,
mailer: mockMailer
})
const route = getRoute(accountRoutes, '/recovery_email/resend_code')

t.test('verification', t => {
t.plan(2)
const mockRequest = mocks.mockRequest({
log: mockLog,
metricsContext: mockMetricsContext,
credentials: {
uid: uuid.v4('binary').toString('hex'),
email: TEST_EMAIL,
emailVerified: false,
tokenVerified: false
},
query: {},
payload: {
metricsContext: {
flowBeginTime: Date.now(),
flowId: 'F1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF103',
entrypoint: 'preferences',
utmContent: 'some-content-string'
}
}
})
mockLog.flowEvent.reset()

return runTest(route, mockRequest, response => {
t.equal(mockLog.flowEvent.callCount, 1, 'log.flowEvent called once')
t.equal(mockLog.flowEvent.args[0][0], 'email.verification.resent')
})
})

t.test('confirmation', t => {
t.plan(2)
const mockRequest = mocks.mockRequest({
log: mockLog,
metricsContext: mockMetricsContext,
credentials: {
uid: uuid.v4('binary').toString('hex'),
email: TEST_EMAIL,
emailVerified: true,
tokenVerified: false
},
query: {},
payload: {
metricsContext: {
flowBeginTime: Date.now(),
flowId: 'F1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF103',
entrypoint: 'preferences',
utmContent: 'some-content-string'
}
}
})
mockLog.flowEvent.reset()

return runTest(route, mockRequest, response => {
t.equal(mockLog.flowEvent.callCount, 1, 'log.flowEvent called once')
t.equal(mockLog.flowEvent.args[0][0], 'email.confirmation.resent')
})
})

})

test('/account/reset', function (t) {
var uid = uuid.v4('binary')
const mockLog = mocks.spyLog()
Expand Down Expand Up @@ -913,14 +997,17 @@ test('/account/login', function (t) {
// Verify that the email code was sent
var verifyCallArgs = mockMailer.sendVerifyCode.getCall(0).args
t.equal(verifyCallArgs[1], emailCode, 'mailer.sendVerifyCode was called with emailCode')

t.equal(mockLog.flowEvent.callCount, 2, 'log.flowEvent was called twice')
t.equal(mockLog.flowEvent.args[0][0], 'account.login', 'first event was login')
t.equal(mockLog.flowEvent.args[1][0], 'email.verification.sent', 'second event was sent')
t.equal(mockMailer.sendVerifyLoginEmail.callCount, 0, 'mailer.sendVerifyLoginEmail was not called')
t.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
t.equal(response.verified, false, 'response indicates account is unverified')
t.equal(response.verificationMethod, 'email', 'verificationMethod is email')
t.equal(response.verificationReason, 'signup', 'verificationReason is signup')
t.equal(response.emailSent, true, 'email sent')
}).then(function () {
mockLog.flowEvent.reset()
mockMailer.sendVerifyCode.reset()
mockDB.createSessionToken.reset()
mockMetricsContext.stash.reset()
Expand Down Expand Up @@ -963,6 +1050,10 @@ test('/account/login', function (t) {
t.equal(response.verificationMethod, 'email', 'verificationMethod is email')
t.equal(response.verificationReason, 'login', 'verificationReason is login')

t.equal(mockLog.flowEvent.callCount, 2, 'log.flowEvent was called twice')
t.equal(mockLog.flowEvent.args[0][0], 'account.login', 'first event was login')
t.equal(mockLog.flowEvent.args[1][0], 'email.confirmation.sent', 'second event was sent')

t.equal(mockMetricsContext.stash.callCount, 3, 'metricsContext.stash was called three times')
var args = mockMetricsContext.stash.args[1]
t.equal(args.length, 1, 'metricsContext.stash was passed one argument second time')
Expand All @@ -975,6 +1066,7 @@ test('/account/login', function (t) {
t.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.country, 'United States')
t.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].timeZone, 'America/Los_Angeles')
}).then(function () {
mockLog.flowEvent.reset()
mockMailer.sendVerifyLoginEmail.reset()
mockDB.createSessionToken.reset()
mockMetricsContext.stash.reset()
Expand Down Expand Up @@ -1492,8 +1584,9 @@ test('/recovery_email/verify_code', function (t) {
t.equal(args[1], mockRequest, 'second argument was request object')
t.deepEqual(args[2], { uid: uid }, 'third argument contained uid')

t.equal(mockLog.flowEvent.callCount, 1, 'flowEvent was called once')
args = mockLog.flowEvent.args[0]
t.equal(mockLog.flowEvent.callCount, 2, 'flowEvent was called twice')
t.equal(mockLog.flowEvent.args[0][0], 'email.verify_code.clicked', 'first event was clicked')
args = mockLog.flowEvent.args[1]
t.equal(args.length, 2, 'flowEvent was passed two arguments')
t.equal(args[0], 'account.verified', 'first argument was event name')
t.equal(args[1], mockRequest, 'second argument was request object')
Expand Down Expand Up @@ -1523,11 +1616,12 @@ test('/recovery_email/verify_code', function (t) {
return runTest(route, mockRequest, function (response) {
t.equal(mockLog.activityEvent.callCount, 1, 'activityEvent was called once')

t.equal(mockLog.flowEvent.callCount, 2, 'flowEvent was called twice')
t.equal(mockLog.flowEvent.args[0][0], 'account.verified', 'first event was account.verified')
const args = mockLog.flowEvent.args[1]
t.equal(mockLog.flowEvent.callCount, 3, 'flowEvent was called thrice')
t.equal(mockLog.flowEvent.args[0][0], 'email.verify_code.clicked', 'first event was clicked')
t.equal(mockLog.flowEvent.args[1][0], 'account.verified', 'second event was account.verified')
const args = mockLog.flowEvent.args[2]
t.equal(args.length, 2, 'flowEvent was passed two arguments')
t.equal(args[0], 'account.reminder', 'second event was account.reminder')
t.equal(args[0], 'account.reminder', 'third event was account.reminder')
t.equal(args[1], mockRequest, 'second argument was request object')

t.equal(mockMailer.sendPostVerifyEmail.callCount, 1, 'sendPostVerifyEmail was called once')
Expand Down

0 comments on commit d903b6c

Please sign in to comment.