Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
303d533
feat(metrics): Add implementation for metrics envelope item
philprime Dec 2, 2025
b991cda
Remove unrelated change
philprime Dec 2, 2025
2aa1f43
fix compilation
philprime Dec 2, 2025
6dfe9bb
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 2, 2025
0ae250e
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 11, 2025
a7c7df9
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 11, 2025
c143d90
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 15, 2025
70d5fd3
refactor latest item batcher
philprime Dec 15, 2025
64cc81f
fixes
philprime Dec 15, 2025
f7a67bb
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 15, 2025
70241f7
cleanup
philprime Dec 15, 2025
40b52a3
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 15, 2025
861161b
cleanup
philprime Dec 15, 2025
c6ce033
WIP
philprime Dec 15, 2025
d073e22
WIP
philprime Dec 15, 2025
4980aad
cleanup
philprime Dec 15, 2025
56320d5
Remove spanId from Metric struct and related tests; update BatcherSco…
philprime Dec 15, 2025
3eca8ca
fix warnings on set
philprime Dec 15, 2025
1b939e6
add comment explaining reasoning for jsonserialization in tests
philprime Dec 15, 2025
06e863a
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 15, 2025
74902aa
WIP
philprime Dec 15, 2025
6dca42f
Update Sources/Swift/Integrations/Metrics/MetricsIntegration.swift
philprime Dec 15, 2025
922ea27
WIP
philprime Dec 15, 2025
b9ed1a3
fix integration setup in tests
philprime Dec 15, 2025
2a05eb7
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 16, 2025
c8b4ab5
fix tests for integraiton checking
philprime Dec 16, 2025
1a6cf02
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 16, 2025
62ee6ad
change metric batcher to struct
philprime Dec 16, 2025
fe0a0ef
update public api
philprime Dec 16, 2025
f10a253
update changelog
philprime Dec 16, 2025
f9efc90
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 17, 2025
a12babf
WIP
philprime Dec 17, 2025
ba4edde
cleanup
philprime Dec 17, 2025
57b889a
move option to experimental
philprime Dec 17, 2025
7f2f81d
cleanup
philprime Dec 17, 2025
31dc8c8
remove duplicate setter
philprime Dec 17, 2025
28d56ce
apply feedback from PR review
philprime Dec 17, 2025
8d2f500
change encodable conformance to be internal
philprime Dec 17, 2025
146146e
update public api
philprime Dec 17, 2025
19293d4
Refactor SentryMetricsBatcher: Update buffer size to 1MB, enhance doc…
philprime Dec 17, 2025
0273433
fix experimental options and add beforeSendMetrics to changelog
philprime Dec 17, 2025
082e70b
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 18, 2025
b9bd8cc
Merge branch 'philprime/metrics-bootstrap' into philprime/metrics-env…
philprime Dec 18, 2025
84255ec
Refactor SentryMetric to merge value and type
philprime Dec 19, 2025
d73700c
apply feedback from PR review
philprime Dec 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add integration to collect Metrics, can be enabled by setting `options.enableMetrics = true` (#6956)
- Add implementation for Metrics Protocol (#6960)

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,12 @@
options.enableLogs = true

// Integration: Metrics
options.enableMetrics = SentrySDKOverrides.Metrics.enable.boolValue

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample visionOS-Swift Debug

'enableMetrics' is inaccessible due to 'internal' protection level

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample macOS-SwiftUI Debug

'enableMetrics' is inaccessible due to 'internal' protection level

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample iOS-Swift Debug

'enableMetrics' is inaccessible due to 'internal' protection level

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample macOS-Swift Debug

'enableMetrics' is inaccessible due to 'internal' protection level

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample iOS-SwiftUI Debug

'enableMetrics' is inaccessible due to 'internal' protection level

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample iOS-ObjectiveC Debug

'enableMetrics' is inaccessible due to 'internal' protection level

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample tvOS-Swift Debug

'enableMetrics' is inaccessible due to 'internal' protection level

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample SessionReplay-CameraTest Debug

'enableMetrics' is inaccessible due to 'internal' protection level

Check failure on line 163 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample watchOS-Swift WatchKit App Debug

'enableMetrics' is inaccessible due to 'internal' protection level
options.beforeSendMetric = { metric in

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample visionOS-Swift Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample macOS-SwiftUI Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample iOS-Swift Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample macOS-Swift Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample iOS-SwiftUI Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample iOS-ObjectiveC Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample tvOS-Swift Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample SessionReplay-CameraTest Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level

Check failure on line 164 in Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

View workflow job for this annotation

GitHub Actions / Sample watchOS-Swift WatchKit App Debug

'beforeSendMetric' is inaccessible due to 'internal' protection level
var metric = metric // Make the metric mutable because it's a value type
metric.attributes["custom-attribute"] = .init(string: "some-value")
return metric
}

// Experimental features
options.enableFileManagerSwizzling = !SentrySDKOverrides.Other.disableFileManagerSwizzling.boolValue
Expand Down
20 changes: 18 additions & 2 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,11 @@
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 */; };
D46B04482EDF25E100AF4A0A /* SentryMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B04472EDF25E100AF4A0A /* SentryMetric.swift */; };
D46B044F2EDF260A00AF4A0A /* SentryMetricBatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46B044E2EDF260A00AF4A0A /* SentryMetricBatcher.swift */; };
D473ACD72D8090FC000F1CC6 /* FileManager+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D473ACD62D8090FC000F1CC6 /* FileManager+SentryTracing.swift */; };
D4749EF02EF042C1000F9AE7 /* SentryMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4749EEF2EF042C1000F9AE7 /* SentryMetricTests.swift */; };
D4749F482EF062FD000F9AE7 /* SentryMetricsBatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4749F472EF062FD000F9AE7 /* SentryMetricsBatcherTests.swift */; };
D480F9D92DE47A50009A0594 /* TestSentryScopePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9D82DE47A48009A0594 /* TestSentryScopePersistentStore.swift */; };
D480F9DB2DE47AF2009A0594 /* SentryScopePersistentStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D480F9DA2DE47AEB009A0594 /* SentryScopePersistentStoreTests.swift */; };
D48225A12EEC4B6D00CDF32C /* BatcherMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48225A02EEC4B6D00CDF32C /* BatcherMetadata.swift */; };
Expand Down Expand Up @@ -2150,9 +2154,13 @@
D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = "<group>"; };
D46B041C2EDF167D00AF4A0A /* SentryMetricsIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsIntegration.swift; sourceTree = "<group>"; };
D46B041F2EDF175600AF4A0A /* SentryMetricsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsIntegrationTests.swift; sourceTree = "<group>"; };
D46B04472EDF25E100AF4A0A /* SentryMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetric.swift; sourceTree = "<group>"; };
D46B044E2EDF260A00AF4A0A /* SentryMetricBatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricBatcher.swift; sourceTree = "<group>"; };
D46D45E12D5F3FD600A1CB35 /* Sentry_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Sentry_Base.xctestplan; sourceTree = "<group>"; };
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 = "<group>"; };
D4749EEF2EF042C1000F9AE7 /* SentryMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricTests.swift; sourceTree = "<group>"; };
D4749F472EF062FD000F9AE7 /* SentryMetricsBatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetricsBatcherTests.swift; sourceTree = "<group>"; };
D480F9D82DE47A48009A0594 /* TestSentryScopePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryScopePersistentStore.swift; sourceTree = "<group>"; };
D480F9DA2DE47AEB009A0594 /* SentryScopePersistentStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScopePersistentStoreTests.swift; sourceTree = "<group>"; };
D48225A02EEC4B6D00CDF32C /* BatcherMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatcherMetadata.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3451,6 +3459,7 @@
7BFC16AC2524BCE700FF6266 /* SentryMessageTests.swift */,
7BFC16B82524D4AF00FF6266 /* SentryMessage+Equality.h */,
7BFC16B92524D4AF00FF6266 /* SentryMessage+Equality.m */,
D4749EEF2EF042C1000F9AE7 /* SentryMetricTests.swift */,
7BC6EC03255C235F0059822A /* SentryFrameTests.swift */,
7BC6EC07255C36DE0059822A /* SentryStacktraceTests.swift */,
7BC6EC0B255C3DF80059822A /* SentryThreadTests.swift */,
Expand Down Expand Up @@ -4402,6 +4411,7 @@
D46B04162EDF167800AF4A0A /* Metrics */ = {
isa = PBXGroup;
children = (
D46B044E2EDF260A00AF4A0A /* SentryMetricBatcher.swift */,
D46B041C2EDF167D00AF4A0A /* SentryMetricsIntegration.swift */,
);
path = Metrics;
Expand All @@ -4410,6 +4420,7 @@
D46B041E2EDF173A00AF4A0A /* Metrics */ = {
isa = PBXGroup;
children = (
D4749F472EF062FD000F9AE7 /* SentryMetricsBatcherTests.swift */,
D46B041F2EDF175600AF4A0A /* SentryMetricsIntegrationTests.swift */,
);
path = Metrics;
Expand Down Expand Up @@ -4686,12 +4697,12 @@
isa = PBXGroup;
children = (
D4D7AA6D2EEAD89300E28DFB /* Batcher */,
F451FAA52E0B304E0050ACF2 /* LoadValidator.swift */,
FA94E68B2E6B92BE00576666 /* SentryClientReport.swift */,
FA01BCB12E69352A00968DFA /* SentryDiscardedEvent.swift */,
FAE57C072E83092A00B710F9 /* SentryDispatchFactory.swift */,
FA94E6B12E6D265500576666 /* SentryEnvelope.swift */,
FA3AEE772E68E2830092283E /* SentryEnvelopeHeader.swift */,
FA01BCB12E69352A00968DFA /* SentryDiscardedEvent.swift */,
F451FAA52E0B304E0050ACF2 /* LoadValidator.swift */,
FA34C1A22E692A5000BC52AA /* SentryEnvelopeItem.swift */,
92235CAB2E15369900865983 /* SentryLogBatcher.swift */,
92235CAD2E15549C00865983 /* SentryLogger.swift */,
Expand Down Expand Up @@ -4920,6 +4931,7 @@
92ECD73F2E05AD500063EC10 /* SentryAttribute.swift */,
92ECD73D2E05AD2B0063EC10 /* SentryLogLevel.swift */,
9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */,
D46B04472EDF25E100AF4A0A /* SentryMetric.swift */,
F458D1122E180BB00028273E /* SentryFileManagerProtocol.swift */,
);
path = Protocol;
Expand Down Expand Up @@ -5859,8 +5871,10 @@
D8ACE3C82762187200F5A213 /* SentryFileIOTrackerHelper.m in Sources */,
D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */,
FA7206E12E0B37C80072FDD4 /* SentryProfileCollector.mm in Sources */,
D46B044F2EDF260A00AF4A0A /* SentryMetricBatcher.swift in Sources */,
9264E1EB2E2E385E00B077CF /* SentryLogMessage.swift in Sources */,
8ECC674A25C23A20000E2BF6 /* SentryTransactionContext.m in Sources */,
D46B04482EDF25E100AF4A0A /* SentryMetric.swift in Sources */,
03BCC38C27E1C01A003232C7 /* SentryTime.mm in Sources */,
A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */,
62C97D3A2CC64E6B00DDA204 /* SentryUncaughtNSExceptions.m in Sources */,
Expand Down Expand Up @@ -6423,6 +6437,7 @@
63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */,
D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */,
7BFC16BA2524D4AF00FF6266 /* SentryMessage+Equality.m in Sources */,
D4749EF02EF042C1000F9AE7 /* SentryMetricTests.swift in Sources */,
92136D672C9D7660002A9FB8 /* SentryNSURLRequestBuilderTests.swift in Sources */,
F48E2E0A2E6637840073CB22 /* TestSentryCrashWrapper.swift in Sources */,
D855B3E827D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift in Sources */,
Expand All @@ -6436,6 +6451,7 @@
7B16FD022654F86B008177D3 /* SentrySysctlTests.swift in Sources */,
7BAF3DB5243C743E008A5414 /* SentryClientTests.swift in Sources */,
D8F67AF12BE0D33F00C9197B /* UIImageHelperTests.swift in Sources */,
D4749F482EF062FD000F9AE7 /* SentryMetricsBatcherTests.swift in Sources */,
8EAE8E5E2681768000D6958B /* URLSessionTaskMock.m in Sources */,
62CB191E2E77F8B500AF5DA2 /* SentryDispatchSourceWrapperTests.swift in Sources */,
D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */,
Expand Down
6 changes: 6 additions & 0 deletions SentryTestUtils/Sources/TestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,10 @@ public class TestClient: SentryClientInternal {
public override func captureLogs() {
captureLogsInvocations.record(())
}

public var captureMetricsDataInvocations = Invocations<(data: Data, count: NSNumber)>()
public override func captureMetricsData(_ data: Data, with itemCount: NSNumber) {
captureMetricsDataInvocations.record((data, itemCount))
super.captureMetricsData(data, with: itemCount)
}
}
12 changes: 12 additions & 0 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,18 @@ - (void)captureLogsData:(NSData *)data with:(NSNumber *)itemCount
[self captureEnvelope:envelope];
}

- (void)captureMetricsData:(NSData *)data with:(NSNumber *)itemCount
{
SentryEnvelopeItem *envelopeItem =
[[SentryEnvelopeItem alloc] initWithType:SentryEnvelopeItemTypes.traceMetric
data:data
contentType:@"application/vnd.sentry.items.trace-metric+json"
itemCount:itemCount];
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty]
singleItem:envelopeItem];
[self captureEnvelope:envelope];
}

