Skip to content

Commit cc28f6b

Browse files
committed
feat: Add support for tags and context fields for termination watch
1 parent 832e958 commit cc28f6b

15 files changed

+850
-8
lines changed

Sources/Sentry/SentryScope.m

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@
1919

2020
@interface SentryScope ()
2121

22-
/**
23-
* Set global tags -> these will be sent with every event
24-
*/
25-
@property (atomic, strong) NSMutableDictionary<NSString *, NSString *> *tagDictionary;
26-
2722
/**
2823
* Set the fingerprint of an event to determine the grouping
2924
*/

Sources/Sentry/SentryWatchdogTerminationScopeObserver.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ - (void)setLevel:(enum SentryLevel)level
7979

8080
- (void)setTags:(nullable NSDictionary<NSString *, NSString *> *)tags
8181
{
82-
// Left blank on purpose
82+
[self.fieldsProcessor setTags:tags];
8383
}
8484

8585
- (void)setUser:(nullable SentryUser *)user
@@ -89,7 +89,7 @@ - (void)setUser:(nullable SentryUser *)user
8989

9090
- (void)setTraceContext:(nullable NSDictionary<NSString *, id> *)traceContext
9191
{
92-
// Left blank on purpose
92+
[self.fieldsProcessor setTraceContext:traceContext];
9393
}
9494

9595
@end

Sources/Sentry/SentryWatchdogTerminationTracker.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ - (void)start
6060
event.user = [self.scopePersistentStore readPreviousUserFromDisk];
6161
event.dist = [self.scopePersistentStore readPreviousDistFromDisk];
6262
event.environment = [self.scopePersistentStore readPreviousEnvironmentFromDisk];
63+
event.tags = [self.scopePersistentStore readPreviousTagsFromDisk];
64+
// TODO: See where to store trace context
6365

6466
SentryException *exception =
6567
[[SentryException alloc] initWithValue:SentryWatchdogTerminationExceptionValue

Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# import <SentryHub.h>
1313
# import <SentryNSProcessInfoWrapper.h>
1414
# import <SentryOptions+Private.h>
15+
# import <SentryPropagationContext.h>
1516
# import <SentrySDK+Private.h>
1617
# import <SentrySwift.h>
1718
# import <SentryWatchdogTerminationBreadcrumbProcessor.h>
@@ -99,6 +100,8 @@ - (BOOL)installWithOptions:(SentryOptions *)options
99100
[scopeObserver setUser:outerScope.userObject];
100101
[scopeObserver setEnvironment:outerScope.environmentString];
101102
[scopeObserver setDist:outerScope.distString];
103+
[scopeObserver setTags:outerScope.tags];
104+
[scopeObserver setTraceContext:[outerScope.propagationContext traceContextForEvent]];
102105
}];
103106

104107
return YES;

Sources/Sentry/include/SentryScope+Private.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ NS_ASSUME_NONNULL_BEGIN
4545
@property (atomic, strong)
4646
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *contextDictionary;
4747

48+
/**
49+
* Set global tags -> these will be sent with every event
50+
*/
51+
@property (atomic, strong) NSMutableDictionary<NSString *, NSString *> *tagDictionary;
52+
4853
- (void)addObserver:(id<SentryScopeObserver>)observer;
4954

5055
- (nullable SentryEvent *)applyToEvent:(SentryEvent *)event

Sources/Swift/Integrations/WatchdogTerminations/Processors/SentryWatchdogTerminationAttributesProcessor.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ import Foundation
4848
}
4949
}
5050

