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: Add extension for Data to track file I/O operations with Sentry #4862

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Add extension for `Data` to track file I/O operations with Sentry (#4862)

### Improvements

- Log message when setting user before starting the SDK (#4882)
Expand Down
20 changes: 20 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,7 @@
D42E48572D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */; };
D43647EF2D5CF9E3001468E0 /* SentrySpanDataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647EE2D5CF9DC001468E0 /* SentrySpanDataKey.swift */; };
D43647F12D5CFB71001468E0 /* SentrySpanKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */; };
D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */; };
D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; };
D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; };
D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; };
Expand All @@ -822,8 +823,10 @@
D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; };
D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; };
D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; };
D4C5F59A2D4249E6002A9BF6 /* DataSentryTracingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */; };
D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */; };
D4E3F35E2D4A877300F79E2B /* SentryNSDictionarySanitize+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */; };
D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; };
D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; };
D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; };
D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; };
Expand Down Expand Up @@ -1962,6 +1965,7 @@
D46D45EA2D5F412100A1CB35 /* SentryTests_Base.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = SentryTests_Base.xctestplan; path = Plans/SentryTests_Base.xctestplan; sourceTree = SOURCE_ROOT; };
D43647EE2D5CF9DC001468E0 /* SentrySpanDataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanDataKey.swift; sourceTree = "<group>"; };
D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentrySpanKeyTests.swift; sourceTree = "<group>"; };
D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = "<group>"; };
D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = "<group>"; };
D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = "<group>"; };
D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = "<group>"; };
Expand All @@ -1979,6 +1983,8 @@
D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = "<group>"; };
D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = "<group>"; };
D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = "<group>"; };
D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSentryTracingIntegrationTests.swift; sourceTree = "<group>"; };
D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SentryTracing.swift"; sourceTree = "<group>"; };
D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashCTests.swift; sourceTree = "<group>"; };
D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = "<group>"; };
D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3826,6 +3832,15 @@
path = Plans;
sourceTree = "<group>";
};
D468C0602D36699700964230 /* IO */ = {
isa = PBXGroup;
children = (
D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */,
D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */,
);
path = IO;
sourceTree = "<group>";
};
D48724D92D35258A005DE483 /* Transactions */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4048,6 +4063,7 @@
D8739CF72BECFF92007D2F66 /* Performance */ = {
isa = PBXGroup;
children = (
D468C0602D36699700964230 /* IO */,
6294287F2CB3BF4E002C454C /* SwizzleClassNameExclude.swift */,
D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */,
);
Expand All @@ -4057,6 +4073,7 @@
D875ED09276CC83200422FAC /* IO */ = {
isa = PBXGroup;
children = (
D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */,
D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */,
D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */,
D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */,
Expand Down Expand Up @@ -5063,6 +5080,7 @@
D84D2CC32C29AD120011AF8A /* SentrySessionReplay.swift in Sources */,
849B8F9B2C6E906900148E1F /* SentryUserFeedbackIntegrationDriver.swift in Sources */,
63FE70DF20DA4C1000CDBAE8 /* SentryCrashMonitorType.c in Sources */,
D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */,
7BF9EF7E2722B91F00B5BBEF /* SentryDefaultObjCRuntimeWrapper.m in Sources */,
7BC3936E25B1AB72004F03D3 /* SentryLevelMapper.m in Sources */,
6304360B1EC0595B00C4D3FA /* SentryNSDataUtils.m in Sources */,
Expand All @@ -5082,6 +5100,7 @@
7BB65501253DC1B500887E87 /* SentryUserFeedback.m in Sources */,
7D5C441A237C2E1F00DAB0A3 /* SentrySDK.m in Sources */,
7D65260E237F649E00113EA2 /* SentryScope.m in Sources */,
D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */,
84281C472A57905700EE88F2 /* SentrySample.m in Sources */,
84AC61D329F7541E009EEF61 /* SentryDispatchSourceWrapper.m in Sources */,
62A456E52B0370E0003F19A1 /* SentryUIEventTrackerTransactionMode.m in Sources */,
Expand Down Expand Up @@ -5381,6 +5400,7 @@
8E70B0FD25CB72BE002B3155 /* SentrySpanTests.swift in Sources */,
7BBD188F2448469A00427C76 /* HttpDateFormatter.swift in Sources */,
63FE720C20DA66EC00CDBAE8 /* SentryCrashMonitor_Tests.m in Sources */,
D4C5F59A2D4249E6002A9BF6 /* DataSentryTracingIntegrationTests.swift in Sources */,
D855B3EA27D652C700BCED76 /* TestCoreDataStack.swift in Sources */,
D8AE48C12C57B1550092A2A6 /* SentryLevelTests.swift in Sources */,
63FE721820DA66EC00CDBAE8 /* TestThread.m in Sources */,
Expand Down
35 changes: 35 additions & 0 deletions Sources/Sentry/SentryFileIOTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@
return result;
}

- (BOOL)measureNSData:(NSData *)data
writeToURL:(NSURL *)url
options:(NSDataWritingOptions)writeOptionsMask
origin:(NSString *)origin
error:(NSError **)error
method:(BOOL (^)(NSURL *, NSDataWritingOptions, NSError **))method
{

Check warning on line 104 in Sources/Sentry/SentryFileIOTracker.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileIOTracker.m#L104

Added line #L104 was not covered by tests
// We dont track reads from a url that is not a file url
// because these reads are handled by NSURLSession and
// SentryNetworkTracker will create spans in these cases.
if (![url.scheme isEqualToString:NSURLFileScheme])
return method(url, writeOptionsMask, error);

Check warning on line 109 in Sources/Sentry/SentryFileIOTracker.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileIOTracker.m#L108-L109

Added lines #L108 - L109 were not covered by tests

id<SentrySpan> span = [self startTrackingWritingNSData:data filePath:[url path] origin:origin];

Check warning on line 111 in Sources/Sentry/SentryFileIOTracker.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileIOTracker.m#L111

Added line #L111 was not covered by tests

BOOL result = method(url, writeOptionsMask, error);

Check warning on line 113 in Sources/Sentry/SentryFileIOTracker.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileIOTracker.m#L113

Added line #L113 was not covered by tests

if (span != nil) {
[self finishTrackingNSData:data span:span];

Check warning on line 116 in Sources/Sentry/SentryFileIOTracker.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileIOTracker.m#L115-L116

Added lines #L115 - L116 were not covered by tests
}

return result;

Check warning on line 119 in Sources/Sentry/SentryFileIOTracker.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileIOTracker.m#L119

Added line #L119 was not covered by tests
}

- (NSData *)measureNSDataFromFile:(NSString *)path
origin:(NSString *)origin
method:(NSData * (^)(NSString *))method
Expand Down Expand Up @@ -178,6 +202,13 @@
return result;
}

- (nullable id<SentrySpan>)spanForPath:(NSString *)path
origin:(NSString *)origin
operation:(NSString *)operation
{
return [self spanForPath:path origin:origin operation:operation size:0];

Check warning on line 209 in Sources/Sentry/SentryFileIOTracker.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileIOTracker.m#L208-L209

Added lines #L208 - L209 were not covered by tests
}

- (nullable id<SentrySpan>)spanForPath:(NSString *)path
origin:(NSString *)origin
operation:(NSString *)operation
Expand All @@ -199,6 +230,10 @@
description:[self transactionDescriptionForFile:path
fileSize:size]];
ioSpan.origin = origin;
if (size > 0) {
[ioSpan setDataValue:[NSNumber numberWithUnsignedInteger:size]
forKey:SentrySpanDataKey.fileSize];

Check warning on line 235 in Sources/Sentry/SentryFileIOTracker.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileIOTracker.m#L233-L235

Added lines #L233 - L235 were not covered by tests
}
}];

