Skip to content

Add sentry private SPM #5269

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,11 @@ jobs:

# If the SentryAsyncSafeLog doesn't contain the SENTRY_ASYNC_SAFE_LOG_LEVEL_ERROR this fails.
- name: Async Safe Log Level is Error
run: grep -c "SENTRY_ASYNC_SAFE_LOG_LEVEL SENTRY_ASYNC_SAFE_LOG_LEVEL_ERROR" Sources/Sentry/SentryAsyncSafeLog.h
run: grep -c "SENTRY_ASYNC_SAFE_LOG_LEVEL SENTRY_ASYNC_SAFE_LOG_LEVEL_ERROR" Sources/Sentry/_SentryPrivate/include/SentryAsyncSafeLog.h

- name: Set Async Safe Log Level to Debug
run: |
sed -i '' 's/#define SENTRY_ASYNC_SAFE_LOG_LEVEL SENTRY_ASYNC_SAFE_LOG_LEVEL_ERROR/#define SENTRY_ASYNC_SAFE_LOG_LEVEL SENTRY_ASYNC_SAFE_LOG_LEVEL_TRACE/' Sources/Sentry/SentryAsyncSafeLog.h
sed -i '' 's/#define SENTRY_ASYNC_SAFE_LOG_LEVEL SENTRY_ASYNC_SAFE_LOG_LEVEL_ERROR/#define SENTRY_ASYNC_SAFE_LOG_LEVEL SENTRY_ASYNC_SAFE_LOG_LEVEL_TRACE/' Sources/Sentry/_SentryPrivate/include/SentryAsyncSafeLog.h
shell: bash

- run: ./scripts/ci-select-xcode.sh 16.3
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ let package = Package(
],
publicHeadersPath: "SentryInternal/"
),
.target(name: "SentryCoreSwift", path: "Sources/Swift/Core")
.target(name: "SentryCoreSwift", path: "Sources/Swift/Core"),
// The name of this package is _SentryPrivate so that it matches the imports already used in Swift code to access ObjC.
.target(name: "_SentryPrivate", dependencies: ["SentryCoreSwift"], path: "Sources/Sentry/_SentryPrivate", cSettings: [.headerSearchPath("Public")])
],
cxxLanguageStandard: .cxx14
)
2 changes: 1 addition & 1 deletion Samples/iOS-SwiftUI/iOS-SwiftUI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ targets:
- ../Shared/SampleAssets.xcassets
- ../../Sources/Sentry/include/SentryTracer.h
- ../../Sources/Sentry/include/SentryPerformanceTracker.h
- ../../Sources/Sentry/Public/SentryProfilingConditionals.h
- ../../Sources/Sentry/_SentryPrivate/Public/SentryProfilingConditionals.h
dependencies:
- target: Sentry/Sentry
- target: Sentry/SentrySwiftUI
Expand Down
4 changes: 2 additions & 2 deletions Sentry.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Pod::Spec.new do |s|
"Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}"
sp.preserve_path = "Sources/Sentry/include/module.modulemap"
sp.public_header_files =
"Sources/Sentry/Public/*.h"
"Sources/Sentry/Public/*.h", "Sources/Sentry/_SentryPrivate/Public/*.h"

sp.preserve_path = "Sources/Sentry/include/module.modulemap"
sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" }
Expand All @@ -50,7 +50,7 @@ Pod::Spec.new do |s|

sp.preserve_path = "Sources/Sentry/include/module.modulemap"
sp.public_header_files =
"Sources/Sentry/Public/*.h", "Sources/Sentry/include/HybridPublic/*.h"
"Sources/Sentry/Public/*.h", "Sources/Sentry/_SentryPrivate/Public/*.h", "Sources/Sentry/include/HybridPublic/*.h"

sp.preserve_path = "Sources/Sentry/include/module.modulemap"
sp.resource_bundles = { "Sentry" => "Sources/Resources/PrivacyInfo.xcprivacy" }
Expand Down
38 changes: 23 additions & 15 deletions Sentry.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions SentryTestUtils/SentryLogExtensions.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
@testable import Sentry
@testable @_spi(Private) import Sentry