@end

NS_ASSUME_NONNULL_END
9 changes: 9 additions & 0 deletions Sources/Sentry/SentryDataCategoryMapper.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
NSString *const kSentryDataCategoryNameSpan = @"span";
NSString *const kSentryDataCategoryNameFeedback = @"feedback";
NSString *const kSentryDataCategoryNameLogItem = @"log_item";
NSString *const kSentryDataCategoryNameTraceMetric = @"trace_metric";
NSString *const kSentryDataCategoryNameUnknown = @"unknown";

NS_ASSUME_NONNULL_BEGIN
Expand Down Expand Up @@ -57,6 +58,9 @@
if ([itemType isEqualToString:SentryEnvelopeItemTypes.log]) {
return kSentryDataCategoryLogItem;
}
if ([itemType isEqualToString:SentryEnvelopeItemTypes.traceMetric]) {
return kSentryDataCategoryTraceMetric;
}

return kSentryDataCategoryDefault;
}
Expand Down Expand Up @@ -113,6 +117,9 @@
if ([value isEqualToString:kSentryDataCategoryNameLogItem]) {
return kSentryDataCategoryLogItem;
}
if ([value isEqualToString:kSentryDataCategoryNameTraceMetric]) {
return kSentryDataCategoryTraceMetric;
}

