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 1 commit
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
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 */; };
5147B3752B50B5FA00129F4B /* NSURLSessionTask+Sentry.h in Headers */ = {isa = PBXBuildFile; fileRef = 5147B3732B50B5FA00129F4B /* NSURLSessionTask+Sentry.h */; };
5147B3762B50B5FA00129F4B /* NSURLSessionTask+Sentry.m in Sources */ = {isa = PBXBuildFile; fileRef = 5147B3742B50B5FA00129F4B /* NSURLSessionTask+Sentry.m */; };
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 @@ -1021,6 +1023,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>"; };
5147B3732B50B5FA00129F4B /* NSURLSessionTask+Sentry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSURLSessionTask+Sentry.h"; path = "include/NSURLSessionTask+Sentry.h"; sourceTree = "<group>"; };
5147B3742B50B5FA00129F4B /* NSURLSessionTask+Sentry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSURLSessionTask+Sentry.m"; path = "include/NSURLSessionTask+Sentry.m"; 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 @@ -2206,6 +2210,8 @@
7B6438A926A70F24000D0F65 /* UIViewController+Sentry.m */,
0A2D8D9728997887008720F6 /* NSLocale+Sentry.h */,
0A2D8D9428997845008720F6 /* NSLocale+Sentry.m */,
5147B3732B50B5FA00129F4B /* NSURLSessionTask+Sentry.h */,
5147B3742B50B5FA00129F4B /* NSURLSessionTask+Sentry.m */,
);
name = Categories;
sourceTree = "<group>";
Expand Down Expand Up @@ -4029,6 +4035,8 @@
639FCFA81EBC80CC00778193 /* SentryFrame.h in Headers */,
D8BFE37229A3782F002E73F3 /* SentryTimeToDisplayTracker.h in Headers */,
8E8C57A625EEFC43001CEEFA /* SentrySampling.h in Headers */,
5147B3752B50B5FA00129F4B /* NSURLSessionTask+Sentry.h in Headers */,
8E8C57A625EEFC43001CEEFA /* SentryTracesSampler.h in Headers */,
7B634599280EB9D100CFA05A /* SentryUIEventTrackingIntegration.h in Headers */,
63FE716D20DA4C1100CDBAE8 /* SentryCrashSysCtl.h in Headers */,
639889BB1EDED18400EA7442 /* SentrySwizzle.h in Headers */,
Expand Down Expand Up @@ -4548,6 +4556,7 @@
7BA61CBB247BC5D800C130A8 /* SentryCrashDefaultBinaryImageProvider.m in Sources */,
63FE713120DA4C1100CDBAE8 /* SentryCrashDynamicLinker.c in Sources */,
8E25C95325F836D000DC215B /* SentryRandom.m in Sources */,
5147B3762B50B5FA00129F4B /* NSURLSessionTask+Sentry.m in Sources */,
7BC85231245812EC005A70F0 /* SentryFileContents.m in Sources */,
D80CD8D22B751447002F710B /* SentryMXManager.swift in Sources */,
03F84D3527DD4191008FE43F /* SentryThreadHandle.cpp 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
20 changes: 20 additions & 0 deletions Sources/Sentry/SentryNetworkTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#import "SentryTraceOrigins.h"
#import "SentryTracer.h"
#import "SentryUser.h"
#import "NSURLSessionTask+Sentry.h"
#import <objc/runtime.h>

/**
Expand All @@ -43,6 +44,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 +64,7 @@ - (instancetype)init
_isNetworkTrackingEnabled = NO;
_isNetworkBreadcrumbEnabled = NO;
_isCaptureFailedRequestsEnabled = NO;
_isGraphQLOperationTrackingEnabled = NO;
}
return self;
}
Expand All @@ -87,12 +90,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 +451,11 @@ - (void)captureFailedRequests:(NSURLSessionTask *)sessionTask
}

context[@"response"] = response;

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

event.context = context;

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

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

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
8 changes: 8 additions & 0 deletions Sources/Sentry/include/NSURLSessionTask+Sentry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import <Foundation/Foundation.h>

@interface
NSURLSessionTask (Sentry)

- (nullable NSString *)sentry_graphQLOperationName;

@end
25 changes: 25 additions & 0 deletions Sources/Sentry/include/NSURLSessionTask+Sentry.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#import "NSURLSessionTask+Sentry.h"


@implementation
NSURLSessionTask (Sentry)

- (nullable NSString *)sentry_graphQLOperationName
{
if (!self.originalRequest.HTTPBody) { return nil; }
if (![[self.originalRequest valueForHTTPHeaderField:@"Content-Type"] isEqual: @"application/json"]) { return nil; }

NSError *error = nil;
id requestDictionary = [NSJSONSerialization JSONObjectWithData:self.originalRequest.HTTPBody options:0 error:&error];

if (error) { return nil; }
if (![requestDictionary isKindOfClass: [NSDictionary class]]) { return nil; } // Could be an array

id operationName = [requestDictionary valueForKey:@"operationName"];
if (![operationName isKindOfClass: [NSString class]]) { return nil; }
if ([operationName length] == 0) { return nil; }

return operationName;
}
brustolin marked this conversation as resolved.
Show resolved Hide resolved

@end
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
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"])
}


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"] 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"])
}

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"] 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