extension SentryLog {
public enum SentryLog {
public static func setTestDefaultLogLevel() {
SentryLogSwiftSupport.configure(true, diagnosticLevel: .debug)
}
Expand Down
156 changes: 1 addition & 155 deletions Sources/Sentry/SentryFileManager.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "SentryFileManager.h"
#import "FileUtils.h"
#import "SentryAppState.h"
#import "SentryDataCategoryMapper.h"
#import "SentryDateUtils.h"
Expand All @@ -22,51 +23,6 @@

#pragma mark - Helper Methods

BOOL
isErrorPathTooLong(NSError *error)
{
NSError *underlyingError = NULL;
if (@available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *)) {
underlyingError = error.underlyingErrors.firstObject;
}
if (underlyingError == NULL) {
id errorInUserInfo = [error.userInfo valueForKey:NSUnderlyingErrorKey];
if (errorInUserInfo && [errorInUserInfo isKindOfClass:[NSError class]]) {
underlyingError = errorInUserInfo;
}
}
if (underlyingError == NULL) {
underlyingError = error;
}
BOOL isEnameTooLong
= underlyingError.domain == NSPOSIXErrorDomain && underlyingError.code == ENAMETOOLONG;
// On older OS versions the error code is NSFileWriteUnknown
// Reference: https://developer.apple.com/forums/thread/128927?answerId=631839022#631839022
BOOL isUnknownError = underlyingError.domain == NSCocoaErrorDomain
&& underlyingError.code == NSFileWriteUnknownError;

return isEnameTooLong || isUnknownError;
}

BOOL
createDirectoryIfNotExists(NSString *path, NSError **error)
{
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:error];
if (success) {
return YES;
}

if (isErrorPathTooLong(*error)) {
SENTRY_LOG_FATAL(@"Failed to create directory, path is too long: %@", path);
}
*error = NSErrorFromSentryErrorWithUnderlyingError(kSentryErrorFileIO,
[NSString stringWithFormat:@"Failed to create the directory at path %@.", path], *error);
return NO;
}

/**
* @warning This is called from a `@synchronized` context in instance methods, but doesn't require
* that when calling from other static functions. Make sure you pay attention to where this is used
Expand Down Expand Up @@ -634,116 +590,6 @@ - (BOOL)isDirectory:(NSString *)path
return [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir] && isDir;
}

/**
* @note This method must be statically accessible because it will be called during app launch,
* before any instance of ``SentryFileManager`` exists, and so wouldn't be able to access this path
* from an objc property on it like the other paths.
*/
NSString *_Nullable sentryStaticCachesPath(void)
{
static NSString *_Nullable sentryStaticCachesPath = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// We request the users cache directory from Foundation.
// For iOS apps and macOS apps with sandboxing, this path will be scoped for the current
// app. For macOS apps without sandboxing, this path is not scoped and will be shared
// between all apps.
NSString *_Nullable cachesDirectory
= NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)
.firstObject;
if (cachesDirectory == nil) {
SENTRY_LOG_WARN(@"No caches directory location reported.");
return;
}

// We need to ensure our own scoped directory so that this path is not shared between other
// apps on the same system.
NSString *_Nullable scopedCachesDirectory = sentryGetScopedCachesDirectory(cachesDirectory);
if (!scopedCachesDirectory) {
SENTRY_LOG_WARN(@"Failed to get scoped static caches directory.");
return;
}
sentryStaticCachesPath = scopedCachesDirectory;
SENTRY_LOG_DEBUG(@"Using static cache directory: %@", sentryStaticCachesPath);
});
return sentryStaticCachesPath;
}

NSString *_Nullable sentryGetScopedCachesDirectory(NSString *cachesDirectory)
{
#if !TARGET_OS_OSX
// iOS apps are always sandboxed, therefore we can just early-return with the provided caches
// directory.
return cachesDirectory;
#else

// For macOS apps, we need to ensure our own sandbox so that this path is not shared between
// all apps that ship the SDK.

// We can not use the SentryNSProcessInfoWrapper here because this method is called before
// the SentryDependencyContainer is initialized.
NSProcessInfo *processInfo = [NSProcessInfo processInfo];

// Only apps running in a sandboxed environment have the `APP_SANDBOX_CONTAINER_ID` set as a
// process environment variable. Reference implementation:
// https://github.com/realm/realm-js/blob/a03127726939f08f608edbdb2341605938f25708/packages/realm/binding/apple/platform.mm#L58-L74
BOOL isSandboxed = processInfo.environment[@"APP_SANDBOX_CONTAINER_ID"] != nil;

// The bundle identifier is used to create a unique cache directory for the app.
// If the bundle identifier is not available, we use the name of the executable.
// Note: `SentryCrash.getBundleName` is using `CFBundleName` to create a scoped directory.
// That value can be absent, therefore we use a more stable approach here.
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *lastPathComponent = [[[NSBundle mainBundle] executablePath] lastPathComponent];

// Due to `NSProcessInfo` and `NSBundle` not being mockable in unit tests, we extract only the
// logic to a separate function.
return sentryBuildScopedCachesDirectoryPath(
cachesDirectory, isSandboxed, bundleIdentifier, lastPathComponent);
#endif
}

