-
Notifications
You must be signed in to change notification settings - Fork 66.9k
Expand file tree
/
Copy pathmiddleware.ts
More file actions
140 lines (126 loc) · 4.96 KB
/
middleware.ts
File metadata and controls
140 lines (126 loc) · 4.96 KB
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
import express from 'express'
import { omit, without, mapValues } from 'lodash-es'
import QuickLRU from 'quick-lru'
import { ErrorObject } from 'ajv'
import type { ExtendedRequest } from '@/types'
import type { Response } from 'express'
import { schemas, hydroNames } from './lib/schema.js'
import catchMiddlewareError from 'src/observability/middleware/catch-middleware-error'
import { noCacheControl } from 'src/frame/middleware/cache-control'
import { getJsonValidator } from 'src/tests/lib/validate-json-schema'
import { formatErrors } from './lib/middleware-errors.js'
import { publish as _publish } from './lib/hydro.js'
import { analyzeComment, getGuessedLanguage } from './lib/analyze-comment.js'
import { EventType, EventProps, EventPropsByType } from './types'
const router = express.Router()
const OMIT_FIELDS = ['type']
const allowedTypes = new Set(without(Object.keys(schemas), 'validation'))
const isProd = process.env.NODE_ENV === 'production'
const validators = mapValues(schemas, (schema) => getJsonValidator(schema))
// In production, fire and not wait to respond.
// _publish will send an error to failbot,
// so we don't get alerts but we still track it.
// This ends up being the same as try > await > catch > (do nothing).
async function publish(...args: Parameters<typeof _publish>) {
if (isProd) {
_publish(...args)
return
}
return await _publish(...args)
}
const sentValidationErrors = new QuickLRU({
maxSize: 10_000,
maxAge: 1000 * 60,
})
router.post(
'/',
catchMiddlewareError(async function postEvents(req: ExtendedRequest, res: Response) {
noCacheControl(res)
const eventsToProcess = Array.isArray(req.body) ? req.body : [req.body]
const validEvents: any[] = []
const validationErrors: any[] = []
for (const eventBody of eventsToProcess) {
try {
if (!eventBody.type || !allowedTypes.has(eventBody.type)) {
validationErrors.push({ event: eventBody, error: 'Invalid type' })
continue
}
const type: EventType = eventBody.type
const body: EventProps & EventPropsByType[EventType] = eventBody
if (isSurvey(body) && body.survey_comment) {
body.survey_rating = await getSurveyCommentRating({
comment: body.survey_comment,
language: body.context.path_language || 'en',
})
body.survey_comment_language = await getGuessedLanguage(body.survey_comment)
}
if (body.context) {
// Add dotcom_user to the context if it's available
// JSON.stringify removes `undefined` values but not `null`, and we don't want to send `null` to Hydro
body.context.dotcom_user = req.cookies?.dotcom_user ? req.cookies.dotcom_user : undefined
body.context.is_staff = Boolean(req.cookies?.staffonly)
}
const validate = validators[type]
if (!validate(body)) {
validationErrors.push({
event: body,
error: validate.errors || [],
})
// This protects so we don't bother sending the same validation
// error, per user, more than once (per time interval).
// This helps if we're bombarded with junk bot traffic. So it
// protects our Hydro instance from being overloaded with things
// that aren't helping anybody.
const hash = `${req.ip}:${(validate.errors || [])
.map((error: ErrorObject) => error.message + error.instancePath)
.join(':')}`
if (!sentValidationErrors.has(hash)) {
sentValidationErrors.set(hash, true)
formatErrors(validate.errors || [], body).map((error) => {
validationErrors.push({ schema: hydroNames.validation, value: error })
})
}
continue
}
validEvents.push({
schema: hydroNames[type],
value: omit(body, OMIT_FIELDS),
})
} catch (eventError) {
console.error('Error validating event:', eventError)
}
}
if (validEvents.length > 0) {
await publish(validEvents)
}
if (validationErrors.length > 0) {
await publish(validationErrors)
}
const statusCode = validationErrors.length > 0 ? 400 : 200
return res.status(statusCode).json(
isProd
? undefined
: {
success_count: validEvents.length,
failure_count: validationErrors.length,
details: validationErrors,
},
)
}),
)
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
function isSurvey(
body: EventProps & EventPropsByType[EventType],
): body is EventProps & EventPropsByType[EventType.survey] {
return body.type === EventType.survey
}
type GetSurveyCommentRatingArgs = {
comment: string
language: string
}
async function getSurveyCommentRating({ comment, language }: GetSurveyCommentRatingArgs) {
if (!comment || !comment.trim()) return
const { rating } = await analyzeComment(comment, language)
return rating
}
export default router