diff --git a/CHANGELOG.md b/CHANGELOG.md index affd51d8516..e68aa483407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add integration to collect Metrics, can be enabled by setting `options.enableMetrics = true` (#6956) + ### Fixes - The transport now correctly discard envelopes on 4xx and 5xx responses and records client reports `send_error` (#6618) This also fixes edge cases in which the SDK kept retrying sending a faulty envelope until the offline cache overflowed. 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..257706bbe5d 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.experimental.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..1b3d8a2ca8b 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": true + environmentVariables: # events - variable: "--io.sentry.events.sampleRate" diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 0d41e416922..05c0e88be74 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 /* SentryMetricsIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B041C2EDF167D00AF4A0A /* SentryMetricsIntegration.swift */; }; + D46B04202EDF175C00AF4A0A /* SentryMetricsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B041F2EDF175600AF4A0A /* SentryMetricsIntegrationTests.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 */; }; @@ -2147,6 +2149,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 /* SentryMetricsIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsIntegration.swift; sourceTree = ""; }; + D46B041F2EDF175600AF4A0A /* SentryMetricsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsIntegrationTests.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 = ""; }; @@ -3521,6 +3525,7 @@ F498EBA52EE0B8E400F57509 /* SentrySwiftIntegrationInstallerTests.swift */, 843FB3422D156B9900558F18 /* Feedback */, 7BF6505D292B77D100BBA5A8 /* MetricKit */, + D46B041E2EDF173A00AF4A0A /* Metrics */, D808FB85281AB2EF009A2A33 /* UIEvents */, D8AB40D92806EBDC00E5E9F7 /* Screenshot */, 0A9BF4E528A123070068D266 /* ViewHierarchy */, @@ -4394,6 +4399,22 @@ path = IO; sourceTree = ""; }; + D46B04162EDF167800AF4A0A /* Metrics */ = { + isa = PBXGroup; + children = ( + D46B041C2EDF167D00AF4A0A /* SentryMetricsIntegration.swift */, + ); + path = Metrics; + sourceTree = ""; + }; + D46B041E2EDF173A00AF4A0A /* Metrics */ = { + isa = PBXGroup; + children = ( + D46B041F2EDF175600AF4A0A /* SentryMetricsIntegrationTests.swift */, + ); + path = Metrics; + sourceTree = ""; + }; D46D45E22D5F3FD600A1CB35 /* Plans */ = { isa = PBXGroup; children = ( @@ -4878,6 +4899,7 @@ children = ( F4FE86BC2EECAC31003D845F /* Screenshot */, 925189AB2EDDA6A300557BD1 /* Log */, + D46B04162EDF167800AF4A0A /* Metrics */, FAB0073C2E9F47DE001C806A /* Session */, FAE579B42E7DBE9400B710F9 /* SentryGlobalEventProcessor.swift */, FAD882C12EDAADF90055AA44 /* SwiftAsyncIntegration.swift */, @@ -6195,6 +6217,7 @@ 62E59A5A2E8FB85300DB7A7B /* SentryTracePropagation.m in Sources */, FAB007522E9FE2FF001C806A /* SentrySwizzleWrapper.swift in Sources */, 62E300942D5202890037AA3F /* SentryExceptionCodable.swift in Sources */, + D46B041D2EDF168400AF4A0A /* SentryMetricsIntegration.swift in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */, @@ -6282,6 +6305,7 @@ 7BFAA6E7297AA16A00E7E02E /* SentryCrashMonitor_CppException_Tests.mm in Sources */, 9286059929A50BAB00F96038 /* SentryGeoTests.swift in Sources */, 621655662DB12A8900810504 /* SentryCrashMach-OTests.m in Sources */, + D46B04202EDF175C00AF4A0A /* SentryMetricsIntegrationTests.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..0b20f594ee3 100644 --- a/SentryTestUtils/Sources/TestOptions.swift +++ b/SentryTestUtils/Sources/TestOptions.swift @@ -21,6 +21,7 @@ public extension Options { attachViewHierarchy = false enableUIViewControllerTracing = false #endif + experimental.enableMetrics = false } static func noIntegrations() -> Options { diff --git a/Sources/Swift/Core/Integrations/Integrations.swift b/Sources/Swift/Core/Integrations/Integrations.swift index 4560548fdfe..86b4cd049f5 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(SentryMetricsIntegration.self) ] #if os(iOS) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift b/Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift index d7566792abb..5c41a28652d 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.experimental.enableMetrics { + features.append("metrics") + } return features } diff --git a/Sources/Swift/Integrations/Metrics/SentryMetricsIntegration.swift b/Sources/Swift/Integrations/Metrics/SentryMetricsIntegration.swift new file mode 100644 index 00000000000..ea296a04b0d --- /dev/null +++ b/Sources/Swift/Integrations/Metrics/SentryMetricsIntegration.swift @@ -0,0 +1,13 @@ +final class SentryMetricsIntegration: NSObject, SwiftIntegration { + init?(with options: Options, dependencies: Dependencies) { + guard options.experimental.enableMetrics else { return nil } + + SentrySDKLog.debug("Integration initialized") + } + + func uninstall() {} + + static var name: String { + "SentryMetricsIntegration" + } +} diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift index 63aca7cbf61..0c5a29d568e 100644 --- a/Sources/Swift/SentryExperimentalOptions.swift +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -29,6 +29,11 @@ public final class SentryExperimentalOptions: NSObject { */ public var enableSessionReplayInUnreliableEnvironment = 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 + @_spi(Private) public func validateOptions(_ options: [String: Any]?) { } } diff --git a/Tests/SentryTests/Helper/SentryEnabledFeaturesBuilderTests.swift b/Tests/SentryTests/Helper/SentryEnabledFeaturesBuilderTests.swift index 731975563de..a78546245c1 100644 --- a/Tests/SentryTests/Helper/SentryEnabledFeaturesBuilderTests.swift +++ b/Tests/SentryTests/Helper/SentryEnabledFeaturesBuilderTests.swift @@ -169,4 +169,30 @@ final class SentryEnabledFeaturesBuilderTests: XCTestCase { // -- Assert -- XCTAssert(features.contains("unhandledCPPExceptionsV2")) } + + func testEnableMetrics_isEnabled_shouldAddFeature() throws { + // -- Arrange -- + let options = Options() + + options.experimental.enableMetrics = true + + // -- Act -- + let features = SentryEnabledFeaturesBuilder.getEnabledFeatures(options: options) + + // -- Assert -- + XCTAssert(features.contains("metrics")) + } + + func testEnableMetrics_isDisabled_shouldNotAddFeature() throws { + // -- Arrange -- + let options = Options() + + options.experimental.enableMetrics = false + + // -- Act -- + let features = SentryEnabledFeaturesBuilder.getEnabledFeatures(options: options) + + // -- Assert -- + XCTAssertFalse(features.contains("metrics")) + } } diff --git a/Tests/SentryTests/Integrations/Metrics/SentryMetricsIntegrationTests.swift b/Tests/SentryTests/Integrations/Metrics/SentryMetricsIntegrationTests.swift new file mode 100644 index 00000000000..18b012f68bf --- /dev/null +++ b/Tests/SentryTests/Integrations/Metrics/SentryMetricsIntegrationTests.swift @@ -0,0 +1,43 @@ +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().first, "Metrics") + } + + // MARK: - Helpers + + private func startSDK(isEnabled: Bool, configure: ((Options) -> Void)? = nil) { + SentrySDK.start { + $0.dsn = TestConstants.dsnForTestCase(type: MetricsIntegrationTests.self) + $0.removeAllIntegrations() + + $0.experimental.enableMetrics = isEnabled + + configure?($0) + } + } +} diff --git a/sdk_api.json b/sdk_api.json index 92b23f9df1b..d6cfe288070 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -47311,6 +47311,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)SentryExperimentalOptions(py)enableMetrics", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC13enableMetricsSbvp", + "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)SentryExperimentalOptions(im)enableMetrics", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC13enableMetricsSbvg", + "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)SentryExperimentalOptions(im)setEnableMetrics:", + "mangledName": "$s6Sentry0A19ExperimentalOptionsC13enableMetricsSbvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "Final", + "ObjC" + ], + "accessorKind": "set" + } + ] + }, { "kind": "Constructor", "name": "init",