Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ amyapp
amannn
aname
APAC
apikey
appname
apresharedkey
armel
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"@opentelemetry/sdk-trace-base": "^1.15.1",
"@opentelemetry/sdk-trace-node": "^1.15.1",
"@opentelemetry/semantic-conventions": "^1.24.1",
"@sentry/node": "^10.27.0",
"@sentry/opentelemetry": "^10.27.0",
"@types/js-yaml": "^3.12.5",
"ansi-escapes": "3.2.0",
"async-file": "^2.0.2",
Expand Down
53 changes: 50 additions & 3 deletions packages/cli/src/global_telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import {APIClient} from '@heroku-cli/command'
import {Config} from '@oclif/core'
import opentelemetry, {SpanStatusCode} from '@opentelemetry/api'
import * as Sentry from '@sentry/node'
import {
SentryPropagator,
SentrySampler,
} from '@sentry/opentelemetry'
import {GDPR_FIELDS, HEROKU_FIELDS, PCI_FIELDS} from './lib/data-scrubber/presets'
import {Scrubber} from './lib/data-scrubber/scrubber'
import {PII_PATTERNS} from './lib/data-scrubber/patterns'

const {Resource} = require('@opentelemetry/resources')
const {SemanticResourceAttributes} = require('@opentelemetry/semantic-conventions')
const {registerInstrumentations} = require('@opentelemetry/instrumentation')
Expand All @@ -26,6 +35,22 @@ registerInstrumentations({
instrumentations: [],
})

const scrubber = new Scrubber({
fields: [...HEROKU_FIELDS, ...GDPR_FIELDS, ...PCI_FIELDS],
patterns: [...PII_PATTERNS],
})

const sentryClient = Sentry.init({
dsn: 'https://76530569188e7ee2961373f37951d916@o4508609692368896.ingest.us.sentry.io/4508767754846208',
environment: isDev ? 'development' : 'production',
release: version,
tracesSampleRate: 1, // needed to ensure we send OTEL data to Honeycomb
beforeSend(event) {
return scrubber.scrub(event).data
},
skipOpenTelemetrySetup: true, // needed since we have our own OTEL setup
})

const resource = Resource
.default()
.merge(
Expand All @@ -37,6 +62,7 @@ const resource = Resource

const provider = new NodeTracerProvider({
resource,
sampler: sentryClient ? new SentrySampler(sentryClient) : undefined,
})

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

export function initializeInstrumentation() {
provider.register()
provider.register({
propagator: new SentryPropagator(),
contextManager: new Sentry.SentryContextManager(),
})
// provider.register()
}

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

const telemetry = currentTelemetry

await sendToHoneycomb(telemetry)
if (telemetry instanceof Error) {
await Promise.all([
sendToHoneycomb(telemetry),
sendToSentry(telemetry),
])
} else {
await sendToHoneycomb(telemetry)
}
}

export async function sendToHoneycomb(data: Telemetry | CLIError) {
Expand Down Expand Up @@ -181,8 +218,18 @@ export async function sendToHoneycomb(data: Telemetry | CLIError) {
}

span.end()
processor.forceFlush()
await processor.forceFlush()
} catch {
debug('could not send telemetry')
}
}

export async function sendToSentry(data: CLIError) {
try {
Sentry.captureException(data)
// ensures all events are sent to Sentry before exiting.
await Sentry.flush()
} catch {
debug('Could not send error report')
}
}
16 changes: 16 additions & 0 deletions packages/cli/src/lib/data-scrubber/patterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Regex patterns for detecting PII in string content
*/
export const PII_PATTERNS = [
// Social Security Numbers (US)
/\b\d{3}-\d{2}-\d{4}\b/g,

// Email addresses
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,

// Phone numbers (US format)
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,

// JWT tokens
/\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
]
120 changes: 120 additions & 0 deletions packages/cli/src/lib/data-scrubber/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Heroku-specific sensitive field patterns
*
* Consolidated list of field names and patterns that contain sensitive data in Heroku applications.
*
* Use this preset to ensure consistent PII handling across Heroku services.
*
* @example
* ```typescript
* import { HEROKU_FIELDS } from '@heroku/js-blanket/core/presets';
* import { Scrubber } from '@heroku/js-blanket';
*
* const scrubber = new Scrubber({ fields: HEROKU_FIELDS });
* const result = scrubber.scrub(data);
* ```
*/
export const HEROKU_FIELDS = [
// Authentication & Sessions
'access_token',
/api[-_]?key/i, // Matches api_key, api-key, apikey (case insensitive)
'authenticity_token',
'heroku_oauth_token',
'heroku_session_nonce',
'heroku_user_session',
'oauth_token',
'sudo_oauth_token',
'super_user_session_secret',
'user_session_secret',
'postgres_session_nonce',

// Passwords & Secrets
'password',
'passwd',
'old_secret',
'secret',
'secret_token',
'confirm_password',
'password_confirmation',
/client[-_]?secret/i, // Matches client_secret, client-secret, clientsecret

// Tokens
'token',
'bouncer.token',
'bouncer.refresh_token',

// Headers (case-insensitive)
/authorization/i,
/cookie/i,
/x-refresh-token/i,

// SSO & Sessions
'www-sso-session',

// Payment
'payment_method',

// Infrastructure
'logplexUrl',
]

/**
* GDPR-relevant PII field patterns
*
* Field names that typically contain personally identifiable information (PII)
* regulated by GDPR (General Data Protection Regulation).
*
* Use this preset when handling EU user data to ensure compliance with GDPR requirements.
*
* @see {@link https://gdpr.eu/what-is-gdpr/|GDPR Official Documentation}
*
* @example
* ```typescript
* import { GDPR_FIELDS, HEROKU_FIELDS } from '@heroku/js-blanket/core/presets';
* import { Scrubber } from '@heroku/js-blanket';
*
* // Combine multiple presets
* const scrubber = new Scrubber({
* fields: [...HEROKU_FIELDS, ...GDPR_FIELDS]
* });
* ```
*/
export const GDPR_FIELDS = [
'email',
'phone',
'address',
'postal_code',
'ssn',
'tax_id',
]

/**
* PCI-DSS relevant field patterns
*
* Field names that typically contain payment card information regulated by
* PCI-DSS (Payment Card Industry Data Security Standard).
*
* Use this preset when handling payment card data to help maintain PCI-DSS compliance.
*
* **Important**: This preset helps reduce exposure of sensitive payment data in logs and
* error reports, but is not a substitute for full PCI-DSS compliance measures.
*
* @see {@link https://www.pcisecuritystandards.org/|PCI Security Standards Council}
*
* @example
* ```typescript
* import { PCI_FIELDS } from '@heroku/js-blanket/core/presets';
* import { Scrubber } from '@heroku/js-blanket';
*
* const scrubber = new Scrubber({
* fields: PCI_FIELDS,
* patterns: [/\d{4}-\d{4}-\d{4}-\d{4}/g] // Also scrub card numbers in text
* });
* ```
*/
export const PCI_FIELDS = [
'card_number',
'cvv',
'credit_card',
'payment_method',
]
Loading
Loading