@@ -217,7 +217,7 @@ class RelayReader {
217
217
}
218
218
}
219
219
220
- _markDataAsMissing ( ) : void {
220
+ _markDataAsMissing ( fieldName : string ) : void {
221
221
if ( this . _isWithinUnmatchedTypeRefinement ) {
222
222
return ;
223
223
}
@@ -226,18 +226,17 @@ class RelayReader {
226
226
}
227
227
228
228
// we will add the path later
229
- const fieldPath = '' ;
230
229
const owner = this . _fragmentName ;
231
230
232
231
this . _errorResponseFields . push (
233
232
this . _selector . node . metadata ?. throwOnFieldError ?? false
234
233
? {
235
234
kind : 'missing_expected_data.throw' ,
236
235
owner,
237
- fieldPath,
236
+ fieldPath : fieldName ,
238
237
handled : false ,
239
238
}
240
- : { kind : 'missing_expected_data.log' , owner, fieldPath} ,
239
+ : { kind : 'missing_expected_data.log' , owner, fieldPath : fieldName } ,
2
F438
41
240
) ;
242
241
243
242
this . _isMissingData = true ;
@@ -266,7 +265,7 @@ class RelayReader {
266
265
this . _seenRecords . add ( dataID ) ;
267
266
if ( record == null ) {
268
267
if ( record === undefined ) {
269
- this . _markDataAsMissing ( ) ;
268
+ this . _markDataAsMissing ( '<record>' ) ;
270
269
}
271
270
return record ;
272
271
}
@@ -489,12 +488,15 @@ class RelayReader {
489
488
this . _createFragmentPointer ( selection , record , data ) ;
490
489
break ;
491
490
case 'AliasedInlineFragmentSpread' : {
491
+ const prevErrors = this . _errorResponseFields ;
492
+ this . _errorResponseFields = null ;
492
493
let fieldValue = this . _readInlineFragment (
493
494
selection . fragment ,
494
495
record ,
495
496
{ } ,
496
497
true ,
497
498
) ;
499
+ this . _prependPreviousErrors ( prevErrors , selection . name ) ;
498
500
if ( fieldValue === false ) {
499
501
fieldValue = null ;
500
502
}
@@ -601,9 +603,12 @@ class RelayReader {
601
603
data : SelectorData ,
602
604
) : mixed {
603
605
const parentRecordID = RelayModernRecord . getDataID ( record ) ;
606
+ const prevErrors = this . _errorResponseFields ;
607
+ this . _errorResponseFields = null ;
604
608
const result = this . _readResolverFieldImpl ( field , parentRecordID ) ;
605
609
606
610
const fieldName = field . alias ?? field . name ;
611
+ this . _prependPreviousErrors ( prevErrors , fieldName ) ;
607
612
data [ fieldName ] = result ;
608
613
return result ;
609
614
}
@@ -982,12 +987,15 @@ class RelayReader {
982
987
RelayModernRecord . getDataID ( record ) ,
983
988
prevData ,
984
989
) ;
990
+ const prevErrors = this . _errorResponseFields ;
991
+ this . _errorResponseFields = null ;
985
992
const edgeValue = this . _traverse (
986
993
field . linkedField ,
987
994
storeID ,
988
995
// $FlowFixMe[incompatible-variance]
989
996
prevData ,
990
997
) ;
998
+ this . _prependPreviousErrors ( prevErrors , fieldName ) ;
991
999
this . _clientEdgeTraversalPath . pop ( ) ;
992
1000
data [ fieldName ] = edgeValue ;
993
1001
return edgeValue ;
@@ -1005,7 +1013,7 @@ class RelayReader {
1005
1013
if ( value === null ) {
1006
1014
this . _maybeAddErrorResponseFields ( record , storageKey ) ;
1007
1015
} else if ( value === undefined ) {
1008
- this . _markDataAsMissing ( ) ;
1016
+ this . _markDataAsMissing ( fieldName ) ;
1009
1017
}
1010
1018
data [ fieldName ] = value ;
1011
1019
return value ;
@@ -1024,7 +1032,7 @@ class RelayReader {
1024
1032
if ( linkedID === null ) {
1025
1033
this . _maybeAddErrorResponseFields ( record , storageKey ) ;
1026
1034
} else if ( linkedID === undefined ) {
1027
- this . _markDataAsMissing ( ) ;
1035
+ this . _markDataAsMissing ( fieldName ) ;
1028
1036
}
1029
1037
return linkedID ;
1030
1038
}
@@ -1039,12 +1047,73 @@ class RelayReader {
1039
1047
RelayModernRecord . getDataID ( record ) ,
1040
1048
prevData ,
1041
1049
) ;
1050
+ const prevErrors = this . _errorResponseFields ;
1051
+ this . _errorResponseFields = null ;
1042
1052
// $FlowFixMe[incompatible-variance]
1043
1053
const value = this . _traverse ( field , linkedID , prevData ) ;
1054
+
1055
+ this . _prependPreviousErrors ( prevErrors , fieldName ) ;
1044
1056
data [ fieldName ] = value ;
1045
1057
return value ;
1046
1058
}
1047
1059
1060
+ /**
1061
+ * Adds a set of field errors to `this._errorResponseFields`, ensuring the
1062
+ * `fieldPath` property of existing field errors are prefixed with the given
1063
+ * `fieldNameOrIndex`.
1064
+ *
1065
+ * In order to make field errors maximally useful in logs/errors, we want to
1066
+ * include the path to the field that caused the error. A naive approach would
1067
+ * be to maintain a path property on RelayReader which we push/pop field names
1068
+ * to as we traverse into fields/etc. However, this would be expensive to
1069
+ * maintain, and in the common case where there are no field errors, the work
1070
+ * would go unused.
1071
+ *
1072
+ * Instead, we take a lazy approach where as we exit the recurison into a
1073
+ * field/etc we prepend any errors encountered while traversing that field
1074
+ * with the field name. This is somewhat more expensive in the error case, but
1075
+ * ~free in the common case where there are no errors.
1076
+ *
1077
+ * To achieve this, named field readers must do the following to correctly
1078
+ * track error filePaths:
1079
+ *
1080
+ * 1. Stash the value of `this._errorResponseFields` in a local variable
1081
+ * 2. Set `this._errorResponseFields` to `null`
1082
+ * 3. Traverse into the field
1083
+ * 4. Call this method with the stashed errors and the field's name
1084
+ *
1085
+ * Similarly, when creating field errors, we simply initialize the `fieldPath`
1086
+ * as the direct field name.
1087
+ *
1088
+ * Today we only use this apporach for `missing_expected_data` errors, but we
1089
+ * intend to broaden it to handle all field error paths.
1090
+ */
1091
+ _prependPreviousErrors (
1092
+ prevErrors : ?Array < ErrorResponseField > ,
1093
+ fieldNameOrIndex : string | number ,
1094
+ ) : void {
1095
+ if ( this . _errorResponseFields != null ) {
1096
+ for ( let i = 0 ; i < this . _errorResponseFields . length ; i ++ ) {
1097
+ const event = this . _errorResponseFields [ i ] ;
1098
+ if (
1099
+ event . owner === this . _fragmentName &&
1100
+ ( event . kind === 'missing_expected_data.throw' ||
1101
+ event . kind === 'missing_expected_data.log' )
1102
+ ) {
1103
+ event . fieldPath = `${ fieldNameOrIndex } .${ event . fieldPath } ` ;
1104
+ }
1105
+ }
1106
+ if ( prevErrors != null ) {
1107
+ for ( let i = this . _errorResponseFields . length - 1 ; i >= 0 ; i -- ) {
1108
+ prevErrors . push ( this . _errorResponseFields [ i ] ) ;
1109
+ }
1110
+ this . _errorResponseFields = prevErrors ;
1111
+ }
1112
+ } else {
1113
+ this . _errorResponseFields = prevErrors ;
1114
+ }
1115
+ }
1116
+
1048
1117
_readActorChange (
1049
1118
field : ReaderActorChange ,
1050
1119
record : Record ,
@@ -1060,7 +1129,7 @@ class RelayReader {
1060
1129
if ( externalRef == null ) {
1061
1130
data [ fieldName ] = externalRef ;
1062
1131
if ( externalRef === undefined ) {
1063
- this . _markDataAsMissing ( ) ;
1132
+ this . _markDataAsMissing ( fieldName ) ;
1064
1133
} else if ( externalRef === null ) {
1065
1134
this . _maybeAddErrorResponseFields ( record , storageKey ) ;
1066
1135
}
@@ -1107,7 +1176,7 @@ class RelayReader {
1107
1176
if ( linkedIDs == null ) {
1108
1177
data [ fieldName ] = linkedIDs ;
1109
1178
if ( linkedIDs === undefined ) {
1110
- this . _markDataAsMissing ( ) ;
1179
+ this . _markDataAsMissing ( fieldName ) ;
1111
1180
}
1112
1181
return linkedIDs ;
1113
1182
}
@@ -1121,11 +1190,13 @@ class RelayReader {
1121
1190
RelayModernRecord . getDataID ( record ) ,
1122
1191
prevData ,
1123
1192
) ;
1193
+ const prevErrors = this . _errorResponseFields ;
1194
+ this . _errorResponseFields = null ;
1124
1195
const linkedArray = prevData || [ ] ;
1125
1196
linkedIDs . forEach ( ( linkedID , nextIndex ) => {
1126
1197
if ( linkedID == null ) {
1127
1198
if ( linkedID === undefined ) {
1128
- this . _markDataAsMissing ( ) ;
1199
+ this . _markDataAsMissing ( String ( nextIndex ) ) ;
1129
1200
}
1130
1201
// $FlowFixMe[cannot-write]
1131
1202
linkedArray [ nextIndex ] = linkedID ;
@@ -1140,10 +1211,14 @@ class RelayReader {
1140
1211
RelayModernRecord . getDataID ( record ) ,
1141
1212
prevItem ,
1142
1213
) ;
1214
+ const prevErrors = this . _errorResponseFields ;
1215
+ this . _errorResponseFields = null ;
1143
1216
// $FlowFixMe[cannot-write]
1144
1217
// $FlowFixMe[incompatible-variance]
1145
1218
linkedArray [ nextIndex ] = this . _traverse ( field , linkedID , prevItem ) ;
1219
+ this . _prependPreviousErrors ( prevErrors , nextIndex ) ;
1146
1220
} ) ;
1221
+ this . _prependPreviousErrors ( prevErrors , fieldName ) ;
1147
1222
data [ fieldName ] = linkedArray ;
1148
1223
return linkedArray ;
1149
1224
}
@@ -1166,7 +1241,7 @@ class RelayReader {
1166
1241
RelayModernRecord . getValue ( record , componentKey ) ;
1167
1242
if ( component == null ) {
1168
1243
if ( component === undefined ) {
1169
- this . _markDataAsMissing ( ) ;
1244
+ this . _markDataAsMissing ( '<module-import>' ) ;
1170
1245
}
1171
1246
return ;
1172
1247
}
@@ -1398,7 +1473,7 @@ class RelayReader {
1398
1473
// fetched the `__is[AbstractType]` flag for this concrete type. In this
1399
1474
// case we need to report that we are missing data, in case that field is
1400
1475
// still in flight.
1401
- this . _markDataAsMissing ( ) ;
1476
+ this . _markDataAsMissing ( '<abstract-type-hint>' ) ;
1402
1477
}
1403
1478
// $FlowFixMe Casting record value
1404
1479
return implementsInterface ;
0 commit comments