diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index cd9c4c182..81a315682 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -886,6 +886,30 @@ 9630820F2E7C1FB7002F3E63 /* BSG_KSPlatformSpecificDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 9630820D2E7C1FB7002F3E63 /* BSG_KSPlatformSpecificDefines.h */; }; 963082102E7C1FB7002F3E63 /* BSG_KSPlatformSpecificDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 9630820D2E7C1FB7002F3E63 /* BSG_KSPlatformSpecificDefines.h */; }; 963082112E7C1FB7002F3E63 /* BSG_KSPlatformSpecificDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 9630820D2E7C1FB7002F3E63 /* BSG_KSPlatformSpecificDefines.h */; }; + 963173BC2F6AC276006643BA /* BugsnagMetricKitTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 963173BB2F6AC276006643BA /* BugsnagMetricKitTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 963173BD2F6AC276006643BA /* BugsnagMetricKitTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 963173BB2F6AC276006643BA /* BugsnagMetricKitTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 963173BE2F6AC276006643BA /* BugsnagMetricKitTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 963173BB2F6AC276006643BA /* BugsnagMetricKitTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 963173BF2F6AC276006643BA /* BugsnagMetricKitTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 963173BB2F6AC276006643BA /* BugsnagMetricKitTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 963173C12F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 963173C02F6AC2A3006643BA /* BugsnagMetricKitTypes.m */; }; + 963173C22F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 963173C02F6AC2A3006643BA /* BugsnagMetricKitTypes.m */; }; + 963173C32F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 963173C02F6AC2A3006643BA /* BugsnagMetricKitTypes.m */; }; + 963173C42F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 963173C02F6AC2A3006643BA /* BugsnagMetricKitTypes.m */; }; + 963173C52F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 963173C02F6AC2A3006643BA /* BugsnagMetricKitTypes.m */; }; + 965B94102F6AC37800EAC583 /* BugsnagMetricKitTypes.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 963173BB2F6AC276006643BA /* BugsnagMetricKitTypes.h */; }; + 965B94192F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 965B94182F6AE0D800EAC583 /* BSGPluginRegistry.m */; }; + 965B941A2F6AE0D800EAC583 /* BSGPluginRegistry.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94172F6AE0D800EAC583 /* BSGPluginRegistry.h */; }; + 965B941B2F6AE0D800EAC583 /* BSGPluginRegistry.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94172F6AE0D800EAC583 /* BSGPluginRegistry.h */; }; + 965B941C2F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 965B94182F6AE0D800EAC583 /* BSGPluginRegistry.m */; }; + 965B941D2F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 965B94182F6AE0D800EAC583 /* BSGPluginRegistry.m */; }; + 965B941E2F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 965B94182F6AE0D800EAC583 /* BSGPluginRegistry.m */; }; + 965B941F2F6AE0D800EAC583 /* BSGPluginRegistry.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94172F6AE0D800EAC583 /* BSGPluginRegistry.h */; }; + 965B94202F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 965B94182F6AE0D800EAC583 /* BSGPluginRegistry.m */; }; + 965B94212F6AE0D800EAC583 /* BSGPluginRegistry.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94172F6AE0D800EAC583 /* BSGPluginRegistry.h */; }; + 965B94232F6AE3EB00EAC583 /* BSGPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94222F6AE3E900EAC583 /* BSGPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 965B94242F6AE3EB00EAC583 /* BSGPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94222F6AE3E900EAC583 /* BSGPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 965B94252F6AE3EB00EAC583 /* BSGPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94222F6AE3E900EAC583 /* BSGPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 965B94262F6AE3EB00EAC583 /* BSGPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94222F6AE3E900EAC583 /* BSGPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 965B94272F6AE4B500EAC583 /* BSGPlugin.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 965B94222F6AE3E900EAC583 /* BSGPlugin.h */; }; 968BFBCB2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 968BFBC92D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.h */; }; 968BFBCC2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBCA2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m */; }; 968BFBCD2D011BC300DCC24B /* BSGPersistentFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 968BFBC92D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.h */; }; @@ -944,6 +968,15 @@ 96E45C0A2D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 96E45C032D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.m */; }; 96E45C0B2D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 96E45C032D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.m */; }; 96E45C0C2D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 96E45C032D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.m */; }; + 96ECDA462F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96ECDA452F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm */; }; + 96ECDA472F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 96ECDA442F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h */; }; + 96ECDA482F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96ECDA452F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm */; }; + 96ECDA492F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 96ECDA442F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h */; }; + 96ECDA4A2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96ECDA452F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm */; }; + 96ECDA4B2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 96ECDA442F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h */; }; + 96ECDA4C2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96ECDA452F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm */; }; + 96ECDA4D2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96ECDA452F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm */; }; + 96ECDA4E2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 96ECDA442F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h */; }; CB156241270707740097334C /* KSCrashNames_Test.m in Sources */ = {isa = PBXBuildFile; fileRef = CB156240270707740097334C /* KSCrashNames_Test.m */; }; CB156242270707740097334C /* KSCrashNames_Test.m in Sources */ = {isa = PBXBuildFile; fileRef = CB156240270707740097334C /* KSCrashNames_Test.m */; }; CB156243270707740097334C /* KSCrashNames_Test.m in Sources */ = {isa = PBXBuildFile; fileRef = CB156240270707740097334C /* KSCrashNames_Test.m */; }; @@ -1379,6 +1412,8 @@ dstPath = include/Bugsnag; dstSubfolderSpec = 16; files = ( + 965B94272F6AE4B500EAC583 /* BSGPlugin.h in CopyFiles */, + 965B94102F6AC37800EAC583 /* BugsnagMetricKitTypes.h in CopyFiles */, 1C3163972F34C80C00A3F1E2 /* BugsnagHttpResponse.h in CopyFiles */, 1C3163982F34C80C00A3F1E2 /* BugsnagHttpRequest.h in CopyFiles */, 1C8CB8B12EB968EA007BF492 /* BugsnagCaptureOptions.h in CopyFiles */, @@ -1727,6 +1762,11 @@ 9627A1392D92200300696E3C /* WriterTestsSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WriterTestsSupport.h; sourceTree = ""; }; 9627A13A2D92201B00696E3C /* WriterTestsSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WriterTestsSupport.m; sourceTree = ""; }; 9630820D2E7C1FB7002F3E63 /* BSG_KSPlatformSpecificDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSG_KSPlatformSpecificDefines.h; sourceTree = ""; }; + 963173BB2F6AC276006643BA /* BugsnagMetricKitTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagMetricKitTypes.h; sourceTree = ""; }; + 963173C02F6AC2A3006643BA /* BugsnagMetricKitTypes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagMetricKitTypes.m; sourceTree = ""; }; + 965B94172F6AE0D800EAC583 /* BSGPluginRegistry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGPluginRegistry.h; sourceTree = ""; }; + 965B94182F6AE0D800EAC583 /* BSGPluginRegistry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGPluginRegistry.m; sourceTree = ""; }; + 965B94222F6AE3E900EAC583 /* BSGPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGPlugin.h; sourceTree = ""; }; 968BFBC92D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGPersistentFeatureFlagStore.h; sourceTree = ""; }; 968BFBCA2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGPersistentFeatureFlagStore.m; sourceTree = ""; }; 968BFBD42D0125C700DCC24B /* BSGStoredFeatureFlag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGStoredFeatureFlag.m; sourceTree = ""; }; @@ -1740,6 +1780,8 @@ 96E45BFD2D103B1F00BEF978 /* BSGFeatureFlagStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGFeatureFlagStore.h; sourceTree = ""; }; 96E45C022D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGCompositeFeatureFlagStore.h; sourceTree = ""; }; 96E45C032D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGCompositeFeatureFlagStore.m; sourceTree = ""; }; + 96ECDA442F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagCrossTalkAPI.h; sourceTree = ""; }; + 96ECDA452F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BugsnagCrossTalkAPI.mm; sourceTree = ""; }; CB156240270707740097334C /* KSCrashNames_Test.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSCrashNames_Test.m; sourceTree = ""; }; CB28F11D282A71AB003AB200 /* BSGWatchKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGWatchKit.h; sourceTree = ""; }; CB33CCFC2703438400C76656 /* BSG_KSCrashNames.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSG_KSCrashNames.h; sourceTree = ""; }; @@ -2242,6 +2284,7 @@ 01D8EC3C256FC6C3006F2A2D /* BugsnagConfiguration+Private.h */, 008967C92486DA2D00DC48C2 /* BugsnagEndpointConfiguration.m */, 008967CF2486DA2D00DC48C2 /* BugsnagErrorTypes.m */, + 963173C02F6AC2A3006643BA /* BugsnagMetricKitTypes.m */, ); path = Configuration; sourceTree = ""; @@ -2272,7 +2315,6 @@ 00AD1CF524869EE500A27979 /* Helpers */ = { isa = PBXGroup; children = ( - 1C3163872F33D65D00A3F1E2 /* BSGHttpKeys.h */, 008969272486DAD000DC48C2 /* BSG_RFC3339DateTool.h */, 008969142486DAD000DC48C2 /* BSG_RFC3339DateTool.m */, 010FF28225ED2A8D00E4F2B0 /* BSGAppHangDetector.h */, @@ -2282,11 +2324,14 @@ CBEC89282A4AC2920088A3CE /* BSGFilesystem.h */, CBEC89292A4AC2920088A3CE /* BSGFilesystem.m */, CB374456283F5FD400A3955E /* BSGHardware.h */, + 1C3163872F33D65D00A3F1E2 /* BSGHttpKeys.h */, 01847D942644140F00ADA4C7 /* BSGInternalErrorReporter.h */, 01847D952644140F00ADA4C7 /* BSGInternalErrorReporter.m */, CBCF77A125010648004AF22A /* BSGJSONSerialization.h */, CBCF77A225010648004AF22A /* BSGJSONSerialization.m */, 008968152486DA5600DC48C2 /* BSGKeys.h */, + 965B94172F6AE0D800EAC583 /* BSGPluginRegistry.h */, + 965B94182F6AE0D800EAC583 /* BSGPluginRegistry.m */, 0154E20028070AEA009044E4 /* BSGRunContext.h */, 0154E20128070AEA009044E4 /* BSGRunContext.m */, 008968112486DA5600DC48C2 /* BSGSerialization.h */, @@ -2297,13 +2342,15 @@ 01B79DA7267CC4A000C8CC5E /* BSGUtils.h */, 01B79DA8267CC4A000C8CC5E /* BSGUtils.m */, CB28F11D282A71AB003AB200 /* BSGWatchKit.h */, + 09E312ED2BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.h */, + 09E312EE2BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m */, 008968102486DA5600DC48C2 /* BugsnagCollections.h */, 008968172486DA5600DC48C2 /* BugsnagCollections.m */, + 96ECDA442F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h */, + 96ECDA452F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm */, 008968142486DA5600DC48C2 /* BugsnagLogger.h */, 015F528325C15BB7000D1915 /* MRCCanary.m */, CB37449B284756C100A3955E /* stb_sprintf.h */, - 09E312ED2BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.h */, - 09E312EE2BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m */, ); path = Helpers; sourceTree = ""; @@ -2405,14 +2452,13 @@ 3A700A7F24A63A8E0068CD1B /* Bugsnag */ = { isa = PBXGroup; children = ( - 1C3163762F327B2000A3F1E2 /* BugsnagHttpResponse.h */, - 1C3163712F327B1200A3F1E2 /* BugsnagHttpRequest.h */, - 1C3E2DB52EAD462500B32AD2 /* BugsnagCaptureOptions.h */, 3A700A8624A63A8E0068CD1B /* BSG_KSCrashReportWriter.h */, + 965B94222F6AE3E900EAC583 /* BSGPlugin.h */, 3A700A8C24A63A8E0068CD1B /* Bugsnag.h */, 3A700A9024A63A8E0068CD1B /* BugsnagApp.h */, 3A700A8B24A63A8E0068CD1B /* BugsnagAppWithState.h */, 3A700A8524A63A8E0068CD1B /* BugsnagBreadcrumb.h */, + 1C3E2DB52EAD462500B32AD2 /* BugsnagCaptureOptions.h */, 3A700A8924A63A8E0068CD1B /* BugsnagClient.h */, 3A700A8D24A63A8E0068CD1B /* BugsnagConfiguration.h */, 01C41A27288FD3EA00BAE31A /* BugsnagDefines.h */, @@ -2424,9 +2470,12 @@ 3A700A8824A63A8E0068CD1B /* BugsnagEvent.h */, 01099391273D123800128BBE /* BugsnagFeatureFlag.h */, 010993B0273D2F6100128BBE /* BugsnagFeatureFlagStore.h */, + 1C3163712F327B1200A3F1E2 /* BugsnagHttpRequest.h */, + 1C3163762F327B2000A3F1E2 /* BugsnagHttpResponse.h */, 01CCAEE825D414D60057268D /* BugsnagLastRunInfo.h */, 3A700A9324A63A8E0068CD1B /* BugsnagMetadata.h */, 3A700A8324A63A8E0068CD1B /* BugsnagMetadataStore.h */, + 963173BB2F6AC276006643BA /* BugsnagMetricKitTypes.h */, 3A700A8E24A63A8E0068CD1B /* BugsnagPlugin.h */, 3A700A8124A63A8E0068CD1B /* BugsnagSession.h */, 3A700A8224A63A8E0068CD1B /* BugsnagStackframe.h */, @@ -2490,6 +2539,7 @@ 3A700A9624A63AC60068CD1B /* BugsnagStackframe.h in Headers */, 96E45C042D10CCBE00BEF978 /* BSGCompositeFeatureFlagStore.h in Headers */, 3A700A9724A63AC60068CD1B /* BugsnagMetadataStore.h in Headers */, + 965B94242F6AE3EB00EAC583 /* BSGPlugin.h in Headers */, 3A700A9824A63AC60068CD1B /* BugsnagEndpointConfiguration.h in Headers */, CBB092902519F891007698BC /* BugsnagSystemState.h in Headers */, 3A700A9924A63AC60068CD1B /* BugsnagBreadcrumb.h in Headers */, @@ -2511,6 +2561,7 @@ 0126F78B25DD508C008483C2 /* BSGEventUploadOperation.h in Headers */, 3A700AA124A63ADC0068CD1B /* BugsnagConfiguration.h in Headers */, 01FF490628BF8B7B001F817B /* BugsnagInternals.h in Headers */, + 963173BF2F6AC276006643BA /* BugsnagMetricKitTypes.h in Headers */, 3A700AA224A63ADC0068CD1B /* BugsnagPlugin.h in Headers */, 3A700AA324A63ADC0068CD1B /* BugsnagDevice.h in Headers */, 3A700AA424A63ADC0068CD1B /* BugsnagApp.h in Headers */, @@ -2573,7 +2624,9 @@ 96E45BF32D1039F200BEF978 /* BSGAtomicFeatureFlagStore.h in Headers */, CB33CCFE2703438400C76656 /* BSG_KSCrashNames.h in Headers */, CBE9062A25A34DAB0045B965 /* BSGStorageMigratorV0V1.h in Headers */, + 965B941B2F6AE0D800EAC583 /* BSGPluginRegistry.h in Headers */, 008969932486DAD100DC48C2 /* BSG_KSLogger.h in Headers */, + 96ECDA4B2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h in Headers */, 008969F92486DAD100DC48C2 /* BSG_KSCrashReportVersion.h in Headers */, 0089680A2486DA4500DC48C2 /* BSGConnectivity.h in Headers */, 008968252486DA5600DC48C2 /* BugsnagLogger.h in Headers */, @@ -2619,15 +2672,18 @@ 01847D972644140F00ADA4C7 /* BSGInternalErrorReporter.h in Headers */, 017DCF8D2874212F000ECB22 /* BSGTelemetry.h in Headers */, 01840B7025DC26E200F95648 /* BSGEventUploader.h in Headers */, + 965B941A2F6AE0D800EAC583 /* BSGPluginRegistry.h in Headers */, 3A700AB024A63CFD0068CD1B /* BugsnagEvent.h in Headers */, CB4C83BF280FFB0600E7E2BD /* BSGDefines.h in Headers */, 3A700AB124A63CFD0068CD1B /* BugsnagClient.h in Headers */, 96E45BF42D1039F200BEF978 /* BSGAtomicFeatureFlagStore.h in Headers */, 3A700AB224A63CFD0068CD1B /* BugsnagUser.h in Headers */, 3A700AB324A63CFD0068CD1B /* BugsnagAppWithState.h in Headers */, + 963173BC2F6AC276006643BA /* BugsnagMetricKitTypes.h in Headers */, 3A700AB424A63CFD0068CD1B /* Bugsnag.h in Headers */, CB37449D284756C200A3955E /* stb_sprintf.h in Headers */, 0126F78C25DD508C008483C2 /* BSGEventUploadOperation.h in Headers */, + 96ECDA472F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h in Headers */, 3A700AB524A63CFD0068CD1B /* BugsnagConfiguration.h in Headers */, 01FF490728BF8B7B001F817B /* BugsnagInternals.h in Headers */, 3A700AB624A63CFD0068CD1B /* BugsnagPlugin.h in Headers */, @@ -2714,6 +2770,7 @@ 008969882486DAD100DC48C2 /* BSG_KSMachApple.h in Headers */, 008969CA2486DAD100DC48C2 /* BSG_RFC3339DateTool.h in Headers */, 008969F42486DAD100DC48C2 /* BSG_KSCrashState.h in Headers */, + 965B94232F6AE3EB00EAC583 /* BSGPlugin.h in Headers */, 0109939D273D13D800128BBE /* BSGMemoryFeatureFlagStore.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2737,15 +2794,18 @@ 01847D982644140F00ADA4C7 /* BSGInternalErrorReporter.h in Headers */, 017DCF8E2874212F000ECB22 /* BSGTelemetry.h in Headers */, 01840B7125DC26E200F95648 /* BSGEventUploader.h in Headers */, + 965B94212F6AE0D800EAC583 /* BSGPluginRegistry.h in Headers */, 3A700AC424A63D110068CD1B /* BugsnagEvent.h in Headers */, CB4C83C0280FFB0600E7E2BD /* BSGDefines.h in Headers */, 3A700AC524A63D110068CD1B /* BugsnagClient.h in Headers */, 96E45BF52D1039F200BEF978 /* BSGAtomicFeatureFlagStore.h in Headers */, 3A700AC624A63D110068CD1B /* BugsnagUser.h in Headers */, 3A700AC724A63D110068CD1B /* BugsnagAppWithState.h in Headers */, + 963173BE2F6AC276006643BA /* BugsnagMetricKitTypes.h in Headers */, 3A700AC824A63D110068CD1B /* Bugsnag.h in Headers */, CB37449E284756C200A3955E /* stb_sprintf.h in Headers */, 0126F78D25DD508C008483C2 /* BSGEventUploadOperation.h in Headers */, + 96ECDA492F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h in Headers */, 3A700AC924A63D110068CD1B /* BugsnagConfiguration.h in Headers */, 01FF490828BF8B7B001F817B /* BugsnagInternals.h in Headers */, 3A700ACA24A63D110068CD1B /* BugsnagPlugin.h in Headers */, @@ -2832,6 +2892,7 @@ 008969892486DAD100DC48C2 /* BSG_KSMachApple.h in Headers */, 008969CB2486DAD100DC48C2 /* BSG_RFC3339DateTool.h in Headers */, 008969F52486DAD100DC48C2 /* BSG_KSCrashState.h in Headers */, + 965B94262F6AE3EB00EAC583 /* BSGPlugin.h in Headers */, 0109939E273D13D800128BBE /* BSGMemoryFeatureFlagStore.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2861,6 +2922,7 @@ CBBDE945280068E60070DCD3 /* BugsnagLogger.h in Headers */, CBEC892D2A4AC2920088A3CE /* BSGFilesystem.h in Headers */, CBBDE99F2800699C0070DCD3 /* BSG_KSCrashSentry_Signal.h in Headers */, + 963173BD2F6AC276006643BA /* BugsnagMetricKitTypes.h in Headers */, CBBDE9952800698F0070DCD3 /* BSG_KSCrashReportVersion.h in Headers */, CBBDE9B2280069B20070DCD3 /* BSG_KSFileUtils.h in Headers */, CBBDE931280068AD0070DCD3 /* BSGEventUploadOperation.h in Headers */, @@ -2874,6 +2936,7 @@ 01C41A2B288FD3EB00BAE31A /* BugsnagDefines.h in Headers */, CBBDE93F280068D40070DCD3 /* BSGSerialization.h in Headers */, CBBDE954280068FD0070DCD3 /* BugsnagEvent.h in Headers */, + 965B941F2F6AE0D800EAC583 /* BSGPluginRegistry.h in Headers */, CBBDE962280068FD0070DCD3 /* BugsnagFeatureFlag.h in Headers */, CBBDE929280068AD0070DCD3 /* BSGEventUploadObjectOperation.h in Headers */, CBBDE92A280068AD0070DCD3 /* BSGEventUploader.h in Headers */, @@ -2920,6 +2983,7 @@ CBBDE9852800698F0070DCD3 /* BSG_KSCrashC.h in Headers */, 969EE10F2E7A9DC200600F63 /* BSG_KSCxaThrowSwapper.h in Headers */, CBBDE9202800688D0070DCD3 /* BSGConfigurationBuilder.h in Headers */, + 96ECDA4E2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.h in Headers */, CBBDE926280068AD0070DCD3 /* BSGEventUploadKSCrashReportOperation.h in Headers */, 969EE1182E7A9FA300600F63 /* BSG_KSMach-O.h in Headers */, CBBDE953280068FD0070DCD3 /* BugsnagErrorTypes.h in Headers */, @@ -2928,6 +2992,7 @@ CBBDE9932800698F0070DCD3 /* BSG_KSCrashIdentifier.h in Headers */, CBBDE99D2800699C0070DCD3 /* BSG_KSCrashSentry.h in Headers */, CBBDE9BE280069B20070DCD3 /* BSG_KSString.h in Headers */, + 965B94252F6AE3EB00EAC583 /* BSGPlugin.h in Headers */, CBBDE9B4280069B20070DCD3 /* BSG_KSSysCtl.h in Headers */, CBBDE952280068FD0070DCD3 /* BugsnagAppWithState.h in Headers */, CBBDE9BD280069B20070DCD3 /* BSG_KSBacktrace.h in Headers */, @@ -3318,6 +3383,7 @@ 008968842486DA9600DC48C2 /* BugsnagNotifier.m in Sources */, CBCF77A625010648004AF22A /* BSGJSONSerialization.m in Sources */, 008968912486DA9600DC48C2 /* BugsnagError.m in Sources */, + 965B941C2F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */, 0089687C2486DA9500DC48C2 /* BugsnagBreadcrumb.m in Sources */, 01A2958028B665F5005FCC8C /* BSGNetworkBreadcrumb.m in Sources */, 008967FA2486DA4500DC48C2 /* BugsnagApiClient.m in Sources */, @@ -3352,9 +3418,11 @@ 1C7ADF7A2EB1608F00A930A6 /* BugsnagCaptureOptions.m in Sources */, 008967BE2486DA1900DC48C2 /* BugsnagClient.m in Sources */, 09E312F32BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m in Sources */, + 963173C12F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */, 008968952486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, 968BFBD62D0125C800DCC24B /* BSGStoredFeatureFlag.m in Sources */, 008967FE2486DA4500DC48C2 /* BSGSessionUploader.m in Sources */, + 96ECDA4C2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */, 0089686B2486DA9500DC48C2 /* BugsnagEvent.m in Sources */, 008969A82486DAD100DC48C2 /* BSG_KSSysCtl.c in Sources */, CBEC89272A49BC1D0088A3CE /* BSGPersistentDeviceID.m in Sources */, @@ -3511,6 +3579,7 @@ 008968852486DA9600DC48C2 /* BugsnagNotifier.m in Sources */, 008968922486DA9600DC48C2 /* BugsnagError.m in Sources */, CBEC892F2A4AC2920088A3CE /* BSGFilesystem.m in Sources */, + 965B94192F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */, CBCF77A725010648004AF22A /* BSGJSONSerialization.m in Sources */, 0089687D2486DA9500DC48C2 /* BugsnagBreadcrumb.m in Sources */, 008967FB2486DA4500DC48C2 /* BugsnagApiClient.m in Sources */, @@ -3545,9 +3614,11 @@ 1C7ADF772EB1608F00A930A6 /* BugsnagCaptureOptions.m in Sources */, 008968BA2486DA9600DC48C2 /* BugsnagStacktrace.m in Sources */, 09E312F42BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m in Sources */, + 963173C22F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */, 00896A152486DAD100DC48C2 /* BSG_KSCrashSentry_Signal.c in Sources */, 968BFBDB2D0125CF00DCC24B /* BSGStoredFeatureFlag.m in Sources */, 01468F5625876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, + 96ECDA462F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */, 01CB95C3278F0C830077744A /* BSG_KSFile.c in Sources */, 008967BF2486DA1900DC48C2 /* BugsnagClient.m in Sources */, 008968962486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, @@ -3701,6 +3772,7 @@ CB33CD032703438400C76656 /* BSG_KSCrashNames.c in Sources */, 008968862486DA9600DC48C2 /* BugsnagNotifier.m in Sources */, CBEC89302A4AC2920088A3CE /* BSGFilesystem.m in Sources */, + 965B94202F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */, 008968932486DA9600DC48C2 /* BugsnagError.m in Sources */, CBCF77A825010648004AF22A /* BSGJSONSerialization.m in Sources */, 0089687E2486DA9600DC48C2 /* BugsnagBreadcrumb.m in Sources */, @@ -3735,9 +3807,11 @@ 1C7ADF782EB1608F00A930A6 /* BugsnagCaptureOptions.m in Sources */, 00896A162486DAD100DC48C2 /* BSG_KSCrashSentry_Signal.c in Sources */, 09E312F52BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m in Sources */, + 963173C42F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */, 01468F5725876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, 968BFBDC2D0125CF00DCC24B /* BSGStoredFeatureFlag.m in Sources */, 008967C02486DA1900DC48C2 /* BugsnagClient.m in Sources */, + 96ECDA4A2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */, 008968972486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, 008968002486DA4500DC48C2 /* BSGSessionUploader.m in Sources */, 0089686D2486DA9500DC48C2 /* BugsnagEvent.m in Sources */, @@ -3923,8 +3997,10 @@ 008968CA2486DA9600DC48C2 /* BugsnagApp.m in Sources */, 008967C12486DA1900DC48C2 /* BugsnagClient.m in Sources */, CBEC89362A4AC7A90088A3CE /* BSGPersistentDeviceID.m in Sources */, + 96ECDA482F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */, 008968752486DA9500DC48C2 /* BugsnagDevice.m in Sources */, 968BFBD32D011BCB00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */, + 965B941D2F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */, 010FF28A25ED2A8D00E4F2B0 /* BSGAppHangDetector.m in Sources */, 00E636C224878D84006CBF1A /* BSG_RFC3339DateTool.m in Sources */, 0089687F2486DA9600DC48C2 /* BugsnagBreadcrumb.m in Sources */, @@ -3934,6 +4010,7 @@ 01468F5825876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, 008968832486DA9600DC48C2 /* BugsnagAppWithState.m in Sources */, 008968AA2486DA9600DC48C2 /* BugsnagSession.m in Sources */, + 963173C52F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */, 008968982486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, 008968B52486DA9600DC48C2 /* BugsnagDeviceWithState.m in Sources */, 00AD1F2A2486A17900A27979 /* BSGCrashSentry.m in Sources */, @@ -4001,6 +4078,7 @@ CBBDE918280068560070DCD3 /* BugsnagSystemState.m in Sources */, CBBDE934280068AD0070DCD3 /* BSGEventUploadKSCrashReportOperation.m in Sources */, CBEC89312A4AC2920088A3CE /* BSGFilesystem.m in Sources */, + 965B941E2F6AE0D800EAC583 /* BSGPluginRegistry.m in Sources */, CBBDE927280068AD0070DCD3 /* BSGEventUploadFileOperation.m in Sources */, CBBDE968280069210070DCD3 /* BugsnagDeviceWithState.m in Sources */, CBBDE946280068E60070DCD3 /* BugsnagCollections.m in Sources */, @@ -4035,9 +4113,11 @@ 1C7ADF792EB1608F00A930A6 /* BugsnagCaptureOptions.m in Sources */, CBBDE923280068970070DCD3 /* BugsnagEndpointConfiguration.m in Sources */, 09E312F62BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m in Sources */, + 963173C32F6AC2A3006643BA /* BugsnagMetricKitTypes.m in Sources */, CBBDE92E280068AD0070DCD3 /* BSGConnectivity.m in Sources */, 968BFBDD2D0125D000DCC24B /* BSGStoredFeatureFlag.m in Sources */, CBBDE912280068560070DCD3 /* BSGCrashSentry.m in Sources */, + 96ECDA4D2F7E639D00E2BF17 /* BugsnagCrossTalkAPI.mm in Sources */, CBBDE9BC280069B20070DCD3 /* BSG_Symbolicate.c in Sources */, CBBDE98E2800698F0070DCD3 /* BSG_KSCrashState.m in Sources */, CBBDE94A280068E60070DCD3 /* BSGUtils.m in Sources */, diff --git a/Bugsnag.xcworkspace/contents.xcworkspacedata b/Bugsnag.xcworkspace/contents.xcworkspacedata index 8d9acb90b..9647576f8 100644 --- a/Bugsnag.xcworkspace/contents.xcworkspacedata +++ b/Bugsnag.xcworkspace/contents.xcworkspacedata @@ -7,4 +7,7 @@ + + diff --git a/Bugsnag/Client/BugsnagClient+Private.h b/Bugsnag/Client/BugsnagClient+Private.h index b46cc2678..b42160e6c 100644 --- a/Bugsnag/Client/BugsnagClient+Private.h +++ b/Bugsnag/Client/BugsnagClient+Private.h @@ -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 *)stacktrace + timestamp:(NSDate * _Nullable)timestamp + block:(BugsnagOnErrorBlock _Nullable)block; + @end NS_ASSUME_NONNULL_END diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 7b483200e..8347f9a6a 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -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" @@ -262,6 +264,8 @@ - (void)start { // Map our bridged API early on. [BugsnagCocoaPerformanceFromBugsnagCocoa sharedInstance]; + + [BugsnagCrossTalkAPI initializeWithClient:self]; BSGCrashSentryInstall(self.configuration, BSSerializeDataCrashHandler); @@ -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]; @@ -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 *)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 { diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index 93a10fff5..9671b59c9 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -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; @@ -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]; @@ -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. diff --git a/Bugsnag/Configuration/BugsnagMetricKitTypes.m b/Bugsnag/Configuration/BugsnagMetricKitTypes.m new file mode 100644 index 000000000..0391d0713 --- /dev/null +++ b/Bugsnag/Configuration/BugsnagMetricKitTypes.m @@ -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 diff --git a/Bugsnag/Helpers/BSGDefines.h b/Bugsnag/Helpers/BSGDefines.h index b3934cf49..11ea34114 100644 --- a/Bugsnag/Helpers/BSGDefines.h +++ b/Bugsnag/Helpers/BSGDefines.h @@ -27,6 +27,7 @@ #define BSG_HAVE_SYSCALL (TARGET_OS_IOS || TARGET_OS_TV ) #define BSG_HAVE_UIDEVICE __has_include() #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) diff --git a/Bugsnag/Helpers/BSGPluginRegistry.h b/Bugsnag/Helpers/BSGPluginRegistry.h new file mode 100644 index 000000000..a0383f1c8 --- /dev/null +++ b/Bugsnag/Helpers/BSGPluginRegistry.h @@ -0,0 +1,33 @@ +// +// BSGPluginRegistry.h +// Bugsnag +// +// Created by Robert Bartoszewski on 17/03/2026. +// + +#import + +@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 diff --git a/Bugsnag/Helpers/BSGPluginRegistry.m b/Bugsnag/Helpers/BSGPluginRegistry.m new file mode 100644 index 000000000..33919b9ab --- /dev/null +++ b/Bugsnag/Helpers/BSGPluginRegistry.m @@ -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 diff --git a/Bugsnag/Helpers/BugsnagCrossTalkAPI.h b/Bugsnag/Helpers/BugsnagCrossTalkAPI.h new file mode 100644 index 000000000..eec24a491 --- /dev/null +++ b/Bugsnag/Helpers/BugsnagCrossTalkAPI.h @@ -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 + +@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 *)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 diff --git a/Bugsnag/Helpers/BugsnagCrossTalkAPI.mm b/Bugsnag/Helpers/BugsnagCrossTalkAPI.mm new file mode 100644 index 000000000..ef984ca2d --- /dev/null +++ b/Bugsnag/Helpers/BugsnagCrossTalkAPI.mm @@ -0,0 +1,300 @@ +// +// BugsnagCrossTalkAPI.mm +// Bugsnag +// +// Created by Robert Bartoszewski on 27/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import "BugsnagCrossTalkAPI.h" +#import "BugsnagClient.h" +#import "BugsnagClient+Private.h" +#import "BugsnagEvent.h" +#import "BugsnagStackframe.h" +#import "BugsnagLogger.h" +#import + + +@interface BugsnagCrossTalkAPI () + +@property(nonatomic, weak) BugsnagClient *client; + +@end + + +@implementation BugsnagCrossTalkAPI + ++ (void)initializeWithClient:(BugsnagClient *)client { + BugsnagCrossTalkAPI.sharedInstance.client = client; +} + +#pragma mark - Exposed API + +/** + * Create a plain event without automatic enrichment. + * API Version: V1 + * + * This is designed for external diagnostic systems (like MetricKit) that provide + * their own timestamp and don't need breadcrumbs, feature flags, etc. + */ +- (void)notifyPlainEventV1:(NSString *)errorClass + errorMessage:(NSString *)errorMessage + stacktrace:(NSArray *)stacktrace + timestamp:(NSDate * _Nullable)timestamp + block:(BOOL (^ _Nullable)(BugsnagEvent *event))block { + BugsnagClient *client = self.client; + if (client == nil) { + bsg_log_warn(@"CrossTalk: Cannot notify plain event - Bugsnag not initialized"); + return; + } + + [client notifyPlainEventWithErrorClass:errorClass + errorMessage:errorMessage + stacktrace:stacktrace + timestamp:timestamp + block:block]; +} + +#pragma mark - Internal Functionality + +static NSString *BSGUserInfoKeyIsSafeToCall = @"isSafeToCall"; +static NSString *BSGUserInfoKeyWillNOOP = @"willNOOP"; + +static bool classImplementsSelector(Class cls, SEL selector) { + bool selectorExists = false; + unsigned int methodCount = 0; + Method *methods = class_copyMethodList(cls, &methodCount); + for (unsigned int i = 0; i < methodCount; i++) { + if (method_getName(methods[i]) == selector) { + selectorExists = true; + break; + } + } + free(methods); + return selectorExists; +} + +/** + * Map a named API to a method with the specified selector. + * + * If an error occurs, the user info dictionary will contain the following NSNumber (boolean) fields: + * - "isSafeToCall": If @(YES), this method is safe to call (it has an implementation). Otherwise, calling it WILL throw a selector-not-found exception. + * - "willNOOP": If @(YES), calling the mapped method will no-op. + * + * Common scenarios: + * - The host library isn't linked in: isSafeToCall = YES, willNOOP = YES + * - apiName doesn't exist: isSafeToCall = YES, willNOOP = YES + * - toSelector already exists: isSafeToCall = YES, willNOOP = NO + * - Tried to map the same thing twice: isSafeToCall = YES, willNOOP = NO + * - Selector signature clash: isSafeToCall = NO, willNOOP = NO + */ ++ (NSError *)mapAPINamed:(NSString * _Nonnull)apiName toSelector:(SEL)toSelector { + NSError *err = nil; + // By default, we map to a "do nothing" implementation in case we don't find a real one. + SEL fromSelector = @selector(internal_doNothing); + + // apiName should map to an existing method in this API + SEL apiSelector = NSSelectorFromString(apiName); + if (classImplementsSelector(self.class, apiSelector)) { + fromSelector = apiSelector; + } else { + err = [NSError errorWithDomain:@"com.bugsnag.Bugsnag" + code:0 + userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat:@"No such API: %@", apiName], + BSGUserInfoKeyIsSafeToCall:@YES, + BSGUserInfoKeyWillNOOP:@YES + }]; + } + + Method method = class_getInstanceMethod(self.class, fromSelector); + if (method == nil) { + return [NSError errorWithDomain:@"com.bugsnag.Bugsnag" + code:0 + userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat: + @"class_getInstanceMethod (while mapping api %@): Failed to find instance method %@ in class %@", + apiName, + NSStringFromSelector(fromSelector), + self.class], + BSGUserInfoKeyIsSafeToCall:@NO, + BSGUserInfoKeyWillNOOP:@NO + }]; + } + + IMP imp = method_getImplementation(method); + if (imp == nil) { + return [NSError errorWithDomain:@"com.bugsnag.Bugsnag" + code:0 + userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat: + @"method_getImplementation (while mapping api %@): Failed to find implementation of instance method %@ in class %@", + apiName, + NSStringFromSelector(fromSelector), + self.class], + BSGUserInfoKeyIsSafeToCall:@NO, + BSGUserInfoKeyWillNOOP:@NO + }]; + } + + const char* encoding = method_getTypeEncoding(method); + if (encoding == nil) { + return [NSError errorWithDomain:@"com.bugsnag.Bugsnag" + code:0 + userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat: + @"method_getTypeEncoding (while mapping api %@): Failed to find signature of instance method %@ in class %@", + apiName, + NSStringFromSelector(fromSelector), + self.class], + BSGUserInfoKeyIsSafeToCall:@NO, + BSGUserInfoKeyWillNOOP:@NO + }]; + } + + // Don't add a method that already exists + if (classImplementsSelector(self.class, toSelector)) { + return [NSError errorWithDomain:@"com.bugsnag.Bugsnag" + code:0 + userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat: + @"class_addMethod (while mapping api %@): Instance method %@ already exists in class %@", + apiName, + NSStringFromSelector(fromSelector), + self.class], + BSGUserInfoKeyIsSafeToCall:@YES, + BSGUserInfoKeyWillNOOP:@NO + }]; + } + + if (!class_addMethod(self.class, toSelector, imp, encoding)) { + return [NSError errorWithDomain:@"com.bugsnag.Bugsnag" + code:0 + userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat: + @"class_addMethod (while mapping api %@): Failed to add instance method %@ to class %@", + apiName, + NSStringFromSelector(fromSelector), + self.class], + BSGUserInfoKeyIsSafeToCall:@NO, + BSGUserInfoKeyWillNOOP:@NO + }]; + } + + return err; +} + +- (void * _Nullable)internal_doNothing { + return NULL; +} + ++ (instancetype)sharedInstance { + static BugsnagCrossTalkAPI *sharedInstance; + static dispatch_once_t once; + dispatch_once(&once, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +@end + + +#pragma mark - BugsnagCrossTalkProxiedObject + +@interface BugsnagCrossTalkProxiedObject () + +@property(nonatomic,strong) id delegate; + +@end + +@implementation BugsnagCrossTalkProxiedObject + ++ (instancetype _Nullable) proxied:(id _Nullable)delegate { + if (delegate == nil) { + return nil; + } + + BugsnagCrossTalkProxiedObject *proxy = [BugsnagCrossTalkProxiedObject alloc]; + proxy.delegate = delegate; + return proxy; +} + +// Allow faster access to ivars in these special cases +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +- (void)forwardInvocation:(NSInvocation *)anInvocation { + if ([_delegate respondsToSelector:anInvocation.selector]) { + [anInvocation setTarget:_delegate]; + [anInvocation invoke]; + } +} + +-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector { + NSMethodSignature *sig = [_delegate methodSignatureForSelector:aSelector]; + if (sig) { + return sig; + } + + bsg_log_warn(@"CrossTalk: Tried to invoke unimplemented selector [%@] on proxied object %@", + NSStringFromSelector(aSelector), [_delegate debugDescription]); + + // Return a no-arg signature that's guaranteed to exist + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + +#pragma mark NSObject protocol (BugsnagCrossTalkProxiedObject) + +- (Class)class { + return [_delegate class]; +} + +- (Class)superclass { + return [_delegate superclass]; +} + +- (BOOL)isKindOfClass:(Class)aClass { + return [_delegate isKindOfClass:aClass]; +} + +- (BOOL)isMemberOfClass:(Class)aClass { + return [_delegate isMemberOfClass:aClass]; +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + // Be truthful about this + return [_delegate respondsToSelector:aSelector]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { + // Be truthful about this + return [_delegate conformsToProtocol:aProtocol]; +} + +- (BOOL)isEqual:(id)object { + return [_delegate isEqual:object]; +} + +- (NSUInteger)hash { + return [_delegate hash]; +} + +- (BOOL)isProxy { + return YES; +} + +- (NSString *)description { + if (_delegate) { + return [_delegate description]; + } + return super.description; +} + +- (NSString *)debugDescription { + if (_delegate) { + return [_delegate debugDescription]; + } + return super.debugDescription; +} + +@end diff --git a/Bugsnag/include/Bugsnag/BSGPlugin.h b/Bugsnag/include/Bugsnag/BSGPlugin.h new file mode 100644 index 000000000..d3128325e --- /dev/null +++ b/Bugsnag/include/Bugsnag/BSGPlugin.h @@ -0,0 +1,37 @@ +// +// BSGPlugin.h +// Bugsnag +// +// Created by Robert Bartoszewski on 17/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import + +@class BugsnagConfiguration; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Protocol for Bugsnag plugins that are automatically discovered and loaded at runtime. + * Plugins are discovered using NSClassFromString and initialized if present. + */ +@protocol BSGPlugin + +/** + * Installs the plugin and initializes any required resources. + * This method is automatically called by Bugsnag during startup if the plugin is linked. + */ ++ (void)install; + +/** + * Configures the plugin with the Bugsnag configuration. + * This method is automatically called by Bugsnag after install. + * + * @param configuration The Bugsnag configuration object + */ ++ (void)configure:(BugsnagConfiguration *)configuration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Bugsnag/include/Bugsnag/Bugsnag.h b/Bugsnag/include/Bugsnag/Bugsnag.h index 2f16cab5b..9020c7503 100644 --- a/Bugsnag/include/Bugsnag/Bugsnag.h +++ b/Bugsnag/include/Bugsnag/Bugsnag.h @@ -39,7 +39,9 @@ #import #import #import +#import #import +#import #import #import #import diff --git a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h index 9b5bb38c5..1136516d8 100644 --- a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h +++ b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h @@ -38,6 +38,7 @@ @class BugsnagUser; @class BugsnagEndpointConfiguration; @class BugsnagErrorTypes; +@class BugsnagMetricKitTypes; NS_ASSUME_NONNULL_BEGIN @@ -309,7 +310,7 @@ BUGSNAG_EXTERN * memory corruption that caused the crashing error in the first place. * * If it fails prior to termination, delivery will be reattempted at next launch - * (the default behavior). + * (the default behavior). * * Use of this feature is discouraged because it: * - may cause the app to hang while delivery occurs and impact the hang rate @@ -360,7 +361,7 @@ BUGSNAG_EXTERN /** * The maximum length of breadcrumb messages and metadata string values. - * + * * Values longer than this will be truncated prior to sending, after running any OnSendError blocks. * * The default value is 10000. @@ -379,6 +380,14 @@ BUGSNAG_EXTERN */ @property (strong, nonatomic) BugsnagErrorTypes *enabledErrorTypes; +/** + * A class defining which MetricKit diagnostics should be reported. By default, + * MetricKit is disabled (opt-in). When enabled, all diagnostic types are reported. + * + * Note: MetricKit is not available when using BugsnagStatic. Use the framework targets instead. + */ +@property (strong, nonatomic) BugsnagMetricKitTypes *enabledMetricKitDiagnostics; + /** * Set the endpoints to send data to. By default we'll send error reports to * https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can diff --git a/Bugsnag/include/Bugsnag/BugsnagMetricKitTypes.h b/Bugsnag/include/Bugsnag/BugsnagMetricKitTypes.h new file mode 100644 index 000000000..e9ac0683a --- /dev/null +++ b/Bugsnag/include/Bugsnag/BugsnagMetricKitTypes.h @@ -0,0 +1,61 @@ +// +// BugsnagMetricKitTypes.h +// Bugsnag +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import + +#import + +/** + * The types of MetricKit diagnostics that should be reported. + */ +BUGSNAG_EXTERN +@interface BugsnagMetricKitTypes : NSObject + +/** + * Determines whether MetricKit integration is enabled. + * + * This flag is false by default (opt-in). + */ +@property (nonatomic) BOOL enabled; + +/** + * Determines whether MetricKit crash diagnostics should be reported to Bugsnag. + * + * This flag is true by default when MetricKit is enabled. + */ +@property (nonatomic) BOOL crashDiagnostics; + +/** + * Determines whether MetricKit CPU exception diagnostics should be reported to Bugsnag. + * + * This flag is true by default when MetricKit is enabled. + */ +@property (nonatomic) BOOL cpuExceptionDiagnostics; + +/** + * Determines whether MetricKit app launch diagnostics should be reported to Bugsnag. + * + * This flag is true by default when MetricKit is enabled. + */ +@property (nonatomic) BOOL appLaunchDiagnostics; + +/** + * Determines whether MetricKit hang diagnostics should be reported to Bugsnag. + * + * This flag is true by default when MetricKit is enabled. + */ +@property (nonatomic) BOOL hangDiagnostics; + +/** + * Determines whether MetricKit disk write exception diagnostics should be reported to Bugsnag. + * + * This flag is true by default when MetricKit is enabled. + */ +@property (nonatomic) BOOL diskWriteExceptionDiagnostics; + +@end diff --git a/BugsnagMetricKitPlugin.podspec.json b/BugsnagMetricKitPlugin.podspec.json new file mode 100644 index 000000000..ec52e99d9 --- /dev/null +++ b/BugsnagMetricKitPlugin.podspec.json @@ -0,0 +1,31 @@ +{ + "name": "BugsnagMetricKitPlugin", + "version": "6.35.0", + "summary": "MetricKit integration for Bugsnag.", + "homepage": "https://bugsnag.com", + "license": "MIT", + "authors": { + "Bugsnag": "notifiers@bugsnag.com" + }, + "readme": "https://raw.githubusercontent.com/bugsnag/bugsnag-cocoa/v6.35.0/BugsnagMetricKitPlugin/README.md", + "source": { + "git": "https://github.com/bugsnag/bugsnag-cocoa.git", + "tag": "v6.35.0" + }, + "dependencies": { + "Bugsnag": "~> 6.13" + }, + "platforms": { + "ios": "13.0", + "osx": "12.0" + }, + "source_files": [ + "BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/{**/,}*.{m,h}" + ], + "requires_arc": true, + "prefix_header_file": false, + "public_header_files": [ + "BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/include/BugsnagMetricKitPlugin/*.h" + ], + "weak_frameworks": "MetricKit" +} diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/project.pbxproj b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/project.pbxproj new file mode 100644 index 000000000..d8746402c --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/project.pbxproj @@ -0,0 +1,531 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1FA1BFA4C1F8443380F203D0642B5767 /* BSGMetricKitStacktraceConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 801F28D04D044D0EAB236029BC770C78 /* BSGMetricKitStacktraceConverter.m */; }; + 2B05460D91EC4C65837ACB1CB1C7253100 /* MetricKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B05460D91EC4C65837ACB1CB1C72531 /* MetricKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 391F90E8E4704D299EA4357C72B7531A /* BSGDiagnosticsHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 679D0480C876409E806A2AD6E1634CF0 /* BSGDiagnosticsHandler.h */; }; + 5BBE891D3D3C4FA0BC188963D1EA955E /* BSGMetricKit.m in Sources */ = {isa = PBXBuildFile; fileRef = DB6FD299855D45CA9CA565F8D7E46451 /* BSGMetricKit.m */; }; + 7E505A60846144C5910C8A9747C4D7CB /* BSGMetricsHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D3BEE06D1E54CBCBA98FCBCA1B2EAB4 /* BSGMetricsHandler.m */; }; + 965B94162F6AD2E900EAC583 /* BugsnagMetricKitPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 965B94132F6AD2E900EAC583 /* BugsnagMetricKitPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 96ECDA512F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 96ECDA4F2F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.h */; }; + 96ECDA522F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 96ECDA502F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.m */; }; + A95770C54C5746C19A427002DE8F2477 /* BSGMetricsHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7C177E93054B6BBDF42D3167477B6D /* BSGMetricsHandler.h */; }; + B0675CC35C7C4F759F43D164C4B436CC /* BSGMetricKitStacktraceConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3056DF912C51424FB551C2640A392895 /* BSGMetricKitStacktraceConverter.h */; }; + C85E8D61CABB4625A414CF1BD3D5ADD9 /* BSGDiagnosticsHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E8AC37A87CD46A684C91A329477B0F6 /* BSGDiagnosticsHandler.m */; }; + D9CC8C141E0C409CBD992B6D6C8C246D /* BSGMetricKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B32C4426A654CE3B6039DFEF4B8C6E4 /* BSGMetricKit.h */; }; + EA0EBE7F086B4D249FE75C8735CCE446 /* BugsnagMetricKitPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = BB851FCEF4E248629C285E3622D43AC4 /* BugsnagMetricKitPlugin.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AB727F3F29FF4BA497F8FD784ACAFDEC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E45A8210A3D64B7982DC73FB5B51F738 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E0ADDC1218BD42DE8162E70954E85C3A; + remoteInfo = "BugsnagMetricKitPlugin-iOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0B32C4426A654CE3B6039DFEF4B8C6E4 /* BSGMetricKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGMetricKit.h; sourceTree = ""; }; + 2B05460D91EC4C65837ACB1CB1C72531 /* MetricKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetricKit.framework; path = System/Library/Frameworks/MetricKit.framework; sourceTree = SDKROOT; }; + 3056DF912C51424FB551C2640A392895 /* BSGMetricKitStacktraceConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGMetricKitStacktraceConverter.h; sourceTree = ""; }; + 39D7C6D57E604758BA7D2113805549E2 /* BugsnagMetricKitPluginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BugsnagMetricKitPluginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 679D0480C876409E806A2AD6E1634CF0 /* BSGDiagnosticsHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGDiagnosticsHandler.h; sourceTree = ""; }; + 801F28D04D044D0EAB236029BC770C78 /* BSGMetricKitStacktraceConverter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGMetricKitStacktraceConverter.m; sourceTree = ""; }; + 965B94132F6AD2E900EAC583 /* BugsnagMetricKitPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagMetricKitPlugin.h; sourceTree = ""; }; + 96ECDA4F2F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagFromBugsnagMetricKitPlugin.h; sourceTree = ""; }; + 96ECDA502F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagFromBugsnagMetricKitPlugin.m; sourceTree = ""; }; + 9D3BEE06D1E54CBCBA98FCBCA1B2EAB4 /* BSGMetricsHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGMetricsHandler.m; sourceTree = ""; }; + 9E8AC37A87CD46A684C91A329477B0F6 /* BSGDiagnosticsHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGDiagnosticsHandler.m; sourceTree = ""; }; + ABEB2D2D732E468EAACE38B206866DCD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BB851FCEF4E248629C285E3622D43AC4 /* BugsnagMetricKitPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagMetricKitPlugin.m; sourceTree = ""; }; + C794D497694547FFBC5C818407AAE8A8 /* Bugsnag.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Bugsnag.framework; path = "../build/Build/Products/Debug-iphonesimulator/Bugsnag.framework"; sourceTree = ""; }; + DB6FD299855D45CA9CA565F8D7E46451 /* BSGMetricKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGMetricKit.m; sourceTree = ""; }; + EE7C177E93054B6BBDF42D3167477B6D /* BSGMetricsHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGMetricsHandler.h; sourceTree = ""; }; + FF09D0743F6C4F73A14E37B7B703D7D3 /* BugsnagMetricKitPlugin_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BugsnagMetricKitPlugin_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 459F9291636947A1A1765B57681DCCD7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 539FB0C8D59A41449976FFCC8586DC49 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B05460D91EC4C65837ACB1CB1C7253100 /* MetricKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 02C32F0A9A874FB6B366BCCEC6265984 /* Metrics */ = { + isa = PBXGroup; + children = ( + EE7C177E93054B6BBDF42D3167477B6D /* BSGMetricsHandler.h */, + 9D3BEE06D1E54CBCBA98FCBCA1B2EAB4 /* BSGMetricsHandler.m */, + ); + path = Metrics; + sourceTree = ""; + }; + 23983348BBA84FCBA19B3808E841E030 /* Products */ = { + isa = PBXGroup; + children = ( + FF09D0743F6C4F73A14E37B7B703D7D3 /* BugsnagMetricKitPlugin_iOS.framework */, + 39D7C6D57E604758BA7D2113805549E2 /* BugsnagMetricKitPluginTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 2E67051625F644D2BDD84E1477FA235E = { + isa = PBXGroup; + children = ( + CBD72E749E8444B7B66A6D89CE2841EA /* BugsnagMetricKitPlugin */, + 23983348BBA84FCBA19B3808E841E030 /* Products */, + BC4F2F88636540A2B4D2F0DD64D84CD7 /* Frameworks */, + ); + sourceTree = ""; + }; + 965B94142F6AD2E900EAC583 /* BugsnagMetricKitPlugin */ = { + isa = PBXGroup; + children = ( + 965B94132F6AD2E900EAC583 /* BugsnagMetricKitPlugin.h */, + ); + path = BugsnagMetricKitPlugin; + sourceTree = ""; + }; + 965B94152F6AD2E900EAC583 /* include */ = { + isa = PBXGroup; + children = ( + 965B94142F6AD2E900EAC583 /* BugsnagMetricKitPlugin */, + ); + path = include; + sourceTree = ""; + }; + BC4F2F88636540A2B4D2F0DD64D84CD7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C794D497694547FFBC5C818407AAE8A8 /* Bugsnag.framework */, + 2B05460D91EC4C65837ACB1CB1C72531 /* MetricKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + CB052CB66C21489FADB4C8D78D580DA8 /* Diagnostics */ = { + isa = PBXGroup; + children = ( + 679D0480C876409E806A2AD6E1634CF0 /* BSGDiagnosticsHandler.h */, + 9E8AC37A87CD46A684C91A329477B0F6 /* BSGDiagnosticsHandler.m */, + ); + path = Diagnostics; + sourceTree = ""; + }; + CBD72E749E8444B7B66A6D89CE2841EA /* BugsnagMetricKitPlugin */ = { + isa = PBXGroup; + children = ( + 0B32C4426A654CE3B6039DFEF4B8C6E4 /* BSGMetricKit.h */, + DB6FD299855D45CA9CA565F8D7E46451 /* BSGMetricKit.m */, + 3056DF912C51424FB551C2640A392895 /* BSGMetricKitStacktraceConverter.h */, + 801F28D04D044D0EAB236029BC770C78 /* BSGMetricKitStacktraceConverter.m */, + 96ECDA4F2F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.h */, + 96ECDA502F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.m */, + BB851FCEF4E248629C285E3622D43AC4 /* BugsnagMetricKitPlugin.m */, + CB052CB66C21489FADB4C8D78D580DA8 /* Diagnostics */, + 965B94152F6AD2E900EAC583 /* include */, + ABEB2D2D732E468EAACE38B206866DCD /* Info.plist */, + 02C32F0A9A874FB6B366BCCEC6265984 /* Metrics */, + ); + path = BugsnagMetricKitPlugin; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 2E84680EFFFC4A14AFF1754C78A1F0F9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 96ECDA512F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.h in Headers */, + D9CC8C141E0C409CBD992B6D6C8C246D /* BSGMetricKit.h in Headers */, + B0675CC35C7C4F759F43D164C4B436CC /* BSGMetricKitStacktraceConverter.h in Headers */, + 391F90E8E4704D299EA4357C72B7531A /* BSGDiagnosticsHandler.h in Headers */, + 965B94162F6AD2E900EAC583 /* BugsnagMetricKitPlugin.h in Headers */, + A95770C54C5746C19A427002DE8F2477 /* BSGMetricsHandler.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + E0ADDC1218BD42DE8162E70954E85C3A /* BugsnagMetricKitPlugin-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = A0D51A64B768403FABDE3D0DF4CD360B /* Build configuration list for PBXNativeTarget "BugsnagMetricKitPlugin-iOS" */; + buildPhases = ( + 2E84680EFFFC4A14AFF1754C78A1F0F9 /* Headers */, + 79204AD5EDD64228BEBBD77571C7D312 /* Sources */, + 539FB0C8D59A41449976FFCC8586DC49 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "BugsnagMetricKitPlugin-iOS"; + productName = BugsnagMetricKitPlugin; + productReference = FF09D0743F6C4F73A14E37B7B703D7D3 /* BugsnagMetricKitPlugin_iOS.framework */; + productType = "com.apple.product-type.framework"; + }; + F47A1D7D2CD24A3B9405E4677CF84D50 /* BugsnagMetricKitPluginTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FC128405BFE44FEDB9C1194C91E4FD17 /* Build configuration list for PBXNativeTarget "BugsnagMetricKitPluginTests" */; + buildPhases = ( + 1CC48551F41F4AB3A97A974A9DDCB144 /* Sources */, + 459F9291636947A1A1765B57681DCCD7 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 557F1BC526254DC4AB19849445A53DB0 /* PBXTargetDependency */, + ); + name = BugsnagMetricKitPluginTests; + productName = BugsnagMetricKitPluginTests; + productReference = 39D7C6D57E604758BA7D2113805549E2 /* BugsnagMetricKitPluginTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E45A8210A3D64B7982DC73FB5B51F738 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1500; + TargetAttributes = { + E0ADDC1218BD42DE8162E70954E85C3A = { + CreatedOnToolsVersion = 15.0; + }; + F47A1D7D2CD24A3B9405E4677CF84D50 = { + CreatedOnToolsVersion = 15.0; + TestTargetID = E0ADDC1218BD42DE8162E70954E85C3A; + }; + }; + }; + buildConfigurationList = F5A2035A089B437B929F7DCA95A5FF7D /* Build configuration list for PBXProject "BugsnagMetricKitPlugin" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2E67051625F644D2BDD84E1477FA235E; + productRefGroup = 23983348BBA84FCBA19B3808E841E030 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E0ADDC1218BD42DE8162E70954E85C3A /* BugsnagMetricKitPlugin-iOS */, + F47A1D7D2CD24A3B9405E4677CF84D50 /* BugsnagMetricKitPluginTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 1CC48551F41F4AB3A97A974A9DDCB144 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 79204AD5EDD64228BEBBD77571C7D312 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EA0EBE7F086B4D249FE75C8735CCE446 /* BugsnagMetricKitPlugin.m in Sources */, + 5BBE891D3D3C4FA0BC188963D1EA955E /* BSGMetricKit.m in Sources */, + 1FA1BFA4C1F8443380F203D0642B5767 /* BSGMetricKitStacktraceConverter.m in Sources */, + 96ECDA522F7E8F4D00E2BF17 /* BugsnagFromBugsnagMetricKitPlugin.m in Sources */, + C85E8D61CABB4625A414CF1BD3D5ADD9 /* BSGDiagnosticsHandler.m in Sources */, + 7E505A60846144C5910C8A9747C4D7CB /* BSGMetricsHandler.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 557F1BC526254DC4AB19849445A53DB0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E0ADDC1218BD42DE8162E70954E85C3A /* BugsnagMetricKitPlugin-iOS */; + targetProxy = AB727F3F29FF4BA497F8FD784ACAFDEC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + C67B09CAB6774D7F88073E747435EB88 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + C67B09CAB6774D7F88073E747435EB8801 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/../build/Build/Products/Release-iphonesimulator", + ); + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = BugsnagMetricKitPlugin/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "-weak_framework", + MetricKit, + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bugsnag.BugsnagMetricKitPlugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + C67B09CAB6774D7F88073E747435EB8802 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = BugsnagMetricKitPluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.bugsnag.BugsnagMetricKitPluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + C82E4945A6CD4C49815887AFA1890EF8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + C82E4945A6CD4C49815887AFA1890EF801 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/../build/Build/Products/Debug-iphonesimulator", + ); + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = BugsnagMetricKitPlugin/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "-weak_framework", + MetricKit, + ); + PRODUCT_BUNDLE_IDENTIFIER = com.bugsnag.BugsnagMetricKitPlugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C82E4945A6CD4C49815887AFA1890EF802 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = BugsnagMetricKitPluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.bugsnag.BugsnagMetricKitPluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A0D51A64B768403FABDE3D0DF4CD360B /* Build configuration list for PBXNativeTarget "BugsnagMetricKitPlugin-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C82E4945A6CD4C49815887AFA1890EF801 /* Debug */, + C67B09CAB6774D7F88073E747435EB8801 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F5A2035A089B437B929F7DCA95A5FF7D /* Build configuration list for PBXProject "BugsnagMetricKitPlugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C82E4945A6CD4C49815887AFA1890EF8 /* Debug */, + C67B09CAB6774D7F88073E747435EB88 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FC128405BFE44FEDB9C1194C91E4FD17 /* Build configuration list for PBXNativeTarget "BugsnagMetricKitPluginTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C82E4945A6CD4C49815887AFA1890EF802 /* Debug */, + C67B09CAB6774D7F88073E747435EB8802 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E45A8210A3D64B7982DC73FB5B51F738 /* Project object */; +} diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/xcshareddata/xcschemes/BugsnagMetricKitPlugin-iOS.xcscheme b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/xcshareddata/xcschemes/BugsnagMetricKitPlugin-iOS.xcscheme new file mode 100644 index 000000000..4ea788997 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.xcodeproj/xcshareddata/xcschemes/BugsnagMetricKitPlugin-iOS.xcscheme @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKit.h b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKit.h new file mode 100644 index 000000000..5a042f3f3 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKit.h @@ -0,0 +1,29 @@ +// +// BSGMetricKit.h +// Bugsnag +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import + +#if __has_include() + +NS_ASSUME_NONNULL_BEGIN + +@interface BSGMetricKit: NSObject + ++ (instancetype)sharedInstance; + +- (void)configure:(id)configuration; + +- (void)installMetricKit; + +- (void)uninstallMetricKit; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKit.m b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKit.m new file mode 100644 index 000000000..de61ddf59 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKit.m @@ -0,0 +1,89 @@ +// +// BSGMetricKit.m +// Bugsnag +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import "BSGMetricKit.h" + +#if __has_include() + +#import "Metrics/BSGMetricsHandler.h" +#import "Diagnostics/BSGDiagnosticsHandler.h" +#import + +@class BugsnagConfiguration; + +@interface BSGMetricKit () +@property (nonatomic, strong) BSGMetricsHandler *metricsHandler; +@property (nonatomic, strong) BSGDiagnosticsHandler *diagnosticsHandler; +@property (nonatomic, assign) BOOL isInstalled; +@end + +@implementation BSGMetricKit + ++ (instancetype)sharedInstance { + static BSGMetricKit *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +- (instancetype)init { + if ((self = [super init])) { + _metricsHandler = [[BSGMetricsHandler alloc] init]; + _diagnosticsHandler = [[BSGDiagnosticsHandler alloc] init]; + _isInstalled = NO; + } + return self; +} + +- (void)configure:(id)configuration { + [self.diagnosticsHandler configure:configuration]; +} + +- (void)installMetricKit { + if (self.isInstalled) { + return; + } + + if (@available(iOS 13.0, macOS 12.0, *)) { + MXMetricManager *metricManager = [MXMetricManager sharedManager]; + [metricManager addSubscriber:self]; + self.isInstalled = YES; + } +} + +- (void)uninstallMetricKit { + if (!self.isInstalled) { + return; + } + + if (@available(iOS 13.0, macOS 12.0, *)) { + MXMetricManager *metricManager = [MXMetricManager sharedManager]; + [metricManager removeSubscriber:self]; + self.isInstalled = NO; + } +} + +#pragma mark - MXMetricManagerSubscriber + +- (void)didReceiveMetricPayloads:(NSArray *)payloads API_AVAILABLE(ios(13.0), macosx(12.0)) { + for (MXMetricPayload *payload in payloads) { + [self.metricsHandler handleMetricsPayload:payload]; + } +} + +- (void)didReceiveDiagnosticPayloads:(NSArray *)payloads API_AVAILABLE(ios(14.0), macosx(12.0)) { + for (MXDiagnosticPayload *payload in payloads) { + [self.diagnosticsHandler handleDiagnosticsPayload:payload]; + } +} + +@end + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKitStacktraceConverter.h b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKitStacktraceConverter.h new file mode 100644 index 000000000..a59180425 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKitStacktraceConverter.h @@ -0,0 +1,33 @@ +// +// BSGMetricKitStacktraceConverter.h +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import + +#if __has_include() + +#import + +@class BugsnagStackframe; + +NS_ASSUME_NONNULL_BEGIN + +@interface BSGMetricKitStacktraceConverter : NSObject + +/** + * Converts a MetricKit MXCallStackTree into an array of BugsnagStackframe objects. + * + * @param callStackTree The MetricKit call stack tree to convert + * @return An array of BugsnagStackframe objects representing the stack trace + */ ++ (NSArray *)stackframesFromCallStackTree:(MXCallStackTree *)callStackTree API_AVAILABLE(ios(14.0), macosx(12.0)); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKitStacktraceConverter.m b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKitStacktraceConverter.m new file mode 100644 index 000000000..521d98f1e --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BSGMetricKitStacktraceConverter.m @@ -0,0 +1,128 @@ +// +// BSGMetricKitStacktraceConverter.m +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import "BSGMetricKitStacktraceConverter.h" + +#if __has_include() + +#import "BugsnagFromBugsnagMetricKitPlugin.h" +#import + +@implementation BSGMetricKitStacktraceConverter + ++ (NSArray *)stackframesFromCallStackTree:(MXCallStackTree *)callStackTree API_AVAILABLE(ios(14.0), macosx(12.0)) { + if (!callStackTree) { + return @[]; + } + + NSMutableArray *frames = [NSMutableArray array]; + + if (@available(iOS 14.0, macOS 12.0, *)) { + // Parse the JSON representation to extract frame information + NSData *jsonData = callStackTree.JSONRepresentation; + if (jsonData) { + NSError *error = nil; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + if (json && !error) { + [self extractFramesFromJSON:json intoArray:frames]; + + } + } + } + + return [frames copy]; +} + ++ (void)extractFramesFromJSON:(NSDictionary *)json intoArray:(NSMutableArray *)frames { + // MetricKit JSON structure has call stacks with frames + // Example structure: { "callStacks": [{ "threadAttributed": true, "callStackRootFrames": [...] }] } + + NSArray *callStacks = json[@"callStacks"]; + if ([callStacks isKindOfClass:[NSArray class]]) { + for (NSDictionary *callStack in callStacks) { + if ([callStack isKindOfClass:[NSDictionary class]]) { + NSArray *rootFrames = callStack[@"callStackRootFrames"]; + if ([rootFrames isKindOfClass:[NSArray class]]) { + [self extractFramesFromRootFrames:rootFrames intoArray:frames]; + } + } + } + } +} + ++ (void)extractFramesFromRootFrames:(NSArray *)rootFrames intoArray:(NSMutableArray *)frames { + for (NSDictionary *frameDict in rootFrames) { + if (![frameDict isKindOfClass:[NSDictionary class]]) { + continue; + } + + BugsnagStackframe *frame = [self stackframeFromDictionary:frameDict]; + if (frame) { + [frames addObject:frame]; + } + + // Recursively process sub-frames + NSArray *subFrames = frameDict[@"subFrames"]; + if ([subFrames isKindOfClass:[NSArray class]]) { + [self extractFramesFromRootFrames:subFrames intoArray:frames]; + } + } +} + ++ (BugsnagStackframe *)stackframeFromDictionary:(NSDictionary *)frameDict { + if (!frameDict || ![frameDict isKindOfClass:[NSDictionary class]]) { + return nil; + } + + // Dynamically load BugsnagStackframe class at runtime + Class BugsnagStackframeClass = NSClassFromString(@"BugsnagStackframe"); + if (!BugsnagStackframeClass) { + return nil; + } + + id frame = [[BugsnagStackframeClass alloc] init]; + + // Extract address + NSNumber *address = frameDict[@"address"]; + if ([address isKindOfClass:[NSNumber class]]) { + [frame setValue:address forKey:@"frameAddress"]; + } + + // Extract binary name + NSString *binaryName = frameDict[@"binaryName"]; + if ([binaryName isKindOfClass:[NSString class]]) { + [frame setValue:binaryName forKey:@"machoFile"]; + } + + // Extract binary UUID + NSString *binaryUUID = frameDict[@"binaryUUID"]; + if ([binaryUUID isKindOfClass:[NSString class]]) { + [frame setValue:binaryUUID forKey:@"machoUuid"]; + } + + // Extract offset into binary text segment + NSNumber *offsetIntoBinaryTextSegment = frameDict[@"offsetIntoBinaryTextSegment"]; + if ([offsetIntoBinaryTextSegment isKindOfClass:[NSNumber class]] && address) { + NSNumber *symbolAddress = @([address unsignedLongLongValue] - [offsetIntoBinaryTextSegment unsignedLongLongValue]); + [frame setValue:symbolAddress forKey:@"symbolAddress"]; + } + + if ([binaryName isKindOfClass:[NSString class]] && [offsetIntoBinaryTextSegment isKindOfClass:[NSNumber class]]) { + NSString *method = [NSString stringWithFormat:@"%@ + %llu", binaryName, [offsetIntoBinaryTextSegment unsignedLongLongValue]]; + [frame setValue:method forKey:@"method"]; + } + + // Mark MetricKit frames as cocoa type + [frame setValue:@"cocoa" forKey:@"type"]; + + return frame; +} + +@end + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagFromBugsnagMetricKitPlugin.h b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagFromBugsnagMetricKitPlugin.h new file mode 100644 index 000000000..1b10ae4f2 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagFromBugsnagMetricKitPlugin.h @@ -0,0 +1,57 @@ +// +// BugsnagFromBugsnagMetricKitPlugin.h +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 27/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import + +@class BugsnagEvent; +@class BugsnagStackframe; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Bridge from BugsnagMetricKitPlugin to Bugsnag. + * + * IMPORTANT: This class name MUST be globally unique across ALL Bugsnag libraries that contain native code! + * When cloning this code as a template for your own bridge, always use the naming style "TargetLibraryFromSourceLibrary" + * For example: + * * "BugsnagCocoaPerformanceFromBugsnagUnity" (bridge from Bugsnag Unity to Bugsnag Cocoa Performance) + * * "BugsnagFromBugsnagMetricKitPlugin" (bridge from BugsnagMetricKitPlugin to Bugsnag) + */ +@interface BugsnagFromBugsnagMetricKitPlugin : NSObject + +#pragma mark - Methods that will be bridged to Bugsnag + +/** + * Symbolicate an array of stack frames. + * + * @param stackframes Array of BugsnagStackframe objects to symbolicate + */ +- (void)symbolicateStackframes:(NSArray *)stackframes; + +/** + * Create and notify a plain event without automatic enrichment. + * + * @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)notifyPlainEvent:(NSString *)errorClass + errorMessage:(NSString *)errorMessage + stacktrace:(NSArray *)stacktrace + timestamp:(NSDate * _Nullable)timestamp + block:(BOOL (^ _Nullable)(BugsnagEvent *event))block; + +#pragma mark - Shared Instance + ++ (instancetype _Nullable)sharedInstance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagFromBugsnagMetricKitPlugin.m b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagFromBugsnagMetricKitPlugin.m new file mode 100644 index 000000000..d41b391e8 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagFromBugsnagMetricKitPlugin.m @@ -0,0 +1,72 @@ +// +// BugsnagFromBugsnagMetricKitPlugin.m +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 27/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import "BugsnagFromBugsnagMetricKitPlugin.h" + +// Bridged API methods won't have implementations until we connect them at runtime. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincomplete-implementation" +@implementation BugsnagFromBugsnagMetricKitPlugin + +static NSString *BSGUserInfoKeyIsSafeToCall = @"isSafeToCall"; +static NSString *BSGUserInfoKeyWillNOOP = @"willNOOP"; + +static id bugsnagCrossTalkAPI = nil; + ++ (void)initialize { + // Look for a cross-talk API + Class cls = NSClassFromString(@"BugsnagCrossTalkAPI"); + if (cls != nil) { + NSError *err = nil; + + // Map the methods we want to use, with the API versions we expect + if ((err = [cls mapAPINamed:@"symbolicateStackframesV1:" + toSelector:@selector(symbolicateStackframes:)]) != nil) { + NSLog(@"Failed to map Bugsnag API symbolicateStackframesV1:: %@", err); + NSNumber *isSafeToCall = err.userInfo[BSGUserInfoKeyIsSafeToCall]; + if (![isSafeToCall boolValue]) { + // Must abort because this method is not mapped, so we'd crash if we tried to call it. + return; + } + } + + if ((err = [cls mapAPINamed:@"notifyPlainEventV1:errorMessage:stacktrace:timestamp:block:" + toSelector:@selector(notifyPlainEvent:errorMessage:stacktrace:timestamp:block:)]) != nil) { + NSLog(@"Failed to map Bugsnag API notifyPlainEventV1:errorMessage:stacktrace:timestamp:block:: %@", err); + NSNumber *isSafeToCall = err.userInfo[BSGUserInfoKeyIsSafeToCall]; + if (![isSafeToCall boolValue]) { + // Must abort because this method is not mapped, so we'd crash if we tried to call it. + return; + } + } + + // Our "sharedInstance" will actually be the cross-talk API whose class we loaded. + bugsnagCrossTalkAPI = [cls sharedInstance]; + } +} + ++ (instancetype _Nullable)sharedInstance { + // We're either going to return the API object we found in +initialize, or nil. + return bugsnagCrossTalkAPI; +} + +/** + * Map a named API to a method with the specified selector. + * If an error occurs, the user info dictionary of the error will contain a field "isSafeToCall". + * If "isSafeToCall" is "YES", then the selector has been mapped to a null implementation (does nothing, returns nil). + * If "isSafeToCall" is "NO", then no mapping has occurred, and the method doesn't exist (calling it will result in no such selector). + */ ++ (NSError *)mapAPINamed:(NSString * _Nonnull __unused)apiName toSelector:(SEL __unused)toSelector { + // This exists only to make the mapAPINamed selector available on our side + // so that we can call it on the API class we found (see +initialize). + // This implementation is never actually called. + return nil; +} + +@end +#pragma clang diagnostic pop diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.m b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.m new file mode 100644 index 000000000..f6f5755d1 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.m @@ -0,0 +1,58 @@ +// +// BugsnagMetricKitPlugin.m +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 17/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import "BugsnagMetricKitPlugin.h" +#import "BSGMetricKit.h" + +#if __has_include() +#import + +static BugsnagConfiguration *pluginConfiguration = nil; + +@implementation BugsnagMetricKitPlugin + ++ (void)install { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Check if MetricKit is available on this platform + if (@available(iOS 13.0, macOS 12.0, *)) { + // Install MetricKit subscriber + [[BSGMetricKit sharedInstance] installMetricKit]; + } + }); +} + ++ (void)configure:(BugsnagConfiguration *)configuration { + pluginConfiguration = configuration; + + // Pass configuration to BSGMetricKit which will pass it to the diagnostics handler + [[BSGMetricKit sharedInstance] configure:configuration]; +} + ++ (BugsnagConfiguration *)configuration { + return pluginConfiguration; +} + +@end + +#else + +// Stub implementation when MetricKit is not available +@implementation BugsnagMetricKitPlugin + ++ (void)install { + // No-op if MetricKit not available +} + ++ (void)configure:(BugsnagConfiguration *)configuration { + // No-op if MetricKit not available +} + +@end + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Diagnostics/BSGDiagnosticsHandler.h b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Diagnostics/BSGDiagnosticsHandler.h new file mode 100644 index 000000000..29c17469e --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Diagnostics/BSGDiagnosticsHandler.h @@ -0,0 +1,27 @@ +// +// BSGDiagnosticsHandler.h +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import + +#if __has_include() + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BSGDiagnosticsHandler: NSObject + +- (void)configure:(id)configuration; + +- (void)handleDiagnosticsPayload:(MXDiagnosticPayload *)payload API_AVAILABLE(ios(14.0), macosx(12.0)); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Diagnostics/BSGDiagnosticsHandler.m b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Diagnostics/BSGDiagnosticsHandler.m new file mode 100644 index 000000000..a30adec20 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Diagnostics/BSGDiagnosticsHandler.m @@ -0,0 +1,378 @@ +// +// BSGDiagnosticsHandler.m +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import "BSGDiagnosticsHandler.h" + +#if __has_include() + +#import "BSGMetricKitStacktraceConverter.h" +#import "../BugsnagFromBugsnagMetricKitPlugin.h" +#import +#import +#import + +#pragma mark - Exception Type / Signal Mapping + +static NSString *BSGExceptionTypeName(NSNumber * _Nullable exceptionType) { + if (!exceptionType) return nil; + switch (exceptionType.integerValue) { + case EXC_BAD_ACCESS: return @"EXC_BAD_ACCESS"; + case EXC_BAD_INSTRUCTION: return @"EXC_BAD_INSTRUCTION"; + case EXC_ARITHMETIC: return @"EXC_ARITHMETIC"; + case EXC_EMULATION: return @"EXC_EMULATION"; + case EXC_SOFTWARE: return @"EXC_SOFTWARE"; + case EXC_BREAKPOINT: return @"EXC_BREAKPOINT"; + case EXC_SYSCALL: return @"EXC_SYSCALL"; + case EXC_MACH_SYSCALL: return @"EXC_MACH_SYSCALL"; + case EXC_RPC_ALERT: return @"EXC_RPC_ALERT"; + case EXC_CRASH: return @"EXC_CRASH"; + case EXC_RESOURCE: return @"EXC_RESOURCE"; + case EXC_GUARD: return @"EXC_GUARD"; + case EXC_CORPSE_NOTIFY: return @"EXC_CORPSE_NOTIFY"; + default: return [NSString stringWithFormat:@"EXC_%@", exceptionType]; + } +} + +static NSString *BSGSignalName(NSNumber * _Nullable signal) { + if (!signal) return nil; + switch (signal.integerValue) { + case SIGHUP: return @"SIGHUP"; + case SIGINT: return @"SIGINT"; + case SIGQUIT: return @"SIGQUIT"; + case SIGILL: return @"SIGILL"; + case SIGTRAP: return @"SIGTRAP"; + case SIGABRT: return @"SIGABRT"; + case SIGEMT: return @"SIGEMT"; + case SIGFPE: return @"SIGFPE"; + case SIGKILL: return @"SIGKILL"; + case SIGBUS: return @"SIGBUS"; + case SIGSEGV: return @"SIGSEGV"; + case SIGSYS: return @"SIGSYS"; + case SIGPIPE: return @"SIGPIPE"; + case SIGALRM: return @"SIGALRM"; + case SIGTERM: return @"SIGTERM"; + default: return [NSString stringWithFormat:@"SIG_%@", signal]; + } +} + +#pragma mark - Metadata helper + +/** + * Calls [event addMetadata:value withKey:key toSection:section] via NSInvocation + * to avoid compile-time dependency on Bugsnag headers. + */ +static void BSGAddMetadata(id event, NSString *section, NSString *key, id _Nullable value) { + if (!value) return; + SEL sel = NSSelectorFromString(@"addMetadata:withKey:toSection:"); + if (![event respondsToSelector:sel]) return; + NSMethodSignature *sig = [event methodSignatureForSelector:sel]; + NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig]; + inv.selector = sel; + inv.target = event; + [inv setArgument:&value atIndex:2]; + [inv setArgument:&key atIndex:3]; + [inv setArgument:§ion atIndex:4]; + [inv invoke]; +} + +/** + * Extracts the diagnosticMetaData dictionary from a diagnostic's JSON representation. + */ +static NSDictionary * _Nullable BSGDiagnosticMetaData(MXDiagnostic *diagnostic) API_AVAILABLE(ios(14.0), macosx(12.0)) { + NSData *jsonData = diagnostic.JSONRepresentation; + if (!jsonData) return nil; + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil]; + if (![json isKindOfClass:[NSDictionary class]]) return nil; + NSDictionary *meta = json[@"diagnosticMetaData"]; + return [meta isKindOfClass:[NSDictionary class]] ? meta : nil; +} + +#pragma mark - Measurement helpers + +static double BSGSecondsFromDuration(NSMeasurement *measurement) { + return [measurement measurementByConvertingToUnit:NSUnitDuration.seconds].doubleValue; +} + +static double BSGBytesFromStorage(NSMeasurement *measurement) { + return [measurement measurementByConvertingToUnit:NSUnitInformationStorage.bytes].doubleValue; +} + +#pragma mark - BSGDiagnosticsHandler + +@interface BSGDiagnosticsHandler () +@property (nonatomic, strong) id configuration; +@end + +@implementation BSGDiagnosticsHandler + +- (void)configure:(id)configuration { + self.configuration = configuration; +} + +- (void)handleDiagnosticsPayload:(MXDiagnosticPayload *)payload API_AVAILABLE(ios(14.0), macosx(12.0)) { + if (!payload || !self.configuration) { + return; + } + +#if !TARGET_OS_WATCH && !TARGET_OS_TV + id metricKitTypes = [self.configuration valueForKey:@"enabledMetricKitDiagnostics"]; + if (!metricKitTypes || ![[metricKitTypes valueForKey:@"enabled"] boolValue]) { + return; + } + + if ([[metricKitTypes valueForKey:@"crashDiagnostics"] boolValue] && payload.crashDiagnostics) { + for (MXCrashDiagnostic *diagnostic in payload.crashDiagnostics) { + [self handleCrashDiagnostic:diagnostic timestamp:payload.timeStampEnd]; + } + } + + if ([[metricKitTypes valueForKey:@"cpuExceptionDiagnostics"] boolValue] && payload.cpuExceptionDiagnostics) { + for (MXCPUExceptionDiagnostic *diagnostic in payload.cpuExceptionDiagnostics) { + [self handleCPUExceptionDiagnostic:diagnostic timestamp:payload.timeStampEnd]; + } + } + + if ([[metricKitTypes valueForKey:@"diskWriteExceptionDiagnostics"] boolValue] && payload.diskWriteExceptionDiagnostics) { + for (MXDiskWriteExceptionDiagnostic *diagnostic in payload.diskWriteExceptionDiagnostics) { + [self handleDiskWriteExceptionDiagnostic:diagnostic timestamp:payload.timeStampEnd]; + } + } + + if ([[metricKitTypes valueForKey:@"hangDiagnostics"] boolValue] && payload.hangDiagnostics) { + for (MXHangDiagnostic *diagnostic in payload.hangDiagnostics) { + [self handleHangDiagnostic:diagnostic timestamp:payload.timeStampEnd]; + } + } + +#if TARGET_OS_IOS + if (@available(iOS 16.0, *)) { + if ([[metricKitTypes valueForKey:@"appLaunchDiagnostics"] boolValue] && payload.appLaunchDiagnostics) { + for (MXAppLaunchDiagnostic *diagnostic in payload.appLaunchDiagnostics) { + [self handleAppLaunchDiagnostic:diagnostic timestamp:payload.timeStampEnd]; + } + } + } +#endif +#endif +} + +#pragma mark - Crash Diagnostic + +- (void)handleCrashDiagnostic:(MXCrashDiagnostic *)diagnostic + timestamp:(NSDate *)timestamp API_AVAILABLE(ios(14.0), macosx(12.0)) { + + // --- errorClass: map exceptionType to Mach exception name, fall back to generic --- + NSString *excName = BSGExceptionTypeName(diagnostic.exceptionType); + NSString *errorClass = excName ?: @"Crash"; + + // --- errorMessage: build from signal / exceptionCode / terminationReason --- + NSMutableString *message = [NSMutableString string]; + if (diagnostic.terminationReason.length > 0) { + [message appendString:diagnostic.terminationReason]; + } + if (diagnostic.signal) { + NSString *sigName = BSGSignalName(diagnostic.signal); + if (message.length > 0) [message appendString:@", "]; + [message appendFormat:@"Signal %@ (%@)", diagnostic.signal, sigName ?: @"?"]; + } + if (diagnostic.exceptionCode) { + if (message.length > 0) [message appendString:@", "]; + [message appendFormat:@"Exception Code %@", diagnostic.exceptionCode]; + } + if (@available(iOS 17.0, macOS 14.0, *)) { + if (diagnostic.exceptionReason) { + if (message.length > 0) [message appendString:@" — "]; + [message appendString:diagnostic.exceptionReason.composedMessage]; + } + } + if (message.length == 0) { + [message appendString:@"MetricKit crash diagnostic"]; + } + + NSArray *stacktrace = [BSGMetricKitStacktraceConverter stackframesFromCallStackTree:diagnostic.callStackTree]; + + // --- Collect rich metadata --- + NSMutableDictionary *metadata = [NSMutableDictionary dictionary]; + metadata[@"diagnosticType"] = @"crash"; + if (diagnostic.exceptionType) metadata[@"exceptionType"] = diagnostic.exceptionType; + if (diagnostic.exceptionCode) metadata[@"exceptionCode"] = diagnostic.exceptionCode; + if (diagnostic.signal) metadata[@"signal"] = diagnostic.signal; + if (diagnostic.terminationReason) metadata[@"terminationReason"] = diagnostic.terminationReason; + if (diagnostic.virtualMemoryRegionInfo) metadata[@"virtualMemoryRegionInfo"] = diagnostic.virtualMemoryRegionInfo; + + [self notifyWithErrorClass:errorClass + errorMessage:message + stacktrace:stacktrace + timestamp:timestamp + context:@"MetricKit Crash" + diagnostic:diagnostic + metadata:metadata]; +} + +#pragma mark - CPU Exception Diagnostic + +- (void)handleCPUExceptionDiagnostic:(MXCPUExceptionDiagnostic *)diagnostic + timestamp:(NSDate *)timestamp API_AVAILABLE(ios(14.0), macosx(12.0)) { + + double cpuTime = BSGSecondsFromDuration(diagnostic.totalCPUTime); + double sampledTime = BSGSecondsFromDuration(diagnostic.totalSampledTime); + + NSString *message = [NSString stringWithFormat:@"Excessive CPU usage: %.2fs CPU time over %.2fs sampled", cpuTime, sampledTime]; + + NSArray *stacktrace = [BSGMetricKitStacktraceConverter stackframesFromCallStackTree:diagnostic.callStackTree]; + + NSMutableDictionary *metadata = [NSMutableDictionary dictionary]; + metadata[@"diagnosticType"] = @"cpuException"; + metadata[@"totalCPUTimeSeconds"] = @(cpuTime); + metadata[@"totalSampledTimeSeconds"] = @(sampledTime); + + [self notifyWithErrorClass:@"CPUException" + errorMessage:message + stacktrace:stacktrace + timestamp:timestamp + context:@"MetricKit CPU Exception" + diagnostic:diagnostic + metadata:metadata]; +} + +#pragma mark - Disk Write Exception Diagnostic + +- (void)handleDiskWriteExceptionDiagnostic:(MXDiskWriteExceptionDiagnostic *)diagnostic + timestamp:(NSDate *)timestamp API_AVAILABLE(ios(14.0), macosx(12.0)) { + + double writesBytes = BSGBytesFromStorage(diagnostic.totalWritesCaused); + NSString *message = [NSString stringWithFormat:@"Excessive disk writes: %.0f bytes", writesBytes]; + + NSArray *stacktrace = [BSGMetricKitStacktraceConverter stackframesFromCallStackTree:diagnostic.callStackTree]; + + NSMutableDictionary *metadata = [NSMutableDictionary dictionary]; + metadata[@"diagnosticType"] = @"diskWriteException"; + metadata[@"totalWritesCausedBytes"] = @(writesBytes); + + [self notifyWithErrorClass:@"DiskWriteException" + errorMessage:message + stacktrace:stacktrace + timestamp:timestamp + context:@"MetricKit Disk Write Exception" + diagnostic:diagnostic + metadata:metadata]; +} + +#pragma mark - Hang Diagnostic + +- (void)handleHangDiagnostic:(MXHangDiagnostic *)diagnostic + timestamp:(NSDate *)timestamp API_AVAILABLE(ios(14.0), macosx(12.0)) { + + double hangSec = BSGSecondsFromDuration(diagnostic.hangDuration); + NSString *message = [NSString stringWithFormat:@"Application hang: %.2fs", hangSec]; + + NSArray *stacktrace = [BSGMetricKitStacktraceConverter stackframesFromCallStackTree:diagnostic.callStackTree]; + + NSMutableDictionary *metadata = [NSMutableDictionary dictionary]; + metadata[@"diagnosticType"] = @"hang"; + metadata[@"hangDurationSeconds"] = @(hangSec); + + [self notifyWithErrorClass:@"App Hang" + errorMessage:message + stacktrace:stacktrace + timestamp:timestamp + context:@"MetricKit Hang" + diagnostic:diagnostic + metadata:metadata]; +} + +#pragma mark - App Launch Diagnostic (iOS 16+ only) + +#if TARGET_OS_IOS +- (void)handleAppLaunchDiagnostic:(MXAppLaunchDiagnostic *)diagnostic + timestamp:(NSDate *)timestamp API_AVAILABLE(ios(16.0)) { + + double launchSec = BSGSecondsFromDuration(diagnostic.launchDuration); + NSString *message = [NSString stringWithFormat:@"Slow app launch: %.2fs", launchSec]; + + NSArray *stacktrace = [BSGMetricKitStacktraceConverter stackframesFromCallStackTree:diagnostic.callStackTree]; + + NSMutableDictionary *metadata = [NSMutableDictionary dictionary]; + metadata[@"diagnosticType"] = @"appLaunch"; + metadata[@"launchDurationSeconds"] = @(launchSec); + + [self notifyWithErrorClass:@"AppLaunchFailure" + errorMessage:message + stacktrace:stacktrace + timestamp:timestamp + context:@"MetricKit App Launch" + diagnostic:diagnostic + metadata:metadata]; +} +#endif + +#pragma mark - Shared notification path + +- (void)notifyWithErrorClass:(NSString *)errorClass + errorMessage:(NSString *)errorMessage + stacktrace:(NSArray *)stacktrace + timestamp:(NSDate * _Nullable)timestamp + context:(NSString *)context + diagnostic:(MXDiagnostic *)diagnostic + metadata:(NSMutableDictionary *)metadata API_AVAILABLE(ios(14.0), macosx(12.0)) { + + BugsnagFromBugsnagMetricKitPlugin *bugsnag = [BugsnagFromBugsnagMetricKitPlugin sharedInstance]; + if (!bugsnag) { + return; + } + + // Enrich metadata with diagnosticMetaData from the diagnostic JSON + NSDictionary *diagMeta = BSGDiagnosticMetaData(diagnostic); + + // Copy the timestamp so the block can reference it + NSDate *eventTimestamp = timestamp; + NSDate *reportedTime = [NSDate date]; + + [bugsnag notifyPlainEvent:errorClass + errorMessage:errorMessage + stacktrace:stacktrace + timestamp:eventTimestamp + block:^BOOL(id event) { + + // --- Set context --- + [event setValue:context forKey:@"context"]; + + // --- Metadata: metrickit section --- + BSGAddMetadata(event, @"metrickit", @"reportingType", @"delayed"); + for (NSString *key in metadata) { + BSGAddMetadata(event, @"metrickit", key, metadata[key]); + } + + // Add timestamp metadata + if (eventTimestamp) { + NSISO8601DateFormatter *fmt = [[NSISO8601DateFormatter alloc] init]; + BSGAddMetadata(event, @"metrickit", @"eventOccurredAt", [fmt stringFromDate:eventTimestamp]); + BSGAddMetadata(event, @"metrickit", @"eventReportedAt", [fmt stringFromDate:reportedTime]); + BSGAddMetadata(event, @"metrickit", @"deliveryDelaySeconds", + @([reportedTime timeIntervalSinceDate:eventTimestamp])); + } + + // Add diagnosticMetaData fields + if (diagMeta) { + NSArray *knownKeys = @[@"regionFormat", @"lowPowerModeEnabled", @"platformArchitecture", + @"deviceType", @"osVersion", @"bundleIdentifier", + @"appVersion", @"appBuildVersion", @"pid", @"isTestFlightApp"]; + for (NSString *key in knownKeys) { + if (diagMeta[key]) { + BSGAddMetadata(event, @"metrickit", key, diagMeta[key]); + } + } + } + + return YES; + }]; +} + +@end + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Info.plist b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Info.plist new file mode 100644 index 000000000..e1fe4cfb7 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Metrics/BSGMetricsHandler.h b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Metrics/BSGMetricsHandler.h new file mode 100644 index 000000000..f470e772f --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Metrics/BSGMetricsHandler.h @@ -0,0 +1,25 @@ +// +// BSGMetricsHandler.h +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import + +#if __has_include() + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BSGMetricsHandler: NSObject + +- (void)handleMetricsPayload:(MXMetricPayload *)payload API_AVAILABLE(ios(13.0), macosx(12.0)); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Metrics/BSGMetricsHandler.m b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Metrics/BSGMetricsHandler.m new file mode 100644 index 000000000..43a851772 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/Metrics/BSGMetricsHandler.m @@ -0,0 +1,19 @@ +// +// BSGMetricsHandler.m +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 09/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import "BSGMetricsHandler.h" + +#if __has_include() + +@implementation BSGMetricsHandler + +- (void)handleMetricsPayload:(MXMetricPayload *)payload {} + +@end + +#endif diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/include/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.h b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/include/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.h new file mode 100644 index 000000000..a8e3104e6 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin/include/BugsnagMetricKitPlugin/BugsnagMetricKitPlugin.h @@ -0,0 +1,51 @@ +// +// BugsnagMetricKitPlugin.h +// BugsnagMetricKitPlugin +// +// Created by Robert Bartoszewski on 17/03/2026. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import + +@class BugsnagConfiguration; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Plugin that integrates Apple's MetricKit framework with Bugsnag. + * Automatically registers for MetricKit callbacks and reports diagnostics as Bugsnag events. + * + * The plugin is automatically discovered and loaded by Bugsnag when linked. + * No additional setup is required beyond linking the plugin framework. + * + * Note: This plugin implements the BSGPlugin protocol methods (install and configure:) + * but doesn't formally declare conformance to avoid build-time dependencies on Bugsnag. + */ +@interface BugsnagMetricKitPlugin : NSObject + +/** + * Installs the MetricKit plugin and registers for MetricKit callbacks. + * This method is automatically called by Bugsnag during startup if the plugin is linked. + */ ++ (void)install; + +/** + * Configures the plugin with the Bugsnag configuration. + * This method is automatically called by Bugsnag after install. + * + * @param configuration The Bugsnag configuration object + */ ++ (void)configure:(BugsnagConfiguration *)configuration; + +/** + * Returns the current configuration for the plugin. + * Used internally by the plugin's components. + * + * @return The stored configuration, or nil if not yet configured + */ ++ (nullable BugsnagConfiguration *)configuration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/BugsnagMetricKitPlugin/BugsnagMetricKitPluginTests/BugsnagMetricKitPluginTests.m b/BugsnagMetricKitPlugin/BugsnagMetricKitPluginTests/BugsnagMetricKitPluginTests.m new file mode 100644 index 000000000..e0f0e51a6 --- /dev/null +++ b/BugsnagMetricKitPlugin/BugsnagMetricKitPluginTests/BugsnagMetricKitPluginTests.m @@ -0,0 +1,31 @@ +// +// BugsnagMetricKitPluginTests.m +// BugsnagMetricKitPluginTests +// +// Created by Bugsnag on 2026-03-18. +// Copyright © 2026 Bugsnag Inc. All rights reserved. +// + +#import +#import + +@interface BugsnagMetricKitPluginTests : XCTestCase + +@end + +@implementation BugsnagMetricKitPluginTests + +- (void)setUp { + [super setUp]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testPluginExists { + // Test that the plugin class exists and responds to install + XCTAssertTrue([BugsnagMetricKitPlugin respondsToSelector:@selector(install)]); +} + +@end diff --git a/BugsnagMetricKitPlugin/README.md b/BugsnagMetricKitPlugin/README.md new file mode 100644 index 000000000..ac6bb2163 --- /dev/null +++ b/BugsnagMetricKitPlugin/README.md @@ -0,0 +1,126 @@ +# BugsnagMetricKitPlugin + +Automatic MetricKit integration for Bugsnag. + +This plugin automatically captures and reports diagnostic information from Apple's MetricKit framework as Bugsnag error events. MetricKit provides system-level crash diagnostics, CPU exceptions, app hangs, disk write exceptions, and app launch failures. + +## Installation + +### CocoaPods + +Add the plugin to your `Podfile`: + +```ruby +pod 'BugsnagMetricKitPlugin', '~> 6.13' +``` + +Then run `pod install`. + +### Swift Package Manager + +Add the plugin as a dependency in your `Package.swift` file: + +```swift +dependencies: [ + .package(url: "https://github.com/bugsnag/bugsnag-cocoa.git", from: "6.13.0") +], +targets: [ + .target( + name: "YourApp", + dependencies: ["Bugsnag", "BugsnagMetricKitPlugin"] + ) +] +``` + +### Manual + +1. Add `BugsnagMetricKitPlugin.framework` to your project's "Link Binary With Libraries" build phase +2. Add `MetricKit.framework` as an optional (weak) framework + +## Usage + +The plugin automatically loads when linked to your app - no additional configuration needed! + +```swift +// 1. Configure and start Bugsnag as normal +let config = BugsnagConfiguration("YOUR-API-KEY") +Bugsnag.start(with: config) + +// 2. That's it! The MetricKit plugin automatically initializes if linked. +``` + +### Configuration + +You can control which MetricKit diagnostic types are reported via the Bugsnag configuration: + +```swift +let config = BugsnagConfiguration("YOUR-API-KEY") + +// Enable or disable specific diagnostic types +config.enabledMetricKitDiagnostics.enabled = true +config.enabledMetricKitDiagnostics.crashDiagnostics = true +config.enabledMetricKitDiagnostics.cpuExceptionDiagnostics = true +config.enabledMetricKitDiagnostics.hangDiagnostics = true +config.enabledMetricKitDiagnostics.diskWriteExceptionDiagnostics = true +config.enabledMetricKitDiagnostics.appLaunchDiagnostics = true // iOS 16+ only + +Bugsnag.start(with: config) +``` + +## Supported Platforms + +- iOS 13.0+ +- macOS 12.0+ +- visionOS 1.0+ + +Note: MetricKit is not available on tvOS or watchOS. + +## Diagnostic Types + +The plugin reports the following MetricKit diagnostic types: + +### Crash Diagnostics +System-recorded app crashes including exception type, termination reason, and call stack. +- Error classes: `EXC_BAD_ACCESS`, `EXC_BAD_INSTRUCTION`, etc. + +### CPU Exception Diagnostics +Terminations due to excessive CPU usage or CPU-related exceptions. +- Error class: `CPUException` + +### Hang Diagnostics +App hangs where the app becomes unresponsive. +- Error class: `App Hang` + +### Disk Write Exception Diagnostics +Errors related to disk write operations. +- Error class: `DiskWriteException` + +### App Launch Diagnostics (iOS 16+) +Failures that occur during app launch. +- Error class: `AppLaunchFailure` + +## Event Metadata + +All MetricKit events include: +- **source**: Set to `"metrickit"` to distinguish from regular crashes +- **diagnosticPayload**: Full JSON representation of the MetricKit diagnostic + +## How It Works + +1. The plugin registers with Apple's MetricKit framework when Bugsnag starts +2. MetricKit delivers diagnostic payloads (typically on the next app launch after an issue) +3. The plugin converts each diagnostic into a Bugsnag error event +4. Events are uploaded using Bugsnag's existing delivery infrastructure + +## Differences from Regular Crash Reporting + +MetricKit diagnostics: +- Are delivered by the system (usually on next launch) +- May not include all contextual data (breadcrumbs, metadata) +- Provide Apple's official crash classification +- Have a 10-25% opt-in rate among users +- Complement Bugsnag's existing crash reporting + +## License + +The BugsnagMetricKitPlugin is MIT licensed. See [LICENSE.txt](../LICENSE.txt) for details. diff --git a/Package.swift b/Package.swift index 811a6399c..9bb9f9a54 100644 --- a/Package.swift +++ b/Package.swift @@ -12,6 +12,7 @@ let package = Package( products: [ .library(name: "Bugsnag", targets: ["Bugsnag"]), .library(name: "BugsnagNetworkRequestPlugin", targets: ["BugsnagNetworkRequestPlugin"]), + .library(name: "BugsnagMetricKitPlugin", targets: ["BugsnagMetricKitPlugin"]), ], dependencies: [], targets: [ @@ -61,6 +62,18 @@ let package = Package( .headerSearchPath("include/BugsnagNetworkRequestPlugin"), ] ), + .target( + name: "BugsnagMetricKitPlugin", + dependencies: ["Bugsnag"], + path: "BugsnagMetricKitPlugin/BugsnagMetricKitPlugin", + publicHeadersPath: "include", + cSettings: [ + .define("NS_BLOCK_ASSERTIONS", .when(configuration: .release)), + .define("NDEBUG", .when(configuration: .release)), + .headerSearchPath("."), + .headerSearchPath("include/BugsnagMetricKitPlugin"), + ] + ), ], cLanguageStandard: .gnu11, cxxLanguageStandard: .gnucxx14 diff --git a/Tests/BugsnagTests/BugsnagClientMirrorTest.m b/Tests/BugsnagTests/BugsnagClientMirrorTest.m index 1f8753cf5..63b77106b 100644 --- a/Tests/BugsnagTests/BugsnagClientMirrorTest.m +++ b/Tests/BugsnagTests/BugsnagClientMirrorTest.m @@ -143,6 +143,7 @@ - (void)setUp { @"threadsCapture:callstack: @32@0:8@16@24", @"stacktraceCapture:callstack: @32@0:8@16@24", @"userCapture: @24@0:8@16", + @"notifyPlainEventWithErrorClass:errorMessage:stacktrace:timestamp:block: v56@0:8@16@24@32@40@?48", ]]; // the following methods are implemented on Bugsnag but do not need to diff --git a/examples/metric-kit/metric-kit.xcodeproj/project.pbxproj b/examples/metric-kit/metric-kit.xcodeproj/project.pbxproj new file mode 100644 index 000000000..d9da1d3eb --- /dev/null +++ b/examples/metric-kit/metric-kit.xcodeproj/project.pbxproj @@ -0,0 +1,396 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 9691C4732F6AF12100BB5CB0 /* Bugsnag in Frameworks */ = {isa = PBXBuildFile; productRef = 9691C4722F6AF12100BB5CB0 /* Bugsnag */; }; + 9691C4752F6AF12100BB5CB0 /* BugsnagMetricKitPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = 9691C4742F6AF12100BB5CB0 /* BugsnagMetricKitPlugin */; }; + 9691C4782F6AF48E00BB5CB0 /* MetricKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9691C4772F6AF48E00BB5CB0 /* MetricKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 965B94312F6AED7100EAC583 /* metric-kit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "metric-kit.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9691C4772F6AF48E00BB5CB0 /* MetricKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetricKit.framework; path = System/Library/Frameworks/MetricKit.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 965B94432F6AED7300EAC583 /* Exceptions for "metric-kit" folder in "metric-kit" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 965B94302F6AED7100EAC583 /* metric-kit */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 965B94332F6AED7100EAC583 /* metric-kit */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 965B94432F6AED7300EAC583 /* Exceptions for "metric-kit" folder in "metric-kit" target */, + ); + path = "metric-kit"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 965B942E2F6AED7100EAC583 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9691C4752F6AF12100BB5CB0 /* BugsnagMetricKitPlugin in Frameworks */, + 9691C4782F6AF48E00BB5CB0 /* MetricKit.framework in Frameworks */, + 9691C4732F6AF12100BB5CB0 /* Bugsnag in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 965B94282F6AED7100EAC583 = { + isa = PBXGroup; + children = ( + 965B94332F6AED7100EAC583 /* metric-kit */, + 9691C4762F6AF48E00BB5CB0 /* Frameworks */, + 965B94322F6AED7100EAC583 /* Products */, + ); + sourceTree = ""; + }; + 965B94322F6AED7100EAC583 /* Products */ = { + isa = PBXGroup; + children = ( + 965B94312F6AED7100EAC583 /* metric-kit.app */, + ); + name = Products; + sourceTree = ""; + }; + 9691C4762F6AF48E00BB5CB0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9691C4772F6AF48E00BB5CB0 /* MetricKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 965B94302F6AED7100EAC583 /* metric-kit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 965B94442F6AED7300EAC583 /* Build configuration list for PBXNativeTarget "metric-kit" */; + buildPhases = ( + 965B942D2F6AED7100EAC583 /* Sources */, + 965B942E2F6AED7100EAC583 /* Frameworks */, + 965B942F2F6AED7100EAC583 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 965B94332F6AED7100EAC583 /* metric-kit */, + ); + name = "metric-kit"; + packageProductDependencies = ( + 9691C4722F6AF12100BB5CB0 /* Bugsnag */, + 9691C4742F6AF12100BB5CB0 /* BugsnagMetricKitPlugin */, + ); + productName = "metric-kit"; + productReference = 965B94312F6AED7100EAC583 /* metric-kit.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 965B94292F6AED7100EAC583 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2610; + LastUpgradeCheck = 2610; + TargetAttributes = { + 965B94302F6AED7100EAC583 = { + CreatedOnToolsVersion = 26.1.1; + }; + }; + }; + buildConfigurationList = 965B942C2F6AED7100EAC583 /* Build configuration list for PBXProject "metric-kit" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 965B94282F6AED7100EAC583; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 9691C4712F6AF12100BB5CB0 /* XCLocalSwiftPackageReference "../../../bugsnag-cocoa" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 965B94322F6AED7100EAC583 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 965B94302F6AED7100EAC583 /* metric-kit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 965B942F2F6AED7100EAC583 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 965B942D2F6AED7100EAC583 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 965B94452F6AED7300EAC583 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7W9PZ27Y5F; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "metric-kit/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.bugsnag.metric-kit"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "metric-kit/metric-kit-Bridging-Header.h"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 965B94462F6AED7300EAC583 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7W9PZ27Y5F; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "metric-kit/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.bugsnag.metric-kit"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "metric-kit/metric-kit-Bridging-Header.h"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 965B94472F6AED7300EAC583 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 7W9PZ27Y5F; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 965B94482F6AED7300EAC583 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 7W9PZ27Y5F; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 965B942C2F6AED7100EAC583 /* Build configuration list for PBXProject "metric-kit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 965B94472F6AED7300EAC583 /* Debug */, + 965B94482F6AED7300EAC583 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 965B94442F6AED7300EAC583 /* Build configuration list for PBXNativeTarget "metric-kit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 965B94452F6AED7300EAC583 /* Debug */, + 965B94462F6AED7300EAC583 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 9691C4712F6AF12100BB5CB0 /* XCLocalSwiftPackageReference "../../../bugsnag-cocoa" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../bugsnag-cocoa"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 9691C4722F6AF12100BB5CB0 /* Bugsnag */ = { + isa = XCSwiftPackageProductDependency; + productName = Bugsnag; + }; + 9691C4742F6AF12100BB5CB0 /* BugsnagMetricKitPlugin */ = { + isa = XCSwiftPackageProductDependency; + productName = BugsnagMetricKitPlugin; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 965B94292F6AED7100EAC583 /* Project object */; +} diff --git a/examples/metric-kit/metric-kit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/metric-kit/metric-kit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/examples/metric-kit/metric-kit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/metric-kit/metric-kit/AnObjCClass.h b/examples/metric-kit/metric-kit/AnObjCClass.h new file mode 100644 index 000000000..2c7c8dc7b --- /dev/null +++ b/examples/metric-kit/metric-kit/AnObjCClass.h @@ -0,0 +1,30 @@ +// Copyright (c) 2016 Bugsnag, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +@interface AnObjCClass : NSObject + +- (void)trap; +- (void)corruptSomeMemory; +- (void)accessInvalidMemoryAddress; +- (void)throwCxxException; + +@end diff --git a/examples/metric-kit/metric-kit/AnObjCClass.mm b/examples/metric-kit/metric-kit/AnObjCClass.mm new file mode 100644 index 000000000..ffa0ca6e8 --- /dev/null +++ b/examples/metric-kit/metric-kit/AnObjCClass.mm @@ -0,0 +1,65 @@ +// Copyright (c) 2016 Bugsnag, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AnObjCClass.h" + +#import + +@implementation AnObjCClass + +- (void)trap { + __builtin_trap(); +} + +- (void)corruptSomeMemory { + /* Some random data */ + void *cache[] = { + NULL, NULL, NULL + }; + + void *displayStrings[6] = { + (void *)"This little piggy went to the meerket", + (void *)"This little piggy stayed at home", + cache, + (void *)"This little piggy had roast beef.", + (void *)"This little piggy had none.", + (void *)"And this little piggy went 'Wee! Wee! Wee!' all the way home", + }; + + /* A corrupted/under-retained/re-used piece of memory */ + struct { + void *isa; + } corruptObj; + corruptObj.isa = displayStrings; + + /* Message an invalid/corrupt object. This will deadlock crash reporters + * using Objective-C. */ + [(__bridge id)&corruptObj class]; +} + +- (void)accessInvalidMemoryAddress { + *(int *)0xdeadbeef = 0; +} + +- (void)throwCxxException { + throw std::runtime_error("This is a C++ exception"); +} + +@end diff --git a/examples/metric-kit/metric-kit/AnotherClass.swift b/examples/metric-kit/metric-kit/AnotherClass.swift new file mode 100644 index 000000000..8d0ef17af --- /dev/null +++ b/examples/metric-kit/metric-kit/AnotherClass.swift @@ -0,0 +1,26 @@ +// Copyright (c) 2016 Bugsnag, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +class AnotherClass: NSObject { + + @objc static func crash3() { + preconditionFailure("This should NEVER happen") + } +} diff --git a/examples/metric-kit/metric-kit/AppDelegate.swift b/examples/metric-kit/metric-kit/AppDelegate.swift new file mode 100644 index 000000000..3064cdb44 --- /dev/null +++ b/examples/metric-kit/metric-kit/AppDelegate.swift @@ -0,0 +1,95 @@ +// Copyright (c) 2026 Bugsnag, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Bugsnag + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + /** + This is the minimum amount of setup required for Bugsnag to work. Simply add your API key to the app's .plist (Supporting Files/Info.plist) and the application will deliver all error and session notifications to the appropriate dashboard. + + You can find your API key in your Bugsnag dashboard under the settings menu. + */ + + /** + Bugsnag behavior can be configured through the plist and/or further extended in code by creating a BugsnagConfiguration object and passing it to [Bugsnag startWithConfiguration]. + + All subsequent setup is optional, and will configure your Bugsnag setup in different ways. A few common examples are included here, for more detailed explanations please look at the documented configuration options at https://docs.bugsnag.com/platforms/ios/configuration-options/ + */ + + // Create config object from the application plist + let config = BugsnagConfiguration.loadConfig() + config.enabledMetricKitDiagnostics.enabled = true + config.enabledMetricKitDiagnostics.crashDiagnostics = true + config.enabledMetricKitDiagnostics.cpuExceptionDiagnostics = true + config.enabledMetricKitDiagnostics.appLaunchDiagnostics = true + config.enabledMetricKitDiagnostics.hangDiagnostics = true + config.enabledMetricKitDiagnostics.diskWriteExceptionDiagnostics = true + + // ... or construct an empty object +// let config = BugsnagConfiguration("YOUR-API-KEY") + + /** + This sets some user information that will be attached to each error. + */ +// config.setUser("DefaultUser", withEmail:"Not@real.fake", andName:"Default User") + + /** + The appVersion will let you see what release an error is present in. This will be picked up automatically from your build settings, but can be manually overwritten as well. + */ +// config.appVersion = "1.5.0" + + /** + When persisting a user you won't need to set the user information everytime the app opens, instead it will be persisted between each app session. + */ +// config.persistUser = true + + /** + This option allows you to send more or less detail about errors to Bugsnag. Setting it to Always or Unhandled means you'll have detailed stacktraces of all app threads available when debugging unexpected errors. + */ +// config.sendThreads = .always + + /** + Enabled error types allow you to customize exactly what errors are automatically captured and delivered to your Bugsnag dashboard. A detailed breakdown of each error type can be found in the configuration option documentation. + */ +// config.enabledErrorTypes.ooms = false +// config.enabledErrorTypes.unhandledExceptions = true +// config.enabledErrorTypes.machExceptions = true + + /** + If there's information that you do not wish sent to your Bugsnag dashboard, such as passwords or user information, you can set the keys as redacted. When a notification is sent to Bugsnag all keys matching your set filters will be redacted before they leave your application. + All automatically captured data can be found here: https://docs.bugsnag.com/platforms/ios/automatically-captured-data/. + */ +// config.redactedKeys = ["password", "credit_card_number"] + + /** + Finally, start Bugsnag with the specified configuration: + */ + Bugsnag.start(with: config) + + return true + } +} + diff --git a/examples/metric-kit/metric-kit/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/metric-kit/metric-kit/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/examples/metric-kit/metric-kit/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/metric-kit/metric-kit/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/metric-kit/metric-kit/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..230588010 --- /dev/null +++ b/examples/metric-kit/metric-kit/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/metric-kit/metric-kit/Assets.xcassets/Contents.json b/examples/metric-kit/metric-kit/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/examples/metric-kit/metric-kit/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/metric-kit/metric-kit/Base.lproj/LaunchScreen.storyboard b/examples/metric-kit/metric-kit/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..865e9329f --- /dev/null +++ b/examples/metric-kit/metric-kit/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/metric-kit/metric-kit/Base.lproj/Main.storyboard b/examples/metric-kit/metric-kit/Base.lproj/Main.storyboard new file mode 100644 index 000000000..246d872ac --- /dev/null +++ b/examples/metric-kit/metric-kit/Base.lproj/Main.storyboard @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/metric-kit/metric-kit/Info.plist b/examples/metric-kit/metric-kit/Info.plist new file mode 100644 index 000000000..578ed00bb --- /dev/null +++ b/examples/metric-kit/metric-kit/Info.plist @@ -0,0 +1,69 @@ + + + + + bugsnag + + apiKey + YOUR_API_KEY + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/metric-kit/metric-kit/OutOfMemoryController.h b/examples/metric-kit/metric-kit/OutOfMemoryController.h new file mode 100644 index 000000000..e2fc13427 --- /dev/null +++ b/examples/metric-kit/metric-kit/OutOfMemoryController.h @@ -0,0 +1,24 @@ +// Copyright (c) 2016 Bugsnag, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +@interface OutOfMemoryController : UIViewController + +@end diff --git a/examples/metric-kit/metric-kit/OutOfMemoryController.m b/examples/metric-kit/metric-kit/OutOfMemoryController.m new file mode 100644 index 000000000..3ebfe5dad --- /dev/null +++ b/examples/metric-kit/metric-kit/OutOfMemoryController.m @@ -0,0 +1,131 @@ +// Copyright (c) 2016 Bugsnag, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "OutOfMemoryController.h" + +#include +#include +#include +#include +#include + +#define PRINT_STATS 1 + +#define MEGABYTE 0x100000 + +@implementation OutOfMemoryController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = UIColor.groupTableViewBackgroundColor; +} + +- (void)didReceiveMemoryWarning { + NSLog(@"--> Received a low memory warning"); +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + struct utsname system = {0}; + uname(&system); + NSLog(@"*** Device = %s", system.machine); + + NSUInteger physicalMemory = (NSUInteger)NSProcessInfo.processInfo.physicalMemory; + NSUInteger megabytes = physicalMemory / MEGABYTE; + NSLog(@"*** Physical memory = %lu MB", (unsigned long)megabytes); + + // + // The ActiveHard limit and point at which a memory warning is sent varies by device + // Some data from http://www.chenxiyao.com/article/10/read + // + // Device Total Warn Limit + // ======================================================= + // iPad3,1 iPad 3rd gen 987 560 700 (70%) + // iPhone5,1 iPhone 5 650 + // iPhone6,2 iPhone 5s 1000 600 650 (65%) + // iPhone7,1 iPhone 6 Plus 650 + // iPhone7,2 iPhone 6 650 + // iPhone8,1 iPhone 6s 1380 + // iPhone8,2 iPhone 6s Plus 1380 + // iPhone8,4 iPhone SE 1380 + // iPhone9,1 iPhone 7 1380 + // iPhone9,2 iPhone 7 Plus 2050 + // iPhone10,1 iPhone 8 1380 + // iPhone10,2 iPhone 8 Plus 2050 + // iPhone10,3 iPhone X 1400 + // iPhone11,2 iPhone XS 2050 + // iPhone11,4 iPhone XS Max 2050 + // iPhone11,8 iPhone XR 1400 + // iPhone12,1 iPhone 11 3859 2098 (54%) + // iPhone12,8 iPhone SE (2) 2965 1994 2098 (70%) + // iPhone13,1 iPhone 12 mini 3718 1546 2098 (57%) + // + NSUInteger limit = 0; + task_vm_info_data_t vm_info = {0}; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vm_info, &count); + if (result == KERN_SUCCESS && count >= TASK_VM_INFO_REV4_COUNT) { + limit = (NSUInteger)(vm_info.phys_footprint + vm_info.limit_bytes_remaining) / MEGABYTE; + NSLog(@"*** Memory limit = %lu MB", (unsigned long)limit); + } else if (!strcmp(system.machine, "iPhone6,2")) { + limit = 650; + NSLog(@"*** Memory limit = %lu MB", (unsigned long)limit); + } else { + limit = MIN(2098, megabytes * 70 / 100); + NSLog(@"*** Memory limit = %lu MB (estimated)", (unsigned long)limit); + } + + // The size of the initial block needs to be under the memory warning limit in order for one to be reliably sent. + NSUInteger initial = limit * 70 / 100; + NSLog(@"*** Dirtying an initial block of %lu MB", (unsigned long)initial); + [self consumeMegabytes:initial]; + + NSLog(@"*** Dirtying remaining memory in ~2 seconds"); + NSTimeInterval timeInterval = 2.0 / (limit - initial); + [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(timerFired) userInfo:nil repeats:YES]; +} + +- (void)timerFired { + [self consumeMegabytes:1]; +} + +- (void)consumeMegabytes:(NSUInteger)megabytes { + for (NSUInteger i = 0; i < megabytes; i++) { + volatile char *ptr = malloc(MEGABYTE); + // Originally used NSPageSize() but on iPhone 5s iOS 12 that didn't result in dirtying all the memory allocated. + const int pagesize = 4096; + const int npages = MEGABYTE / pagesize; + for (int page = 0; page < npages; page++) { + ptr[page * pagesize] = 42; // Dirty each page + } + } +#if PRINT_STATS + task_vm_info_data_t info; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &info, &count); + assert(result == KERN_SUCCESS); + unsigned long long physicalMemory = NSProcessInfo.processInfo.physicalMemory; + NSLog(@"%4llu / %4llu MB (%llu%%)", info.phys_footprint / MEGABYTE, physicalMemory / MEGABYTE, info.phys_footprint * 100 / physicalMemory); +#endif +} + +@end diff --git a/examples/metric-kit/metric-kit/SceneDelegate.swift b/examples/metric-kit/metric-kit/SceneDelegate.swift new file mode 100644 index 000000000..6a5d50111 --- /dev/null +++ b/examples/metric-kit/metric-kit/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// metric-kit +// +// Created by Robert Bartoszewski on 12/03/2026. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/examples/metric-kit/metric-kit/ViewController.swift b/examples/metric-kit/metric-kit/ViewController.swift new file mode 100644 index 000000000..ada381d47 --- /dev/null +++ b/examples/metric-kit/metric-kit/ViewController.swift @@ -0,0 +1,242 @@ +// Copyright (c) 2016 Bugsnag, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit +import Bugsnag + +class ViewController: UITableViewController { + + private struct Row { + let title: String + let action: (ViewController) -> Void + } + + private struct Section { + let title: String + let rows: [Row] + let footer: String + } + + private let sections: [Section] = [ + Section( + title: "Crashes", + rows: [ + Row(title: "Uncaught Objective-C Exception") { $0.generateUncaughtException() }, + Row(title: "POSIX Signal") { $0.generatePOSIXSignal() }, + Row(title: "Mach exception") { $0.generateMachException() }, + Row(title: "Memory Corruption") { $0.generateMemoryCorruption() }, + Row(title: "Stack Overflow") { $0.generateStackOverflow() }, + Row(title: "Swift Precondition Failure") { $0.generateAssertionFailure() }, + Row(title: "Out Of Memory") { $0.generateOutOfMemoryError() }, + Row(title: "Uncaught C++ Exception") { $0.generateCxxException() }, + Row(title: "Fatal App Hang") { $0.generateFatalAppHang() }, + ], + footer: "Events which terminate the app are sent to Bugsnag automatically. Reopen the app after a crash to send reports."), + + Section( + title: "Handled Errors", + rows: [ + Row(title: "Send error with notifyError()") { $0.sendAnError() }, + ], + footer: "Events which can be handled gracefully can also be reported to Bugsnag."), + + Section( + title: "Metadata, breadcrumbs, and callbacks", + rows: [ + Row(title: "Add client metadata") { $0.addClientMetadata() }, + Row(title: "Add filtered metadata") { $0.addFilteredMetadata() }, + Row(title: "Clear metadata") { $0.clearMetadata() }, + Row(title: "Add custom breadcrumb") { $0.addCustomBreadcrumb() }, + Row(title: "Add breadcrumb with callback") { $0.addBreadcrumbWithCallback() }, + Row(title: "Set user") { $0.setUser() }, + ], + footer: "Adding diagnostic data and modifying how the event shows on the Bugsnag dashboard."), + + Section( + title: "Sessions", + rows: [ + Row(title: "Start new session") { $0.startNewSession() }, + Row(title: "Pause current session") { $0.pauseCurrentSession() }, + Row(title: "Resume current session") { $0.resumeCurrentSession() }, + ], + footer: "Demonstrates the methods of manually determining when sessions are created and expire."), + ] + + // MARK: - Crashes + + func generateUncaughtException() { + let someJson : Dictionary = ["foo":self] + do { + let data = try JSONSerialization.data(withJSONObject: someJson, options: .prettyPrinted) + print("Received data: %@", data) + } catch { + // Why does this crash the app? A very good question. + } + } + + func generatePOSIXSignal() { + AnObjCClass().trap() + } + + func generateMachException() { + AnObjCClass().accessInvalidMemoryAddress() + } + + func generateStackOverflow() { + let items = ["Something!"] + // Use if statement to remove warning about calling self through any path + if (items[0] == "Something!") { + generateStackOverflow() + } + print("items: %@", items) + } + + func generateMemoryCorruption() { + AnObjCClass().corruptSomeMemory() + } + + func generateAssertionFailure() { + AnotherClass.crash3() + } + + func generateOutOfMemoryError() { + Bugsnag.leaveBreadcrumb(withMessage: "Starting an OutOfMemoryController for an OOM") + let controller = OutOfMemoryController(); + navigationController?.pushViewController(controller, animated: true) + } + + func generateCxxException() { + AnObjCClass().throwCxxException() + } + + func generateFatalAppHang() { + Thread.sleep(forTimeInterval: 3) + _exit(1) + } + + // MARK: - Handled Errors + + func sendAnError() { + do { + try FileManager.default.removeItem(atPath:"//invalid/file") + } catch { + Bugsnag.notifyError(error) { event in + // modify report properties in the (optional) block + event.severity = .info + return true + } + } + } + + // MARK: - Metadata, breadcrumbs, and callbacks + + private enum MetadataSection: String { + case extras + } + + func addClientMetadata() { + // This method adds some metadata to your application client, that will be included in all subsequent error reports, and visible on the "extras" tab on the Bugsnag dashboard. + Bugsnag.addMetadata("metadata!", key: "client", section: MetadataSection.extras.rawValue) + } + + func addFilteredMetadata() { + // This method adds some metadata that will be redacted on the Bugsnag dashboard. + // It will only work if the optional configuration is uncommented in the `AppDelegate.swift` file. + Bugsnag.addMetadata("secret123", key: "password", section: MetadataSection.extras.rawValue) + } + + func clearMetadata() { + // This method clears all metadata in the "extras" tab that would be attached to the error reports. + // It won't clear data that hasn't been added yet, like data attached through a callback. + Bugsnag.clearMetadata(section: MetadataSection.extras.rawValue) + } + + func addCustomBreadcrumb() { + // This is the simplest example of leaving a custom breadcrumb. + // This will show up under the "breadcrumbs" tab of your error on the Bugsnag dashboard. + Bugsnag.leaveBreadcrumb(withMessage: "This is our custom breadcrumb!") + } + + func addBreadcrumbWithCallback() { + // This adds a callback to the breadcrumb process, setting a different breadcrumb type if a specific message is present. + // It leaves a slightly more detailed breadcrumb than before, with a message, metadata, and type all specified. + Bugsnag.addOnBreadcrumb { + if $0.message == "Custom breadcrumb name" { + $0.type = .process + } + return true + } + Bugsnag.leaveBreadcrumb("Custom breadcrumb name", metadata: ["metadata": "here!"], type: .manual) + } + + func setUser() { + // This sets a user on the client, similar to setting one on the configuration. + // It will also set the user in a session payload. + Bugsnag.setUser("user123", withEmail: "TestUser@example.com", andName: "Test Userson") + } + + // MARK: - Sessions + + func startNewSession() { + // This starts a new session within Bugsnag. + // While sessions are generally configured to work automatically, this allows you to define when a session begins. + Bugsnag.startSession() + } + + func pauseCurrentSession() { + // This pauses the current session. + // If an error occurs when a session is paused it will not be included in the session statistics for the project. + Bugsnag.pauseSession() + } + + func resumeCurrentSession() { + // This allows you to resume the previous session, keeping a record of any errors that previously occurred within a single session intact. + Bugsnag.resumeSession(); + } + + // MARK: - + + override func numberOfSections(in tableView: UITableView) -> Int { + return self.sections.count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return self.sections[section].title + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.sections[section].rows.count + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + self.sections[section].footer + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + cell.textLabel?.text = self.sections[indexPath.section].rows[indexPath.row].title + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + self.sections[indexPath.section].rows[indexPath.row].action(self) + tableView.deselectRow(at: indexPath, animated: true) + } +} diff --git a/examples/metric-kit/metric-kit/metric-kit-Bridging-Header.h b/examples/metric-kit/metric-kit/metric-kit-Bridging-Header.h new file mode 100644 index 000000000..1891b82d3 --- /dev/null +++ b/examples/metric-kit/metric-kit/metric-kit-Bridging-Header.h @@ -0,0 +1,22 @@ +// Copyright (c) 2016 Bugsnag, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AnObjCClass.h" +#import "OutOfMemoryController.h" diff --git a/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj index d54daf605..df67c9932 100644 --- a/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj @@ -128,6 +128,7 @@ 8AF6FD7A225E3FA00056EF9E /* ResumeSessionOOMScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 8AF6FD79225E3FA00056EF9E /* ResumeSessionOOMScenario.m */; }; 8AF8FCAC22BD1E5400A967CA /* UnhandledInternalNotifyScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF8FCAB22BD1E5400A967CA /* UnhandledInternalNotifyScenario.swift */; }; 8AF8FCAE22BD23BA00A967CA /* HandledInternalNotifyScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF8FCAD22BD23BA00A967CA /* HandledInternalNotifyScenario.swift */; }; + 963173B22F6AC0D5006643BA /* MetricKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 963173B12F6AC0D5006643BA /* MetricKit.framework */; }; 967F6F1229B2236A0054EED8 /* InternalWorkingsScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967F6F1129B2236A0054EED8 /* InternalWorkingsScenario.swift */; }; 96A25F322D66AFAE00A18116 /* ObjCExceptionFeatureFlagsScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A25F312D66AFAE00A18116 /* ObjCExceptionFeatureFlagsScenario.m */; }; A1117E552535A59100014FDA /* OOMLoadScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1117E542535A59100014FDA /* OOMLoadScenario.swift */; }; @@ -344,6 +345,7 @@ 8AF6FD79225E3FA00056EF9E /* ResumeSessionOOMScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ResumeSessionOOMScenario.m; sourceTree = ""; }; 8AF8FCAB22BD1E5400A967CA /* UnhandledInternalNotifyScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnhandledInternalNotifyScenario.swift; sourceTree = ""; }; 8AF8FCAD22BD23BA00A967CA /* HandledInternalNotifyScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandledInternalNotifyScenario.swift; sourceTree = ""; }; + 963173B12F6AC0D5006643BA /* MetricKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetricKit.framework; path = System/Library/Frameworks/MetricKit.framework; sourceTree = SDKROOT; }; 967F6F1129B2236A0054EED8 /* InternalWorkingsScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalWorkingsScenario.swift; sourceTree = ""; }; 96A25F312D66AFAE00A18116 /* ObjCExceptionFeatureFlagsScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjCExceptionFeatureFlagsScenario.m; sourceTree = ""; }; A1117E542535A59100014FDA /* OOMLoadScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOMLoadScenario.swift; sourceTree = ""; }; @@ -443,6 +445,7 @@ 01DF442C2798044E00C31104 /* SystemConfiguration.framework in Frameworks */, CB42FF9026F1EDB500E8D5D2 /* libBugsnagNetworkRequestPluginStatic.a in Frameworks */, 01CD103926690463007FA5F0 /* libBugsnagStatic.a in Frameworks */, + 963173B22F6AC0D5006643BA /* MetricKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -503,6 +506,7 @@ D018DCE3210BCB4D0B8FA6D2 /* Frameworks */ = { isa = PBXGroup; children = ( + 963173B12F6AC0D5006643BA /* MetricKit.framework */, 01DF442B2798044E00C31104 /* SystemConfiguration.framework */, CB42FF8F26F1EDB500E8D5D2 /* libBugsnagNetworkRequestPluginStatic.a */, AAFEFA0226EB6B5A00980A10 /* BugsnagNetworkRequestPlugin.framework */,