Skip to content

Commit

Permalink
feat(metrics): Add BeforeEmitMetrics callback (#3799)
Browse files Browse the repository at this point in the history
Add the BeforeEmitMetrics to the options, as described in develop docs
https://develop.sentry.dev/sdk/metrics/#hooks.
  • Loading branch information
philipphofmann authored Mar 28, 2024
1 parent 96eb740 commit b15521e
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 13 deletions.
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 @@ - (void)testEnableSpanLocalMetricAggregation
[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;
};
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
38 changes: 38 additions & 0 deletions Tests/SentryTests/Swift/Metrics/BucketMetricsAggregatorTests.swift
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

0 comments on commit b15521e

Please sign in to comment.