NSString *_Nullable sentryBuildScopedCachesDirectoryPath(NSString *cachesDirectory,
BOOL isSandboxed, NSString *_Nullable bundleIdentifier, NSString *_Nullable lastPathComponent)
{
// If the app is sandboxed, we can just use the provided caches directory.
if (isSandboxed) {
return cachesDirectory;
}

// If the macOS app is not sandboxed, we need to manually create a scoped cache
// directory. The cache path must be unique an stable over app launches, therefore we
// can not use any changing identifier.
SENTRY_LOG_DEBUG(
@"App is not sandboxed, extending default cache directory with bundle identifier.");
NSString *_Nullable identifier = bundleIdentifier;
if (identifier == nil) {
SENTRY_LOG_WARN(@"No bundle identifier found, using main bundle executable name.");
identifier = lastPathComponent;
} else if (identifier.length == 0) {
SENTRY_LOG_WARN(@"Bundle identifier exists but is zero length, using main bundle "
@"executable name.");
identifier = lastPathComponent;
}

// If neither the bundle identifier nor the executable name are available, we can't
// create a unique and stable cache directory.
// We do not fall back to any default path, because it could be shared with other apps
// and cause leaks impacting other apps.
if (identifier == nil) {
SENTRY_LOG_ERROR(@"No bundle identifier found, cannot create cache directory.");
return nil;
}

// It's unlikely that the executable name will be zero length, but we'll cover this case anyways
if (identifier.length == 0) {
SENTRY_LOG_ERROR(@"Executable name was zero length.");
return nil;
}

return [cachesDirectory stringByAppendingPathComponent:identifier];
}

NSString *_Nullable sentryStaticBasePath(void)
{
static NSString *_Nullable sentryStaticBasePath = nil;
Expand Down
158 changes: 158 additions & 0 deletions Sources/Sentry/_SentryPrivate/FileUtils.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#import "FileUtils.h"
#import "SentryError.h"
#import "SentryLog.h"

BOOL
isErrorPathTooLong(NSError *error)
{
NSError *underlyingError = NULL;
if (@available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *)) {
underlyingError = error.underlyingErrors.firstObject;
}
if (underlyingError == NULL) {
id errorInUserInfo = [error.userInfo valueForKey:NSUnderlyingErrorKey];
if (errorInUserInfo && [errorInUserInfo isKindOfClass:[NSError class]]) {
underlyingError = errorInUserInfo;

Check warning on line 15 in Sources/Sentry/_SentryPrivate/FileUtils.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/_SentryPrivate/FileUtils.m#L15

Added line #L15 was not covered by tests
}
}
if (underlyingError == NULL) {
underlyingError = error;
}
BOOL isEnameTooLong
= underlyingError.domain == NSPOSIXErrorDomain && underlyingError.code == ENAMETOOLONG;
// On older OS versions the error code is NSFileWriteUnknown
// Reference: https://developer.apple.com/forums/thread/128927?answerId=631839022#631839022
BOOL isUnknownError = underlyingError.domain == NSCocoaErrorDomain
&& underlyingError.code == NSFileWriteUnknownError;

return isEnameTooLong || isUnknownError;
}

BOOL
createDirectoryIfNotExists(NSString *path, NSError **error)
{
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:error];
if (success) {
return YES;
}

if (isErrorPathTooLong(*error)) {
SENTRY_LOG_FATAL(@"Failed to create directory, path is too long: %@", path);
}
*error = NSErrorFromSentryErrorWithUnderlyingError(kSentryErrorFileIO,
[NSString stringWithFormat:@"Failed to create the directory at path %@.", path], *error);
return NO;
}

