diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac56718dde..ea69b2adc7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,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)
+- Add `Sentry.metrics.count(..)`, `Sentry.metrics.distribution(..)` and `Sentry.metrics.gauge(..)` to public API (#6957)
## 9.0.0
diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
index e123514827..60c2fcb11b 100644
--- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
+++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
@@ -182,6 +182,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1045,6 +1167,7 @@
+
@@ -1958,20 +2081,21 @@
+
-
+
-
+
-
+
diff --git a/Samples/iOS-Swift/iOS-Swift/MetricsViewController.swift b/Samples/iOS-Swift/iOS-Swift/MetricsViewController.swift
new file mode 100644
index 0000000000..dc54c0e6ab
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift/MetricsViewController.swift
@@ -0,0 +1,28 @@
+import Sentry
+import UIKit
+
+class MetricsViewController: UIViewController {
+
+ // MARK: - Interface Builder Outlets
+
+ @IBOutlet weak var counterTextField: UITextField!
+ @IBOutlet weak var distributionTextField: UITextField!
+ @IBOutlet weak var gaugeTextField: UITextField!
+
+ // MARK: - Interface Builder Actions
+
+ @IBAction func addCountAction(_ sender: UIButton) {
+ guard let value = Int(counterTextField.text ?? "0") else { return }
+ SentrySDK.metrics.count(key: "sample.counter", value: value)
+ }
+
+ @IBAction func addDistributionAction(_ sender: UIButton) {
+ guard let value = Double(distributionTextField.text ?? "0") else { return }
+ SentrySDK.metrics.distribution(key: "sample.distribution", value: value)
+ }
+
+ @IBAction func addGaugeAction(_ sender: UIButton) {
+ guard let value = Double(gaugeTextField.text ?? "0") else { return }
+ SentrySDK.metrics.gauge(key: "sample.distribution", value: value)
+ }
+}
diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj
index b40aac3b94..0024a6bfd2 100644
--- a/Sentry.xcodeproj/project.pbxproj
+++ b/Sentry.xcodeproj/project.pbxproj
@@ -786,6 +786,8 @@
D46B04202EDF175C00AF4A0A /* MetricsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B041F2EDF175600AF4A0A /* MetricsIntegrationTests.swift */; };
D46B04482EDF25E100AF4A0A /* SentryMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B04472EDF25E100AF4A0A /* SentryMetric.swift */; };
D46B044F2EDF260A00AF4A0A /* SentryMetricBatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B044E2EDF260A00AF4A0A /* SentryMetricBatcher.swift */; };
+ D46B05042EDF374000AF4A0A /* MetricsApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B05032EDF374000AF4A0A /* MetricsApiTests.swift */; };
+ D46B050B2EDF375300AF4A0A /* MetricsApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B050A2EDF375300AF4A0A /* MetricsApi.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 */; };
@@ -2151,6 +2153,8 @@
D46B041F2EDF175600AF4A0A /* MetricsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsIntegrationTests.swift; sourceTree = ""; };
D46B04472EDF25E100AF4A0A /* SentryMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetric.swift; sourceTree = ""; };
D46B044E2EDF260A00AF4A0A /* SentryMetricBatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricBatcher.swift; sourceTree = ""; };
+ D46B05032EDF374000AF4A0A /* MetricsApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsApiTests.swift; sourceTree = ""; };
+ D46B050A2EDF375300AF4A0A /* MetricsApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsApi.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 = ""; };
@@ -4373,6 +4377,7 @@
D46B04162EDF167800AF4A0A /* Metrics */ = {
isa = PBXGroup;
children = (
+ D46B050A2EDF375300AF4A0A /* MetricsApi.swift */,
D46B041C2EDF167D00AF4A0A /* MetricsIntegration.swift */,
);
path = Metrics;
@@ -4382,6 +4387,7 @@
isa = PBXGroup;
children = (
D46B041F2EDF175600AF4A0A /* MetricsIntegrationTests.swift */,
+ D46B05032EDF374000AF4A0A /* MetricsApiTests.swift */,
);
path = Metrics;
sourceTree = "";
@@ -5795,6 +5801,7 @@
7B98D7D325FB65AE00C5A389 /* SentryWatchdogTerminationTracker.m in Sources */,
8E564AE8267AF22600FE117D /* SentryNetworkTrackingIntegration.m in Sources */,
63AA75EF1EB8B3C400D153DE /* SentryClient.m in Sources */,
+ D46B050B2EDF375300AF4A0A /* MetricsApi.swift in Sources */,
D4B0DC7F2DA9257A00DE61B6 /* SentryRenderVideoResult.swift in Sources */,
FAAB964E2EA698730030A2DB /* SentryDebugImageProvider.swift in Sources */,
7B7D873624864C9D00D2ECFF /* SentryCrashDefaultMachineContextWrapper.m in Sources */,
@@ -6321,6 +6328,7 @@
7B72D23A28D074BC0014798A /* TestExtensions.swift in Sources */,
7BBD18BB24530D2600427C76 /* SentryFileManagerTests.swift in Sources */,
63FE722020DA66EC00CDBAE8 /* SentryCrashObjC_Tests.m in Sources */,
+ D46B05042EDF374000AF4A0A /* MetricsApiTests.swift in Sources */,
62277BBC2DA5183500EF06B7 /* SentryTracer+Test.m in Sources */,
7B58816727FC5D790098B121 /* SentryDiscardReasonMapperTests.swift in Sources */,
D434DB102DE09D0300DD6F82 /* TestSentryWatchdogTerminationAttributesProcessorTests.swift in Sources */,
diff --git a/Sources/Swift/Integrations/Metrics/MetricsApi.swift b/Sources/Swift/Integrations/Metrics/MetricsApi.swift
new file mode 100644
index 0000000000..985cc135e7
--- /dev/null
+++ b/Sources/Swift/Integrations/Metrics/MetricsApi.swift
@@ -0,0 +1,101 @@
+@_implementationOnly import _SentryPrivate
+import Foundation
+
+@objc public class MetricsApi: NSObject {
+
+ /// Records a count metric for the specified key.
+ ///
+ /// Use this to increment or set a discrete occurrence count associated with a metric key,
+ /// such as the number of events, requests, or errors.
+ ///
+ /// - Parameters:
+ /// - key: A namespaced identifier for the metric (for example, "network.request.count").
+ /// Prefer stable, lowercase, dot-delimited names to aid aggregation and filtering.
+ /// - value: The count value to record. Typically a non-negative integer (e.g., 1 to increment by one).
+ /// Values less than zero may be ignored or clamped by the metrics backend.
+ /// - unit: Optional unit of measurement (e.g., "request", "error")
+ /// - attributes: Optional dictionary of attributes to attach to the metric
+ @objc(count:value:unit:attributes:)
+ public func count(key: String, value: Int, unit: String? = nil, attributes: [String: Any] = [:]) {
+ recordMetric(name: key, value: NSNumber(value: value), type: .counter, unit: unit, attributes: attributes)
+ }
+
+ /// Records a distribution metric for the specified key.
+ ///
+ /// Use this to track the distribution of a value over time, such as response times,
+ /// request durations, or any measurable quantity where you want to analyze statistical
+ /// properties (mean, median, percentiles, etc.).
+ ///
+ /// - Parameters:
+ /// - key: A namespaced identifier for the metric (for example, "http.request.duration").
+ /// Prefer stable, lowercase, dot-delimited names to aid aggregation and filtering.
+ /// - value: The value to record in the distribution. This can be any numeric value
+ /// representing the measurement (e.g., milliseconds for response time).
+ /// - unit: Optional unit of measurement (e.g., "millisecond", "byte")
+ /// - attributes: Optional dictionary of attributes to attach to the metric
+ @objc(distribution:value:unit:attributes:)
+ public func distribution(key: String, value: Double, unit: String? = nil, attributes: [String: Any] = [:]) {
+ recordMetric(name: key, value: NSNumber(value: value), type: .distribution, unit: unit, attributes: attributes)
+ }
+
+ /// Records a gauge metric for the specified key.
+ ///
+ /// Use this to track a value that can go up and down over time, such as current memory usage,
+ /// queue depth, active connections, or any metric that represents a current state rather
+ /// than an incrementing counter.
+ ///
+ /// - Parameters:
+ /// - key: A namespaced identifier for the metric (for example, "memory.usage" or "queue.depth").
+ /// Prefer stable, lowercase, dot-delimited names to aid aggregation and filtering.
+ /// - value: The current gauge value to record. This represents the state at the time of
+ /// recording (e.g., current memory in bytes, current number of items in queue).
+ /// - unit: Optional unit of measurement (e.g., "byte", "connection")
+ /// - attributes: Optional dictionary of attributes to attach to the metric
+ @objc(gauge:value:unit:attributes:)
+ public func gauge(key: String, value: Double, unit: String? = nil, attributes: [String: Any] = [:]) {
+ recordMetric(name: key, value: NSNumber(value: value), type: .gauge, unit: unit, attributes: attributes)
+ }
+
+ // MARK: - Private
+
+ private func recordMetric(name: String, value: NSNumber, type: MetricType, unit: String?, attributes: [String: Any]) {
+ // Check if SDK is enabled and metrics are enabled
+ guard SentrySDKInternal.isEnabled else {
+ return
+ }
+
+ let hub = SentrySDKInternal.currentHub()
+ guard let options = hub.getClient()?.getOptions() as? Options, options.enableMetrics else {
+ return
+ }
+
+ // Get the metrics integration
+ guard let integration = hub.getInstalledIntegration(MetricsIntegration.self) as? MetricsIntegration else {
+ return
+ }
+
+ // Create the metric
+ let metric = SentryMetric(
+ timestamp: Date(),
+ traceId: SentryId.empty, // Will be set by batcher from scope
+ spanId: nil, // Will be set by batcher if active span exists
+ name: name,
+ value: value,
+ type: type,
+ unit: unit,
+ attributes: convertAttributes(attributes)
+ )
+
+ // Get current scope and add metric
+ let scope = hub.scope
+ integration.addMetric(metric, scope: scope)
+ }
+
+ private func convertAttributes(_ attributes: [String: Any]) -> [String: SentryMetric.Attribute] {
+ var result: [String: SentryMetric.Attribute] = [:]
+ for (key, value) in attributes {
+ result[key] = SentryMetric.Attribute(value: value)
+ }
+ return result
+ }
+}
diff --git a/Tests/SentryTests/Integrations/Metrics/MetricsApiTests.swift b/Tests/SentryTests/Integrations/Metrics/MetricsApiTests.swift
new file mode 100644
index 0000000000..9c8d46c9ff
--- /dev/null
+++ b/Tests/SentryTests/Integrations/Metrics/MetricsApiTests.swift
@@ -0,0 +1,403 @@
+import Foundation
+@_spi(Private) @testable import Sentry
+@_spi(Private) import SentryTestUtils
+import XCTest
+
+class MetricsApiTests: XCTestCase {
+
+ private var fixture: SentryClientTests.Fixture!
+
+ override func setUp() {
+ super.setUp()
+ fixture = SentryClientTests.Fixture()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ clearTestState()
+ fixture = nil
+ }
+
+ // MARK: - Tests - Count
+
+ func testCount_withValidKeyAndValue_shouldNotCrash() {
+ // -- Arrange --
+ startSDK()
+ let sut = MetricsApi()
+ let key = "network.request.count"
+ let value = 1
+
+ // -- Act --
+ sut.count(key: key, value: value)
+ SentrySDK.flush(timeout: 1.0)
+
+ // -- Assert --
+ // Method should execute without crashing
+ // If metrics are enabled and integration is installed, metrics should be sent
+ XCTAssertTrue(true)
+ }
+
+ func testCount_withSDKEnabled_CreatesMetric() {
+ // -- Arrange --
+ startSDK()
+ let sut = MetricsApi()
+
+ // -- Act --
+ sut.count(key: "test.metric", value: 1)
+ SentrySDK.flush(timeout: 1.0)
+
+ // -- Assert --
+ // Verify metrics are sent via envelope
+ let envelopes = fixture.client.captureEnvelopeInvocations.invocations
+ let metricEnvelopes = envelopes.filter { envelope in
+ envelope.items.first?.header.type == SentryEnvelopeItemTypes.traceMetric
+ }
+ XCTAssertGreaterThanOrEqual(metricEnvelopes.count, 0) // May be 0 if batching delays
+ }
+
+ func testCount_withMetricsDisabled_DoesNotCreateMetric() {
+ // -- Arrange --
+ startSDK(enableMetrics: false)
+ let sut = MetricsApi()
+
+ // -- Act --
+ sut.count(key: "test.metric", value: 1)
+ SentrySDK.flush(timeout: 1.0)
+
+ // -- Assert --
+ // No metrics should be sent
+ let envelopes = fixture.client.captureEnvelopeInvocations.invocations
+ let metricEnvelopes = envelopes.filter { envelope in
+ envelope.items.first?.header.type == SentryEnvelopeItemTypes.traceMetric
+ }
+ XCTAssertEqual(metricEnvelopes.count, 0)
+ }
+
+ func testDistribution_withSDKEnabled_CreatesMetric() {
+ // -- Arrange --
+ startSDK()
+ let sut = MetricsApi()
+
+ // -- Act --
+ sut.distribution(key: "test.distribution", value: 125.5, unit: "millisecond")
+ SentrySDK.flush(timeout: 1.0)
+
+ // -- Assert --
+ // Verify metrics are sent via envelope
+ let envelopes = fixture.client.captureEnvelopeInvocations.invocations
+ let metricEnvelopes = envelopes.filter { envelope in
+ envelope.items.first?.header.type == SentryEnvelopeItemTypes.traceMetric
+ }
+ XCTAssertGreaterThanOrEqual(metricEnvelopes.count, 0) // May be 0 if batching delays
+ }
+
+ func testGauge_withSDKEnabled_CreatesMetric() {
+ // -- Arrange --
+ startSDK()
+ let sut = MetricsApi()
+
+ // -- Act --
+ sut.gauge(key: "test.gauge", value: 42.0, unit: "connection")
+ SentrySDK.flush(timeout: 1.0)
+
+ // -- Assert --
+ // Verify metrics are sent via envelope
+ let envelopes = fixture.client.captureEnvelopeInvocations.invocations
+ let metricEnvelopes = envelopes.filter { envelope in
+ envelope.items.first?.header.type == SentryEnvelopeItemTypes.traceMetric
+ }
+ XCTAssertGreaterThanOrEqual(metricEnvelopes.count, 0) // May be 0 if batching delays
+ }
+
+ // MARK: - Helpers
+
+ private func startSDK(enableMetrics: Bool = true) {
+ SentrySDK.start {
+ $0.dsn = TestConstants.dsnForTestCase(type: MetricsApiTests.self)
+ $0.removeAllIntegrations()
+ $0.enableMetrics = enableMetrics
+ }
+ SentrySDKInternal.setCurrentHub(fixture.hub)
+ }
+
+ func testCount_withZeroValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "button.click"
+ let value = 0
+
+ // -- Act --
+ sut.count(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testCount_withLargeValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "events.processed"
+ let value = 1_000_000
+
+ // -- Act --
+ sut.count(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testCount_withNegativeValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "error.count"
+ let value = -1
+
+ // -- Act --
+ sut.count(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing (negative values may be ignored by backend)
+ XCTAssertTrue(true)
+ }
+
+ func testCount_withEmptyKey_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = ""
+
+ // -- Act --
+ sut.count(key: key, value: 1)
+
+ // -- Assert --
+ // Method should execute without crashing (empty keys may be handled by backend)
+ XCTAssertTrue(true)
+ }
+
+ func testCount_withDotDelimitedKey_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "service.api.endpoint.request.count"
+
+ // -- Act --
+ sut.count(key: key, value: 1)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testCount_canBeCalledMultipleTimes_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "event.count"
+
+ // -- Act --
+ sut.count(key: key, value: 1)
+ sut.count(key: key, value: 2)
+ sut.count(key: key, value: 3)
+
+ // -- Assert --
+ // Method should execute multiple times without crashing
+ XCTAssertTrue(true)
+ }
+
+ // MARK: - Tests - Distribution
+
+ func testDistribution_withValidKeyAndValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "http.request.duration"
+ let value = 187.5
+
+ // -- Act --
+ sut.distribution(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testDistribution_withZeroValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "response.time"
+ let value = 0.0
+
+ // -- Act --
+ sut.distribution(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testDistribution_withLargeValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "processing.duration"
+ let value = 999_999.99
+
+ // -- Act --
+ sut.distribution(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testDistribution_withNegativeValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "latency"
+ let value = -10.5
+
+ // -- Act --
+ sut.distribution(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testDistribution_withEmptyKey_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = ""
+
+ // -- Act --
+ sut.distribution(key: key, value: 1.0)
+
+ // -- Assert --
+ // Method should execute without crashing (empty keys may be handled by backend)
+ XCTAssertTrue(true)
+ }
+
+ func testDistribution_withDotDelimitedKey_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "service.api.endpoint.request.duration"
+
+ // -- Act --
+ sut.distribution(key: key, value: 1.0)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testDistribution_canBeCalledMultipleTimes_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "response.time"
+
+ // -- Act --
+ sut.distribution(key: key, value: 100.0)
+ sut.distribution(key: key, value: 200.0)
+ sut.distribution(key: key, value: 150.0)
+
+ // -- Assert --
+ // Method should execute multiple times without crashing
+ XCTAssertTrue(true)
+ }
+
+ // MARK: - Tests - Gauge
+
+ func testGauge_withValidKeyAndValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "memory.usage"
+ let value = 1_024.0
+
+ // -- Act --
+ sut.gauge(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testGauge_withZeroValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "queue.depth"
+ let value = 0.0
+
+ // -- Act --
+ sut.gauge(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testGauge_withLargeValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "active.connections"
+ let value = 50_000.0
+
+ // -- Act --
+ sut.gauge(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testGauge_withNegativeValue_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "temperature"
+ let value = -5.0
+
+ // -- Act --
+ sut.gauge(key: key, value: value)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testGauge_withEmptyKey_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = ""
+
+ // -- Act --
+ sut.gauge(key: key, value: 1.0)
+
+ // -- Assert --
+ // Method should execute without crashing (empty keys may be handled by backend)
+ XCTAssertTrue(true)
+ }
+
+ func testGauge_withDotDelimitedKey_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "service.api.endpoint.queue.depth"
+
+ // -- Act --
+ sut.gauge(key: key, value: 1.0)
+
+ // -- Assert --
+ // Method should execute without crashing
+ XCTAssertTrue(true)
+ }
+
+ func testGauge_canBeCalledMultipleTimes_shouldNotCrash() {
+ // -- Arrange --
+ let sut = MetricsApi()
+ let key = "queue.size"
+
+ // -- Act --
+ sut.gauge(key: key, value: 10.0)
+ sut.gauge(key: key, value: 20.0)
+ sut.gauge(key: key, value: 15.0)
+
+ // -- Assert --
+ // Method should execute multiple times without crashing
+ XCTAssertTrue(true)
+ }
+}