Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(metrics): Add BeforeEmitMetrics callback #3799

Merged
merged 33 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e9bde83
chore(metrics): Add Set, Gauge, Distribution metric classes
philipphofmann Mar 22, 2024
1f3a779
chore(metrics): Add LocaleMetricsAggregator
philipphofmann Mar 20, 2024
2ec3fd7
call local aggregator in buckets aggregator
philipphofmann Mar 21, 2024
24ec7ca
backup
philipphofmann Mar 22, 2024
98c8900
Merge branch 'main' into feat/local-metrics-aggregator
philipphofmann Mar 26, 2024
4100ed7
make LocalMetricsAggregator struct private
philipphofmann Mar 26, 2024
74f0d7e
remove global tagskey function
philipphofmann Mar 26, 2024
605d126
ditch private typealiases
philipphofmann Mar 26, 2024
3921a17
fix build error
philipphofmann Mar 26, 2024
7e94683
Merge branch 'feat/local-metrics-aggregator' into feat/metrics
philipphofmann Mar 26, 2024
56ad35a
tags
philipphofmann Mar 26, 2024
34d29d1
Merge branch 'main' into feat/metrics
philipphofmann Mar 26, 2024
eec5849
changelog
philipphofmann Mar 26, 2024
63c6259
feat(metrics): Change adding set to string
philipphofmann Mar 26, 2024
6cec1cb
use isKindOfClass
philipphofmann Mar 26, 2024
74b9c83
Merge branch 'feat/metrics' into feat/strings-for-metric-set
philipphofmann Mar 26, 2024
4ad6c65
fix unit test compile issues
philipphofmann Mar 26, 2024
679e982
ref: Avoid casting for set metric
philipphofmann Mar 26, 2024
0c61e55
remove zlib
philipphofmann Mar 27, 2024
b8120c9
Merge branch 'main' into feat/metrics
philipphofmann Mar 27, 2024
a34ff72
Merge branch 'feat/metrics' into feat/strings-for-metric-set
philipphofmann Mar 27, 2024
0950289
Merge branch 'feat/strings-for-metric-set' into ref/avoid-casting-for…
philipphofmann Mar 27, 2024
9aed2e3
feat(metrics): Change adding set to string (#3792)
philipphofmann Mar 27, 2024
8943206
Merge branch 'feat/metrics' into ref/avoid-casting-for-set-metric
philipphofmann Mar 27, 2024
4489b13
fix merge problems
philipphofmann Mar 27, 2024
414c351
Merge branch 'main' into ref/avoid-casting-for-set-metric
philipphofmann Mar 27, 2024
fca1d5d
fix build error
philipphofmann Mar 27, 2024
ec97c43
feat(metrics): Add BeforeEmitMetrics callback
philipphofmann Mar 27, 2024
eaf4828
fix Xcode 13 build issues
philipphofmann Mar 28, 2024
87bc52a
Merge branch 'main' into ref/avoid-casting-for-set-metric
philipphofmann Mar 28, 2024
5a47197
Merge branch 'ref/avoid-casting-for-set-metric' into feat/metrics-bef…
philipphofmann Mar 28, 2024
f7b8cdf
make callback nullable
philipphofmann Mar 28, 2024
b316400
Merge branch 'main' into feat/metrics-before-emit-metrics
philipphofmann Mar 28, 2024
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Features

- Add Metrics API (#3791): Read our [docs](https://docs.sentry.io/platforms/apple/metrics/) to learn
- Add Metrics API (#3791, #3799): Read our [docs](https://docs.sentry.io/platforms/apple/metrics/) to learn
more about how to use the Metrics API.
- Pre-main profiling data is now attached to the app start transaction (#3736)
- Release framework without UIKit/AppKit (#3793)
Expand Down
10 changes: 10 additions & 0 deletions Sources/Sentry/Public/SentryDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ typedef NSNumber *_Nullable (^SentryTracesSamplerCallback)(
*/
typedef void (^SentrySpanCallback)(id<SentrySpan> _Nullable span);

/**
* A callback block which gets called right before a metric is about to be emitted.
* @param key The key of the metric.
* @param tags A dictionary of key-value pairs associated with the metric.
* @return BOOL YES if the metric should be emitted, NO otherwise.
*/
typedef BOOL (^SentryBeforeEmitMetricCallback)(
NSString *_Nonnull key, NSDictionary<NSString *, NSString *> *_Nonnull tags);

/**
* Log level.
*/
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,11 @@ NS_SWIFT_NAME(Options)
*/
@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;

@end

NS_ASSUME_NONNULL_END
11 changes: 6 additions & 5 deletions Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ - (instancetype)initWithClient:(nullable SentryClient *)client
SentryMetricsClient *metricsClient =
[[SentryMetricsClient alloc] initWithClient:statsdClient];
_metrics = [[SentryMetricsAPI alloc]
initWithEnabled:client.options.enableMetrics
client:metricsClient
currentDate:SentryDependencyContainer.sharedInstance.dateProvider
dispatchQueue:SentryDependencyContainer.sharedInstance.dispatchQueueWrapper
random:SentryDependencyContainer.sharedInstance.random];
initWithEnabled:client.options.enableMetrics
client:metricsClient
currentDate:SentryDependencyContainer.sharedInstance.dateProvider
dispatchQueue:SentryDependencyContainer.sharedInstance.dispatchQueueWrapper
random:SentryDependencyContainer.sharedInstance.random
beforeEmitMetric:client.options.beforeEmitMetric];
[_metrics setDelegate:self];

_sessionLock = [[NSObject alloc] init];
Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,10 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
[self setBool:options[@"enableSpanLocalMetricAggregation"]
block:^(BOOL value) { self->_enableSpanLocalMetricAggregation = value; }];

if ([self isBlock:options[@"beforeEmitMetric"]]) {
self.beforeEmitMetric = options[@"beforeEmitMetric"];
}

return YES;
}

Expand Down
9 changes: 9 additions & 0 deletions Sources/Swift/Metrics/BucketsMetricsAggregator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class BucketMetricsAggregator: MetricsAggregator {
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
Expand All @@ -35,6 +36,7 @@ class BucketMetricsAggregator: MetricsAggregator {
currentDate: SentryCurrentDateProvider,
dispatchQueue: SentryDispatchQueueWrapper,
random: SentryRandomProtocol,
beforeEmitMetric: BeforeEmitMetricCallback? = nil,
totalMaxWeight: UInt = 1_000,
flushInterval: TimeInterval = 10.0,
flushTolerance: TimeInterval = 0.5
Expand All @@ -43,6 +45,7 @@ class BucketMetricsAggregator: MetricsAggregator {
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
Expand Down Expand Up @@ -133,6 +136,12 @@ class BucketMetricsAggregator: MetricsAggregator {
}

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)"
Expand Down
8 changes: 6 additions & 2 deletions Sources/Swift/Metrics/SentryMetricsAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import Foundation
func getLocalMetricsAggregator() -> 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 weak var delegate: SentryMetricsAPIDelegate?

@objc init(enabled: Bool, client: SentryMetricsClient, currentDate: SentryCurrentDateProvider, dispatchQueue: SentryDispatchQueueWrapper, random: SentryRandomProtocol) {
@objc init(enabled: Bool, client: SentryMetricsClient, currentDate: SentryCurrentDateProvider, dispatchQueue: SentryDispatchQueueWrapper, random: SentryRandomProtocol, beforeEmitMetric: BeforeEmitMetricCallback?) {

if enabled {
self.aggregator = BucketMetricsAggregator(client: client, currentDate: currentDate, dispatchQueue: dispatchQueue, random: random)
self.aggregator = BucketMetricsAggregator(client: client, currentDate: currentDate, dispatchQueue: dispatchQueue, random: random, beforeEmitMetric: beforeEmitMetric ?? { _, _ in true })
} else {
self.aggregator = NoOpMetricsAggregator()
}
Expand Down
21 changes: 21 additions & 0 deletions Tests/SentryTests/SentryOptionsTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,27 @@
[self testBooleanField:@"enableSpanLocalMetricAggregation" defaultValue:YES];
}

- (void)testBeforeEmitMetric
{
SentryBeforeEmitMetricCallback callback
= ^(NSString *_Nonnull key, NSDictionary<NSString *, NSString *> *_Nonnull tags) {
// Use tags and key to silence unused compiler error
XCTAssertNotNil(key);
XCTAssertNotNil(tags);
return YES;

Check warning on line 1301 in Tests/SentryTests/SentryOptionsTest.m

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/SentryOptionsTest.m#L1299-L1301

Added lines #L1299 - L1301 were not covered by tests
};
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<NSString *> *)expected actual:(NSArray<NSString *> *)actual
Expand Down
16 changes: 16 additions & 0 deletions Tests/SentryTests/SentrySDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,22 @@ class SentrySDKTests: XCTestCase {
expect(envelopeItem.header.type) == SentryEnvelopeItemTypeStatsd
expect(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)

expect(client.captureEnvelopeInvocations.count) == 0
}

#if SENTRY_HAS_UIKIT

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,5 +406,43 @@ final class BucketMetricsAggregatorTests: XCTestCase {
expect(metric["count"] as? Int) == 2
expect(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)

expect(metricsClient.captureInvocations.count) == 1
let buckets = try XCTUnwrap(metricsClient.captureInvocations.first)

let bucket = try XCTUnwrap(buckets[currentDate.bucketTimestamp])
expect(bucket.count) == 1
let metric = try XCTUnwrap(bucket.first as? DistributionMetric)

expect(metric.key) == "key1"
expect(metric.tags.isEmpty) == true
}

}
10 changes: 5 additions & 5 deletions Tests/SentryTests/Swift/Metrics/SentryMetricsAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ final class SentryMetricsAPITests: XCTestCase, SentryMetricsAPIDelegate {

func testInitWithDisabled_AllOperationsAreNoOps() throws {
let metricsClient = try TestMetricsClient()
let sut = SentryMetricsAPI(enabled: false, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom())
let sut = SentryMetricsAPI(enabled: false, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), beforeEmitMetric: { _, _ in true })

sut.increment(key: "some", value: 1.0, unit: .none, tags: ["yeah": "sentry"])
sut.gauge(key: "some", value: 1.0, unit: .none, tags: ["yeah": "sentry"])
Expand All @@ -22,7 +22,7 @@ final class SentryMetricsAPITests: XCTestCase, SentryMetricsAPIDelegate {

func testIncrement_EmitsIncrementMetric() throws {
let metricsClient = try TestMetricsClient()
let sut = SentryMetricsAPI(enabled: true, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom())
let sut = SentryMetricsAPI(enabled: true, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), beforeEmitMetric: { _, _ in return true })
sut.setDelegate(self)

sut.increment(key: "key", value: 1.0, unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"])
Expand All @@ -44,7 +44,7 @@ final class SentryMetricsAPITests: XCTestCase, SentryMetricsAPIDelegate {

func testGauge_EmitsGaugeMetric() throws {
let metricsClient = try TestMetricsClient()
let sut = SentryMetricsAPI(enabled: true, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom())
let sut = SentryMetricsAPI(enabled: true, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), beforeEmitMetric: { _, _ in return true })
sut.setDelegate(self)

sut.gauge(key: "key", value: 1.0, unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"])
Expand All @@ -66,7 +66,7 @@ final class SentryMetricsAPITests: XCTestCase, SentryMetricsAPIDelegate {

func testDistribution_EmitsDistributionMetric() throws {
let metricsClient = try TestMetricsClient()
let sut = SentryMetricsAPI(enabled: true, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom())
let sut = SentryMetricsAPI(enabled: true, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), beforeEmitMetric: { _, _ in return true })
sut.setDelegate(self)

sut.distribution(key: "key", value: 1.0, unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"])
Expand All @@ -89,7 +89,7 @@ final class SentryMetricsAPITests: XCTestCase, SentryMetricsAPIDelegate {

func testSet_EmitsSetMetric() throws {
let metricsClient = try TestMetricsClient()
let sut = SentryMetricsAPI(enabled: true, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom())
let sut = SentryMetricsAPI(enabled: true, client: metricsClient, currentDate: SentryCurrentDateProvider(), dispatchQueue: SentryDispatchQueueWrapper(), random: SentryRandom(), beforeEmitMetric: { _, _ in return true })
sut.setDelegate(self)

sut.set(key: "key", value: "value1", unit: MeasurementUnitFraction.percent, tags: ["yeah": "sentry"])
Expand Down
Loading