From 965185cbb8bf801fcddebea3a0fead4e3f152c29 Mon Sep 17 00:00:00 2001 From: Jacob Hands Date: Thu, 23 Jan 2025 11:41:14 -0600 Subject: [PATCH] feat(core): Improve error formatting in ZodErrors integration (#15111) - Include full key path rather than the top level key in title - Improve message for validation issues with no path - Add option to include extended issue information as an attachment --- packages/core/package.json | 5 +- packages/core/src/integrations/zoderrors.ts | 170 +++++-- .../test/lib/integrations/zoderrrors.test.ts | 432 +++++++++++++++++- yarn.lock | 43 +- 4 files changed, 576 insertions(+), 74 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 06e8d253a628..7724f703833e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -61,5 +61,8 @@ "volta": { "extends": "../../package.json" }, - "sideEffects": false + "sideEffects": false, + "devDependencies": { + "zod": "^3.24.1" + } } diff --git a/packages/core/src/integrations/zoderrors.ts b/packages/core/src/integrations/zoderrors.ts index a408285800d9..4859ca5167fa 100644 --- a/packages/core/src/integrations/zoderrors.ts +++ b/packages/core/src/integrations/zoderrors.ts @@ -5,33 +5,45 @@ import { truncate } from '../utils-hoist/string'; interface ZodErrorsOptions { key?: string; + /** + * Limits the number of Zod errors inlined in each Sentry event. + * + * @default 10 + */ limit?: number; + /** + * Save full list of Zod issues as an attachment in Sentry + * + * @default false + */ + saveZodIssuesAsAttachment?: boolean; } const DEFAULT_LIMIT = 10; const INTEGRATION_NAME = 'ZodErrors'; -// Simplified ZodIssue type definition +/** + * Simplified ZodIssue type definition + */ interface ZodIssue { path: (string | number)[]; message?: string; - expected?: string | number; - received?: string | number; + expected?: unknown; + received?: unknown; unionErrors?: unknown[]; keys?: unknown[]; + invalid_literal?: 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) + Array.isArray((originalException as ZodError).issues) ); } @@ -45,9 +57,18 @@ type SingleLevelZodIssue = { /** * Formats child objects or arrays to a string - * That is preserved when sent to Sentry + * that is preserved when sent to Sentry. + * + * Without this, we end up with something like this in Sentry: + * + * [ + * [Object], + * [Object], + * [Object], + * [Object] + * ] */ -function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue { +export function flattenIssue(issue: ZodIssue): SingleLevelZodIssue { return { ...issue, path: 'path' in issue && Array.isArray(issue.path) ? issue.path.join('.') : undefined, @@ -56,64 +77,145 @@ function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue { }; } +/** + * Takes ZodError issue path array and returns a flattened version as a string. + * This makes it easier to display paths within a Sentry error message. + * + * Array indexes are normalized to reduce duplicate entries + * + * @param path ZodError issue path + * @returns flattened path + * + * @example + * flattenIssuePath([0, 'foo', 1, 'bar']) // -> '.foo..bar' + */ +export function flattenIssuePath(path: Array): string { + return path + .map(p => { + if (typeof p === 'number') { + return ''; + } else { + return p; + } + }) + .join('.'); +} + /** * 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 { +export function formatIssueMessage(zodError: ZodError): string { const errorKeyMap = new Set(); for (const iss of zodError.issues) { - if (iss.path?.[0]) { - errorKeyMap.add(iss.path[0]); + const issuePath = flattenIssuePath(iss.path); + if (issuePath.length > 0) { + errorKeyMap.add(issuePath); } } - const errorKeys = Array.from(errorKeyMap); + const errorKeys = Array.from(errorKeyMap); + if (errorKeys.length === 0) { + // If there are no keys, then we're likely validating the root + // variable rather than a key within an object. This attempts + // to extract what type it was that failed to validate. + // For example, z.string().parse(123) would return "string" here. + let rootExpectedType = 'variable'; + if (zodError.issues.length > 0) { + const iss = zodError.issues[0]; + if (iss !== undefined && 'expected' in iss && typeof iss.expected === 'string') { + rootExpectedType = iss.expected; + } + } + return `Failed to validate ${rootExpectedType}`; + } return `Failed to validate keys: ${truncate(errorKeys.join(', '), 100)}`; } /** - * Applies ZodError issues to an event extras and replaces the error message + * Applies ZodError issues to an event extra and replaces the error message */ -export function applyZodErrorsToEvent(limit: number, event: Event, hint?: EventHint): Event { +export function applyZodErrorsToEvent( + limit: number, + saveZodIssuesAsAttachment: boolean = false, + event: Event, + hint: EventHint, +): Event { if ( !event.exception?.values || - !hint?.originalException || + !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), + try { + const issuesToFlatten = saveZodIssuesAsAttachment + ? hint.originalException.issues + : hint.originalException.issues.slice(0, limit); + const flattenedIssues = issuesToFlatten.map(flattenIssue); + + if (saveZodIssuesAsAttachment) { + // 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({ + issues: flattenedIssues, + }), + }); + } + + return { + ...event, + exception: { + ...event.exception, + values: [ + { + ...event.exception.values[0], + value: formatIssueMessage(hint.originalException), + }, + ...event.exception.values.slice(1), + ], + }, + extra: { + ...event.extra, + 'zoderror.issues': flattenedIssues.slice(0, limit), + }, + }; + } catch (e) { + // Hopefully we never throw errors here, but record it + // with the event just in case. + return { + ...event, + extra: { + ...event.extra, + 'zoderrors sentry integration parse error': { + message: 'an exception was thrown while processing ZodError within applyZodErrorsToEvent()', + error: e instanceof Error ? `${e.name}: ${e.message}\n${e.stack}` : 'unknown', }, - ...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; + const limit = options.limit ?? DEFAULT_LIMIT; return { name: INTEGRATION_NAME, - processEvent(originalEvent, hint) { - const processedEvent = applyZodErrorsToEvent(limit, originalEvent, hint); + processEvent(originalEvent, hint): Event { + const processedEvent = applyZodErrorsToEvent(limit, options.saveZodIssuesAsAttachment, originalEvent, hint); return processedEvent; }, }; }) satisfies IntegrationFn; +/** + * Sentry integration to process Zod errors, making them easier to work with in Sentry. + */ export const zodErrorsIntegration = defineIntegration(_zodErrorsIntegration); diff --git a/packages/core/test/lib/integrations/zoderrrors.test.ts b/packages/core/test/lib/integrations/zoderrrors.test.ts index 924ee5dd27da..cd80e2347f36 100644 --- a/packages/core/test/lib/integrations/zoderrrors.test.ts +++ b/packages/core/test/lib/integrations/zoderrrors.test.ts @@ -1,6 +1,12 @@ +import { z } from 'zod'; import type { Event, EventHint } from '../../../src/types-hoist'; -import { applyZodErrorsToEvent } from '../../../src/integrations/zoderrors'; +import { + applyZodErrorsToEvent, + flattenIssue, + flattenIssuePath, + formatIssueMessage, +} from '../../../src/integrations/zoderrors'; // Simplified type definition interface ZodIssue { @@ -40,13 +46,13 @@ 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); + applyZodErrorsToEvent(100, false, event, eventHint); // no changes expect(event).toStrictEqual({}); }); - test('should add ZodError issues to extras and format message', () => { + test('should add ZodError issues to extra and format message', () => { const issues = [ { code: 'invalid_type', @@ -71,13 +77,13 @@ describe('applyZodErrorsToEvent()', () => { }; const eventHint: EventHint = { originalException }; - const processedEvent = applyZodErrorsToEvent(100, event, eventHint); + const processedEvent = applyZodErrorsToEvent(100, false, event, eventHint); expect(processedEvent.exception).toStrictEqual({ values: [ { type: 'Error', - value: 'Failed to validate keys: names', + value: 'Failed to validate keys: names.', }, ], }); @@ -92,5 +98,421 @@ describe('applyZodErrorsToEvent()', () => { }, ], }); + + // No attachments added + expect(eventHint.attachments).toBe(undefined); + }); + + test('should add all ZodError issues as attachment', () => { + const issues = [ + { + code: 'invalid_type', + expected: 'string', + received: 'number', + path: ['names', 1], + keys: ['extra'], + message: 'Invalid input: expected string, received number', + }, + { + code: 'invalid_type', + expected: 'string', + received: 'number', + path: ['foo', 1], + keys: ['extra2'], + 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(1, true, event, eventHint); + + expect(processedEvent.exception).toStrictEqual({ + values: [ + { + type: 'Error', + value: 'Failed to validate keys: names., foo.', + }, + ], + }); + + // Only adds the first issue to extra due to the limit + expect(processedEvent.extra).toStrictEqual({ + 'zoderror.issues': [ + { + ...issues[0], + path: issues[0]?.path.join('.'), + keys: JSON.stringify(issues[0]?.keys), + unionErrors: undefined, + }, + ], + }); + + // hint attachments contains the full issue list + expect(Array.isArray(eventHint.attachments)).toBe(true); + expect(eventHint.attachments?.length).toBe(1); + const attachment = eventHint.attachments?.[0]; + if (attachment === undefined) { + throw new Error('attachment is undefined'); + } + expect(attachment.filename).toBe('zod_issues.json'); + expect(JSON.parse(attachment.data.toString())).toMatchInlineSnapshot(` +Object { + "issues": Array [ + Object { + "code": "invalid_type", + "expected": "string", + "keys": "[\\"extra\\"]", + "message": "Invalid input: expected string, received number", + "path": "names.1", + "received": "number", + }, + Object { + "code": "invalid_type", + "expected": "string", + "keys": "[\\"extra2\\"]", + "message": "Invalid input: expected string, received number", + "path": "foo.1", + "received": "number", + }, + ], +} +`); + }); +}); + +describe('flattenIssue()', () => { + it('flattens path field', () => { + const zodError = z + .object({ + foo: z.string().min(1), + nested: z.object({ + bar: z.literal('baz'), + }), + }) + .safeParse({ + foo: '', + nested: { + bar: 'not-baz', + }, + }).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "too_small", + "exact": false, + "inclusive": true, + "message": "String must contain at least 1 character(s)", + "minimum": 1, + "path": Array [ + "foo", + ], + "type": "string", + }, + Object { + "code": "invalid_literal", + "expected": "baz", + "message": "Invalid literal value, expected \\"baz\\"", + "path": Array [ + "nested", + "bar", + ], + "received": "not-baz", + }, +] +`); + + const issues = zodError.issues; + expect(issues.length).toBe(2); + + // Format it for use in Sentry + expect(issues.map(flattenIssue)).toMatchInlineSnapshot(` +Array [ + Object { + "code": "too_small", + "exact": false, + "inclusive": true, + "keys": undefined, + "message": "String must contain at least 1 character(s)", + "minimum": 1, + "path": "foo", + "type": "string", + "unionErrors": undefined, + }, + Object { + "code": "invalid_literal", + "expected": "baz", + "keys": undefined, + "message": "Invalid literal value, expected \\"baz\\"", + "path": "nested.bar", + "received": "not-baz", + "unionErrors": undefined, + }, +] +`); + + expect(zodError.flatten(flattenIssue)).toMatchInlineSnapshot(` +Object { + "fieldErrors": Object { + "foo": Array [ + Object { + "code": "too_small", + "exact": false, + "inclusive": true, + "keys": undefined, + "message": "String must contain at least 1 character(s)", + "minimum": 1, + "path": "foo", + "type": "string", + "unionErrors": undefined, + }, + ], + "nested": Array [ + Object { + "code": "invalid_literal", + "expected": "baz", + "keys": undefined, + "message": "Invalid literal value, expected \\"baz\\"", + "path": "nested.bar", + "received": "not-baz", + "unionErrors": undefined, + }, + ], + }, + "formErrors": Array [], +} +`); + }); + + it('flattens keys field to string', () => { + const zodError = z + .object({ + foo: z.string().min(1), + }) + .strict() + .safeParse({ + foo: 'bar', + extra_key_abc: 'hello', + extra_key_def: 'world', + }).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "unrecognized_keys", + "keys": Array [ + "extra_key_abc", + "extra_key_def", + ], + "message": "Unrecognized key(s) in object: 'extra_key_abc', 'extra_key_def'", + "path": Array [], + }, +] +`); + + const issues = zodError.issues; + expect(issues.length).toBe(1); + + // Format it for use in Sentry + const iss = issues[0]; + if (iss === undefined) { + throw new Error('iss is undefined'); + } + const formattedIssue = flattenIssue(iss); + + // keys is now a string rather than array. + // Note: path is an empty string because the issue is at the root. + // TODO: Maybe somehow make it clearer that this is at the root? + expect(formattedIssue).toMatchInlineSnapshot(` +Object { + "code": "unrecognized_keys", + "keys": "[\\"extra_key_abc\\",\\"extra_key_def\\"]", + "message": "Unrecognized key(s) in object: 'extra_key_abc', 'extra_key_def'", + "path": "", + "unionErrors": undefined, +} +`); + expect(typeof formattedIssue.keys === 'string').toBe(true); + }); +}); + +describe('flattenIssuePath()', () => { + it('returns single path', () => { + expect(flattenIssuePath(['foo'])).toBe('foo'); + }); + + it('flattens nested string paths', () => { + expect(flattenIssuePath(['foo', 'bar'])).toBe('foo.bar'); + }); + + it('uses placeholder for path index within array', () => { + expect(flattenIssuePath([0, 'foo', 1, 'bar', 'baz'])).toBe('.foo..bar.baz'); + }); +}); + +describe('formatIssueMessage()', () => { + it('adds invalid keys to message', () => { + const zodError = z + .object({ + foo: z.string().min(1), + nested: z.object({ + bar: z.literal('baz'), + }), + }) + .safeParse({ + foo: '', + nested: { + bar: 'not-baz', + }, + }).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate keys: foo, nested.bar"'); + }); + + describe('adds expected type if root variable is invalid', () => { + test('object', () => { + const zodError = z + .object({ + foo: z.string().min(1), + }) + .safeParse(123).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "object", + "message": "Expected object, received number", + "path": Array [], + "received": "number", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate object"'); + }); + + test('number', () => { + const zodError = z.number().safeParse('123').error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "number", + "message": "Expected number, received string", + "path": Array [], + "received": "string", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate number"'); + }); + + test('string', () => { + const zodError = z.string().safeParse(123).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "string", + "message": "Expected string, received number", + "path": Array [], + "received": "number", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate string"'); + }); + + test('array', () => { + const zodError = z.string().array().safeParse('123').error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "array", + "message": "Expected array, received string", + "path": Array [], + "received": "string", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate array"'); + }); + + test('wrong type in array', () => { + const zodError = z.string().array().safeParse([123]).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "string", + "message": "Expected string, received number", + "path": Array [ + 0, + ], + "received": "number", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate keys: "'); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 5627089a7941..2e69bdb6d480 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7890,12 +7890,7 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -27426,16 +27421,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@4.2.3, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -27538,14 +27524,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -30335,16 +30314,7 @@ wrangler@^3.67.1: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@7.0.0, wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -30646,6 +30616,11 @@ zod@^3.22.3, zod@^3.22.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== +zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== + zone.js@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.12.0.tgz#a4a6e5fab6d34bd37d89c77e89ac2e6f4a3d2c30"