Skip to content

Commit b5f4592

Browse files
committed
Add GraphQL errors support to RUM resource events
1 parent 0359f47 commit b5f4592

5 files changed

Lines changed: 222 additions & 3 deletions

File tree

features/dd-sdk-android-rum/api/apiSurface

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ object com.datadog.android.rum.RumAttributes
3131
const val GRAPHQL_OPERATION_NAME: String
3232
const val GRAPHQL_PAYLOAD: String
3333
const val GRAPHQL_VARIABLES: String
34+
const val GRAPHQL_ERRORS: String
3435
const val ERROR_RESOURCE_METHOD: String
3536
const val ERROR_RESOURCE_STATUS_CODE: String
3637
const val ERROR_RESOURCE_URL: String

features/dd-sdk-android-rum/api/dd-sdk-android-rum.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public final class com/datadog/android/rum/RumAttributes {
6262
public static final field ERROR_RESOURCE_STATUS_CODE Ljava/lang/String;
6363
public static final field ERROR_RESOURCE_URL Ljava/lang/String;
6464
public static final field FLUTTER_FIRST_BUILD_COMPLETE Ljava/lang/String;
65+
public static final field GRAPHQL_ERRORS Ljava/lang/String;
6566
public static final field GRAPHQL_OPERATION_NAME Ljava/lang/String;
6667
public static final field GRAPHQL_OPERATION_TYPE Ljava/lang/String;
6768
public static final field GRAPHQL_PAYLOAD Ljava/lang/String;

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ object RumAttributes {
126126
*/
127127
const val GRAPHQL_VARIABLES: String = "_dd.graphql.variables"
128128

129+
/**
130+
* JSON representation of GraphQL errors (String).
131+
*/
132+
const val GRAPHQL_ERRORS: String = "_dd.graphql.errors"
133+
129134
// endregion
130135

131136
// region Error

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ internal class RumResourceScope(
253253
val graphqlOperationType = resourceAttributes.remove(RumAttributes.GRAPHQL_OPERATION_TYPE) as? String
254254
val graphqlVariables = resourceAttributes.remove(RumAttributes.GRAPHQL_VARIABLES) as? String
255255

256+
val graphqlErrors = (resourceAttributes.remove(RumAttributes.GRAPHQL_ERRORS) as? List<*>)
257+
?.mapNotNull { it as? ResourceEvent.Error }
258+
256259
// The decision whether to send payloads is determined by a DatadogApolloInterceptor parameter
257260
val rawPayload = resourceAttributes.remove(RumAttributes.GRAPHQL_PAYLOAD) as? String
258261
val graphqlPayload = rawPayload?.truncateToUtf8Bytes(MAX_GRAPHQL_PAYLOAD_SIZE_BYTES)
@@ -261,7 +264,8 @@ internal class RumResourceScope(
261264
operationType = graphqlOperationType,
262265
operationName = graphqlOperationName,
263266
variables = graphqlVariables,
264-
payload = graphqlPayload
267+
payload = graphqlPayload,
268+
errors = graphqlErrors
265269
)
266270

267271
sdkCore.newRumEventWriteOperation(datadogContext, writeScope, writer) {
@@ -554,14 +558,17 @@ internal class RumResourceScope(
554558
operationType: String?,
555559
operationName: String?,
556560
variables: String?,
557-
payload: String?
561+
payload: String?,
562+
errors: List<ResourceEvent.Error>?
558563
): ResourceEvent.Graphql? {
559564
operationType?.toOperationType(sdkCore.internalLogger)?.let {
560565
return ResourceEvent.Graphql(
561566
operationType = it,
562567
operationName = operationName,
563568
variables = variables,
564-
payload = payload
569+
payload = payload,
570+
errorCount = errors?.takeIf { errors -> errors.isNotEmpty() }?.size?.toLong(),
571+
errors = errors?.takeIf { errors -> errors.isNotEmpty() }
565572
)
566573
}
567574

features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3668,6 +3668,211 @@ internal class RumResourceScopeTest {
36683668
}
36693669
}
36703670

3671+
@Test
3672+
fun `M parse GraphQL errors W handleEvent { multiple errors with locations and path }`(
3673+
@Forgery kind: RumResourceKind,
3674+
@LongForgery(200, 600) statusCode: Long,
3675+
@LongForgery(0, 1024) size: Long,
3676+
forge: Forge
3677+
) {
3678+
// Given
3679+
val operationType = forge.aValueFrom(ResourceEvent.OperationType::class.java)
3680+
val operationName = forge.aNullable { aString() }
3681+
val variables = forge.aNullable { aString() }
3682+
3683+
val errors = listOf(
3684+
ResourceEvent.Error(
3685+
message = "User not found",
3686+
code = "NOT_FOUND",
3687+
locations = listOf(ResourceEvent.Location(line = 3, column = 5)),
3688+
path = listOf(ResourceEvent.Path.String("user"), ResourceEvent.Path.String("profile"))
3689+
),
3690+
ResourceEvent.Error(
3691+
message = "Validation failed",
3692+
code = null,
3693+
locations = null,
3694+
path = null
3695+
)
3696+
)
3697+
3698+
val attributes = forge.exhaustiveAttributes(excludedKeys = fakeResourceAttributes.keys) +
3699+
mapOf(
3700+
RumAttributes.GRAPHQL_OPERATION_TYPE to operationType.toString(),
3701+
RumAttributes.GRAPHQL_OPERATION_NAME to operationName,
3702+
RumAttributes.GRAPHQL_VARIABLES to variables,
3703+
RumAttributes.GRAPHQL_ERRORS to errors
3704+
)
3705+
3706+
mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes)
3707+
3708+
// When
3709+
testedScope.handleEvent(mockEvent, fakeDatadogContext, mockEventWriteScope, mockWriter)
3710+
3711+
// Then
3712+
argumentCaptor<ResourceEvent> {
3713+
verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT))
3714+
val graphql = firstValue.resource.graphql
3715+
checkNotNull(graphql)
3716+
assertThat(graphql.errorCount).isEqualTo(2)
3717+
assertThat(graphql.errors).hasSize(2)
3718+
assertThat(graphql.errors?.get(0)?.message).isEqualTo("User not found")
3719+
assertThat(graphql.errors?.get(0)?.code).isEqualTo("NOT_FOUND")
3720+
assertThat(graphql.errors?.get(0)?.locations).hasSize(1)
3721+
assertThat(graphql.errors?.get(0)?.locations?.get(0)?.line).isEqualTo(3)
3722+
assertThat(graphql.errors?.get(0)?.locations?.get(0)?.column).isEqualTo(5)
3723+
assertThat(graphql.errors?.get(0)?.path).hasSize(2)
3724+
assertThat(graphql.errors?.get(1)?.message).isEqualTo("Validation failed")
3725+
assertThat(graphql.errors?.get(1)?.code).isNull()
3726+
}
3727+
}
3728+
3729+
@Test
3730+
fun `M parse GraphQL errors W handleEvent { single error }`(
3731+
@Forgery kind: RumResourceKind,
3732+
@LongForgery(200, 600) statusCode: Long,
3733+
@LongForgery(0, 1024) size: Long,
3734+
forge: Forge
3735+
) {
3736+
// Given
3737+
val operationType = forge.aValueFrom(ResourceEvent.OperationType::class.java)
3738+
val operationName = forge.aNullable { aString() }
3739+
3740+
val errors = listOf(
3741+
ResourceEvent.Error(
3742+
message = "Field does not exist",
3743+
code = "GRAPHQL_VALIDATION_FAILED"
3744+
)
3745+
)
3746+
3747+
val attributes = forge.exhaustiveAttributes(excludedKeys = fakeResourceAttributes.keys) +
3748+
mapOf(
3749+
RumAttributes.GRAPHQL_OPERATION_TYPE to operationType.toString(),
3750+
RumAttributes.GRAPHQL_OPERATION_NAME to operationName,
3751+
RumAttributes.GRAPHQL_ERRORS to errors
3752+
)
3753+
3754+
mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes)
3755+
3756+
// When
3757+
testedScope.handleEvent(mockEvent, fakeDatadogContext, mockEventWriteScope, mockWriter)
3758+
3759+
// Then
3760+
argumentCaptor<ResourceEvent> {
3761+
verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT))
3762+
val graphql = firstValue.resource.graphql
3763+
checkNotNull(graphql)
3764+
assertThat(graphql.errorCount).isEqualTo(1)
3765+
assertThat(graphql.errors).hasSize(1)
3766+
assertThat(graphql.errors?.get(0)?.message).isEqualTo("Field does not exist")
3767+
assertThat(graphql.errors?.get(0)?.code).isEqualTo("GRAPHQL_VALIDATION_FAILED")
3768+
}
3769+
}
3770+
3771+
@Test
3772+
fun `M handle null errors W handleEvent { no errors in GraphQL response }`(
3773+
@Forgery kind: RumResourceKind,
3774+
@LongForgery(200, 600) statusCode: Long,
3775+
@LongForgery(0, 1024) size: Long,
3776+
forge: Forge
3777+
) {
3778+
// Given
3779+
val operationType = forge.aValueFrom(ResourceEvent.OperationType::class.java)
3780+
val operationName = forge.aNullable { aString() }
3781+
3782+
val attributes = forge.exhaustiveAttributes(excludedKeys = fakeResourceAttributes.keys) +
3783+
mapOf(
3784+
RumAttributes.GRAPHQL_OPERATION_TYPE to operationType.toString(),
3785+
RumAttributes.GRAPHQL_OPERATION_NAME to operationName
3786+
// No GRAPHQL_ERRORS attribute
3787+
)
3788+
3789+
mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes)
3790+
3791+
// When
3792+
testedScope.handleEvent(mockEvent, fakeDatadogContext, mockEventWriteScope, mockWriter)
3793+
3794+
// Then
3795+
argumentCaptor<ResourceEvent> {
3796+
verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT))
3797+
val graphql = firstValue.resource.graphql
3798+
checkNotNull(graphql)
3799+
assertThat(graphql.errorCount).isNull()
3800+
assertThat(graphql.errors).isNull()
3801+
}
3802+
}
3803+
3804+
@Test
3805+
fun `M handle invalid type W handleEvent { non-list GraphQL errors }`(
3806+
@Forgery kind: RumResourceKind,
3807+
@LongForgery(200, 600) statusCode: Long,
3808+
@LongForgery(0, 1024) size: Long,
3809+
forge: Forge
3810+
) {
3811+
// Given
3812+
val operationType = forge.aValueFrom(ResourceEvent.OperationType::class.java)
3813+
val operationName = forge.aNullable { aString() }
3814+
3815+
val invalidErrors = "not a list"
3816+
3817+
val attributes = forge.exhaustiveAttributes(excludedKeys = fakeResourceAttributes.keys) +
3818+
mapOf(
3819+
RumAttributes.GRAPHQL_OPERATION_TYPE to operationType.toString(),
3820+
RumAttributes.GRAPHQL_OPERATION_NAME to operationName,
3821+
RumAttributes.GRAPHQL_ERRORS to invalidErrors
3822+
)
3823+
3824+
mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes)
3825+
3826+
// When
3827+
testedScope.handleEvent(mockEvent, fakeDatadogContext, mockEventWriteScope, mockWriter)
3828+
3829+
// Then
3830+
argumentCaptor<ResourceEvent> {
3831+
verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT))
3832+
val graphql = firstValue.resource.graphql
3833+
// Should handle gracefully with null errors when type is invalid
3834+
checkNotNull(graphql)
3835+
assertThat(graphql.errorCount).isNull()
3836+
assertThat(graphql.errors).isNull()
3837+
}
3838+
}
3839+
3840+
@Test
3841+
fun `M handle empty errors array W handleEvent { GraphQL errors is empty list }`(
3842+
@Forgery kind: RumResourceKind,
3843+
@LongForgery(200, 600) statusCode: Long,
3844+
@LongForgery(0, 1024) size: Long,
3845+
forge: Forge
3846+
) {
3847+
// Given
3848+
val operationType = forge.aValueFrom(ResourceEvent.OperationType::class.java)
3849+
val operationName = forge.aNullable { aString() }
3850+
3851+
val emptyErrors = emptyList<ResourceEvent.Error>()
3852+
3853+
val attributes = forge.exhaustiveAttributes(excludedKeys = fakeResourceAttributes.keys) +
3854+
mapOf(
3855+
RumAttributes.GRAPHQL_OPERATION_TYPE to operationType.toString(),
3856+
RumAttributes.GRAPHQL_OPERATION_NAME to operationName,
3857+
RumAttributes.GRAPHQL_ERRORS to emptyErrors
3858+
)
3859+
3860+
mockEvent = RumRawEvent.StopResource(fakeKey, statusCode, size, kind, attributes)
3861+
3862+
// When
3863+
testedScope.handleEvent(mockEvent, fakeDatadogContext, mockEventWriteScope, mockWriter)
3864+
3865+
// Then
3866+
argumentCaptor<ResourceEvent> {
3867+
verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT))
3868+
val graphql = firstValue.resource.graphql
3869+
// Empty errors should result in null
3870+
checkNotNull(graphql)
3871+
assertThat(graphql.errorCount).isNull()
3872+
assertThat(graphql.errors).isNull()
3873+
}
3874+
}
3875+
36713876
// endregion
36723877

36733878
companion object {

0 commit comments

Comments
 (0)