getsentry / sentry-cocoa

The official Sentry SDK for iOS, tvOS, macOS, watchOS.
https://sentry.io/for/cocoa/
MIT License
797 stars 315 forks source link

Custom URLSession Memory & CPU Usage #4158

Open Memocana opened 2 months ago

Memocana commented 2 months ago

Platform

iOS

Environment

Develop, TestFlight

Installed

Swift Package Manager

Version

8.30.0

Xcode Version

15.2

Did it work on previous versions?

No response

Steps to Reproduce

Using the sample URLSession here: https://github.com/getsentry/sentry-cocoa/discussions/4047#discussioncomment-9739280

I added a button that sends an exception and an error in to my app. When I disable the URL Session, everything sends nicely. When I reenable it, I see two things happening: 1- I memory usage continuously increases with 100% CPU usage until it force shuts the app 2- The canonicalRequest function never gets called after the handshake with the backend, no matter how many times SentrySDK.capture(exception:) or SentrySDK.capture(error:) are called

I just have an empty screen with a button to test this and the app reliably always crashes.

Expected Result

No crashes and consistent memory usage similar to the default URLSession.

Actual Result

The app exhausts the memory

image image
class CustomURLProtocol: URLProtocol {
    override class func canInit(with request: URLRequest) -> Bool {
        return URLProtocol.property(forKey: "Handled", in: request) == nil
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        guard let newUrl = URL(string: "xxx/forwardSentry") else { return request }
        var newRequest = URLRequest(url: newUrl)
        newRequest.httpBody = request.httpBody
        newRequest.httpBodyStream = request.httpBodyStream
        newRequest.httpMethod = request.httpMethod
        newRequest.allHTTPHeaderFields = request.allHTTPHeaderFields

        // Breakpoint here never gets stopped after the first 5 times
        return newRequest
    }

    override func startLoading() {
        URLProtocol.setProperty(true, forKey: "Handled", in: request as! NSMutableURLRequest)
        let newTask = URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data { self.client?.urlProtocol(self, didLoad: data) }
            if let response = response { self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) }
            if let error = error { self.client?.urlProtocol(self, didFailWithError: error) }
            self.client?.urlProtocolDidFinishLoading(self)
        }
        newTask.resume()
    }

    override func stopLoading() {
        // Not implemented on purpose
    }
}
@main
struct TestApp: App {

    init() {
        let config = URLSessionConfiguration.default
        config.protocolClasses = [CustomURLProtocol.self]

        SentrySDK.start { options in
            options.dsn = "https://734ed0faf0e9b359e6490bf0272427bf@o4507339912904704.ingest.us.sentry.io/4507391647547392"
            options.urlSession = URLSession(configuration: config)
            options.debug = true // Enabled debug when first installing is always helpful
            // Enable tracing to capture 100% of transactions for tracing.
            // Use 'options.tracesSampleRate' to set the sampling rate.
            // We recommend setting a sample rate in production.
            options.enableTracing = true
            options.tracesSampleRate = 0.005
            options.profilesSampleRate = 0.1
            options.maxBreadcrumbs = 1
        }
    }

  var body: some Scene {
    WindowGroup {
      Button {
        let id = SentrySDK.capture(exception: NSException(name: .genericException, reason: "Login Failure"))
        print(id.description) //ID is correctly populated, but the backend never receives a request, nor the dashboard has an exception when the URL Session is on
      } label: {
        Text("Sign-in")
          .foregroundColor(.white)
          .padding(.horizontal, 24)
          .padding(.vertical, 12)
          .background(RoundedRectangle(cornerRadius: 10).fill(.cyan))
      }
    }
  }
}

Are you willing to submit a PR?

No response

brustolin commented 2 months ago

Hello @Memocana, thanks for reaching out. The code snippet provided at #4047 is a starting point where you can continue to write your own custom URLSession. However, we don't have the bandwidth to help you optimize it and handle all the edge cases. Users should be cautious when using a custom URLSession.

We will discuss about providing a safer alternative to implement proxies.

kahest commented 2 months ago

We will try to repro the issue and follow up here.

Memocana commented 1 month ago

Hey there! I was able to identify the root cause of the issue on our end. It looks like when the backend sends 200 with body/headers that are missing/wrong the SDK goes in to a frozen state. We worked with our backend guys to make sure the responses are sent the correct way, but the SDK should still be able to handle these scenarios more gracefully.

brustolin commented 1 month ago

Can you give an example of wrong header/body that your server was returning?

Memocana commented 1 month ago
URLResponse:
{ URL: <tunnel_url> },
{ 
  Status Code: 200, 
  Headers {
    "Content-Length" =     (
        2
    );
    "Content-Type" =     (
        "application/json; charset=utf-8"
    );
    Date =     (
        "Fri, 19 Jul 2024 17:12:02 GMT"
    );
    Etag =     (
        "W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\""
    );
    "Strict-Transport-Security" =     (
        "max-age=15552000; includeSubDomains"
    );
    "content-security-policy" =     (
        "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests"
    );
    "cross-origin-embedder-policy" =     (
        "require-corp"
    );
    "cross-origin-opener-policy" =     (
        "same-origin"
    );
    "cross-origin-resource-policy" =     (
        "same-origin"
    );
    "origin-agent-cluster" =     (
        "?1"
    );
    "referrer-policy" =     (
        "no-referrer"
    );
    "x-content-type-options" =     (
        nosniff
    );
    "x-dns-prefetch-control" =     (
        off
    );
    "x-download-options" =     (
        noopen
    );
    "x-frame-options" =     (
        SAMEORIGIN
    );
    "x-permitted-cross-domain-policies" =     (
        none
    );
    "x-xss-protection" =     (
        0
    );
  } 
}

This was the initial response we had, and it contained no body. We also tried modifying the body to return { "id": <id> } similarly to how Sentry API returns but the cpu issues persisted even after that until the headers were identical too.