return kSentryDataCategoryUnknown;
}
Expand Down Expand Up @@ -148,6 +155,8 @@
return kSentryDataCategoryNameFeedback;
case kSentryDataCategoryLogItem:
return kSentryDataCategoryNameLogItem;
case kSentryDataCategoryTraceMetric:
return kSentryDataCategoryNameTraceMetric;

default: // !!!: fall-through!
case kSentryDataCategoryUnknown:
Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryClient+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)_swiftCaptureLog:(NSObject *)log withScope:(SentryScope *)scope;

- (void)captureLogs;
- (void)captureMetricsData:(NSData *)data with:(NSNumber *)itemCount;

@end

Expand Down
3 changes: 2 additions & 1 deletion Sources/Sentry/include/SentryDataCategory.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ typedef NS_ENUM(NSUInteger, SentryDataCategory) {
kSentryDataCategorySpan = 11,
kSentryDataCategoryFeedback = 12,
kSentryDataCategoryLogItem = 13,
kSentryDataCategoryUnknown = 14,
kSentryDataCategoryTraceMetric = 14,
kSentryDataCategoryUnknown = 15,
};
1 change: 1 addition & 0 deletions Sources/Swift/Helper/SentryEnvelopeItemType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
public static let statsd = "statsd"
public static let profileChunk = "profile_chunk"
public static let log = "log"
public static let traceMetric = "trace_metric"
}
100 changes: 100 additions & 0 deletions Sources/Swift/Integrations/Metrics/SentryMetricBatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
@_implementationOnly import _SentryPrivate
import Foundation

