|
4 | 4 |
|
5 | 5 | 'use strict'; |
6 | 6 |
|
7 | | -const Hoek = require('@hapi/hoek'); |
8 | 7 | const Sentry = require('@sentry/node'); |
9 | | -const verror = require('verror'); |
10 | | -const { ignoreErrors } = require('@fxa/accounts/errors'); |
11 | 8 |
|
12 | 9 | const { |
13 | 10 | formatMetadataValidationErrorMessage, |
14 | 11 | reportValidationError, |
15 | 12 | } = require('fxa-shared/sentry/report-validation-error'); |
16 | 13 |
|
17 | | -function reportSentryMessage(message, captureContext) { |
18 | | - Sentry.withScope((scope) => { |
19 | | - scope.setExtra('report', true); |
20 | | - |
21 | | - if (captureContext && typeof captureContext === 'object') { |
22 | | - Hoek.merge(scope, captureContext); |
23 | | - } |
24 | | - |
25 | | - Sentry.captureMessage(message, captureContext); |
26 | | - }); |
27 | | -} |
28 | | - |
29 | | -function reportSentryError(err, request) { |
30 | | - let exception = ''; |
31 | | - if (err && err.stack) { |
32 | | - try { |
33 | | - exception = err.stack.split('\n')[0]; |
34 | | - } catch (e) { |
35 | | - // ignore bad stack frames |
36 | | - } |
37 | | - } |
38 | | - |
39 | | - if (ignoreErrors(err)) { |
40 | | - return; |
41 | | - } |
| 14 | +// Anything with these keys containing these strings will be redacted. |
| 15 | +const SENSITIVE_KEY_TERMS = new Set(['auth', 'pw', 'kb', 'key']); |
42 | 16 |
|
43 | | - Sentry.withScope((scope) => { |
44 | | - if (request) { |
45 | | - scope.addEventProcessor((sentryEvent) => { |
46 | | - // As of sentry v9, this should automatically happen by adding, Sentry.requestDataIntegration() |
47 | | - // Leaving note here for historical context. |
48 | | - // event.request = Sentry.extractRequestData(request); |
49 | | - sentryEvent.level = 'error'; |
50 | | - return sentryEvent; |
51 | | - }); |
52 | | - } |
53 | | - |
54 | | - // Important! Set a flag so that we know this is an error captured |
55 | | - // and reported by our code. Once we added tracing, we started seeing errors |
56 | | - // propagate in other ways. By setting a breakpoint or adding a console.trace |
57 | | - // in beforeSend, we can see that Sentry's internal libraries are picking up |
58 | | - // and reporting errors too. This causes problems because: |
59 | | - // - It means errors are double captured |
60 | | - // - It means that extra error info added below won't be there are errors |
61 | | - // where captured by this function. |
62 | | - // - It means the duplicate error might have a different shape, and fool |
63 | | - // our ignoreErrors() check. |
64 | | - // |
65 | | - // See the filterSentryEvent function to see how this flag is used. |
66 | | - // |
67 | | - scope.setExtra('report', true); |
68 | | - scope.setExtra('exception', exception); |
69 | | - // If additional data was added to the error, extract it. |
70 | | - if (err.output && typeof err.output.payload === 'object') { |
71 | | - const payload = err.output.payload; |
72 | | - if (typeof payload.data === 'object') { |
73 | | - scope.setContext('payload.data', payload.data); |
74 | | - delete payload.data; |
| 17 | +function filterExtras(obj, depth = 0) { |
| 18 | + return Object.fromEntries( |
| 19 | + Object.entries(obj).map(([k, v]) => { |
| 20 | + const lower = k.toLowerCase(); |
| 21 | + if ([...SENSITIVE_KEY_TERMS].some((term) => lower.includes(term))) { |
| 22 | + return [k, '[Filtered]']; |
75 | 23 | } |
76 | | - scope.setContext('payload', payload); |
77 | | - } |
78 | | - const cause = verror.cause(err); |
79 | | - if (cause && cause.message) { |
80 | | - const causeContext = { |
81 | | - errorName: cause.name, |
82 | | - reason: cause.reason, |
83 | | - errorMessage: cause.message, |
84 | | - }; |
85 | | - |
86 | | - // Poolee EndpointError's have a few other things and oddly don't include |
87 | | - // a stack at all. We try and extract a bit more to reflect what actually |
88 | | - // happened as 'socket hang up' is somewhat inaccurate when the remote server |
89 | | - // throws a 500. |
90 | | - const output = cause.output; |
91 | | - if (output && output.payload) { |
92 | | - for (const key of ['error', 'message', 'statusCode']) { |
93 | | - causeContext[key] = output.payload[key]; |
| 24 | + if (v && typeof v === 'object' && !Array.isArray(v)) { |
| 25 | + if (depth >= 4) { |
| 26 | + return [k, '[Filtered]']; |
94 | 27 | } |
| 28 | + return [k, filterExtras(v, depth + 1)]; |
95 | 29 | } |
96 | | - const attempt = cause.attempt; |
97 | | - if (attempt) { |
98 | | - causeContext.method = attempt.method; |
99 | | - causeContext.path = attempt.path; |
100 | | - } |
101 | | - scope.setContext('cause', causeContext); |
102 | | - } |
| 30 | + return [k, v]; |
| 31 | + }) |
| 32 | + ); |
| 33 | +} |
103 | 34 |
|
104 | | - if (request) { |
105 | | - // Merge the request scope into the temp scope |
106 | | - Hoek.merge(scope, request.sentryScope); |
107 | | - } |
108 | | - Sentry.captureException(err); |
109 | | - }); |
| 35 | +function reportSentryMessage(message, captureContext) { |
| 36 | + Sentry.captureMessage(message, captureContext); |
| 37 | +} |
| 38 | + |
| 39 | +function reportSentryError(err, request) { |
| 40 | + Sentry.captureException(err); |
110 | 41 | } |
111 | 42 |
|
112 | 43 | async function configureSentry(server, config, processName = 'key_server') { |
113 | 44 | if (config.sentry.dsn) { |
114 | | - Sentry.getCurrentScope().setTag('process', processName); |
| 45 | + Sentry.getGlobalScope().setTag('process', processName); |
115 | 46 |
|
116 | 47 | if (!server) { |
117 | 48 | return; |
118 | 49 | } |
119 | | - |
120 | | - // Attach a new Sentry scope to the request for breadcrumbs/tags/extras |
121 | 50 | server.ext({ |
122 | | - type: 'onRequest', |
| 51 | + type: 'onPreHandler', |
123 | 52 | method(request, h) { |
124 | | - request.sentryScope = new Sentry.Scope(); |
125 | | - |
126 | | - /** |
127 | | - // Make a transaction per request so we can get performance monitoring. There are |
128 | | - // some limitations to this approach, and distributed tracing will be off due to |
129 | | - // hapi's architecture. |
130 | | - // |
131 | | - // See https://github.com/getsentry/sentry-javascript/issues/2172 for more into. It |
132 | | - // looks like there might be some other solutions that are more complex, but would work |
133 | | - // with hapi and distributed tracing. |
134 | | - // |
135 | | - const transaction = Sentry.startInactiveSpan({ |
136 | | - op: 'auth-server', |
137 | | - name: `${request.method.toUpperCase()} ${request.path}`, |
138 | | - forceTransaction: true, |
139 | | - // As of sentry v9, this should automatically happen by adding, Sentry.requestDataIntegration() |
140 | | - // Leaving note here for historical context. |
141 | | - // request: Sentry.extractRequestData(request.raw.req), |
| 53 | + // hapiIntegration() manages per-request isolation scopes via async context. |
| 54 | + // Set tags/extras directly on the current scope — no withIsolationScope |
| 55 | + // wrapper here, which would create a synchronous child scope that is |
| 56 | + // discarded before the async handler runs, causing breadcrumbs to leak |
| 57 | + // onto the global scope and accumulate across requests. |
| 58 | + const UNKNOWN = 'Unknown'; |
| 59 | + Sentry.setTag('route', request.route.path); |
| 60 | + Sentry.setTag('method', request.method); |
| 61 | + Sentry.setUser({ |
| 62 | + email: |
| 63 | + request.credentials?.email || |
| 64 | + request.payload?.email || |
| 65 | + request.params?.email || |
| 66 | + UNKNOWN, |
| 67 | + geo: { |
| 68 | + city: request.app?.geo?.location?.city || UNKNOWN, |
| 69 | + country_code: |
| 70 | + request.app?.geo?.location?.countryCode || |
| 71 | + request.app?.geo?.location?.country || |
| 72 | + UNKNOWN, |
| 73 | + region: |
| 74 | + request.app?.geo?.location?.stateCode || |
| 75 | + request.app?.geo?.location?.state || |
| 76 | + UNKNOWN, |
| 77 | + }, |
| 78 | + id: request.auth?.credentials?.uid || UNKNOWN, |
| 79 | + ip_address: request.app?.clientAddress || UNKNOWN, |
142 | 80 | }); |
143 | | -
|
144 | | - request.app.sentry = { |
145 | | - transaction, |
146 | | - }; |
147 | | - //*/ |
148 | | - |
| 81 | + Sentry.setExtra('acceptLanguage', request.app?.acceptLanguage || {}); |
| 82 | + Sentry.setExtra('request_payload', filterExtras(request.payload || {})); |
| 83 | + Sentry.setExtra('request_headers', filterExtras(request.headers || {})); |
| 84 | + Sentry.setExtra('request_params', filterExtras(request.params || {})); |
149 | 85 | return h.continue; |
150 | 86 | }, |
151 | 87 | }); |
152 | 88 |
|
153 | 89 | server.events.on('request', (request, event, tags) => { |
154 | 90 | if (event?.error && tags?.handler && tags?.error) { |
155 | | - reportSentryError(event.error, request); |
| 91 | + Sentry.withScope((scope) => { |
| 92 | + scope.setExtra('hapi_event', event); |
| 93 | + Sentry.captureException(event.error); |
| 94 | + }); |
156 | 95 | } |
157 | 96 | }); |
158 | 97 | } |
159 | 98 | } |
160 | 99 |
|
161 | 100 | module.exports = { |
162 | 101 | configureSentry, |
| 102 | + filterExtras, |
163 | 103 | reportSentryMessage, |
164 | 104 | reportSentryError, |
165 | 105 | reportValidationError, |
|
0 commit comments