diff --git a/CHANGELOG.md b/CHANGELOG.md index c75dc8ddc7b..3bffea59127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add attributes data to `SentryScope` (#6830) - Add `SentryScope` attributes into log messages (#6834) +- Add integration to collect Metrics, can be enabled by setting `options.enableMetrics = true` (#6956) ### Improvements diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift index 1b241a3acbd..3944d2500c6 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKOverrides.swift @@ -47,6 +47,7 @@ public enum SentrySDKOverrides: String, CaseIterable { case .tracing: return SentrySDKOverrides.Tracing.allCases case .profiling: return SentrySDKOverrides.Profiling.allCases case .networking: return SentrySDKOverrides.Networking.allCases + case .metrics: return SentrySDKOverrides.Metrics.allCases } } @@ -167,6 +168,11 @@ public enum SentrySDKOverrides: String, CaseIterable { case immediateStop = "--io.sentry.profiling.continuous-profiler-immediate-stop" } case profiling = "Profiling" + + public enum Metrics: String, SentrySDKOverride { + case enable = "--io.sentry.metrics.enable" + } + case metrics = "Metrics" } // MARK: Public flag/variable value access @@ -348,6 +354,14 @@ extension SentrySDKOverrides.Special { } } +extension SentrySDKOverrides.Metrics { + public var overrideType: OverrideType { + switch self { + case .enable: return .boolean + } + } +} + // MARK: Disable Everything Helper // These are listed exhaustively, without using default cases, so that when new cases are added to the enums above, the compiler helps remind you to annotate what type it is down here. @@ -429,4 +443,11 @@ extension SentrySDKOverrides.Special { } } +extension SentrySDKOverrides.Metrics { + public var ignoresDisableEverything: Bool { + switch self { + case .enable: return true + } + } +} // swiftlint:enable file_length diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index 9846e1ba4a1..02c666f9fa5 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -156,8 +156,12 @@ public struct SentrySDKWrapper { options.configureUserFeedback = configureFeedback(config:) #endif // !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) + // Integration: Logs options.enableLogs = true + // Integration: Metrics + options.enableMetrics = SentrySDKOverrides.Metrics.enable.boolValue + // Experimental features options.enableFileManagerSwizzling = !SentrySDKOverrides.Other.disableFileManagerSwizzling.boolValue options.experimental.enableUnhandledCPPExceptionsV2 = true diff --git a/Samples/Shared/feature-flags.yml b/Samples/Shared/feature-flags.yml index 9d66c7419e6..83c12b9544d 100644 --- a/Samples/Shared/feature-flags.yml +++ b/Samples/Shared/feature-flags.yml @@ -82,6 +82,9 @@ schemeTemplates: "--io.sentry.other.reject-view-hierarchy-in-before-capture-view-hierarchy": false "--io.sentry.other.reject-all-spans": false + # metrics + "--io.sentry.metrics.enable": false + environmentVariables: # events - variable: "--io.sentry.events.sampleRate" diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index f5450e2f09b..088c50dad63 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -777,6 +777,8 @@ D46712622DCD059900D4074A /* SentryRedactDefaultOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46712612DCD059500D4074A /* SentryRedactDefaultOptionsTests.swift */; }; D46712642DCD063800D4074A /* PreviewRedactOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46712632DCD062700D4074A /* PreviewRedactOptionsTests.swift */; }; D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */; }; + D46B041D2EDF168400AF4A0A /* MetricsIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B041C2EDF167D00AF4A0A /* MetricsIntegration.swift */; }; + D46B04202EDF175C00AF4A0A /* MetricsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B041F2EDF175600AF4A0A /* MetricsIntegrationTests.swift */; }; D473ACD72D8090FC000F1CC6 /* FileManager+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D473ACD62D8090FC000F1CC6 /* FileManager+SentryTracing.swift */; }; D480F9D92DE47A50009A0594 /* TestSentryScopePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9D82DE47A48009A0594 /* TestSentryScopePersistentStore.swift */; }; D480F9DB2DE47AF2009A0594 /* SentryScopePersistentStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9DA2DE47AEB009A0594 /* SentryScopePersistentStoreTests.swift */; }; @@ -2145,6 +2147,8 @@ D46712612DCD059500D4074A /* SentryRedactDefaultOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactDefaultOptionsTests.swift; sourceTree = ""; }; D46712632DCD062700D4074A /* PreviewRedactOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewRedactOptionsTests.swift; sourceTree = ""; }; D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = ""; }; + D46B041C2EDF167D00AF4A0A /* MetricsIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsIntegration.swift; sourceTree = ""; }; + D46B041F2EDF175600AF4A0A /* MetricsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsIntegrationTests.swift; sourceTree = ""; }; D46D45E12D5F3FD600A1CB35 /* Sentry_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Sentry_Base.xctestplan; sourceTree = ""; }; D46D45E92D5F411700A1CB35 /* SentrySwiftUI_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = SentrySwiftUI_Base.xctestplan; path = Plans/SentrySwiftUI_Base.xctestplan; sourceTree = SOURCE_ROOT; }; D473ACD62D8090FC000F1CC6 /* FileManager+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SentryTracing.swift"; sourceTree = ""; }; @@ -3520,6 +3524,7 @@ F498EBA52EE0B8E400F57509 /* SentrySwiftIntegrationInstallerTests.swift */, 843FB3422D156B9900558F18 /* Feedback */, 7BF6505D292B77D100BBA5A8 /* MetricKit */, + D46B041E2EDF173A00AF4A0A /* Metrics */, D808FB85281AB2EF009A2A33 /* UIEvents */, D8AB40D92806EBDC00E5E9F7 /* Screenshot */, 0A9BF4E528A123070068D266 /* ViewHierarchy */, @@ -4392,6 +4397,22 @@ path = IO; sourceTree = ""; }; + D46B04162EDF167800AF4A0A /* Metrics */ = { + isa = PBXGroup; + children = ( + D46B041C2EDF167D00AF4A0A /* MetricsIntegration.swift */, + ); + path = Metrics; + sourceTree = ""; + }; + D46B041E2EDF173A00AF4A0A /* Metrics */ = { + isa = PBXGroup; + children = ( + D46B041F2EDF175600AF4A0A /* MetricsIntegrationTests.swift */, + ); + path = Metrics; + sourceTree = ""; + }; D46D45E22D5F3FD600A1CB35 /* Plans */ = { isa = PBXGroup; children = ( @@ -4876,6 +4897,7 @@ isa = PBXGroup; children = ( 925189AB2EDDA6A300557BD1 /* Log */, + D46B04162EDF167800AF4A0A /* Metrics */, FAB0073C2E9F47DE001C806A /* Session */, FAE579B42E7DBE9400B710F9 /* SentryGlobalEventProcessor.swift */, FAD882C12EDAADF90055AA44 /* SwiftAsyncIntegration.swift */, @@ -6191,6 +6213,7 @@ 62E59A5A2E8FB85300DB7A7B /* SentryTracePropagation.m in Sources */, FAB007522E9FE2FF001C806A /* SentrySwizzleWrapper.swift in Sources */, 62E300942D5202890037AA3F /* SentryExceptionCodable.swift in Sources */, + D46B041D2EDF168400AF4A0A /* MetricsIntegration.swift in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */, @@ -6278,6 +6301,7 @@ 7BFAA6E7297AA16A00E7E02E /* SentryCrashMonitor_CppException_Tests.mm in Sources */, 9286059929A50BAB00F96038 /* SentryGeoTests.swift in Sources */, 621655662DB12A8900810504 /* SentryCrashMach-OTests.m in Sources */, + D46B04202EDF175C00AF4A0A /* MetricsIntegrationTests.swift in Sources */, 62B220BB2E93A9EC004620FF /* SentryTracePropagationTests.swift in Sources */, D8B76B0828081461000A58C4 /* TestSentryScreenshotProvider.swift in Sources */, A8AFFCD22907DA7600967CD7 /* SentryHttpStatusCodeRangeTests.swift in Sources */, diff --git a/SentryTestUtils/Sources/TestOptions.swift b/SentryTestUtils/Sources/TestOptions.swift index 3de768a42a5..532ce456212 100644 --- a/SentryTestUtils/Sources/TestOptions.swift +++ b/SentryTestUtils/Sources/TestOptions.swift @@ -21,6 +21,7 @@ public extension Options { attachViewHierarchy = false enableUIViewControllerTracing = false #endif + enableMetrics = false } static func noIntegrations() -> Options { diff --git a/Sources/Sentry/SentryDependencyContainerSwiftHelper.m b/Sources/Sentry/SentryDependencyContainerSwiftHelper.m index 1108668cb6a..f52b3ea47ad 100644 --- a/Sources/Sentry/SentryDependencyContainerSwiftHelper.m +++ b/Sources/Sentry/SentryDependencyContainerSwiftHelper.m @@ -45,6 +45,11 @@ + (BOOL)enableLogs:(SentryOptions *)options return options.enableLogs; } ++ (BOOL)enableMetrics:(SentryOptions *)options +{ + return options.enableMetrics; +} + + (NSArray *)enabledFeatures:(SentryOptions *)options { return [SentryEnabledFeaturesBuilder getEnabledFeaturesWithOptions:options]; diff --git a/Sources/Sentry/SentryOptionsInternal.m b/Sources/Sentry/SentryOptionsInternal.m index 5a7bcd03611..f3bb4533139 100644 --- a/Sources/Sentry/SentryOptionsInternal.m +++ b/Sources/Sentry/SentryOptionsInternal.m @@ -95,6 +95,9 @@ + (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enableLogs"] block:^(BOOL value) { sentryOptions.enableLogs = value; }]; + [self setBool:options[@"enableMetrics"] + block:^(BOOL value) { sentryOptions.enableMetrics = value; }]; + [self setBool:options[@"enableNetworkBreadcrumbs"] block:^(BOOL value) { sentryOptions.enableNetworkBreadcrumbs = value; }]; diff --git a/Sources/Sentry/include/SentryDependencyContainerSwiftHelper.h b/Sources/Sentry/include/SentryDependencyContainerSwiftHelper.h index c28159bfe74..aab804e61af 100644 --- a/Sources/Sentry/include/SentryDependencyContainerSwiftHelper.h +++ b/Sources/Sentry/include/SentryDependencyContainerSwiftHelper.h @@ -41,6 +41,7 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)environment:(SentryOptionsObjC *)options; + (NSObject *_Nullable)beforeSendLog:(NSObject *)beforeSendLog options:(SentryOptionsObjC *)options; + (BOOL)enableLogs:(SentryOptionsObjC *)options; ++ (BOOL)enableMetrics:(SentryOptionsObjC *)options; + (NSArray *)enabledFeatures:(SentryOptionsObjC *)options; + (BOOL)sendDefaultPii:(SentryOptionsObjC *)options; diff --git a/Sources/Swift/Core/Integrations/Integrations.swift b/Sources/Swift/Core/Integrations/Integrations.swift index 42f2f97aed5..1fed48b7e72 100644 --- a/Sources/Swift/Core/Integrations/Integrations.swift +++ b/Sources/Swift/Core/Integrations/Integrations.swift @@ -37,7 +37,8 @@ private struct AnyIntegration { var integrations: [AnyIntegration] = [ .init(SwiftAsyncIntegration.self), - .init(SentryAutoSessionTrackingIntegration.self) + .init(SentryAutoSessionTrackingIntegration.self), + .init(MetricsIntegration.self) ] #if os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift b/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift index d7566792abb..7319c84cfe8 100644 --- a/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift +++ b/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift @@ -45,6 +45,9 @@ import Foundation if options.experimental.enableUnhandledCPPExceptionsV2 { features.append("unhandledCPPExceptionsV2") } + if options.enableMetrics { + features.append("metrics") + } return features } diff --git a/Sources/Swift/Integrations/Metrics/MetricsIntegration.swift b/Sources/Swift/Integrations/Metrics/MetricsIntegration.swift new file mode 100644 index 00000000000..2e3dcb025ca --- /dev/null +++ b/Sources/Swift/Integrations/Metrics/MetricsIntegration.swift @@ -0,0 +1,13 @@ +final class MetricsIntegration: NSObject, SwiftIntegration { + init?(with options: Options, dependencies: Dependencies) { + guard options.enableMetrics else { return nil } + + SentrySDKLog.debug("Integration initialized") + } + + func uninstall() {} + + static var name: String { + "SentryMetricsIntegration" + } +} diff --git a/Sources/Swift/Options.swift b/Sources/Swift/Options.swift index d71d20f4c24..a8d9054beda 100644 --- a/Sources/Swift/Options.swift +++ b/Sources/Swift/Options.swift @@ -130,6 +130,11 @@ /// @note Default value is @c false. @objc public var enableLogs: Bool = false + /// When enabled, the SDK sends metrics to Sentry. Metrics can be captured using the SentrySDK.metrics + /// API, which allows you to send, view and query counters, gauges and measurements. + /// @note Default value is @c false. + @objc public var enableMetrics: Bool = false + /// Use this callback to drop or modify a log before the SDK sends it to Sentry. Return nil to /// drop the log. @objc public var beforeSendLog: ((SentryLog) -> SentryLog?)? diff --git a/Tests/SentryTests/Integrations/Metrics/MetricsIntegrationTests.swift b/Tests/SentryTests/Integrations/Metrics/MetricsIntegrationTests.swift new file mode 100644 index 00000000000..33c6c70b9bd --- /dev/null +++ b/Tests/SentryTests/Integrations/Metrics/MetricsIntegrationTests.swift @@ -0,0 +1,48 @@ +import Foundation +@_spi(Private) @testable import Sentry +@_spi(Private) import SentryTestUtils +import XCTest + +class MetricsIntegrationTests: XCTestCase { + + override func tearDown() { + super.tearDown() + clearTestState() + } + + // MARK: - Tests + + func testStartSDK_whenIntegrationIsNotEnabled_shouldNotBeInstalled() { + // -- Act -- + startSDK(isEnabled: false) + + // -- Assert -- + XCTAssertEqual(SentrySDKInternal.currentHub().trimmedInstalledIntegrationNames().count, 0) + } + + func testStartSDK_whenIntegrationIsEnabled_shouldBeInstalled() { + // -- Act -- + startSDK(isEnabled: true) + + // -- Assert -- + XCTAssertEqual(SentrySDKInternal.currentHub().trimmedInstalledIntegrationNames().count, 1) + } + + // MARK: - Helpers + + private func startSDK(isEnabled: Bool, configure: ((Options) -> Void)? = nil) { + SentrySDK.start { + $0.dsn = TestConstants.dsnForTestCase(type: MetricsIntegrationTests.self) + $0.removeAllIntegrations() + + $0.enableMetrics = isEnabled + + configure?($0) + } + SentrySDKInternal.currentHub().startSession() + } + + private func getSut() throws -> MetricsIntegration { + return try XCTUnwrap(SentrySDKInternal.currentHub().installedIntegrations().first as? MetricsIntegration) + } +} diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 9ca5afb041e..44da6548bf8 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -205,6 +205,11 @@ - (void)testEnableLogs [self testBooleanField:@"enableLogs" defaultValue:NO]; } +- (void)testEnableMetrics +{ + [self testBooleanField:@"enableMetrics" defaultValue:NO]; +} + - (void)testEnableAutoBreadcrumbTracking { [self testBooleanField:@"enableAutoBreadcrumbTracking"]; diff --git a/sdk_api.json b/sdk_api.json index cd0b7a2b292..390cd65ef00 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -45757,6 +45757,82 @@ } ] }, + { + "kind": "Var", + "name": "enableMetrics", + "printedName": "enableMetrics", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryOptions(py)enableMetrics", + "mangledName": "$s6Sentry7OptionsC13enableMetricsSbvp", + "moduleName": "Sentry", + "declAttributes": [ + "Final", + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryOptions(im)enableMetrics", + "mangledName": "$s6Sentry7OptionsC13enableMetricsSbvg", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "Final", + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryOptions(im)setEnableMetrics:", + "mangledName": "$s6Sentry7OptionsC13enableMetricsSbvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "Final", + "ObjC" + ], + "accessorKind": "set" + } + ] + }, { "kind": "Var", "name": "beforeSendLog",