Skip to content

Commit b76c014

Browse files
RUM-12441 Tests for GraphQL error tracking
1 parent 0bb4df5 commit b76c014

File tree

2 files changed

+284
-0
lines changed

2 files changed

+284
-0
lines changed

DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,81 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase {
10081008
XCTAssertEqual(attributes[CrossPlatformAttributes.graphqlOperationType] as? String, "mutation")
10091009
}
10101010

1011+
func testGivenGraphQLResponseBodyWithErrors_whenInterceptionCompletes_itExtractsErrors() throws {
1012+
let receiveCommand = expectation(description: "Receive RUMStopResourceCommand")
1013+
var stopResourceCommand: RUMStopResourceCommand?
1014+
commandSubscriber.onCommandReceived = { command in
1015+
if let command = command as? RUMStopResourceCommand {
1016+
stopResourceCommand = command
1017+
receiveCommand.fulfill()
1018+
}
1019+
}
1020+
1021+
// Given
1022+
var mockRequest: URLRequest = .mockWith(url: "https://graphql.example.com/api")
1023+
mockRequest.setValue("GetUser", forHTTPHeaderField: GraphQLHeaders.operationName)
1024+
1025+
let responseWithErrors = """
1026+
{
1027+
"errors": [{"message": "Not found"}],
1028+
"data": null
1029+
}
1030+
"""
1031+
1032+
let immutableRequest = ImmutableRequest(request: mockRequest)
1033+
let taskInterception = URLSessionTaskInterception(request: immutableRequest, isFirstParty: false)
1034+
let response: HTTPURLResponse = .mockWith(statusCode: 200, mimeType: "application/json")
1035+
taskInterception.register(nextData: responseWithErrors.data(using: .utf8)!)
1036+
taskInterception.register(response: response, error: nil)
1037+
1038+
// When
1039+
handler.interceptionDidComplete(interception: taskInterception)
1040+
1041+
// Then
1042+
waitForExpectations(timeout: 0.5, handler: nil)
1043+
1044+
let attributes = try XCTUnwrap(stopResourceCommand?.attributes)
1045+
XCTAssertNotNil(attributes[CrossPlatformAttributes.graphqlErrors])
1046+
let errorsJSON = try XCTUnwrap(attributes[CrossPlatformAttributes.graphqlErrors] as? String)
1047+
XCTAssertTrue(errorsJSON.contains("Not found"))
1048+
}
1049+
1050+
func testGivenGraphQLResponseWithNonJSONContentType_whenInterceptionCompletes_itDoesNotParseErrors() throws {
1051+
let receiveCommand = expectation(description: "Receive RUMStopResourceCommand")
1052+
var stopResourceCommand: RUMStopResourceCommand?
1053+
commandSubscriber.onCommandReceived = { command in
1054+
if let command = command as? RUMStopResourceCommand {
1055+
stopResourceCommand = command
1056+
receiveCommand.fulfill()
1057+
}
1058+
}
1059+
1060+
// Given
1061+
var mockRequest: URLRequest = .mockWith(url: "https://graphql.example.com/api")
1062+
mockRequest.setValue("GetUser", forHTTPHeaderField: GraphQLHeaders.operationName)
1063+
1064+
let responseWithErrors = """
1065+
{
1066+
"errors": [{"message": "Not found"}],
1067+
"data": null
1068+
}
1069+
"""
1070+
1071+
let immutableRequest = ImmutableRequest(request: mockRequest)
1072+
let taskInterception = URLSessionTaskInterception(request: immutableRequest, isFirstParty: false)
1073+
let nonJSONResponse: HTTPURLResponse = .mockWith(statusCode: 200, mimeType: "text/html")
1074+
taskInterception.register(nextData: responseWithErrors.data(using: .utf8)!)
1075+
taskInterception.register(response: nonJSONResponse, error: nil)
1076+
1077+
// When
1078+
handler.interceptionDidComplete(interception: taskInterception)
1079+
1080+
// Then
1081+
waitForExpectations(timeout: 0.5, handler: nil)
1082+
1083+
XCTAssertNil(stopResourceCommand?.attributes[CrossPlatformAttributes.graphqlErrors])
1084+
}
1085+
10111086
// MARK: - Helper Methods
10121087