/// Protocol for batching metrics with scope-based attribute enrichment.
protocol SentryMetricBatcherProtocol {
/// Adds a metric to the batcher.
/// - Parameters:
/// - metric: The metric to add
/// - scope: The scope to add the metric to
func addMetric(_ metric: SentryMetric, scope: Scope)

/// Captures batched metrics synchronously and returns the duration.
/// - Returns: The time taken to capture items in seconds
///
/// - Note: This method blocks until all items are captured. The batcher's buffer is cleared after capture.
/// This is safe to call from any thread, but be aware that it uses dispatchSync internally,
/// so calling it from a context that holds locks or is on the batcher's queue itself could cause a deadlock.
@discardableResult func captureMetrics() -> TimeInterval
}

protocol SentryMetricBatcherOptionsProtocol {
var enableMetrics: Bool { get }
var beforeSendMetric: ((SentryMetric) -> SentryMetric?)? { get }
var environment: String { get }
var releaseName: String? { get }
var cacheDirectoryPath: String { get }
var sendDefaultPii: Bool { get }
}

struct SentryMetricBatcher: SentryMetricBatcherProtocol {
private let isEnabled: Bool
private let batcher: any BatcherProtocol<SentryMetric, Scope>

/// Initializes a new MetricBatcher.
/// - Parameters:
/// - options: The Sentry configuration options
/// - flushTimeout: The timeout interval after which buffered metrics will be flushed
/// - maxMetricCount: Maximum number of metrics to batch before triggering an immediate flush.
/// - maxBufferSizeBytes: The maximum buffer size in bytes before triggering an immediate flush
/// - dateProvider: Instance used to determine current time
/// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state
/// - capturedDataCallback: The callback to handle captured metric batches. This callback is responsible
/// for invoking client.captureMetricsData() with the batched data.
///
/// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety.
/// Passing a concurrent queue will result in undefined behavior and potential data races.
///
/// - Note: Metrics are flushed when either `maxMetricCount` or `maxBufferSizeBytes` limit is reached.
init(
options: SentryMetricBatcherOptionsProtocol,
flushTimeout: TimeInterval = 5,
maxMetricCount: Int = 100, // Maximum 100 metrics per batch
maxBufferSizeBytes: Int = 2 * 1_024, // 2 KiB buffer size, see: https://develop.sentry.dev/sdk/data-model/envelopes/#size-limits
dateProvider: SentryCurrentDateProvider,
dispatchQueue: SentryDispatchQueueWrapper,
capturedDataCallback: @escaping (_ data: Data, _ count: Int) -> Void
) {
self.isEnabled = options.enableMetrics
self.batcher = Batcher(
config: .init(
sendDefaultPii: options.sendDefaultPii,
flushTimeout: flushTimeout,
maxItemCount: maxMetricCount,
maxBufferSizeBytes: maxBufferSizeBytes,
beforeSendItem: options.beforeSendMetric,
capturedDataCallback: capturedDataCallback
),
metadata: .init(
environment: options.environment,
releaseName: options.releaseName,
installationId: SentryInstallation.cachedId(withCacheDirectoryPath: options.cacheDirectoryPath)
),
buffer: InMemoryBatchBuffer(),
dateProvider: dateProvider,
dispatchQueue: dispatchQueue
)
}

func addMetric(_ metric: SentryMetric, scope: Scope) {
guard isEnabled else {
return
}
batcher.add(metric, scope: scope)
}

@discardableResult
func captureMetrics() -> TimeInterval {
return batcher.capture()
}
}

