Skip to content

Commit 15fe316

Browse files
authored
feat(W-19894449): add Sentry error reporting (#3413)
* feat: add sentry for error monitoring * promisify sendToSentry function * fix: remove scrubbing of API key pattern as too aggressive * remove overly-aggressive scrubber presets and patterns * test: add unit test for sending data to sentry * fix: incorporate otel into sentry reporting * add notes and remove unnecessary code * remove promise rejection from sendToSentry function as unnecessary
1 parent 89a95ab commit 15fe316

10 files changed

Lines changed: 1766 additions & 5 deletions

File tree

cspell-dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ amyapp
1010
amannn
1111
aname
1212
APAC
13+
apikey
1314
appname
1415
apresharedkey
1516
armel

packages/cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"@opentelemetry/sdk-trace-base": "^1.15.1",
3636
"@opentelemetry/sdk-trace-node": "^1.15.1",
3737
"@opentelemetry/semantic-conventions": "^1.24.1",
38+
"@sentry/node": "^10.27.0",
39+
"@sentry/opentelemetry": "^10.27.0",
3840
"@types/js-yaml": "^3.12.5",
3941
"ansi-escapes": "3.2.0",
4042
"async-file": "^2.0.2",

packages/cli/src/global_telemetry.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import {APIClient} from '@heroku-cli/command'
22
import {Config} from '@oclif/core'
33
import opentelemetry, {SpanStatusCode} from '@opentelemetry/api'
4+
import * as Sentry from '@sentry/node'
5+
import {
6+
SentryPropagator,
7+
SentrySampler,
8+
} from '@sentry/opentelemetry'
9+
import {GDPR_FIELDS, HEROKU_FIELDS, PCI_FIELDS} from './lib/data-scrubber/presets'
10+
import {Scrubber} from './lib/data-scrubber/scrubber'
11+
import {PII_PATTERNS} from './lib/data-scrubber/patterns'
12+
413
const {Resource} = require('@opentelemetry/resources')
514
const {SemanticResourceAttributes} = require('@opentelemetry/semantic-conventions')
615
const {registerInstrumentations} = require('@opentelemetry/instrumentation')
@@ -26,6 +35,22 @@ registerInstrumentations({
2635
instrumentations: [],
2736
})
2837

38+
const scrubber = new Scrubber({
39+
fields: [...HEROKU_FIELDS, ...GDPR_FIELDS, ...PCI_FIELDS],
40+
patterns: [...PII_PATTERNS],
41+
})
42+
43+
const sentryClient = Sentry.init({
44+
dsn: 'https://76530569188e7ee2961373f37951d916@o4508609692368896.ingest.us.sentry.io/4508767754846208',
45+
environment: isDev ? 'development' : 'production',
46+
release: version,
47+
tracesSampleRate: 1, // needed to ensure we send OTEL data to Honeycomb
48+
beforeSend(event) {
49+
return scrubber.scrub(event).data
50+
},
51+
skipOpenTelemetrySetup: true, // needed since we have our own OTEL setup
52+
})
53+
2954
const resource = Resource
3055
.default()
3156
.merge(
@@ -37,6 +62,7 @@ const resource = Resource
3762

3863
const provider = new NodeTracerProvider({
3964
resource,
65+
sampler: sentryClient ? new SentrySampler(sentryClient) : undefined,
4066
})
4167

4268
const headers = {Authorization: `Bearer ${process.env.IS_HEROKU_TEST_ENV !== 'true' ? getToken() : ''}`}
@@ -75,7 +101,11 @@ interface CLIError extends Error {
75101
}
76102

77103
export function initializeInstrumentation() {
78-
provider.register()
104+
provider.register({
105+
propagator: new SentryPropagator(),
106+
contextManager: new Sentry.SentryContextManager(),
107+
})
108+
// provider.register()
79109
}
80110

81111
export function setupTelemetry(config: any, opts: any) {
@@ -152,7 +182,14 @@ export async function sendTelemetry(currentTelemetry: any) {
152182

153183
const telemetry = currentTelemetry
154184

155-
await sendToHoneycomb(telemetry)
185+
if (telemetry instanceof Error) {
186+
await Promise.all([
187+
sendToHoneycomb(telemetry),
188+
sendToSentry(telemetry),
189+
])
190+
} else {
191+
await sendToHoneycomb(telemetry)
192+
}
156193
}
157194

158195
export async function sendToHoneycomb(data: Telemetry | CLIError) {
@@ -181,8 +218,18 @@ export async function sendToHoneycomb(data: Telemetry | CLIError) {
181218
}
182219

183220
span.end()
184-
processor.forceFlush()
221+
await processor.forceFlush()
185222
} catch {
186223
debug('could not send telemetry')
187224
}
188225
}
226+
227+
export async function sendToSentry(data: CLIError) {
228+
try {
229+
Sentry.captureException(data)
230+
// ensures all events are sent to Sentry before exiting.
231+
await Sentry.flush()
232+
} catch {
233+
debug('Could not send error report')
234+
}
235+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Regex patterns for detecting PII in string content
3+
*/
4+
export const PII_PATTERNS = [
5+
// Social Security Numbers (US)
6+
/\b\d{3}-\d{2}-\d{4}\b/g,
7+
8+
// Email addresses
9+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
10+
11+
// Phone numbers (US format)
12+
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
13+
14+
// JWT tokens
15+
/\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
16+
]
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Heroku-specific sensitive field patterns
3+
*
4+
* Consolidated list of field names and patterns that contain sensitive data in Heroku applications.
5+
*
6+
* Use this preset to ensure consistent PII handling across Heroku services.
7+
*
8+
* @example
9+
* ```typescript
10+
* import { HEROKU_FIELDS } from '@heroku/js-blanket/core/presets';
11+
* import { Scrubber } from '@heroku/js-blanket';
12+
*
13+
* const scrubber = new Scrubber({ fields: HEROKU_FIELDS });
14+
* const result = scrubber.scrub(data);
15+
* ```
16+
*/
17+
export const HEROKU_FIELDS = [
18+
// Authentication & Sessions
19+
'access_token',
20+
/api[-_]?key/i, // Matches api_key, api-key, apikey (case insensitive)
21+
'authenticity_token',
22+
'heroku_oauth_token',
23+
'heroku_session_nonce',
24+
'heroku_user_session',
25+
'oauth_token',
26+
'sudo_oauth_token',
27+
'super_user_session_secret',
28+
'user_session_secret',
29+
'postgres_session_nonce',
30+
31+
// Passwords & Secrets
32+
'password',
33+
'passwd',
34+
'old_secret',
35+
'secret',
36+
'secret_token',
37+
'confirm_password',
38+
'password_confirmation',
39+
/client[-_]?secret/i, // Matches client_secret, client-secret, clientsecret
40+
41+
// Tokens
42+
'token',
43+
'bouncer.token',
44+
'bouncer.refresh_token',
45+
46+
// Headers (case-insensitive)
47+
/authorization/i,
48+
/cookie/i,
49+
/x-refresh-token/i,
50+
51+
// SSO & Sessions
52+
'www-sso-session',
53+
54+
// Payment
55+
'payment_method',
56+
57+
// Infrastructure
58+
'logplexUrl',
59+
]
60+
61+
/**
62+
* GDPR-relevant PII field patterns
63+
*
64+
* Field names that typically contain personally identifiable information (PII)
65+
* regulated by GDPR (General Data Protection Regulation).
66+
*
67+
* Use this preset when handling EU user data to ensure compliance with GDPR requirements.
68+
*
69+
* @see {@link https://gdpr.eu/what-is-gdpr/|GDPR Official Documentation}
70+
*
71+
* @example
72+
* ```typescript
73+
* import { GDPR_FIELDS, HEROKU_FIELDS } from '@heroku/js-blanket/core/presets';
74+
* import { Scrubber } from '@heroku/js-blanket';
75+
*
76+
* // Combine multiple presets
77+
* const scrubber = new Scrubber({
78+
* fields: [...HEROKU_FIELDS, ...GDPR_FIELDS]
79+
* });
80+
* ```
81+
*/
82+
export const GDPR_FIELDS = [
83+
'email',
84+
'phone',
85+
'address',
86+
'postal_code',
87+
'ssn',
88+
'tax_id',
89+
]
90+
91+
/**
92+
* PCI-DSS relevant field patterns
93+
*
94+
* Field names that typically contain payment card information regulated by
95+
* PCI-DSS (Payment Card Industry Data Security Standard).
96+
*
97+
* Use this preset when handling payment card data to help maintain PCI-DSS compliance.
98+
*
99+
* **Important**: This preset helps reduce exposure of sensitive payment data in logs and
100+
* error reports, but is not a substitute for full PCI-DSS compliance measures.
101+
*
102+
* @see {@link https://www.pcisecuritystandards.org/|PCI Security Standards Council}
103+
*
104+
* @example
105+
* ```typescript
106+
* import { PCI_FIELDS } from '@heroku/js-blanket/core/presets';
107+
* import { Scrubber } from '@heroku/js-blanket';
108+
*
109+
* const scrubber = new Scrubber({
110+
* fields: PCI_FIELDS,
111+
* patterns: [/\d{4}-\d{4}-\d{4}-\d{4}/g] // Also scrub card numbers in text
112+
* });
113+
* ```
114+
*/
115+
export const PCI_FIELDS = [
116+
'card_number',
117+
'cvv',
118+
'credit_card',
119+
'payment_method',
120+
]

0 commit comments

Comments
 (0)