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
92 changes: 86 additions & 6 deletions Bugsnag.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Bugsnag.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions Bugsnag/Client/BugsnagClient+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,25 @@ NS_ASSUME_NONNULL_BEGIN
options:(BugsnagErrorOptions *_Nullable)options
block:(_Nullable BugsnagOnErrorBlock)block;

/**
* Create and notify a plain event without automatic enrichment.
*
* This method creates a minimal event without breadcrumbs, feature flags, threads, or other
* automatic context enrichment. It's designed for external diagnostic systems (like MetricKit)
* that provide their own timestamp and metadata.
*
* @param errorClass The error class name
* @param errorMessage The error message
* @param stacktrace Array of BugsnagStackframe objects
* @param timestamp Event timestamp (or nil for current time)
* @param block Optional callback to customize the event before sending
*/
- (void)notifyPlainEventWithErrorClass:(NSString *)errorClass
errorMessage:(NSString *)errorMessage
stacktrace:(NSArray<BugsnagStackframe *> *)stacktrace
timestamp:(NSDate * _Nullable)timestamp
block:(BugsnagOnErrorBlock _Nullable)block;

@end

NS_ASSUME_NONNULL_END
76 changes: 76 additions & 0 deletions Bugsnag/Client/BugsnagClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
#import "BugsnagUser+Private.h"
#import "BSGPersistentDeviceID.h"
#import "BugsnagCocoaPerformanceFromBugsnagCocoa.h"
#import "BugsnagCrossTalkAPI.h"
#import "BSGPluginRegistry.h"
#import "BSGPersistentFeatureFlagStore.h"
#import "BSGAtomicFeatureFlagStore.h"
#import "BSGCompositeFeatureFlagStore.h"
Expand Down Expand Up @@ -262,6 +264,8 @@ - (void)start {

// Map our bridged API early on.
[BugsnagCocoaPerformanceFromBugsnagCocoa sharedInstance];

[BugsnagCrossTalkAPI initializeWithClient:self];

BSGCrashSentryInstall(self.configuration, BSSerializeDataCrashHandler);

Expand Down Expand Up @@ -328,6 +332,8 @@ - (void)start {
}
}

[BSGPluginRegistry loadPluginsWithConfiguration:self.configuration];

self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:self];
[self.sessionTracker startWithNotificationCenter:center isInForeground:bsg_runContext->isForeground];

Expand Down Expand Up @@ -994,6 +1000,76 @@ - (void)notifyInternal:(BugsnagEvent *_Nonnull)event
[self addAutoBreadcrumbForEvent:event];
}

/**
* Create and notify a plain event without automatic enrichment.
*
* This is designed for external diagnostic systems (like MetricKit) that provide
* their own timestamp and don't need breadcrumbs, feature flags, etc.
*/
- (void)notifyPlainEventWithErrorClass:(NSString *)errorClass
errorMessage:(NSString *)errorMessage
stacktrace:(NSArray<BugsnagStackframe *> *)stacktrace
timestamp:(NSDate * _Nullable)timestamp
block:(BugsnagOnErrorBlock _Nullable)block {

// Minimal config checks
if (!self.configuration.shouldSendReports) {
bsg_log_info("Discarding plain event because shouldSendReports is NO");
return;
}

if ([self.configuration shouldDiscardErrorClass:errorClass]) {
bsg_log_info(@"Discarding plain event because errorClass \"%@\" matched configuration.discardClasses", errorClass);
return;
}

// Get system info for app/device generation
NSDictionary *systemInfo = [BSG_KSSystemInfo systemInfo];

// Create minimal metadata (no automatic enrichment)
BugsnagMetadata *metadata = [[BugsnagMetadata alloc] init];

// Create error
BugsnagError *error = [[BugsnagError alloc] initWithErrorClass:errorClass
errorMessage:errorMessage
errorType:BSGErrorTypeCocoa
stacktrace:stacktrace];

// Create handled state (plain events are considered handled)
BugsnagHandledState *handledState = [BugsnagHandledState handledStateWithSeverityReason:HandledError];

// Generate app and device with current state
BugsnagAppWithState *app = [self generateAppWithState:systemInfo];
BugsnagDeviceWithState *device = [self generateDeviceWithState:systemInfo];

// Set custom timestamp if provided
if (timestamp) {
device.time = timestamp;
}

// Create event with minimal context (no breadcrumbs, no feature flags, no threads)
BugsnagEvent *event = [[BugsnagEvent alloc] initWithApp:app
device:device
handledState:handledState
user:[self.user withId]
metadata:metadata
breadcrumbs:@[] // No breadcrumbs for plain events
errors:@[error]
threads:@[] // No threads for plain events
session:nil // Will be set in notifyInternal if needed
attemptDeliveryOnCrash:self.configuration.attemptDeliveryOnCrash];

event.apiKey = self.configuration.apiKey;
event.context = self.context;
event.groupingDiscriminator = self.groupingDiscriminator_;
event.correlation = [self getCurrentCorrelation];

// No feature flags for plain events

// Call notifyInternal to handle the rest (onError callbacks, session tracking, delivery)
[self notifyInternal:event block:block];
}