10131088
private func extractBaggageKeyValuePairs(from header: String) -> [String: String] {

DatadogRUM/Tests/RUMMonitor/Scopes/RUMResourceScopeTests.swift

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ import DatadogInternal
99
@testable import DatadogRUM
1010
@testable import TestUtilities
1111

12+
// Extension to make Path conform to Equatable for testing
13+
extension RUMResourceEvent.Resource.Graphql.Errors.Path: Equatable {
14+
public static func == (lhs: RUMResourceEvent.Resource.Graphql.Errors.Path, rhs: RUMResourceEvent.Resource.Graphql.Errors.Path) -> Bool {
15+
switch (lhs, rhs) {
16+
case (.string(let lhsValue), .string(let rhsValue)):
17+
return lhsValue == rhsValue
18+
case (.integer(let lhsValue), .integer(let rhsValue)):
19+
return lhsValue == rhsValue
20+
default:
21+
return false
22+
}
23+
}
24+
}
25+
1226
class RUMResourceScopeTests: XCTestCase {
1327
let context: DatadogContext = .mockWith(
1428
service: "test-service",
@@ -1511,4 +1525,199 @@ class RUMResourceScopeTests: XCTestCase {
15111525
// Then
15121526
XCTAssertEqual(metric.resourcesDropped, [resourceUUID])
15131527
}
1528+
1529+
// MARK: - GraphQL Error Parsing Tests
1530+
1531+
func testGivenResourceWithComplexGraphQLResponse_whenResourceEnds_itParsesAllErrorsCorrectly() throws {
1532+
// Given
1533+
let scope = RUMResourceScope.mockWith(
1534+
parent: provider,
1535+
dependencies: dependencies,
1536+
resourceKey: "/graphql",
1537+
startTime: .mockDecember15th2019At10AMUTC(),
1538+
url: "https://api.example.com/graphql",
1539+
httpMethod: .post
1540+
)
1541+
1542+
let graphQLResponseJSON = """
1543+
{
1544+
"errors": [
1545+
{
1546+
"message": "Book not found",
1547+
"locations": [
1548+
{ "line": 2, "column": 7 },
1549+
{ "line": 5, "column": 12 }
1550+
],
1551+
"path": ["library", "book", "1234"],
1552+
"extensions": {
1553+
"code": "NOT_FOUND",
1554+
"timestamp": "2024-01-15T10:00:00Z"
1555+
}
1556+
},
1557+
{
1558+
"message": "Unauthorized access to user profile",
1559+
"locations": [{ "line": 10, "column": 3 }],
1560+
"path": ["user", "profile"],
1561+
"extensions": {
1562+
"code": "UNAUTHORIZED"
1563+
}
1564+
}
1565+
],
1566+
"data": {
1567+
"library": {
1568+
"book": null
1569+
},
1570+
"user": null,
1571+
"firstShip": "3001",
1572+
"secondShip": null,
1573+
"launch": [
1574+
{
1575+
"id": "1",
1576+
"status": null
1577+
}
1578+
],
1579+
"oldField": "some value"
1580+
}
1581+
}
1582+
"""
1583+
1584+
// When
1585+
XCTAssertFalse(
1586+
scope.process(
1587+
command: RUMStopResourceCommand(
1588+
resourceKey: "/graphql",
1589+
time: .mockDecember15th2019At10AMUTC(addingTimeInterval: 1),
1590+
attributes: [
1591+
CrossPlatformAttributes.graphqlErrors: graphQLResponseJSON,
1592+
CrossPlatformAttributes.graphqlOperationType: "query",
1593+
CrossPlatformAttributes.graphqlOperationName: "GetBookAndUser"
1594+
],
1595+
kind: .xhr,
1596+
httpStatusCode: 200,
1597+
size: nil
1598+
),
1599+
context: context,
1600+
writer: writer
1601+
)
1602+
)
1603+
1604+
// Then
1605+
let event = try XCTUnwrap(writer.events(ofType: RUMResourceEvent.self).first)
1606+
let graphql = try XCTUnwrap(event.resource.graphql)
1607+
1608+
// Verify error count
1609+
XCTAssertEqual(graphql.errorCount, 2)
1610+
1611+
// Verify errors array structure
1612+
let errors = try XCTUnwrap(graphql.errors)
1613+
XCTAssertEqual(errors.count, 2)
1614+
1615+
// Verify first error
1616+
let error1 = errors[0]
1617+
XCTAssertEqual(error1.message, "Book not found")
1618+
XCTAssertEqual(error1.code, "NOT_FOUND")
1619+
let path1 = try XCTUnwrap(error1.path)
1620+
XCTAssertEqual(path1.count, 3)
1621+
XCTAssertEqual(path1[0].self, .string(value: "library"))
1622+
XCTAssertEqual(path1[1], .string(value: "book"))
1623+
XCTAssertEqual(path1[2], .string(value: "1234"))
1624+
let locations1 = try XCTUnwrap(error1.locations)
1625+
XCTAssertEqual(locations1.count, 2)
1626+
XCTAssertEqual(locations1[0].line, 2)
1627+
XCTAssertEqual(locations1[0].column, 7)
1628+
XCTAssertEqual(locations1[1].line, 5)
1629+
XCTAssertEqual(locations1[1].column, 12)
1630+
1631+
// Verify second error
1632+
let error2 = errors[1]
1633+
XCTAssertEqual(error2.message, "Unauthorized access to user profile")
1634+
XCTAssertEqual(error2.code, "UNAUTHORIZED")
1635+
let path2 = try XCTUnwrap(error2.path)
1636+
XCTAssertEqual(path2.count, 2)
1637+
XCTAssertEqual(path2[0], .string(value: "user"))
1638+
XCTAssertEqual(path2[1], .string(value: "profile"))
1639+
let locations2 = try XCTUnwrap(error2.locations)
1640+
XCTAssertEqual(locations2.count, 1)
1641+
XCTAssertEqual(locations2[0].line, 10)
1642+
XCTAssertEqual(locations2[0].column, 3)
1643+
}
1644+
1645+
func testGivenResourceWithInvalidGraphQLJSON_whenResourceEnds_itHandlesGracefully() throws {
1646+
let scope = RUMResourceScope.mockWith(
1647+
parent: provider,
1648+
dependencies: dependencies,
1649+
resourceKey: "/graphql",
1650+
startTime: .mockDecember15th2019At10AMUTC(),
1651+
url: "https://api.example.com/graphql",
1652+
httpMethod: .post
1653+
)
1654+
1655+
// When - Invalid JSON should not crash
1656+
XCTAssertFalse(
1657+
scope.process(
1658+
command: RUMStopResourceCommand(
1659+
resourceKey: "/graphql",
1660+
time: .mockDecember15th2019At10AMUTC(addingTimeInterval: 1),
1661+
attributes: [
1662+
CrossPlatformAttributes.graphqlErrors: "{ invalid json }",
1663+
CrossPlatformAttributes.graphqlOperationType: "query"
1664+
],
1665+
kind: .xhr,
1666+
httpStatusCode: 200,
1667+
size: nil
1668+
),
1669+
context: context,
1670+
writer: writer
1671+
)
1672+
)
1673+
1674+
// Then
1675+
let event = try XCTUnwrap(writer.events(ofType: RUMResourceEvent.self).first)
1676+
let graphql = try XCTUnwrap(event.resource.graphql)
1677+
XCTAssertNil(graphql.errors)
1678+
XCTAssertNil(graphql.errorCount)
1679+
}
1680+
1681+
func testGivenResourceWithEmptyGraphQLErrorsArray_whenResourceEnds_itDoesNotSetErrors() throws {
1682+
let scope = RUMResourceScope.mockWith(
1683+
parent: provider,
1684+
dependencies: dependencies,
1685+
resourceKey: "/graphql",
1686+
startTime: .mockDecember15th2019At10AMUTC(),
1687+
url: "https://api.example.com/graphql",
1688+
httpMethod: .post
1689+
)
1690+
1691+
let emptyErrorsJSON = """
1692+
{
1693+
"errors": [],
1694+
"data": null
1695+
}
1696+
"""
1697+
1698+
// When
1699+
XCTAssertFalse(
1700+
scope.process(
1701+
command: RUMStopResourceCommand(
1702+
resourceKey: "/graphql",
1703+
time: .mockDecember15th2019At10AMUTC(addingTimeInterval: 1),
1704+
attributes: [
1705+
CrossPlatformAttributes.graphqlErrors: emptyErrorsJSON,
1706+
CrossPlatformAttributes.graphqlOperationType: "query"
1707+
],
1708+
kind: .xhr,
1709+
httpStatusCode: 200,
1710+
size: nil
1711+
),
1712+
context: context,
1713+
writer: writer
1714+
)
1715+
)
1716+
1717+
// Then
1718+
let event = try XCTUnwrap(writer.events(ofType: RUMResourceEvent.self).first)
1719+
let graphql = try XCTUnwrap(event.resource.graphql)
1720+
XCTAssertNil(graphql.errors)
1721+
XCTAssertNil(graphql.errorCount)
1722+
}
15141723
}

0 commit comments

Comments
 (0)