@vyro-x/sentry 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +77 -0
  2. package/dist/cjs/browser.d.ts +29 -0
  3. package/dist/cjs/browser.d.ts.map +1 -0
  4. package/dist/cjs/browser.js +57 -0
  5. package/dist/cjs/config.d.ts +8 -0
  6. package/dist/cjs/config.d.ts.map +1 -0
  7. package/dist/cjs/config.js +16 -0
  8. package/dist/cjs/index.d.ts +4 -0
  9. package/dist/cjs/index.d.ts.map +1 -0
  10. package/dist/cjs/index.js +13 -0
  11. package/dist/cjs/nextjs.d.ts +29 -0
  12. package/dist/cjs/nextjs.d.ts.map +1 -0
  13. package/dist/cjs/nextjs.js +52 -0
  14. package/dist/cjs/node.d.ts +39 -0
  15. package/dist/cjs/node.d.ts.map +1 -0
  16. package/dist/cjs/node.js +84 -0
  17. package/dist/cjs/package.json +3 -0
  18. package/dist/cjs/scrub.d.ts +36 -0
  19. package/dist/cjs/scrub.d.ts.map +1 -0
  20. package/dist/cjs/scrub.js +70 -0
  21. package/dist/cjs/scrubbers.d.ts +27 -0
  22. package/dist/cjs/scrubbers.d.ts.map +1 -0
  23. package/dist/cjs/scrubbers.js +51 -0
  24. package/dist/esm/browser.d.ts +29 -0
  25. package/dist/esm/browser.d.ts.map +1 -0
  26. package/dist/esm/browser.js +31 -0
  27. package/dist/esm/config.d.ts +8 -0
  28. package/dist/esm/config.d.ts.map +1 -0
  29. package/dist/esm/config.js +12 -0
  30. package/dist/esm/index.d.ts +4 -0
  31. package/dist/esm/index.d.ts.map +1 -0
  32. package/dist/esm/index.js +5 -0
  33. package/dist/esm/nextjs.d.ts +29 -0
  34. package/dist/esm/nextjs.d.ts.map +1 -0
  35. package/dist/esm/nextjs.js +26 -0
  36. package/dist/esm/node.d.ts +39 -0
  37. package/dist/esm/node.d.ts.map +1 -0
  38. package/dist/esm/node.js +57 -0
  39. package/dist/esm/package.json +3 -0
  40. package/dist/esm/scrub.d.ts +36 -0
  41. package/dist/esm/scrub.d.ts.map +1 -0
  42. package/dist/esm/scrub.js +66 -0
  43. package/dist/esm/scrubbers.d.ts +27 -0
  44. package/dist/esm/scrubbers.d.ts.map +1 -0
  45. package/dist/esm/scrubbers.js +47 -0
  46. package/package.json +86 -0
  47. package/src/browser.ts +50 -0
  48. package/src/config.ts +11 -0
  49. package/src/index.ts +10 -0
  50. package/src/nextjs.ts +48 -0
  51. package/src/node.ts +81 -0
  52. package/src/scrub.test.ts +93 -0
  53. package/src/scrub.ts +85 -0
  54. package/src/scrubbers.ts +68 -0