NSString *_Nullable sentryBuildScopedCachesDirectoryPath(NSString *cachesDirectory,
BOOL isSandboxed, NSString *_Nullable bundleIdentifier, NSString *_Nullable lastPathComponent)
{
// If the app is sandboxed, we can just use the provided caches directory.
if (isSandboxed) {
return cachesDirectory;
}

// If the macOS app is not sandboxed, we need to manually create a scoped cache
// directory. The cache path must be unique an stable over app launches, therefore we
// can not use any changing identifier.
SENTRY_LOG_DEBUG(
@"App is not sandboxed, extending default cache directory with bundle identifier.");
NSString *_Nullable identifier = bundleIdentifier;
if (identifier == nil) {
SENTRY_LOG_WARN(@"No bundle identifier found, using main bundle executable name.");
identifier = lastPathComponent;
} else if (identifier.length == 0) {
SENTRY_LOG_WARN(@"Bundle identifier exists but is zero length, using main bundle "
@"executable name.");
identifier = lastPathComponent;
}

// If neither the bundle identifier nor the executable name are available, we can't
// create a unique and stable cache directory.
// We do not fall back to any default path, because it could be shared with other apps
// and cause leaks impacting other apps.
if (identifier == nil) {
SENTRY_LOG_ERROR(@"No bundle identifier found, cannot create cache directory.");
return nil;
}

// It's unlikely that the executable name will be zero length, but we'll cover this case anyways
if (identifier.length == 0) {
SENTRY_LOG_ERROR(@"Executable name was zero length.");
return nil;
}

return [cachesDirectory stringByAppendingPathComponent:identifier];
}

NSString *_Nullable sentryGetScopedCachesDirectory(NSString *cachesDirectory)
{
#if !TARGET_OS_OSX
// iOS apps are always sandboxed, therefore we can just early-return with the provided caches
// directory.
return cachesDirectory;
#else

// For macOS apps, we need to ensure our own sandbox so that this path is not shared between
// all apps that ship the SDK.

// We can not use the SentryNSProcessInfoWrapper here because this method is called before
// the SentryDependencyContainer is initialized.
NSProcessInfo *processInfo = [NSProcessInfo processInfo];

// Only apps running in a sandboxed environment have the `APP_SANDBOX_CONTAINER_ID` set as a
// process environment variable. Reference implementation:
// https://github.com/realm/realm-js/blob/a03127726939f08f608edbdb2341605938f25708/packages/realm/binding/apple/platform.mm#L58-L74
BOOL isSandboxed = processInfo.environment[@"APP_SANDBOX_CONTAINER_ID"] != nil;

// The bundle identifier is used to create a unique cache directory for the app.
// If the bundle identifier is not available, we use the name of the executable.
// Note: `SentryCrash.getBundleName` is using `CFBundleName` to create a scoped directory.
// That value can be absent, therefore we use a more stable approach here.
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *lastPathComponent = [[[NSBundle mainBundle] executablePath] lastPathComponent];

// Due to `NSProcessInfo` and `NSBundle` not being mockable in unit tests, we extract only the
// logic to a separate function.
return sentryBuildScopedCachesDirectoryPath(
cachesDirectory, isSandboxed, bundleIdentifier, lastPathComponent);
#endif
}

/**
* @note This method must be statically accessible because it will be called during app launch,
* before any instance of ``SentryFileManager`` exists, and so wouldn't be able to access this path
* from an objc property on it like the other paths.
*/
NSString *_Nullable sentryStaticCachesPath(void)
{
static NSString *_Nullable sentryStaticCachesPath = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// We request the users cache directory from Foundation.
// For iOS apps and macOS apps with sandboxing, this path will be scoped for the current
// app. For macOS apps without sandboxing, this path is not scoped and will be shared
// between all apps.
NSString *_Nullable cachesDirectory
= NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)
.firstObject;
if (cachesDirectory == nil) {
SENTRY_LOG_WARN(@"No caches directory location reported.");
return;

Check warning on line 144 in Sources/Sentry/_SentryPrivate/FileUtils.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/_SentryPrivate/FileUtils.m#L144

Added line #L144 was not covered by tests
}

// We need to ensure our own scoped directory so that this path is not shared between other
// apps on the same system.
NSString *_Nullable scopedCachesDirectory = sentryGetScopedCachesDirectory(cachesDirectory);
if (!scopedCachesDirectory) {
SENTRY_LOG_WARN(@"Failed to get scoped static caches directory.");
return;

Check warning on line 152 in Sources/Sentry/_SentryPrivate/FileUtils.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/_SentryPrivate/FileUtils.m#L152

Added line #L152 was not covered by tests
}
sentryStaticCachesPath = scopedCachesDirectory;
SENTRY_LOG_DEBUG(@"Using static cache directory: %@", sentryStaticCachesPath);
});
return sentryStaticCachesPath;
}
Loading
Loading