• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / blurts-server / #12229

pending completion
#12229

push

circleci

web-flow
Merge pull request #2803 from mozilla/MNTOR-1115

MNTOR-1115: add sentry to v2

282 of 1329 branches covered (21.22%)

Branch coverage included in aggregate %.

20 of 20 new or added lines in 5 files covered. (100.0%)

959 of 3531 relevant lines covered (27.16%)

2.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/app.js
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4

5
import express from 'express'
6
import session from 'express-session'
7
import connectRedis from 'connect-redis'
8
import helmet from 'helmet'
9
import accepts from 'accepts'
10
import redis from 'redis'
11
import cookieParser from 'cookie-parser'
12
import rateLimit from 'express-rate-limit'
13
import Sentry from '@sentry/node'
14
import '@sentry/tracing'
15

16
import AppConstants from './app-constants.js'
17
import { localStorage } from './utils/local-storage.js'
18
import { errorHandler } from './middleware/error.js'
19
import { doubleCsrfProtection } from './utils/csrf.js'
20
import { initFluentBundles, updateLocale, getMessageWithLocale, getMessage } from './utils/fluent.js'
21
import { loadBreachesIntoApp } from './utils/hibp.js'
22
import { RateLimitError } from './utils/error.js'
23
import { initEmail } from './utils/email.js'
24
import indexRouter from './routes/index.js'
25

26
const app = express()
×
27
const isDev = AppConstants.NODE_ENV === 'dev'
×
28

29
// init sentry
30
Sentry.init({
×
31
  dsn: AppConstants.SENTRY_DSN,
32
  environment: AppConstants.NODE_ENV,
33
  debug: isDev,
34
  beforeSend (event, hint) {
35
    if (!hint.originalException.locales || hint.originalException.locales[0] === 'en') return event // return if no localization or localization is in english
×
36

37
    // try to force an english translation for the error message if localized
38
    if (hint.originalException.fluentID) {
×
39
      event.exception.values[0].value = getMessageWithLocale(hint.originalException.fluentID, 'en') || getMessage(hint.originalException.fluentID)
×
40
    }
41

42
    return event
×
43
  }
44
})
45

46
// Determine from where to serve client code/assets:
47
// Build script is triggered for `npm start` and assets are served from /dist.
48
// Build script is NOT run for `npm run dev`, assets are served from /src, and nodemon restarts server without build (faster dev).
49
const staticPath =
50
  process.env.npm_lifecycle_event === 'start' ? '../dist' : './client'
×
51

52
await initFluentBundles()
×
53

54
async function getRedisStore () {
55
  const RedisStoreConstructor = connectRedis(session)
×
56
  if (['', 'redis-mock'].includes(AppConstants.REDIS_URL)) {
×
57
    const redisMock = await import('redis-mock') // for devs without local redis
×
58
    return new RedisStoreConstructor({
×
59
      client: redisMock.default.createClient()
60
    })
61
  }
62
  return new RedisStoreConstructor({
×
63
    client: redis.createClient({ url: AppConstants.REDIS_URL })
64
  })
65
}
66

67
// middleware
68
app.use(
×
69
  helmet({
70
    crossOriginEmbedderPolicy: false
71
  })
72
)
73

74
app.use(
×
75
  Sentry.Handlers.requestHandler({
76
    request: ['headers', 'method', 'url'], // omit cookies, data, query_string
77
    user: ['id'] // omit username, email
78
  })
79
)
80

81
const imgSrc = [
×
82
  "'self'"
83
]
84

85
if (AppConstants.FXA_ENABLED) {
×
86
  const fxaSrc = new URL(AppConstants.OAUTH_PROFILE_URI).origin
×
87
  imgSrc.push(fxaSrc)
×
88
}
89

90
// disable forced https to allow localhost on Safari
91
app.use(
×
92
  helmet.contentSecurityPolicy({
93
    directives: {
94
      imgSrc,
95
      upgradeInsecureRequests: isDev ? null : []
×
96
    }
97
  })
98
)
99

100
// fallback to default 'no-referrer' only when 'strict-origin-when-cross-origin' not available
101
app.use(
×
102
  helmet.referrerPolicy({
103
    policy: ['no-referrer', 'strict-origin-when-cross-origin']
104
  })
105
)
106

107
// When a text/html request is received, negotiate and store the requested language
108
// Using asyncLocalStorage avoids having to pass req context down through every function (e.g. getMessage())
109
app.use((req, res, next) => {
×
110
  if (!req.headers.accept?.startsWith('text/html')) return next()
×
111

112
  localStorage.run(new Map(), () => {
×
113
    req.locale = updateLocale(accepts(req).languages())
×
114
    localStorage.getStore().set('locale', req.locale)
×
115
    next()
×
116
  })
117
})
118

119
// MNTOR-1009, 1117:
120
// Because of proxy settings, request / cookies are not persisted between calls
121
// Setting the trust proxy to high and securing the cookie allowed the cookie to persist
122
// If cookie.secure is set as true, for nodejs behind proxy, "trust proxy" needs to be set
123
app.set('trust proxy', 1)
×
124

125
// session
126
const SESSION_DURATION_HOURS = AppConstants.SESSION_DURATION_HOURS || 48
×
127
app.use(
×
128
  session({
129
    cookie: {
130
      maxAge: SESSION_DURATION_HOURS * 60 * 60 * 1000, // 48 hours
131
      rolling: true,
132
      sameSite: 'lax',
133
      secure: !isDev
134
    },
135
    resave: false,
136
    saveUninitialized: true,
137
    secret: AppConstants.COOKIE_SECRET,
138
    store: await getRedisStore()
139
  })
140
)
141

142
// Load breaches into namespaced cache
143
try {
×
144
  await loadBreachesIntoApp(app)
×
145
} catch (error) {
146
  console.error('Error loading breaches into app.locals', error)
×
147
}
148

149
app.use(express.static(staticPath))
×
150
app.use(express.json())
×
151
app.use(cookieParser(AppConstants.COOKIE_SECRET))
×
152
app.use(doubleCsrfProtection)
×
153

154
const apiLimiter = rateLimit({
×
155
  windowMs: 15 * 60 * 1000, // 15 minutes
156
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
157
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
158
  legacyHeaders: false // Disable the `X-RateLimit-*` headers
159
})
160

161
app.use('/api', apiLimiter)
×
162

163
// routing
164
app.use('/', indexRouter)
×
165

166
// sentry error handler
167
app.use(Sentry.Handlers.errorHandler({
×
168
  shouldHandleError (error) {
169
    if (error instanceof RateLimitError) return true
×
170
  }
171
}))
172

173
// app error handler
174
app.use(errorHandler)
×
175

176
app.listen(AppConstants.PORT, async function () {
×
177
  console.info(`MONITOR V2: Server listening at ${this.address().port}`)
×
178
  console.info(`Static files served from ${staticPath}`)
×
179
  try {
×
180
    await initEmail()
×
181
    console.info('Email initialized')
×
182
  } catch (ex) {
183
    console.error('try-initialize-email-error', { ex })
×
184
  }
185
})
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc