Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 20 additions & 5 deletions Sources/Sentry/SentryScope.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
#import "SentryLogC.h"
#import "SentryScope+Private.h"
#import "SentryScope+PrivateSwift.h"
#import "SentrySpanInternal+Private.h"
#import "SentrySpanInternal.h"
#import "SentrySwift.h"
#import "SentryTracer.h"
#import "SentryTransactionContext.h"
#import "SentryUser.h"

NS_ASSUME_NONNULL_BEGIN

static NSString *const kSentryScopeSpanStatusSerializationKey = @"status";

@interface SentryScope ()

@property (atomic) NSUInteger maxBreadcrumbs;
Expand All @@ -37,6 +39,7 @@ @implementation SentryScope {
NSObject *_spanLock;
NSObject *_observersLock;
NSObject *_propagationContextLock;
SentryFeatureFlagBufferWrapper *_featureFlagBuffer;
}

@synthesize span = _span;
Expand All @@ -56,6 +59,7 @@ - (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs
self.attachmentArray = [[NSMutableArray alloc] init];
self.fingerprintArray = [[NSMutableArray alloc] init];
self.attributesDictionary = [[NSMutableDictionary alloc] init];
_featureFlagBuffer = [SentryFeatureFlagBufferWrapper scopeBuffer];
_spanLock = [[NSObject alloc] init];
_observersLock = [[NSObject alloc] init];
_propagationContextLock = [[NSObject alloc] init];
Expand All @@ -70,9 +74,14 @@ - (instancetype)init
return [self initWithMaxBreadcrumbs:defaultMaxBreadcrumbs];
}

- (SentryFeatureFlagBufferWrapper *)featureFlagBuffer
{
return _featureFlagBuffer;
}

