Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Send GraphQL "operationName" in HTTP breadcrumbs #3931

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add option to use own NSURLSession for transport (#3811)
- Support sending GraphQL operation names in HTTP breadcrumbs (#3931)

### Fixes

Expand Down
9 changes: 9 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */ = {isa = PBXBuildFile; fileRef = 15E0A8F12411A45A00F044E3 /* SentrySession.m */; };
33042A0D29DAF79A00C60085 /* SentryExtraContextProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 33042A0C29DAF79A00C60085 /* SentryExtraContextProvider.m */; };
33042A1729DC2C4300C60085 /* SentryExtraContextProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33042A1629DC2C4300C60085 /* SentryExtraContextProviderTests.swift */; };
51B15F7E2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift */; };
51B15F802BE88D510026A2F2 /* URLSessionTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B15F7F2BE88D510026A2F2 /* URLSessionTaskTests.swift */; };
620379DB2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h in Headers */ = {isa = PBXBuildFile; fileRef = 620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */; };
620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */ = {isa = PBXBuildFile; fileRef = 620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */; };
621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */; };
Expand Down Expand Up @@ -1026,6 +1028,8 @@
33042A0B29DAF5F400C60085 /* SentryExtraContextProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryExtraContextProvider.h; sourceTree = "<group>"; };
33042A0C29DAF79A00C60085 /* SentryExtraContextProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryExtraContextProvider.m; sourceTree = "<group>"; };
33042A1629DC2C4300C60085 /* SentryExtraContextProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExtraContextProviderTests.swift; sourceTree = "<group>"; };
51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskExtensions.swift; sourceTree = "<group>"; };
51B15F7F2BE88D510026A2F2 /* URLSessionTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskTests.swift; sourceTree = "<group>"; };
620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBuildAppStartSpans.h; path = include/SentryBuildAppStartSpans.h; sourceTree = "<group>"; };
620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBuildAppStartSpans.m; sourceTree = "<group>"; };
621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2085,6 +2089,7 @@
isa = PBXGroup;
children = (
62872B622BA1B86100A4FA7D /* NSLockTests.swift */,
51B15F7F2BE88D510026A2F2 /* URLSessionTaskTests.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -3783,6 +3788,7 @@
children = (
D8F016B52B962548007B9AFB /* StringExtensions.swift */,
62872B5E2BA1B7F300A4FA7D /* NSLock.swift */,
51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -4043,6 +4049,7 @@
639FCFA81EBC80CC00778193 /* SentryFrame.h in Headers */,
D8BFE37229A3782F002E73F3 /* SentryTimeToDisplayTracker.h in Headers */,
8E8C57A625EEFC43001CEEFA /* SentrySampling.h in Headers */,
8E8C57A625EEFC43001CEEFA /* SentrySampling.h in Headers */,
7B634599280EB9D100CFA05A /* SentryUIEventTrackingIntegration.h in Headers */,
63FE716D20DA4C1100CDBAE8 /* SentryCrashSysCtl.h in Headers */,
639889BB1EDED18400EA7442 /* SentrySwizzle.h in Headers */,
Expand Down Expand Up @@ -4387,6 +4394,7 @@
7B3B473825D6CC7E00D01640 /* SentryNSError.m in Sources */,
D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */,
7BE3C77D2446112C00A38442 /* SentryRateLimitParser.m in Sources */,
51B15F7E2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift in Sources */,
D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */,
8ECC674A25C23A20000E2BF6 /* SentryTransactionContext.mm in Sources */,
03BCC38C27E1C01A003232C7 /* SentryTime.mm in Sources */,
Expand Down Expand Up @@ -4722,6 +4730,7 @@
63FE721420DA66EC00CDBAE8 /* SentryCrashMemory_Tests.m in Sources */,
62885DA729E946B100554F38 /* TestConncurrentModifications.swift in Sources */,
63FE720520DA66EC00CDBAE8 /* FileBasedTestCase.m in Sources */,
51B15F802BE88D510026A2F2 /* URLSessionTaskTests.swift in Sources */,
63EED6C32237989300E02400 /* SentryOptionsTest.m in Sources */,
7BBD18B22451804C00427C76 /* SentryRetryAfterHeaderParserTests.swift in Sources */,
7BD337E424A356180050DB6E /* SentryCrashIntegrationTests.swift in Sources */,
Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ NS_SWIFT_NAME(Options)
*/
@property (nonatomic, assign) BOOL enableAutoSessionTracking;