// MARK: - Breadcrumbs

- (void)addAutoBreadcrumbForEvent:(BugsnagEvent *)event {
Expand Down
9 changes: 9 additions & 0 deletions Bugsnag/Configuration/BugsnagConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#import "BugsnagErrorTypes.h"
#import "BugsnagLogger.h"
#import "BugsnagMetadata+Private.h"
#import "BugsnagMetricKitTypes.h"
#import "BugsnagUser+Private.h"

const NSUInteger BugsnagAppHangThresholdFatalOnly = INT_MAX;
Expand Down Expand Up @@ -89,6 +90,9 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone {
[copy setContext:self.context];
[copy setEnabledBreadcrumbTypes:self.enabledBreadcrumbTypes];
[copy setEnabledErrorTypes:self.enabledErrorTypes];
#if BSG_HAVE_METRICKIT
[copy setEnabledMetricKitDiagnostics:self.enabledMetricKitDiagnostics];
#endif
[copy setEnabledReleaseStages:self.enabledReleaseStages];
copy.discardClasses = self.discardClasses;
[copy setRedactedKeys:self.redactedKeys];
Expand Down Expand Up @@ -195,6 +199,11 @@ - (instancetype)initWithApiKey:(NSString *)apiKey {
#endif
// Default to recording all error types
_enabledErrorTypes = [BugsnagErrorTypes new];

#if BSG_HAVE_METRICKIT
// Default to MetricKit disabled (opt-in)
_enabledMetricKitDiagnostics = [BugsnagMetricKitTypes new];
#endif

// Enabling OOM detection only happens in release builds, to avoid triggering
// the heuristic when killing/restarting an app in Xcode or similar.
Expand Down
27 changes: 27 additions & 0 deletions Bugsnag/Configuration/BugsnagMetricKitTypes.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// BugsnagMetricKitTypes.m
// Bugsnag
//
// Created by Robert Bartoszewski on 09/03/2026.
// Copyright © 2026 Bugsnag Inc. All rights reserved.
//

#import "BugsnagMetricKitTypes.h"

@implementation BugsnagMetricKitTypes

- (instancetype)init {
if ((self = [super init])) {
// MetricKit is opt-in and disabled by default
_enabled = NO;
// All diagnostic types are enabled by default when MetricKit is enabled
_crashDiagnostics = YES;
_cpuExceptionDiagnostics = YES;
_appLaunchDiagnostics = YES;
_hangDiagnostics = YES;
_diskWriteExceptionDiagnostics = YES;
}
return self;
}

@end
1 change: 1 addition & 0 deletions Bugsnag/Helpers/BSGDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#define BSG_HAVE_SYSCALL (TARGET_OS_IOS || TARGET_OS_TV )
#define BSG_HAVE_UIDEVICE __has_include(<UIKit/UIDevice.h>)
#define BSG_HAVE_WINDOW (TARGET_OS_OSX || TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION)
#define BSG_HAVE_METRICKIT (TARGET_OS_OSX || TARGET_OS_IOS || TARGET_OS_VISION)

// Capabilities dependent upon previously defined capabilities
#define BSG_HAVE_APP_HANG_DETECTION (BSG_HAVE_MACH_THREADS)
Expand Down
33 changes: 33 additions & 0 deletions Bugsnag/Helpers/BSGPluginRegistry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// BSGPluginRegistry.h
// Bugsnag
//
// Created by Robert Bartoszewski on 17/03/2026.
//

#import <Foundation/Foundation.h>

@class BugsnagConfiguration;
@protocol BSGPlugin;

NS_ASSUME_NONNULL_BEGIN

/**
* Registry for discovering and loading Bugsnag plugins at runtime.
* Plugins are discovered using NSClassFromString and automatically initialized if present.
* Plugins should conform to the BSGPlugin protocol.
*/
@interface BSGPluginRegistry : NSObject

/**
* Loads all available Bugsnag plugins with configuration.
* Plugins must implement a +install class method to be loaded.
* If plugins implement +configure: they will receive the configuration.
*
* @param configuration The Bugsnag configuration to pass to plugins
*/
+ (void)loadPluginsWithConfiguration:(nullable BugsnagConfiguration *)configuration;

@end

NS_ASSUME_NONNULL_END
30 changes: 30 additions & 0 deletions Bugsnag/Helpers/BSGPluginRegistry.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// BSGPluginRegistry.m
// Bugsnag
//
// Created by Robert Bartoszewski on 17/03/2026.
//

#import "BSGPluginRegistry.h"
#import "BSGPlugin.h"
#import "BugsnagConfiguration.h"

@implementation BSGPluginRegistry

+ (void)loadPluginsWithConfiguration:(BugsnagConfiguration *)configuration {
// Check for MetricKit plugin
Class metricKitPlugin = NSClassFromString(@"BugsnagMetricKitPlugin");
if (metricKitPlugin) {
if ([metricKitPlugin respondsToSelector:@selector(install)]) {
[metricKitPlugin performSelector:@selector(install)];
}

if (configuration && [metricKitPlugin respondsToSelector:@selector(configure:)]) {
[metricKitPlugin performSelector:@selector(configure:) withObject:configuration];
}
}

// Future plugins can be added here
}

@end
101 changes: 101 additions & 0 deletions Bugsnag/Helpers/BugsnagCrossTalkAPI.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// BugsnagCrossTalkAPI.h
// Bugsnag
//
// Created by Robert Bartoszewski on 27/03/2026.
// Copyright © 2026 Bugsnag Inc. All rights reserved.
//

// Bugsnag CrossTalk API
//
// CrossTalk is an Objective-C layer for sharing private APIs between Bugsnag libraries.
// It allows client libraries (like BugsnagMetricKitPlugin) to call internal functions
// without the usual worries of breaking downstream clients whenever internal code changes.
//
// This API exposes methods for:
// - Reporting errors (notifyError:block:)
// - Symbolicating stack frames (symbolicateStackframes:)
//
// NOTE: This class name MUST be globally unique across ALL Bugsnag libraries!

#import <Foundation/Foundation.h>

@class BugsnagClient;
@class BugsnagEvent;
@class BugsnagStackframe;

NS_ASSUME_NONNULL_BEGIN

/**
* CrossTalk API for Bugsnag error reporting and symbolication.
* Allows plugins like BugsnagMetricKitPlugin to access Bugsnag functionality.
*/
@interface BugsnagCrossTalkAPI : NSObject

+ (instancetype) sharedInstance;

/**
* Initialize the CrossTalk API with a Bugsnag client.
* This should be called during Bugsnag startup.
*
* @param client The BugsnagClient instance to use for error reporting
*/
+ (void)initializeWithClient:(BugsnagClient *)client;

/**
* Map a named API to a method with the specified selector.
* This is used by client libraries to safely access versioned APIs.
*
* @param apiName The name of the API version (e.g., "notifyErrorV1")
* @param toSelector The selector to map the API to
* @return An error if mapping failed, or nil on success
*/
+ (NSError * _Nullable)mapAPINamed:(NSString *)apiName toSelector:(SEL)toSelector;

#pragma mark - Internal API Methods (Not for direct use - accessed via mapAPINamed:)

// These methods are internal and accessed via runtime mapping.
// DO NOT call these directly - use the mapped selectors instead.

/**
* Create a plain event without automatic enrichment (V1).
* Internal versioned method - access via mapAPINamed:@"notifyPlainEventV1:errorMessage:stacktrace:timestamp:block:"
*
* @param errorClass The error class name
* @param errorMessage The error message
* @param stacktrace Array of BugsnagStackframe objects
* @param timestamp Event timestamp (or nil for current time)
* @param block Optional callback to customize the event before sending
*/
- (void)notifyPlainEventV1:(NSString *)errorClass
errorMessage:(NSString *)errorMessage
stacktrace:(NSArray<BugsnagStackframe *> *)stacktrace
timestamp:(NSDate * _Nullable)timestamp
block:(BOOL (^ _Nullable)(BugsnagEvent *event))block;

@end

/**
* A very permissive proxy that won't crash if a method or property doesn't exist.
*
* When returning instances of Bugsnag classes, wrap them in this proxy so that
* they don't crash when that class's API changes.
*
* WARNING: Returning internal classes is effectively creating a contract between Bugsnag libraries!
* Be VERY conservative about any internal class you expose, because its interfaces will effectively
* be "published", and changing a method's signature could break client libraries that use it.
*
* Adding/removing methods/properties is fine, but changing signatures WILL break things.
*
* Some ways to protect against breakage due to changed method signatures:
* - Convert to maps and arrays instead
* - Create custom classes designed specifically for library interop
* - Create versioned wrapper methods in the classes and access those instead (doStuffV1, doStuffV2, etc)
*/
@interface BugsnagCrossTalkProxiedObject : NSProxy

+ (instancetype _Nullable) proxied:(id _Nullable)delegate;

@end

NS_ASSUME_NONNULL_END
Loading
Loading