package/src/nextjs.ts ADDED
@@ -0,0 +1,48 @@
1
+ import * as Sentry from '@sentry/nextjs'
2
+ import { DEFAULT_TRACES_SAMPLE_RATE, resolveEnvironment } from './config'
3
+ import type { SentryScrubOptions } from './scrub'
4
+ import { createSentryScrubbers } from './scrubbers'
5
+
6
+ /** Re-exported SDK, for the rare escape hatch. Prefer the helper below. */
7
+ export { Sentry }
8
+ export const captureException = Sentry.captureException
9
+
10
+ /**
11
+ * Options for `initSentry`. Used by all three Next.js runtimes
12
+ * (`sentry.server.config.ts`, `sentry.client.config.ts`, `sentry.edge.config.ts`).
13
+ */
14
+ export interface NextSentryInitOptions {
15
+ /** DSN; defaults to `process.env.NEXT_PUBLIC_SENTRY_DSN` then `process.env.SENTRY_DSN`. */
16
+ dsn?: string
17
+ /** Logical name shown as `server_name` (server/edge runtimes). */
18
+ serverName?: string
19
+ /** Performance tracing sample rate. Defaults to `0.2`. */
20
+ tracesSampleRate?: number
21
+ /** Overrides the resolved `environment`. */
22
+ environment?: string
23
+ /** Scrubber options (extra keys/patterns, placeholder, …). */
24
+ scrub?: SentryScrubOptions
25
+ /** Extra `@sentry/nextjs` init options, merged last so they can override defaults. */
26
+ init?: Parameters<typeof Sentry.init>[0]
27
+ }
28
+
29
+ /**
30
+ * Initialises `@sentry/nextjs` (server, client or edge) for the Vyro website with
31
+ * secret-scrubbing already wired in. Idempotent and a no-op when no DSN is configured.
32
+ */
33
+ export function initSentry(options: NextSentryInitOptions = {}): void {
34
+ const dsn =
35
+ options.dsn ??
36
+ (typeof process !== 'undefined'
37
+ ? (process.env.NEXT_PUBLIC_SENTRY_DSN ?? process.env.SENTRY_DSN)
38
+ : undefined)
39
+ if (!dsn) return
40
+ Sentry.init({
41
+ ...createSentryScrubbers(options.scrub),
42
+ dsn,
43
+ serverName: options.serverName,
44
+ environment: options.environment ?? resolveEnvironment(),
45
+ tracesSampleRate: options.tracesSampleRate ?? DEFAULT_TRACES_SAMPLE_RATE,
46
+ ...options.init,
47
+ })
48
+ }
package/src/node.ts ADDED
@@ -0,0 +1,81 @@
1
+ import * as Sentry from '@sentry/node'
2
+ import { DEFAULT_TRACES_SAMPLE_RATE, resolveEnvironment } from './config'
3
+ import type { SentryScrubOptions } from './scrub'
4
+ import { createSentryScrubbers } from './scrubbers'
5
+
6
+ /** Re-exported SDK, for the rare escape hatch. Prefer the helpers below. */
7
+ export { Sentry }
8
+ /**
9
+ * Express error handler (Sentry v8+). Call after your routes are registered so
10
+ * errors bubbling out of the app are reported. Replaces the removed
11
+ * `Sentry.Handlers.errorHandler()`.
12
+ */
13
+ export const setupExpressErrorHandler = Sentry.setupExpressErrorHandler
14
+
15
+ let initialised = false
16
+
17
+ export interface NodeSentryInitOptions {
18
+ /** Logical service name — shown in Sentry as `server_name`. */
19
+ serverName: string
20
+ /** DSN; defaults to `process.env.SENTRY_DSN`. When absent, init is skipped. */
21
+ dsn?: string
22
+ /** Performance tracing sample rate. Defaults to `0.2`. */
23
+ tracesSampleRate?: number
24
+ /** Overrides the resolved `environment`. */
25
+ environment?: string
26
+ /** Scrubber options (extra keys/patterns, placeholder, …). */
27
+ scrub?: SentryScrubOptions
28
+ /** Extra `@sentry/node` init options, merged last so they can override defaults. */
29
+ init?: Parameters<typeof Sentry.init>[0]
30
+ }
31
+
32
+ /**
33
+ * Initialises `@sentry/node` for a Vyro service with secret-scrubbing already wired
34
+ * in. Idempotent and a no-op when no DSN is configured.
35
+ */
36
+ export function initSentry(options: NodeSentryInitOptions): void {
37
+ if (initialised) return
38
+ const dsn = options.dsn ?? process.env.SENTRY_DSN
39
+ if (!dsn) {
40
+ initialised = true
41
+ return
42
+ }
43
+ Sentry.init({
44
+ ...createSentryScrubbers(options.scrub),
45
+ dsn,
46
+ serverName: options.serverName,
47
+ environment: options.environment ?? resolveEnvironment(),
48
+ tracesSampleRate: options.tracesSampleRate ?? DEFAULT_TRACES_SAMPLE_RATE,
49
+ ...options.init,
50
+ })
51
+ initialised = true
52
+ }
53
+
54
+ export interface CaptureContext {
55
+ tags?: Record<string, string>
56
+ extra?: Record<string, unknown>
57
+ }
58
+
59
+ /**
60
+ * Reports an exception with optional tags/extra. No-op until `initSentry` has run
61
+ * with a DSN. Consolidates the helper previously copied into individual services.
62
+ */
63
+ export function captureException(
64
+ error: unknown,
65
+ context?: CaptureContext,
66
+ ): void {
67
+ if (!initialised) return
68
+ if (!context) {
69
+ Sentry.captureException(error)
70
+ return
71
+ }
72
+ Sentry.withScope((scope) => {
73
+ if (context.tags) {
74
+ for (const [key, value] of Object.entries(context.tags)) {
75
+ scope.setTag(key, value)
76
+ }
77
+ }
78
+ if (context.extra) scope.setExtras(context.extra)
79
+ Sentry.captureException(error)
80
+ })
81
+ }
@@ -0,0 +1,93 @@
1
+ import { createSentryScrubbers, scrubSentrySensitiveData } from './index'
2
+
3
+ describe('scrubSentrySensitiveData', () => {
4
+ it('redacts authorization and api-key style headers, case-insensitively', () => {
5
+ const headers: Record<string, unknown> = {
6
+ Authorization: 'Bearer abc123',
7
+ 'x-api-key': 'secret-key',
8
+ 'X-Api-Key': 'other',
9
+ apikey: 'k',
10
+ api_key: 'k2',
11
+ 'x-hasura-admin-secret': 'hasura',
12
+ 'content-type': 'application/json',
13
+ }
14
+ scrubSentrySensitiveData(headers)
15
+ expect(headers.Authorization).toBe('[Filtered]')
16
+ expect(headers['x-api-key']).toBe('[Filtered]')
17
+ expect(headers['X-Api-Key']).toBe('[Filtered]')
18
+ expect(headers.apikey).toBe('[Filtered]')
19
+ expect(headers.api_key).toBe('[Filtered]')
20
+ expect(headers['x-hasura-admin-secret']).toBe('[Filtered]')
21
+ expect(headers['content-type']).toBe('application/json')
22
+ })
23
+
24
+ it('walks nested objects and arrays', () => {
25
+ const value = {
26
+ list: [{ token: 't' }, { safe: 'ok' }],
27
+ nested: { deep: { secret: 's', keep: 1 } },
28
+ }
29
+ scrubSentrySensitiveData(value)
30
+ expect(value.list[0].token).toBe('[Filtered]')
31
+ expect((value.list[1] as { safe: string }).safe).toBe('ok')
32
+ expect(value.nested.deep.secret).toBe('[Filtered]')
33
+ expect(value.nested.deep.keep).toBe(1)
34
+ })
35
+
36
+ it('ignores primitives and null without throwing', () => {
37
+ expect(() => scrubSentrySensitiveData(null)).not.toThrow()
38
+ expect(() => scrubSentrySensitiveData('string')).not.toThrow()
39
+ expect(() => scrubSentrySensitiveData(42)).not.toThrow()
40
+ })
41
+
42
+ it('handles circular references', () => {
43
+ const a: Record<string, unknown> = { token: 't' }
44
+ a.self = a
45
+ expect(() => scrubSentrySensitiveData(a)).not.toThrow()
46
+ expect(a.token).toBe('[Filtered]')
47
+ })
48
+
49
+ it('supports custom placeholder and extra keys', () => {
50
+ const value = { requestId: 'abc', keep: 'y' }
51
+ scrubSentrySensitiveData(value, {
52
+ placeholder: '***',
53
+ extraKeys: ['requestid'],
54
+ })
55
+ expect(value.requestId).toBe('***')
56
+ expect(value.keep).toBe('y')
57
+ })
58
+ })
59
+
60
+ describe('createSentryScrubbers', () => {
61
+ it('scrubs request headers/cookies and returns the same event', () => {
62
+ const { beforeSend } = createSentryScrubbers()
63
+ const event = {
64
+ request: {
65
+ headers: { authorization: 'Bearer x', accept: '*/*' },
66
+ cookies: { session: 'abc', 'x-api-key': 'leak' },
67
+ },
68
+ extra: { apiKey: 'leak2', note: 'keep' },
69
+ }
70
+ const result = beforeSend(event)
71
+ expect(result).toBe(event)
72
+ expect(event.request.headers.authorization).toBe('[Filtered]')
73
+ expect(event.request.headers.accept).toBe('*/*')
74
+ expect(event.request.cookies['x-api-key']).toBe('[Filtered]')
75
+ expect(event.extra.apiKey).toBe('[Filtered]')
76
+ expect(event.extra.note).toBe('keep')
77
+ })
78
+
79
+ it('scrubs breadcrumb data and returns the same breadcrumb', () => {
80
+ const { beforeBreadcrumb } = createSentryScrubbers()
81
+ const breadcrumb = {
82
+ category: 'fetch',
83
+ data: {
84
+ request_headers: { 'x-api-key': 'leak' },
85
+ status_code: 200,
86
+ },
87
+ }
88
+ const result = beforeBreadcrumb(breadcrumb)
89
+ expect(result).toBe(breadcrumb)
90
+ expect(breadcrumb.data.request_headers['x-api-key']).toBe('[Filtered]')
91
+ expect(breadcrumb.data.status_code).toBe(200)
92
+ })
93
+ })
package/src/scrub.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * SDK-agnostic Sentry scrubbing helpers.
3
+ *
4
+ * This package intentionally does NOT depend on any `@sentry/*` package so it can
5
+ * be dropped into services and frontends running different Sentry SDK versions
6
+ * (`@sentry/node`, `@sentry/react`, `@sentry/nextjs`, …). The public `beforeSend` /
7
+ * `beforeBreadcrumb` helpers use a generic identity signature (`<T>(x: T) => T`) so
8
+ * they satisfy every SDK's `beforeSend` / `beforeBreadcrumb` option type while
9
+ * operating on the event purely at runtime.
10
+ */
11
+
12
+ /**
13
+ * Default set of key/header-name fragments that get redacted (case-insensitive).
14
+ * Matches `authorization`, `x-api-key` and every `api-key` / `apikey` / `api_key`
15
+ * variant, `x-hasura-admin-secret`, `access-token` / `refresh-token` / `token`,
16
+ * `secret`, `cookie`, `password` and `credential`.
17
+ */
18
+ export const DEFAULT_SENSITIVE_KEY_PATTERN =
19
+ /authorization|api[-_ ]?key|x-hasura-admin-secret|access[-_ ]?token|refresh[-_ ]?token|token|secret|cookie|password|credential/i
20
+
21
+ export interface SentryScrubOptions {
22
+ /** Extra regexes matched against key/header names (case-insensitive recommended). */
23
+ extraPatterns?: RegExp[]
24
+ /** Extra literal substrings matched against key/header names (case-insensitive). */
25
+ extraKeys?: string[]
26
+ /** Replace the whole default pattern instead of extending it. */
27
+ pattern?: RegExp
28
+ /** Value written in place of a redacted entry. Defaults to `"[Filtered]"`. */
29
+ placeholder?: string
30
+ /** Maximum object depth to walk. Defaults to `6`. */
31
+ maxDepth?: number
32
+ }
33
+
34
+ const buildMatcher = (
35
+ options: SentryScrubOptions,
36
+ ): ((key: string) => boolean) => {
37
+ const patterns: RegExp[] = [
38
+ options.pattern ?? DEFAULT_SENSITIVE_KEY_PATTERN,
39
+ ...(options.extraPatterns ?? []),
40
+ ]
41
+ const keys = (options.extraKeys ?? []).map((key) => key.toLowerCase())
42
+ return (key: string): boolean => {
43
+ if (patterns.some((pattern) => pattern.test(key))) return true
44
+ if (keys.length === 0) return false
45
+ const lower = key.toLowerCase()
46
+ return keys.some((needle) => lower.includes(needle))
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Recursively redacts sensitive keys of `value` **in place**. Safe on any input:
52
+ * primitives and `null`/`undefined` are ignored, arrays are walked, and circular
53
+ * references are handled. Sensitive values are replaced with the placeholder.
54
+ */
55
+ export const scrubSentrySensitiveData = (
56
+ value: unknown,
57
+ options: SentryScrubOptions = {},
58
+ ): void => {
59
+ const placeholder = options.placeholder ?? '[Filtered]'
60
+ const maxDepth = options.maxDepth ?? 6
61
+ const isSensitive = buildMatcher(options)
62
+ const seen = new WeakSet<object>()
63
+
64
+ const walk = (node: unknown, depth: number): void => {
65
+ if (!node || typeof node !== 'object' || depth > maxDepth) return
66
+ if (seen.has(node as object)) return
67
+ seen.add(node as object)
68
+
69
+ if (Array.isArray(node)) {
70
+ for (const item of node) walk(item, depth + 1)
71
+ return
72
+ }
73
+
74
+ const record = node as Record<string, unknown>
75
+ for (const key of Object.keys(record)) {
76
+ if (isSensitive(key)) {
77
+ record[key] = placeholder
78
+ continue
79
+ }
80
+ walk(record[key], depth + 1)
81
+ }
82
+ }
83
+
84
+ walk(value, 0)
85
+ }
@@ -0,0 +1,68 @@
1
+ import { type SentryScrubOptions, scrubSentrySensitiveData } from './scrub'
2
+
3
+ /**
4
+ * A pair of scrubbing hooks. The signatures are generic identity functions so the
5
+ * object can be spread straight into any SDK's `Sentry.init({ ... })` regardless of
6
+ * its `Event` / `Breadcrumb` types.
7
+ */
8
+ export interface SentryScrubbers {
9
+ beforeSend: <T>(event: T) => T
10
+ beforeBreadcrumb: <T>(breadcrumb: T) => T
11
+ }
12
+
13
+ const scrubEvent = (event: unknown, options: SentryScrubOptions): void => {
14
+ if (!event || typeof event !== 'object') return
15
+ const record = event as Record<string, unknown>
16
+
17
+ const request = record.request as Record<string, unknown> | undefined
18
+ if (request) {
19
+ scrubSentrySensitiveData(request.headers, options)
20
+ scrubSentrySensitiveData(request.cookies, options)
21
+ scrubSentrySensitiveData(request.data, options)
22
+ }
23
+
24
+ scrubSentrySensitiveData(record.contexts, options)
25
+ scrubSentrySensitiveData(record.extra, options)
26
+ scrubSentrySensitiveData(record.tags, options)
27
+ }
28
+
29
+ const scrubBreadcrumb = (
30
+ breadcrumb: unknown,
31
+ options: SentryScrubOptions,
32
+ ): void => {
33
+ if (!breadcrumb || typeof breadcrumb !== 'object') return
34
+ scrubSentrySensitiveData(
35
+ (breadcrumb as Record<string, unknown>).data,
36
+ options,
37
+ )
38
+ }
39
+
40
+ /**
41
+ * Builds `beforeSend` / `beforeBreadcrumb` hooks that redact authorization headers,
42
+ * API keys, tokens and other secrets from outgoing Sentry events and breadcrumbs.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import * as Sentry from "@sentry/node"
47
+ * import { createSentryScrubbers } from "@vyro-x/sentry"
48
+ *
49
+ * Sentry.init({
50
+ * ...createSentryScrubbers(),
51
+ * dsn: process.env.SENTRY_DSN,
52
+ * })
53
+ * ```
54
+ */
55
+ export function createSentryScrubbers(
56
+ options: SentryScrubOptions = {},
57
+ ): SentryScrubbers {
58
+ return {
59
+ beforeSend: <T>(event: T): T => {
60
+ scrubEvent(event, options)
61
+ return event
62
+ },
63
+ beforeBreadcrumb: <T>(breadcrumb: T): T => {
64
+ scrubBreadcrumb(breadcrumb, options)
65
+ return breadcrumb
66
+ },
67
+ }
68
+ }