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:
Родитель
88c36056f8
Коммит
2168ea7afe
|
@ -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',
|
||||
|
|
Загрузка…
Ссылка в новой задаче