extension Options: SentryMetricBatcherOptionsProtocol {
var enableMetrics: Bool {
return experimental.enableMetrics
}

var beforeSendMetric: ((SentryMetric) -> SentryMetric?)? {
return experimental.beforeSendMetric
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
final class SentryMetricsIntegration<Dependencies>: NSObject, SwiftIntegration {
@_implementationOnly import _SentryPrivate

final class SentryMetricsIntegration<Dependencies: DateProviderProvider & DispatchQueueWrapperProvider>: NSObject, SwiftIntegration {
private let metricBatcher: SentryMetricBatcherProtocol

init?(with options: Options, dependencies: Dependencies) {
guard options.experimental.enableMetrics else { return nil }

SentrySDKLog.debug("Integration initialized")
self.metricBatcher = SentryMetricBatcher(
options: options,
dateProvider: dependencies.dateProvider,
dispatchQueue: dependencies.dispatchQueueWrapper,
capturedDataCallback: { data, count in
let hub = SentrySDKInternal.currentHub()
guard let client = hub.getClient() else {
SentrySDKLog.debug("MetricsIntegration: No client available, dropping metrics")
return
}
client.captureMetricsData(data, with: NSNumber(value: count))
}
)
}

func uninstall() {}
func uninstall() {
// Flush any pending metrics before uninstalling.
//
// Note: This calls captureMetrics() synchronously, which uses dispatchSync internally.
// This is safe because uninstall() is typically called from the main thread during
// app lifecycle events, and the batcher's dispatch queue is a separate serial queue.
metricBatcher.captureMetrics()
}

static var name: String {
"SentryMetricsIntegration"
}

// MARK: - Public API for MetricsApi

func addMetric(_ metric: SentryMetric, scope: Scope) {
metricBatcher.addMetric(metric, scope: scope)
}
}
2 changes: 1 addition & 1 deletion Sources/Swift/Protocol/SentryAttribute.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// A typed attribute that can be attached to structured item entries used by Logs
/// A typed attribute that can be attached to structured item entries used by Logs & Metrics
///
/// `Attribute` provides a type-safe way to store structured data alongside item messages.
/// Supports String, Bool, Int, and Double types.
Expand Down
Loading
Loading