diff --git a/packages/core/src/integrations/zoderrors.ts b/packages/core/src/integrations/zoderrors.ts index 28c50b4948cc..e0d870d00bdb 100644 --- a/packages/core/src/integrations/zoderrors.ts +++ b/packages/core/src/integrations/zoderrors.ts @@ -32,6 +32,7 @@ interface ZodIssue { expected?: unknown; received?: unknown; unionErrors?: unknown[]; + errors?: unknown[]; keys?: unknown[]; invalid_literal?: unknown; } @@ -43,7 +44,8 @@ interface ZodError extends Error { function originalExceptionIsZodError(originalException: unknown): originalException is ZodError { return ( isError(originalException) && - originalException.name === 'ZodError' && + // `zod/mini` (Zod 4) throws `$ZodError` + (originalException.name === 'ZodError' || originalException.name === '$ZodError') && Array.isArray((originalException as ZodError).issues) ); } @@ -75,6 +77,8 @@ export function flattenIssue(issue: ZodIssue): SingleLevelZodIssue { 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 4 nests union failures under `errors` instead of `unionErrors` + ...('errors' in issue ? { errors: JSON.stringify(issue.errors) } : {}), }; } diff --git a/packages/core/test/lib/integrations/zoderrrors.test.ts b/packages/core/test/lib/integrations/zoderrrors.test.ts index 984636408e5c..77ee103626a5 100644 --- a/packages/core/test/lib/integrations/zoderrrors.test.ts +++ b/packages/core/test/lib/integrations/zoderrrors.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it, test } from 'vitest'; import { z } from 'zod'; +import { z as z4 } from 'zod/v4'; +import * as z4mini from 'zod/v4/mini'; import { applyZodErrorsToEvent, flattenIssue, @@ -516,3 +518,41 @@ describe('formatIssueMessage()', () => { }); }); }); + +describe('Zod 4 errors', () => { + function createEvent(originalException: Error): Event { + return { exception: { values: [{ type: originalException.name, value: originalException.message }] } }; + } + + test('processes errors thrown by `zod/mini`', () => { + const zodError = z4mini.object({ foo: z4mini.string() }).safeParse({ foo: 123 }).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + expect(zodError.name).toBe('$ZodError'); + + const processedEvent = applyZodErrorsToEvent(100, false, createEvent(zodError), { originalException: zodError }); + + expect(processedEvent.exception?.values?.[0]?.value).toBe('Failed to validate keys: foo'); + expect(processedEvent.extra?.['zoderror.issues']).toEqual([ + expect.objectContaining({ code: 'invalid_type', expected: 'string', path: 'foo' }), + ]); + }); + + test('flattens nested union errors to a string', () => { + const zodError = z4.object({ value: z4.union([z4.string(), z4.number()]) }).safeParse({ value: true }).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + const processedEvent = applyZodErrorsToEvent(100, false, createEvent(zodError), { originalException: zodError }); + + expect(processedEvent.exception?.values?.[0]?.value).toBe('Failed to validate keys: value'); + const [issue] = processedEvent.extra?.['zoderror.issues'] as Array>; + expect(issue).toEqual(expect.objectContaining({ code: 'invalid_union', path: 'value' })); + expect(JSON.parse(issue?.errors as string)).toEqual([ + [expect.objectContaining({ code: 'invalid_type', expected: 'string' })], + [expect.objectContaining({ code: 'invalid_type', expected: 'number' })], + ]); + }); +});