if (ioSpan == nil) {
Expand Down
20 changes: 20 additions & 0 deletions Sources/Sentry/include/SentryFileIOTracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ SENTRY_NO_INIT
error:(NSError **)error
method:(BOOL (^)(NSString *, NSDataWritingOptions, NSError **))method;

/**
* Measure NSData 'writeToFile:options:error:' method.
*/
- (BOOL)measureNSData:(NSData *)data
writeToURL:(NSURL *)url
options:(NSDataWritingOptions)writeOptionsMask
origin:(NSString *)origin
error:(NSError **)error
method:(BOOL (^)(NSURL *, NSDataWritingOptions, NSError **))method;

/**
* Measure NSData 'initWithContentsOfFile:' method.
*/
Expand Down Expand Up @@ -80,6 +90,16 @@ SENTRY_NO_INIT
method:(BOOL (^)(NSString *, NSData *,
NSDictionary<NSFileAttributeKey, id> *))method;

// MARK: - Internal Methods available for Swift Extension

- (nullable id<SentrySpan>)spanForPath:(NSString *)path
origin:(NSString *)origin
operation:(NSString *)operation;
- (nullable id<SentrySpan>)spanForPath:(NSString *)path
origin:(NSString *)origin
operation:(NSString *)operation
size:(NSUInteger)size;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@_implementationOnly import _SentryPrivate

/// A ``Data`` extension that tracks read and write operations with Sentry.
///
/// - Note: Methods provided by this extension reflect the same functionality as the original ``Data`` methods, but they track the operation with Sentry.
public extension Data {

// MARK: - Reading Data from a File

/// Creates a data object from the data at the specified file URL, tracking the operation with Sentry.
///
/// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions.
/// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting
/// `options.enableSwizzling` to `false` when initializing Sentry.
/// - Parameters:
/// - url: The location on disk of the data to read.
/// - options: The mask specifying the options to use when reading the data. For more information, see ``NSData.ReadingOptions``.
/// - Note: See ``Data.init(contentsOf:options:)`` for more information.
init(contentsOfUrlWithSentryTracing url: URL, options: Data.ReadingOptions = []) throws {
let tracker = SentryFileIOTracker.sharedInstance()
self = try tracker
.measureReadingData(
from: url,
options: options,
origin: SentryTraceOrigin.manualFileData) { url, options in
try Data(contentsOf: url, options: options)

Check warning on line 26 in Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift#L19-L26

Added lines #L19 - L26 were not covered by tests
}
}

// MARK: - Writing Data to a File

/// Write the contents of the `Data` to a location, tracking the operation with Sentry.
///
/// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions.
/// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting
/// `options.enableSwizzling` to `false` when initializing Sentry.
/// - Parameters:
/// - url: The location to write the data into.
/// - options: Options for writing the data. Default value is `[]`.
/// - Note: See ``Data.write(to:options:)`` for more information.
func writeWithSentryTracing(to url: URL, options: Data.WritingOptions = []) throws {
let tracker = SentryFileIOTracker.sharedInstance()
try tracker
.measureWritingData(
self,
to: url,
options: options,
origin: SentryTraceOrigin.manualFileData) { data, url, options in
try data.write(to: url, options: options)

Check warning on line 49 in Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift#L41-L49

Added lines #L41 - L49 were not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@_implementationOnly import _SentryPrivate

extension SentryFileIOTracker {
func measureReadingData(
from url: URL,
options: Data.ReadingOptions,
origin: String,
method: (_ url: URL, _ options: Data.ReadingOptions) throws -> Data
) rethrows -> Data {
// We dont track reads from a url that is not a file url
// because these reads are handled by NSURLSession and
// SentryNetworkTracker will create spans in these cases.
guard url.scheme == NSURLFileScheme else {
return try method(url, options)

Check warning on line 14 in Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift#L9-L14

Added lines #L9 - L14 were not covered by tests
}
guard let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileRead) else {
return try method(url, options)

Check warning on line 17 in Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift#L16-L17

Added lines #L16 - L17 were not covered by tests
}
defer {
span.finish()

Check warning on line 20 in Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift#L19-L20

Added lines #L19 - L20 were not covered by tests
}
let data = try method(url, options)
span.setData(value: data.count, key: SentrySpanDataKey.fileSize)
return data

Check warning on line 24 in Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift#L22-L24

Added lines #L22 - L24 were not covered by tests
}

func measureWritingData(
_ data: Data,
to url: URL,
options: Data.WritingOptions,
origin: String,
method: (_ data: Data, _ url: URL, _ options: Data.WritingOptions) throws -> Void
) rethrows {
// We dont track reads from a url that is not a file url
// because these reads are handled by NSURLSession and
// SentryNetworkTracker will create spans in these cases.
guard url.scheme == NSURLFileScheme else {
return try method(data, url, options)

Check warning on line 38 in Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift#L33-L38

Added lines #L33 - L38 were not covered by tests
}
guard let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileWrite, size: UInt(data.count)) else {
return try method(data, url, options)

Check warning on line 41 in Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift#L40-L41

Added lines #L40 - L41 were not covered by tests
}
defer {
span.finish()

Check warning on line 44 in Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift#L43-L44

Added lines #L43 - L44 were not covered by tests
}
try method(data, url, options)

Check warning on line 46 in Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift#L46

Added line #L46 was not covered by tests
}
}
Loading
Loading