From 913fab76543f3893b2e1f620546aa2e0636c3102 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 8 Oct 2024 14:40:56 +0200 Subject: [PATCH 1/7] chore: Remove deprecated experimental MetricsAPI (#4406) After October 7th, the Metrics page and all adjacent functionality will be removed from Sentry and no longer be accessible. Any metrics sent after this date will not be accepted. Therefore, we can remove the metrics API entirely from the SDK. --- CHANGELOG.md | 6 + Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 1 - Sentry.xcodeproj/project.pbxproj | 105 ---- Sources/Sentry/Public/SentryOptions.h | 32 -- Sources/Sentry/SentryHub.m | 53 +-- Sources/Sentry/SentryOptions.m | 15 - Sources/Sentry/SentrySpan.m | 13 - Sources/Sentry/SentryStatsdClient.m | 51 -- Sources/Sentry/SentryTransaction.m | 12 +- Sources/Sentry/include/SentryPrivate.h | 1 - Sources/Sentry/include/SentrySpan.h | 3 - Sources/Sentry/include/SentryStatsdClient.h | 16 - .../Helper/SentryEnabledFeaturesBuilder.swift | 4 - .../Metrics/BucketsMetricsAggregator.swift | 241 ---------- Sources/Swift/Metrics/CounterMetric.swift | 20 - .../Swift/Metrics/DistributionMetric.swift | 22 - Sources/Swift/Metrics/EncodeMetrics.swift | 72 --- Sources/Swift/Metrics/GaugeMetric.swift | 34 -- .../Metrics/LocalMetricsAggregator.swift | 70 --- Sources/Swift/Metrics/Metric.swift | 31 -- Sources/Swift/Metrics/MetricsAggregator.swift | 48 -- Sources/Swift/Metrics/SentryMetricsAPI.swift | 148 ------ .../Swift/Metrics/SentryMetricsClient.swift | 19 - Sources/Swift/Metrics/SetMetric.swift | 22 - .../SentryEnabledFeaturesBuilderTests.swift | 2 - Tests/SentryTests/SentryHubTests.swift | 124 ----- Tests/SentryTests/SentryOptionsTest.m | 36 -- Tests/SentryTests/SentrySDKTests.swift | 41 -- .../BucketMetricsAggregatorTests.swift | 450 ------------------ .../Swift/Metrics/CounterMetricTests.swift | 34 -- .../Metrics/DistributionMetricTests.swift | 34 -- .../Swift/Metrics/EncodeMetricTests.swift | 117 ----- .../Swift/Metrics/GaugeMetricTests.swift | 44 -- .../Metrics/LocalMetricsAggregatorTests.swift | 110 ----- .../Swift/Metrics/SentryMetricsAPITests.swift | 247 ---------- .../Metrics/SentryMetricsClientTests.swift | 44 -- .../Swift/Metrics/SetMetricTests.swift | 48 -- .../Swift/Metrics/TestMetricsClient.swift | 22 - .../Transaction/SentrySpanTests.swift | 27 -- .../Transaction/SentryTransactionTests.swift | 19 - 40 files changed, 8 insertions(+), 2430 deletions(-) delete mode 100644 Sources/Sentry/SentryStatsdClient.m delete mode 100644 Sources/Sentry/include/SentryStatsdClient.h delete mode 100644 Sources/Swift/Metrics/BucketsMetricsAggregator.swift delete mode 100644 Sources/Swift/Metrics/CounterMetric.swift delete mode 100644 Sources/Swift/Metrics/DistributionMetric.swift delete mode 100644 Sources/Swift/Metrics/EncodeMetrics.swift delete mode 100644 Sources/Swift/Metrics/GaugeMetric.swift delete mode 100644 Sources/Swift/Metrics/LocalMetricsAggregator.swift delete mode 100644 Sources/Swift/Metrics/Metric.swift delete mode 100644 Sources/Swift/Metrics/MetricsAggregator.swift delete mode 100644 Sources/Swift/Metrics/SentryMetricsAPI.swift delete mode 100644 Sources/Swift/Metrics/SentryMetricsClient.swift delete mode 100644 Sources/Swift/Metrics/SetMetric.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/BucketMetricsAggregatorTests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/CounterMetricTests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/DistributionMetricTests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/EncodeMetricTests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/GaugeMetricTests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/LocalMetricsAggregatorTests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/SentryMetricsAPITests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/SentryMetricsClientTests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/SetMetricTests.swift delete mode 100644 Tests/SentryTests/Swift/Metrics/TestMetricsClient.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 1736a243191..8cc28363cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Removal of Experimental API + +- Remove the deprecated experimental Metrics API (#4406): [Learn more](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Coming-to-an-End) + ## 8.38.0-beta.1 ### Features diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index ff01f04398e..90163bef6df 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -95,7 +95,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #endif options.enableTimeToFullDisplayTracing = true options.enablePerformanceV2 = true - options.enableMetrics = !args.contains("--disable-metrics") options.add(inAppInclude: "iOS_External") diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index c64e8f922a8..2926e5df337 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -92,10 +92,6 @@ 62375FB92B47F9F000CC55F1 /* SentryDependencyContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62375FB82B47F9F000CC55F1 /* SentryDependencyContainerTests.swift */; }; 623C45B02A651D8200D9E88B /* SentryCoreDataTracker+Test.m in Sources */ = {isa = PBXBuildFile; fileRef = 623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */; }; 624688192C048EF10006179C /* SentryBaggageSerialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624688182C048EF10006179C /* SentryBaggageSerialization.swift */; }; - 626866722BA89641006995EA /* MetricsAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626866712BA89641006995EA /* MetricsAggregator.swift */; }; - 626866742BA89683006995EA /* BucketMetricsAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626866732BA89683006995EA /* BucketMetricsAggregatorTests.swift */; }; - 626866762BA896AD006995EA /* TestMetricsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626866752BA896AD006995EA /* TestMetricsClient.swift */; }; - 626866782BA89928006995EA /* BucketsMetricsAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626866772BA89928006995EA /* BucketsMetricsAggregator.swift */; }; 626E2D4C2BEA0C37005596FE /* SentryEnabledFeaturesBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626E2D4B2BEA0C37005596FE /* SentryEnabledFeaturesBuilderTests.swift */; }; 6271ADF32BA06D9B0098D2E9 /* SentryInternalSerializable.h in Headers */ = {isa = PBXBuildFile; fileRef = 6271ADF22BA06D9B0098D2E9 /* SentryInternalSerializable.h */; }; 627E7589299F6FE40085504D /* SentryInternalDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 627E7588299F6FE40085504D /* SentryInternalDefines.h */; }; @@ -107,25 +103,13 @@ 6294774C2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */; }; 62950F1029E7FE0100A42624 /* SentryTransactionContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */; }; 629690532AD3E060000185FA /* SentryReachabilitySwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629690522AD3E060000185FA /* SentryReachabilitySwiftTests.swift */; }; - 62991A8D2BAC1B4A0078A8B8 /* SentryMetricsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62991A8C2BAC1B4A0078A8B8 /* SentryMetricsAPI.swift */; }; - 62991A8F2BAC24ED0078A8B8 /* SentryMetricsAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62991A8E2BAC24ED0078A8B8 /* SentryMetricsAPITests.swift */; }; - 62A2F43E2BA9AC10000C9FDD /* DistributionMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A2F43D2BA9AC10000C9FDD /* DistributionMetric.swift */; }; - 62A2F4402BA9AC93000C9FDD /* GaugeMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A2F43F2BA9AC93000C9FDD /* GaugeMetric.swift */; }; - 62A2F4422BA9AE12000C9FDD /* SetMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A2F4412BA9AE12000C9FDD /* SetMetric.swift */; }; - 62A2F4442BA9BF08000C9FDD /* GaugeMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A2F4432BA9BF08000C9FDD /* GaugeMetricTests.swift */; }; 62A3C7BE2B7E2A6A00C75227 /* SentrySpotlightTransportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A3C7BD2B7E2A6A00C75227 /* SentrySpotlightTransportTests.swift */; }; 62A456E12B03704A003F19A1 /* SentryUIEventTrackerMode.h in Headers */ = {isa = PBXBuildFile; fileRef = 62A456E02B03704A003F19A1 /* SentryUIEventTrackerMode.h */; }; 62A456E32B0370AA003F19A1 /* SentryUIEventTrackerTransactionMode.h in Headers */ = {isa = PBXBuildFile; fileRef = 62A456E22B0370AA003F19A1 /* SentryUIEventTrackerTransactionMode.h */; }; 62A456E52B0370E0003F19A1 /* SentryUIEventTrackerTransactionMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 62A456E42B0370E0003F19A1 /* SentryUIEventTrackerTransactionMode.m */; }; 62AB8C9E2BF3925700BFC2AC /* WeakReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62AB8C9D2BF3925700BFC2AC /* WeakReference.swift */; }; - 62B0C30D2BA9D39600648D59 /* CounterMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B0C30C2BA9D39600648D59 /* CounterMetricTests.swift */; }; - 62B0C30F2BA9D74800648D59 /* DistributionMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B0C30E2BA9D74800648D59 /* DistributionMetricTests.swift */; }; - 62B0C3112BA9D85C00648D59 /* SetMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B0C3102BA9D85C00648D59 /* SetMetricTests.swift */; }; 62B558B02C6B9C3C00C34FEC /* SentryFramesDelayResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B558AF2C6B9C3C00C34FEC /* SentryFramesDelayResult.swift */; }; 62B86CFC29F052BB008F3947 /* SentryTestLogConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 62B86CFB29F052BB008F3947 /* SentryTestLogConfig.m */; }; - 62BAD74E2BA1C58D00EBAAFC /* EncodeMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62262B952BA1C564004DA3DD /* EncodeMetricTests.swift */; }; - 62BAD7502BA1C5AF00EBAAFC /* SentryMetricsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62BAD74F2BA1C5AF00EBAAFC /* SentryMetricsClientTests.swift */; }; - 62BAD7572BA2033F00EBAAFC /* SentryMetricsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62BAD7552BA202C300EBAAFC /* SentryMetricsClient.swift */; }; 62C1AFAB2B7E10EA0038C5F7 /* SentrySpotlightTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = 62C1AFAA2B7E10EA0038C5F7 /* SentrySpotlightTransport.m */; }; 62C25C862B075F4900C68CBD /* TestOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C25C852B075F4900C68CBD /* TestOptions.swift */; }; 62C316812B1F2E93000D7031 /* SentryDelayedFramesTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 62C316802B1F2E93000D7031 /* SentryDelayedFramesTracker.h */; }; @@ -137,8 +121,6 @@ 62CFD9AE2C99770B00834E1B /* SentryInvalidJSONString.m in Sources */ = {isa = PBXBuildFile; fileRef = 62CFD9AC2C99770B00834E1B /* SentryInvalidJSONString.m */; }; 62E081A929ED4260000F69FC /* SentryBreadcrumbDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 62E081A829ED4260000F69FC /* SentryBreadcrumbDelegate.h */; }; 62E081AB29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E081AA29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift */; }; - 62E146D02BAAE47600ED34FD /* LocalMetricsAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E146CF2BAAE47600ED34FD /* LocalMetricsAggregator.swift */; }; - 62E146D22BAAF55B00ED34FD /* LocalMetricsAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E146D12BAAF55B00ED34FD /* LocalMetricsAggregatorTests.swift */; }; 62EF86A12C626D39004E058B /* SentryANRTrackerV2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */; }; 62F05D2B2C0DB1F100916E3F /* SentryLogTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 62F05D2A2C0DB1F100916E3F /* SentryLogTestHelper.m */; }; 62F226B729A37C120038080D /* SentryBooleanSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 62F226B629A37C120038080D /* SentryBooleanSerialization.m */; }; @@ -1103,10 +1085,6 @@ 623C45AE2A651C4500D9E88B /* SentryCoreDataTracker+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryCoreDataTracker+Test.h"; sourceTree = ""; }; 623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryCoreDataTracker+Test.m"; sourceTree = ""; }; 624688182C048EF10006179C /* SentryBaggageSerialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageSerialization.swift; sourceTree = ""; }; - 626866712BA89641006995EA /* MetricsAggregator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsAggregator.swift; sourceTree = ""; }; - 626866732BA89683006995EA /* BucketMetricsAggregatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketMetricsAggregatorTests.swift; sourceTree = ""; }; - 626866752BA896AD006995EA /* TestMetricsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMetricsClient.swift; sourceTree = ""; }; - 626866772BA89928006995EA /* BucketsMetricsAggregator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketsMetricsAggregator.swift; sourceTree = ""; }; 626E2D4B2BEA0C37005596FE /* SentryEnabledFeaturesBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilderTests.swift; sourceTree = ""; }; 6271ADF22BA06D9B0098D2E9 /* SentryInternalSerializable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInternalSerializable.h; path = include/SentryInternalSerializable.h; sourceTree = ""; }; 627E7588299F6FE40085504D /* SentryInternalDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInternalDefines.h; path = include/SentryInternalDefines.h; sourceTree = ""; }; @@ -1118,24 +1096,13 @@ 6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Delegate.swift; sourceTree = ""; }; 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTransactionContextTests.swift; sourceTree = ""; }; 629690522AD3E060000185FA /* SentryReachabilitySwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReachabilitySwiftTests.swift; sourceTree = ""; }; - 62991A8C2BAC1B4A0078A8B8 /* SentryMetricsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsAPI.swift; sourceTree = ""; }; - 62991A8E2BAC24ED0078A8B8 /* SentryMetricsAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsAPITests.swift; sourceTree = ""; }; - 62A2F43D2BA9AC10000C9FDD /* DistributionMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistributionMetric.swift; sourceTree = ""; }; - 62A2F43F2BA9AC93000C9FDD /* GaugeMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GaugeMetric.swift; sourceTree = ""; }; - 62A2F4412BA9AE12000C9FDD /* SetMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetMetric.swift; sourceTree = ""; }; - 62A2F4432BA9BF08000C9FDD /* GaugeMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GaugeMetricTests.swift; sourceTree = ""; }; 62A3C7BD2B7E2A6A00C75227 /* SentrySpotlightTransportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpotlightTransportTests.swift; sourceTree = ""; }; 62A456E02B03704A003F19A1 /* SentryUIEventTrackerMode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryUIEventTrackerMode.h; path = include/SentryUIEventTrackerMode.h; sourceTree = ""; }; 62A456E22B0370AA003F19A1 /* SentryUIEventTrackerTransactionMode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryUIEventTrackerTransactionMode.h; path = include/SentryUIEventTrackerTransactionMode.h; sourceTree = ""; }; 62A456E42B0370E0003F19A1 /* SentryUIEventTrackerTransactionMode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryUIEventTrackerTransactionMode.m; sourceTree = ""; }; 62AB8C9D2BF3925700BFC2AC /* WeakReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakReference.swift; sourceTree = ""; }; - 62B0C30C2BA9D39600648D59 /* CounterMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterMetricTests.swift; sourceTree = ""; }; - 62B0C30E2BA9D74800648D59 /* DistributionMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistributionMetricTests.swift; sourceTree = ""; }; - 62B0C3102BA9D85C00648D59 /* SetMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetMetricTests.swift; sourceTree = ""; }; 62B558AF2C6B9C3C00C34FEC /* SentryFramesDelayResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFramesDelayResult.swift; sourceTree = ""; }; 62B86CFB29F052BB008F3947 /* SentryTestLogConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTestLogConfig.m; sourceTree = ""; }; - 62BAD74F2BA1C5AF00EBAAFC /* SentryMetricsClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsClientTests.swift; sourceTree = ""; }; - 62BAD7552BA202C300EBAAFC /* SentryMetricsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsClient.swift; sourceTree = ""; }; 62C1AFA92B7E10D30038C5F7 /* SentrySpotlightTransport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpotlightTransport.h; path = include/SentrySpotlightTransport.h; sourceTree = ""; }; 62C1AFAA2B7E10EA0038C5F7 /* SentrySpotlightTransport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySpotlightTransport.m; sourceTree = ""; }; 62C25C852B075F4900C68CBD /* TestOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestOptions.swift; sourceTree = ""; }; @@ -1147,8 +1114,6 @@ 62CFD9AC2C99770B00834E1B /* SentryInvalidJSONString.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryInvalidJSONString.m; sourceTree = ""; }; 62E081A829ED4260000F69FC /* SentryBreadcrumbDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBreadcrumbDelegate.h; path = include/SentryBreadcrumbDelegate.h; sourceTree = ""; }; 62E081AA29ED4322000F69FC /* SentryBreadcrumbTestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBreadcrumbTestDelegate.swift; sourceTree = ""; }; - 62E146CF2BAAE47600ED34FD /* LocalMetricsAggregator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMetricsAggregator.swift; sourceTree = ""; }; - 62E146D12BAAF55B00ED34FD /* LocalMetricsAggregatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMetricsAggregatorTests.swift; sourceTree = ""; }; 62F05D292C0DB1C800916E3F /* SentryLogTestHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryLogTestHelper.h; sourceTree = ""; }; 62F05D2A2C0DB1F100916E3F /* SentryLogTestHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryLogTestHelper.m; sourceTree = ""; }; 62F226B629A37C120038080D /* SentryBooleanSerialization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBooleanSerialization.m; sourceTree = ""; }; @@ -2135,54 +2100,9 @@ path = Helper; sourceTree = ""; }; - 62262B842BA1C453004DA3DD /* Metrics */ = { - isa = PBXGroup; - children = ( - 62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */, - 62262B872BA1C490004DA3DD /* SentryStatsdClient.m */, - ); - name = Metrics; - sourceTree = ""; - }; - 62262B892BA1C4B0004DA3DD /* Metrics */ = { - isa = PBXGroup; - children = ( - 62262B8C2BA1C4DB004DA3DD /* Metric.swift */, - 62262B902BA1C520004DA3DD /* CounterMetric.swift */, - 62A2F43D2BA9AC10000C9FDD /* DistributionMetric.swift */, - 62A2F43F2BA9AC93000C9FDD /* GaugeMetric.swift */, - 62A2F4412BA9AE12000C9FDD /* SetMetric.swift */, - 626866712BA89641006995EA /* MetricsAggregator.swift */, - 626866772BA89928006995EA /* BucketsMetricsAggregator.swift */, - 62E146CF2BAAE47600ED34FD /* LocalMetricsAggregator.swift */, - 62262B8A2BA1C4C1004DA3DD /* EncodeMetrics.swift */, - 62BAD7552BA202C300EBAAFC /* SentryMetricsClient.swift */, - 62991A8C2BAC1B4A0078A8B8 /* SentryMetricsAPI.swift */, - ); - path = Metrics; - sourceTree = ""; - }; - 62262B942BA1C550004DA3DD /* Metrics */ = { - isa = PBXGroup; - children = ( - 62991A8E2BAC24ED0078A8B8 /* SentryMetricsAPITests.swift */, - 62262B952BA1C564004DA3DD /* EncodeMetricTests.swift */, - 62BAD74F2BA1C5AF00EBAAFC /* SentryMetricsClientTests.swift */, - 626866732BA89683006995EA /* BucketMetricsAggregatorTests.swift */, - 62E146D12BAAF55B00ED34FD /* LocalMetricsAggregatorTests.swift */, - 626866752BA896AD006995EA /* TestMetricsClient.swift */, - 62B0C30C2BA9D39600648D59 /* CounterMetricTests.swift */, - 62B0C30E2BA9D74800648D59 /* DistributionMetricTests.swift */, - 62A2F4432BA9BF08000C9FDD /* GaugeMetricTests.swift */, - 62B0C3102BA9D85C00648D59 /* SetMetricTests.swift */, - ); - path = Metrics; - sourceTree = ""; - }; 62872B602BA1B84400A4FA7D /* Swift */ = { isa = PBXGroup; children = ( - 62262B942BA1C550004DA3DD /* Metrics */, 62872B612BA1B84C00A4FA7D /* Extensions */, ); path = Swift; @@ -2622,7 +2542,6 @@ 63AA75C61EB8B06100D153DE /* Sentry */ = { isa = PBXGroup; children = ( - 62262B842BA1C453004DA3DD /* Metrics */, 630436031EC058FA00C4D3FA /* Categories */, 639889D51EDF10BE00EA7442 /* Helper */, 630436001EBCB87500C4D3FA /* Networking */, @@ -3667,7 +3586,6 @@ isa = PBXGroup; children = ( D8CAC02D2BA0663E00E38F34 /* Integrations */, - 62262B892BA1C4B0004DA3DD /* Metrics */, 621D9F2D2B9B030E003D94DE /* Helper */, D8F016B42B962533007B9AFB /* Extensions */, 7BF65060292B8EFE00BBA5A8 /* MetricKit */, @@ -4055,7 +3973,6 @@ D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */, 7B6C5F8126034354007F7DFF /* SentryWatchdogTerminationLogic.h in Headers */, 63FE708520DA4C1000CDBAE8 /* SentryCrashReportFilter.h in Headers */, - 62262B862BA1C46D004DA3DD /* SentryStatsdClient.h in Headers */, 84354E1129BF944900CDBB8B /* SentryProfileTimeseries.h in Headers */, D8ACE3CD2762187D00F5A213 /* SentryNSDataSwizzling.h in Headers */, 7B08A3452924CF6C0059603A /* SentryMetricKitIntegration.h in Headers */, @@ -4627,7 +4544,6 @@ 03F84D3727DD4191008FE43F /* SentrySamplingProfiler.cpp in Sources */, 8453421628BE8A9500C22EEC /* SentrySpanStatus.m in Sources */, 7B08A3472924CF9C0059603A /* SentryMetricKitIntegration.m in Sources */, - 62262B8B2BA1C4C1004DA3DD /* EncodeMetrics.swift in Sources */, 7B63459B280EB9E200CFA05A /* SentryUIEventTrackingIntegration.m in Sources */, D8AE48AE2C577EAB0092A2A6 /* SentryLog.swift in Sources */, 15E0A8ED240F2CB000F044E3 /* SentrySerialization.m in Sources */, @@ -4641,7 +4557,6 @@ 7BA61CCA247D128B00C130A8 /* SentryThreadInspector.m in Sources */, D8CA12952C203E71005894F4 /* SentrySessionListener.swift in Sources */, 63FE718D20DA4C1100CDBAE8 /* SentryCrashReportStore.c in Sources */, - 62A2F43E2BA9AC10000C9FDD /* DistributionMetric.swift in Sources */, 7BA0C0482805600A003E0326 /* SentryTransportAdapter.m in Sources */, 63FE712920DA4C1000CDBAE8 /* SentryCrashCPU_arm.c in Sources */, 03F84D3427DD4191008FE43F /* SentryThreadMetadataCache.cpp in Sources */, @@ -4664,7 +4579,6 @@ 8E133FA225E72DEF00ABD0BF /* SentrySamplingContext.m in Sources */, D81988C02BEBFFF70020E36C /* SentryReplayRecording.swift in Sources */, 7B6C5F8726034395007F7DFF /* SentryWatchdogTerminationLogic.m in Sources */, - 62A2F4402BA9AC93000C9FDD /* GaugeMetric.swift in Sources */, 7BBC827125DFD039005F1ED8 /* SentryInAppLogic.m in Sources */, 63FE708D20DA4C1000CDBAE8 /* SentryCrashReportFilterBasic.m in Sources */, 63FE718120DA4C1100CDBAE8 /* SentryCrashDoctor.m in Sources */, @@ -4744,9 +4658,7 @@ 849B8F9A2C6E906900148E1F /* SentryUserFeedbackConfiguration.swift in Sources */, 63295AF71EF3C7DB002D4490 /* SentryNSDictionarySanitize.m in Sources */, 7B8ECBFC26498958005FE2EF /* SentryAppStateManager.m in Sources */, - 62262B8D2BA1C4DB004DA3DD /* Metric.swift in Sources */, 7B2A70DD27D6083D008B0D15 /* SentryThreadWrapper.m in Sources */, - 62E146D02BAAE47600ED34FD /* LocalMetricsAggregator.swift in Sources */, D8ACE3C72762187200F5A213 /* SentryNSDataSwizzling.m in Sources */, 638DC9A11EBC6B6400A66E41 /* SentryRequestOperation.m in Sources */, 63AA767A1EB8D20500D153DE /* SentryLogC.m in Sources */, @@ -4764,7 +4676,6 @@ 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, D8739D182BEEA33F007D2F66 /* SentryLevelHelper.m in Sources */, - 62BAD7572BA2033F00EBAAFC /* SentryMetricsClient.swift in Sources */, 63FE712720DA4C1000CDBAE8 /* SentryCrashThread.c in Sources */, 7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */, 62C316832B1F2EA1000D7031 /* SentryDelayedFramesTracker.m in Sources */, @@ -4802,7 +4713,6 @@ 63FE712D20DA4C1100CDBAE8 /* SentryCrashJSONCodecObjC.m in Sources */, 7BBD18932449BEDD00427C76 /* SentryDefaultRateLimits.m in Sources */, 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, - 62262B882BA1C490004DA3DD /* SentryStatsdClient.m in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, 849B8F992C6E906900148E1F /* SentryUserFeedbackFormConfiguration.swift in Sources */, 8E8C57A225EEFC07001CEEFA /* SentrySampling.m in Sources */, @@ -4839,8 +4749,6 @@ D8CAC0412BA0984500E38F34 /* SentryIntegrationProtocol.swift in Sources */, 63FE710F20DA4C1000CDBAE8 /* SentryCrashNSErrorUtil.m in Sources */, 8ECC674925C23A20000E2BF6 /* SentrySpanId.m in Sources */, - 626866722BA89641006995EA /* MetricsAggregator.swift in Sources */, - 626866782BA89928006995EA /* BucketsMetricsAggregator.swift in Sources */, 6344DDB51EC309E000D9160D /* SentryCrashReportSink.m in Sources */, 8EAE9806261E87120073B6B3 /* SentryUIViewControllerPerformanceTracker.m in Sources */, D81988C72BEC18E20020E36C /* SentryRRWebVideoEvent.swift in Sources */, @@ -4877,14 +4785,11 @@ 63FE714520DA4C1100CDBAE8 /* SentryCrashObjC.c in Sources */, 63FE710520DA4C1000CDBAE8 /* SentryAsyncSafeLog.c in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, - 62262B912BA1C520004DA3DD /* CounterMetric.swift in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, D8BC28CA2BFF68CA0054DA4D /* NumberExtensions.swift in Sources */, D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */, 632F43521F581D5400A18A36 /* SentryCrashExceptionApplication.m in Sources */, - 62A2F4422BA9AE12000C9FDD /* SetMetric.swift in Sources */, 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */, - 62991A8D2BAC1B4A0078A8B8 /* SentryMetricsAPI.swift in Sources */, 7B77BE3727EC8460003C9020 /* SentryDiscardReasonMapper.m in Sources */, 63FE712520DA4C1000CDBAE8 /* SentryCrashSignalInfo.c in Sources */, 63FE70F320DA4C1000CDBAE8 /* SentryCrashMonitor_Signal.c in Sources */, @@ -4917,7 +4822,6 @@ 7B68345128F7EB3D00FB7064 /* SentryMeasurementUnitTests.swift in Sources */, 7B14089A248791660035403D /* SentryCrashStackEntryMapperTests.swift in Sources */, D85790292976A69F00C6AC1F /* TestDebugImageProvider.swift in Sources */, - 62E146D22BAAF55B00ED34FD /* LocalMetricsAggregatorTests.swift in Sources */, 7B869EBC249B91D8004F4FDB /* SentryDebugMetaEquality.swift in Sources */, 7B01CE3D271993AC00B5AF31 /* SentryTransportFactoryTests.swift in Sources */, 7B30B68026527C3C006B2752 /* SentryFramesTrackerTests.swift in Sources */, @@ -4929,7 +4833,6 @@ 7BE3C78724472E9800A38442 /* TestRequestManager.swift in Sources */, 7BD4E8E627FD84480086C410 /* TestFileManagerDelegate.swift in Sources */, 63FE722220DA66EC00CDBAE8 /* SentryCrashJSONCodec_Tests.m in Sources */, - 62B0C3112BA9D85C00648D59 /* SetMetricTests.swift in Sources */, 7B0A5452252311CE00A71716 /* SentryBreadcrumbTests.swift in Sources */, 7BE3C7752445C82300A38442 /* SentryCurrentDateTests.swift in Sources */, 7B3398672459C4AE00BD9C96 /* SentryEnvelopeRateLimitTests.swift in Sources */, @@ -4946,9 +4849,7 @@ 7B98D7EC25FB7C4900C5A389 /* SentryAppStateTests.swift in Sources */, 62A3C7BE2B7E2A6A00C75227 /* SentrySpotlightTransportTests.swift in Sources */, 8EE017A126704CD500470616 /* SentryUIViewControllerPerformanceTrackerTests.swift in Sources */, - 62B0C30D2BA9D39600648D59 /* CounterMetricTests.swift in Sources */, 7B5B94352657AD21002E474B /* SentryFramesTrackingIntegrationTests.swift in Sources */, - 626866762BA896AD006995EA /* TestMetricsClient.swift in Sources */, 8431EE5B29ADB8EA00D8DC56 /* SentryTimeTests.m in Sources */, 7B0A54562523178700A71716 /* SentryScopeSwiftTests.swift in Sources */, 7B5B94332657A816002E474B /* SentryAppStartTrackingIntegrationTests.swift in Sources */, @@ -4989,7 +4890,6 @@ 7B2A70DF27D60904008B0D15 /* SentryTestThreadWrapper.swift in Sources */, 62F4DDA12C04CB9700588890 /* SentryBaggageSerializationTests.swift in Sources */, 7BE912AF272166DD00E49E62 /* SentryNoOpSpanTests.swift in Sources */, - 62991A8F2BAC24ED0078A8B8 /* SentryMetricsAPITests.swift in Sources */, 6229416A2BB2F123004765D1 /* SentryNSDataUtilsTests.swift in Sources */, 7B56D73524616E5600B842DA /* SentryConcurrentRateLimitsDictionaryTests.swift in Sources */, 7B7D8730248648AD00D2ECFF /* SentryStacktraceBuilderTests.swift in Sources */, @@ -5027,7 +4927,6 @@ 62375FB92B47F9F000CC55F1 /* SentryDependencyContainerTests.swift in Sources */, 7BC6EC08255C36DE0059822A /* SentryStacktraceTests.swift in Sources */, D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */, - 62B0C30F2BA9D74800648D59 /* DistributionMetricTests.swift in Sources */, 7B26BBFB24C0A66D00A79CCC /* SentrySdkInfoNilTests.m in Sources */, 7B984A9F28E572AF001F4BEE /* CrashReport.swift in Sources */, 7BED3576266F7BFF00EAA70D /* TestSentryCrashWrapper.m in Sources */, @@ -5038,7 +4937,6 @@ 7B87C916295ECFD700510C52 /* SentryMetricKitEventTests.swift in Sources */, 7B6D98ED24C703F8005502FA /* Async.swift in Sources */, 7BA0C04C28056556003E0326 /* SentryTransportAdapterTests.swift in Sources */, - 62BAD74E2BA1C58D00EBAAFC /* EncodeMetricTests.swift in Sources */, 7BE0DC29272A9E1C004FA8B7 /* SentryBreadcrumbTrackerTests.swift in Sources */, 63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */, D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */, @@ -5094,7 +4992,6 @@ 7B6D135C27F4605D00331ED2 /* TestEnvelopeRateLimitDelegate.swift in Sources */, 7B2A70D827D5F080008B0D15 /* SentryANRTrackerV1Tests.swift in Sources */, 63FE722120DA66EC00CDBAE8 /* SentryCrashDynamicLinker_Tests.m in Sources */, - 62A2F4442BA9BF08000C9FDD /* GaugeMetricTests.swift in Sources */, 63FE720120DA66EC00CDBAE8 /* RFC3339UTFString_Tests.m in Sources */, 63AA76701EB8CB4B00D153DE /* SentryTests.m in Sources */, 8E70B0FD25CB72BE002B3155 /* SentrySpanTests.swift in Sources */, @@ -5133,7 +5030,6 @@ 7BA61CAF247BBF3C00C130A8 /* SentryDebugImageProviderTests.swift in Sources */, 7BB7E7C729267A28004BF96B /* EmptyIntegration.swift in Sources */, 7B965728268321CD00C66E25 /* SentryCrashScopeObserverTests.swift in Sources */, - 626866742BA89683006995EA /* BucketMetricsAggregatorTests.swift in Sources */, 7BD86ECB264A6DB5005439DB /* TestSysctl.swift in Sources */, D861301C2BB5A267004C0F5E /* SentrySessionReplayTests.swift in Sources */, 7B0DC73428869BF40039995F /* NSMutableDictionarySentryTests.swift in Sources */, @@ -5147,7 +5043,6 @@ 7B4F22DC294089530067EA17 /* FormatHexAddress.swift in Sources */, 8EAC7FF8265C8910005B44E5 /* SentryTracerTests.swift in Sources */, 0A1B497328E597DD00D7BFA3 /* TestLogOutput.swift in Sources */, - 62BAD7502BA1C5AF00EBAAFC /* SentryMetricsClientTests.swift in Sources */, D8CB742E294B294B00A5F964 /* MockUIScene.m in Sources */, 7BA61CBD247BC6B900C130A8 /* TestSentryCrashBinaryImageProvider.swift in Sources */, 7BFA69F627E0840400233199 /* SentryANRTrackingIntegrationTests.swift in Sources */, diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 7914b54437e..597dd11e08d 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -714,38 +714,6 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, copy) NSString *spotlightUrl; -/** - * Wether to enable DDM (delightful developer metrics) or not. For more information see - * https://docs.sentry.io/product/metrics/. - * - * @warning This is an experimental feature and may still have bugs. - * @note Default value is @c NO . - */ -@property (nonatomic, assign) BOOL enableMetrics; - -/** - * Wether to enable adding some default tags to every metrics or not. You need to enable @c - * enableMetrics for this flag to work. - * - * @warning This is an experimental feature and may still have bugs. - * @note Default value is @c YES . - */ -@property (nonatomic, assign) BOOL enableDefaultTagsForMetrics; - -/** - * Wether to enable connecting metrics to spans and transactions or not. You need to enable @c - * enableMetrics for this flag to work. - * - * @warning This is an experimental feature and may still have bugs. - * @note Default value is @c YES . - */ -@property (nonatomic, assign) BOOL enableSpanLocalMetricAggregation; - -/** - * This block can be used to modify the event before it will be serialized and sent. - */ -@property (nullable, nonatomic, copy) SentryBeforeEmitMetricCallback beforeEmitMetric; - /** * This aggregates options for experimental features. * Be aware that the options available for experimental can change at any time. diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 27549bf4f19..79989327f5b 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -22,7 +22,6 @@ #import "SentryScope+Private.h" #import "SentrySerialization.h" #import "SentrySession+Private.h" -#import "SentryStatsdClient.h" #import "SentrySwift.h" #import "SentryTraceOrigins.h" #import "SentryTracer.h" @@ -35,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface SentryHub () +@interface SentryHub () @property (nullable, nonatomic, strong) SentryClient *client; @property (nullable, nonatomic, strong) SentryScope *scope; @@ -73,18 +72,6 @@ - (instancetype)initWithClient:(nullable SentryClient *)client _scope = scope; _crashWrapper = crashWrapper; _dispatchQueue = dispatchQueue; - SentryStatsdClient *statsdClient = [[SentryStatsdClient alloc] initWithClient:client]; - SentryMetricsClient *metricsClient = - [[SentryMetricsClient alloc] initWithClient:statsdClient]; - _metrics = [[SentryMetricsAPI alloc] - initWithEnabled:client.options.enableMetrics - client:metricsClient - currentDate:SentryDependencyContainer.sharedInstance.dateProvider - dispatchQueue:_dispatchQueue - random:SentryDependencyContainer.sharedInstance.random - beforeEmitMetric:client.options.beforeEmitMetric]; - [_metrics setDelegate:self]; - _sessionLock = [[NSObject alloc] init]; _integrationsLock = [[NSObject alloc] init]; _installedIntegrations = [[NSMutableArray alloc] init]; @@ -762,7 +749,6 @@ - (NSString *)createSessionDebugString:(SentrySession *)session - (void)flush:(NSTimeInterval)timeout { - [_metrics flush]; SentryClient *client = _client; if (client != nil) { [client flush:timeout]; @@ -771,47 +757,10 @@ - (void)flush:(NSTimeInterval)timeout - (void)close { - [_metrics close]; [_client close]; SENTRY_LOG_DEBUG(@"Closed the Hub."); } -#pragma mark - SentryMetricsAPIDelegate - -- (NSDictionary *)getDefaultTagsForMetrics -{ - SentryOptions *options = [_client options]; - if (options == nil || options.enableDefaultTagsForMetrics == NO) { - return @{}; - } - - NSMutableDictionary *defaultTags = [NSMutableDictionary dictionary]; - - if (options.releaseName != nil) { - defaultTags[@"release"] = options.releaseName; - } - - defaultTags[@"environment"] = options.environment; - - return defaultTags; -} - -- (id _Nullable)getCurrentSpan -{ - return _scope.span; -} - -- (LocalMetricsAggregator *_Nullable)getLocalMetricsAggregatorWithSpan:(id)span -{ - // We don't want to add them LocalMetricsAggregator to the SentrySpan protocol and make it - // public. Instead, we check if the span responds to the getLocalMetricsAggregator which, every - // span should do. - if ([span isKindOfClass:SentrySpan.class]) { - return [(SentrySpan *)span getLocalMetricsAggregator]; - } - return nil; -} - - (void)registerSessionListener:(id)listener { _sessionListener = listener; diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 554eb37b43b..0de5de589f8 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -139,9 +139,6 @@ - (instancetype)init self.swiftAsyncStacktraces = NO; self.enableSpotlight = NO; self.spotlightUrl = @"http://localhost:8969/stream"; - self.enableMetrics = NO; - self.enableDefaultTagsForMetrics = YES; - self.enableSpanLocalMetricAggregation = YES; #if TARGET_OS_OSX NSString *dsn = [[[NSProcessInfo processInfo] environment] objectForKey:@"SENTRY_DSN"]; @@ -550,18 +547,6 @@ - (BOOL)validateOptions:(NSDictionary *)options self.spotlightUrl = options[@"spotlightUrl"]; } - [self setBool:options[@"enableMetrics"] block:^(BOOL value) { self->_enableMetrics = value; }]; - - [self setBool:options[@"enableDefaultTagsForMetrics"] - block:^(BOOL value) { self->_enableDefaultTagsForMetrics = value; }]; - - [self setBool:options[@"enableSpanLocalMetricAggregation"] - block:^(BOOL value) { self->_enableSpanLocalMetricAggregation = value; }]; - - if ([self isBlock:options[@"beforeEmitMetric"]]) { - self.beforeEmitMetric = options[@"beforeEmitMetric"]; - } - if ([options[@"experimental"] isKindOfClass:NSDictionary.class]) { [self.experimental validateOptions:options[@"experimental"]]; } diff --git a/Sources/Sentry/SentrySpan.m b/Sources/Sentry/SentrySpan.m index 7f8395e6d0a..e76a7683ff0 100644 --- a/Sources/Sentry/SentrySpan.m +++ b/Sources/Sentry/SentrySpan.m @@ -42,7 +42,6 @@ @implementation SentrySpan { NSObject *_stateLock; BOOL _isFinished; uint64_t _startSystemTime; - LocalMetricsAggregator *localMetricsAggregator; #if SENTRY_HAS_UIKIT NSUInteger initTotalFrames; NSUInteger initSlowFrames; @@ -310,14 +309,6 @@ - (nullable SentryTraceContext *)traceContext return self.tracer.traceContext; } -- (LocalMetricsAggregator *)getLocalMetricsAggregator -{ - if (localMetricsAggregator == nil) { - localMetricsAggregator = [[LocalMetricsAggregator alloc] init]; - } - return localMetricsAggregator; -} - - (NSDictionary *)serialize { NSMutableDictionary *mutableDictionary = @{ @@ -358,10 +349,6 @@ - (NSDictionary *)serialize [mutableDictionary setValue:@(self.startTimestamp.timeIntervalSince1970) forKey:@"start_timestamp"]; - if (localMetricsAggregator != nil) { - mutableDictionary[@"_metrics_summary"] = [localMetricsAggregator serialize]; - } - @synchronized(_data) { NSMutableDictionary *data = _data.mutableCopy; diff --git a/Sources/Sentry/SentryStatsdClient.m b/Sources/Sentry/SentryStatsdClient.m deleted file mode 100644 index 219d683c480..00000000000 --- a/Sources/Sentry/SentryStatsdClient.m +++ /dev/null @@ -1,51 +0,0 @@ -#import "SentryStatsdClient.h" -#import "SentryClient+Private.h" -#import "SentryEnvelope.h" -#import "SentryEnvelopeItemHeader.h" -#import "SentryEnvelopeItemType.h" -#import "SentrySwift.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface SentryStatsdClient () - -@property (nonatomic, strong) SentryClient *client; - -@end - -@implementation SentryStatsdClient - -- (instancetype)initWithClient:(SentryClient *)client -{ - if (self = [super init]) { - self.client = client; - } - - return self; -} - -- (void)captureStatsdEncodedData:(NSData *)statsdEncodedData -{ - if (statsdEncodedData.length == 0) { - return; - } - - SentryEnvelopeItemHeader *header = - [[SentryEnvelopeItemHeader alloc] initWithType:SentryEnvelopeItemTypeStatsd - length:statsdEncodedData.length - contentType:@"application/octet-stream"]; - - SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithHeader:header - data:statsdEncodedData]; - - SentryEnvelopeHeader *envelopeHeader = - [[SentryEnvelopeHeader alloc] initWithId:[[SentryId alloc] init]]; - NSArray *items = @[ item ]; - SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader items:items]; - - [self.client captureEnvelope:envelope]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryTransaction.m b/Sources/Sentry/SentryTransaction.m index 57778f61419..9c55a92bc4f 100644 --- a/Sources/Sentry/SentryTransaction.m +++ b/Sources/Sentry/SentryTransaction.m @@ -45,17 +45,7 @@ - (instancetype)initWithTrace:(SentryTracer *)trace children:(NSArray *serializedTrace = [self.trace serialize].mutableCopy; - NSDictionary *metricsSummary = serializedTrace[@"_metrics_summary"]; - if (metricsSummary != nil) { - serializedData[@"_metrics_summary"] = metricsSummary; - [serializedTrace removeObjectForKey:@"_metrics_summary"]; - } - mutableContext[@"trace"] = serializedTrace; + mutableContext[@"trace"] = [self.trace serialize]; [serializedData setValue:mutableContext forKey:@"contexts"]; diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 5d9f0e21906..5cbf613c036 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -2,7 +2,6 @@ #import "SentryDispatchQueueWrapper.h" #import "SentryNSDataUtils.h" #import "SentryRandom.h" -#import "SentryStatsdClient.h" #import "SentryTime.h" // Headers that also import SentryDefines should be at the end of this list diff --git a/Sources/Sentry/include/SentrySpan.h b/Sources/Sentry/include/SentrySpan.h index 2653239d384..acbc382ed96 100644 --- a/Sources/Sentry/include/SentrySpan.h +++ b/Sources/Sentry/include/SentrySpan.h @@ -5,7 +5,6 @@ NS_ASSUME_NONNULL_BEGIN @class SentryTracer, SentryId, SentrySpanId, SentryFrame, SentrySpanContext; -@class LocalMetricsAggregator; #if SENTRY_HAS_UIKIT @class SentryFramesTracker; @@ -85,8 +84,6 @@ SENTRY_NO_INIT */ @property (nullable, nonatomic, strong) NSArray *frames; -- (LocalMetricsAggregator *)getLocalMetricsAggregator; - /** * Init a @c SentrySpan with given transaction and context. * @param transaction The @c SentryTracer managing the transaction this span is associated with. diff --git a/Sources/Sentry/include/SentryStatsdClient.h b/Sources/Sentry/include/SentryStatsdClient.h deleted file mode 100644 index 1d332e09295..00000000000 --- a/Sources/Sentry/include/SentryStatsdClient.h +++ /dev/null @@ -1,16 +0,0 @@ -#import "SentryDefines.h" - -NS_ASSUME_NONNULL_BEGIN - -@class SentryClient; - -@interface SentryStatsdClient : NSObject -SENTRY_NO_INIT - -- (instancetype)initWithClient:(SentryClient *)client; - -- (void)captureStatsdEncodedData:(NSData *)statsdEncodedData; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift b/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift index 288b43e5372..b6f2db89a99 100644 --- a/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift +++ b/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift @@ -36,10 +36,6 @@ import Foundation features.append("swiftAsyncStacktraces") } - if options.enableMetrics { - features.append("metrics") - } - return features } } diff --git a/Sources/Swift/Metrics/BucketsMetricsAggregator.swift b/Sources/Swift/Metrics/BucketsMetricsAggregator.swift deleted file mode 100644 index beff3a4e499..00000000000 --- a/Sources/Swift/Metrics/BucketsMetricsAggregator.swift +++ /dev/null @@ -1,241 +0,0 @@ -@_implementationOnly import _SentryPrivate - -/// The bucket timestamp is calculated: -/// ( timeIntervalSince1970 / ROLLUP_IN_SECONDS ) * ROLLUP_IN_SECONDS -typealias BucketTimestamp = UInt64 -let ROLLUP_IN_SECONDS: TimeInterval = 10 - -extension SentryCurrentDateProvider { - var bucketTimestamp: BucketTimestamp { - let now = self.date() - let seconds = now.timeIntervalSince1970 - - return (UInt64(seconds) / UInt64(ROLLUP_IN_SECONDS)) * UInt64(ROLLUP_IN_SECONDS) - } -} - -class BucketMetricsAggregator: MetricsAggregator { - - private let client: SentryMetricsClient - private let currentDate: SentryCurrentDateProvider - private let dispatchQueue: SentryDispatchQueueWrapper - private let random: SentryRandomProtocol - private let beforeEmitMetric: BeforeEmitMetricCallback? - private let totalMaxWeight: UInt - private let flushShift: TimeInterval - private let flushInterval: TimeInterval - private let flushTolerance: TimeInterval - - private var timer: DispatchSourceTimer? - private var totalBucketsWeight: UInt = 0 - private var buckets: [BucketTimestamp: [String: Metric]] = [:] - private let lock = NSLock() - - init( - client: SentryMetricsClient, - currentDate: SentryCurrentDateProvider, - dispatchQueue: SentryDispatchQueueWrapper, - random: SentryRandomProtocol, - beforeEmitMetric: BeforeEmitMetricCallback? = nil, - totalMaxWeight: UInt = 1_000, - flushInterval: TimeInterval = 10.0, - flushTolerance: TimeInterval = 0.5 - ) { - self.client = client - self.currentDate = currentDate - self.dispatchQueue = dispatchQueue - self.random = random - self.beforeEmitMetric = beforeEmitMetric - - // The aggregator shifts its flushing by up to an entire rollup window to - // avoid multiple clients trampling on end of a 10 second window as all the - // buckets are anchored to multiples of ROLLUP seconds. We randomize this - // number once per aggregator boot to achieve some level of offsetting - // across a fleet of deployed SDKs. - let flushShift = random.nextNumber() * ROLLUP_IN_SECONDS - self.totalMaxWeight = totalMaxWeight - self.flushInterval = flushInterval - self.flushShift = flushShift - self.flushTolerance = flushTolerance - - startTimer() - } - - private func startTimer() { - let timer = DispatchSource.makeTimerSource(flags: [], queue: dispatchQueue.queue) - - // Set leeway to reduce energy impact - let leewayInMilliseconds: Int = Int(flushTolerance * 1_000) - timer.schedule(deadline: .now() + flushInterval, repeating: self.flushInterval, leeway: .milliseconds(leewayInMilliseconds)) - timer.setEventHandler { [weak self] in - self?.flush(force: false) - } - timer.activate() - self.timer = timer - } - - func increment(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) { - self.add(type: MetricType.counter, - key: key, value: value, - unit: unit, - tags: tags, - localMetricsAggregator: localMetricsAggregator, - initMetric: { - CounterMetric(first: value, key: key, unit: unit, tags: tags) - }, addValueToMetric: { metric in - // Unit tests validate that the cast works. If it doesn't, a test will fail. - let castedMetric = metric as? CounterMetric - castedMetric?.add(value: value) - }) - } - - func gauge(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) { - self.add(type: MetricType.gauge, - key: key, value: value, - unit: unit, - tags: tags, - localMetricsAggregator: localMetricsAggregator, - initMetric: { - GaugeMetric(first: value, key: key, unit: unit, tags: tags) - }, addValueToMetric: { metric in - // Unit tests validate that the cast works. If it doesn't, a test will fail. - let castedMetric = metric as? GaugeMetric - castedMetric?.add(value: value) - }) - } - - func distribution(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) { - self.add(type: MetricType.distribution, - key: key, value: value, - unit: unit, - tags: tags, - localMetricsAggregator: localMetricsAggregator, - initMetric: { - DistributionMetric(first: value, key: key, unit: unit, tags: tags) - }, addValueToMetric: { metric in - // Unit tests validate that the cast works. If it doesn't, a test will fail. - let castedMetric = metric as? DistributionMetric - castedMetric?.add(value: value) - }) - } - - func set(key: String, value: UInt, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) { - self.add(type: MetricType.set, - key: key, - value: 0, // Value is not used for set - unit: unit, - tags: tags, - localMetricsAggregator: localMetricsAggregator, - initMetric: { - SetMetric(first: value, key: key, unit: unit, tags: tags) - }, addValueToMetric: { metric in - // Unit tests validate that the cast works. If it doesn't, a test will fail. - let castedMetric = metric as? SetMetric - castedMetric?.add(value: value) - }) - } - - private func add(type: MetricType, key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?, initMetric: () -> Metric, addValueToMetric: (Metric) -> Void) { - - if let beforeEmitMetric = self.beforeEmitMetric { - if !beforeEmitMetric(key, tags) { - return - } - } - - let tagsKey = tags.getMetricsTagsKey() - let bucketKey = "\(type)_\(key)_\(unit.unit)_\(tagsKey)" - - let bucketTimestamp = currentDate.bucketTimestamp - - var isOverWeight = false - - lock.synchronized { - var bucket = buckets[bucketTimestamp] ?? [:] - let oldWeight = bucket[bucketKey]?.weight ?? 0 - - let metric = bucket[bucketKey] ?? initMetric() - let metricExists = bucket[bucketKey] != nil - - if metricExists { - addValueToMetric(metric) - } - - let addedWeight = metric.weight - oldWeight - - bucket[bucketKey] = metric - totalBucketsWeight += addedWeight - - buckets[bucketTimestamp] = bucket - - let totalWeight = UInt(buckets.count) + totalBucketsWeight - isOverWeight = totalWeight >= totalMaxWeight - - // For sets, we only record that a value has been added to the set but not which one. See develop docs: https://develop.sentry.dev/sdk/metrics/#sets - if localMetricsAggregator != nil { - let localValue = type == .set ? Double(addedWeight) : value - localMetricsAggregator?.add(type: type, key: key, value: localValue, unit: unit, tags: tags) - } - } - - if isOverWeight { - dispatchQueue.dispatchAsync({ [weak self] in - self?.flush(force: true) - }) - } - } - - func flush(force: Bool) { - var flushableBuckets: [BucketTimestamp: [Metric]] = [:] - - if force { - lock.synchronized { - for (timestamp, metrics) in buckets { - flushableBuckets[timestamp] = Array(metrics.values) - } - - buckets.removeAll() - totalBucketsWeight = 0 - } - } else { - let cutoff = BucketTimestamp(currentDate.date().timeIntervalSince1970 - ROLLUP_IN_SECONDS - flushShift) - - lock.synchronized { - for (bucketTimestamp, bucket) in buckets { - if bucketTimestamp <= cutoff { - flushableBuckets[bucketTimestamp] = Array(bucket.values) - } - } - - var weightToRemove: UInt = 0 - for (bucketTimestamp, metrics) in flushableBuckets { - for metric in metrics { - weightToRemove += metric.weight - } - buckets.removeValue(forKey: bucketTimestamp) - } - - totalBucketsWeight -= weightToRemove - } - } - - if !flushableBuckets.isEmpty { - client.capture(flushableBuckets: flushableBuckets) - } - } - - func close() { - self.flush(force: true) - - cancelTimer() - } - - deinit { - cancelTimer() - } - - private func cancelTimer() { - self.timer?.cancel() - self.timer = nil - } -} diff --git a/Sources/Swift/Metrics/CounterMetric.swift b/Sources/Swift/Metrics/CounterMetric.swift deleted file mode 100644 index 0434d1d3afb..00000000000 --- a/Sources/Swift/Metrics/CounterMetric.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -class CounterMetric: Metric { - - private var value: Double - var weight: UInt = 1 - - init(first: Double, key: String, unit: MeasurementUnit, tags: [String: String]) { - value = first - super.init(type: .counter, key: key, unit: unit, tags: tags) - } - - func add(value: Double) { - self.value += value - } - - func serialize() -> [String] { - return ["\(value)"] - } -} diff --git a/Sources/Swift/Metrics/DistributionMetric.swift b/Sources/Swift/Metrics/DistributionMetric.swift deleted file mode 100644 index 4a31c49f435..00000000000 --- a/Sources/Swift/Metrics/DistributionMetric.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -class DistributionMetric: Metric { - - private var values: [Double] - var weight: UInt { - return UInt(values.count) - } - - init(first: Double, key: String, unit: MeasurementUnit, tags: [String: String]) { - values = [first] - super.init(type: .distribution, key: key, unit: unit, tags: tags) - } - - func add(value: Double) { - self.values.append(value) - } - - func serialize() -> [String] { - return values.map { "\($0)" } - } -} diff --git a/Sources/Swift/Metrics/EncodeMetrics.swift b/Sources/Swift/Metrics/EncodeMetrics.swift deleted file mode 100644 index 83b9762c115..00000000000 --- a/Sources/Swift/Metrics/EncodeMetrics.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation - -/// Encodes the metrics into a Statsd compatible format. -/// See https://github.com/statsd/statsd#usage and https://getsentry.github.io/relay/relay_metrics/index.html for more details about the format. -func encodeToStatsd(flushableBuckets: [BucketTimestamp: [Metric]]) -> Data { - var statsdString = "" - - for bucket in flushableBuckets { - let timestamp = bucket.key - let buckets = bucket.value - for metric in buckets { - - statsdString.append(sanitize(metricKey: metric.key)) - statsdString.append("@") - - statsdString.append(sanitize(metricUnit: metric.unit.unit)) - - for serializedValue in metric.serialize() { - statsdString.append(":\(serializedValue)") - } - - statsdString.append("|") - statsdString.append(metric.type.rawValue) - - var firstTag = true - for (tagKey, tagValue) in metric.tags { - let sanitizedTagKey = sanitize(tagKey: tagKey) - - if firstTag { - statsdString.append("|#") - firstTag = false - } else { - statsdString.append(",") - } - - statsdString.append("\(sanitizedTagKey):") - statsdString.append(replaceTagValueCharacters(tagValue: tagValue)) - } - - statsdString.append("|T") - statsdString.append("\(timestamp)") - statsdString.append("\n") - } - } - - return statsdString.data(using: .utf8) ?? Data() -} - -private func sanitize(metricUnit: String) -> String { - // We can't use \w because it includes chars like ä on Swift - return metricUnit.replacingOccurrences(of: "[^a-zA-Z0-9_]", with: "", options: .regularExpression) -} - -private func sanitize(metricKey: String) -> String { - // We can't use \w because it includes chars like ä on Swift - return metricKey.replacingOccurrences(of: "[^a-zA-Z0-9_.-]+", with: "_", options: .regularExpression) -} - -private func sanitize(tagKey: String) -> String { - // We can't use \w because it includes chars like ä on Swift - return tagKey.replacingOccurrences(of: "[^a-zA-Z0-9_/.-]+", with: "", options: .regularExpression) -} - -private func replaceTagValueCharacters(tagValue: String) -> String { - var result = tagValue.replacingOccurrences(of: "\\", with: #"\\\\"#) - result = result.replacingOccurrences(of: "\n", with: #"\\n"#) - result = result.replacingOccurrences(of: "\r", with: #"\\r"#) - result = result.replacingOccurrences(of: "\t", with: #"\\t"#) - result = result.replacingOccurrences(of: "|", with: #"\\u{7c}"#) - return result.replacingOccurrences(of: ",", with: #"\\u{2c}"#) - -} diff --git a/Sources/Swift/Metrics/GaugeMetric.swift b/Sources/Swift/Metrics/GaugeMetric.swift deleted file mode 100644 index f5b938498be..00000000000 --- a/Sources/Swift/Metrics/GaugeMetric.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation - -class GaugeMetric: Metric { - - private var last: Double - private var min: Double - private var max: Double - private var sum: Double - private var count: UInt - - var weight: UInt = 5 - - init(first: Double, key: String, unit: MeasurementUnit, tags: [String: String]) { - self.last = first - self.min = first - self.max = first - self.sum = first - self.count = 1 - - super.init(type: .gauge, key: key, unit: unit, tags: tags) - } - - func add(value: Double) { - self.last = value - min = Swift.min(min, value) - max = Swift.max(max, value) - sum += value - count += 1 - } - - func serialize() -> [String] { - return ["\(last)", "\(min)", "\(max)", "\(sum)", "\(count)"] - } -} diff --git a/Sources/Swift/Metrics/LocalMetricsAggregator.swift b/Sources/Swift/Metrics/LocalMetricsAggregator.swift deleted file mode 100644 index a9f393fcb22..00000000000 --- a/Sources/Swift/Metrics/LocalMetricsAggregator.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation - -/// Used for correlating metrics to spans. See https://github.com/getsentry/rfcs/blob/main/text/0123-metrics-correlation.md -/// -@objc class LocalMetricsAggregator: NSObject { - - private struct Metric { - let min: Double - let max: Double - let count: Int - let sum: Double - let tags: [String: String] - } - - private var metricBuckets: [String: [String: Metric]] = [:] - private let lock = NSLock() - - func add(type: MetricType, key: String, value: Double, unit: MeasurementUnit, tags: [String: String]) { - - let exportKey = unit.unit.isEmpty ? "\(type.rawValue):\(key)" : "\(type.rawValue):\(key)@\(unit.unit)" - let tagsKey = tags.getMetricsTagsKey() - - lock.synchronized { - var bucket = metricBuckets[exportKey] ?? [:] - - var metric = bucket[tagsKey] ?? Metric(min: value, max: value, count: 1, sum: value, tags: tags) - - if bucket[tagsKey] != nil { - let newMin = min(metric.min, value) - let newMax = max(metric.max, value) - let newSum = metric.sum + value - let count = metric.count + 1 - - metric = Metric(min: newMin, max: newMax, count: count, sum: newSum, tags: tags) - } - - bucket[tagsKey] = metric - metricBuckets[exportKey] = bucket - } - } - - @objc func serialize() -> [String: [[String: Any]]] { - var returnValue: [String: [[String: Any]]] = [:] - - lock.synchronized { - - for (exportKey, bucket) in metricBuckets { - - var metrics: [[String: Any]] = [] - for (_, metric) in bucket { - var dict: [String: Any] = [ - "min": metric.min, - "max": metric.max, - "count": metric.count, - "sum": metric.sum - ] - if !metric.tags.isEmpty { - dict["tags"] = metric.tags - } - - metrics.append(dict) - } - - returnValue[exportKey] = metrics - } - } - - return returnValue - } -} diff --git a/Sources/Swift/Metrics/Metric.swift b/Sources/Swift/Metrics/Metric.swift deleted file mode 100644 index f2ab5294fd5..00000000000 --- a/Sources/Swift/Metrics/Metric.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation - -typealias Metric = MetricBase & MetricProtocol - -protocol MetricProtocol { - - var weight: UInt { get } - func serialize() -> [String] -} - -class MetricBase { - - let type: MetricType - let key: String - let unit: MeasurementUnit - let tags: [String: String] - - init(type: MetricType, key: String, unit: MeasurementUnit, tags: [String: String]) { - self.type = type - self.key = key - self.unit = unit - self.tags = tags - } -} - -enum MetricType: Character { - case counter = "c" - case gauge = "g" - case distribution = "d" - case set = "s" -} diff --git a/Sources/Swift/Metrics/MetricsAggregator.swift b/Sources/Swift/Metrics/MetricsAggregator.swift deleted file mode 100644 index 08e8661f633..00000000000 --- a/Sources/Swift/Metrics/MetricsAggregator.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -protocol MetricsAggregator { - func increment(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) - - func gauge(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) - - func distribution(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) - - func set(key: String, value: UInt, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) - - func flush(force: Bool) - func close() -} - -extension Dictionary where Key == String, Value == String { - func getMetricsTagsKey() -> String { - // It's important to sort the tags in order to - // obtain the same bucket key. - return self.sorted(by: { $0.key < $1.key }).map({ "\($0.key)=\($0.value)" }).joined(separator: ",") - } -} - -class NoOpMetricsAggregator: MetricsAggregator { - func increment(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) { - // empty on purpose - } - - func gauge(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) { - // empty on purpose - } - - func distribution(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) { - // empty on purpose - } - - func set(key: String, value: UInt, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) { - // empty on purpose - } - - func flush(force: Bool) { - // empty on purpose - } - - func close() { - // empty on purpose - } -} diff --git a/Sources/Swift/Metrics/SentryMetricsAPI.swift b/Sources/Swift/Metrics/SentryMetricsAPI.swift deleted file mode 100644 index d4a2f856f22..00000000000 --- a/Sources/Swift/Metrics/SentryMetricsAPI.swift +++ /dev/null @@ -1,148 +0,0 @@ -@_implementationOnly import _SentryPrivate -import Foundation - -@objc protocol SentryMetricsAPIDelegate: AnyObject { - - func getDefaultTagsForMetrics() -> [String: String] - - func getCurrentSpan() -> Span? - - // We don't want to add the LocalMetricsAggregator to the SpanProtocol - // because it would be public then. Exposing the LocalMetricsAggregator - // on the Span internally in Swift is a bit tricky, so we ask the - // delegate written in ObjC for it. - func getLocalMetricsAggregator(span: Span) -> LocalMetricsAggregator? -} - -/// Using SentryBeforeEmitMetricCallback of SentryDefines.h leads to compiler errors because of -/// Swift to ObjC interoperability. Defining the callback again in Swift with the same signature is a workaround. -typealias BeforeEmitMetricCallback = (String, [String: String]) -> Bool - -@objc public class SentryMetricsAPI: NSObject { - - private let aggregator: MetricsAggregator - private let currentDate: SentryCurrentDateProvider - - private weak var delegate: SentryMetricsAPIDelegate? - - @objc init(enabled: Bool, client: SentryMetricsClient, currentDate: SentryCurrentDateProvider, dispatchQueue: SentryDispatchQueueWrapper, random: SentryRandomProtocol, beforeEmitMetric: BeforeEmitMetricCallback?) { - - self.currentDate = currentDate - - if enabled { - self.aggregator = BucketMetricsAggregator(client: client, currentDate: currentDate, dispatchQueue: dispatchQueue, random: random, beforeEmitMetric: beforeEmitMetric ?? { _, _ in true }) - } else { - self.aggregator = NoOpMetricsAggregator() - } - } - - @objc func setDelegate(_ delegate: SentryMetricsAPIDelegate?) { - self.delegate = delegate - } - - /// Emits a Counter metric. - /// - /// - Parameter key: A unique key identifying the metric. - /// - Parameter value: The value to be added. - /// - Parameter unit: The value for the metric see `MeasurementUnit`. - /// - Parameter tags: Tags to associate with the metric. - @objc public func increment(key: String, value: Double = 1.0, unit: MeasurementUnit = .none, tags: [String: String] = [:]) { - let mergedTags = mergeDefaultTagsInto(tags: tags) - - aggregator.increment(key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: getLocalMetricsAggregator()) - } - - /// Emits a Gauge metric. - /// - /// - Parameter key: A unique key identifying the metric. - /// - Parameter value: The value to be added. - /// - Parameter unit: The value for the metric see `MeasurementUnit`. - /// - Parameter tags: Tags to associate with the metric. - @objc - public func gauge(key: String, value: Double, unit: MeasurementUnit = .none, tags: [String: String] = [:]) { - let mergedTags = mergeDefaultTagsInto(tags: tags) - aggregator.gauge(key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: getLocalMetricsAggregator()) - } - - /// Emits a Distribution metric. - /// - /// - Parameter key: A unique key identifying the metric. - /// - Parameter value: The value to be added. - /// - Parameter unit: The value for the metric see `MeasurementUnit`. - /// - Parameter tags: Tags to associate with the metric. - @objc - public func distribution(key: String, value: Double, unit: MeasurementUnit = .none, tags: [String: String] = [:]) { - let mergedTags = mergeDefaultTagsInto(tags: tags) - aggregator.distribution(key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: getLocalMetricsAggregator()) - } - - /// Emits a Set metric. - /// - /// - Parameter key: A unique key identifying the metric. - /// - Parameter value: The value to be added. - /// - Parameter unit: The value for the metric see `MeasurementUnit`. - /// - Parameter tags: Tags to associate with the metric. - @objc - public func set(key: String, value: String, unit: MeasurementUnit = .none, tags: [String: String] = [:]) { - let mergedTags = mergeDefaultTagsInto(tags: tags) - let crc32 = sentry_crc32ofString(value) - - aggregator.set(key: key, value: crc32, unit: unit, tags: mergedTags, localMetricsAggregator: getLocalMetricsAggregator()) - } - - /// Measures how long it takes to run the given closure by emitting a distribution metric in seconds. - /// - /// - Note: This method also creates a child span with the operation `metric.timing` and the - /// description `key` if a span is bound to the scope. - /// - /// - Parameter key: A unique key identifying the metric. - /// - Parameter tags: Tags to associate with the metric. - public func timing(key: String, tags: [String: String] = [:], _ closure: () throws -> T) rethrows -> T { - - guard let currentSpan = delegate?.getCurrentSpan() else { - return try closure() - } - - let span = currentSpan.startChild(operation: "metric.timing", description: key) - let aggregator = delegate?.getLocalMetricsAggregator(span: span) - - let mergedTags = mergeDefaultTagsInto(tags: tags) - for tag in mergedTags { - span.setTag(value: tag.value, key: tag.key) - } - - defer { - span.finish() - if let timestamp = span.timestamp, let startTimestamp = span.startTimestamp { - let duration = timestamp.timeIntervalSince(startTimestamp) - - self.aggregator.distribution(key: key, value: duration, unit: MeasurementUnitDuration.second, tags: mergedTags, localMetricsAggregator: aggregator) - } - } - - return try closure() - } - - @objc public func close() { - aggregator.close() - delegate = nil - } - - @objc public func flush() { - aggregator.flush(force: true) - } - - /// Merges the default tags into the passed tags. If there are duplicates the method keeps the passed in tags and discards the default tags. - private func mergeDefaultTagsInto(tags: [String: String]) -> [String: String] { - let defaultTags = delegate?.getDefaultTagsForMetrics() ?? [:] - return tags.merging(defaultTags) { (tagValue, _) in tagValue } - } - - private func getLocalMetricsAggregator() -> LocalMetricsAggregator? { - if let currentSpan = delegate?.getCurrentSpan() { - return delegate?.getLocalMetricsAggregator(span: currentSpan) - } - return nil - } - -} diff --git a/Sources/Swift/Metrics/SentryMetricsClient.swift b/Sources/Swift/Metrics/SentryMetricsClient.swift deleted file mode 100644 index 7d2dc25495e..00000000000 --- a/Sources/Swift/Metrics/SentryMetricsClient.swift +++ /dev/null @@ -1,19 +0,0 @@ -@_implementationOnly import _SentryPrivate -import Foundation - -@objc class SentryMetricsClient: NSObject { - - /// Exposing envelopes to Swift code is challenging because the - /// SentryEnvelope.h is part of the podspec SentryHybridPublic, which causes - /// problems. As the envelope logic is simple, we keep it in ObjC and do the - /// rest in Swift. - private let client: SentryStatsdClient - - @objc init(client: SentryStatsdClient) { - self.client = client - } - - func capture(flushableBuckets: [BucketTimestamp: [Metric]]) { - client.captureStatsdEncodedData(encodeToStatsd(flushableBuckets: flushableBuckets)) - } -} diff --git a/Sources/Swift/Metrics/SetMetric.swift b/Sources/Swift/Metrics/SetMetric.swift deleted file mode 100644 index bc6116709aa..00000000000 --- a/Sources/Swift/Metrics/SetMetric.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -class SetMetric: Metric { - - private var set: Set - var weight: UInt { - return UInt(set.count) - } - - init(first: UInt, key: String, unit: MeasurementUnit, tags: [String: String]) { - set = [first] - super.init(type: .set, key: key, unit: unit, tags: tags) - } - - func add(value: UInt) { - set.insert(value) - } - - func serialize() -> [String] { - return set.map { "\($0)" } - } -} diff --git a/Tests/SentryTests/Helper/SentryEnabledFeaturesBuilderTests.swift b/Tests/SentryTests/Helper/SentryEnabledFeaturesBuilderTests.swift index 0c27d694632..1fb4f4547df 100644 --- a/Tests/SentryTests/Helper/SentryEnabledFeaturesBuilderTests.swift +++ b/Tests/SentryTests/Helper/SentryEnabledFeaturesBuilderTests.swift @@ -15,7 +15,6 @@ final class SentryEnabledFeaturesBuilderTests: XCTestCase { options.enablePerformanceV2 = true options.enableTimeToFullDisplayTracing = true options.swiftAsyncStacktraces = true - options.enableMetrics = true #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) options.enableAppLaunchProfiling = true @@ -33,7 +32,6 @@ final class SentryEnabledFeaturesBuilderTests: XCTestCase { XCTAssert(features.contains("performanceV2")) XCTAssert(features.contains("timeToFullDisplayTracing")) XCTAssert(features.contains("swiftAsyncStacktraces")) - XCTAssert(features.contains("metrics")) #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) XCTAssert(features.contains("appLaunchProfiling")) diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 60ce83c389d..8a638ce39a5 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -1039,130 +1039,6 @@ class SentryHubTests: XCTestCase { ])) } - func testInitHubWithDefaultOptions_DoesNotEnableMetrics() { - let sut = fixture.getSut() - - sut.metrics.increment(key: "key") - sut.close() - - XCTAssertEqual(self.fixture.client.captureEnvelopeInvocations.count, 0) - } - - func testMetrics_IncrementOneValue() throws { - let options = fixture.options - options.enableMetrics = true - let sut = fixture.getSut(options) - - sut.metrics.increment(key: "key") - sut.flush(timeout: 1.0) - - let client = self.fixture.client - XCTAssertEqual(client.captureEnvelopeInvocations.count, 1) - - let envelope = try XCTUnwrap(client.captureEnvelopeInvocations.first) - XCTAssertNotNil(envelope.header.eventId) - - // We only check if it's an envelope with a statsd envelope item. - // We validate the contents of the envelope in SentryMetricsClientTests - XCTAssertEqual(envelope.items.count, 1) - let envelopeItem = try XCTUnwrap(envelope.items.first) - XCTAssertEqual(envelopeItem.header.type, SentryEnvelopeItemTypeStatsd) - XCTAssertEqual(envelopeItem.header.contentType, "application/octet-stream") - } - - func testAddIncrementMetric_GetsLocalMetricsAggregatorFromCurrentSpan() throws { - let options = fixture.options - options.enableMetrics = true - let sut = fixture.getSut(options) - - let span = sut.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation, bindToScope: true) - let tracer = try XCTUnwrap(span as? SentryTracer) - - sut.metrics.increment(key: "key") - - let aggregator = tracer.getLocalMetricsAggregator() - - let metricsSummary = aggregator.serialize() - XCTAssertEqual(metricsSummary.count, 1) - - let bucket = try XCTUnwrap(metricsSummary["c:key"]) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - XCTAssertEqual(metric["min"] as? Double, 1.0) - XCTAssertEqual(metric["max"] as? Double, 1.0) - XCTAssertEqual(metric["count"] as? Int, 1) - XCTAssertEqual(metric["sum"] as? Double, 1.0) - } - - func testAddIncrementMetric_AddsDefaultTags() throws { - let options = fixture.options - options.releaseName = "release1" - options.environment = "test" - options.enableMetrics = true - let sut = fixture.getSut(options) - - let span = sut.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation, bindToScope: true) - let tracer = try XCTUnwrap(span as? SentryTracer) - - sut.metrics.increment(key: "key", tags: ["my": "tag", "release": "overwritten"]) - - let aggregator = tracer.getLocalMetricsAggregator() - - let metricsSummary = aggregator.serialize() - XCTAssertEqual(metricsSummary.count, 1) - - let bucket = try XCTUnwrap(metricsSummary["c:key"]) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - XCTAssertEqual(metric["tags"] as? [String: String], ["my": "tag", "release": "overwritten", "environment": options.environment]) - } - - func testAddIncrementMetric_ReleaseNameNil() throws { - let options = fixture.options - options.releaseName = nil - options.enableMetrics = true - let sut = fixture.getSut(options) - - let span = sut.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation, bindToScope: true) - let tracer = try XCTUnwrap(span as? SentryTracer) - - sut.metrics.increment(key: "key", tags: ["my": "tag"]) - - let aggregator = tracer.getLocalMetricsAggregator() - - let metricsSummary = aggregator.serialize() - XCTAssertEqual(metricsSummary.count, 1) - - let bucket = try XCTUnwrap(metricsSummary["c:key"]) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - XCTAssertEqual(metric["tags"] as? [String: String], ["my": "tag", "environment": options.environment]) - } - - func testAddIncrementMetric_DefaultTagsDisabled() throws { - let options = fixture.options - options.releaseName = "release1" - options.environment = "test" - options.enableMetrics = true - options.enableDefaultTagsForMetrics = false - let sut = fixture.getSut(options) - - let span = sut.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation, bindToScope: true) - let tracer = try XCTUnwrap(span as? SentryTracer) - - sut.metrics.increment(key: "key", tags: ["my": "tag"]) - - let aggregator = tracer.getLocalMetricsAggregator() - - let metricsSummary = aggregator.serialize() - XCTAssertEqual(metricsSummary.count, 1) - - let bucket = try XCTUnwrap(metricsSummary["c:key"]) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - XCTAssertEqual(metric["tags"] as? [String: String], ["my": "tag"]) - } - private func captureEventEnvelope(level: SentryLevel) { let event = TestData.event event.level = level diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 14925b4a48f..f240fbfc961 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -1504,42 +1504,6 @@ - (void)testSpotlightUrl XCTAssertEqualObjects(options3.spotlightUrl, @"http://localhost:8969/stream"); } -- (void)testEnableMetrics -{ - [self testBooleanField:@"enableMetrics" defaultValue:NO]; -} - -- (void)testEnableDefaultTagsForMetrics -{ - [self testBooleanField:@"enableDefaultTagsForMetrics" defaultValue:YES]; -} - -- (void)testEnableSpanLocalMetricAggregation -{ - [self testBooleanField:@"enableSpanLocalMetricAggregation" defaultValue:YES]; -} - -- (void)testBeforeEmitMetric -{ - SentryBeforeEmitMetricCallback callback - = ^(NSString *_Nonnull key, NSDictionary *_Nonnull tags) { - // Use tags and key to silence unused compiler error - XCTAssertNotNil(key); - XCTAssertNotNil(tags); - return YES; - }; - SentryOptions *options = [self getValidOptions:@{ @"beforeEmitMetric" : callback }]; - - XCTAssertEqual(callback, options.beforeEmitMetric); -} - -- (void)testDefaultBeforeEmitMetric -{ - SentryOptions *options = [self getValidOptions:@{}]; - - XCTAssertNil(options.beforeEmitMetric); -} - #pragma mark - Private - (void)assertArrayEquals:(NSArray *)expected actual:(NSArray *)actual diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index 8e316fd975d..75239a90d05 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -847,47 +847,6 @@ class SentrySDKTests: XCTestCase { XCTAssert(mainThreadIntegration.installedInTheMainThread, "SDK is not being initialized in the main thread") } - - func testMetrics_IncrementOneValue() throws { - let options = fixture.options - options.enableMetrics = true - - SentrySDK.start(options: options) - let client = try XCTUnwrap(TestClient(options: options)) - let hub = SentryHub(client: client, andScope: nil) - SentrySDK.setCurrentHub(hub) - - SentrySDK.metrics.increment(key: "key") - SentrySDK.flush(timeout: 1.0) - - XCTAssertEqual(client.captureEnvelopeInvocations.count, 1) - - let envelope = try XCTUnwrap(client.captureEnvelopeInvocations.first) - XCTAssertNotNil(envelope.header.eventId) - - // We only check if it's an envelope with a statsd envelope item. - // We validate the contents of the envelope in SentryMetricsClientTests - XCTAssertEqual(envelope.items.count, 1) - let envelopeItem = try XCTUnwrap(envelope.items.first) - XCTAssertEqual(envelopeItem.header.type, SentryEnvelopeItemTypeStatsd) - XCTAssertEqual(envelopeItem.header.contentType, "application/octet-stream") - } - - func testMetrics_BeforeEmitMetricCallback_DiscardEveryThing() throws { - let options = fixture.options - options.enableMetrics = true - options.beforeEmitMetric = { _, _ in false } - - SentrySDK.start(options: options) - let client = try XCTUnwrap(TestClient(options: options)) - let hub = SentryHub(client: client, andScope: nil) - SentrySDK.setCurrentHub(hub) - - SentrySDK.metrics.increment(key: "key") - SentrySDK.flush(timeout: 1.0) - - XCTAssertEqual(client.captureEnvelopeInvocations.count, 0) - } #if SENTRY_HAS_UIKIT diff --git a/Tests/SentryTests/Swift/Metrics/BucketMetricsAggregatorTests.swift b/Tests/SentryTests/Swift/Metrics/BucketMetricsAggregatorTests.swift deleted file mode 100644 index 7eb48087652..00000000000 --- a/Tests/SentryTests/Swift/Metrics/BucketMetricsAggregatorTests.swift +++ /dev/null @@ -1,450 +0,0 @@ -@testable import _SentryPrivate -@testable import Sentry -import SentryTestUtils -import XCTest - -final class BucketMetricsAggregatorTests: XCTestCase { - - private func getSut(totalMaxWeight: UInt = 4, flushShift: Double = 0.0, dispatchQueue: SentryDispatchQueueWrapper = TestSentryDispatchQueueWrapper()) throws -> (BucketMetricsAggregator, TestCurrentDateProvider, TestMetricsClient) { - let currentDate = TestCurrentDateProvider() - let metricsClient = try TestMetricsClient() - let random = TestRandom(value: flushShift) - - return (BucketMetricsAggregator(client: metricsClient, currentDate: currentDate, dispatchQueue: dispatchQueue, random: random, totalMaxWeight: totalMaxWeight, flushInterval: 10.0, flushTolerance: 1.0), currentDate, metricsClient) - } - - func testSameMetricAggregated_WhenInSameBucket() throws { - let (sut, currentDate, metricsClient) = try getSut() - - sut.distribution( key: "key", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - currentDate.setDate(date: currentDate.date().addingTimeInterval(9.99)) - sut.distribution(key: "key", value: 1.1, unit: MeasurementUnitDuration.day, tags: [:]) - - sut.flush(force: true) - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets[currentDate.bucketTimestamp]) - XCTAssertEqual(bucket.count, 1) - let counterMetric = try XCTUnwrap(bucket.first as? DistributionMetric) - - XCTAssertEqual(counterMetric.key, "key") - XCTAssert(counterMetric.serialize().contains("1.0")) - XCTAssert(counterMetric.serialize().contains("1.1")) - XCTAssertEqual(counterMetric.unit.unit, MeasurementUnitDuration.day.unit) - XCTAssertEqual(counterMetric.tags, [:]) - } - - func testFlushShift_MetricsUsuallyInSameBucket_AreInDifferent() throws { - let (sut, currentDate, metricsClient) = try getSut(totalMaxWeight: 100, flushShift: 0.1) - - sut.gauge(key: "key", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - currentDate.setDate(date: currentDate.date().addingTimeInterval( 9.99)) - sut.gauge(key: "key", value: -1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - // Not flushing yet - currentDate.setDate(date: currentDate.date().addingTimeInterval( 1.0)) - sut.flush(force: false) - XCTAssertEqual(metricsClient.captureInvocations.count, 0) - - // This ends up in a different bucket - sut.gauge(key: "key", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - // Now we pass the flush shift threshold - currentDate.setDate(date: currentDate.date().addingTimeInterval( 0.01)) - sut.flush(force: false) - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let previousBucketTimestamp = currentDate.bucketTimestamp - 10 - let bucket = try XCTUnwrap(buckets[previousBucketTimestamp]) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first as? GaugeMetric) - - XCTAssertEqual(metric.key, "key") - XCTAssertEqual(metric.serialize(), ["-1.0", "-1.0", "1.0", "0.0", "2"]) - XCTAssertEqual(metric.unit.unit, MeasurementUnitDuration.day.unit) - XCTAssertEqual(metric.tags, [:]) - } - - func testDifferentMetrics_NotInSameBucket() throws { - let (sut, currentDate, metricsClient) = try getSut() - - sut.set( key: "key1", value: 1, unit: MeasurementUnitDuration.day, tags: ["some": "tag", "and": "another-one"]) - sut.set(key: "key2", value: 2, unit: MeasurementUnitDuration.day, tags: ["and": "another-one", "some": "tag"]) - - sut.flush(force: true) - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets[currentDate.bucketTimestamp]) - XCTAssertEqual(bucket.count, 2) - - let metric1 = try XCTUnwrap(bucket.first { $0.key == "key1" } as? SetMetric) - XCTAssertEqual(metric1.key, "key1") - XCTAssertEqual(metric1.serialize(), ["1"]) - XCTAssertEqual(metric1.unit.unit, MeasurementUnitDuration.day.unit) - XCTAssertEqual(metric1.tags, ["some": "tag", "and": "another-one"]) - - let metric2 = try XCTUnwrap(bucket.first { $0.key == "key2" } as? SetMetric) - XCTAssertEqual(metric2.key, "key2") - XCTAssertEqual(metric2.serialize(), ["2"]) - XCTAssertEqual(metric2.unit.unit, MeasurementUnitDuration.day.unit) - XCTAssertEqual(metric2.tags, ["some": "tag", "and": "another-one"]) - } - - func testSameMetricDifferentTag_NotInSameBucket() throws { - let (sut, currentDate, metricsClient) = try getSut() - - sut.increment(key: "key", value: 1.0, unit: MeasurementUnitDuration.day, tags: ["some": "tag"]) - sut.increment(key: "key", value: 2.0, unit: MeasurementUnitDuration.day, tags: ["some": "other-tag"]) - - sut.flush(force: true) - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets[currentDate.bucketTimestamp]) - XCTAssertEqual(bucket.count, 2) - - let counterMetric1 = try XCTUnwrap(bucket.first { $0.tags == ["some": "tag"] } as? CounterMetric) - XCTAssertEqual(counterMetric1.key, "key") - XCTAssertEqual(counterMetric1.serialize(), ["1.0"]) - XCTAssertEqual(counterMetric1.unit.unit, MeasurementUnitDuration.day.unit) - XCTAssertEqual(counterMetric1.tags, ["some": "tag"]) - - let counterMetric2 = try XCTUnwrap(bucket.first { $0.tags == ["some": "other-tag"] } as? CounterMetric) - XCTAssertEqual(counterMetric2.key, "key") - XCTAssertEqual(counterMetric2.serialize(), ["2.0"]) - XCTAssertEqual(counterMetric2.unit.unit, MeasurementUnitDuration.day.unit) - XCTAssertEqual(counterMetric2.tags, ["some": "other-tag"]) - } - - func testSameMetricNotAggregated_WhenNotInSameBucket() throws { - let (sut, currentDate, metricsClient) = try getSut(totalMaxWeight: 5) - - sut.increment(key: "key", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - currentDate.setDate(date: currentDate.date().addingTimeInterval( 10.0)) - sut.increment(key: "key", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - sut.flush(force: true) - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - XCTAssertEqual(buckets.count, 2) - - let bucket1 = try XCTUnwrap(buckets.values.first) - XCTAssertEqual(bucket1.count, 1) - let counterMetric1 = try XCTUnwrap(bucket1.first as? CounterMetric) - - XCTAssertEqual(counterMetric1.key, "key") - XCTAssertEqual(counterMetric1.serialize(), ["1.0"]) - XCTAssertEqual(counterMetric1.unit.unit, MeasurementUnitDuration.day.unit) - XCTAssertEqual(counterMetric1.tags, [:]) - - let bucket2 = try XCTUnwrap(Array(buckets.values).last) - let counterMetric2 = try XCTUnwrap(bucket2.first as? CounterMetric) - - XCTAssertEqual(counterMetric2.key, "key") - XCTAssertEqual(counterMetric2.serialize(), ["1.0"]) - XCTAssertEqual(counterMetric2.unit.unit, MeasurementUnitDuration.day.unit) - XCTAssertEqual(counterMetric2.tags, [:]) - } - - func testCallFlushWhenOverweight() throws { - let (sut, _, metricsClient) = try getSut(totalMaxWeight: 3, dispatchQueue: SentryDispatchQueueWrapper()) - - let expectation = expectation(description: "Before capture block called") - metricsClient.afterRecordingCaptureInvocationBlock = { - XCTAssertFalse(Thread.isMainThread, "Flush must be called on a background thread, but was called on the main thread.") - expectation.fulfill() - } - - sut.increment(key: "key1", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - sut.increment(key: "key2", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - wait(for: [expectation], timeout: 1.0) - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - } - - func testConvenienceInit_SetsCorrectMaxWeight() throws { - let metricsClient = try TestMetricsClient() - let sut = BucketMetricsAggregator(client: metricsClient, currentDate: TestCurrentDateProvider(), dispatchQueue: TestSentryDispatchQueueWrapper(), random: SentryRandom()) - - for i in 0..<998 { - sut.increment(key: "key\(i)", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - } - - // Total weight is now 999 because the bucket counts for one - // So nothing should be sent - XCTAssertEqual(metricsClient.captureInvocations.count, 0) - - // Now we pass the 1000 threshold - sut.increment(key: "another key", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - } - - func testFlushOnlyWhenNeeded() throws { - let (sut, currentDate, metricsClient) = try getSut(totalMaxWeight: 5) - - sut.increment(key: "key1", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - sut.flush(force: false) - XCTAssertEqual(metricsClient.captureInvocations.invocations.count, 0) - - currentDate.setDate(date: currentDate.date().addingTimeInterval( 9.99)) - sut.flush(force: false) - XCTAssertEqual(metricsClient.captureInvocations.invocations.count, 0) - - currentDate.setDate(date: currentDate.date().addingTimeInterval( 0.01)) - sut.increment(key: "key2", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - sut.flush(force: false) - let buckets1 = try XCTUnwrap(metricsClient.captureInvocations.first) - XCTAssertEqual(buckets1.count, 1) - XCTAssertEqual(buckets1.values.count, 1) - - // Key2 wasn't flushed. We increment it to 2.0 - sut.increment( key: "key2", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - // The weight should be 2 now, so we need to add 3 more to trigger a flush - sut.increment(key: "key3", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - sut.increment(key: "key4", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - sut.increment(key: "key5", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - XCTAssertEqual(metricsClient.captureInvocations.count, 2) - let buckets2 = try XCTUnwrap(try XCTUnwrap(metricsClient.captureInvocations.invocations.element(at: 1))) - - XCTAssertEqual(buckets2.count, 1) - let bucket = try XCTUnwrap(buckets2.first) - - // All 4 metrics should be in the bucket - XCTAssertEqual(bucket.value.count, 4) - - // Check that key2 was incremented - let counterMetric = try XCTUnwrap(bucket.value.first { $0.key == "key2" } as? CounterMetric) - XCTAssertEqual(counterMetric.serialize(), ["2.0"]) - } - - func testWeightWithMultipleDifferent() throws { - let (sut, currentDate, metricsClient) = try getSut(totalMaxWeight: 4) - - sut.distribution(key: "key", value: 1.0, unit: .none, tags: [:]) - sut.distribution(key: "key", value: 1.0, unit: .none, tags: [:]) - - // Weight should be 3, no flush yet - sut.flush(force: false) - XCTAssertEqual(metricsClient.captureInvocations.count, 0) - - // Time passed, must flush - currentDate.setDate(date: currentDate.date().addingTimeInterval(10.0)) - sut.flush(force: false) - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - - sut.distribution(key: "key", value: 1.0, unit: .none, tags: [:]) - sut.distribution(key: "key", value: 1.0, unit: .none, tags: [:]) - sut.distribution(key: "key", value: 1.0, unit: .none, tags: [:]) - - // Reached overweight, must flush - XCTAssertEqual(metricsClient.captureInvocations.count, 2) - } - - func testInitStartsRepeatingTimer() throws { - let currentDate = TestCurrentDateProvider() - let metricsClient = try TestMetricsClient() - - // Start the flush timer with very high interval - let sut = BucketMetricsAggregator(client: metricsClient, currentDate: currentDate, dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), totalMaxWeight: 1_000, flushInterval: 0.000001, flushTolerance: 0.0) - - SentryLog.withOutLogs { - let expectation = expectation(description: "Adding metrics") - expectation.expectedFulfillmentCount = 100 - - // Keep adding metrics async so the flush timer has a few chances to - // send metrics - for i in 0..<100 { - DispatchQueue.global().async { - sut.increment(key: "key\(i)", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - currentDate.setDate(date: currentDate.date().addingTimeInterval(10.0)) - - expectation.fulfill() - } - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertGreaterThan(metricsClient.captureInvocations.count, 0, "Repeating flush timer should send some metrics.") - } - } - - func testClose_InvalidatesTimer() throws { - let currentDate = TestCurrentDateProvider() - let metricsClient = try TestMetricsClient() - - // Start the flush timer with very high interval - let sut = BucketMetricsAggregator(client: metricsClient, currentDate: currentDate, dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), totalMaxWeight: 1_000, flushInterval: 0.000001, flushTolerance: 0.0) - - sut.close() - - let expectation = expectation(description: "Adding metrics") - expectation.expectedFulfillmentCount = 100 - - // Keep adding metrics async so the flush timer has a few chances to - // send metrics - for i in 0..<100 { - DispatchQueue.global().async { - sut.increment(key: "key\(i)", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - currentDate.setDate(date: currentDate.date().addingTimeInterval(10.0)) - - expectation.fulfill() - } - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertEqual(metricsClient.captureInvocations.count, 0, "No metrics should be sent cause the flush timer should be cancelled.") - } - - func testFlushCalledOnCallingThread() throws { - let (sut, _, metricsClient) = try getSut(dispatchQueue: SentryDispatchQueueWrapper()) - - let expectation = expectation(description: "Before capture block called") - metricsClient.afterRecordingCaptureInvocationBlock = { - XCTAssertEqual(Thread.isMainThread, true, "Flush must be called on the calling thread, but was called on a background thread.") - expectation.fulfill() - } - - sut.increment(key: "key1", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - sut.increment(key: "key2", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - sut.flush(force: true) - - wait(for: [expectation], timeout: 1.0) - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - } - - func testCloseCallsFlush() throws { - let (sut, _, metricsClient) = try getSut() - - sut.increment(key: "key1", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - sut.increment(key: "key2", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - sut.close() - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - } - - func testWriteMultipleMetricsInParallel_NonForceFlush_DoesNotCrash() throws { - let currentDate = TestCurrentDateProvider() - let metricsClient = try TestMetricsClient() - - // Start the flush timer with very high interval - let sut = BucketMetricsAggregator(client: metricsClient, currentDate: currentDate, dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), totalMaxWeight: 1_000, flushInterval: 0.001, flushTolerance: 0.0) - - testConcurrentModifications(asyncWorkItems: 10, writeLoopCount: 1_000, writeWork: { i in - sut.increment(key: "key\(i)", value: 1.1, unit: .none, tags: ["some": "tag"]) - currentDate.setDate(date: currentDate.date().addingTimeInterval(0.01)) - }) - - sut.close() - } - - func testWriteMultipleMetricsInParallel_ForceFlush_DoesNotCrash() throws { - let (sut, _, _) = try getSut(totalMaxWeight: 5) - - testConcurrentModifications(asyncWorkItems: 10, writeLoopCount: 1_000, writeWork: { i in - sut.increment(key: "key\(i)", value: 1.1, unit: .none, tags: ["some": "tag"]) - sut.gauge(key: "key\(i)", value: 1.1, unit: .none, tags: ["some": "tag"]) - sut.distribution(key: "key\(i)", value: 1.1, unit: .none, tags: ["some": "tag"]) - sut.set(key: "key\(i)", value: 11, unit: .none, tags: ["some": "tag"]) - }) - } - - func testCounterMetricGetsForwardedToLocalAggregator() throws { - let localMetricsAggregator = LocalMetricsAggregator() - let (sut, _, _) = try getSut() - - sut.increment(key: "key1", value: 1.0, unit: MeasurementUnitDuration.day, tags: ["some": "tag"], localMetricsAggregator: localMetricsAggregator) - - let serialized = localMetricsAggregator.serialize() - XCTAssertEqual(serialized.count, 1) - let bucket = try XCTUnwrap(serialized["c:key1@day"]) - - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - - XCTAssertEqual(metric["tags"] as? [String: String], ["some": "tag"]) - XCTAssertEqual(metric["min"] as? Double, 1.0) - XCTAssertEqual(metric["max"] as? Double, 1.0) - XCTAssertEqual(metric["count"] as? Int, 1) - XCTAssertEqual(metric["sum"] as? Double, 1.0) - } - - func testSetMetricOnlyForwardsAddedWeight() throws { - let localMetricsAggregator = LocalMetricsAggregator() - let (sut, _, _) = try getSut() - - sut.set(key: "key1", value: 1, unit: MeasurementUnitDuration.day, tags: ["some": "tag"], localMetricsAggregator: localMetricsAggregator) - // This one doesn't add new weight - sut.set(key: "key1", value: 1, unit: MeasurementUnitDuration.day, tags: ["some": "tag"], localMetricsAggregator: localMetricsAggregator) - - let serialized = localMetricsAggregator.serialize() - XCTAssertEqual(serialized.count, 1) - let bucket = try XCTUnwrap(serialized["s:key1@day"]) - - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - - XCTAssertEqual(metric["tags"] as? [String: String], ["some": "tag"]) - // When no weight added the value is 0.0 - XCTAssertEqual(metric["min"] as? Double, 0.0) - XCTAssertEqual(metric["max"] as? Double, 1.0) - XCTAssertEqual(metric["count"] as? Int, 2) - XCTAssertEqual(metric["sum"] as? Double, 1.0) - } - - func testBeforeEmitMetricCallback() throws { - let currentDate = TestCurrentDateProvider() - let metricsClient = try TestMetricsClient() - - let sut = BucketMetricsAggregator(client: metricsClient, currentDate: currentDate, dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), beforeEmitMetric: { key, tags in - if key == "key" { - return false - } - - if tags == ["my": "tag"] { - return false - } - - return true - }, totalMaxWeight: 1_000) - - // removed - sut.distribution( key: "key", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - // kept - sut.distribution(key: "key1", value: 1.0, unit: MeasurementUnitDuration.day, tags: [:]) - - // removed - sut.distribution(key: "key1", value: 1.0, unit: MeasurementUnitDuration.day, tags: ["my": "tag"]) - - sut.flush(force: true) - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets[currentDate.bucketTimestamp]) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first as? DistributionMetric) - - XCTAssertEqual(metric.key, "key1") - XCTAssertEqual(metric.tags.isEmpty, true) - } - -} diff --git a/Tests/SentryTests/Swift/Metrics/CounterMetricTests.swift b/Tests/SentryTests/Swift/Metrics/CounterMetricTests.swift deleted file mode 100644 index 1bb5b2e1889..00000000000 --- a/Tests/SentryTests/Swift/Metrics/CounterMetricTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -@testable import Sentry -import XCTest - -final class CounterMetricTests: XCTestCase { - - func testAddingValues() { - let sut = CounterMetric(first: 1.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - sut.add(value: 1.0) - sut.add(value: -1.0) - sut.add(value: 2.0) - - XCTAssertEqual(sut.serialize(), ["3.0"]) - } - - func testType() { - let sut = CounterMetric(first: 0.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.type, .counter) - } - - func testWeight() { - let sut = CounterMetric(first: 0.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.weight, 1) - - sut.add(value: 5.0) - sut.add(value: 5.0) - - // The weight stays the same - XCTAssertEqual(sut.weight, 1) - } - -} diff --git a/Tests/SentryTests/Swift/Metrics/DistributionMetricTests.swift b/Tests/SentryTests/Swift/Metrics/DistributionMetricTests.swift deleted file mode 100644 index cfbce775f58..00000000000 --- a/Tests/SentryTests/Swift/Metrics/DistributionMetricTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -@testable import Sentry -import XCTest - -final class DistributionMetricTests: XCTestCase { - - func testAddingValues() { - let sut = DistributionMetric(first: 1.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - sut.add(value: 0.0) - sut.add(value: -1.0) - sut.add(value: 2.0) - - XCTAssertEqual(sut.serialize(), ["1.0", "0.0", "-1.0", "2.0"]) - } - - func testType() { - let sut = DistributionMetric(first: 0.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.type, .distribution) - } - - func testWeight() { - let sut = DistributionMetric(first: 0.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.weight, 1) - - for _ in 0..<100 { - sut.add(value: 5.0) - } - - XCTAssertEqual(sut.weight, 101) - } - -} diff --git a/Tests/SentryTests/Swift/Metrics/EncodeMetricTests.swift b/Tests/SentryTests/Swift/Metrics/EncodeMetricTests.swift deleted file mode 100644 index 39f8921e981..00000000000 --- a/Tests/SentryTests/Swift/Metrics/EncodeMetricTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -@testable import Sentry -import SentryTestUtils -import XCTest - -final class EncodeMetricTests: XCTestCase { - - func testEncodeCounterMetricWithoutTags() { - let counterMetric = CounterMetric(first: 1.0, key: "app.start", unit: .none, tags: [:]) - - let data = encodeToStatsd(flushableBuckets: [12_345: [counterMetric]]) - - XCTAssertEqual(data.decodeStatsd(), "app.start@:1.0|c|T12345\n") - } - - func testEncodeGaugeMetricWithOneTag() { - let metric = GaugeMetric(first: 0.0, key: "app.start", unit: .none, tags: ["key": "value"]) - metric.add(value: 5.0) - metric.add(value: 4.0) - metric.add(value: 3.0) - metric.add(value: 2.0) - metric.add(value: 1.0) - - let data = encodeToStatsd(flushableBuckets: [12_345: [metric]]) - - XCTAssertEqual(data.decodeStatsd(), "app.start@:1.0:0.0:5.0:15.0:6|g|#key:value|T12345\n") - } - - func testEncodeDistributionMetricWithOutTags() { - let metric = DistributionMetric(first: 0.0, key: "app.start", unit: .none, tags: [:]) - metric.add(value: 5.12) - metric.add(value: 1.0) - - let data = encodeToStatsd(flushableBuckets: [12_345: [metric]]) - - XCTAssertEqual(data.decodeStatsd(), "app.start@:0.0:5.12:1.0|d|T12345\n") - } - - func testEncodeSetMetricWithOutTags() { - let metric = SetMetric(first: 0, key: "app.start", unit: .none, tags: [:]) - metric.add(value: 0) - metric.add(value: 1) - metric.add(value: 1) - - let data = encodeToStatsd(flushableBuckets: [12_345: [metric]]) - - let statsd = data.decodeStatsd() - XCTAssert(statsd.contains("app.start@:")) - XCTAssert(statsd.contains("|s|T12345\n")) - - // the set is unordered, so we have to check for both - XCTAssert(statsd.contains("1:0") || statsd.contains("0:1"), "statsd expected to contain either '1:0' or '0:1' for the set metric values") - } - - func testEncodeCounterMetricWithFractionalPart() { - let counterMetric = CounterMetric(first: 1.123456, key: "app.start", unit: MeasurementUnitDuration.second, tags: [:]) - - let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - - XCTAssertEqual(data.decodeStatsd(), "app.start@second:1.123456|c|T10234\n") - } - - func testEncodeCounterMetricWithOneTag() { - let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["key": "value"]) - - let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - - XCTAssertEqual(data.decodeStatsd(), "app.start@second:10.1|c|#key:value|T10234\n") - } - - func testEncodeCounterMetricWithTwoTags() { - let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["key1": "value1", "key2": "value2"]) - - let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]).decodeStatsd() - XCTAssert(data.hasPrefix("app.start@second:10.1|c|")) - XCTAssert(data.hasSuffix("|T10234\n")) - XCTAssert(data.contains("key1:value1")) - XCTAssert(data.contains("key2:value2")) - } - - func testEncodeCounterMetricWithKeyToSanitize() { - let counterMetric = CounterMetric(first: 10.1, key: "abyzABYZ09_/.-!@a#$Äa", unit: MeasurementUnitDuration.second, tags: [:]) - - let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - - XCTAssertEqual(data.decodeStatsd(), "abyzABYZ09__.-_a_a@second:10.1|c|T10234\n") - } - - func testEncodeCounterMetricWithTagKeyToSanitize() { - let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["abcABC123_-./äöü$%&abcABC123": "value"]) - - let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - - XCTAssertEqual(data.decodeStatsd(), "app.start@second:10.1|c|#abcABC123_-./abcABC123:value|T10234\n") - } - - func testEncodeCounterMetricWithTagValueToSanitize() { - let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["key": "abc\n\r\t|,\\123"]) - - let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - - XCTAssert(data.decodeStatsd().contains(#"abc\\n\\r\\t\\u{7c}\\u{2c}\\\\123"#)) - } - - func testEncodeCounterMetricWithUnitToSanitize() { - let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnit(unit: "abyzABYZ09_/.ä"), tags: [:]) - - let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - - XCTAssertEqual(data.decodeStatsd(), "app.start@abyzABYZ09_:10.1|c|T10234\n") - } -} - -private extension Data { - func decodeStatsd() -> String { - return String(data: self, encoding: .utf8) ?? "" - } -} diff --git a/Tests/SentryTests/Swift/Metrics/GaugeMetricTests.swift b/Tests/SentryTests/Swift/Metrics/GaugeMetricTests.swift deleted file mode 100644 index cd826a1fa84..00000000000 --- a/Tests/SentryTests/Swift/Metrics/GaugeMetricTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -@testable import Sentry -import XCTest - -final class GaugeMetricTests: XCTestCase { - - func testAddingValues() throws { - let sut = GaugeMetric(first: 1.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.serialize(), ["1.0", "1.0", "1.0", "1.0", "1"]) - - sut.add(value: 5.0) - sut.add(value: 4.0) - sut.add(value: 3.0) - sut.add(value: 2.0) - sut.add(value: 2.5) - sut.add(value: 1.0) - - XCTAssertEqual(sut.serialize(), [ - "1.0", // last - "1.0", // min - "5.0", // max - "18.5", // sum - "7" // count - ]) - } - - func testType() { - let sut = GaugeMetric(first: 1.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.type, .gauge) - } - - func testWeight() { - let sut = GaugeMetric(first: 1.0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.weight, 5) - - sut.add(value: 5.0) - sut.add(value: 5.0) - - // The weight stays the same - XCTAssertEqual(sut.weight, 5) - } -} diff --git a/Tests/SentryTests/Swift/Metrics/LocalMetricsAggregatorTests.swift b/Tests/SentryTests/Swift/Metrics/LocalMetricsAggregatorTests.swift deleted file mode 100644 index 9df8a839757..00000000000 --- a/Tests/SentryTests/Swift/Metrics/LocalMetricsAggregatorTests.swift +++ /dev/null @@ -1,110 +0,0 @@ -@testable import Sentry -import XCTest - -final class LocalMetricsAggregatorTests: XCTestCase { - - func testAddOneCounterMetric() throws { - let sut = LocalMetricsAggregator() - - sut.add(type: .counter, key: "key", value: 1.0, unit: MeasurementUnitDuration.second, tags: [:]) - - let serialized = sut.serialize() - XCTAssertEqual(serialized.count, 1) - let bucket = try XCTUnwrap(serialized["c:key@second"]) - - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - - XCTAssertNil(metric["tags"]) - XCTAssertEqual(metric["min"] as? Double, 1.0) - XCTAssertEqual(metric["max"] as? Double, 1.0) - XCTAssertEqual(metric["count"] as? Int, 1) - XCTAssertEqual(metric["sum"] as? Double, 1.0) - } - - func testAddTwoSameDistributionMetrics() throws { - let sut = LocalMetricsAggregator() - - sut.add(type: .distribution, key: "key", value: 1.0, unit: .none, tags: [:]) - sut.add(type: .distribution, key: "key", value: 1.1, unit: .none, tags: [:]) - - let serialized = sut.serialize() - XCTAssertEqual(serialized.count, 1) - let bucket = try XCTUnwrap(serialized["d:key"]) - - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - - XCTAssertNil(metric["tags"]) - XCTAssertEqual(metric["min"] as? Double, 1.0) - XCTAssertEqual(metric["max"] as? Double, 1.1) - XCTAssertEqual(metric["count"] as? Int, 2) - XCTAssertEqual(metric["sum"] as? Double, 2.1) - } - - func testAddTwoGaugeMetrics_WithDifferentTags() throws { - let sut = LocalMetricsAggregator() - - sut.add(type: .gauge, key: "key", value: 1.0, unit: MeasurementUnitDuration.second, tags: ["some0": "tag0"]) - sut.add(type: .gauge, key: "key", value: 10.0, unit: MeasurementUnitDuration.second, tags: ["some1": "tag1"]) - - let serialized = sut.serialize() - XCTAssertEqual(serialized.count, 1) - let bucket = try XCTUnwrap(serialized["g:key@second"]) - - XCTAssertEqual(bucket.count, 2) - let metric0 = try XCTUnwrap(bucket.first { $0.contains { $0.value as? [String: String] == ["some0": "tag0"] } }) - - XCTAssertEqual(metric0["min"] as? Double, 1.0) - XCTAssertEqual(metric0["max"] as? Double, 1.0) - XCTAssertEqual(metric0["count"] as? Int, 1) - XCTAssertEqual(metric0["sum"] as? Double, 1.0) - - let metric1 = try XCTUnwrap(bucket.first { $0.contains { $0.value as? [String: String] == ["some1": "tag1"] } }) - - XCTAssertEqual(metric1["min"] as? Double, 10.0) - XCTAssertEqual(metric1["max"] as? Double, 10.0) - XCTAssertEqual(metric1["count"] as? Int, 1) - XCTAssertEqual(metric1["sum"] as? Double, 10.0) - } - - func testAddTwoDifferentMetrics() throws { - let sut = LocalMetricsAggregator() - - sut.add(type: .gauge, key: "key", value: 1.0, unit: MeasurementUnitDuration.day, tags: ["some0": "tag0"]) - sut.add(type: .gauge, key: "key", value: 10.0, unit: MeasurementUnitDuration.second, tags: ["some1": "tag1"]) - sut.add(type: .gauge, key: "key", value: -10.0, unit: MeasurementUnitDuration.second, tags: ["some1": "tag1"]) - - let serialized = sut.serialize() - XCTAssertEqual(serialized.count, 2) - let dayBucket = try XCTUnwrap(serialized["g:key@day"]) - - XCTAssertEqual(dayBucket.count, 1) - let dayMetric = try XCTUnwrap(dayBucket.first) - XCTAssertEqual(dayMetric["min"] as? Double, 1.0) - XCTAssertEqual(dayMetric["max"] as? Double, 1.0) - XCTAssertEqual(dayMetric["count"] as? Int, 1) - XCTAssertEqual(dayMetric["sum"] as? Double, 1.0) - - let secondBucket = try XCTUnwrap(serialized["g:key@second"]) - XCTAssertEqual(secondBucket.count, 1) - let secondMetric = try XCTUnwrap(secondBucket.first) - XCTAssertEqual(secondMetric["min"] as? Double, -10.0) - XCTAssertEqual(secondMetric["max"] as? Double, 10.0) - XCTAssertEqual(secondMetric["count"] as? Int, 2) - XCTAssertEqual(secondMetric["sum"] as? Double, 0.0) - } - - func testWriteMultipleMetricsInParallel_DoesNotCrash() throws { - let sut = LocalMetricsAggregator() - - testConcurrentModifications(asyncWorkItems: 10, writeLoopCount: 100, writeWork: { i in - sut.add(type: .counter, key: "key\(i)", value: 1.1, unit: .none, tags: ["some": "tag"]) - sut.add(type: .gauge, key: "key\(i)", value: 1.1, unit: .none, tags: ["some": "tag"]) - sut.add(type: .distribution, key: "key\(i)", value: 1.1, unit: .none, tags: ["some": "tag"]) - sut.add(type: .set, key: "key\(i)", value: 1.1, unit: .none, tags: ["some": "tag"]) - }, readWork: { - XCTAssertNotNil(sut.serialize()) - }) - } -} diff --git a/Tests/SentryTests/Swift/Metrics/SentryMetricsAPITests.swift b/Tests/SentryTests/Swift/Metrics/SentryMetricsAPITests.swift deleted file mode 100644 index f90aa29fd5c..00000000000 --- a/Tests/SentryTests/Swift/Metrics/SentryMetricsAPITests.swift +++ /dev/null @@ -1,247 +0,0 @@ -import _SentryPrivate -@testable import Sentry -import SentryTestUtils -import XCTest - -final class SentryMetricsAPITests: XCTestCase { - - override func tearDown() { - super.tearDown() - clearTestState() - } - - private func getSut(enabled: Bool = true) throws -> (SentryMetricsAPI, TestMetricsClient, TestCurrentDateProvider, TestHub, Options) { - - let metricsClient = try TestMetricsClient() - let currentDate = TestCurrentDateProvider() - let sut = SentryMetricsAPI(enabled: enabled, client: metricsClient, currentDate: currentDate, dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), beforeEmitMetric: nil) - - SentryDependencyContainer.sharedInstance().dateProvider = currentDate - - let options = Options() - options.enableMetrics = true - - let testClient = try XCTUnwrap(TestClient(options: options)) - let testHub = TestHub(client: testClient, andScope: Scope()) - - sut.setDelegate(testHub as? SentryMetricsAPIDelegate) - - return (sut, metricsClient, currentDate, testHub, options) - } - - func testInitWithDisabled_AllOperationsAreNoOps() throws { - let (sut, metricsClient, _, _, _) = try getSut(enabled: false) - - sut.increment(key: "some", value: 1.0, unit: .none, tags: ["yeah": "sentry"]) - sut.gauge(key: "some", value: 1.0, unit: .none, tags: ["yeah": "sentry"]) - sut.distribution(key: "some", value: 1.0, unit: .none, tags: ["yeah": "sentry"]) - sut.set(key: "some", value: "value", unit: .none, tags: ["yeah": "sentry"]) - - sut.close() - - XCTAssertEqual(metricsClient.captureInvocations.count, 0) - } - - func testIncrement_EmitsIncrementMetric() throws { - let (sut, metricsClient, _, _, _) = try getSut() - let delegate = TestSentryMetricsAPIDelegate() - sut.setDelegate(delegate) - - sut.increment(key: "key", value: 1.0, unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"]) - - sut.flush() - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets.first?.value) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first as? CounterMetric) - - XCTAssertEqual(metric.key, "key") - XCTAssert(metric.serialize().contains("1.0")) - XCTAssertEqual(metric.unit.unit, MeasurementUnitFraction.percent.unit) - XCTAssertEqual(metric.tags, ["yeah": "sentry", "some": "tag"]) - } - - func testGauge_EmitsGaugeMetric() throws { - let (sut, metricsClient, _, _, _) = try getSut() - let delegate = TestSentryMetricsAPIDelegate() - sut.setDelegate(delegate) - - sut.gauge(key: "key", value: 1.0, unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"]) - - sut.flush() - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets.first?.value) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first as? GaugeMetric) - - XCTAssertEqual(metric.key, "key") - XCTAssertEqual(metric.serialize(), ["1.0", "1.0", "1.0", "1.0", "1"]) - XCTAssertEqual(metric.unit.unit, MeasurementUnitFraction.percent.unit) - XCTAssertEqual(metric.tags, ["yeah": "sentry", "some": "tag"]) - } - - func testDistribution_EmitsDistributionMetric() throws { - let (sut, metricsClient, _, _, _) = try getSut() - let delegate = TestSentryMetricsAPIDelegate() - sut.setDelegate(delegate) - - sut.distribution(key: "key", value: 1.0, unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"]) - sut.distribution(key: "key", value: 12.0, unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"]) - - sut.flush() - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets.first?.value) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first as? DistributionMetric) - - XCTAssertEqual(metric.key, "key") - XCTAssertEqual(metric.serialize(), ["1.0", "12.0"]) - XCTAssertEqual(metric.unit.unit, MeasurementUnitFraction.percent.unit) - XCTAssertEqual(metric.tags, ["yeah": "sentry", "some": "tag"]) - } - - func testSet_EmitsSetMetric() throws { - let (sut, metricsClient, _, _, _) = try getSut() - let delegate = TestSentryMetricsAPIDelegate() - sut.setDelegate(delegate) - - sut.set(key: "key", value: "value1", unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"]) - sut.set(key: "key", value: "value1", unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"]) - sut.set(key: "key", value: "value12", unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"]) - - sut.flush() - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets.first?.value) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first as? SetMetric) - - XCTAssertEqual(metric.key, "key") - XCTAssert(metric.serialize().contains("2445898635")) - XCTAssert(metric.serialize().contains("2725604442")) - XCTAssertEqual(metric.unit.unit, MeasurementUnitFraction.percent.unit) - XCTAssertEqual(metric.tags, ["yeah": "sentry", "some": "tag"]) - } - - func testTiming_WhenNoCurrentSpan_NoSpanCreatedAndNoMetricEmitted() throws { - let (sut, metricsClient, _, _, _) = try getSut() - let delegate = TestSentryMetricsAPIDelegate() - sut.setDelegate(delegate) - - let errorMessage = "It's broken" - do { - try sut.timing(key: "key") { - throw MetricsAPIError.runtimeError(errorMessage) - } - } catch MetricsAPIError.runtimeError(let actualErrorMessage) { - XCTAssertEqual(actualErrorMessage, errorMessage) - } - - XCTAssertEqual(metricsClient.captureInvocations.count, 0) - } - - func testTiming_WithCurrentSpan_CapturesGaugeMetric() throws { - let (sut, metricsClient, currentDate, testHub, options) = try getSut() - - let transaction = testHub.startTransaction(name: "hello", operation: "operation", bindToScope: true) - - let errorMessage = "It's broken" - do { - try sut.timing(key: "key", tags: ["some": "tag"]) { - currentDate.setDate(date: currentDate.date().addingTimeInterval(1.0)) - throw MetricsAPIError.runtimeError(errorMessage) - } - } catch MetricsAPIError.runtimeError(let actualErrorMessage) { - XCTAssertEqual(actualErrorMessage, errorMessage) - } - - sut.flush() - transaction.finish() - - XCTAssertEqual(metricsClient.captureInvocations.count, 1) - let buckets = try XCTUnwrap(metricsClient.captureInvocations.first) - - let bucket = try XCTUnwrap(buckets.first?.value) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first as? DistributionMetric) - - XCTAssertEqual(metric.key, "key") - XCTAssertEqual(metric.serialize(), ["1.0"]) - XCTAssertEqual(metric.unit.unit, MeasurementUnitDuration.second.unit) - XCTAssertEqual(metric.tags, ["some": "tag", "release": options.releaseName, "environment": options.environment]) - } - - func testTiming_WithCurrentSpan_CreatesSpanWithMetricsSummary() throws { - let (sut, _, currentDate, testHub, options) = try getSut() - - let transaction = testHub.startTransaction(name: "hello", operation: "operation", bindToScope: true) - - sut.timing(key: "key", tags: ["some": "tag"]) { - currentDate.setDate(date: currentDate.date().addingTimeInterval(1.0)) - } - - transaction.finish() - - XCTAssertEqual(testHub.capturedTransactionsWithScope.count, 1) - let serializedTransaction = try XCTUnwrap(testHub.capturedTransactionsWithScope.first?.transaction) - XCTAssertNotEqual(serializedTransaction.count, 0) - - let spans = try XCTUnwrap(serializedTransaction["spans"] as? [[String: Any]]) - XCTAssertEqual(spans.count, 1) - let span = try XCTUnwrap(spans.first) - - XCTAssertEqual(span["op"] as? String, "metric.timing") - XCTAssertEqual(span["description"] as? String, "key") - XCTAssertEqual(span["origin"] as? String, "manual") - XCTAssertEqual(span["start_timestamp"] as? Int, 978_307_200) - XCTAssertEqual(span["timestamp"] as? Int, 978_307_201) - XCTAssertEqual(span["parent_span_id"] as? String, transaction.spanId.sentrySpanIdString) - XCTAssertEqual(try XCTUnwrap(span["tags"] as? [String: String]), ["some": "tag", "release": options.releaseName, "environment": options.environment]) - - let metricsSummary = try XCTUnwrap(span["_metrics_summary"] as? [String: [[String: Any]]]) - XCTAssertEqual(metricsSummary.count, 1) - - let bucket = try XCTUnwrap(metricsSummary["d:key@second"] ) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - XCTAssertEqual(metric["min"] as? Double, 1.0) - XCTAssertEqual(metric["max"] as? Double, 1.0) - XCTAssertEqual(metric["count"] as? Int, 1) - XCTAssertEqual(metric["sum"] as? Double, 1.0) - } - - enum MetricsAPIError: Error { - case runtimeError(String) - } -} - -class TestSentryMetricsAPIDelegate: SentryMetricsAPIDelegate { - var currentSpan: SentrySpan? - - func getDefaultTagsForMetrics() -> [String: String] { - return ["some": "tag", "yeah": "not-taken"] - } - - func getLocalMetricsAggregator() -> Sentry.LocalMetricsAggregator? { - return currentSpan?.getLocalMetricsAggregator() - } - - func getCurrentSpan() -> Span? { - return currentSpan - } - - func getLocalMetricsAggregator(span: Span) -> Sentry.LocalMetricsAggregator? { - return (span as? SentrySpan)?.getLocalMetricsAggregator() - } -} diff --git a/Tests/SentryTests/Swift/Metrics/SentryMetricsClientTests.swift b/Tests/SentryTests/Swift/Metrics/SentryMetricsClientTests.swift deleted file mode 100644 index a2257301d5a..00000000000 --- a/Tests/SentryTests/Swift/Metrics/SentryMetricsClientTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -import _SentryPrivate -@testable import Sentry -import SentryTestUtils -import XCTest - -final class SentryMetricsClientTests: XCTestCase { - - func testCaptureMetricsWithCounterMetric() throws { - let testClient = try XCTUnwrap(TestClient(options: Options())) - - let sut = SentryMetricsClient(client: SentryStatsdClient(client: testClient)) - - let metric = CounterMetric(first: 0.0, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["sentry": "awesome"]) - let flushableBuckets: [BucketTimestamp: [Metric]] = [0: [metric]] - - let encodedMetricsData = encodeToStatsd(flushableBuckets: flushableBuckets) - - sut.capture(flushableBuckets: flushableBuckets) - - XCTAssertEqual(testClient.captureEnvelopeInvocations.count, 1) - - let envelope = try XCTUnwrap(testClient.captureEnvelopeInvocations.first) - XCTAssertNotNil(envelope.header.eventId) - - XCTAssertEqual(envelope.items.count, 1) - let envelopeItem = try XCTUnwrap(envelope.items.first) - XCTAssertEqual(envelopeItem.header.type, SentryEnvelopeItemTypeStatsd) - XCTAssertEqual(envelopeItem.header.contentType, "application/octet-stream") - XCTAssertEqual(envelopeItem.header.length, UInt(encodedMetricsData.count)) - XCTAssertEqual(envelopeItem.data, encodedMetricsData) - } - - func testCaptureMetricsWithNoMetrics() throws { - let testClient = try XCTUnwrap(TestClient(options: Options())) - - let sut = SentryMetricsClient(client: SentryStatsdClient(client: testClient)) - - let flushableBuckets: [BucketTimestamp: [CounterMetric]] = [:] - sut.capture(flushableBuckets: flushableBuckets) - - XCTAssertEqual(testClient.captureEnvelopeInvocations.count, 0) - } - -} diff --git a/Tests/SentryTests/Swift/Metrics/SetMetricTests.swift b/Tests/SentryTests/Swift/Metrics/SetMetricTests.swift deleted file mode 100644 index 51f6c7e6741..00000000000 --- a/Tests/SentryTests/Swift/Metrics/SetMetricTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -@testable import Sentry -import XCTest - -final class SetMetricTests: XCTestCase { - - func testAddingValues() { - let sut = SetMetric(first: 1, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - sut.add(value: 0) - sut.add(value: 1) - sut.add(value: 2) - - let serialized = sut.serialize() - XCTAssert(serialized.contains("0")) - XCTAssert(serialized.contains("1")) - XCTAssert(serialized.contains("2")) - } - - func testAddUIntMax() { - let sut = SetMetric(first: 1, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - sut.add(value: UInt.max) - - XCTAssert(sut.serialize().contains("\(UInt.max)")) - } - - func testType() { - let sut = SetMetric(first: 0, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.type, .set) - } - - func testWeight() { - let sut = SetMetric(first: 1, key: "key", unit: MeasurementUnitDuration.hour, tags: [:]) - - XCTAssertEqual(sut.weight, 1) - - for _ in 0..<10 { - sut.add(value: 5) - } - - sut.add(value: 3) - sut.add(value: 2) - - XCTAssertEqual(sut.weight, 4) - } - -} diff --git a/Tests/SentryTests/Swift/Metrics/TestMetricsClient.swift b/Tests/SentryTests/Swift/Metrics/TestMetricsClient.swift deleted file mode 100644 index 106ff5745ad..00000000000 --- a/Tests/SentryTests/Swift/Metrics/TestMetricsClient.swift +++ /dev/null @@ -1,22 +0,0 @@ -import _SentryPrivate -import Foundation -@testable import Sentry -import SentryTestUtils -import XCTest - -class TestMetricsClient: SentryMetricsClient { - - init() throws { - let testClient = try XCTUnwrap(TestClient(options: Options())) - let statsdClient = SentryStatsdClient(client: testClient) - - super.init(client: statsdClient) - } - - var afterRecordingCaptureInvocationBlock: (() -> Void)? - var captureInvocations = Invocations<[BucketTimestamp: [Metric]]>() - override func capture(flushableBuckets: [BucketTimestamp: [Metric]]) { - captureInvocations.record(flushableBuckets) - afterRecordingCaptureInvocationBlock?() - } -} diff --git a/Tests/SentryTests/Transaction/SentrySpanTests.swift b/Tests/SentryTests/Transaction/SentrySpanTests.swift index b52f6109517..409a129d2c0 100644 --- a/Tests/SentryTests/Transaction/SentrySpanTests.swift +++ b/Tests/SentryTests/Transaction/SentrySpanTests.swift @@ -534,33 +534,6 @@ class SentrySpanTests: XCTestCase { XCTAssertNil(serialization["tag"]) } - func testInit_DoesNotInitializeLocalMetricAggregator() { - let sut = fixture.getSut() - - let serialized = sut.serialize() - XCTAssertNil(serialized["_metrics_summary"]) - } - - func testLocalMetricsAggregator_GetsSerializedAsMetricsSummary() throws { - let sut = fixture.getSutWithTracer() - - let aggregator = sut.getLocalMetricsAggregator() - aggregator.add(type: .counter, key: "key", value: 1.0, unit: .none, tags: [:]) - - let serialized = sut.serialize() - - let metricsSummary = try XCTUnwrap(serialized["_metrics_summary"] as? [String: [[String: Any]]]) - XCTAssertEqual(metricsSummary.count, 1) - - let bucket = try XCTUnwrap(metricsSummary["c:key"]) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - XCTAssertEqual(metric["min"] as? Double, 1.0) - XCTAssertEqual(metric["max"] as? Double, 1.0) - XCTAssertEqual(metric["count"] as? Int, 1) - XCTAssertEqual(metric["sum"] as? Double, 1.0) - } - func testTraceHeaderNotSampled() { fixture.options.tracesSampleRate = 0 let span = fixture.getSut() diff --git a/Tests/SentryTests/Transaction/SentryTransactionTests.swift b/Tests/SentryTests/Transaction/SentryTransactionTests.swift index e501c1df96d..b6a79f0bb7d 100644 --- a/Tests/SentryTests/Transaction/SentryTransactionTests.swift +++ b/Tests/SentryTests/Transaction/SentryTransactionTests.swift @@ -197,25 +197,6 @@ class SentryTransactionTests: XCTestCase { XCTAssertEqual(actualTransaction, fixture.transactionName) } - func testSerializeMetricsSummary() throws { - let sut = fixture.getTransaction() - let aggregator = sut.trace.getLocalMetricsAggregator() - aggregator.add(type: .counter, key: "key", value: 1.0, unit: .none, tags: [:]) - - let serialized = sut.serialize() - - let metricsSummary = try XCTUnwrap(serialized["_metrics_summary"] as? [String: [[String: Any]]]) - XCTAssertEqual(metricsSummary.count, 1) - - let bucket = try XCTUnwrap(metricsSummary["c:key"]) - XCTAssertEqual(bucket.count, 1) - let metric = try XCTUnwrap(bucket.first) - XCTAssertEqual(metric["min"] as? Double, 1.0) - XCTAssertEqual(metric["max"] as? Double, 1.0) - XCTAssertEqual(metric["count"] as? Int, 1) - XCTAssertEqual(metric["sum"] as? Double, 1.0) - } - func testSerializedSpanData() throws { let sut = fixture.getTransaction() let serialized = sut.serialize() From 9cd19dd93b0f89dd653100007166aa46634aec8b Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 8 Oct 2024 15:22:34 +0200 Subject: [PATCH 2/7] fix: Edge case for swizzleClassNameExclude (#4405) Skip creating transactions for UIViewControllers ignored for swizzling via the option swizzleClassNameExclude. Due to some edge cases with nib files, the SDK doesn't swizzle the loadView method of the UIViewController subclasses, but instead, it swizzles the UIViewController.loadView method directly. Although the SDK doesn't swizzle the classes specified in swizzleClassNameExclude, it created transactions. Now, this is fixed. Fixes GH-4386 --- CHANGELOG.md | 5 +++ Sentry.xcodeproj/project.pbxproj | 4 ++ Sources/Sentry/Public/SentryOptions.h | 8 ++-- Sources/Sentry/SentrySubClassFinder.m | 11 ++---- ...SentryUIViewControllerPerformanceTracker.m | 12 ++++++ .../Sentry/SentryUIViewControllerSwizzling.m | 11 ++++++ .../Performance/SwizzleClassNameExclude.swift | 13 +++++++ ...iewControllerPerformanceTrackerTests.swift | 39 ++++++++++++++----- ...SentryUIViewControllerSwizzlingTests.swift | 21 +++++++--- 9 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 Sources/Swift/Integrations/Performance/SwizzleClassNameExclude.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc28363cdf..b97c1640b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - Remove the deprecated experimental Metrics API (#4406): [Learn more](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Coming-to-an-End) +### Fixes + +- Edge case for swizzleClassNameExclude (#4405): Skip creating transactions for UIViewControllers ignored for swizzling +via the option `swizzleClassNameExclude`. + ## 8.38.0-beta.1 ### Features diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 2926e5df337..d402f6fb71b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ 62872B5F2BA1B7F300A4FA7D /* NSLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62872B5E2BA1B7F300A4FA7D /* NSLock.swift */; }; 62872B632BA1B86100A4FA7D /* NSLockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62872B622BA1B86100A4FA7D /* NSLockTests.swift */; }; 62885DA729E946B100554F38 /* TestConncurrentModifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62885DA629E946B100554F38 /* TestConncurrentModifications.swift */; }; + 629428802CB3BF69002C454C /* SwizzleClassNameExclude.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6294287F2CB3BF4E002C454C /* SwizzleClassNameExclude.swift */; }; 6294774C2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */; }; 62950F1029E7FE0100A42624 /* SentryTransactionContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */; }; 629690532AD3E060000185FA /* SentryReachabilitySwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629690522AD3E060000185FA /* SentryReachabilitySwiftTests.swift */; }; @@ -1093,6 +1094,7 @@ 62872B5E2BA1B7F300A4FA7D /* NSLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLock.swift; sourceTree = ""; }; 62872B622BA1B86100A4FA7D /* NSLockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLockTests.swift; sourceTree = ""; }; 62885DA629E946B100554F38 /* TestConncurrentModifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConncurrentModifications.swift; sourceTree = ""; }; + 6294287F2CB3BF4E002C454C /* SwizzleClassNameExclude.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwizzleClassNameExclude.swift; sourceTree = ""; }; 6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Delegate.swift; sourceTree = ""; }; 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTransactionContextTests.swift; sourceTree = ""; }; 629690522AD3E060000185FA /* SentryReachabilitySwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReachabilitySwiftTests.swift; sourceTree = ""; }; @@ -3759,6 +3761,7 @@ D8739CF72BECFF92007D2F66 /* Performance */ = { isa = PBXGroup; children = ( + 6294287F2CB3BF4E002C454C /* SwizzleClassNameExclude.swift */, D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */, ); path = Performance; @@ -4660,6 +4663,7 @@ 7B8ECBFC26498958005FE2EF /* SentryAppStateManager.m in Sources */, 7B2A70DD27D6083D008B0D15 /* SentryThreadWrapper.m in Sources */, D8ACE3C72762187200F5A213 /* SentryNSDataSwizzling.m in Sources */, + 629428802CB3BF69002C454C /* SwizzleClassNameExclude.swift in Sources */, 638DC9A11EBC6B6400A66E41 /* SentryRequestOperation.m in Sources */, 63AA767A1EB8D20500D153DE /* SentryLogC.m in Sources */, 6344DDBA1EC3115C00D9160D /* SentryCrashReportConverter.m in Sources */, diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 597dd11e08d..bb67691dccd 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -446,17 +446,17 @@ NS_SWIFT_NAME(Options) @property (nonatomic, assign) BOOL enableSwizzling; /** - * An array of class names to ignore for swizzling. + * A set of class names to ignore for swizzling. * * @discussion The SDK checks if a class name of a class to swizzle contains a class name of this * array. For example, if you add MyUIViewController to this list, the SDK excludes the following * classes from swizzling: YourApp.MyUIViewController, YourApp.MyUIViewControllerA, * MyApp.MyUIViewController. - * We can't use an @c NSArray here because we use this as a workaround for which users have + * We can't use an @c NSSet here because we use this as a workaround for which users have * to pass in class names that aren't available on specific iOS versions. By using @c - * NSArray, users can specify unavailable class names. + * NSSet, users can specify unavailable class names. * - * @note Default is an empty array. + * @note Default is an empty set. */ @property (nonatomic, strong) NSSet *swizzleClassNameExcludes; diff --git a/Sources/Sentry/SentrySubClassFinder.m b/Sources/Sentry/SentrySubClassFinder.m index 2960b540ef1..b730f01f687 100644 --- a/Sources/Sentry/SentrySubClassFinder.m +++ b/Sources/Sentry/SentrySubClassFinder.m @@ -2,6 +2,7 @@ #import "SentryDispatchQueueWrapper.h" #import "SentryLog.h" #import "SentryObjCRuntimeWrapper.h" +#import "SentrySwift.h" #import #import @@ -61,13 +62,9 @@ - (void)actOnSubclassesOfViewControllerInImage:(NSString *)imageName block:(void for (int i = 0; i < count; i++) { NSString *className = [NSString stringWithUTF8String:classes[i]]; - BOOL shouldExcludeClassFromSwizzling = NO; - for (NSString *swizzleClassNameExclude in self.swizzleClassNameExcludes) { - if ([className containsString:swizzleClassNameExclude]) { - shouldExcludeClassFromSwizzling = YES; - break; - } - } + BOOL shouldExcludeClassFromSwizzling = [SentrySwizzleClassNameExclude + shouldExcludeClassWithClassName:className + swizzleClassNameExcludes:self.swizzleClassNameExcludes]; // It is vital to avoid calling NSClassFromString for the excluded classes because we // had crashes for specific classes when calling NSClassFromString, such as diff --git a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m index 51be581e7d6..200a6f933df 100644 --- a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m +++ b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m @@ -68,6 +68,18 @@ - (void)viewControllerLoadView:(UIViewController *)controller return; } + SentryOptions *options = [SentrySDK options]; + + if ([SentrySwizzleClassNameExclude + shouldExcludeClassWithClassName:NSStringFromClass([controller class]) + swizzleClassNameExcludes:options.swizzleClassNameExcludes]) { + SENTRY_LOG_DEBUG(@"Won't track view controller because it's excluded with the option " + @"swizzleClassNameExcludes: %@", + controller); + callbackToOrigin(); + return; + } + [self limitOverride:@"loadView" target:controller callbackToOrigin:callbackToOrigin diff --git a/Sources/Sentry/SentryUIViewControllerSwizzling.m b/Sources/Sentry/SentryUIViewControllerSwizzling.m index 0cd42b99119..456dd933e16 100644 --- a/Sources/Sentry/SentryUIViewControllerSwizzling.m +++ b/Sources/Sentry/SentryUIViewControllerSwizzling.m @@ -36,6 +36,7 @@ @interface UIApplication (SentryUIApplication) @interface SentryUIViewControllerSwizzling () +@property (nonatomic, strong) SentryOptions *options; @property (nonatomic, strong) SentryInAppLogic *inAppLogic; @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueue; @property (nonatomic, strong) id objcRuntimeWrapper; @@ -56,6 +57,7 @@ - (instancetype)initWithOptions:(SentryOptions *)options binaryImageCache:(SentryBinaryImageCache *)binaryImageCache { if (self = [super init]) { + self.options = options; self.inAppLogic = [[SentryInAppLogic alloc] initWithInAppIncludes:options.inAppIncludes inAppExcludes:options.inAppExcludes]; self.dispatchQueue = dispatchQueue; @@ -341,6 +343,15 @@ - (void)swizzleViewControllerSubClass:(Class)class */ - (BOOL)shouldSwizzleViewController:(Class)class { + NSString *className = NSStringFromClass(class); + + BOOL shouldExcludeClassFromSwizzling = [SentrySwizzleClassNameExclude + shouldExcludeClassWithClassName:className + swizzleClassNameExcludes:self.options.swizzleClassNameExcludes]; + if (shouldExcludeClassFromSwizzling) { + return NO; + } + return [self.inAppLogic isClassInApp:class]; } diff --git a/Sources/Swift/Integrations/Performance/SwizzleClassNameExclude.swift b/Sources/Swift/Integrations/Performance/SwizzleClassNameExclude.swift new file mode 100644 index 00000000000..9b60ae87ff1 --- /dev/null +++ b/Sources/Swift/Integrations/Performance/SwizzleClassNameExclude.swift @@ -0,0 +1,13 @@ +import Foundation + +@objcMembers +class SentrySwizzleClassNameExclude: NSObject { + static func shouldExcludeClass(className: String, swizzleClassNameExcludes: Set) -> Bool { + for exclude in swizzleClassNameExcludes { + if className.contains(exclude) { + return true + } + } + return false + } +} diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift index dac6d9af223..24fa222d4a3 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift @@ -26,15 +26,7 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { private class Fixture { - var options: Options { - let options = Options.noIntegrations() - let imageName = String( - cString: class_getImageName(SentryUIViewControllerSwizzlingTests.self)!, - encoding: .utf8)! as NSString - options.add(inAppInclude: imageName.lastPathComponent) - options.debug = true - return options - } + var options: Options let viewController = TestViewController() let tracker = SentryPerformanceTracker.shared @@ -50,6 +42,13 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { } init() { + options = Options.noIntegrations() + let imageName = String( + cString: class_getImageName(SentryUIViewControllerSwizzlingTests.self)!, + encoding: .utf8)! as NSString + options.add(inAppInclude: imageName.lastPathComponent) + options.debug = true + framesTracker = SentryFramesTracker(displayLinkWrapper: displayLinkWrapper, dateProvider: dateProvider, dispatchQueueWrapper: TestSentryDispatchQueueWrapper(), notificationCenter: TestNSNotificationCenterWrapper(), keepDelayedFramesDuration: 0) SentryDependencyContainer.sharedInstance().framesTracker = framesTracker @@ -500,7 +499,7 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { wait(for: [callbackExpectation], timeout: 0) } - func testLoadView_withUIViewController() { + func testLoadView_withNonInAppUIViewController_DoesNotStartTransaction() { let sut = fixture.getSut() let viewController = UIViewController() let tracker = fixture.tracker @@ -519,6 +518,26 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { wait(for: [callbackExpectation], timeout: 0) } + func testLoadView_withIgnoreSwizzleUIViewController_DoesNotStartTransaction() { + fixture.options.swizzleClassNameExcludes = ["TestViewController"] + let sut = fixture.getSut() + let viewController = fixture.viewController + let tracker = fixture.tracker + var transactionSpan: Span! + let callbackExpectation = expectation(description: "Callback Expectation") + + XCTAssertTrue(getStack(tracker).isEmpty) + + sut.viewControllerLoadView(viewController) { + let spans = self.getStack(tracker) + transactionSpan = spans.first + callbackExpectation.fulfill() + } + + XCTAssertNil(transactionSpan, "Expected to transaction.") + wait(for: [callbackExpectation], timeout: 0) + } + func testSecondLoadView() throws { let sut = fixture.getSut() let viewController = fixture.viewController diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift index f11525dc60b..fa4a7cad04c 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift @@ -13,14 +13,13 @@ class SentryUIViewControllerSwizzlingTests: XCTestCase { let subClassFinder: TestSubClassFinder let processInfoWrapper = SentryNSProcessInfoWrapper() let binaryImageCache: SentryBinaryImageCache + var options: Options init() { subClassFinder = TestSubClassFinder(dispatchQueue: dispatchQueue, objcRuntimeWrapper: objcRuntimeWrapper, swizzleClassNameExcludes: []) binaryImageCache = SentryDependencyContainer.sharedInstance().binaryImageCache - } - - var options: Options { - let options = Options.noIntegrations() + + options = Options.noIntegrations() let imageName = String( cString: class_getImageName(SentryUIViewControllerSwizzlingTests.self)!, @@ -31,8 +30,6 @@ class SentryUIViewControllerSwizzlingTests: XCTestCase { cString: class_getImageName(ExternalUIViewController.self)!, encoding: .utf8)! as NSString options.add(inAppInclude: externalImageName.lastPathComponent) - - return options } var sut: SentryUIViewControllerSwizzling { @@ -98,6 +95,18 @@ class SentryUIViewControllerSwizzlingTests: XCTestCase { XCTAssertFalse(result) } + func testShouldNotSwizzle_UIViewControllerExcludedFromSwizzling() { + fixture.options.swizzleClassNameExcludes = ["TestViewController"] + + XCTAssertFalse(fixture.sut.shouldSwizzleViewController(TestViewController.self)) + } + + func testShouldSwizzle_UIViewControllerNotExcludedFromSwizzling() { + fixture.options.swizzleClassNameExcludes = ["TestViewController1"] + + XCTAssertTrue(fixture.sut.shouldSwizzleViewController(TestViewController.self)) + } + func testUIViewController_loadView_noTransactionBoundToScope() { fixture.sut.start() let controller = UIViewController() From 9b973722a5714efc79e095c6697e34f70a90b635 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 8 Oct 2024 16:54:10 +0200 Subject: [PATCH 3/7] fix: Swizzling RootUIViewController if ignored (#4407) The SDK didn't exclude the RootViewController from swizzling when ignored by the option swizzleClassNameExclude. This is fixed now. Fixes GH-4385 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b97c1640b91..93c5cdf60f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ via the option `swizzleClassNameExclude`. - Fix the versioning to support app release with Beta versions (#4368) - Linking ongoing trace to crash event (#4393) +- Edge case for swizzleClassNameExclude (#4405): Skip creating transactions for UIViewControllers ignored for swizzling +via the option `swizzleClassNameExclude`. +- Swizzling RootUIViewController if ignored by `swizzleClassNameExclude` (#4407) ## 8.37.0 From 75f7fc893a18f02a58d5f9843adaaf857084d9c7 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Wed, 9 Oct 2024 10:55:43 +0200 Subject: [PATCH 4/7] Fix CHANGELOG.md (#4416) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c5cdf60f3..9afd0f2b5d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Edge case for swizzleClassNameExclude (#4405): Skip creating transactions for UIViewControllers ignored for swizzling via the option `swizzleClassNameExclude`. +- Swizzling RootUIViewController if ignored by `swizzleClassNameExclude` (#4407) ## 8.38.0-beta.1 @@ -34,7 +35,6 @@ via the option `swizzleClassNameExclude`. - Linking ongoing trace to crash event (#4393) - Edge case for swizzleClassNameExclude (#4405): Skip creating transactions for UIViewControllers ignored for swizzling via the option `swizzleClassNameExclude`. -- Swizzling RootUIViewController if ignored by `swizzleClassNameExclude` (#4407) ## 8.37.0 From 887502e715b9885c637e0573934cf75128a63097 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Wed, 9 Oct 2024 11:42:07 +0200 Subject: [PATCH 5/7] fix: Add TTID/TTFD spans when loadView skipped (#4415) The SDK creates a transaction when only viewDidLoad gets called for a UIViewController but doesn't create TTID/TTFD spans, leading to transactions missing TTID/TTFD data. Now, the SDK also creates TTID/TTFD spans when only viewDidLoad gets called and loadView gets skipped. Fixes GH-4396 Co-authored-by: Andrew McKnight --- CHANGELOG.md | 1 + .../ViewControllers/SplitViewController.swift | 1 - ...SentryUIViewControllerPerformanceTracker.m | 1 + ...iewControllerPerformanceTrackerTests.swift | 95 +++++++++++++++++-- 4 files changed, 89 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9afd0f2b5d7..f00e3c97189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Edge case for swizzleClassNameExclude (#4405): Skip creating transactions for UIViewControllers ignored for swizzling via the option `swizzleClassNameExclude`. +- Add TTID/TTFD spans when loadView gets skipped (#4415) - Swizzling RootUIViewController if ignored by `swizzleClassNameExclude` (#4407) ## 8.38.0-beta.1 diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/SplitViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/SplitViewController.swift index b7ad604e61c..1356fcca4f3 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/SplitViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/SplitViewController.swift @@ -66,7 +66,6 @@ class SecondarySplitViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - SentrySDK.reportFullyDisplayed() if let topvc = TopViewControllerInspector.shared { topvc.bringToFront() diff --git a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m index 200a6f933df..408356f11e9 100644 --- a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m +++ b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m @@ -102,6 +102,7 @@ - (void)viewControllerViewDidLoad:(UIViewController *)controller block:^{ SENTRY_LOG_DEBUG(@"Tracking viewDidLoad"); [self createTransaction:controller]; + [self createTimeToDisplay:controller]; [self measurePerformance:@"viewDidLoad" target:controller callbackToOrigin:callbackToOrigin]; diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift index 24fa222d4a3..8ec08e52b50 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift @@ -576,7 +576,6 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { sut.enableWaitForFullDisplay = true - //The first view controller creates a transaction sut.viewControllerLoadView(firstController) { tracer = self.getStack(tracker).first as? SentryTracer } @@ -594,13 +593,92 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { sut.enableWaitForFullDisplay = false - //The first view controller creates a transaction sut.viewControllerLoadView(firstController) { tracer = self.getStack(tracker).first as? SentryTracer } XCTAssertEqual(tracer?.children.count, 2) } + + func test_OnlyViewDidLoad_CreatesTTIDSpan() throws { + let sut = fixture.getSut() + let tracker = fixture.tracker + + var tracer: SentryTracer! + + sut.viewControllerViewDidLoad(TestViewController()) { + tracer = self.getStack(tracker).first as? SentryTracer + } + + let children: [Span]? = Dynamic(tracer).children as [Span]? + + XCTAssertEqual(children?.count, 2) + + let firstChild = try XCTUnwrap(children?.first) + XCTAssertEqual("ui.load.initial_display", firstChild.operation) + XCTAssertEqual("TestViewController initial display", firstChild.spanDescription) + let secondChild = try XCTUnwrap(children?[1]) + XCTAssertEqual("ui.load", secondChild.operation) + XCTAssertEqual("viewDidLoad", secondChild.spanDescription) + } + + func test_OnlyViewDidLoadTTFDEnabled_CreatesTTIDAndTTFDSpans() throws { + let sut = fixture.getSut() + sut.enableWaitForFullDisplay = true + let tracker = fixture.tracker + + var tracer: SentryTracer! + + sut.viewControllerViewDidLoad(TestViewController()) { + tracer = self.getStack(tracker).first as? SentryTracer + } + + let children: [Span]? = Dynamic(tracer).children as [Span]? + + XCTAssertEqual(children?.count, 3) + + let child1 = try XCTUnwrap(children?.first) + XCTAssertEqual("ui.load.initial_display", child1.operation) + XCTAssertEqual("TestViewController initial display", child1.spanDescription) + + let child2 = try XCTUnwrap(children?[1]) + XCTAssertEqual("ui.load.full_display", child2.operation) + XCTAssertEqual("TestViewController full display", child2.spanDescription) + + let child3 = try XCTUnwrap(children?[2]) + XCTAssertEqual("ui.load", child3.operation) + XCTAssertEqual("viewDidLoad", child3.spanDescription) + } + + func test_BothLoadViewAndViewDidLoad_CreatesOneTTIDSpan() throws { + let sut = fixture.getSut() + let tracker = fixture.tracker + let controller = TestViewController() + var tracer: SentryTracer! + + sut.viewControllerLoadView(controller) { + tracer = self.getStack(tracker).first as? SentryTracer + } + + sut.viewControllerViewDidLoad(controller) { + // Empty on purpose + } + + let children: [Span]? = Dynamic(tracer).children as [Span]? + + XCTAssertEqual(children?.count, 3) + + let child1 = try XCTUnwrap(children?.first) + XCTAssertEqual("ui.load.initial_display", child1.operation) + + let child2 = try XCTUnwrap(children?[1]) + XCTAssertEqual("ui.load", child2.operation) + XCTAssertEqual("loadView", child2.spanDescription) + + let child3 = try XCTUnwrap(children?[2]) + XCTAssertEqual("ui.load", child3.operation) + XCTAssertEqual("viewDidLoad", child3.spanDescription) + } func test_captureAllAutomaticSpans() { let sut = fixture.getSut() @@ -643,12 +721,13 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { let children: [Span]? = Dynamic(tracer).children as [Span]? - //First Controller viewDidLoad - //Second Controller root span - //Second Controller viewDidLoad - //Third Controller root span - //Third Controller viewDidLoad - XCTAssertEqual(children?.count, 5) + // First Controller initial_display + // First Controller viewDidLoad + // Second Controller root span + // Second Controller viewDidLoad + // Third Controller root span + // Third Controller viewDidLoad + XCTAssertEqual(children?.count, 6) } private func assertSpanDuration(span: Span?, expectedDuration: TimeInterval) throws { From 0418702573fb6cd470099060bc9fc5cbd5bea085 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Wed, 9 Oct 2024 11:52:23 +0200 Subject: [PATCH 6/7] impr: Capture trace based profiles on a BG Thread (#4377) Serializing and capturing a profile can run on the main thread and block it for a couple of milliseconds. Therefore, we move this to a background thread to avoid potentially blocking the main thread. Co-authored-by: Dhiogo Brustolin --- CHANGELOG.md | 4 ++ Sentry.xcodeproj/project.pbxproj | 8 +++ .../SentryCaptureTransactionWithProfile.mm | 50 +++++++++++++++++++ .../Profiling/SentryProfilerSerialization.mm | 14 ++---- Sources/Sentry/SentryTracer.m | 24 ++------- .../SentryCaptureTransactionWithProfile.h | 22 ++++++++ .../include/SentryProfilerSerialization.h | 4 +- .../SentryProfileTestFixture.swift | 2 +- .../SentryTraceProfilerTests.swift | 23 +++++++++ 9 files changed, 118 insertions(+), 33 deletions(-) create mode 100644 Sources/Sentry/Profiling/SentryCaptureTransactionWithProfile.mm create mode 100644 Sources/Sentry/include/SentryCaptureTransactionWithProfile.h diff --git a/CHANGELOG.md b/CHANGELOG.md index f00e3c97189..ec8e51cac9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ via the option `swizzleClassNameExclude`. - Add TTID/TTFD spans when loadView gets skipped (#4415) - Swizzling RootUIViewController if ignored by `swizzleClassNameExclude` (#4407) +### Improvements + +- Serializing profile on a BG Thread (#4377) to avoid potentially slightly blocking the main thread. + ## 8.38.0-beta.1 ### Features diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index d402f6fb71b..da002f2c39b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -78,6 +78,8 @@ 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */ = {isa = PBXBuildFile; fileRef = 620379DC2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m */; }; 621AE74B2C626C230012E730 /* SentryANRTrackerV2.h in Headers */ = {isa = PBXBuildFile; fileRef = 621AE74A2C626C230012E730 /* SentryANRTrackerV2.h */; }; 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */; }; + 621C88482CAD23B9000EABCB /* SentryCaptureTransactionWithProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 621C88472CAD23B9000EABCB /* SentryCaptureTransactionWithProfile.h */; }; + 621C884A2CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm in Sources */ = {isa = PBXBuildFile; fileRef = 621C88492CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm */; }; 621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */; }; 621F61F12BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */; }; 6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6221BBC92CAA932100C627CA /* SentryANRType.swift */; }; @@ -1070,6 +1072,8 @@ 621AE74A2C626C230012E730 /* SentryANRTrackerV2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryANRTrackerV2.h; path = include/SentryANRTrackerV2.h; sourceTree = ""; }; 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryANRTrackerV2.m; sourceTree = ""; }; 621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Tests.swift; sourceTree = ""; }; + 621C88472CAD23B9000EABCB /* SentryCaptureTransactionWithProfile.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCaptureTransactionWithProfile.h; path = Sources/Sentry/include/SentryCaptureTransactionWithProfile.h; sourceTree = SOURCE_ROOT; }; + 621C88492CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryCaptureTransactionWithProfile.mm; sourceTree = ""; }; 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = ""; }; 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilder.swift; sourceTree = ""; }; 6221BBC92CAA932100C627CA /* SentryANRType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRType.swift; sourceTree = ""; }; @@ -3381,6 +3385,8 @@ 8459FCBD2BD73E810038E9C9 /* SentryProfilerSerialization.h */, 8459FCBF2BD73EB20038E9C9 /* SentryProfilerSerialization.mm */, 8459FCC12BD73EEF0038E9C9 /* SentryProfilerSerialization+Test.h */, + 621C88472CAD23B9000EABCB /* SentryCaptureTransactionWithProfile.h */, + 621C88492CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm */, 84AF45A429A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h */, 84AF45A529A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm */, 840B7EF22BBF83DF008B8120 /* SentryProfiler+Private.h */, @@ -3995,6 +4001,7 @@ 7D7F0A5F23DF3D2C00A4629C /* SentryGlobalEventProcessor.h in Headers */, D867063D27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h in Headers */, 0A2D8D5D289815EB008720F6 /* SentryBaseIntegration.h in Headers */, + 621C88482CAD23B9000EABCB /* SentryCaptureTransactionWithProfile.h in Headers */, 84AC61D629F75A98009EEF61 /* SentryDispatchFactory.h in Headers */, 63FE716520DA4C1100CDBAE8 /* SentryCrashMemory.h in Headers */, 63FE713F20DA4C1100CDBAE8 /* SentryCrashStackCursor_SelfThread.h in Headers */, @@ -4573,6 +4580,7 @@ D80CD8D32B751447002F710B /* SentryMXCallStackTree.swift in Sources */, 7BCFA71627D0BB50008C662C /* SentryANRTrackerV1.m in Sources */, 8459FCC02BD73EB20038E9C9 /* SentryProfilerSerialization.mm in Sources */, + 621C884A2CAD23E9000EABCB /* SentryCaptureTransactionWithProfile.mm in Sources */, 63EED6C02237923600E02400 /* SentryOptions.m in Sources */, D8CB741B2947286500A5F964 /* SentryEnvelopeItemHeader.m in Sources */, D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */, diff --git a/Sources/Sentry/Profiling/SentryCaptureTransactionWithProfile.mm b/Sources/Sentry/Profiling/SentryCaptureTransactionWithProfile.mm new file mode 100644 index 00000000000..35b3fa2a27c --- /dev/null +++ b/Sources/Sentry/Profiling/SentryCaptureTransactionWithProfile.mm @@ -0,0 +1,50 @@ +#import "SentryCaptureTransactionWithProfile.h" + +#if SENTRY_TARGET_PROFILING_SUPPORTED + +# import "SentryDispatchQueueWrapper.h" +# import "SentryHub+Private.h" +# import "SentryLog.h" +# import "SentryProfiledTracerConcurrency.h" +# import "SentryProfiler+Private.h" +# import "SentryProfilerSerialization.h" +# import "SentryProfilerState.h" +# import "SentrySwift.h" +# import "SentryTracer+Private.h" +# import "SentryTransaction.h" + +NS_ASSUME_NONNULL_BEGIN + +void +sentry_captureTransactionWithProfile(SentryHub *hub, SentryDispatchQueueWrapper *dispatchQueue, + SentryTransaction *transaction, NSDate *startTimestamp) +{ + const auto profiler = sentry_profilerForFinishedTracer(transaction.trace.internalID); + if (!profiler) { + [hub captureTransaction:transaction withScope:hub.scope]; + return; + } + + // This code can run on the main thread, and the profile serialization can take a couple of + // milliseconds. Therefore, we move this to a background thread to avoid potentially blocking + // the main thread. + [dispatchQueue dispatchAsyncWithBlock:^{ + const auto profilingData = [profiler.state copyProfilingData]; + + const auto profileEnvelopeItem = sentry_traceProfileEnvelopeItem( + hub, profiler, profilingData, transaction, startTimestamp); + + if (!profileEnvelopeItem) { + [hub captureTransaction:transaction withScope:hub.scope]; + return; + } + + [hub captureTransaction:transaction + withScope:hub.scope + additionalEnvelopeItems:@[ profileEnvelopeItem ]]; + }]; +} + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/Profiling/SentryProfilerSerialization.mm b/Sources/Sentry/Profiling/SentryProfilerSerialization.mm index c61ce414aec..a2335a68303 100644 --- a/Sources/Sentry/Profiling/SentryProfilerSerialization.mm +++ b/Sources/Sentry/Profiling/SentryProfilerSerialization.mm @@ -346,22 +346,16 @@ return [[SentryEnvelope alloc] initWithId:chunkID singleItem:envelopeItem]; } -SentryEnvelopeItem *_Nullable sentry_traceProfileEnvelopeItem( +SentryEnvelopeItem *_Nullable sentry_traceProfileEnvelopeItem(SentryHub *hub, + SentryProfiler *profiler, NSDictionary *profilingData, SentryTransaction *transaction, NSDate *startTimestamp) { - SENTRY_LOG_DEBUG(@"Creating profiling envelope item"); - const auto profiler = sentry_profilerForFinishedTracer(transaction.trace.internalID); - if (!profiler) { - return nil; - } - const auto payload = sentry_serializedTraceProfileData( - [profiler.state copyProfilingData], transaction.startSystemTime, transaction.endSystemTime, + profilingData, transaction.startSystemTime, transaction.endSystemTime, sentry_profilerTruncationReasonName(profiler.truncationReason), [profiler.metricProfiler serializeTraceProfileMetricsBetween:transaction.startSystemTime and:transaction.endSystemTime], - [SentryDependencyContainer.sharedInstance.debugImageProvider getDebugImagesCrashed:NO], - transaction.trace.hub + [SentryDependencyContainer.sharedInstance.debugImageProvider getDebugImagesCrashed:NO], hub # if SENTRY_HAS_UIKIT , profiler.screenFrameData diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 1a7b7e72629..2001a21da35 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -36,6 +36,7 @@ #import #if SENTRY_TARGET_PROFILING_SUPPORTED +# import "SentryCaptureTransactionWithProfile.h" # import "SentryLaunchProfiling.h" # import "SentryProfiledTracerConcurrency.h" # import "SentryProfilerSerialization.h" @@ -623,7 +624,8 @@ - (void)finishInternal startTimestamp = [SentryDependencyContainer.sharedInstance.dateProvider date]; } - [self captureTransactionWithProfile:transaction startTimestamp:startTimestamp]; + sentry_captureTransactionWithProfile( + self.hub, self.dispatchQueue, transaction, startTimestamp); return; } #endif // SENTRY_TARGET_PROFILING_SUPPORTED @@ -631,26 +633,6 @@ - (void)finishInternal [_hub captureTransaction:transaction withScope:_hub.scope]; } -#if SENTRY_TARGET_PROFILING_SUPPORTED -- (void)captureTransactionWithProfile:(SentryTransaction *)transaction - startTimestamp:(NSDate *)startTimestamp -{ - SentryEnvelopeItem *profileEnvelopeItem - = sentry_traceProfileEnvelopeItem(transaction, startTimestamp); - - if (!profileEnvelopeItem) { - [_hub captureTransaction:transaction withScope:_hub.scope]; - return; - } - - SENTRY_LOG_DEBUG(@"Capturing transaction id %@ with profiling data attached for tracer id %@.", - transaction.eventId.sentryIdString, self.internalID.sentryIdString); - [_hub captureTransaction:transaction - withScope:_hub.scope - additionalEnvelopeItems:@[ profileEnvelopeItem ]]; -} -#endif // SENTRY_TARGET_PROFILING_SUPPORTED - - (void)trimEndTimestamp { NSDate *oldest = self.startTimestamp; diff --git a/Sources/Sentry/include/SentryCaptureTransactionWithProfile.h b/Sources/Sentry/include/SentryCaptureTransactionWithProfile.h new file mode 100644 index 00000000000..23a9491ad35 --- /dev/null +++ b/Sources/Sentry/include/SentryCaptureTransactionWithProfile.h @@ -0,0 +1,22 @@ +#import "SentryProfilingConditionals.h" + +#if SENTRY_TARGET_PROFILING_SUPPORTED + +# import "SentryDefines.h" +# import + +@class SentryEnvelope; +@class SentryEnvelopeItem; +@class SentryHub; +@class SentryTransaction; +@class SentryDispatchQueueWrapper; + +NS_ASSUME_NONNULL_BEGIN + +SENTRY_EXTERN void sentry_captureTransactionWithProfile(SentryHub *hub, + SentryDispatchQueueWrapper *dispatchQueue, SentryTransaction *transaction, + NSDate *startTimestamp); + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/include/SentryProfilerSerialization.h b/Sources/Sentry/include/SentryProfilerSerialization.h index b3efeb002cd..e4da299fbab 100644 --- a/Sources/Sentry/include/SentryProfilerSerialization.h +++ b/Sources/Sentry/include/SentryProfilerSerialization.h @@ -11,10 +11,12 @@ @class SentryId; @class SentryScreenFrames; @class SentryTransaction; +@class SentryProfiler; NS_ASSUME_NONNULL_BEGIN -SENTRY_EXTERN SentryEnvelopeItem *_Nullable sentry_traceProfileEnvelopeItem( +SENTRY_EXTERN SentryEnvelopeItem *_Nullable sentry_traceProfileEnvelopeItem(SentryHub *hub, + SentryProfiler *profiler, NSDictionary *profilingData, SentryTransaction *transaction, NSDate *startTimestamp); SentryEnvelope *_Nullable sentry_continuousProfileChunkEnvelope( diff --git a/Tests/SentryProfilerTests/SentryProfileTestFixture.swift b/Tests/SentryProfilerTests/SentryProfileTestFixture.swift index 71560b48934..e65d235febb 100644 --- a/Tests/SentryProfilerTests/SentryProfileTestFixture.swift +++ b/Tests/SentryProfilerTests/SentryProfileTestFixture.swift @@ -78,7 +78,7 @@ class SentryProfileTestFixture { } /// Advance the mock date provider, start a new transaction and return its handle. - func newTransaction(testingAppLaunchSpans: Bool = false, automaticTransaction: Bool = false, idleTimeout: TimeInterval? = nil, expectedProfileMetrics: MockMetric = MockMetric()) throws -> SentryTracer { + func newTransaction(testingAppLaunchSpans: Bool = false, automaticTransaction: Bool = false, idleTimeout: TimeInterval? = nil) throws -> SentryTracer { let operation = testingAppLaunchSpans ? SentrySpanOperationUILoad : transactionOperation if automaticTransaction { diff --git a/Tests/SentryProfilerTests/SentryTraceProfilerTests.swift b/Tests/SentryProfilerTests/SentryTraceProfilerTests.swift index 24e89e6f4a6..291709da2e2 100644 --- a/Tests/SentryProfilerTests/SentryTraceProfilerTests.swift +++ b/Tests/SentryProfilerTests/SentryTraceProfilerTests.swift @@ -31,6 +31,29 @@ class SentryTraceProfilerTests: XCTestCase { span.finish() try self.assertMetricsPayload() } + + func testCaptureTransactionWithProfile_StopsProfileOnCallingThread() throws { + let span = try fixture.newTransaction() + try addMockSamples() + try fixture.gatherMockedTraceProfileMetrics() + + self.fixture.dispatchQueueWrapper.dispatchAsyncExecutesBlock = false + let currentProfiler = try XCTUnwrap(SentryTraceProfiler.getCurrentProfiler()) + + XCTAssertTrue(currentProfiler.isRunning()) + + span.finish() + + XCTAssertFalse(currentProfiler.isRunning(), "Profiler must be stopped on the calling thread.") + XCTAssertEqual(SentryProfilerTruncationReason.normal, currentProfiler.truncationReason) + + self.fixture.currentDateProvider.advanceBy(nanoseconds: 1.toNanoSeconds()) + self.fixture.dispatchQueueWrapper.dispatchAsyncExecutesBlock = true + self.fixture.dispatchQueueWrapper.invokeLastDispatchAsync() + + try self.assertMetricsPayload() + try self.assertValidTraceProfileData() + } func testTransactionWithMutatedTracerID() throws { let span = try fixture.newTransaction() From 0a23401352a8449cd5a38140bb36d941194e8800 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 9 Oct 2024 13:12:51 +0200 Subject: [PATCH 7/7] ref: SwiftUI custom redact (#4392) Added sentryReplayUnmask as SwiftUI modifier --- CHANGELOG.md | 4 ++ .../iOS-SwiftUI/iOS-SwiftUI/ContentView.swift | 20 +++---- .../iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift | 2 - Sources/SentrySwiftUI/SentryReplayView.swift | 37 ++++++++++--- .../Swift/Extensions/UIViewExtensions.swift | 1 + .../Swift/Tools/SentryViewPhotographer.swift | 2 +- Sources/Swift/Tools/UIRedactBuilder.swift | 54 +++++++++++++++---- .../SwiftUI/SentryRedactModifierTests.swift | 9 +++- 8 files changed, 101 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8e51cac9e..56ea7c02058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Custom redact modifier for SwiftUI (#4362, #4392) + ### Removal of Experimental API - Remove the deprecated experimental Metrics API (#4406): [Learn more](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Coming-to-an-End) diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift index 2767e1574f2..be7b0b0f7ff 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift @@ -118,14 +118,16 @@ struct ContentView: View { return SentryTracedView("Content View Body") { NavigationView { VStack(alignment: HorizontalAlignment.center, spacing: 16) { - Text(getCurrentTracer()?.transactionContext.name ?? "NO SPAN") - .accessibilityIdentifier("TRANSACTION_NAME") - Text(getCurrentTracer()?.transactionContext.spanId.sentrySpanIdString ?? "NO ID") - .accessibilityIdentifier("TRANSACTION_ID") - - Text(getCurrentTracer()?.transactionContext.origin ?? "NO ORIGIN") - .accessibilityIdentifier("TRACE_ORIGIN") - + Group { + Text(getCurrentTracer()?.transactionContext.name ?? "NO SPAN") + .accessibilityIdentifier("TRANSACTION_NAME") + Text(getCurrentTracer()?.transactionContext.spanId.sentrySpanIdString ?? "NO ID") + .accessibilityIdentifier("TRANSACTION_ID") + .sentryReplayMask() + + Text(getCurrentTracer()?.transactionContext.origin ?? "NO ORIGIN") + .accessibilityIdentifier("TRACE_ORIGIN") + }.sentryReplayUnmask() SentryTracedView("Child Span") { VStack { Text(getCurrentSpan()?.spanDescription ?? "NO SPAN") @@ -199,7 +201,7 @@ struct ContentView: View { Text("Form Screen") } } - .sentryReplayMask() + .background(Color.white) } SecondView() } diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift index 5a490cd2861..ee92d4a10c3 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift @@ -11,8 +11,6 @@ struct SwiftUIApp: App { options.tracesSampleRate = 1.0 options.profilesSampleRate = 1.0 options.experimental.sessionReplay.sessionSampleRate = 1.0 - options.experimental.sessionReplay.maskAllImages = false - options.experimental.sessionReplay.maskAllText = false options.initialScope = { scope in scope.injectGitInformation() return scope diff --git a/Sources/SentrySwiftUI/SentryReplayView.swift b/Sources/SentrySwiftUI/SentryReplayView.swift index e35db36e512..78fe5d8342f 100644 --- a/Sources/SentrySwiftUI/SentryReplayView.swift +++ b/Sources/SentrySwiftUI/SentryReplayView.swift @@ -3,26 +3,41 @@ import Sentry import SwiftUI import UIKit +#if CARTHAGE || SWIFT_PACKAGE +@_implementationOnly import SentryInternal +#endif + +enum MaskBehavior { + case mask + case unmask +} + @available(iOS 13, macOS 10.15, tvOS 13, *) struct SentryReplayView: UIViewRepresentable { + let maskBehavior: MaskBehavior + class SentryRedactView: UIView { } func makeUIView(context: Context) -> UIView { - let result = SentryRedactView() - result.sentryReplayMask() - return result + let view = SentryRedactView() + view.isUserInteractionEnabled = false + return view } func updateUIView(_ uiView: UIView, context: Context) { - // This is blank on purpose. UIViewRepresentable requires this function. + switch maskBehavior { + case .mask: SentryRedactViewHelper.maskSwiftUI(uiView) + case .unmask: SentryRedactViewHelper.clipOutView(uiView) + } } } @available(iOS 13, macOS 10.15, tvOS 13, *) struct SentryReplayModifier: ViewModifier { + let behavior: MaskBehavior func body(content: Content) -> some View { - content.background(SentryReplayView()) + content.overlay(SentryReplayView(maskBehavior: behavior)) } } @@ -38,7 +53,17 @@ public extension View { /// - Returns: A modifier that redacts sensitive information during session replays. /// - Experiment: This is an experimental feature and may still have bugs. func sentryReplayMask() -> some View { - modifier(SentryReplayModifier()) + modifier(SentryReplayModifier(behavior: .mask)) + } + + /// Marks the view as safe to not be masked during session replay. + /// + /// Anything that is behind this view will also not be masked anymore. + /// + /// - Returns: A modifier that prevents a view from being masked in the session replay. + /// - Experiment: This is an experimental feature and may still have bugs. + func sentryReplayUnmask() -> some View { + modifier(SentryReplayModifier(behavior: .unmask)) } } #endif diff --git a/Sources/Swift/Extensions/UIViewExtensions.swift b/Sources/Swift/Extensions/UIViewExtensions.swift index 83bf0ade79d..7c5e8cf27c6 100644 --- a/Sources/Swift/Extensions/UIViewExtensions.swift +++ b/Sources/Swift/Extensions/UIViewExtensions.swift @@ -21,6 +21,7 @@ public extension UIView { func sentryReplayUnmask() { SentryRedactViewHelper.unmaskView(self) } + } #endif diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index 192585ff217..322a7017a84 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -58,7 +58,7 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { let path = CGPath(rect: rect, transform: &transform) switch region.type { - case .redact: + case .redact, .redactSwiftUI: (region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect.applying(region.transform))).setFill() context.cgContext.addPath(path) context.cgContext.fillPath() diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 01b99a3e296..72ff985d95a 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -22,6 +22,9 @@ enum RedactRegionType { /// Pop the last Pushed region from the drawing context. /// Used after prossing every child of a view that clip to its bounds. case clipEnd + + /// These regions are redacted first, there is no way to avoid it. + case redactSwiftUI } struct RedactRegion { @@ -155,7 +158,19 @@ class UIRedactBuilder { rootFrame: view.frame, transform: CGAffineTransform.identity) - return redactingRegions.reversed() + var swiftUIRedact = [RedactRegion]() + var otherRegions = [RedactRegion]() + + for region in redactingRegions { + if region.type == .redactSwiftUI { + swiftUIRedact.append(region) + } else { + otherRegions.append(region) + } + } + + //The swiftUI type needs to appear first in the list so it always get masked + return swiftUIRedact + otherRegions.reversed() } private func shouldIgnore(view: UIView) -> Bool { @@ -187,11 +202,12 @@ class UIRedactBuilder { let newTransform = concatenateTranform(transform, with: layer) let ignore = !forceRedact && shouldIgnore(view: view) - let redact = forceRedact || shouldRedact(view: view) + let swiftUI = SentryRedactViewHelper.shouldRedactSwiftUI(view) + let redact = forceRedact || shouldRedact(view: view) || swiftUI var enforceRedact = forceRedact if !ignore && redact { - redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .redact, color: self.color(for: view))) + redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: swiftUI ? .redactSwiftUI : .redact, color: self.color(for: view))) guard !view.clipsToBounds else { return } enforceRedact = true @@ -248,14 +264,22 @@ class UIRedactBuilder { Indicates whether the view is opaque and will block other view behind it */ private func isOpaque(_ view: UIView) -> Bool { - return view.alpha == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1 + return SentryRedactViewHelper.shouldClipOut(view) || (view.alpha == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1) } } @objcMembers -class SentryRedactViewHelper: NSObject { +public class SentryRedactViewHelper: NSObject { private static var associatedRedactObjectHandle: UInt8 = 0 private static var associatedIgnoreObjectHandle: UInt8 = 0 + private static var associatedClipOutObjectHandle: UInt8 = 0 + private static var associatedSwiftUIRedactObjectHandle: UInt8 = 0 + + override private init() {} + + static func maskView(_ view: UIView) { + objc_setAssociatedObject(view, &associatedRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN) + } static func shouldMaskView(_ view: UIView) -> Bool { (objc_getAssociatedObject(view, &associatedRedactObjectHandle) as? NSNumber)?.boolValue ?? false @@ -265,13 +289,25 @@ class SentryRedactViewHelper: NSObject { (objc_getAssociatedObject(view, &associatedIgnoreObjectHandle) as? NSNumber)?.boolValue ?? false } - static func maskView(_ view: UIView) { - objc_setAssociatedObject(view, &associatedRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN) - } - static func unmaskView(_ view: UIView) { objc_setAssociatedObject(view, &associatedIgnoreObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN) } + + static func shouldClipOut(_ view: UIView) -> Bool { + (objc_getAssociatedObject(view, &associatedClipOutObjectHandle) as? NSNumber)?.boolValue ?? false + } + + static public func clipOutView(_ view: UIView) { + objc_setAssociatedObject(view, &associatedClipOutObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN) + } + + static func shouldRedactSwiftUI(_ view: UIView) -> Bool { + (objc_getAssociatedObject(view, &associatedSwiftUIRedactObjectHandle) as? NSNumber)?.boolValue ?? false + } + + static public func maskSwiftUI(_ view: UIView) { + objc_setAssociatedObject(view, &associatedSwiftUIRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN) + } } #endif diff --git a/Tests/SentryTests/SwiftUI/SentryRedactModifierTests.swift b/Tests/SentryTests/SwiftUI/SentryRedactModifierTests.swift index d2ae74fc7ed..48caa3246a4 100644 --- a/Tests/SentryTests/SwiftUI/SentryRedactModifierTests.swift +++ b/Tests/SentryTests/SwiftUI/SentryRedactModifierTests.swift @@ -6,12 +6,19 @@ import XCTest class SentryRedactModifierTests: XCTestCase { - func testViewRedacted() throws { + func testViewMask() throws { let text = Text("Hello, World!") let redactedText = text.sentryReplayMask() XCTAssertTrue(redactedText is ModifiedContent) } + + func testViewUnmask() throws { + let text = Text("Hello, World!") + let redactedText = text.sentryReplayUnmask() + + XCTAssertTrue(redactedText is ModifiedContent) + } }