Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(integrations): Add zod integration (#11144)
This adds a [Zod](https://github.com/colinhacks/zod) integration to sentry that adds better support for ZodError issues. Currently, the ZodError message is a formatted json string that gets truncated and the full list of issues are lost. - Adds the full list of issues to `extras['zoderror.issues']`. - Replaces the error message with a simple string. before ![image](https://github.com/getsentry/sentry-javascript/assets/1400464/835f4388-398b-42bf-9c6c-dae111207de8) ![image](https://github.com/getsentry/sentry-javascript/assets/1400464/1647b16d-3990-4726-805f-93ad863f71ea) after ![image](https://github.com/getsentry/sentry-javascript/assets/1400464/561751c3-1455-41f5-b700-8116daae419f) ![image](https://github.com/getsentry/sentry-javascript/assets/1400464/3c6df13a-6c0e-46fd-9631-80345743c061) ![image](https://github.com/getsentry/sentry-javascript/assets/1400464/1556cad3-2b78-42af-be1c-c8cb9d79fb4a) --------- Co-authored-by: Francesco Novy <francesco.novy@sentry.io>
- Loading branch information
Showing
10 changed files
with
227 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import type { IntegrationFn } from '@sentry/types'; | ||
import type { Event, EventHint } from '@sentry/types'; | ||
import { isError, truncate } from '@sentry/utils'; | ||
import { defineIntegration } from '../integration'; | ||
|
||
interface ZodErrorsOptions { | ||
key?: string; | ||
limit?: number; | ||
} | ||
|
||
const DEFAULT_LIMIT = 10; | ||
const INTEGRATION_NAME = 'ZodErrors'; | ||
|
||
// Simplified ZodIssue type definition | ||
interface ZodIssue { | ||
path: (string | number)[]; | ||
message?: string; | ||
expected?: string | number; | ||
received?: string | number; | ||
unionErrors?: unknown[]; | ||
keys?: unknown[]; | ||
} | ||
|
||
interface ZodError extends Error { | ||
issues: ZodIssue[]; | ||
|
||
get errors(): ZodError['issues']; | ||
} | ||
|
||
function originalExceptionIsZodError(originalException: unknown): originalException is ZodError { | ||
return ( | ||
isError(originalException) && | ||
originalException.name === 'ZodError' && | ||
Array.isArray((originalException as ZodError).errors) | ||
); | ||
} | ||
|
||
type SingleLevelZodIssue<T extends ZodIssue> = { | ||
[P in keyof T]: T[P] extends string | number | undefined | ||
? T[P] | ||
: T[P] extends unknown[] | ||
? string | undefined | ||
: unknown; | ||
}; | ||
|
||
/** | ||
* Formats child objects or arrays to a string | ||
* That is preserved when sent to Sentry | ||
*/ | ||
function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue<ZodIssue> { | ||
return { | ||
...issue, | ||
path: 'path' in issue && Array.isArray(issue.path) ? issue.path.join('.') : undefined, | ||
keys: 'keys' in issue ? JSON.stringify(issue.keys) : undefined, | ||
unionErrors: 'unionErrors' in issue ? JSON.stringify(issue.unionErrors) : undefined, | ||
}; | ||
} | ||
|
||
/** | ||
* Zod error message is a stringified version of ZodError.issues | ||
* This doesn't display well in the Sentry UI. Replace it with something shorter. | ||
*/ | ||
function formatIssueMessage(zodError: ZodError): string { | ||
const errorKeyMap = new Set<string | number | symbol>(); | ||
for (const iss of zodError.issues) { | ||
if (iss.path) errorKeyMap.add(iss.path[0]); | ||
} | ||
const errorKeys = Array.from(errorKeyMap); | ||
|
||
return `Failed to validate keys: ${truncate(errorKeys.join(', '), 100)}`; | ||
} | ||
|
||
/** | ||
* Applies ZodError issues to an event extras and replaces the error message | ||
*/ | ||
export function applyZodErrorsToEvent(limit: number, event: Event, hint?: EventHint): Event { | ||
if ( | ||
!event.exception || | ||
!event.exception.values || | ||
!hint || | ||
!hint.originalException || | ||
!originalExceptionIsZodError(hint.originalException) || | ||
hint.originalException.issues.length === 0 | ||
) { | ||
return event; | ||
} | ||
|
||
return { | ||
...event, | ||
exception: { | ||
...event.exception, | ||
values: [ | ||
{ | ||
...event.exception.values[0], | ||
value: formatIssueMessage(hint.originalException), | ||
}, | ||
...event.exception.values.slice(1), | ||
], | ||
}, | ||
extra: { | ||
...event.extra, | ||
'zoderror.issues': hint.originalException.errors.slice(0, limit).map(formatIssueTitle), | ||
}, | ||
}; | ||
} | ||
|
||
const _zodErrorsIntegration = ((options: ZodErrorsOptions = {}) => { | ||
const limit = options.limit || DEFAULT_LIMIT; | ||
|
||
return { | ||
name: INTEGRATION_NAME, | ||
processEvent(originalEvent, hint) { | ||
const processedEvent = applyZodErrorsToEvent(limit, originalEvent, hint); | ||
return processedEvent; | ||
}, | ||
}; | ||
}) satisfies IntegrationFn; | ||
|
||
export const zodErrorsIntegration = defineIntegration(_zodErrorsIntegration); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import type { Event, EventHint } from '@sentry/types'; | ||
|
||
import { applyZodErrorsToEvent } from '../../../src/integrations/zoderrors'; | ||
|
||
// Simplified type definition | ||
interface ZodIssue { | ||
code: string; | ||
path: (string | number)[]; | ||
expected?: string | number; | ||
received?: string | number; | ||
keys?: string[]; | ||
message?: string; | ||
} | ||
|
||
class ZodError extends Error { | ||
issues: ZodIssue[] = []; | ||
|
||
// https://github.com/colinhacks/zod/blob/8910033b861c842df59919e7d45e7f51cf8b76a2/src/ZodError.ts#L199C1-L211C4 | ||
constructor(issues: ZodIssue[]) { | ||
super(); | ||
|
||
const actualProto = new.target.prototype; | ||
if (Object.setPrototypeOf) { | ||
Object.setPrototypeOf(this, actualProto); | ||
} else { | ||
(this as any).__proto__ = actualProto; | ||
} | ||
|
||
this.name = 'ZodError'; | ||
this.issues = issues; | ||
} | ||
|
||
get errors() { | ||
return this.issues; | ||
} | ||
|
||
static create = (issues: ZodIssue[]) => { | ||
const error = new ZodError(issues); | ||
return error; | ||
}; | ||
} | ||
|
||
describe('applyZodErrorsToEvent()', () => { | ||
test('should not do anything if exception is not a ZodError', () => { | ||
const event: Event = {}; | ||
const eventHint: EventHint = { originalException: new Error() }; | ||
applyZodErrorsToEvent(100, event, eventHint); | ||
|
||
// no changes | ||
expect(event).toStrictEqual({}); | ||
}); | ||
|
||
test('should add ZodError issues to extras and format message', () => { | ||
const issues = [ | ||
{ | ||
code: 'invalid_type', | ||
expected: 'string', | ||
received: 'number', | ||
path: ['names', 1], | ||
keys: ['extra'], | ||
message: 'Invalid input: expected string, received number', | ||
}, | ||
] satisfies ZodIssue[]; | ||
const originalException = ZodError.create(issues); | ||
|
||
const event: Event = { | ||
exception: { | ||
values: [ | ||
{ | ||
type: 'Error', | ||
value: originalException.message, | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
const eventHint: EventHint = { originalException }; | ||
const processedEvent = applyZodErrorsToEvent(100, event, eventHint); | ||
|
||
expect(processedEvent.exception).toStrictEqual({ | ||
values: [ | ||
{ | ||
type: 'Error', | ||
value: 'Failed to validate keys: names', | ||
}, | ||
], | ||
}); | ||
|
||
expect(processedEvent.extra).toStrictEqual({ | ||
'zoderror.issues': [ | ||
{ | ||
...issues[0], | ||
path: issues[0].path.join('.'), | ||
keys: JSON.stringify(issues[0].keys), | ||
unionErrors: undefined, | ||
}, | ||
], | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters