@@ -5,33 +5,45 @@ import { truncate } from '../utils-hoist/string';
5
5
6
6
interface ZodErrorsOptions {
7
7
key ?: string ;
8
+ /**
9
+ * Limits the number of Zod errors inlined in each Sentry event.
10
+ *
11
+ * @default 10
12
+ */
8
13
limit ?: number ;
14
+ /**
15
+ * Save full list of Zod issues as an attachment in Sentry
16
+ *
17
+ * @default false
18
+ */
19
+ saveZodIssuesAsAttachment ?: boolean ;
9
20
}
10
21
11
22
const DEFAULT_LIMIT = 10 ;
12
23
const INTEGRATION_NAME = 'ZodErrors' ;
13
24
14
- // Simplified ZodIssue type definition
25
+ /**
26
+ * Simplified ZodIssue type definition
27
+ */
15
28
interface ZodIssue {
16
29
path : ( string | number ) [ ] ;
17
30
message ?: string ;
18
- expected ?: string | number ;
19
- received ?: string | number ;
31
+ expected ?: unknown ;
32
+ received ?: unknown ;
20
33
unionErrors ?: unknown [ ] ;
21
34
keys ?: unknown [ ] ;
35
+ invalid_literal ?: unknown ;
22
36
}
23
37
24
38
interface ZodError extends Error {
25
39
issues : ZodIssue [ ] ;
26
-
27
- get errors ( ) : ZodError [ 'issues' ] ;
28
40
}
29
41
30
42
function originalExceptionIsZodError ( originalException : unknown ) : originalException is ZodError {
31
43
return (
32
44
isError ( originalException ) &&
33
45
originalException . name === 'ZodError' &&
34
- Array . isArray ( ( originalException as ZodError ) . errors )
46
+ Array . isArray ( ( originalException as ZodError ) . issues )
35
47
) ;
36
48
}
37
49
@@ -45,9 +57,18 @@ type SingleLevelZodIssue<T extends ZodIssue> = {
45
57
46
58
/**
47
59
* Formats child objects or arrays to a string
48
- * That is preserved when sent to Sentry
60
+ * that is preserved when sent to Sentry.
61
+ *
62
+ * Without this, we end up with something like this in Sentry:
63
+ *
64
+ * [
65
+ * [Object],
66
+ * [Object],
67
+ * [Object],
68
+ * [Object]
69
+ * ]
49
70
*/
50
- function formatIssueTitle ( issue : ZodIssue ) : SingleLevelZodIssue < ZodIssue > {
71
+ export function flattenIssue ( issue : ZodIssue ) : SingleLevelZodIssue < ZodIssue > {
51
72
return {
52
73
...issue ,
53
74
path : 'path' in issue && Array . isArray ( issue . path ) ? issue . path . join ( '.' ) : undefined ,
@@ -56,26 +77,70 @@ function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue<ZodIssue> {
56
77
} ;
57
78
}
58
79
80
+ /**
81
+ * Takes ZodError issue path array and returns a flattened version as a string.
82
+ * This makes it easier to display paths within a Sentry error message.
83
+ *
84
+ * Array indexes are normalized to reduce duplicate entries
85
+ *
86
+ * @param path ZodError issue path
87
+ * @returns flattened path
88
+ *
89
+ * @example
90
+ * flattenIssuePath([0, 'foo', 1, 'bar']) // -> '<array>.foo.<array>.bar'
91
+ */
92
+ export function flattenIssuePath ( path : Array < string | number > ) : string {
93
+ return path
94
+ . map ( p => {
95
+ if ( typeof p === 'number' ) {
96
+ return '<array>' ;
97
+ } else {
98
+ return p ;
99
+ }
100
+ } )
101
+ . join ( '.' ) ;
102
+ }
103
+
59
104
/**
60
105
* Zod error message is a stringified version of ZodError.issues
61
106
* This doesn't display well in the Sentry UI. Replace it with something shorter.
62
107
*/
63
- function formatIssueMessage ( zodError : ZodError ) : string {
108
+ export function formatIssueMessage ( zodError : ZodError ) : string {
64
109
const errorKeyMap = new Set < string | number | symbol > ( ) ;
65
110
for ( const iss of zodError . issues ) {
66
- if ( iss . path && iss . path [ 0 ] ) {
67
- errorKeyMap . add ( iss . path [ 0 ] ) ;
111
+ const issuePath = flattenIssuePath ( iss . path ) ;
112
+ if ( issuePath . length > 0 ) {
113
+ errorKeyMap . add ( issuePath ) ;
68
114
}
69
115
}
70
- const errorKeys = Array . from ( errorKeyMap ) ;
71
116
117
+ const errorKeys = Array . from ( errorKeyMap ) ;
118
+ if ( errorKeys . length === 0 ) {
119
+ // If there are no keys, then we're likely validating the root
120
+ // variable rather than a key within an object. This attempts
121
+ // to extract what type it was that failed to validate.
122
+ // For example, z.string().parse(123) would return "string" here.
123
+ let rootExpectedType = 'variable' ;
124
+ if ( zodError . issues . length > 0 ) {
125
+ const iss = zodError . issues [ 0 ] ;
126
+ if ( iss !== undefined && 'expected' in iss && typeof iss . expected === 'string' ) {
127
+ rootExpectedType = iss . expected ;
128
+ }
129
+ }
130
+ return `Failed to validate ${ rootExpectedType } ` ;
131
+ }
72
132
return `Failed to validate keys: ${ truncate ( errorKeys . join ( ', ' ) , 100 ) } ` ;
73
133
}
74
134
75
135
/**
76
- * Applies ZodError issues to an event extras and replaces the error message
136
+ * Applies ZodError issues to an event extra and replaces the error message
77
137
*/
78
- export function applyZodErrorsToEvent ( limit : number , event : Event , hint ?: EventHint ) : Event {
138
+ export function applyZodErrorsToEvent (
139
+ limit : number ,
140
+ saveZodIssuesAsAttachment : boolean = false ,
141
+ event : Event ,
142
+ hint : EventHint ,
143
+ ) : Event {
79
144
if (
80
145
! event . exception ||
81
146
! event . exception . values ||
@@ -87,35 +152,72 @@ export function applyZodErrorsToEvent(limit: number, event: Event, hint?: EventH
87
152
return event ;
88
153
}
89
154
90
- return {
91
- ...event ,
92
- exception : {
93
- ...event . exception ,
94
- values : [
95
- {
96
- ...event . exception . values [ 0 ] ,
97
- value : formatIssueMessage ( hint . originalException ) ,
155
+ try {
156
+ const issuesToFlatten = saveZodIssuesAsAttachment
157
+ ? hint . originalException . issues
158
+ : hint . originalException . issues . slice ( 0 , limit ) ;
159
+ const flattenedIssues = issuesToFlatten . map ( flattenIssue ) ;
160
+
161
+ if ( saveZodIssuesAsAttachment ) {
162
+ // Sometimes having the full error details can be helpful.
163
+ // Attachments have much higher limits, so we can include the full list of issues.
164
+ if ( ! Array . isArray ( hint . attachments ) ) {
165
+ hint . attachments = [ ] ;
166
+ }
167
+ hint . attachments . push ( {
168
+ filename : 'zod_issues.json' ,
169
+ data : JSON . stringify ( {
170
+ issues : flattenedIssues ,
171
+ } ) ,
172
+ } ) ;
173
+ }
174
+
175
+ return {
176
+ ...event ,
177
+ exception : {
178
+ ...event . exception ,
179
+ values : [
180
+ {
181
+ ...event . exception . values [ 0 ] ,
182
+ value : formatIssueMessage ( hint . originalException ) ,
183
+ } ,
184
+ ...event . exception . values . slice ( 1 ) ,
185
+ ] ,
186
+ } ,
187
+ extra : {
188
+ ...event . extra ,
189
+ 'zoderror.issues' : flattenedIssues . slice ( 0 , limit ) ,
190
+ } ,
191
+ } ;
192
+ } catch ( e ) {
193
+ // Hopefully we never throw errors here, but record it
194
+ // with the event just in case.
195
+ return {
196
+ ...event ,
197
+ extra : {
198
+ ...event . extra ,
199
+ 'zoderrors sentry integration parse error' : {
200
+ message : 'an exception was thrown while processing ZodError within applyZodErrorsToEvent()' ,
201
+ error : e instanceof Error ? `${ e . name } : ${ e . message } \n${ e . stack } ` : 'unknown' ,
98
202
} ,
99
- ...event . exception . values . slice ( 1 ) ,
100
- ] ,
101
- } ,
102
- extra : {
103
- ...event . extra ,
104
- 'zoderror.issues' : hint . originalException . errors . slice ( 0 , limit ) . map ( formatIssueTitle ) ,
105
- } ,
106
- } ;
203
+ } ,
204
+ } ;
205
+ }
107
206
}
108
207
109
208
const _zodErrorsIntegration = ( ( options : ZodErrorsOptions = { } ) => {
110
- const limit = options . limit || DEFAULT_LIMIT ;
209
+ const limit = typeof options . limit === 'undefined' ? DEFAULT_LIMIT : options . limit ;
111
210
112
211
return {
113
212
name : INTEGRATION_NAME ,
114
- processEvent ( originalEvent , hint ) {
115
- const processedEvent = applyZodErrorsToEvent ( limit , originalEvent , hint ) ;
213
+ processEvent ( originalEvent , hint ) : Event {
214
+ const processedEvent = applyZodErrorsToEvent ( limit , options . saveZodIssuesAsAttachment , originalEvent , hint ) ;
116
215
return processedEvent ;
117
216
} ,
118
217
} ;
119
218
} ) satisfies IntegrationFn ;
120
219
220
+ /**
221
+ * Sentry integration to process Zod errors, making them easier to work with in Sentry.
222
+ */
121
223
export const zodErrorsIntegration = defineIntegration ( _zodErrorsIntegration ) ;
0 commit comments