diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a86689c0ead..4589ad56069 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,6 +149,7 @@ jobs: - uses: actions/download-artifact@v4 with: name: ${{ github.sha }} + path: Carthage/ - run: ./scripts/ci-select-xcode.sh 15.2 - run: make build-xcframework-sample shell: sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3b00ac62d2..ae098939acd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -238,7 +238,7 @@ jobs: # We don't upload codecov for scheduled runs as CodeCov only accepts a limited amount of uploads per commit. - name: Push code coverage to codecov id: codecov_1 - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # pin@v4.3.0 + uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # pin@v4.3.1 if: ${{ contains(matrix.platform, 'iOS') && !contains(github.ref, 'release') && github.event.schedule == '' }} with: # Although public repos should not have to specify a token there seems to be a bug with the Codecov GH action, which can @@ -250,7 +250,7 @@ jobs: # Sometimes codecov uploads etc can fail. Retry one time to rule out e.g. intermittent network failures. - name: Push code coverage to codecov id: codecov_2 - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # pin@v4.3.0 + uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # pin@v4.3.1 if: ${{ steps.codecov_1.outcome == 'failure' && contains(matrix.platform, 'iOS') && !contains(github.ref, 'release') && github.event.schedule == '' }} with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index d6cbbdf8a29..8ff77f8d2d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,25 @@ ## Unreleased +### Features + +- Add option to use own NSURLSession for transport (#3811) +- Support sending GraphQL operation names in HTTP breadcrumbs (#3931) + ### Fixes - Ignore SentryFramesTracker thread sanitizer data races (#3922) - Handle no releaseName in WatchDogTerminationLogic (#3919) - Fix data race when calling reportFullyDisplayed from a background thread (#3926) +- Stop SessionReplay when closing SDK (#3941) + +### Improvements +- Remove not needed lock for logging (#3934) +- Session replay Improvements (#3877) + - Use image average color and text font color to redact session replay + - Removed iOS 16 restriction from session replay + - Performance improvement ## 8.25.0 diff --git a/LICENSE.md b/LICENSE.md index 5a483f25178..95b14dbf956 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Sentry +Copyright (c) 2015-2024 Sentry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 03d38b423b9..40135715ec2 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.debug = true if #available(iOS 16.0, *) { - options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 1, redactAllText: true, redactAllImages: true) + options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: true, redactAllImages: true) } if #available(iOS 15.0, *) { diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 716ee9b912e..89ad8d24dc0 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -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 */; }; @@ -795,7 +797,6 @@ D820CDB42BB1886100BA339D /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB12BB1886100BA339D /* SentrySessionReplay.h */; }; D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */; }; D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */; }; - D820CE132BB2F13C00BA339D /* SentryCoreGraphicsHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */; }; D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; @@ -838,7 +839,6 @@ D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; }; D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; }; D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; }; - D878C6A82BC7F01C0039D6A3 /* SentryCoreGraphicsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */; }; D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */; }; D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */; }; D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; }; @@ -859,6 +859,10 @@ D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */; }; D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */; }; D8AFC0012BD252B900118BE1 /* SentryOnDemandReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */; }; + D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */; }; + D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */; }; + D8AFC0572BDA895400118BE1 /* UIRedactBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */; }; + D8AFC05A2BDA89C100118BE1 /* RedactRegionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */; }; D8AFC0622BDBEE4200118BE1 /* SentrySessionReplayIntegration+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */; }; D8B0542E2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */; }; D8B088B629C9E3FF00213258 /* SentryTracerConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */; }; @@ -890,6 +894,9 @@ D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */; }; D8F016B32B9622D6007B9AFB /* SentryId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016B22B9622D6007B9AFB /* SentryId.swift */; }; D8F016B62B962548007B9AFB /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F016B52B962548007B9AFB /* StringExtensions.swift */; }; + D8F67AEE2BE0D19200C9197B /* UIImageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F67AED2BE0D19200C9197B /* UIImageHelper.swift */; }; + D8F67AF12BE0D33F00C9197B /* UIImageHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F67AEF2BE0D31A00C9197B /* UIImageHelperTests.swift */; }; + D8F67AF42BE10F9600C9197B /* UIRedactBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F67AF22BE10F7600C9197B /* UIRedactBuilderTests.swift */; }; D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */; }; D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */; }; D8F6A24E288553A800320515 /* SentryPredicateDescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */; }; @@ -1021,6 +1028,8 @@ 33042A0B29DAF5F400C60085 /* SentryExtraContextProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryExtraContextProvider.h; sourceTree = ""; }; 33042A0C29DAF79A00C60085 /* SentryExtraContextProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryExtraContextProvider.m; sourceTree = ""; }; 33042A1629DC2C4300C60085 /* SentryExtraContextProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExtraContextProviderTests.swift; sourceTree = ""; }; + 51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskExtensions.swift; sourceTree = ""; }; + 51B15F7F2BE88D510026A2F2 /* URLSessionTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskTests.swift; sourceTree = ""; }; 620379DA2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBuildAppStartSpans.h; path = include/SentryBuildAppStartSpans.h; sourceTree = ""; }; 620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBuildAppStartSpans.m; sourceTree = ""; }; 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = ""; }; @@ -1819,8 +1828,6 @@ D820CDB22BB1886100BA339D /* SentrySessionReplay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; - D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCoreGraphicsHelper.h; path = include/SentryCoreGraphicsHelper.h; sourceTree = ""; }; - D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCoreGraphicsHelper.m; sourceTree = ""; }; D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = ""; }; D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; @@ -1890,6 +1897,10 @@ D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataTracker.h; path = include/SentryNSDataTracker.h; sourceTree = ""; }; D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackingIntegration.h; path = include/SentryFileIOTrackingIntegration.h; sourceTree = ""; }; D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplayTests.swift; sourceTree = ""; }; + D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewScreenshotProvider.swift; sourceTree = ""; }; + D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayVideoMaker.swift; sourceTree = ""; }; + D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIRedactBuilder.swift; sourceTree = ""; }; + D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactRegionTests.swift; sourceTree = ""; }; D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySessionReplayIntegration+Private.h"; path = "include/SentrySessionReplayIntegration+Private.h"; sourceTree = ""; }; D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTracerConfiguration.h; path = include/SentryTracerConfiguration.h; sourceTree = ""; }; @@ -1927,6 +1938,9 @@ D8F016B52B962548007B9AFB /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; D8F01DE42A126B62008F4996 /* HybridPod.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = HybridPod.podspec; sourceTree = ""; }; D8F01DE52A126BF5008F4996 /* HybridTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HybridTest.swift; sourceTree = ""; }; + D8F67AED2BE0D19200C9197B /* UIImageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageHelper.swift; sourceTree = ""; }; + D8F67AEF2BE0D31A00C9197B /* UIImageHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageHelperTests.swift; sourceTree = ""; }; + D8F67AF22BE10F7600C9197B /* UIRedactBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIRedactBuilderTests.swift; sourceTree = ""; }; D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPredicateDescriptor.m; sourceTree = ""; }; D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryPredicateDescriptor.h; path = include/SentryPredicateDescriptor.h; sourceTree = ""; }; D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPredicateDescriptorTests.swift; sourceTree = ""; }; @@ -2075,6 +2089,7 @@ isa = PBXGroup; children = ( 62872B622BA1B86100A4FA7D /* NSLockTests.swift */, + 51B15F7F2BE88D510026A2F2 /* URLSessionTaskTests.swift */, ); path = Extensions; sourceTree = ""; @@ -3543,8 +3558,6 @@ D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */, D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */, D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */, - D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */, - D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */, ); name = SessionReplay; sourceTree = ""; @@ -3583,6 +3596,9 @@ D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */, D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */, D8B425112B9A0FD6000BFDF3 /* StringExtensionTests.swift */, + D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */, + D8F67AEF2BE0D31A00C9197B /* UIImageHelperTests.swift */, + D8F67AF22BE10F7600C9197B /* UIRedactBuilderTests.swift */, ); name = Tools; sourceTree = ""; @@ -3611,6 +3627,9 @@ D856272B2A374A8600FB8062 /* UrlSanitized.swift */, D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */, D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */, + D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */, + D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */, + D8F67AED2BE0D19200C9197B /* UIImageHelper.swift */, ); path = Tools; sourceTree = ""; @@ -3741,6 +3760,7 @@ D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */, D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */, + D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */, ); path = SessionReplay; sourceTree = ""; @@ -3768,6 +3788,7 @@ children = ( D8F016B52B962548007B9AFB /* StringExtensions.swift */, 62872B5E2BA1B7F300A4FA7D /* NSLock.swift */, + 51B15F7D2BE88A7C0026A2F2 /* URLSessionTaskExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -3828,7 +3849,6 @@ 7B0A54222521C21E00A71716 /* SentryFrameRemover.h in Headers */, 63FE70CD20DA4C1000CDBAE8 /* SentryCrashDoctor.h in Headers */, D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */, - D820CE132BB2F13C00BA339D /* SentryCoreGraphicsHelper.h in Headers */, 7B6438AA26A70F24000D0F65 /* UIViewController+Sentry.h in Headers */, 639FCFAC1EBC811400778193 /* SentryUser.h in Headers */, D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */, @@ -4029,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 */, @@ -4347,6 +4368,7 @@ 7BFC16A125249A9D00FF6266 /* SentryMessage.m in Sources */, 7BCFBD6F2681D0EE00BC27D8 /* SentryCrashScopeObserver.m in Sources */, 7BD86ED1264A7CF6005439DB /* SentryAppStartMeasurement.m in Sources */, + D8F67AEE2BE0D19200C9197B /* UIImageHelper.swift in Sources */, 7DC27EC723997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.m in Sources */, 63FE717B20DA4C1100CDBAE8 /* SentryCrashReport.c in Sources */, 7B7A599726B692F00060A676 /* SentryScreenFrames.m in Sources */, @@ -4372,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 */, @@ -4420,6 +4443,7 @@ 7B18DE4228D9F794004845C6 /* SentryNSNotificationCenterWrapper.m in Sources */, 639FCFA91EBC80CC00778193 /* SentryFrame.m in Sources */, D858FA672A29EAB3002A3503 /* SentryBinaryImageCache.m in Sources */, + D8AFC0572BDA895400118BE1 /* UIRedactBuilder.swift in Sources */, 8E564AEA267AF22600FE117D /* SentryNetworkTracker.m in Sources */, 15360CED2433A15500112302 /* SentryInstallation.m in Sources */, 7B98D7E825FB7BCD00C5A389 /* SentryAppState.m in Sources */, @@ -4459,9 +4483,11 @@ 0A9BF4E228A114940068D266 /* SentryViewHierarchyIntegration.m in Sources */, 0ADC33EC28D9BB780078D980 /* SentryUIDeviceWrapper.m in Sources */, 7BBD188B244841FB00427C76 /* SentryHttpDateParser.m in Sources */, + D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */, 840A11122B61E27500650D02 /* SentrySamplerDecision.m in Sources */, 8E4E7C8225DAB2A5006AB9E2 /* SentryTracer.m in Sources */, 848A45192BBF8D33006AAAEC /* SentryContinuousProfiler.m in Sources */, + D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */, 15E0A8E5240C457D00F044E3 /* SentryEnvelope.m in Sources */, 03F84D3627DD4191008FE43F /* SentryProfilingLogging.mm in Sources */, 8EC3AE7A25CA23B600E7591A /* SentrySpan.m in Sources */, @@ -4538,7 +4564,6 @@ 63FE712D20DA4C1100CDBAE8 /* SentryCrashJSONCodecObjC.m in Sources */, 7BBD18932449BEDD00427C76 /* SentryDefaultRateLimits.m in Sources */, 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, - D878C6A82BC7F01C0039D6A3 /* SentryCoreGraphicsHelper.m in Sources */, 62262B882BA1C490004DA3DD /* SentryStatsdClient.m in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, 8E8C57A225EEFC07001CEEFA /* SentrySampling.m in Sources */, @@ -4637,6 +4662,7 @@ 63FE722420DA66EC00CDBAE8 /* SentryCrashMonitor_NSException_Tests.m in Sources */, 7B5AB65D27E48E5200F1D1BA /* TestThreadInspector.swift in Sources */, 7BF9EF742722A85B00B5BBEF /* SentryClassRegistrator.m in Sources */, + D8F67AF42BE10F9600C9197B /* UIRedactBuilderTests.swift in Sources */, 63B819141EC352A7002FDF4C /* SentryInterfacesTests.m in Sources */, 7B68345128F7EB3D00FB7064 /* SentryMeasurementUnitTests.swift in Sources */, 7B14089A248791660035403D /* SentryCrashStackEntryMapperTests.swift in Sources */, @@ -4704,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 */, @@ -4765,6 +4792,7 @@ 62BAD74E2BA1C58D00EBAAFC /* EncodeMetricTests.swift in Sources */, 7BE0DC29272A9E1C004FA8B7 /* SentryBreadcrumbTrackerTests.swift in Sources */, 63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */, + D8AFC05A2BDA89C100118BE1 /* RedactRegionTests.swift in Sources */, D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */, 7BFC16BA2524D4AF00FF6266 /* SentryMessage+Equality.m in Sources */, 7B4260342630315C00B36EDD /* SampleError.swift in Sources */, @@ -4775,6 +4803,7 @@ D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */, 7B16FD022654F86B008177D3 /* SentrySysctlTests.swift in Sources */, 7BAF3DB5243C743E008A5414 /* SentryClientTests.swift in Sources */, + D8F67AF12BE0D33F00C9197B /* UIImageHelperTests.swift in Sources */, 8EAE8E5E2681768000D6958B /* URLSessionTaskMock.m in Sources */, D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */, D85D3BEA278DF63D001B2889 /* SentryByteCountFormatterTests.swift in Sources */, @@ -5569,7 +5598,6 @@ OTHER_SWIFT_FLAGS = "-DCARTHAGE"; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.SentrySwiftUI; SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = Sources/SentrySwiftUI/; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -6300,7 +6328,6 @@ OTHER_SWIFT_FLAGS = "-DCARTHAGE"; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.SentrySwiftUI; SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = Sources/SentrySwiftUI/; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -6496,7 +6523,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -6552,7 +6578,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; diff --git a/Sources/Configuration/SDK.xcconfig b/Sources/Configuration/SDK.xcconfig index d092a97c74b..fccf5ae1380 100644 --- a/Sources/Configuration/SDK.xcconfig +++ b/Sources/Configuration/SDK.xcconfig @@ -32,3 +32,11 @@ CLANG_CXX_LIBRARY = libc++ // leads to an error Module _SentryPrivate not found in the SentryTests-Swift.h header when import the // SentryPrivate module with @import _SentryPrivate. HEADER_SEARCH_PATHS = $(SRCROOT)/Sources/Sentry/include/** + +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Debug = DEBUG +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Debug_without_UIKit = DEBUG +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Test = TEST +SWIFT_ACTIVE_COMPILATION_CONDITIONS_TestCI = TESTCI +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Release = +SWIFT_ACTIVE_COMPILATION_CONDITIONS_Release_without_UIKit = +SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(SWIFT_ACTIVE_COMPILATION_CONDITIONS_$(CONFIGURATION)) diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 8f67f545c8d..c1019db4d01 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -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; + /** * Whether to enable Watchdog Termination tracking or not. * @note This feature requires the @c SentryCrashIntegration being enabled, otherwise it would @@ -358,9 +364,22 @@ NS_SWIFT_NAME(Options) /** * Set as delegate on the @c NSURLSession used for all network data-transfer tasks performed by * Sentry. + * + * @discussion The SDK ignores this option when using @c urlSession. */ @property (nullable, nonatomic, weak) id urlSessionDelegate; +/** + * Use this property, so the transport uses this @c NSURLSession with your configuration for + * sending requests to Sentry. + * + * If not set, the SDK will create a new @c NSURLSession with @c [NSURLSessionConfiguration + * ephemeralSessionConfiguration]. + * + * @note Default is @c nil. + */ +@property (nullable, nonatomic, strong) NSURLSession *urlSession; + /** * Wether the SDK should use swizzling or not. * @discussion When turned off the following features are disabled: breadcrumbs for touch events and diff --git a/Sources/Sentry/SentryAppStartTracker.m b/Sources/Sentry/SentryAppStartTracker.m index 3170b275257..e9f8d84ade2 100644 --- a/Sources/Sentry/SentryAppStartTracker.m +++ b/Sources/Sentry/SentryAppStartTracker.m @@ -226,12 +226,12 @@ - (void)buildAppStartMeasurement:(NSDate *)appStartEnd // With only running this once we know that the process is a new one when the following // code is executed. // We need to make sure the block runs on each test instead of only once -# if TEST +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) block(); # else static dispatch_once_t once; [self.dispatchQueue dispatchOnce:&once block:block]; -# endif +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) } /** @@ -316,9 +316,9 @@ - (void)stop [self.framesTracker removeListener:self]; -# if TEST +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) self.isRunning = NO; -# endif +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) } - (void)dealloc diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m deleted file mode 100644 index 56bb3816299..00000000000 --- a/Sources/Sentry/SentryCoreGraphicsHelper.m +++ /dev/null @@ -1,18 +0,0 @@ -#import "SentryCoreGraphicsHelper.h" -#if SENTRY_HAS_UIKIT -@implementation SentryCoreGraphicsHelper -+ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path -{ -# if (TARGET_OS_IOS || TARGET_OS_TV) -# ifdef __IPHONE_16_0 - if (@available(iOS 16.0, tvOS 16.0, *)) { - CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); - CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); - return CGPathCreateMutableCopy(newPath); - } -# endif // defined(__IPHONE_16_0) -# endif // (TARGET_OS_IOS || TARGET_OS_TV) - return path; -} -@end -#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m index 2c60867da1b..a9a8a6d8ba1 100644 --- a/Sources/Sentry/SentryHttpTransport.m +++ b/Sources/Sentry/SentryHttpTransport.m @@ -42,9 +42,9 @@ @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueue; @property (nonatomic, strong) dispatch_group_t dispatchGroup; -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) @property (nullable, nonatomic, strong) void (^startFlushCallback)(void); -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Relay expects the discarded events split by data category and reason; see @@ -161,12 +161,12 @@ - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason } } -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setStartFlushCallback:(void (^)(void))callback { _startFlushCallback = callback; } -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) - (SentryFlushResult)flush:(NSTimeInterval)timeout { @@ -192,11 +192,11 @@ - (SentryFlushResult)flush:(NSTimeInterval)timeout _isFlushing = YES; dispatch_group_enter(self.dispatchGroup); -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) if (self.startFlushCallback != nil) { self.startFlushCallback(); } -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) } [self sendAllCachedEnvelopes]; diff --git a/Sources/Sentry/SentryLog.m b/Sources/Sentry/SentryLog.m index 4335fc67fd0..d53d219a57b 100644 --- a/Sources/Sentry/SentryLog.m +++ b/Sources/Sentry/SentryLog.m @@ -1,4 +1,5 @@ #import "SentryLog.h" +#import "SentryInternalCDefines.h" #import "SentryLevelMapper.h" #import "SentryLogOutput.h" @@ -37,10 +38,12 @@ + (void)logWithMessage:(NSString *)message andLevel:(SentryLevel)level } + (BOOL)willLogAtLevel:(SentryLevel)level + SENTRY_DISABLE_THREAD_SANITIZER( + "The SDK usually configures the log level and isDebug once when it starts. For tests, we " + "accept a data race causing some log messages of the wrong level over using a synchronized " + "block for this method, as it's called frequently in production.") { - @synchronized(logConfigureLock) { - return isDebug && level != kSentryLevelNone && level >= diagnosticLevel; - } + return isDebug && level != kSentryLevelNone && level >= diagnosticLevel; } // Internal and only needed for testing. diff --git a/Sources/Sentry/SentryNSProcessInfoWrapper.mm b/Sources/Sentry/SentryNSProcessInfoWrapper.mm index 87b289041e0..bf7713e5f88 100644 --- a/Sources/Sentry/SentryNSProcessInfoWrapper.mm +++ b/Sources/Sentry/SentryNSProcessInfoWrapper.mm @@ -1,7 +1,7 @@ #import "SentryNSProcessInfoWrapper.h" @implementation SentryNSProcessInfoWrapper { -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) NSString *_executablePath; } - (void)setProcessPath:(NSString *)path @@ -20,7 +20,7 @@ - (instancetype)init #else } # define SENTRY_BINARY_EXECUTABLE_PATH NSBundle.mainBundle.executablePath; -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) + (SentryNSProcessInfoWrapper *)shared { diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index b55d0876891..93287f991c6 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -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 @@ -62,6 +63,7 @@ - (instancetype)init _isNetworkTrackingEnabled = NO; _isNetworkBreadcrumbEnabled = NO; _isCaptureFailedRequestsEnabled = NO; + _isGraphQLOperationTrackingEnabled = NO; } return self; } @@ -87,12 +89,20 @@ - (void)enableCaptureFailedRequests } } +- (void)enableGraphQLOperationTracking +{ + @synchronized(self) { + _isGraphQLOperationTrackingEnabled = YES; + } +} + - (void)disable { @synchronized(self) { _isNetworkBreadcrumbEnabled = NO; _isNetworkTrackingEnabled = NO; _isCaptureFailedRequestsEnabled = NO; + _isGraphQLOperationTrackingEnabled = NO; } } @@ -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]; @@ -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) { diff --git a/Sources/Sentry/SentryNetworkTrackingIntegration.m b/Sources/Sentry/SentryNetworkTrackingIntegration.m index 63ee302eb7b..5c70c80a2d5 100644 --- a/Sources/Sentry/SentryNetworkTrackingIntegration.m +++ b/Sources/Sentry/SentryNetworkTrackingIntegration.m @@ -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]; diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 8e114e7773f..e6ca2aaa6fb 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -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; @@ -353,6 +354,9 @@ - (BOOL)validateOptions:(NSDictionary *)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; }]; @@ -446,6 +450,10 @@ - (BOOL)validateOptions:(NSDictionary *)options _inAppExcludes = [options[@"inAppExcludes"] filteredArrayUsingPredicate:isNSString]; } + if ([options[@"urlSession"] isKindOfClass:[NSURLSession class]]) { + self.urlSession = options[@"urlSession"]; + } + if ([options[@"urlSessionDelegate"] conformsToProtocol:@protocol(NSURLSessionDelegate)]) { self.urlSessionDelegate = options[@"urlSessionDelegate"]; } diff --git a/Sources/Sentry/SentryReachability.m b/Sources/Sentry/SentryReachability.m index d50d46a508d..3d5aca8f17f 100644 --- a/Sources/Sentry/SentryReachability.m +++ b/Sources/Sentry/SentryReachability.m @@ -39,7 +39,7 @@ NSString *const SentryConnectivityWiFi = @"wifi"; NSString *const SentryConnectivityNone = @"none"; -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) static BOOL sentry_reachability_ignore_actual_callback = NO; void @@ -48,7 +48,7 @@ SENTRY_LOG_DEBUG(@"Setting ignore actual callback to %@", value ? @"YES" : @"NO"); sentry_reachability_ignore_actual_callback = value; } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Check whether the connectivity change should be noted or ignored. @@ -135,12 +135,12 @@ { SENTRY_LOG_DEBUG( @"SentryConnectivityCallback called with target: %@; flags: %u", target, flags); -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) if (sentry_reachability_ignore_actual_callback) { SENTRY_LOG_DEBUG(@"Ignoring actual callback."); return; } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) SentryConnectivityCallback(flags); } @@ -161,7 +161,7 @@ + (void)initialize } } -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (instancetype)init { @@ -172,7 +172,7 @@ - (instancetype)init return self; } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)addObserver:(id)observer; { @@ -190,12 +190,12 @@ - (void)addObserver:(id)observer; return; } -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) if (self.skipRegisteringActualCallbacks) { SENTRY_LOG_DEBUG(@"Skip registering actual callbacks"); return; } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) sentry_reachability_queue = dispatch_queue_create("io.sentry.cocoa.connectivity", DISPATCH_QUEUE_SERIAL); @@ -240,11 +240,11 @@ - (void)removeAllObservers - (void)unsetReachabilityCallback { -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) if (self.skipRegisteringActualCallbacks) { SENTRY_LOG_DEBUG(@"Skip unsetting actual callbacks"); } -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) sentry_current_reachability_state = kSCNetworkReachabilityFlagsUninitialized; diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index a31659ebb99..aeb763c9fa9 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -35,7 +35,7 @@ @implementation SentrySessionReplay { NSDate *_sessionStart; NSMutableArray *imageCollection; SentryReplayOptions *_replayOptions; - SentryOnDemandReplay *_replayMaker; + id _replayMaker; SentryDisplayLinkWrapper *_displayLink; SentryCurrentDateProvider *_dateProvider; id _sentryRandom; @@ -48,7 +48,7 @@ @implementation SentrySessionReplay { - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions replayFolderPath:(NSURL *)folderPath screenshotProvider:(id)screenshotProvider - replayMaker:(id)replayMaker + replayMaker:(id)replayMaker dateProvider:(SentryCurrentDateProvider *)dateProvider random:(id)random displayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper; @@ -114,6 +114,11 @@ - (void)stop } } +- (void)dealloc +{ + [self stop]; +} + - (void)captureReplayForEvent:(SentryEvent *)event; { if (!_isRunning) { @@ -242,6 +247,7 @@ - (void)createAndCapture:(NSURL *)videoUrl duration:(NSTimeInterval)duration startedAt:(NSDate *)start { + __weak SentrySessionReplay *weakSelf = self; [_replayMaker createVideoWithDuration:duration beginning:start @@ -251,17 +257,22 @@ - (void)createAndCapture:(NSURL *)videoUrl if (error != nil) { SENTRY_LOG_ERROR(@"Could not create replay video - %@", error); } else { - [self captureSegment:self->_currentSegmentId++ - video:videoInfo - replayId:self->_sessionReplayId - replayType:kSentryReplayTypeSession]; - - [self->_replayMaker releaseFramesUntil:videoInfo.end]; - self->_videoSegmentStart = nil; + [weakSelf newSegmentAvailable:videoInfo]; } }]; } +- (void)newSegmentAvailable:(SentryVideoInfo *)videoInfo +{ + [self captureSegment:self->_currentSegmentId++ + video:videoInfo + replayId:self->_sessionReplayId + replayType:kSentryReplayTypeSession]; + + [_replayMaker releaseFramesUntil:videoInfo.end]; + _videoSegmentStart = nil; +} + - (void)captureSegment:(NSInteger)segment video:(SentryVideoInfo *)videoInfo replayId:(SentryId *)replayid @@ -306,11 +317,16 @@ - (void)takeScreenshot _processingScreenshot = YES; } - UIImage *screenshot = [_screenshotProvider imageWithView:_rootView options:_replayOptions]; + __weak SentrySessionReplay *weakSelf = self; + [_screenshotProvider imageWithView:_rootView + options:_replayOptions + onComplete:^(UIImage *screenshot) { [weakSelf newImage:screenshot]; }]; +} +- (void)newImage:(UIImage *)image +{ _processingScreenshot = NO; - - [self->_replayMaker addFrameAsyncWithImage:screenshot]; + [_replayMaker addFrameAsyncWithImage:image]; } @end diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 7007b66c73c..bb08c45d3fe 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -28,17 +28,6 @@ - (void)newSceneActivate; @end -API_AVAILABLE(ios(16.0), tvos(16.0)) -@interface -SentryViewPhotographer (SentryViewScreenshotProvider) -@end - -API_AVAILABLE(ios(16.0), tvos(16.0)) -@interface -SentryOnDemandReplay (SentryReplayMaker) - -@end - @implementation SentrySessionReplayIntegration { BOOL _startedAsFullSession; SentryReplayOptions *_replayOptions; @@ -150,6 +139,7 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { + [self stop]; } - (BOOL)shouldReplayFullSession:(CGFloat)rate diff --git a/Sources/Sentry/SentrySpotlightTransport.m b/Sources/Sentry/SentrySpotlightTransport.m index a4e3754c077..7f528bed8fb 100644 --- a/Sources/Sentry/SentrySpotlightTransport.m +++ b/Sources/Sentry/SentrySpotlightTransport.m @@ -96,12 +96,12 @@ - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason // Empty on purpose } -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setStartFlushCallback:(nonnull void (^)(void))callback { // Empty on purpose } -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 2874d1f76cb..516492a34d5 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -25,11 +25,11 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation return NULL; } -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) @synchronized(self) { self.originalCalled = YES; } -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) // Casting IMP to SentrySwizzleOriginalIMP to force user casting. return (SentrySwizzleOriginalIMP)_impProviderBlock(); diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 5145392c2e7..50d2ecaf491 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -50,10 +50,10 @@ # import #endif // SENTRY_HAS_UIKIT -#if defined(TEST) || defined(TESTCI) +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) # import "SentryFileManager+Test.h" # import "SentryInternalDefines.h" -#endif // defined(TEST) || defined(TESTCI) +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/SentryTransportFactory.m b/Sources/Sentry/SentryTransportFactory.m index 3ee49f8e8e5..4f7a03b047b 100644 --- a/Sources/Sentry/SentryTransportFactory.m +++ b/Sources/Sentry/SentryTransportFactory.m @@ -28,11 +28,18 @@ @implementation SentryTransportFactory sentryFileManager:(SentryFileManager *)sentryFileManager currentDateProvider:(SentryCurrentDateProvider *)currentDateProvider { - NSURLSessionConfiguration *configuration = - [NSURLSessionConfiguration ephemeralSessionConfiguration]; - NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration - delegate:options.urlSessionDelegate - delegateQueue:nil]; + NSURLSession *session; + + if (options.urlSession) { + session = options.urlSession; + } else { + NSURLSessionConfiguration *configuration = + [NSURLSessionConfiguration ephemeralSessionConfiguration]; + session = [NSURLSession sessionWithConfiguration:configuration + delegate:options.urlSessionDelegate + delegateQueue:nil]; + } + id requestManager = [[SentryQueueableRequestManager alloc] initWithSession:session]; diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index d0d8f5a7cb0..6a824167417 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -159,12 +159,12 @@ typedef void (*SentrySwizzleOriginalIMP)(void /* id, SEL, ... */); */ @property (nonatomic, readonly) SEL selector; -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * A flag to check whether the original implementation was called. */ @property (nonatomic) BOOL originalCalled; -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end @@ -367,7 +367,7 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { // and remove it later. #define _SentrySWArguments(arguments...) DEL, ##arguments -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) # define _SentrySWReplacement(code...) \ @try { \ code \ @@ -379,7 +379,7 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { } #else # define _SentrySWReplacement(code...) code -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) #define _SentrySwizzleInstanceMethod(classToSwizzle, selector, SentrySWReturnType, \ SentrySWArguments, SentrySWReplacement, SentrySwizzleMode, KEY) \ diff --git a/Sources/Sentry/include/SentryCoreGraphicsHelper.h b/Sources/Sentry/include/SentryCoreGraphicsHelper.h deleted file mode 100644 index e561984de1b..00000000000 --- a/Sources/Sentry/include/SentryCoreGraphicsHelper.h +++ /dev/null @@ -1,13 +0,0 @@ -#import "SentryDefines.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - -@interface SentryCoreGraphicsHelper : NSObject -+ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path; -@end - -#endif -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryNSProcessInfoWrapper.h b/Sources/Sentry/include/SentryNSProcessInfoWrapper.h index ab3be20a061..5bd38f6fc3d 100644 --- a/Sources/Sentry/include/SentryNSProcessInfoWrapper.h +++ b/Sources/Sentry/include/SentryNSProcessInfoWrapper.h @@ -8,9 +8,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, nonatomic, readonly) NSString *processPath; @property (readonly) NSUInteger processorCount; -#if TEST +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setProcessPath:(NSString *)path; -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end diff --git a/Sources/Sentry/include/SentryNetworkTracker.h b/Sources/Sentry/include/SentryNetworkTracker.h index 237e55dbd43..e0aa040da81 100644 --- a/Sources/Sentry/include/SentryNetworkTracker.h +++ b/Sources/Sentry/include/SentryNetworkTracker.h @@ -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 diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 5d66a9971e1..c2507583c5a 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -11,4 +11,3 @@ // Headers that also import SentryDefines should be at the end of this list // otherwise it wont compile -#import "SentryCoreGraphicsHelper.h" diff --git a/Sources/Sentry/include/SentryReachability.h b/Sources/Sentry/include/SentryReachability.h index a93c20fd8b4..585d511d7a5 100644 --- a/Sources/Sentry/include/SentryReachability.h +++ b/Sources/Sentry/include/SentryReachability.h @@ -34,13 +34,13 @@ NS_ASSUME_NONNULL_BEGIN void SentryConnectivityCallback(SCNetworkReachabilityFlags flags); -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Needed for testing. */ void SentrySetReachabilityIgnoreActualCallback(BOOL value); -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) NSString *SentryConnectivityFlagRepresentation(SCNetworkReachabilityFlags flags); @@ -68,7 +68,7 @@ SENTRY_EXTERN NSString *const SentryConnectivityNone; */ @interface SentryReachability : NSObject -# if TEST || TESTCI +# if defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Only needed for testing. Use this flag to skip registering and unregistering the actual callbacks @@ -76,7 +76,7 @@ SENTRY_EXTERN NSString *const SentryConnectivityNone; */ @property (nonatomic, assign) BOOL skipRegisteringActualCallbacks; -# endif // TEST || TESTCI +# endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) /** * Add an observer which is called each time network connectivity changes. diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 1d8293ccaad..123f1252325 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -13,27 +13,11 @@ @protocol SentryRandom; @protocol SentryRedactOptions; +@protocol SentryViewScreenshotProvider; +@protocol SentryReplayVideoMaker; NS_ASSUME_NONNULL_BEGIN -@protocol SentryReplayMaker - -- (void)addFrameAsyncWithImage:(UIImage *)image; -- (void)releaseFramesUntil:(NSDate *)date; -- (BOOL)createVideoWithDuration:(NSTimeInterval)duration - beginning:(NSDate *)beginning - outputFileURL:(NSURL *)outputFileURL - error:(NSError *_Nullable *_Nullable)error - completion: - (void (^)(SentryVideoInfo *_Nullable, NSError *_Nullable))completion; - -@end - -@protocol SentryViewScreenshotProvider -- (UIImage *)imageWithView:(UIView *)view options:(id)options; -@end - -API_AVAILABLE(ios(16.0), tvos(16.0)) @interface SentrySessionReplay : NSObject @property (nonatomic, strong, readonly) SentryId *sessionReplayId; @@ -41,7 +25,7 @@ API_AVAILABLE(ios(16.0), tvos(16.0)) - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions replayFolderPath:(NSURL *)folderPath screenshotProvider:(id)photographer - replayMaker:(id)replayMaker + replayMaker:(id)replayMaker dateProvider:(SentryCurrentDateProvider *)dateProvider random:(id)random displayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper; diff --git a/Sources/Sentry/include/SentryTransport.h b/Sources/Sentry/include/SentryTransport.h index 76fbd1a813d..340562d4d15 100644 --- a/Sources/Sentry/include/SentryTransport.h +++ b/Sources/Sentry/include/SentryTransport.h @@ -21,9 +21,9 @@ NS_SWIFT_NAME(Transport) - (SentryFlushResult)flush:(NSTimeInterval)timeout; -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setStartFlushCallback:(void (^)(void))callback; -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c index b14aead36d9..f40c7327ada 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c @@ -31,10 +31,7 @@ static const struct { const SentryCrashMonitorType type; const char *const name; } g_monitorTypes[] = { -#define MONITORTYPE(NAME) \ - { \ - NAME, #NAME \ - } +#define MONITORTYPE(NAME) { NAME, #NAME } MONITORTYPE(SentryCrashMonitorTypeMachException), MONITORTYPE(SentryCrashMonitorTypeSignal), MONITORTYPE(SentryCrashMonitorTypeCPPException), diff --git a/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c b/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c index 9e412937151..62212e614dd 100644 --- a/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c +++ b/Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c @@ -7,7 +7,7 @@ #include #include -#if TEST || TESTCI +#if defined(TEST) || defined(TESTCI) || defined(DEBUG) typedef void (*SentryRegisterImageCallback)(const struct mach_header *mh, intptr_t vmaddr_slide); typedef void (*SentryRegisterFunction)(SentryRegisterImageCallback function); @@ -57,7 +57,7 @@ sentry_resetFuncForAddRemoveImage(void) # define sentry_dyld_register_func_for_remove_image(CALLBACK) \ _dyld_register_func_for_remove_image(CALLBACK) # define _will_add_image() -#endif +#endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) typedef struct SentryCrashBinaryImageNode { SentryCrashBinaryImage image; diff --git a/Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c b/Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c index 8d27ed32597..c52897917b1 100644 --- a/Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c +++ b/Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c @@ -42,10 +42,7 @@ typedef struct { const int numCodes; } SentryCrashSignalInfo; -#define ENUM_NAME_MAPPING(A) \ - { \ - A, #A \ - } +#define ENUM_NAME_MAPPING(A) { A, #A } static const SentryCrashSignalCodeInfo g_sigIllCodes[] = { #ifdef ILL_NOOP @@ -98,14 +95,8 @@ static const SentryCrashSignalCodeInfo g_sigSegVCodes[] = { ENUM_NAME_MAPPING(SEGV_ACCERR), }; -#define SIGNAL_INFO(SIGNAL, CODES) \ - { \ - SIGNAL, #SIGNAL, CODES, sizeof(CODES) / sizeof(*CODES) \ - } -#define SIGNAL_INFO_NOCODES(SIGNAL) \ - { \ - SIGNAL, #SIGNAL, 0, 0 \ - } +#define SIGNAL_INFO(SIGNAL, CODES) { SIGNAL, #SIGNAL, CODES, sizeof(CODES) / sizeof(*CODES) } +#define SIGNAL_INFO_NOCODES(SIGNAL) { SIGNAL, #SIGNAL, 0, 0 } static const SentryCrashSignalInfo g_fatalSignalData[] = { SIGNAL_INFO_NOCODES(SIGABRT), diff --git a/Sources/Swift/Extensions/URLSessionTaskExtensions.swift b/Sources/Swift/Extensions/URLSessionTaskExtensions.swift new file mode 100644 index 00000000000..acf7bda79f0 --- /dev/null +++ b/Sources/Swift/Extensions/URLSessionTaskExtensions.swift @@ -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 +} diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift index d56db06b02e..4b299e46fa5 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -22,7 +22,7 @@ enum SentryOnDemandReplayError: Error { } @objcMembers -class SentryOnDemandReplay: NSObject { +class SentryOnDemandReplay: NSObject, SentryReplayVideoMaker { private let _outputPath: String private var _currentPixelBuffer: SentryPixelBuffer? private var _totalFrames = 0 @@ -30,13 +30,13 @@ class SentryOnDemandReplay: NSObject { private let workingQueue: SentryDispatchQueueWrapper private var _frames = [SentryReplayFrame]() - #if TEST + #if TEST || TESTCI || DEBUG //This is exposed only for tests, no need to make it thread safe. var frames: [SentryReplayFrame] { get { _frames } set { _frames = newValue } } - #endif + #endif // TEST || TESTCI || DEBUG var videoWidth = 200 var videoHeight = 434 diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift new file mode 100644 index 00000000000..e10ca6bf3ef --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift @@ -0,0 +1,11 @@ +#if canImport(UIKit) +import Foundation +import UIKit + +@objc +protocol SentryReplayVideoMaker: NSObjectProtocol { + func addFrameAsync(image: UIImage) + func releaseFramesUntil(_ date: Date) + func createVideoWith(duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws +} +#endif diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index 3ae3f54d7de..d1089a801f2 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -1,123 +1,47 @@ #if canImport(UIKit) && !SENTRY_NO_UIKIT #if os(iOS) || os(tvOS) -@_implementationOnly import _SentryPrivate import CoreGraphics import Foundation import UIKit -@available(iOS, introduced: 16.0) -@available(tvOS, introduced: 16.0) @objcMembers -class SentryViewPhotographer: NSObject { - - //This is a list of UIView subclasses that will be ignored during redact process - private var ignoreClasses: [AnyClass] = [] - //This is a list of UIView subclasses that need to be redacted from screenshot - private var redactClasses: [AnyClass] = [] +class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { static let shared = SentryViewPhotographer() - override init() { -#if os(iOS) - ignoreClasses = [ UISlider.self, UISwitch.self ] -#endif // os(iOS) - redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] + [ - "_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", - "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", - "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer" - ].compactMap { NSClassFromString($0) } - } - - @objc(imageWithView:options:) - func image(view: UIView, options: SentryRedactOptions) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0) + //This is a list of UIView subclasses that will be ignored during redact process + private var redactBuilder = UIRedactBuilder() - defer { - UIGraphicsEndImageContext() + func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback ) { + let image = UIGraphicsImageRenderer(size: view.bounds.size).image { _ in + view.drawHierarchy(in: view.bounds, afterScreenUpdates: false) } - guard let currentContext = UIGraphicsGetCurrentContext() else { return nil } - - view.layer.render(in: currentContext) - self.mask(view: view, context: currentContext, options: options) - - guard let screenshot = UIGraphicsGetImageFromCurrentImageContext() else { return nil } - return screenshot + let redact = redactBuilder.redactRegionsFor(view: view, options: options) + let imageSize = view.bounds.size + DispatchQueue.global().async { + let screenshot = UIGraphicsImageRenderer(size: imageSize, format: .init(for: .init(displayScale: 1))).image { context in + context.cgContext.interpolationQuality = .none + image.draw(at: .zero) + + for region in redact { + (region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: region.rect)).setFill() + context.fill(region.rect) + } + } + onComplete(screenshot) + } } - + @objc(addIgnoreClasses:) func addIgnoreClasses(classes: [AnyClass]) { - ignoreClasses += classes + redactBuilder.ignoreClasses += classes } @objc(addRedactClasses:) func addRedactClasses(classes: [AnyClass]) { - redactClasses += classes - } - - private func mask(view: UIView, context: CGContext, options: SentryRedactOptions?) { - UIColor.black.setFill() - let maskPath = self.buildPath(view: view, - path: CGMutablePath(), - area: view.frame, - redactText: options?.redactAllText ?? true, - redactImage: options?.redactAllImages ?? true) - context.addPath(maskPath) - context.fillPath() - } - - private func shouldIgnore(view: UIView) -> Bool { - ignoreClasses.contains { view.isKind(of: $0) } - } - - private func shouldRedact(view: UIView) -> Bool { - return redactClasses.contains { view.isKind(of: $0) } - } - - private func shouldRedact(imageView: UIImageView) -> Bool { - // Checking the size is to avoid redact gradient backgroud that - // are usually small lines repeating - guard let image = imageView.image, image.size.width > 10 && image.size.height > 10 else { return false } - return image.imageAsset?.value(forKey: "_containingBundle") == nil - } - - private func buildPath(view: UIView, path: CGMutablePath, area: CGRect, redactText: Bool, redactImage: Bool) -> CGMutablePath { - let rectInWindow = view.convert(view.bounds, to: nil) - - if (!redactImage && !redactText) || !area.intersects(rectInWindow) || view.isHidden || view.alpha == 0 { - return path - } - - var result = path - - let ignore = shouldIgnore(view: view) - - let redact: Bool = { - if redactImage, let imageView = view as? UIImageView { - return shouldRedact(imageView: imageView) - } - return redactText && shouldRedact(view: view) - }() - - if !ignore && redact { - result.addRect(rectInWindow) - return result - } else if isOpaqueOrHasBackground(view) { - result = SentryCoreGraphicsHelper.excludeRect(rectInWindow, from: result).takeRetainedValue() - } - - if !ignore { - for subview in view.subviews { - result = buildPath(view: subview, path: path, area: area, redactText: redactText, redactImage: redactImage) - } - } - - return result - } - - private func isOpaqueOrHasBackground(_ view: UIView) -> Bool { - return view.isOpaque || (view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9) + redactBuilder.redactClasses += classes } } diff --git a/Sources/Swift/Tools/SentryViewScreenshotProvider.swift b/Sources/Swift/Tools/SentryViewScreenshotProvider.swift new file mode 100644 index 00000000000..7fc012deeb5 --- /dev/null +++ b/Sources/Swift/Tools/SentryViewScreenshotProvider.swift @@ -0,0 +1,13 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) +import Foundation +import UIKit + +typealias ScreenshotCallback = (UIImage) -> Void + +@objc +protocol SentryViewScreenshotProvider: NSObjectProtocol { + func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback) +} +#endif +#endif diff --git a/Sources/Swift/Tools/UIImageHelper.swift b/Sources/Swift/Tools/UIImageHelper.swift new file mode 100644 index 00000000000..f53a95995aa --- /dev/null +++ b/Sources/Swift/Tools/UIImageHelper.swift @@ -0,0 +1,36 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + +import Foundation +import UIKit + +final class UIImageHelper { + private init() { } + + static func averageColor(of image: UIImage, at region: CGRect) -> UIColor { + let scaledRegion = region.applying(CGAffineTransform(scaleX: image.scale, y: image.scale)) + guard let croppedImage = image.cgImage?.cropping(to: scaledRegion), let colorSpace = croppedImage.colorSpace else { + return .black + } + + let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue + + guard let context = CGContext(data: nil, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return .black } + context.interpolationQuality = .high + context.draw(croppedImage, in: CGRect(x: 0, y: 0, width: 1, height: 1)) + guard let pixelBuffer = context.data else { return .black } + + let data = pixelBuffer.bindMemory(to: UInt8.self, capacity: 4) + + let blue = CGFloat(data[0]) / 255.0 + let green = CGFloat(data[1]) / 255.0 + let red = CGFloat(data[2]) / 255.0 + let alpha = CGFloat(data[3]) / 255.0 + + return UIColor(red: red, green: green, blue: blue, alpha: alpha) + } + +} + +#endif +#endif diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift new file mode 100644 index 00000000000..bb9c5a4b4eb --- /dev/null +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -0,0 +1,139 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) +import Foundation +import UIKit + +struct RedactRegion { + let rect: CGRect + let color: UIColor? + + init(rect: CGRect, color: UIColor? = nil) { + self.rect = rect + self.color = color + } + + func splitBySubtracting(region: CGRect) -> [RedactRegion] { + guard rect.intersects(region) else { return [self] } + guard !region.contains(rect) else { return [] } + + let intersectionRect = rect.intersection(region) + var resultRegions: [CGRect] = [] + + // Calculate the top region. + resultRegions.append(CGRect(x: rect.minX, + y: rect.minY, + width: rect.width, + height: intersectionRect.minY - rect.minY)) + + // Calculate the bottom region. + resultRegions.append(CGRect(x: rect.minX, + y: intersectionRect.maxY, + width: rect.width, + height: rect.maxY - intersectionRect.maxY)) + + // Calculate the left region. + resultRegions.append(CGRect(x: rect.minX, + y: max(rect.minY, intersectionRect.minY), + width: intersectionRect.minX - rect.minX, + height: min(intersectionRect.maxY, rect.maxY) - max(rect.minY, intersectionRect.minY))) + + // Calculate the right region. + resultRegions.append(CGRect(x: intersectionRect.maxX, + y: max(rect.minY, intersectionRect.minY), + width: rect.maxX - intersectionRect.maxX, + height: min(intersectionRect.maxY, rect.maxY) - max(rect.minY, intersectionRect.minY))) + + return resultRegions.filter { !$0.isEmpty }.map { RedactRegion(rect: $0, color: color) } + } +} + +class UIRedactBuilder { + + //This is a list of UIView subclasses that will be ignored during redact process + var ignoreClasses: [AnyClass] + //This is a list of UIView subclasses that need to be redacted from screenshot + var redactClasses: [AnyClass] + + init() { + + redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] + + //this classes are used by SwiftUI to display images. + ["_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", + "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", + "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer" + ].compactMap { NSClassFromString($0) } +#if os(iOS) + ignoreClasses = [ UISlider.self, UISwitch.self ] +#else + ignoreClasses = [] +#endif + } + + func redactRegionsFor(view: UIView, options: SentryRedactOptions?) -> [RedactRegion] { + var redactingRegions = [RedactRegion]() + + self.mapRedactRegion(fromView: view, + to: view, + redacting: &redactingRegions, + area: view.frame, + redactText: options?.redactAllText ?? true, + redactImage: options?.redactAllImages ?? true) + + return redactingRegions + } + + private func shouldIgnore(view: UIView) -> Bool { + ignoreClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(view: UIView, redactText: Bool, redactImage: Bool) -> Bool { + if redactImage, let imageView = view as? UIImageView { + return shouldRedact(imageView: imageView) + } + return redactText && redactClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(imageView: UIImageView) -> Bool { + // Checking the size is to avoid redact gradient background that + // are usually small lines repeating + guard let image = imageView.image, image.size.width > 10 && image.size.height > 10 else { return false } + return image.imageAsset?.value(forKey: "_containingBundle") == nil + } + + private func mapRedactRegion(fromView view: UIView, to: UIView, redacting: inout [RedactRegion], area: CGRect, redactText: Bool, redactImage: Bool) { + let rectInWindow = view.convert(view.bounds, to: to) + guard (redactImage || redactText) && area.intersects(rectInWindow) && !view.isHidden && view.alpha != 0 else { return } + + let ignore = shouldIgnore(view: view) + let redact = shouldRedact(view: view, redactText: redactText, redactImage: redactImage) + + if !ignore && redact { + redacting.append(RedactRegion(rect: rectInWindow, color: self.color(for: view))) + return + } else if hasBackground(view) { + if rectInWindow == area { + redacting.removeAll() + } else { + redacting = redacting.flatMap { $0.splitBySubtracting(region: rectInWindow) } + } + } + + if !ignore { + for subview in view.subviews { + mapRedactRegion(fromView: subview, to: to, redacting: &redacting, area: area, redactText: redactText, redactImage: redactImage) + } + } + } + + private func color(for view: UIView) -> UIColor? { + return (view as? UILabel)?.textColor + } + + private func hasBackground(_ view: UIView) -> Bool { + //Anything with an alpha greater than 0.9 is opaque enough that it's impossible to see anything behind it. + return view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9 + } +} + +#endif +#endif diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift index 11a974dacc9..5a9cff7c03b 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift @@ -156,20 +156,6 @@ class SentryFramesTrackerTests: XCTestCase { try assert(slow: 1, frozen: 1, total: 2, frameRates: 2) } - - func testPerformanceOfTrackingFrames() throws { - let sut = fixture.sut - sut.start() - - let frames: UInt = 1_000 - self.measure { - for _ in 0 ..< frames { - fixture.displayLinkWrapper.normalFrame() - } - } - - try assert(slow: 0, frozen: 0) - } /** * The following test validates one slow and one frozen frame in the time interval. The slow frame starts at diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift index bff5d482fa7..3b8f0e0b3c8 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift @@ -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") diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift index 4c75ec7c8f5..96cbc56aaa5 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift @@ -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", @@ -48,6 +48,7 @@ class SentryNetworkTrackerTests: XCTestCase { result.enableNetworkTracking() result.enableNetworkBreadcrumbs() result.enableCaptureFailedRequests() + result.enableGraphQLOperationTracking() return result } } @@ -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() @@ -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) } @@ -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) } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 03d00728f2d..1cccec763b2 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -8,10 +8,13 @@ import XCTest class SentrySessionReplayTests: XCTestCase { private class ScreenshotProvider: NSObject, SentryViewScreenshotProvider { - func image(with view: UIView, options: SentryRedactOptions) -> UIImage { UIImage.add } + func image(view: UIView, options: Sentry.SentryRedactOptions, onComplete: @escaping Sentry.ScreenshotCallback) { + onComplete(UIImage.add) + } } - private class TestReplayMaker: NSObject, SentryReplayMaker { + private class TestReplayMaker: NSObject, SentryReplayVideoMaker { + struct CreateVideoCall { var duration: TimeInterval var beginning: Date @@ -20,8 +23,8 @@ class SentrySessionReplayTests: XCTestCase { } var lastCallToCreateVideo: CreateVideoCall? - func createVideo(withDuration duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { - lastCallToCreateVideo = CreateVideoCall(duration: duration, + func createVideoWith(duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (Sentry.SentryVideoInfo?, (Error)?) -> Void) throws { + lastCallToCreateVideo = CreateVideoCall(duration: duration, beginning: beginning, outputFileURL: outputFileURL, completion: completion) @@ -34,12 +37,12 @@ class SentrySessionReplayTests: XCTestCase { } var lastFrame: UIImage? - func addFrameAsync(with image: UIImage) { + func addFrameAsync(image: UIImage) { lastFrame = image } var lastReleaseUntil: Date? - func releaseFrames(until date: Date) { + func releaseFramesUntil(_ date: Date) { lastReleaseUntil = date } } @@ -56,7 +59,6 @@ class SentrySessionReplayTests: XCTestCase { } } - @available(iOS 16.0, tvOS 16.0, *) private class Fixture { let dateProvider = TestCurrentDateProvider() let random = TestRandom(value: 0) @@ -71,19 +73,13 @@ class SentrySessionReplayTests: XCTestCase { return SentrySessionReplay(settings: options, replayFolderPath: cacheFolder, screenshotProvider: screenshotProvider, - replayMaker: replayMaker, + replay: replayMaker, dateProvider: dateProvider, random: random, displayLinkWrapper: displayLink) } } - override func setUpWithError() throws { - guard #available(iOS 16.0, tvOS 16.0, *) else { - throw XCTSkip("iOS version not supported") - } - } - override func setUp() { super.setUp() } @@ -93,14 +89,12 @@ class SentrySessionReplayTests: XCTestCase { clearTestState() } - @available(iOS 16.0, tvOS 16, *) private func startFixture() -> Fixture { let fixture = Fixture() SentrySDK.setCurrentHub(fixture.hub) return fixture } - @available(iOS 16.0, tvOS 16, *) func testDontSentReplay_NoFullSession() { let fixture = startFixture() let sut = fixture.getSut() @@ -114,7 +108,6 @@ class SentrySessionReplayTests: XCTestCase { expect(fixture.hub.lastEvent) == nil } - @available(iOS 16.0, tvOS 16, *) func testSentReplay_FullSession() { let fixture = startFixture() @@ -144,7 +137,6 @@ class SentrySessionReplayTests: XCTestCase { assertFullSession(sut, expected: true) } - @available(iOS 16.0, tvOS 16, *) func testDontSentReplay_NotFullSession() { let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) @@ -164,7 +156,6 @@ class SentrySessionReplayTests: XCTestCase { assertFullSession(sut, expected: false) } - @available(iOS 16.0, tvOS 16, *) func testChangeReplayMode_forErrorEvent() { let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) @@ -178,7 +169,6 @@ class SentrySessionReplayTests: XCTestCase { assertFullSession(sut, expected: true) } - @available(iOS 16.0, tvOS 16, *) func testDontChangeReplayMode_forNonErrorEvent() { let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) @@ -218,8 +208,18 @@ class SentrySessionReplayTests: XCTestCase { expect(Dynamic(sut).isRunning) == false } - + @available(iOS 16.0, tvOS 16, *) + func testDealloc_CallsStop() { + let fixture = startFixture() + func sutIsDeallocatedAfterCallingMe() { + _ = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + } + sutIsDeallocatedAfterCallingMe() + + expect(fixture.displayLink.invalidateInvocations.count) == 1 + } + func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { expect(Dynamic(sessionReplay).isFullSession) == expected } diff --git a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift index 291692a16f0..93a224bb114 100644 --- a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift +++ b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift @@ -1,3 +1,4 @@ +import Nimble @testable import Sentry import SentryTestUtils import XCTest @@ -557,42 +558,27 @@ class SentryHttpTransportTests: XCTestCase { XCTAssertEqual(1, attachment?.quantity) } - func testPerformanceOfSending() { - self.measure { - givenNoInternetConnection() - for _ in 0...5 { - sendEventAsync() - } - givenOkResponse() - for _ in 0...5 { - sendEventAsync() - } - } - } - func testSendEnvelopesConcurrent() { - self.measure { - fixture.requestManager.responseDelay = 0.0001 - - let queue = fixture.queue - - let group = DispatchGroup() - for _ in 0...20 { - group.enter() - queue.async { - self.givenRecordedLostEvents() - self.sendEventAsync() - group.leave() - } - } + fixture.requestManager.responseDelay = 0.0001 - queue.activate() - group.waitWithTimeout() + let queue = fixture.queue - waitForAllRequests() + let group = DispatchGroup() + for _ in 0...20 { + group.enter() + queue.async { + self.givenRecordedLostEvents() + self.sendEventAsync() + group.leave() + } } - XCTAssertEqual(210, fixture.requestManager.requests.count) + queue.activate() + group.waitWithTimeout() + + waitForAllRequests() + + expect(self.fixture.requestManager.requests.count) == 21 } func testBuildingRequestFails_DeletesEnvelopeAndSendsNext() { diff --git a/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift b/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift index 7a2b9436708..d7cbee9652e 100644 --- a/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift +++ b/Tests/SentryTests/Networking/SentryTransportFactoryTests.swift @@ -31,6 +31,33 @@ class SentryTransportFactoryTests: XCTestCase { wait(for: [expect], timeout: 10) } + func testShouldReturnTransports_WhenURLSessionPassed() throws { + + let urlSessionDelegateSpy = UrlSessionDelegateSpy() + let expect = expectation(description: "UrlSession Delegate of Options called in RequestManager") + + let sessionConfiguration = URLSession(configuration: .ephemeral, delegate: urlSessionDelegateSpy, delegateQueue: nil) + urlSessionDelegateSpy.delegateCallback = { + expect.fulfill() + } + + let options = Options() + options.urlSession = sessionConfiguration + + let fileManager = try! SentryFileManager(options: options, dispatchQueueWrapper: TestSentryDispatchQueueWrapper()) + let transports = TransportInitializer.initTransports(options, sentryFileManager: fileManager, currentDateProvider: TestCurrentDateProvider()) + + let httpTransport = transports.first + let requestManager = Dynamic(httpTransport).requestManager.asObject as! SentryQueueableRequestManager + + let imgUrl = URL(string: "https://github.com")! + let request = URLRequest(url: imgUrl) + + requestManager.add(request) { _, _ in /* We don't care about the result */ } + wait(for: [expect], timeout: 10) + + } + func testShouldReturnTwoTransports_WhenSpotlightEnabled() throws { let options = Options() options.enableSpotlight = true diff --git a/Tests/SentryTests/RedactRegionTests.swift b/Tests/SentryTests/RedactRegionTests.swift new file mode 100644 index 00000000000..31919e18008 --- /dev/null +++ b/Tests/SentryTests/RedactRegionTests.swift @@ -0,0 +1,143 @@ +import Foundation +import Nimble +@testable import Sentry +import XCTest +#if os(iOS) || os(tvOS) +class RedactRegionTests: XCTestCase { + + func testSplitBySubtractingBottom() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 100, height: 50)) + + expect(result.count) == 1 + expect(result.first?.rect) == CGRect(x: 0, y: 0, width: 100, height: 50) + expect(result.first?.color) == .red + } + + func testSplitBySubtractingTop() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 100, height: 50)) + + expect(result.count) == 1 + expect(result.first?.rect) == CGRect(x: 0, y: 50, width: 100, height: 50) + } + + func testSplitBySubtractingTopRight() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 50, y: 0, width: 50, height: 50)) + + expect(result.count) == 2 + expect(result.first?.rect) == CGRect(x: 0, y: 50, width: 100, height: 50) + expect(result[1].rect) == CGRect(x: 0, y: 0, width: 50, height: 50) + } + + func testSplitBySubtractingBottomLeft() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 50, height: 50)) + + expect(result.count) == 2 + expect(result.first?.rect) == CGRect(x: 0, y: 0, width: 100, height: 50) + expect(result[1].rect) == CGRect(x: 50, y: 50, width: 50, height: 50) + } + + func testSplitBySubtractingMiddle() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 25, width: 50, height: 50)) + + expect(result.count) == 4 + expect(result[0].rect) == CGRect(x: 0, y: 0, width: 100, height: 25) + expect(result[1].rect) == CGRect(x: 0, y: 75, width: 100, height: 25) + expect(result[2].rect) == CGRect(x: 0, y: 25, width: 25, height: 50) + expect(result[3].rect) == CGRect(x: 75, y: 25, width: 25, height: 50) + } + + func testSplitBySubtractingInHalfHorizontally() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 25, width: 100, height: 50)) + + expect(result.count) == 2 + expect(result[0].rect) == CGRect(x: 0, y: 0, width: 100, height: 25) + expect(result[1].rect) == CGRect(x: 0, y: 75, width: 100, height: 25) + } + + func testSplitBySubtractingInHalfVertically() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 0, width: 50, height: 100)) + + expect(result.count) == 2 + expect(result[0].rect) == CGRect(x: 0, y: 0, width: 25, height: 100) + expect(result[1].rect) == CGRect(x: 75, y: 0, width: 25, height: 100) + } + + func testSplitBySubtractingMiddleRight() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 25, width: 100, height: 50)) + + expect(result.count) == 3 + expect(result[0].rect) == CGRect(x: 0, y: 0, width: 100, height: 25) + expect(result[1].rect) == CGRect(x: 0, y: 75, width: 100, height: 25) + expect(result[2].rect) == CGRect(x: 0, y: 25, width: 25, height: 50) + } + + func testSplitBySubtractingMiddleLeft() { + let sut = RedactRegion(rect: CGRect(x: 50, y: 0, width: 100, height: 100)) + + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 25, width: 100, height: 50)) + + expect(result.count) == 3 + expect(result[0].rect) == CGRect(x: 50, y: 0, width: 100, height: 25) + expect(result[1].rect) == CGRect(x: 50, y: 75, width: 100, height: 25) + expect(result[2].rect) == CGRect(x: 100, y: 25, width: 50, height: 50) + } + + func testSplitBySubtracting_TopIsWider() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 150, height: 50)) + + expect(result.count) == 1 + expect(result.first?.rect) == CGRect(x: 0, y: 50, width: 100, height: 50) + expect(result.first?.color) == .red + } + + func testSplitBySubtracting_BottomIsWider() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 150, height: 50)) + + expect(result.count) == 1 + expect(result.first?.rect) == CGRect(x: 0, y: 0, width: 100, height: 50) + expect(result.first?.color) == .red + } + + func testNoResultForEqualRegion() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 100, height: 100)) + + expect(result.count) == 0 + } + + func testNoResultForLargerRegion() { + let sut = RedactRegion(rect: CGRect(x: 50, y: 50, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 200, height: 200)) + + expect(result.count) == 0 + } + + func testSameRegionForOutsideOfBounds() { + let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) + let result = sut.splitBySubtracting(region: CGRect(x: 110, y: 110, width: 200, height: 200)) + + expect(result.count) == 1 + expect(result.first?.rect) == sut.rect + expect(result.first?.color) == .red + } + +} +#endif diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 8e345e638ff..d50996eb8dc 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -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]; @@ -518,6 +523,7 @@ - (void)testEmptyConstructorSetsDefaultValues - (void)testNSNull_SetsDefaultValue { SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ + @"urlSession" : [NSNull null], @"dsn" : [NSNull null], @"enabled" : [NSNull null], @"debug" : [NSNull null], @@ -617,6 +623,7 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqualObjects([self getDefaultInAppIncludes], options.inAppIncludes); XCTAssertEqual(@[], options.inAppExcludes); XCTAssertNil(options.urlSessionDelegate); + XCTAssertNil(options.urlSession); XCTAssertEqual(YES, options.enableSwizzling); XCTAssertEqual([NSSet new], options.swizzleClassNameExcludes); XCTAssertEqual(YES, options.enableFileIOTracing); @@ -1261,6 +1268,16 @@ - (SentryOptions *)getValidOptions:(NSDictionary *)dict return sentryOptions; } +- (void)testURLSession +{ + NSURLSession *urlSession = [NSURLSession + sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; + + SentryOptions *options = [self getValidOptions:@{ @"urlSession" : urlSession }]; + + XCTAssertNotNil(options.urlSession); +} + - (void)testUrlSessionDelegate { id urlSessionDelegate = [[UrlSessionDelegateSpy alloc] init]; diff --git a/Tests/SentryTests/SentryScopeSwiftTests.swift b/Tests/SentryTests/SentryScopeSwiftTests.swift index d623c38a56a..a553916ed09 100644 --- a/Tests/SentryTests/SentryScopeSwiftTests.swift +++ b/Tests/SentryTests/SentryScopeSwiftTests.swift @@ -408,31 +408,6 @@ class SentryScopeSwiftTests: XCTestCase { XCTAssertEqual(0, scope.attachments.count) } - func testPeformanceOfSyncToSentryCrash() { - // To avoid spamming the test logs - SentryLog.configure(true, diagnosticLevel: .error) - - let scope = fixture.scope - scope.add(SentryCrashScopeObserver(maxBreadcrumbs: 100)) - - self.measure { - modifyScope(scope: scope) - } - - setTestDefaultLogLevel() - } - - func testPeformanceOfSyncToSentryCrash_OneCrumb() { - let scope = fixture.scope - scope.add(SentryCrashScopeObserver(maxBreadcrumbs: 100)) - - modifyScope(scope: scope) - - self.measure { - scope.addBreadcrumb(self.fixture.breadcrumb) - } - } - // With this test we test if modifications from multiple threads don't lead to a crash. func testModifyingFromMultipleThreads() { let scope = fixture.scope diff --git a/Tests/SentryTests/Swift/Extensions/URLSessionTaskTests.swift b/Tests/SentryTests/Swift/Extensions/URLSessionTaskTests.swift new file mode 100644 index 00000000000..bc97e3ab648 --- /dev/null +++ b/Tests/SentryTests/Swift/Extensions/URLSessionTaskTests.swift @@ -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) + } + +} diff --git a/Tests/SentryTests/UIImageHelperTests.swift b/Tests/SentryTests/UIImageHelperTests.swift new file mode 100644 index 00000000000..608d98df436 --- /dev/null +++ b/Tests/SentryTests/UIImageHelperTests.swift @@ -0,0 +1,65 @@ +#if canImport(UIKit) +import Foundation +import Nimble +@testable import Sentry +import XCTest + +class UIImageHelperTests: XCTestCase { + + private let testFrame = CGRect(x: 0, y: 0, width: 100, height: 100) + + func testAverageColorRed() { + let begin = Date() + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.red.setFill() + context.fill(testFrame) + } + + expect(UIImageHelper.averageColor(of: image, at: self.testFrame)) == .red + + let end = Date() + print("Duration = \(end.timeIntervalSince(begin))") + } + + func testAverageColorGreen() { + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.green.setFill() + context.fill(testFrame) + } + + expect(UIImageHelper.averageColor(of: image, at: self.testFrame)) == .green + } + + func testAverageColorBlue() { + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.blue.setFill() + context.fill(testFrame) + } + + expect(UIImageHelper.averageColor(of: image, at: self.testFrame)) == .blue + } + + func testAverageColorYellow() { + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.yellow.setFill() + context.fill(testFrame) + } + + expect(UIImageHelper.averageColor(of: image, at: self.testFrame)) == .yellow + } + + func testGreenAreaInARedImage() { + let focusArea = CGRect(x: 25, y: 25, width: 50, height: 50) + + let image = UIGraphicsImageRenderer(size: testFrame.size).image { context in + UIColor.red.setFill() + context.fill(testFrame) + UIColor.green.setFill() + context.fill(focusArea) + } + + expect(UIImageHelper.averageColor(of: image, at: focusArea)) == .green + } +} + +#endif diff --git a/Tests/SentryTests/UIRedactBuilderTests.swift b/Tests/SentryTests/UIRedactBuilderTests.swift new file mode 100644 index 00000000000..f6adf66a20d --- /dev/null +++ b/Tests/SentryTests/UIRedactBuilderTests.swift @@ -0,0 +1,162 @@ +#if os(iOS) +import Foundation +import Nimble +@testable import Sentry +import UIKit +import XCTest + +class UIRedactBuilderTests: XCTestCase { + + private class RedactOptions: SentryRedactOptions { + var redactAllText: Bool + var redactAllImages: Bool + + init(redactAllText: Bool = true, redactAllImages: Bool = true) { + self.redactAllText = redactAllText + self.redactAllImages = redactAllImages + } + } + + private let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + func testNoNeedForRedact() { + let sut = UIRedactBuilder() + rootView.addSubview(UIView(frame: CGRect(x: 20, y: 20, width: 40, height: 40))) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 0 + } + + func testRedactALabel() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + label.textColor = .purple + rootView.addSubview(label) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 1 + expect(result.first?.color) == .purple + expect(result.first?.rect) == CGRect(x: 20, y: 20, width: 40, height: 40) + } + + func testDontRedactALabelOptionDisabled() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + label.textColor = .purple + rootView.addSubview(label) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions(redactAllText: false)) + + expect(result.count) == 0 + } + + func testRedactAImage() { + let sut = UIRedactBuilder() + + let image = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40)).image { context in + context.fill(CGRect(x: 0, y: 0, width: 40, height: 40)) + } + + let imageView = UIImageView(image: image) + imageView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) + rootView.addSubview(imageView) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 1 + expect(result.first?.color) == nil + expect(result.first?.rect) == CGRect(x: 20, y: 20, width: 40, height: 40) + } + + func testDontRedactAImageOptionDisabled() { + let sut = UIRedactBuilder() + + let image = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40)).image { context in + context.fill(CGRect(x: 0, y: 0, width: 40, height: 40)) + } + + let imageView = UIImageView(image: image) + imageView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) + rootView.addSubview(imageView) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions(redactAllImages: false)) + + expect(result.count) == 0 + } + + func testDontRedactABundleImage() { + //The check for bundled image only works for iOS 16 and above + //For others versions all images will be redacted + guard #available(iOS 16, *) else { return } + let sut = UIRedactBuilder() + + let imageView = UIImageView(image: .add) + imageView.frame = CGRect(x: 20, y: 20, width: 40, height: 40) + rootView.addSubview(imageView) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 0 + } + + func testDontRedactAHiddenView() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + label.isHidden = true + rootView.addSubview(label) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 0 + } + + func testDontRedactATransparentView() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + label.alpha = 0 + rootView.addSubview(label) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + + expect(result.count) == 0 + } + + func testDontRedactALabelBehindAOpaqueView() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + rootView.addSubview(label) + let topView = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60)) + topView.backgroundColor = .white + rootView.addSubview(topView) + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + expect(result.count) == 0 + } + + func testRedactALabelBehindATransparentView() { + let sut = UIRedactBuilder() + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + rootView.addSubview(label) + let topView = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60)) + topView.backgroundColor = .clear + rootView.addSubview(topView) + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + expect(result.count) == 1 + } + + func testIgnoreClasses() { + class AnotherLabel: UILabel { + } + + let sut = UIRedactBuilder() + sut.ignoreClasses.append(AnotherLabel.self) + rootView.addSubview(AnotherLabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40))) + + let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) + expect(result.count) == 0 + } + +} + +#endif diff --git a/scripts/create-carthage-json.sh b/scripts/create-carthage-json.sh index 25430ea9f8c..1d45034ba6d 100755 --- a/scripts/create-carthage-json.sh +++ b/scripts/create-carthage-json.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euo pipefail -echo "{ \"1.0\": \"file:///$(pwd)/Sentry.framework.zip?alt=file:///$(pwd)/Sentry.xcframework.zip\" }" > ./Samples/Carthage-Validation/Sentry.Carthage.json -echo "{ \"1.0\": \"file:///$(pwd)/SentrySwiftUI.framework.zip?alt=file:///$(pwd)/SentrySwiftUI.xcframework.zip\" }" > ./Samples/Carthage-Validation/SentrySwiftUI.Carthage.json +echo "{ \"1.0\": \"file:///$(pwd)/Carthage/Sentry.framework.zip?alt=file:///$(pwd)/Carthage/Sentry.xcframework.zip\" }" > ./Samples/Carthage-Validation/Sentry.Carthage.json +echo "{ \"1.0\": \"file:///$(pwd)/Carthage/SentrySwiftUI.framework.zip?alt=file:///$(pwd)/Carthage/SentrySwiftUI.xcframework.zip\" }" > ./Samples/Carthage-Validation/SentrySwiftUI.Carthage.json