Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/itchy-rats-brush.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions packages/toucan-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
extraErrorDataIntegration,
rewriteFramesIntegration,
sessionTimingIntegration,
zodErrorsIntegration
} from './integrations';
export type { LinkedErrorsOptions, RequestDataOptions } from './integrations';
export { Toucan } from './sdk';
Expand Down
61 changes: 35 additions & 26 deletions packages/toucan-js/src/integrations/zod/zoderrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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<string | number>;
interface ZodIssue {
path: (string | number)[];
message?: string;
expected?: unknown;
received?: unknown;
unionErrors?: unknown[];
keys?: unknown[];
invalid_literal?: unknown;
};
}

type SingleLevelZodIssue<T extends SimpleZodIssue> = {
interface ZodError extends Error {
issues: ZodIssue[];
}

type SingleLevelZodIssue<T extends ZodIssue> = {
[P in keyof T]: T[P] extends string | number | undefined
? T[P]
: T[P] extends unknown[]
Expand All @@ -67,9 +72,7 @@ type SingleLevelZodIssue<T extends SimpleZodIssue> = {
* [Object]
* ]
*/
export function flattenIssue(
issue: ZodIssue,
): SingleLevelZodIssue<SimpleZodIssue> {
export function flattenIssue(issue: ZodIssue): SingleLevelZodIssue<ZodIssue> {
return {
...issue,
path:
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -138,38 +145,39 @@ 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
) {
return event;
}

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,
}),
});
}
Expand Down Expand Up @@ -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}`
Expand Down