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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ All options are passed to `init()` as an `ObservabilityConfig` object.
| `otlp` | `OtlpConfig` | — | OTLP trace export configuration. See below. |
| `filters` | `FilterConfig` | — | Filtering rules for traces, errors, and breadcrumbs. See below. |
| `shouldDropError` | `(error: unknown) => boolean` | — | Predicate to suppress specific errors from Sentry. Return `true` to drop. |
| `classifyError` | `(error: unknown) => 'drop' \| 'warning' \| undefined` | — | Classify errors before sending to Sentry. `'drop'` suppresses the error, `'warning'` downgrades its severity level, `undefined` leaves it unchanged. |

### `SentryConfig`

Expand Down Expand Up @@ -126,6 +127,10 @@ init({
},
shouldDropError: (error) =>
error instanceof MyClientError && error.code === 'NOT_FOUND',
classifyError: (error) => {
if (error instanceof TransportError && error.transient) return 'warning'
return undefined
},
})
```

Expand Down Expand Up @@ -282,6 +287,7 @@ When the consuming application (e.g. `agentic-api`) calls `init({ enableOtel: tr
Several things happen without any additional configuration:

- **`JWTExpired` errors are silently dropped** from Sentry — expired tokens are expected and not actionable.
- **Error classification runs in order**: `shouldDropError` is checked first, then `classifyError`, then the built-in `JWTExpired` filter. The first match wins.
- **Pyroscope initialization failures are caught** — if Pyroscope fails to start, the error is reported to Sentry and the process continues normally.
- **Pyroscope and PostHog breadcrumbs are filtered** from Sentry by default via `filters.ignoredBreadcrumbPatterns`.
- **Test environments are inert** — when `NODE_ENV=test`, `init()` resolves the config but skips all instrumentation. No Sentry, no Pyroscope, no OTel side effects.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@scope3data/observability-js",
"version": "2.0.0",
"version": "2.1.0",
"description": "Unified observability (Sentry, OpenTelemetry, Pyroscope) for Node.js services",
"keywords": [
"observability",
Expand Down
48 changes: 48 additions & 0 deletions src/__tests__/filtering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,54 @@ describe('Observability Filtering', () => {
})
})

describe('classifyError callback pattern', () => {
class TransportError extends Error {
constructor(
message: string,
public readonly transient: boolean,
) {
super(message)
this.name = 'TransportError'
}
}

const classifyError = (error: unknown): 'drop' | 'warning' | undefined => {
if (error instanceof TransportError) {
return error.transient ? 'warning' : undefined
}
if (
error instanceof Error &&
error.message.includes('client disconnected')
) {
return 'drop'
}
return undefined
}

it('returns drop to suppress errors', () => {
expect(classifyError(new Error('client disconnected unexpectedly'))).toBe(
'drop',
)
})

it('returns warning to downgrade severity', () => {
expect(classifyError(new TransportError('timeout', true))).toBe('warning')
})

it('returns undefined to leave errors unchanged', () => {
expect(
classifyError(new TransportError('connection refused', false)),
).toBeUndefined()
expect(classifyError(new Error('unexpected failure'))).toBeUndefined()
})

it('handles non-Error values gracefully', () => {
expect(classifyError(undefined)).toBeUndefined()
expect(classifyError(null)).toBeUndefined()
expect(classifyError('string error')).toBeUndefined()
})
})

describe('beforeBreadcrumb logic', () => {
type MockBreadcrumb = {
category?: string
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function resolveConfig(config: ObservabilityConfig): ResolvedConfig {
},

shouldDropError: config.shouldDropError,
classifyError: config.classifyError,
}

return resolvedConfig
Expand Down
9 changes: 9 additions & 0 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ function initializeSentry(config: ResolvedConfig): void {
return null
}

const classification = config.classifyError?.(error)
if (classification === 'drop') {
return null
}
if (classification === 'warning') {
event.level = 'warning'
return event
}

if (error instanceof joseErrors.JWTExpired) {
return null
}
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ export interface ObservabilityConfig {
* Return `true` to drop the error, `false` to allow it through.
*/
shouldDropError?: (error: unknown) => boolean

/**
* Optional callback to classify errors before they are sent to Sentry.
* Return `'drop'` to suppress the error, `'warning'` to downgrade its
* severity level, or `undefined` to leave it unchanged.
*/
classifyError?: (error: unknown) => 'drop' | 'warning' | undefined
}

export interface ResolvedConfig {
Expand Down Expand Up @@ -173,6 +180,7 @@ export interface ResolvedConfig {
}

shouldDropError?: (error: unknown) => boolean
classifyError?: (error: unknown) => 'drop' | 'warning' | undefined
}

/** Context fields attached to MCP tool spans and error reports. */
Expand Down
Loading