From fb69cd26070a23a3b6e3ab1b9aca3c9500861087 Mon Sep 17 00:00:00 2001 From: Jacob Hands Date: Tue, 11 Feb 2025 04:45:02 -0600 Subject: [PATCH] chore: Export Zod errors integration and add upstream improvements - Adds improvements based on feedback I got while PR'ing this to sentry-javascript: https://github.com/getsentry/sentry-javascript/pull/15111 - Exports zodErrorsIntegration in the root index.ts (missed this in the original PR) --- .changeset/itchy-rats-brush.md | 8 +++ packages/toucan-js/src/index.ts | 1 + .../src/integrations/zod/zoderrors.ts | 61 +++++++++++-------- 3 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 .changeset/itchy-rats-brush.md diff --git a/.changeset/itchy-rats-brush.md b/.changeset/itchy-rats-brush.md new file mode 100644 index 00000000..702c1275 --- /dev/null +++ b/.changeset/itchy-rats-brush.md @@ -0,0 +1,8 @@ +--- +'toucan-js': patch +--- + +chore: Export Zod errors integration and add upstream improvements + +- Adds improvements based on feedback I got while PR'ing this to sentry-javascript: https://github.com/getsentry/sentry-javascript/pull/15111 +- Exports zodErrorsIntegration in the root index.ts (missed this in the original PR) diff --git a/packages/toucan-js/src/index.ts b/packages/toucan-js/src/index.ts index 81ffd936..7ef169fa 100644 --- a/packages/toucan-js/src/index.ts +++ b/packages/toucan-js/src/index.ts @@ -5,6 +5,7 @@ export { extraErrorDataIntegration, rewriteFramesIntegration, sessionTimingIntegration, + zodErrorsIntegration } from './integrations'; export type { LinkedErrorsOptions, RequestDataOptions } from './integrations'; export { Toucan } from './sdk'; diff --git a/packages/toucan-js/src/integrations/zod/zoderrors.ts b/packages/toucan-js/src/integrations/zod/zoderrors.ts index 65942b10..30b96360 100644 --- a/packages/toucan-js/src/integrations/zod/zoderrors.ts +++ b/packages/toucan-js/src/integrations/zod/zoderrors.ts @@ -5,7 +5,6 @@ import { isError, truncate } from '@sentry/utils'; import { defineIntegration } from './integration'; import type { Event, EventHint } from '@sentry/types'; -import type { ZodError, ZodIssue } from 'zod'; const INTEGRATION_NAME = 'ZodErrors'; const DEFAULT_LIMIT = 10; @@ -18,7 +17,9 @@ interface ZodErrorsOptions { */ limit?: number; /** - * Optionally save full error info as an attachment in Sentry + * Save full list of Zod issues as an attachment in Sentry + * + * @default false */ saveAttachments?: boolean; } @@ -29,24 +30,28 @@ function originalExceptionIsZodError( return ( isError(originalException) && originalException.name === 'ZodError' && - Array.isArray((originalException as ZodError).errors) + Array.isArray((originalException as ZodError).issues) ); } /** * Simplified ZodIssue type definition */ -type SimpleZodIssue = { - path: Array; +interface ZodIssue { + path: (string | number)[]; message?: string; expected?: unknown; received?: unknown; unionErrors?: unknown[]; keys?: unknown[]; invalid_literal?: unknown; -}; +} -type SingleLevelZodIssue = { +interface ZodError extends Error { + issues: ZodIssue[]; +} + +type SingleLevelZodIssue = { [P in keyof T]: T[P] extends string | number | undefined ? T[P] : T[P] extends unknown[] @@ -67,9 +72,7 @@ type SingleLevelZodIssue = { * [Object] * ] */ -export function flattenIssue( - issue: ZodIssue, -): SingleLevelZodIssue { +export function flattenIssue(issue: ZodIssue): SingleLevelZodIssue { return { ...issue, path: @@ -128,7 +131,11 @@ export function formatIssueMessage(zodError: ZodError): string { let rootExpectedType = 'variable'; if (zodError.issues.length > 0) { const iss = zodError.issues[0]; - if ('expected' in iss && typeof iss.expected === 'string') { + if ( + iss !== undefined && + 'expected' in iss && + typeof iss.expected === 'string' + ) { rootExpectedType = iss.expected; } } @@ -138,19 +145,17 @@ export function formatIssueMessage(zodError: ZodError): string { } /** - * Applies ZodError issues to an event context and replaces the error message + * Applies ZodError issues to an event extra and replaces the error message */ -function applyZodErrorsToEvent( +export function applyZodErrorsToEvent( limit: number, event: Event, - saveAttachments?: boolean, - hint?: EventHint, + saveAttachments: boolean = false, + hint: EventHint, ): Event { if ( - event.exception === undefined || - event.exception.values === undefined || - hint === undefined || - hint.originalException === undefined || + !event.exception?.values || + !hint.originalException || !originalExceptionIsZodError(hint.originalException) || hint.originalException.issues.length === 0 ) { @@ -158,18 +163,21 @@ function applyZodErrorsToEvent( } try { - const flattenedIssues = hint.originalException.errors.map(flattenIssue); - - if (saveAttachments === true) { - // Add an attachment with all issues (no limits), as well as the default - // flatten format to see if it's preferred over our custom flatten format. + const issuesToFlatten = saveAttachments + ? hint.originalException.issues + : hint.originalException.issues.slice(0, limit); + const flattenedIssues = issuesToFlatten.map(flattenIssue); + + if (saveAttachments) { + // Sometimes having the full error details can be helpful. + // Attachments have much higher limits, so we can include the full list of issues. if (!Array.isArray(hint.attachments)) { hint.attachments = []; } hint.attachments.push({ filename: 'zod_issues.json', data: JSON.stringify({ - issueDetails: hint.originalException.flatten(flattenIssue), + issues: flattenedIssues, }), }); } @@ -199,7 +207,8 @@ function applyZodErrorsToEvent( extra: { ...event.extra, 'zoderrors sentry integration parse error': { - message: `an exception was thrown while processing ZodError within applyZodErrorsToEvent()`, + message: + 'an exception was thrown while processing ZodError within applyZodErrorsToEvent()', error: e instanceof Error ? `${e.name}: ${e.message}\n${e.stack}`