/**
* Whether to attach the top level `operationName` node of HTTP json requests to HTTP breadcrumbs
* @note Default is @c NO.
*/
@property (nonatomic, assign) BOOL enableGraphQLOperationTracking;
maxchuquimia marked this conversation as resolved.
Show resolved Hide resolved

/**
* Whether to enable Watchdog Termination tracking or not.
* @note This feature requires the @c SentryCrashIntegration being enabled, otherwise it would
Expand Down
19 changes: 19 additions & 0 deletions Sources/Sentry/SentryNetworkTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
@property (nonatomic, assign) BOOL isNetworkTrackingEnabled;
@property (nonatomic, assign) BOOL isNetworkBreadcrumbEnabled;
@property (nonatomic, assign) BOOL isCaptureFailedRequestsEnabled;
@property (nonatomic, assign) BOOL isGraphQLOperationTrackingEnabled;

@end

Expand All @@ -62,6 +63,7 @@ - (instancetype)init
_isNetworkTrackingEnabled = NO;
_isNetworkBreadcrumbEnabled = NO;
_isCaptureFailedRequestsEnabled = NO;
_isGraphQLOperationTrackingEnabled = NO;
}
return self;
}
Expand All @@ -87,12 +89,20 @@ - (void)enableCaptureFailedRequests
}
}

- (void)enableGraphQLOperationTracking
{
@synchronized(self) {
_isGraphQLOperationTrackingEnabled = YES;
}
}

- (void)disable
{
@synchronized(self) {
_isNetworkBreadcrumbEnabled = NO;
_isNetworkTrackingEnabled = NO;
_isCaptureFailedRequestsEnabled = NO;
_isGraphQLOperationTrackingEnabled = NO;
}
}

Expand Down Expand Up @@ -440,6 +450,11 @@ - (void)captureFailedRequests:(NSURLSessionTask *)sessionTask
}

context[@"response"] = response;

if (self.isGraphQLOperationTrackingEnabled) {
context[@"graphql_operation_name"] = [sessionTask getGraphQLOperationName];
}

event.context = context;

[SentrySDK captureEvent:event];
Expand Down Expand Up @@ -489,6 +504,10 @@ - (void)addBreadcrumbForSessionTask:(NSURLSessionTask *)sessionTask
breadcrumbData[@"status_code"] = statusCode;
breadcrumbData[@"reason"] =
[NSHTTPURLResponse localizedStringForStatusCode:responseStatusCode];

if (self.isGraphQLOperationTrackingEnabled) {
breadcrumbData[@"graphql_operation_name"] = [sessionTask getGraphQLOperationName];
}
}

if (urlComponents.query != nil) {
Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryNetworkTrackingIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ - (BOOL)installWithOptions:(SentryOptions *)options
[SentryNetworkTracker.sharedInstance enableCaptureFailedRequests];
}

if (options.enableGraphQLOperationTracking) {
[SentryNetworkTracker.sharedInstance enableGraphQLOperationTracking];
}

if (shouldEnableNetworkTracking || options.enableNetworkBreadcrumbs
|| options.enableCaptureFailedRequests) {
[SentryNetworkTrackingIntegration swizzleURLSessionTask];
Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ - (instancetype)init
_integrations = SentryOptions.defaultIntegrations;
self.sampleRate = SENTRY_DEFAULT_SAMPLE_RATE;
self.enableAutoSessionTracking = YES;
self.enableGraphQLOperationTracking = NO;
self.enableWatchdogTerminationTracking = YES;
self.sessionTrackingIntervalMillis = [@30000 unsignedIntValue];
self.attachStacktrace = YES;
Expand Down Expand Up @@ -353,6 +354,9 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
[self setBool:options[@"enableAutoSessionTracking"]
block:^(BOOL value) { self->_enableAutoSessionTracking = value; }];

[self setBool:options[@"enableGraphQLOperationTracking"]
block:^(BOOL value) { self->_enableGraphQLOperationTracking = value; }];

[self setBool:options[@"enableWatchdogTerminationTracking"]
block:^(BOOL value) { self->_enableWatchdogTerminationTracking = value; }];

Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryNetworkTracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ static NSString *const SENTRY_NETWORK_REQUEST_TRACKER_BREADCRUMB
- (void)enableNetworkTracking;
- (void)enableNetworkBreadcrumbs;
- (void)enableCaptureFailedRequests;
- (void)enableGraphQLOperationTracking;
- (BOOL)isTargetMatch:(NSURL *)URL withTargets:(NSArray *)targets;
- (void)disable;

@property (nonatomic, readonly) BOOL isNetworkTrackingEnabled;
@property (nonatomic, readonly) BOOL isNetworkBreadcrumbEnabled;
@property (nonatomic, readonly) BOOL isCaptureFailedRequestsEnabled;
@property (nonatomic, readonly) BOOL isGraphQLOperationTrackingEnabled;

@end

Expand Down
19 changes: 19 additions & 0 deletions Sources/Swift/Extensions/URLSessionTaskExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

public extension URLSessionTask {

@objc
func getGraphQLOperationName() -> String? {
guard originalRequest?.value(forHTTPHeaderField: "Content-Type") == "application/json" else { return nil }
guard let requestBody = originalRequest?.httpBody else { return nil }

let requestInfo = try? JSONDecoder().decode(GraphQLRequest.self, from: requestBody)

return requestInfo?.operationName
}

}

private struct GraphQLRequest: Decodable {
let operationName: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,20 @@ class SentryNetworkTrackerIntegrationTests: XCTestCase {

XCTAssertFalse(SentryNetworkTracker.sharedInstance.isCaptureFailedRequestsEnabled)
}


func testGraphQLOperationTrackingEnabled() {
fixture.options.enableGraphQLOperationTracking = true
startSDK()

XCTAssertTrue(SentryNetworkTracker.sharedInstance.isGraphQLOperationTrackingEnabled)
}

func testGraphQLOperationTrackingDisabled() {
startSDK()

XCTAssertFalse(SentryNetworkTracker.sharedInstance.isGraphQLOperationTrackingEnabled)
}

func testGetCaptureFailedRequestsEnabled() {
let expect = expectation(description: "Request completed")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class SentryNetworkTrackerTests: XCTestCase {
let dateProvider = TestCurrentDateProvider()
let options: Options
let scope: Scope
let nsUrlRequest = NSURLRequest(url: SentryNetworkTrackerTests.fullUrl)
let nsUrlRequest = NSMutableURLRequest(url: SentryNetworkTrackerTests.fullUrl)
let client: TestClient!
let hub: TestHub!
let securityHeader = [ "X-FORWARDED-FOR": "value",
Expand Down Expand Up @@ -48,6 +48,7 @@ class SentryNetworkTrackerTests: XCTestCase {
result.enableNetworkTracking()
result.enableNetworkBreadcrumbs()
result.enableCaptureFailedRequests()
result.enableGraphQLOperationTracking()
return result
}
}
Expand Down Expand Up @@ -337,8 +338,41 @@ class SentryNetworkTrackerTests: XCTestCase {
XCTAssertEqual(breadcrumb!.data!["response_body_size"] as! Int64, DATA_BYTES_RECEIVED)
XCTAssertEqual(breadcrumb!.data!["http.query"] as? String, "query=value&query2=value2")
XCTAssertEqual(breadcrumb!.data!["http.fragment"] as? String, "fragment")
XCTAssertNil(breadcrumb!.data!["graphql_operation_name"])
}


func testBreadcrumb_GraphQLEnabled() {
let body = """
{
"operationName": "someOperationName",
"variables":{"a": 1},
"query":"query someOperationName {\\n someField\\n}\\n"
}
"""
fixture.nsUrlRequest.httpBody = body.data(using: .utf8)
fixture.nsUrlRequest.setValue("application/json", forHTTPHeaderField: "content-type")
assertStatus(status: .ok, state: .completed, response: createResponse(code: 200))

let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]?
let breadcrumb = breadcrumbs!.first
XCTAssertEqual(breadcrumb!.data!["graphql_operation_name"] as? String, "someOperationName")
}

func testBreadcrumb_GraphQLEnabledInvalidData() {
let body = """
[
{"message": "arrays are valid json"}
]
"""
fixture.nsUrlRequest.httpBody = body.data(using: .utf8)
fixture.nsUrlRequest.setValue("application/json", forHTTPHeaderField: "content-type")
assertStatus(status: .ok, state: .completed, response: createResponse(code: 200))

let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]?
let breadcrumb = breadcrumbs!.first
XCTAssertNil(breadcrumb!.data!["graphql_operation_name"])
}

func testNoBreadcrumb_DisablingBreadcrumb() {
assertStatus(status: .ok, state: .completed, response: createResponse(code: 200)) {
$0.disable()
Expand Down Expand Up @@ -868,13 +902,15 @@ class SentryNetworkTrackerTests: XCTestCase {
let requestType = span.data["type"] as? String
let query = span.data["http.query"] as? String
let fragment = span.data["http.fragment"] as? String
let graphql = span.data["graphql_operation_name"] as? String

XCTAssertEqual(path, "https://www.domain.com/api")
XCTAssertEqual(method, task.currentRequest!.httpMethod)
XCTAssertEqual(requestType, "fetch")
XCTAssertEqual(query, "query=value&query2=value2")
XCTAssertEqual(fragment, "fragment")

XCTAssertNil(graphql)

XCTAssertEqual(span.status, status)
XCTAssertNil(task.observationInfo)
}
Expand Down Expand Up @@ -925,6 +961,11 @@ class SentryNetworkTrackerTests: XCTestCase {
func createDataTask(method: String = "GET", modifyRequest: ((URLRequest) -> (URLRequest))? = nil) -> URLSessionDataTaskMock {
var request = URLRequest(url: SentryNetworkTrackerTests.fullUrl)
request.httpMethod = method
request.httpBody = fixture.nsUrlRequest.httpBody
fixture.nsUrlRequest.allHTTPHeaderFields?.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}

if let modifyRequest = modifyRequest {
request = modifyRequest(request)
}
Expand Down
5 changes: 5 additions & 0 deletions Tests/SentryTests/SentryOptionsTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ - (void)testEnableCoreDataTracking
[self testBooleanField:@"enableCoreDataTracing" defaultValue:YES];
}

- (void)testEnableGraphQLOperationTracking
{
[self testBooleanField:@"enableGraphQLOperationTracking" defaultValue:NO];
}

- (void)testSendClientReports
{
[self testBooleanField:@"sendClientReports" defaultValue:YES];
Expand Down
71 changes: 71 additions & 0 deletions Tests/SentryTests/Swift/Extensions/URLSessionTaskTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation
import Nimble
@testable import Sentry
import XCTest

final class URLSessionTaskTests: XCTestCase {

func testHTTPContentTypeInvalid() {
let task = makeTask(
headers: ["Content-Type": "image/jpeg"],
body: "8J+YiQo="
)

let operationName = task.getGraphQLOperationName()

expect(operationName) == nil
}

func testHTTPBodyDataInvalid() {
let task = makeTask(
headers: ["Content-Type": "application/json"],
body: "not json"
)

let operationName = task.getGraphQLOperationName()

expect(operationName) == nil
}

func testHTTPBodyDataMissing() {
let task = makeTask(
headers: ["Content-Type": "application/json"],
body: nil
)

let operationName = task.getGraphQLOperationName()

expect(operationName) == nil
}

func testHTTPBodyDataValidGraphQL() {
let task = makeTask(
headers: ["Content-Type": "application/json"],
body: """
{
"operationName": "MyOperation",
"variables": {
"id": "1234"
},
"query": "query MyOperation($id: ID!) { node(id: $id) { id } }"
}
"""
)

let operationName = task.getGraphQLOperationName()

expect(operationName) == "MyOperation"
}

}

private extension URLSessionTaskTests {

func makeTask(headers: [String: String], body: String?) -> URLSessionTask {
var request = URLRequest(url: URL(string: "https://anything.com")!)
request.httpBody = body?.data(using: .utf8)
request.allHTTPHeaderFields = headers
return URLSession(configuration: .ephemeral).dataTask(with: request)
}

}