fix(metrics): ensure email events use stashed flow data where applicable

A number of places that were sending emails assumed they had access to
flowId and flowBeginTime on the request payload. However, we're in the
process of changing some of those endpoints so that they don't receive
metrics context data in the payload.

To fix this, all endpoints are changed here to read metrics context data
via a lazy getter that falls back to loading from memcached if there's
no flow data in the payload. This happened to be a nice refactor anyway,
fitting in with our broader adoption of lazy getters. It makes the code
more resilient to change and reduces the frequency that we hit the cache
with.
This commit is contained in:
Phil Booth 2018-10-23 17:34:33 +01:00
Родитель 88c36056f8
Коммит 2168ea7afe
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 36FBB106F9C32516
9 изменённых файлов: 356 добавлений и 364 удалений

Просмотреть файл

@ -34,6 +34,7 @@ module.exports = function (log, config) {
return {
stash,
get,
gather,
propagate,
clear,
@ -76,6 +77,45 @@ module.exports = function (log, config) {
}))
}
/**
* Returns a promise that resolves to the current metrics context data,
* which may come from the request payload or have been stashed previously.
* If no there is no metrics context data, the promise resolves to an empty
* object.
*
* Unlike the rest of the methods here, this is not exposed on the request
* object and should not be called directly. Its result is instead exposed
* using a lazy getter, which can be accessed via request.app.metricsContext.
*
* @param request
*/
async function get (request) {
let token
try {
const metadata = request.payload && request.payload.metricsContext
if (metadata) {
return metadata
}
token = getToken(request)
if (token) {
return await cache.get(getKey(token)) || {}
}
} catch (err) {
log.error({
op: 'metricsContext.get',
err,
hasToken: !! token,
hasId: !! (token && token.id),
hasUid: !! (token && token.uid)
})
}
return {}
}
/**
* Gathers metrics context metadata onto data, using either metadata
* passed in with a request or previously-stashed metadata for a
@ -86,54 +126,33 @@ module.exports = function (log, config) {
* @this request
* @param data target object
*/
function gather (data) {
let token
async function gather (data) {
const metadata = await this.app.metricsContext
return P.resolve()
.then(() => {
const metadata = this.payload && this.payload.metricsContext
if (metadata) {
data.time = Date.now()
data.device_id = metadata.deviceId
data.flow_id = metadata.flowId
data.flow_time = calculateFlowTime(data.time, metadata.flowBeginTime)
data.flowBeginTime = metadata.flowBeginTime
data.flowCompleteSignal = metadata.flowCompleteSignal
data.flowType = metadata.flowType
if (metadata) {
return metadata
}
if (metadata.service) {
data.service = metadata.service
}
token = getToken(this)
if (token) {
return cache.get(getKey(token))
}
})
.then(metadata => {
if (metadata) {
data.time = Date.now()
data.device_id = metadata.deviceId
data.flow_id = metadata.flowId
data.flow_time = calculateFlowTime(data.time, metadata.flowBeginTime)
data.flowBeginTime = metadata.flowBeginTime
data.flowCompleteSignal = metadata.flowCompleteSignal
data.flowType = metadata.flowType
const doNotTrack = this.headers && this.headers.dnt === '1'
if (! doNotTrack) {
data.utm_campaign = metadata.utmCampaign
data.utm_content = metadata.utmContent
data.utm_medium = metadata.utmMedium
data.utm_source = metadata.utmSource
data.utm_term = metadata.utmTerm
}
}
if (metadata.service) {
data.service = metadata.service
}
const doNotTrack = this.headers && this.headers.dnt === '1'
if (! doNotTrack) {
data.utm_campaign = metadata.utmCampaign
data.utm_content = metadata.utmContent
data.utm_medium = metadata.utmMedium
data.utm_source = metadata.utmSource
data.utm_term = metadata.utmTerm
}
}
})
.catch(err => log.error({
op: 'metricsContext.gather',
err: err,
hasToken: !! token,
hasId: !! (token && token.id),
hasUid: !! (token && token.uid)
}))
.then(() => data)
return data
}
function getToken (request) {

Просмотреть файл

@ -74,12 +74,7 @@ module.exports = (log, db, mailer, Password, config, customs, signinUtils, push)
request.validateMetricsContext()
// Store flowId and flowBeginTime to send in email
let flowId, flowBeginTime
if (request.payload.metricsContext) {
flowId = request.payload.metricsContext.flowId
flowBeginTime = request.payload.metricsContext.flowBeginTime
}
const { flowId, flowBeginTime } = await request.app.metricsContext
return customs.check(request, email, 'accountCreate')
.then(deleteAccountIfUnverified)
@ -654,54 +649,51 @@ module.exports = (log, db, mailer, Password, config, customs, signinUtils, push)
return false
}
function sendSigninNotifications() {
return signinUtils.sendSigninNotifications(request, accountRecord, sessionToken, verificationMethod)
.then(() => {
// For new sync logins that don't send some other sort of email,
// send an after-the-fact notification email so that the user
// is aware that something happened on their account.
if (accountRecord.primaryEmail.isVerified) {
if (sessionToken.tokenVerified || ! sessionToken.mustVerify) {
if (requestHelper.wantsKeys(request)) {
const geoData = request.app.geo
const service = request.payload.service || request.query.service
const ip = request.app.clientAddress
let flowId, flowBeginTime
if (request.payload.metricsContext) {
flowId = request.payload.metricsContext.flowId
flowBeginTime = request.payload.metricsContext.flowBeginTime
async function sendSigninNotifications() {
await signinUtils.sendSigninNotifications(request, accountRecord, sessionToken, verificationMethod)
// For new sync logins that don't send some other sort of email,
// send an after-the-fact notification email so that the user
// is aware that something happened on their account.
if (accountRecord.primaryEmail.isVerified) {
if (sessionToken.tokenVerified || ! sessionToken.mustVerify) {
if (requestHelper.wantsKeys(request)) {
const geoData = request.app.geo
const service = request.payload.service || request.query.service
const ip = request.app.clientAddress
const { flowId, flowBeginTime } = await request.app.metricsContext
try {
await mailer.sendNewDeviceLoginNotification(
accountRecord.emails,
accountRecord,
{
acceptLanguage: request.app.acceptLanguage,
flowId,
flowBeginTime,
ip,
location: geoData.location,
service,
timeZone: geoData.timeZone,
uaBrowser: request.app.ua.browser,
uaBrowserVersion: request.app.ua.browserVersion,
uaOS: request.app.ua.os,
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
}
mailer.sendNewDeviceLoginNotification(
accountRecord.emails,
accountRecord,
{
acceptLanguage: request.app.acceptLanguage,
flowId: flowId,
flowBeginTime: flowBeginTime,
ip: ip,
location: geoData.location,
service,
timeZone: geoData.timeZone,
uaBrowser: request.app.ua.browser,
uaBrowserVersion: request.app.ua.browserVersion,
uaOS: request.app.ua.os,
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
}
)
.catch(e => {
// If we couldn't email them, no big deal. Log
// and pretend everything worked.
log.trace({
op: 'Account.login.sendNewDeviceLoginNotification.error',
error: e
})
})
}
)
} catch (err) {
// If we couldn't email them, no big deal. Log
// and pretend everything worked.
log.trace({
op: 'Account.login.sendNewDeviceLoginNotification.error',
error: err
})
}
}
})
}
}
}
function createKeyFetchToken() {

Просмотреть файл

@ -150,8 +150,6 @@ module.exports = (log, db, mailer, config, customs, push) => {
let verifyFunction
let event
let emails = []
let flowId
let flowBeginTime
let sendEmail = true
// Return immediately if this session or token is already verified. Only exception
@ -161,10 +159,7 @@ module.exports = (log, db, mailer, config, customs, push) => {
return {}
}
if (request.payload.metricsContext) {
flowId = request.payload.metricsContext.flowId
flowBeginTime = request.payload.metricsContext.flowBeginTime
}
const { flowId, flowBeginTime } = await request.app.metricsContext
return customs.check(request, sessionToken.email, 'recoveryEmailResendCode')
.then(setVerifyCode)

Просмотреть файл

@ -441,12 +441,7 @@ module.exports = function (
}
request.setMetricsFlowCompleteSignal(flowCompleteSignal)
// Store flowId and flowBeginTime to send in email
let flowId, flowBeginTime
if (request.payload.metricsContext) {
flowId = request.payload.metricsContext.flowId
flowBeginTime = request.payload.metricsContext.flowBeginTime
}
const { flowId, flowBeginTime } = await request.app.metricsContext
let passwordForgotToken
@ -488,9 +483,9 @@ module.exports = function (
redirectTo: request.payload.redirectTo,
resume: request.payload.resume,
acceptLanguage: request.app.acceptLanguage,
flowId: flowId,
flowBeginTime: flowBeginTime,
ip: ip,
flowId,
flowBeginTime,
ip,
location: geoData.location,
timeZone: geoData.timeZone,
uaBrowser,
@ -546,12 +541,7 @@ module.exports = function (
request.validateMetricsContext()
// Store flowId and flowBeginTime to send in email
let flowId, flowBeginTime
if (request.payload.metricsContext) {
flowId = request.payload.metricsContext.flowId
flowBeginTime = request.payload.metricsContext.flowBeginTime
}
const { flowId, flowBeginTime } = await request.app.metricsContext
return P.all([
request.emitMetricsEvent('password.forgot.resend_code.start'),
@ -637,12 +627,7 @@ module.exports = function (
request.validateMetricsContext()
// Store flowId and flowBeginTime to send in email
let flowId, flowBeginTime
if (request.payload.metricsContext) {
flowId = request.payload.metricsContext.flowId
flowBeginTime = request.payload.metricsContext.flowBeginTime
}
const { flowId, flowBeginTime } = await request.app.metricsContext
let accountResetToken

Просмотреть файл

@ -33,12 +33,7 @@ module.exports = (log, db, mailer, config, customs) => {
request.validateMetricsContext()
// Store flowId and flowBeginTime to send in email
let flowId, flowBeginTime
if (request.payload.metricsContext) {
flowId = request.payload.metricsContext.flowId
flowBeginTime = request.payload.metricsContext.flowBeginTime
}
const { flowId, flowBeginTime } = await request.app.metricsContext
return customs.check(request, email, 'sendUnblockCode')
.then(lookupAccount)
@ -74,8 +69,8 @@ module.exports = (log, db, mailer, config, customs) => {
return mailer.sendUnblockCode(emails, emailRecord, {
acceptLanguage: request.app.acceptLanguage,
unblockCode: code,
flowId: flowId,
flowBeginTime: flowBeginTime,
flowId,
flowBeginTime,
ip: request.app.clientAddress,
location: geoData.location,
timeZone: geoData.timeZone,

Просмотреть файл

@ -172,7 +172,7 @@ module.exports = (log, config, customs, db, mailer) => {
* This includes emailing the user, logging metrics events, and
* notifying attached services.
*/
sendSigninNotifications(request, accountRecord, sessionToken, verificationMethod) {
async sendSigninNotifications (request, accountRecord, sessionToken, verificationMethod) {
const service = request.payload.service || request.query.service
const redirectTo = request.payload.redirectTo
const resume = request.payload.resume
@ -180,12 +180,7 @@ module.exports = (log, config, customs, db, mailer) => {
let sessions
// Store flowId and flowBeginTime to send in email
let flowId, flowBeginTime
if (request.payload.metricsContext) {
flowId = request.payload.metricsContext.flowId
flowBeginTime = request.payload.metricsContext.flowBeginTime
}
const { flowId, flowBeginTime } = await request.app.metricsContext
const mustVerifySession = sessionToken.mustVerify && ! sessionToken.tokenVerified
@ -333,9 +328,9 @@ module.exports = (log, config, customs, db, mailer) => {
{
acceptLanguage: request.app.acceptLanguage,
code: sessionToken.tokenVerificationId,
flowId: flowId,
flowBeginTime: flowBeginTime,
ip: ip,
flowId,
flowBeginTime,
ip,
location: geoData.location,
redirectTo: redirectTo,
resume: resume,
@ -370,9 +365,9 @@ module.exports = (log, config, customs, db, mailer) => {
{
acceptLanguage: request.app.acceptLanguage,
code: sessionToken.tokenVerificationCode,
flowId: flowId,
flowBeginTime: flowBeginTime,
ip: ip,
flowId,
flowBeginTime,
ip,
location: geoData.location,
redirectTo: redirectTo,
resume: resume,

Просмотреть файл

@ -75,6 +75,8 @@ function configureSentry(server, config) {
async function create (log, error, config, routes, db, translator) {
const getGeoData = require('./geodb')(log)
const metricsContext = require('./metrics/context')(log, config)
const metricsEvents = require('./metrics/events')(log, config)
// Hawk needs to calculate request signatures based on public URL,
// not the local URL to which it is bound.
@ -209,6 +211,7 @@ async function create (log, error, config, routes, db, translator) {
defineLazyGetter(request.app, 'ua', () => userAgent(request.headers['user-agent']))
defineLazyGetter(request.app, 'geo', () => getGeoData(request.app.clientAddress))
defineLazyGetter(request.app, 'metricsContext', () => metricsContext.get(request))
defineLazyGetter(request.app, 'devices', () => {
let uid
@ -260,7 +263,6 @@ async function create (log, error, config, routes, db, translator) {
// configure Sentry
configureSentry(server, config)
const metricsContext = require('./metrics/context')(log, config)
server.decorate('request', 'stashMetricsContext', metricsContext.stash)
server.decorate('request', 'gatherMetricsContext', metricsContext.gather)
server.decorate('request', 'propagateMetricsContext', metricsContext.propagate)
@ -268,7 +270,6 @@ async function create (log, error, config, routes, db, translator) {
server.decorate('request', 'validateMetricsContext', metricsContext.validate)
server.decorate('request', 'setMetricsFlowCompleteSignal', metricsContext.setFlowCompleteSignal)
const metricsEvents = require('./metrics/events')(log, config)
server.decorate('request', 'emitMetricsEvent', metricsEvents.emit)
server.decorate('request', 'emitRouteFlowEvent', metricsEvents.emitRouteFlowEvent)

Просмотреть файл

@ -44,37 +44,36 @@ describe('metricsContext', () => {
metricsContext = proxyquire(modulePath, { '../cache': cacheFactory })(log, config)
})
it(
'metricsContext interface is correct',
() => {
assert.equal(typeof metricsContextModule, 'function', 'function is exported')
assert.equal(typeof metricsContextModule.schema, 'object', 'metricsContext.schema is object')
assert.notEqual(metricsContextModule.schema, null, 'metricsContext.schema is not null')
it('metricsContext interface is correct', () => {
assert.isFunction(metricsContextModule)
assert.isObject(metricsContextModule.schema)
assert.isNotNull(metricsContextModule.schema)
assert.equal(typeof metricsContext, 'object', 'metricsContext is object')
assert.notEqual(metricsContext, null, 'metricsContext is not null')
assert.lengthOf(Object.keys(metricsContext), 6)
assert.isObject(metricsContext)
assert.isNotNull(metricsContext)
assert.lengthOf(Object.keys(metricsContext), 7)
assert.equal(typeof metricsContext.stash, 'function', 'metricsContext.stash is function')
assert.equal(metricsContext.stash.length, 1, 'metricsContext.stash expects 1 argument')
assert.isFunction(metricsContext.stash)
assert.lengthOf(metricsContext.stash, 1)
assert.equal(typeof metricsContext.gather, 'function', 'metricsContext.gather is function')
assert.equal(metricsContext.gather.length, 1, 'metricsContext.gather expects 1 argument')
assert.isFunction(metricsContext.get)
assert.lengthOf(metricsContext.get, 1)
assert.isFunction(metricsContext.propagate)
assert.lengthOf(metricsContext.propagate, 2)
assert.isFunction(metricsContext.gather)
assert.lengthOf(metricsContext.gather, 1)
assert.equal(typeof metricsContext.clear, 'function', 'metricsContext.clear is function')
assert.equal(metricsContext.clear.length, 0, 'metricsContext.gather expects no arguments')
assert.isFunction(metricsContext.propagate)
assert.lengthOf(metricsContext.propagate, 2)
assert.equal(typeof metricsContext.validate, 'function', 'metricsContext.validate is function')
assert.equal(metricsContext.validate.length, 0, 'metricsContext.validate expects no arguments')
assert.isFunction(metricsContext.clear)
assert.lengthOf(metricsContext.clear, 0)
assert.equal(typeof metricsContext.setFlowCompleteSignal, 'function', 'metricsContext.setFlowCompleteSignal is function')
assert.equal(metricsContext.setFlowCompleteSignal.length, 2, 'metricsContext.setFlowCompleteSignal expects 2 arguments')
assert.isFunction(metricsContext.validate)
assert.lengthOf(metricsContext.validate, 0)
}
)
assert.isFunction(metricsContext.setFlowCompleteSignal)
assert.lengthOf(metricsContext.setFlowCompleteSignal, 2)
})
it('instantiated cache correctly', () => {
assert.equal(cacheFactory.callCount, 1)
@ -216,6 +215,198 @@ describe('metricsContext', () => {
}
)
it('metricsContext.get with payload', async () => {
results.get = P.resolve({
flowId: 'not this flow id',
flowBeginTime: 0
})
const result = await metricsContext.get({
payload: {
metricsContext: {
flowId: 'mock flow id',
flowBeginTime: 42
}
}
})
assert.deepEqual(result, {
flowId: 'mock flow id',
flowBeginTime: 42
})
assert.equal(cache.get.callCount, 0)
assert.equal(log.error.callCount, 0)
})
it('metricsContext.get with payload', async () => {
results.get = P.resolve({
flowId: 'not this flow id',
flowBeginTime: 0
})
const result = await metricsContext.get({
payload: {
metricsContext: {
flowId: 'mock flow id',
flowBeginTime: 42
}
}
})
assert.isObject(result)
assert.deepEqual(result, {
flowId: 'mock flow id',
flowBeginTime: 42
})
assert.equal(cache.get.callCount, 0)
assert.equal(log.error.callCount, 0)
})
it('metricsContext.get with token', async () => {
results.get = P.resolve({
flowId: 'flowId',
flowBeginTime: 1977
})
const token = {
uid: Array(64).fill('7').join(''),
id: 'wibble'
}
const result = await metricsContext.get({
auth: {
credentials: token
}
})
assert.deepEqual(result, {
flowId: 'flowId',
flowBeginTime: 1977
})
assert.equal(cache.get.callCount, 1)
assert.lengthOf(cache.get.args[0], 1)
assert.equal(cache.get.args[0][0], hashToken(token))
assert.equal(log.error.callCount, 0)
})
it('metricsContext.get with fake token', async () => {
results.get = P.resolve({
flowId: 'flowId',
flowBeginTime: 1977
})
const uid = Array(64).fill('7').join('')
const id = 'wibble'
const token = { uid, id }
const result = await metricsContext.get({
payload: {
uid,
code: id
}
})
assert.deepEqual(result, {
flowId: 'flowId',
flowBeginTime: 1977
})
assert.equal(cache.get.callCount, 1)
assert.lengthOf(cache.get.args[0], 1)
assert.equal(cache.get.args[0][0], hashToken(token))
assert.deepEqual(cache.get.args[0][0], hashToken({ uid, id }))
assert.equal(log.error.callCount, 0)
})
it('metricsContext.get with bad token', async () => {
const result = await metricsContext.get({
auth: {
credentials: {
uid: Array(64).fill('c').join('')
}
}
})
assert.deepEqual(result, {})
assert.equal(log.error.callCount, 1)
assert.lengthOf(log.error.args[0], 1)
assert.equal(log.error.args[0][0].op, 'metricsContext.get')
assert.equal(log.error.args[0][0].err.message, 'Invalid token')
assert.strictEqual(log.error.args[0][0].hasToken, true)
assert.strictEqual(log.error.args[0][0].hasId, false)
assert.strictEqual(log.error.args[0][0].hasUid, true)
})
it('metricsContext.get with no token and no payload', async () => {
const result = await metricsContext.get({
auth: {}
})
assert.deepEqual(result, {})
assert.equal(log.error.callCount, 0)
})
it('metricsContext.get with token and payload', async () => {
results.get = P.resolve({
flowId: 'foo',
flowBeginTime: 1977
})
const result = await metricsContext.get({
auth: {
credentials: {
uid: Array(16).fill('f').join(''),
id: 'bar'
}
},
payload: {
metricsContext: {
flowId: 'baz',
flowBeginTime: 42
}
}
})
assert.deepEqual(result, {
flowId: 'baz',
flowBeginTime: 42
})
assert.equal(cache.get.callCount, 0)
assert.equal(log.error.callCount, 0)
})
it('metricsContext.get with cache.get error', async () => {
results.get = P.reject('foo')
const result = await metricsContext.get({
auth: {
credentials: {
uid: Array(16).fill('f').join(''),
id: 'bar'
}
}
})
assert.deepEqual(result, {})
assert.equal(cache.get.callCount, 1)
assert.equal(log.error.callCount, 1)
assert.lengthOf(log.error.args[0], 1)
assert.equal(log.error.args[0][0].op, 'metricsContext.get')
assert.equal(log.error.args[0][0].err, 'foo')
assert.strictEqual(log.error.args[0][0].hasToken, true)
assert.strictEqual(log.error.args[0][0].hasId, true)
assert.strictEqual(log.error.args[0][0].hasUid, true)
})
it(
'metricsContext.gather with metadata',
() => {
@ -225,8 +416,8 @@ describe('metricsContext', () => {
})
const time = Date.now() - 1
return metricsContext.gather.call({
payload: {
metricsContext: {
app: {
metricsContext: P.resolve({
deviceId: 'mock device id',
flowId: 'mock flow id',
flowBeginTime: time,
@ -242,7 +433,7 @@ describe('metricsContext', () => {
utmSource: 'mock utm_source',
utmTerm: 'mock utm_term',
ignore: 'mock ignorable property'
}
})
}
}, {}).then(function (result) {
assert.equal(typeof result, 'object', 'result is object')
@ -276,8 +467,8 @@ describe('metricsContext', () => {
headers: {
dnt: '1'
},
payload: {
metricsContext: {
app: {
metricsContext: P.resolve({
deviceId: 'mock device id',
flowId: 'mock flow id',
flowBeginTime: Date.now(),
@ -293,7 +484,7 @@ describe('metricsContext', () => {
utmSource: 'mock utm_source',
utmTerm: 'mock utm_term',
ignore: 'mock ignorable property'
}
})
}
}, {}).then(function (result) {
assert.equal(Object.keys(result).length, 8, 'result has 8 properties')
@ -312,10 +503,10 @@ describe('metricsContext', () => {
'metricsContext.gather with bad flowBeginTime',
() => {
return metricsContext.gather.call({
payload: {
metricsContext: {
app: {
metricsContext: P.resolve({
flowBeginTime: Date.now() + 10000
}
})
}
}, {}).then(function (result) {
assert.equal(typeof result, 'object', 'result is object')
@ -328,193 +519,6 @@ describe('metricsContext', () => {
}
)
it(
'metricsContext.gather with token',
() => {
const time = Date.now() - 1
const token = {
uid: Array(64).fill('7').join(''),
id: 'wibble'
}
results.get = P.resolve({
deviceId: 'deviceId',
flowId: 'flowId',
flowBeginTime: time,
flowCompleteSignal: 'flowCompleteSignal',
flowType: 'flowType',
service: null,
utmCampaign: 'utmCampaign',
utmContent: 'utmContent',
utmMedium: 'utmMedium',
utmSource: 'utmSource',
utmTerm: 'utmTerm'
})
return metricsContext.gather.call({
auth: {
credentials: token
}
}, { service: 'do not clobber me' }).then(function (result) {
assert.equal(cache.get.callCount, 1, 'cache.get was called once')
assert.equal(cache.get.args[0].length, 1, 'cache.get was passed one argument')
assert.equal(cache.get.args[0][0], hashToken(token), 'cache.get argument was correct')
assert.equal(typeof result, 'object', 'result is object')
assert.notEqual(result, null, 'result is not null')
assert.equal(Object.keys(result).length, 13, 'result has 13 properties')
assert.ok(result.time > time, 'result.time seems correct')
assert.equal(result.device_id, 'deviceId', 'result.device_id is correct')
assert.equal(result.flow_id, 'flowId', 'result.flow_id is correct')
assert.ok(result.flow_time > 0, 'result.flow_time is greater than zero')
assert.ok(result.flow_time < time, 'result.flow_time is less than the current time')
assert.equal(result.flowBeginTime, time, 'result.flowBeginTime is correct')
assert.equal(result.flowCompleteSignal, 'flowCompleteSignal', 'result.flowCompleteSignal is correct')
assert.equal(result.flowType, 'flowType', 'result.flowType is correct')
assert.equal(result.service, 'do not clobber me', 'result.service is correct')
assert.equal(result.utm_campaign, 'utmCampaign', 'result.utm_campaign is correct')
assert.equal(result.utm_content, 'utmContent', 'result.utm_content is correct')
assert.equal(result.utm_medium, 'utmMedium', 'result.utm_medium is correct')
assert.equal(result.utm_source, 'utmSource', 'result.utm_source is correct')
assert.equal(result.utm_term, 'utmTerm', 'result.utm_term is correct')
assert.equal(log.error.callCount, 0, 'log.error was not called')
})
}
)
it(
'metricsContext.gather with fake token',
() => {
const time = Date.now() - 1
const uid = Array(64).fill('7').join('')
const id = 'wibble'
results.get = P.resolve({
flowId: 'flowId',
flowBeginTime: time
})
return metricsContext.gather.call({
payload: {
uid: uid,
code: id
}
}, {}).then(function (result) {
assert.equal(cache.get.callCount, 1, 'cache.get was called once')
assert.equal(cache.get.args[0].length, 1, 'cache.get was passed one argument')
assert.deepEqual(cache.get.args[0][0], hashToken({ uid, id }), 'cache.get argument was correct')
assert.equal(typeof result, 'object', 'result is object')
assert.notEqual(result, null, 'result is not null')
assert.equal(Object.keys(result).length, 12, 'result has 12 properties')
assert.ok(result.time > time, 'result.time seems correct')
assert.equal(result.flow_id, 'flowId', 'result.flow_id is correct')
assert.ok(result.flow_time > 0, 'result.flow_time is greater than zero')
assert.ok(result.flow_time < time, 'result.flow_time is less than the current time')
assert.equal(log.error.callCount, 0, 'log.error was not called')
})
}
)
it(
'metricsContext.gather with bad token',
() => {
return metricsContext.gather.call({
auth: {
credentials: {
uid: Array(64).fill('c').join('')
}
}
}, {}).then(function (result) {
assert.equal(typeof result, 'object', 'result is object')
assert.notEqual(result, null, 'result is not null')
assert.equal(Object.keys(result).length, 0, 'result is empty')
assert.equal(log.error.callCount, 1, 'log.error was called once')
assert.equal(log.error.args[0].length, 1, 'log.error was passed one argument')
assert.equal(log.error.args[0][0].op, 'metricsContext.gather', 'op property was correct')
assert.equal(log.error.args[0][0].err.message, 'Invalid token', 'err.message property was correct')
assert.strictEqual(log.error.args[0][0].hasToken, true, 'hasToken property was correct')
assert.strictEqual(log.error.args[0][0].hasId, false, 'hasId property was correct')
assert.strictEqual(log.error.args[0][0].hasUid, true, 'hasUid property was correct')
})
}
)
it(
'metricsContext.gather with no token',
() => {
results.get = P.resolve({
flowId: 'flowId',
flowBeginTime: Date.now()
})
return metricsContext.gather.call({
auth: {}
}, {}).then(function (result) {
assert.equal(typeof result, 'object', 'result is object')
assert.notEqual(result, null, 'result is not null')
assert.equal(Object.keys(result).length, 0, 'result is empty')
assert.equal(log.error.callCount, 0, 'log.error was not called')
})
}
)
it(
'metricsContext.gather with metadata and token',
() => {
const time = Date.now() - 1
results.get = P.resolve({
deviceId: 'foo',
flowId: 'bar',
flowBeginTime: time - 1
})
return metricsContext.gather.call({
auth: {
credentials: {
uid: Array(16).fill('f').join(''),
id: 'baz'
}
},
payload: {
metricsContext: {
deviceId: 'qux',
flowId: 'wibble',
flowBeginTime: time
}
}
}, {}).then(function (result) {
assert.equal(typeof result, 'object', 'result is object')
assert.notEqual(result, null, 'result is not null')
assert.equal(result.device_id, 'qux', 'result.device_id is correct')
assert.equal(result.flow_id, 'wibble', 'result.flow_id is correct')
assert.equal(cache.get.callCount, 0, 'cache.get was not called')
assert.equal(log.error.callCount, 0, 'log.error was not called')
})
}
)
it(
'metricsContext.gather with get error',
() => {
results.get = P.reject('foo')
return metricsContext.gather.call({
auth: {
credentials: {
uid: Array(16).fill('f').join(''),
id: 'bar'
}
}
}, {}).then(function () {
assert.equal(log.error.callCount, 1, 'log.error was called once')
assert.equal(log.error.args[0].length, 1, 'log.error was passed one argument')
assert.equal(log.error.args[0][0].op, 'metricsContext.gather', 'argument op property was correct')
assert.equal(log.error.args[0][0].err, 'foo', 'argument err property was correct')
assert.equal(cache.get.callCount, 1, 'cache.get was called once')
})
}
)
it('metricsContext.propagate', () => {
results.get = P.resolve('wibble')
results.add = P.resolve()

Просмотреть файл

@ -537,6 +537,11 @@ function mockRequest (data, errors) {
devices = P.resolve(data.devices || [])
}
let metricsContextData = data.payload && data.payload.metricsContext
if (! metricsContextData) {
metricsContextData = {}
}
return {
app: {
acceptLanguage: data.acceptLanguage || 'en-US',
@ -545,6 +550,7 @@ function mockRequest (data, errors) {
features: new Set(data.features),
geo,
locale: data.locale || 'en-US',
metricsContext: P.resolve(metricsContextData),
ua: {
browser: data.uaBrowser || 'Firefox',
browserVersion: data.uaBrowserVersion || '57.0',