51+
public func setTags(_ tags: [String: String]?) {
52+
setData(data: tags, field: .tags) { [weak self] data in
53+
self?.scopePersistentStore.writeTagsToDisk(tags: data)
54+
}
55+
}
56+
57+
public func setTraceContext(_ traceContext: [String: Any]?) {
58+
setData(data: traceContext, field: .traceContext) { [weak self] data in
59+
self?.scopePersistentStore.writeTraceContextToDisk(traceContext: data)
60+
}
61+
}
62+
5163
// MARK: - Private
5264
private func setData<T>(data: T?, field: SentryScopeField, save: @escaping (T) -> Void) {
5365
SentrySDKLog.debug("Setting \(field.name) in background queue: \(String(describing: data))")

Sources/Swift/Persistence/SentryScopePersistentStore.swift

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ enum SentryScopeField: UInt, CaseIterable {
66
case user
77
case dist
88
case environment
9+
case traceContext
10+
case tags
911

1012
var name: String {
1113
switch self {
@@ -17,6 +19,10 @@ enum SentryScopeField: UInt, CaseIterable {
1719
return "dist"
1820
case .environment:
1921
return "environment"
22+
case .tags:
23+
return "tags"
24+
case .traceContext:
25+
return "trace_context"
2026
}
2127
}
2228
}
@@ -121,6 +127,30 @@ enum SentryScopeField: UInt, CaseIterable {
121127
writeFieldToDisk(field: .environment, data: encode(string: environment))
122128
}
123129

130+
// MARK: - Tags
131+
@objc
132+
public func readPreviousTagsFromDisk() -> [String: String]? {
133+
readFieldFromDisk(field: .tags) { data in
134+
decodeTags(from: data)
135+
}
136+
}
137+
138+
func writeTagsToDisk(tags: [String: String]) {
139+
writeFieldToDisk(field: .tags, data: encode(tags: tags))
140+
}
141+
142+
// MARK: - Trace Context
143+
@objc
144+
public func readPreviousTraceContextFromDisk() -> [String: Any]? {
145+
readFieldFromDisk(field: .traceContext) { data in
146+
decodeTraceContext(from: data)
147+
}
148+
}
149+
150+
func writeTraceContextToDisk(traceContext: [String: Any]) {
151+
writeFieldToDisk(field: .traceContext, data: encode(traceContext: traceContext))
152+
}
153+
124154
// MARK: - Private Functions
125155

126156
private func moveCurrentFileToPreviousFile(field: SentryScopeField) {
@@ -261,3 +291,75 @@ extension SentryScopePersistentStore {
261291
return String(data: data, encoding: .utf8)
262292
}
263293
}
294+
295+
// MARK: - Tags
296+
extension SentryScopePersistentStore {
297+
private func encode(tags: [String: String]) -> Data? {
298+
// We need to check if the Tags is a valid JSON object before encoding it.
299+
// Otherwise it will throw an unhandled `NSInvalidArgumentException` exception.
300+
// The error handler is required due but seems not to be executed.
301+
guard let sanitizedTags = sentry_sanitize(tags) else {
302+
SentrySDKLog.error("Failed to sanitize tags, reason: tags is not valid json: \(tags)")
303+
return nil
304+
}
305+
guard let data = SentrySerialization.data(withJSONObject: sanitizedTags) else {
306+
SentrySDKLog.error("Failed to serialize tags, reason: tags is not valid json: \(tags)")
307+
return nil
308+
}
309+
310+
return data
311+
}
312+
313+
private func decodeTags(from data: Data) -> [String: String]? {
314+
guard let deserialized = SentrySerialization.deserializeDictionary(fromJsonData: data) else {
315+
SentrySDKLog.error("Failed to deserialize tags, reason: data is not valid json")
316+
return nil
317+
}
318+
319+
// `SentrySerialization` is a wrapper around `NSJSONSerialization` which returns any type of data (`id`).
320+
// It is the casted to a `NSDictionary`, which is then casted to a `[AnyHashable: Any]` in Swift.
321+
//
322+
// The responsibility of validating and casting the deserialized data from any data to a dictionary is delegated
323+
// to the `SentrySerialization` class.
324+
//
325+
// As this decode Tags method specifically returns a dictionary of strings, we need to ensure that
326+
// each value is a string.
327+
//
328+
// If the deserialized value is not a string, something clearly went wrong and we should discard the data.
329+
330+
// Iterate through the deserialized dictionary and check if the type is a dictionary.
331+
// When all values are strings, we can safely cast it to `[String: String]` without allocating
332+
// additional memory (like when mapping values).
333+
for (key, value) in deserialized {
334+
guard value is String else {
335+
SentrySDKLog.error("Failed to deserialize tags, reason: value for key \(key) is not a valid string")
336+
return nil
337+
}
338+
}
339+
340+
return deserialized as? [String: String]
341+
}
342+
}
343+
344+
// MARK: - Trace Context
345+
extension SentryScopePersistentStore {
346+
private func encode(traceContext: [String: Any]) -> Data? {
347+
guard let sanitized = sentry_sanitize(traceContext) else {
348+
SentrySDKLog.error("Failed to sanitize traceContext, reason: not valid json: \(traceContext)")
349+
return nil
350+
}
351+
guard let data = SentrySerialization.data(withJSONObject: sanitized) else {
352+
SentrySDKLog.error("Failed to serialize traceContext, reason: not valid json: \(traceContext)")
353+
return nil
354+
}
355+
return data
356+
}
357+
358+
private func decodeTraceContext(from data: Data) -> [String: Any]? {
359+
guard let deserialized = SentrySerialization.deserializeDictionary(fromJsonData: data) else {
360+
SentrySDKLog.error("Failed to deserialize traceContext, reason: data is not valid json")
361+
return nil
362+
}
363+
return deserialized as? [String: Any]
364+
}
365+
}

0 commit comments

Comments
 (0)