- (instancetype)initWithScope:(SentryScope *)scope
{
if (self = [self init]) {
if (self = [self initWithMaxBreadcrumbs:scope.maxBreadcrumbs]) {
[_extraDictionary addEntriesFromDictionary:[scope extras]];
[_tagDictionary addEntriesFromDictionary:[scope tags]];
[_contextDictionary addEntriesFromDictionary:[scope context]];
Expand All @@ -83,6 +92,7 @@ - (instancetype)initWithScope:(SentryScope *)scope
[_fingerprintArray addObjectsFromArray:[scope fingerprints]];
[_attachmentArray addObjectsFromArray:[scope attachments]];
[_attributesDictionary addEntriesFromDictionary:[scope attributes]];
_featureFlagBuffer = [scope.featureFlagBuffer copyBuffer];

self.propagationContext = scope.propagationContext;
self.maxBreadcrumbs = scope.maxBreadcrumbs;
Expand Down Expand Up @@ -216,6 +226,7 @@ - (void)clear
@synchronized(_attributesDictionary) {
[_attributesDictionary removeAllObjects];
}
[_featureFlagBuffer removeAll];

self.userObject = nil;
self.distString = nil;
Expand Down Expand Up @@ -547,7 +558,11 @@ - (void)removeAttributeForKey:(NSString *)key
traceContext = [self buildTraceContext:span];
serializedData[@"traceContext"] = traceContext;

NSDictionary *context = [self context];
NSMutableDictionary *context = [self context].mutableCopy;
NSDictionary<NSString *, id> *_Nullable featureFlags = [_featureFlagBuffer serializeForContext];
if (featureFlags.count > 0) {
context[@"flags"] = featureFlags;
}
if (context.count > 0) {
[serializedData setValue:context forKey:@"context"];
}
Comment thread
denrase marked this conversation as resolved.
Expand Down Expand Up @@ -742,7 +757,7 @@ - (NSDictionary *)buildTraceContext:(nullable id<SentrySpan>)span
{
if (span != nil) {
NSDictionary *dict = [SENTRY_UNWRAP_NULLABLE_VALUE(id<SentrySpan>, span) serialize];
if (dict[kSentrySpanStatusSerializationKey] != nil) {
Comment thread
denrase marked this conversation as resolved.
if (dict[kSentryScopeSpanStatusSerializationKey] != nil) {
return dict;
}

Expand All @@ -753,7 +768,7 @@ - (NSDictionary *)buildTraceContext:(nullable id<SentrySpan>)span
// to any state other than undefined. Spans first have a default status of OK, but we don't
// want to change this for the trace context status.
NSMutableDictionary *mutableDict = [dict mutableCopy];
mutableDict[kSentrySpanStatusSerializationKey] = kSentrySpanStatusNameOk;
mutableDict[kSentryScopeSpanStatusSerializationKey] = kSentrySpanStatusNameOk;
return mutableDict;

} else {
Expand Down
3 changes: 3 additions & 0 deletions Sources/Sentry/SentrySpanInternal.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
NS_ASSUME_NONNULL_BEGIN

@interface SentrySpanInternal ()
@property (nonatomic, strong) SentryFeatureFlagBufferWrapper *featureFlagBuffer;
@end

@implementation SentrySpanInternal {
Expand Down Expand Up @@ -76,6 +77,7 @@ - (instancetype)initWithContext:(SentrySpanContext *)context
#endif // SENTRY_HAS_UIKIT

_tags = [[NSMutableDictionary alloc] init];
self.featureFlagBuffer = [SentryFeatureFlagBufferWrapper spanBuffer];
_stateLock = [[NSObject alloc] init];
_isFinished = NO;

Expand Down Expand Up @@ -361,6 +363,7 @@ - (NSDictionary *)serialize

@synchronized(_data) {
NSMutableDictionary *data = _data.mutableCopy;
[data addEntriesFromDictionary:[self.featureFlagBuffer serializeForSpanData]];

if (self.frames && self.frames.count > 0) {
NSMutableArray *frames = [[NSMutableArray alloc] initWithCapacity:self.frames.count];
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryScope+PrivateSwift.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ static NSString *const SENTRY_CONTEXT_APP_KEY = @"app";
@property (atomic, strong) SentryUser *_Nullable userObject;

@property (nonatomic, nullable, copy) NSString *currentScreen;
@property (atomic, strong, readonly) SENTRY_SWIFT_MIGRATION_ID(SentryFeatureFlagBufferWrapper)
featureFlagBuffer;

- (NSArray<SentryBreadcrumb *> *)breadcrumbs;

Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentrySpanInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ SENTRY_NO_INIT
* Frames of the stack trace associated with the span.
*/
@property (nullable, nonatomic, strong) NSArray<SentryFrame *> *frames;
@property (nonatomic, strong, readonly) SENTRY_SWIFT_MIGRATION_ID(SentryFeatureFlagBufferWrapper)
featureFlagBuffer;

/**
* Init a @c SentrySpan with given transaction and context.
Expand Down
130 changes: 130 additions & 0 deletions Sources/Swift/FeatureFlags/SentryFeatureFlagBuffer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// swiftlint:disable missing_docs
import Foundation

private let maxScopeFeatureFlags = 100
private let maxSpanFeatureFlags = 10

final class SentryFeatureFlagBuffer {
Comment thread
denrase marked this conversation as resolved.
private let maxSize: Int
private let overflowBehavior: SentryFeatureFlagBufferOverflowBehavior
private let lock = NSLock()
private var evaluations: [SentryFeatureFlagEvaluation]
private var indexesByFlag: [String: Int]

init(maxSize: Int,
overflowBehavior: SentryFeatureFlagBufferOverflowBehavior,
evaluations: [SentryFeatureFlagEvaluation] = []) {
self.maxSize = maxSize
self.overflowBehavior = overflowBehavior
self.evaluations = evaluations
Comment thread
denrase marked this conversation as resolved.
self.indexesByFlag = [:]
let capacity = max(maxSize, 0)
self.evaluations.reserveCapacity(capacity)
self.indexesByFlag.reserveCapacity(capacity)
rebuildIndexes()
}

static func scopeBuffer() -> SentryFeatureFlagBuffer {
SentryFeatureFlagBuffer(
// Error events record the 100 most recent, unique feature flag evaluations.
// https://develop.sentry.dev/sdk/foundations/client/integrations/feature-flags/#tracking-feature-flag-evaluations
maxSize: maxScopeFeatureFlags,
overflowBehavior: .dropOldest
)
}

static func spanBuffer() -> SentryFeatureFlagBuffer {
SentryFeatureFlagBuffer(
// Spans track the first 10 feature flags evaluated within the span's scope.
// https://develop.sentry.dev/sdk/foundations/client/integrations/feature-flags/#tracking-feature-flag-evaluations
maxSize: maxSpanFeatureFlags,
overflowBehavior: .rejectNew
)
}

var allEvaluations: [SentryFeatureFlagEvaluation] {
return lock.synchronized {
evaluations
}
}

func add<Value: SentryFeatureFlagValue>(name: String, value: Value) {
lock.synchronized {
guard maxSize > 0 else {
return
}
let evaluation = SentryFeatureFlagEvaluation(flag: name, result: value.asSentryFeatureFlagValue)
Comment thread
denrase marked this conversation as resolved.
if let existingIndex = indexesByFlag[evaluation.flag] {
switch overflowBehavior {
case .dropOldest:
evaluations.remove(at: existingIndex)
evaluations.append(evaluation)
rebuildIndexes()
case .rejectNew:
evaluations[existingIndex] = evaluation
}
return
}

if evaluations.count >= maxSize {
switch overflowBehavior {
case .dropOldest:
evaluations.removeFirst()
rebuildIndexes()
case .rejectNew:
return
}
}

indexesByFlag[evaluation.flag] = evaluations.count
evaluations.append(evaluation)
}
}

func remove(name: String) {
lock.synchronized {
guard let index = indexesByFlag[name] else {
return
}
evaluations.remove(at: index)
rebuildIndexes()
}
}

func removeAll() {
lock.synchronized {
evaluations.removeAll(keepingCapacity: true)
indexesByFlag.removeAll(keepingCapacity: true)
}
}

func serializeForContext() -> [String: Any]? {
let values = allEvaluations.map { $0.serializeForContext() }
guard !values.isEmpty else {
return nil
}
return ["values": values]
}

func serializeForSpanData() -> [String: Any] {
return allEvaluations.reduce(into: [String: Any]()) { result, evaluation in
result[evaluation.spanDataKey] = evaluation.result.serializedValue
}
}

func copy() -> SentryFeatureFlagBuffer {
return SentryFeatureFlagBuffer(
maxSize: maxSize,
overflowBehavior: overflowBehavior,
evaluations: allEvaluations
)
}

private func rebuildIndexes() {
indexesByFlag.removeAll(keepingCapacity: true)
for (index, evaluation) in evaluations.enumerated() {
indexesByFlag[evaluation.flag] = index
}
}
}
// swiftlint:enable missing_docs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// swiftlint:disable missing_docs
enum SentryFeatureFlagBufferOverflowBehavior {
case dropOldest
case rejectNew
}
// swiftlint:enable missing_docs
48 changes: 48 additions & 0 deletions Sources/Swift/FeatureFlags/SentryFeatureFlagBufferWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// swiftlint:disable missing_docs
@_implementationOnly import _SentryPrivate
import Foundation

// Objective-C scope/span internals need to own and serialize feature flag buffers, but
// SentryFeatureFlagBuffer is a pure Swift type. Keep this wrapper thin: ObjC gets only the
// serialization/copying methods it needs, while Swift can access the wrapped buffer directly.
@_spi(Private)
@objc(SentryFeatureFlagBufferWrapper)
public final class SentryFeatureFlagBufferWrapper: NSObject {
let buffer: SentryFeatureFlagBuffer

private init(buffer: SentryFeatureFlagBuffer) {
self.buffer = buffer
super.init()
}

@objc
public static func scopeBuffer() -> SentryFeatureFlagBufferWrapper {
SentryFeatureFlagBufferWrapper(buffer: SentryFeatureFlagBuffer.scopeBuffer())
}

@objc
public static func spanBuffer() -> SentryFeatureFlagBufferWrapper {
SentryFeatureFlagBufferWrapper(buffer: SentryFeatureFlagBuffer.spanBuffer())
}

@objc
public func removeAll() {
buffer.removeAll()
}

@objc
public func copyBuffer() -> SentryFeatureFlagBufferWrapper {
SentryFeatureFlagBufferWrapper(buffer: buffer.copy())
}

@objc
public func serializeForContext() -> [String: Any]? {
buffer.serializeForContext()
}

@objc
public func serializeForSpanData() -> [String: Any] {
buffer.serializeForSpanData()
}
}
// swiftlint:enable missing_docs
23 changes: 23 additions & 0 deletions Sources/Swift/FeatureFlags/SentryFeatureFlagEvaluation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// swiftlint:disable missing_docs
struct SentryFeatureFlagEvaluation: Equatable {
private static let spanDataKeyPrefix = "flag.evaluation."

let flag: String
let result: SentryFeatureFlagValueContent

var spanDataKey: String {
return "\(Self.spanDataKeyPrefix)\(flag)"
}

func serializeForContext() -> [String: Any] {
return [
"flag": flag,
"result": result.serializedValue
]
}

func serializeForSpanData() -> [String: Any] {
return [spanDataKey: result.serializedValue]
}
}
// swiftlint:enable missing_docs
11 changes: 11 additions & 0 deletions Sources/Swift/FeatureFlags/SentryFeatureFlagValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// swiftlint:disable missing_docs
protocol SentryFeatureFlagValue {
var asSentryFeatureFlagValue: SentryFeatureFlagValueContent { get }
}

extension Bool: SentryFeatureFlagValue {
var asSentryFeatureFlagValue: SentryFeatureFlagValueContent {
return .boolean(self)
}
}
// swiftlint:enable missing_docs
12 changes: 12 additions & 0 deletions Sources/Swift/FeatureFlags/SentryFeatureFlagValueContent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// swiftlint:disable missing_docs
enum SentryFeatureFlagValueContent: Equatable {
case boolean(Bool)

var serializedValue: Any {
switch self {
case .boolean(let value):
return value
}
}
}
// swiftlint:enable missing_docs
21 changes: 21 additions & 0 deletions Sources/Swift/Scope.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// swiftlint:disable missing_docs
@_implementationOnly import _SentryPrivate

// Feature flag APIs live in this file so the eventual public Scope API has a clear home.
// These methods stay SPI while the public surface is being finalized.
extension Scope {
@_spi(Private) public func addFeatureFlag(name: String, result: Bool) {
guard let wrapper = featureFlagBuffer as? SentryFeatureFlagBufferWrapper else {
return
}
wrapper.buffer.add(name: name, value: result)
}

@_spi(Private) public func removeFeatureFlag(name: String) {
guard let wrapper = featureFlagBuffer as? SentryFeatureFlagBufferWrapper else {
return
}
wrapper.buffer.remove(name: name)
}
}
// swiftlint:enable missing_docs
Loading
Loading