From 55498e86e726a25728893f54766f6a6f838a2de1 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 11 Dec 2024 10:34:03 +0100 Subject: [PATCH] Feature flags performance improvements --- Bugsnag.xcodeproj/project.pbxproj | 98 ++++-- Bugsnag/BugsnagInternals.h | 6 +- Bugsnag/Client/BugsnagClient+Private.h | 3 + Bugsnag/Client/BugsnagClient.m | 21 +- .../BugsnagConfiguration+Private.h | 2 +- Bugsnag/Configuration/BugsnagConfiguration.m | 6 +- .../BSGEventUploadKSCrashReportOperation.m | 2 +- .../FeatureFlags/BSGMemoryFeatureFlagStore.h | 56 +++ .../FeatureFlags/BSGMemoryFeatureFlagStore.m | 319 ++++++++++++++++++ .../BSGPersistentFeatureFlagStore.h | 32 ++ .../BSGPersistentFeatureFlagStore.m | 135 ++++++++ .../FeatureFlags/BugsnagStoredFeatureFlag.h | 27 ++ .../FeatureFlags/BugsnagStoredFeatureFlag.m | 43 +++ Bugsnag/Helpers/BSGFeatureFlagStore.h | 44 --- Bugsnag/Helpers/BSGFeatureFlagStore.m | 182 ---------- Bugsnag/Payload/BugsnagEvent+Private.h | 2 +- Bugsnag/Payload/BugsnagEvent.m | 23 +- Bugsnag/Storage/BSGFileLocations.h | 2 +- Bugsnag/Storage/BSGFileLocations.m | 1 + Tests/BugsnagTests/BSGFeatureFlagStoreTests.m | 8 +- Tests/BugsnagTests/BugsnagClientMirrorTest.m | 2 + .../shared/scenarios/OOMSessionlessScenario.m | 3 + features/release/out_of_memory.feature | 7 + 23 files changed, 748 insertions(+), 276 deletions(-) create mode 100644 Bugsnag/FeatureFlags/BSGMemoryFeatureFlagStore.h create mode 100644 Bugsnag/FeatureFlags/BSGMemoryFeatureFlagStore.m create mode 100644 Bugsnag/FeatureFlags/BSGPersistentFeatureFlagStore.h create mode 100644 Bugsnag/FeatureFlags/BSGPersistentFeatureFlagStore.m create mode 100644 Bugsnag/FeatureFlags/BugsnagStoredFeatureFlag.h create mode 100644 Bugsnag/FeatureFlags/BugsnagStoredFeatureFlag.m delete mode 100644 Bugsnag/Helpers/BSGFeatureFlagStore.h delete mode 100644 Bugsnag/Helpers/BSGFeatureFlagStore.m diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index c1ede6ab0..cafabd3c6 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -514,13 +514,13 @@ 01099397273D123800128BBE /* BugsnagFeatureFlag.m in Sources */ = {isa = PBXBuildFile; fileRef = 01099392273D123800128BBE /* BugsnagFeatureFlag.m */; }; 01099398273D123800128BBE /* BugsnagFeatureFlag.m in Sources */ = {isa = PBXBuildFile; fileRef = 01099392273D123800128BBE /* BugsnagFeatureFlag.m */; }; 01099399273D123800128BBE /* BugsnagFeatureFlag.m in Sources */ = {isa = PBXBuildFile; fileRef = 01099392273D123800128BBE /* BugsnagFeatureFlag.m */; }; - 0109939C273D13D800128BBE /* BSGFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0109939A273D13D800128BBE /* BSGFeatureFlagStore.h */; }; - 0109939D273D13D800128BBE /* BSGFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0109939A273D13D800128BBE /* BSGFeatureFlagStore.h */; }; - 0109939E273D13D800128BBE /* BSGFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0109939A273D13D800128BBE /* BSGFeatureFlagStore.h */; }; - 0109939F273D13D800128BBE /* BSGFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGFeatureFlagStore.m */; }; - 010993A0273D13D800128BBE /* BSGFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGFeatureFlagStore.m */; }; - 010993A1273D13D800128BBE /* BSGFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGFeatureFlagStore.m */; }; - 010993A2273D13D800128BBE /* BSGFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGFeatureFlagStore.m */; }; + 0109939C273D13D800128BBE /* BSGMemoryFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0109939A273D13D800128BBE /* BSGMemoryFeatureFlagStore.h */; }; + 0109939D273D13D800128BBE /* BSGMemoryFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0109939A273D13D800128BBE /* BSGMemoryFeatureFlagStore.h */; }; + 0109939E273D13D800128BBE /* BSGMemoryFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0109939A273D13D800128BBE /* BSGMemoryFeatureFlagStore.h */; }; + 0109939F273D13D800128BBE /* BSGMemoryFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGMemoryFeatureFlagStore.m */; }; + 010993A0273D13D800128BBE /* BSGMemoryFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGMemoryFeatureFlagStore.m */; }; + 010993A1273D13D800128BBE /* BSGMemoryFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGMemoryFeatureFlagStore.m */; }; + 010993A2273D13D800128BBE /* BSGMemoryFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGMemoryFeatureFlagStore.m */; }; 010993A4273D188B00128BBE /* BSGFeatureFlagStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 010993A3273D188B00128BBE /* BSGFeatureFlagStoreTests.m */; }; 010993A5273D188B00128BBE /* BSGFeatureFlagStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 010993A3273D188B00128BBE /* BSGFeatureFlagStoreTests.m */; }; 010993A6273D188B00128BBE /* BSGFeatureFlagStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 010993A3273D188B00128BBE /* BSGFeatureFlagStoreTests.m */; }; @@ -832,6 +832,24 @@ 3A700AF824A6492F0068CD1B /* BugsnagError.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3A700A9124A63A8E0068CD1B /* BugsnagError.h */; }; 3A700AF924A6492F0068CD1B /* BugsnagDeviceWithState.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3A700A9224A63A8E0068CD1B /* BugsnagDeviceWithState.h */; }; 3A700AFA24A6492F0068CD1B /* BugsnagMetadata.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3A700A9324A63A8E0068CD1B /* BugsnagMetadata.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 */; }; + 968BFBCE2D011BC400DCC24B /* BSGPersistentFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 968BFBC92D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.h */; }; + 968BFBCF2D011BC400DCC24B /* BSGPersistentFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 968BFBC92D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.h */; }; + 968BFBD02D011BC900DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBCA2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m */; }; + 968BFBD12D011BCA00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBCA2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m */; }; + 968BFBD22D011BCA00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBCA2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m */; }; + 968BFBD32D011BCB00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBCA2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m */; }; + 968BFBD62D0125C800DCC24B /* BugsnagStoredFeatureFlag.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBD42D0125C700DCC24B /* BugsnagStoredFeatureFlag.m */; }; + 968BFBD72D0125C800DCC24B /* BugsnagStoredFeatureFlag.h in Headers */ = {isa = PBXBuildFile; fileRef = 968BFBD52D0125C800DCC24B /* BugsnagStoredFeatureFlag.h */; }; + 968BFBD82D0125CB00DCC24B /* BugsnagStoredFeatureFlag.h in Headers */ = {isa = PBXBuildFile; fileRef = 968BFBD52D0125C800DCC24B /* BugsnagStoredFeatureFlag.h */; }; + 968BFBD92D0125CB00DCC24B /* BugsnagStoredFeatureFlag.h in Headers */ = {isa = PBXBuildFile; fileRef = 968BFBD52D0125C800DCC24B /* BugsnagStoredFeatureFlag.h */; }; + 968BFBDA2D0125CC00DCC24B /* BugsnagStoredFeatureFlag.h in Headers */ = {isa = PBXBuildFile; fileRef = 968BFBD52D0125C800DCC24B /* BugsnagStoredFeatureFlag.h */; }; + 968BFBDB2D0125CF00DCC24B /* BugsnagStoredFeatureFlag.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBD42D0125C700DCC24B /* BugsnagStoredFeatureFlag.m */; }; + 968BFBDC2D0125CF00DCC24B /* BugsnagStoredFeatureFlag.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBD42D0125C700DCC24B /* BugsnagStoredFeatureFlag.m */; }; + 968BFBDD2D0125D000DCC24B /* BugsnagStoredFeatureFlag.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBD42D0125C700DCC24B /* BugsnagStoredFeatureFlag.m */; }; + 968BFBDE2D0125D000DCC24B /* BugsnagStoredFeatureFlag.m in Sources */ = {isa = PBXBuildFile; fileRef = 968BFBD42D0125C700DCC24B /* BugsnagStoredFeatureFlag.m */; }; 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 */; }; @@ -995,8 +1013,8 @@ CBBDE93D280068D40070DCD3 /* BSGKeys.h in Headers */ = {isa = PBXBuildFile; fileRef = 008968152486DA5600DC48C2 /* BSGKeys.h */; }; CBBDE93E280068D40070DCD3 /* BSGJSONSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = CBCF77A125010648004AF22A /* BSGJSONSerialization.h */; }; CBBDE93F280068D40070DCD3 /* BSGSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = 008968112486DA5600DC48C2 /* BSGSerialization.h */; }; - CBBDE940280068D40070DCD3 /* BSGFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGFeatureFlagStore.m */; }; - CBBDE941280068D40070DCD3 /* BSGFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0109939A273D13D800128BBE /* BSGFeatureFlagStore.h */; }; + CBBDE940280068D40070DCD3 /* BSGMemoryFeatureFlagStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 0109939B273D13D800128BBE /* BSGMemoryFeatureFlagStore.m */; }; + CBBDE941280068D40070DCD3 /* BSGMemoryFeatureFlagStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 0109939A273D13D800128BBE /* BSGMemoryFeatureFlagStore.h */; }; CBBDE942280068E60070DCD3 /* BugsnagCollections.h in Headers */ = {isa = PBXBuildFile; fileRef = 008968102486DA5600DC48C2 /* BugsnagCollections.h */; }; CBBDE945280068E60070DCD3 /* BugsnagLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 008968142486DA5600DC48C2 /* BugsnagLogger.h */; }; CBBDE946280068E60070DCD3 /* BugsnagCollections.m in Sources */ = {isa = PBXBuildFile; fileRef = 008968172486DA5600DC48C2 /* BugsnagCollections.m */; }; @@ -1483,8 +1501,8 @@ 00E636C324878FFC006CBF1A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 01099391273D123800128BBE /* BugsnagFeatureFlag.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagFeatureFlag.h; sourceTree = ""; }; 01099392273D123800128BBE /* BugsnagFeatureFlag.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagFeatureFlag.m; sourceTree = ""; }; - 0109939A273D13D800128BBE /* BSGFeatureFlagStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGFeatureFlagStore.h; sourceTree = ""; }; - 0109939B273D13D800128BBE /* BSGFeatureFlagStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGFeatureFlagStore.m; sourceTree = ""; }; + 0109939A273D13D800128BBE /* BSGMemoryFeatureFlagStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGMemoryFeatureFlagStore.h; sourceTree = ""; }; + 0109939B273D13D800128BBE /* BSGMemoryFeatureFlagStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGMemoryFeatureFlagStore.m; sourceTree = ""; }; 010993A3273D188B00128BBE /* BSGFeatureFlagStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGFeatureFlagStoreTests.m; sourceTree = ""; }; 010993B0273D2F6100128BBE /* BugsnagFeatureFlagStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagFeatureFlagStore.h; sourceTree = ""; }; 010F80C128645B4200D6569E /* BSGDefinesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGDefinesTests.m; sourceTree = ""; }; @@ -1600,6 +1618,10 @@ 3A700A9124A63A8E0068CD1B /* BugsnagError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagError.h; sourceTree = ""; }; 3A700A9224A63A8E0068CD1B /* BugsnagDeviceWithState.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagDeviceWithState.h; sourceTree = ""; }; 3A700A9324A63A8E0068CD1B /* BugsnagMetadata.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagMetadata.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 /* BugsnagStoredFeatureFlag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagStoredFeatureFlag.m; sourceTree = ""; }; + 968BFBD52D0125C800DCC24B /* BugsnagStoredFeatureFlag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagStoredFeatureFlag.h; 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 = ""; }; @@ -1948,6 +1970,7 @@ 00AD1C7424869B0E00A27979 /* Bugsnag */ = { isa = PBXGroup; children = ( + 00AD1CF124869EBD00A27979 /* Breadcrumbs */, 00AD1F002486A17900A27979 /* BSGCrashSentry.h */, 00AD1EFF2486A17900A27979 /* BSGCrashSentry.m */, 00AD1EFE2486A17800A27979 /* Bugsnag.m */, @@ -1960,10 +1983,10 @@ 00AD1F012486A17900A27979 /* BugsnagSessionTracker.m */, CBB0928B2519F891007698BC /* BugsnagSystemState.h */, CBB0928A2519F891007698BC /* BugsnagSystemState.m */, - 00AD1CF124869EBD00A27979 /* Breadcrumbs */, 00AD1CF224869ECF00A27979 /* Client */, 00AD1CF324869ED700A27979 /* Configuration */, 00AD1CF424869EDF00A27979 /* Delivery */, + 968BFBC82CFFE21B00DCC24B /* FeatureFlags */, 00AD1CF524869EE500A27979 /* Helpers */, 3A700A7E24A63A8E0068CD1B /* include */, 008968F72486DAD000DC48C2 /* KSCrash */, @@ -2128,8 +2151,6 @@ 010FF28325ED2A8D00E4F2B0 /* BSGAppHangDetector.m */, 019480C42625EE9800E833ED /* BSGAppKit.h */, CB4C83BD280FFA4800E7E2BD /* BSGDefines.h */, - 0109939A273D13D800128BBE /* BSGFeatureFlagStore.h */, - 0109939B273D13D800128BBE /* BSGFeatureFlagStore.m */, CBEC89282A4AC2920088A3CE /* BSGFilesystem.h */, CBEC89292A4AC2920088A3CE /* BSGFilesystem.m */, CB374456283F5FD400A3955E /* BSGHardware.h */, @@ -2281,6 +2302,19 @@ path = Bugsnag; sourceTree = ""; }; + 968BFBC82CFFE21B00DCC24B /* FeatureFlags */ = { + isa = PBXGroup; + children = ( + 968BFBD52D0125C800DCC24B /* BugsnagStoredFeatureFlag.h */, + 968BFBD42D0125C700DCC24B /* BugsnagStoredFeatureFlag.m */, + 968BFBC92D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.h */, + 968BFBCA2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m */, + 0109939A273D13D800128BBE /* BSGMemoryFeatureFlagStore.h */, + 0109939B273D13D800128BBE /* BSGMemoryFeatureFlagStore.m */, + ); + path = FeatureFlags; + sourceTree = ""; + }; E746292324890BFE00F92D67 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -2343,6 +2377,7 @@ 3A700AA624A63ADC0068CD1B /* BugsnagDeviceWithState.h in Headers */, 3A700AA724A63ADC0068CD1B /* BugsnagMetadata.h in Headers */, 008967F72486DA4500DC48C2 /* BugsnagApiClient.h in Headers */, + 968BFBCB2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.h in Headers */, 008969CC2486DAD100DC48C2 /* BSG_KSMach.h in Headers */, 008968282486DA5600DC48C2 /* BSGKeys.h in Headers */, CBCAF6FA25A457F90095771F /* BSGFileLocations.h in Headers */, @@ -2363,6 +2398,7 @@ 008969E72486DAD100DC48C2 /* BSG_KSSystemInfoC.h in Headers */, 00AD1F102486A17900A27979 /* BugsnagSessionTracker.h in Headers */, 0126F79B25DD510E008483C2 /* BSGEventUploadObjectOperation.h in Headers */, + 968BFBD72D0125C800DCC24B /* BugsnagStoredFeatureFlag.h in Headers */, 008968882486DA9600DC48C2 /* BugsnagHandledState.h in Headers */, CBCF77A325010648004AF22A /* BSGJSONSerialization.h in Headers */, 013D9CD126C5262F0077F0AD /* UISceneStub.h in Headers */, @@ -2411,7 +2447,7 @@ 008969872486DAD100DC48C2 /* BSG_KSMachApple.h in Headers */, 008969C92486DAD100DC48C2 /* BSG_RFC3339DateTool.h in Headers */, 008969F32486DAD100DC48C2 /* BSG_KSCrashState.h in Headers */, - 0109939C273D13D800128BBE /* BSGFeatureFlagStore.h in Headers */, + 0109939C273D13D800128BBE /* BSGMemoryFeatureFlagStore.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2426,6 +2462,7 @@ 3A700AAC24A63CFD0068CD1B /* BugsnagEndpointConfiguration.h in Headers */, CBB092912519F891007698BC /* BugsnagSystemState.h in Headers */, 3A700AAD24A63CFD0068CD1B /* BugsnagBreadcrumb.h in Headers */, + 968BFBCD2D011BC300DCC24B /* BSGPersistentFeatureFlagStore.h in Headers */, 0126F7AC25DD5118008483C2 /* BSGEventUploadFileOperation.h in Headers */, 3A700AAE24A63CFD0068CD1B /* BSG_KSCrashReportWriter.h in Headers */, 3A700AAF24A63CFD0068CD1B /* BugsnagErrorTypes.h in Headers */, @@ -2498,6 +2535,7 @@ 008969942486DAD100DC48C2 /* BSG_KSLogger.h in Headers */, CBE9062B25A34DAB0045B965 /* BSGStorageMigratorV0V1.h in Headers */, 008969FA2486DAD100DC48C2 /* BSG_KSCrashReportVersion.h in Headers */, + 968BFBD82D0125CB00DCC24B /* BugsnagStoredFeatureFlag.h in Headers */, 0089680B2486DA4500DC48C2 /* BSGConnectivity.h in Headers */, 008968262486DA5600DC48C2 /* BugsnagLogger.h in Headers */, 0089698B2486DAD100DC48C2 /* BSG_KSString.h in Headers */, @@ -2517,7 +2555,7 @@ 008969882486DAD100DC48C2 /* BSG_KSMachApple.h in Headers */, 008969CA2486DAD100DC48C2 /* BSG_RFC3339DateTool.h in Headers */, 008969F42486DAD100DC48C2 /* BSG_KSCrashState.h in Headers */, - 0109939D273D13D800128BBE /* BSGFeatureFlagStore.h in Headers */, + 0109939D273D13D800128BBE /* BSGMemoryFeatureFlagStore.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2532,6 +2570,7 @@ 3A700AC024A63D110068CD1B /* BugsnagEndpointConfiguration.h in Headers */, CBB092922519F891007698BC /* BugsnagSystemState.h in Headers */, 3A700AC124A63D110068CD1B /* BugsnagBreadcrumb.h in Headers */, + 968BFBCE2D011BC400DCC24B /* BSGPersistentFeatureFlagStore.h in Headers */, 0126F7AD25DD5118008483C2 /* BSGEventUploadFileOperation.h in Headers */, 3A700AC224A63D110068CD1B /* BSG_KSCrashReportWriter.h in Headers */, 3A700AC324A63D110068CD1B /* BugsnagErrorTypes.h in Headers */, @@ -2604,6 +2643,7 @@ 008969952486DAD100DC48C2 /* BSG_KSLogger.h in Headers */, CBE9062C25A34DAB0045B965 /* BSGStorageMigratorV0V1.h in Headers */, 008969FB2486DAD100DC48C2 /* BSG_KSCrashReportVersion.h in Headers */, + 968BFBD92D0125CB00DCC24B /* BugsnagStoredFeatureFlag.h in Headers */, 0089680C2486DA4500DC48C2 /* BSGConnectivity.h in Headers */, 008968272486DA5600DC48C2 /* BugsnagLogger.h in Headers */, 0089698C2486DAD100DC48C2 /* BSG_KSString.h in Headers */, @@ -2623,7 +2663,7 @@ 008969892486DAD100DC48C2 /* BSG_KSMachApple.h in Headers */, 008969CB2486DAD100DC48C2 /* BSG_RFC3339DateTool.h in Headers */, 008969F52486DAD100DC48C2 /* BSG_KSCrashState.h in Headers */, - 0109939E273D13D800128BBE /* BSGFeatureFlagStore.h in Headers */, + 0109939E273D13D800128BBE /* BSGMemoryFeatureFlagStore.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2642,6 +2682,7 @@ CBBDE95E280068FD0070DCD3 /* BugsnagMetadataStore.h in Headers */, 01A2957F28B665F5005FCC8C /* BSGNetworkBreadcrumb.h in Headers */, CBBDE9AF280069B20070DCD3 /* BSG_KSArchSpecific.h in Headers */, + 968BFBCF2D011BC400DCC24B /* BSGPersistentFeatureFlagStore.h in Headers */, CBBDE91C2800687E0070DCD3 /* BugsnagBreadcrumbs.h in Headers */, CBBDE9252800689F0070DCD3 /* BSGConnectivity.h in Headers */, CBBDE9842800698F0070DCD3 /* BSG_KSCrashDoctor.h in Headers */, @@ -2688,7 +2729,7 @@ CBBDE9882800698F0070DCD3 /* BSG_KSCrashReport.h in Headers */, CBBDE933280068AD0070DCD3 /* BSGEventUploadFileOperation.h in Headers */, CBBDE9972800699C0070DCD3 /* BSG_KSCrashSentry_CPPException.h in Headers */, - CBBDE941280068D40070DCD3 /* BSGFeatureFlagStore.h in Headers */, + CBBDE941280068D40070DCD3 /* BSGMemoryFeatureFlagStore.h in Headers */, CBBDE96B2800693F0070DCD3 /* BugsnagNotifier.h in Headers */, CBBDE97F2800698F0070DCD3 /* BSG_KSFile.h in Headers */, CBBDE961280068FD0070DCD3 /* BugsnagConfiguration.h in Headers */, @@ -2696,6 +2737,7 @@ CBBDE9992800699C0070DCD3 /* BSG_KSCrashSentry_Private.h in Headers */, CBBDE938280068C40070DCD3 /* BSGAppHangDetector.h in Headers */, CBBDE978280069670070DCD3 /* BSGStorageMigratorV0V1.h in Headers */, + 968BFBDA2D0125CC00DCC24B /* BugsnagStoredFeatureFlag.h in Headers */, CBBDE979280069670070DCD3 /* BSGFileLocations.h in Headers */, CBBDE9AB280069B20070DCD3 /* BSG_KSJSONCodec.h in Headers */, CBBDE960280068FD0070DCD3 /* BSG_KSCrashReportWriter.h in Headers */, @@ -3086,7 +3128,7 @@ 008968722486DA9500DC48C2 /* BugsnagDevice.m in Sources */, 008969842486DAD100DC48C2 /* BSG_KSMachHeaders.c in Sources */, 00896A322486DAD100DC48C2 /* BSG_KSCrashC.c in Sources */, - 0109939F273D13D800128BBE /* BSGFeatureFlagStore.m in Sources */, + 0109939F273D13D800128BBE /* BSGMemoryFeatureFlagStore.m in Sources */, 008969902486DAD100DC48C2 /* BSG_RFC3339DateTool.m in Sources */, CB33CD012703438400C76656 /* BSG_KSCrashNames.c in Sources */, 008968842486DA9600DC48C2 /* BugsnagNotifier.m in Sources */, @@ -3122,6 +3164,7 @@ 008967BE2486DA1900DC48C2 /* BugsnagClient.m in Sources */, 09E312F32BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m in Sources */, 008968952486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, + 968BFBD62D0125C800DCC24B /* BugsnagStoredFeatureFlag.m in Sources */, 008967FE2486DA4500DC48C2 /* BSGSessionUploader.m in Sources */, 0089686B2486DA9500DC48C2 /* BugsnagEvent.m in Sources */, 008969A82486DAD100DC48C2 /* BSG_KSSysCtl.c in Sources */, @@ -3135,6 +3178,7 @@ 01099396273D123800128BBE /* BugsnagFeatureFlag.m in Sources */, 00896A172486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.mm in Sources */, 008969CF2486DAD100DC48C2 /* BSG_KSCrashState.m in Sources */, + 968BFBCC2D011BBB00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */, 01840B7225DC26E200F95648 /* BSGEventUploader.m in Sources */, 008968C32486DA9600DC48C2 /* BugsnagUser.m in Sources */, 01CB95C2278F0C830077744A /* BSG_KSFile.c in Sources */, @@ -3284,7 +3328,7 @@ 00AD1F242486A17900A27979 /* Bugsnag.m in Sources */, 008967B52486D9D800DC48C2 /* BugsnagBreadcrumbs.m in Sources */, 00AD1F2F2486A17900A27979 /* BugsnagSessionTracker.m in Sources */, - 010993A0273D13D800128BBE /* BSGFeatureFlagStore.m in Sources */, + 010993A0273D13D800128BBE /* BSGMemoryFeatureFlagStore.m in Sources */, 008969B22486DAD100DC48C2 /* BSG_KSMach_x86_64.c in Sources */, 0126F7BF25DD512B008483C2 /* BSGEventUploadKSCrashReportOperation.m in Sources */, CB3744982845FA9500A3955E /* BSG_KSCrashStringConversion.c in Sources */, @@ -3303,6 +3347,7 @@ 008968BA2486DA9600DC48C2 /* BugsnagStacktrace.m in Sources */, 09E312F42BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m in Sources */, 00896A152486DAD100DC48C2 /* BSG_KSCrashSentry_Signal.c in Sources */, + 968BFBDB2D0125CF00DCC24B /* BugsnagStoredFeatureFlag.m in Sources */, 01468F5625876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, 01CB95C3278F0C830077744A /* BSG_KSFile.c in Sources */, 008967BF2486DA1900DC48C2 /* BugsnagClient.m in Sources */, @@ -3316,6 +3361,7 @@ 0126F79F25DD510E008483C2 /* BSGEventUploadObjectOperation.m in Sources */, 0089682C2486DA5600DC48C2 /* BSGSerialization.m in Sources */, 01847D9A2644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */, + 968BFBD02D011BC900DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */, 00896A182486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.mm in Sources */, 008969D02486DAD100DC48C2 /* BSG_KSCrashState.m in Sources */, 01099397273D123800128BBE /* BugsnagFeatureFlag.m in Sources */, @@ -3446,7 +3492,7 @@ 0126F7B025DD5118008483C2 /* BSGEventUploadFileOperation.m in Sources */, 008969862486DAD100DC48C2 /* BSG_KSMachHeaders.c in Sources */, 00896A342486DAD100DC48C2 /* BSG_KSCrashC.c in Sources */, - 010993A1273D13D800128BBE /* BSGFeatureFlagStore.m in Sources */, + 010993A1273D13D800128BBE /* BSGMemoryFeatureFlagStore.m in Sources */, 008969922486DAD100DC48C2 /* BSG_RFC3339DateTool.m in Sources */, CB33CD032703438400C76656 /* BSG_KSCrashNames.c in Sources */, 008968862486DA9600DC48C2 /* BugsnagNotifier.m in Sources */, @@ -3481,6 +3527,7 @@ 00896A162486DAD100DC48C2 /* BSG_KSCrashSentry_Signal.c in Sources */, 09E312F52BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m in Sources */, 01468F5725876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, + 968BFBDC2D0125CF00DCC24B /* BugsnagStoredFeatureFlag.m in Sources */, 008967C02486DA1900DC48C2 /* BugsnagClient.m in Sources */, 008968972486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, 008968002486DA4500DC48C2 /* BSGSessionUploader.m in Sources */, @@ -3494,6 +3541,7 @@ 01847D9B2644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */, 01099398273D123800128BBE /* BugsnagFeatureFlag.m in Sources */, 00896A192486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.mm in Sources */, + 968BFBD12D011BCA00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */, 008969D12486DAD100DC48C2 /* BSG_KSCrashState.m in Sources */, 01840B7425DC26E200F95648 /* BSGEventUploader.m in Sources */, 008968C52486DA9600DC48C2 /* BugsnagUser.m in Sources */, @@ -3622,10 +3670,11 @@ E7462909248907E500F92D67 /* BSG_KSMach_x86_32.c in Sources */, E746290B248907E500F92D67 /* BSG_KSMach_Arm.c in Sources */, 0126F7B125DD5118008483C2 /* BSGEventUploadFileOperation.m in Sources */, + 968BFBDE2D0125D000DCC24B /* BugsnagStoredFeatureFlag.m in Sources */, 017DCF942874212F000ECB22 /* BSGTelemetry.m in Sources */, E746290C248907E500F92D67 /* BSG_KSJSONCodec.c in Sources */, E746290D248907E500F92D67 /* BSG_KSMach.c in Sources */, - 010993A2273D13D800128BBE /* BSGFeatureFlagStore.m in Sources */, + 010993A2273D13D800128BBE /* BSGMemoryFeatureFlagStore.m in Sources */, E746290E248907E500F92D67 /* BSG_KSMach_Arm64.c in Sources */, E746290F248907E500F92D67 /* BSG_KSFileUtils.c in Sources */, E7462910248907E500F92D67 /* BSG_KSBacktrace.c in Sources */, @@ -3659,6 +3708,7 @@ 008967C12486DA1900DC48C2 /* BugsnagClient.m in Sources */, CBEC89362A4AC7A90088A3CE /* BSGPersistentDeviceID.m in Sources */, 008968752486DA9500DC48C2 /* BugsnagDevice.m in Sources */, + 968BFBD32D011BCB00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */, 010FF28A25ED2A8D00E4F2B0 /* BSGAppHangDetector.m in Sources */, 00E636C224878D84006CBF1A /* BSG_RFC3339DateTool.m in Sources */, 0089687F2486DA9600DC48C2 /* BugsnagBreadcrumb.m in Sources */, @@ -3760,6 +3810,7 @@ CBBDE923280068970070DCD3 /* BugsnagEndpointConfiguration.m in Sources */, 09E312F62BF230660081F219 /* BugsnagCocoaPerformanceFromBugsnagCocoa.m in Sources */, CBBDE92E280068AD0070DCD3 /* BSGConnectivity.m in Sources */, + 968BFBDD2D0125D000DCC24B /* BugsnagStoredFeatureFlag.m in Sources */, CBBDE912280068560070DCD3 /* BSGCrashSentry.m in Sources */, CBBDE9BC280069B20070DCD3 /* BSG_Symbolicate.c in Sources */, CBBDE98E2800698F0070DCD3 /* BSG_KSCrashState.m in Sources */, @@ -3773,6 +3824,7 @@ CBBDE9702800694E0070DCD3 /* BugsnagStackframe.m in Sources */, CBBDE937280068C40070DCD3 /* BSG_RFC3339DateTool.m in Sources */, CBBDE9A8280069B20070DCD3 /* BSG_KSMach_Arm.c in Sources */, + 968BFBD22D011BCA00DCC24B /* BSGPersistentFeatureFlagStore.m in Sources */, CBBDE9AC280069B20070DCD3 /* BSG_KSMach_x86_64.c in Sources */, CBBDE9B5280069B20070DCD3 /* BSG_KSFileUtils.c in Sources */, CBBDE932280068AD0070DCD3 /* BugsnagApiClient.m in Sources */, @@ -3796,7 +3848,7 @@ CBBDE96A280069290070DCD3 /* BugsnagEvent.m in Sources */, CBBDE91B2800687B0070DCD3 /* BSGNotificationBreadcrumbs.m in Sources */, CBBDE9662800691A0070DCD3 /* BugsnagBreadcrumb.m in Sources */, - CBBDE940280068D40070DCD3 /* BSGFeatureFlagStore.m in Sources */, + CBBDE940280068D40070DCD3 /* BSGMemoryFeatureFlagStore.m in Sources */, CBBDE9C2280069B20070DCD3 /* BSG_KSMach_Arm64.c in Sources */, CBBDE9212800688F0070DCD3 /* BSGConfigurationBuilder.m in Sources */, CBBDE964280069130070DCD3 /* BugsnagApp.m in Sources */, diff --git a/Bugsnag/BugsnagInternals.h b/Bugsnag/BugsnagInternals.h index 76c00bea4..050e235f0 100644 --- a/Bugsnag/BugsnagInternals.h +++ b/Bugsnag/BugsnagInternals.h @@ -22,7 +22,7 @@ #import "BugsnagHandledState.h" #import "BugsnagNotifier.h" -@interface BSGFeatureFlagStore : NSObject +@interface BSGMemoryFeatureFlagStore : NSObject @end NS_ASSUME_NONNULL_BEGIN @@ -79,7 +79,7 @@ typedef void (^ BSGClientObserver)(BSGClientObserverEvent event, _Nullable id va @property (retain, nonatomic) BugsnagConfiguration *configuration; -@property (readonly, nonatomic) BSGFeatureFlagStore *featureFlagStore; +@property (readonly, nonatomic) BSGMemoryFeatureFlagStore *featureFlagStore; @property (strong, nonatomic) BugsnagMetadata *metadata; @@ -161,7 +161,7 @@ typedef void (^ BSGClientObserver)(BSGClientObserverEvent event, _Nullable id va - (NSDictionary *)toJsonWithRedactedKeys:(nullable NSSet *)redactedKeys; -@property (readwrite, strong, nonnull, nonatomic) BSGFeatureFlagStore *featureFlagStore; +@property (readwrite, strong, nonnull, nonatomic) BSGMemoryFeatureFlagStore *featureFlagStore; @end diff --git a/Bugsnag/Client/BugsnagClient+Private.h b/Bugsnag/Client/BugsnagClient+Private.h index 068e40727..5f1130e67 100644 --- a/Bugsnag/Client/BugsnagClient+Private.h +++ b/Bugsnag/Client/BugsnagClient+Private.h @@ -19,6 +19,7 @@ @class BugsnagNotifier; @class BugsnagSessionTracker; @class BugsnagSystemState; +@class BSGPersistentFeatureFlagStore; NS_ASSUME_NONNULL_BEGIN @@ -67,6 +68,8 @@ BSG_OBJC_DIRECT_MEMBERS /// } @property (strong, nonatomic) BugsnagMetadata *state; +@property (strong, nonatomic) BSGPersistentFeatureFlagStore *featureFlags; + @property (strong, nonatomic) NSMutableArray *stateEventBlocks; @property (strong, nonatomic) BugsnagSystemState *systemState; diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 362ac68f2..c47cf0686 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -72,6 +72,7 @@ #import "BugsnagUser+Private.h" #import "BSGPersistentDeviceID.h" #import "BugsnagCocoaPerformanceFromBugsnagCocoa.h" +#import "BSGPersistentFeatureFlagStore.h" static struct { // Contains the user-specified metadata, including the user tab from config. @@ -141,6 +142,7 @@ static void BSSerializeDataCrashHandler(const BSG_KSCrashReportWriter *writer, b writer->addIntegerElement(writer, "thermalState", bsg_runContext->thermalState); BugsnagBreadcrumbsWriteCrashReport(writer, requiresAsyncSafety); + BugsnagFeatureFlagsWriteCrashReport(writer, requiresAsyncSafety); // Create a file to indicate that the crash has been handled by // the library. This exists in case the subsequent `onCrash` handler @@ -213,11 +215,11 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { } _featureFlagStore = [configuration.featureFlagStore copy]; + _featureFlagStore.isMainStore = YES; _state = [[BugsnagMetadata alloc] initWithDictionary:@{ BSGKeyClient: @{ BSGKeyContext: _configuration.context ?: [NSNull null], - BSGKeyFeatureFlags: BSGFeatureFlagStoreToJSON(_featureFlagStore), }, BSGKeyUser: [_configuration.user toJson] ?: @{} }]; @@ -236,6 +238,8 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { bsg_g_bugsnag_data.onCrash = (void (*)(const BSG_KSCrashReportWriter *))self.configuration.onCrashHandler; _breadcrumbStore = [[BugsnagBreadcrumbs alloc] initWithConfiguration:self.configuration]; + + _featureFlags = [[BSGPersistentFeatureFlagStore alloc] initWithStorageDirectory:fileLocations.featureFlags]; // Start with a copy of the configuration metadata self.metadata = [[_configuration metadata] copy]; @@ -285,6 +289,7 @@ - (void)start { [self.metadata setStorageBuffer:&bsg_g_bugsnag_data.metadataJSON file:BSGFileLocations.current.metadata]; [self.state setStorageBuffer:&bsg_g_bugsnag_data.stateJSON file:BSGFileLocations.current.state]; [self.breadcrumbStore removeAllBreadcrumbs]; + [self.featureFlags clear]; #if BSG_HAVE_REACHABILITY [self setupConnectivityListener]; @@ -883,7 +888,7 @@ - (void)dealloc { - (void)addFeatureFlagWithName:(NSString *)name variant:(nullable NSString *)variant { @synchronized (self.featureFlagStore) { BSGFeatureFlagStoreAddFeatureFlag(self.featureFlagStore, name, variant); - [self.state addMetadata:BSGFeatureFlagStoreToJSON(self.featureFlagStore) withKey:BSGKeyFeatureFlags toSection:BSGKeyClient]; + [self.featureFlags addFeatureFlag:name withVariant:variant]; } if (self.observer) { self.observer(BSGClientObserverAddFeatureFlag, [BugsnagFeatureFlag flagWithName:name variant:variant]); @@ -893,7 +898,7 @@ - (void)addFeatureFlagWithName:(NSString *)name variant:(nullable NSString *)var - (void)addFeatureFlagWithName:(NSString *)name { @synchronized (self.featureFlagStore) { BSGFeatureFlagStoreAddFeatureFlag(self.featureFlagStore, name, nil); - [self.state addMetadata:BSGFeatureFlagStoreToJSON(self.featureFlagStore) withKey:BSGKeyFeatureFlags toSection:BSGKeyClient]; + [self.featureFlags addFeatureFlag:name withVariant:nil]; } if (self.observer) { self.observer(BSGClientObserverAddFeatureFlag, [BugsnagFeatureFlag flagWithName:name]); @@ -903,7 +908,7 @@ - (void)addFeatureFlagWithName:(NSString *)name { - (void)addFeatureFlags:(NSArray *)featureFlags { @synchronized (self.featureFlagStore) { BSGFeatureFlagStoreAddFeatureFlags(self.featureFlagStore, featureFlags); - [self.state addMetadata:BSGFeatureFlagStoreToJSON(self.featureFlagStore) withKey:BSGKeyFeatureFlags toSection:BSGKeyClient]; + [self.featureFlags addFeatureFlags:featureFlags]; } if (self.observer) { for (BugsnagFeatureFlag *featureFlag in featureFlags) { @@ -915,7 +920,7 @@ - (void)addFeatureFlags:(NSArray *)featureFlags { - (void)clearFeatureFlagWithName:(NSString *)name { @synchronized (self.featureFlagStore) { BSGFeatureFlagStoreClear(self.featureFlagStore, name); - [self.state addMetadata:BSGFeatureFlagStoreToJSON(self.featureFlagStore) withKey:BSGKeyFeatureFlags toSection:BSGKeyClient]; + [self.featureFlags clear:name]; } if (self.observer) { self.observer(BSGClientObserverClearFeatureFlag, name); @@ -925,7 +930,7 @@ - (void)clearFeatureFlagWithName:(NSString *)name { - (void)clearFeatureFlags { @synchronized (self.featureFlagStore) { BSGFeatureFlagStoreClear(self.featureFlagStore, nil); - [self.state addMetadata:BSGFeatureFlagStoreToJSON(self.featureFlagStore) withKey:BSGKeyFeatureFlags toSection:BSGKeyClient]; + [self.featureFlags clear]; } if (self.observer) { self.observer(BSGClientObserverClearFeatureFlag, nil); @@ -1225,9 +1230,7 @@ - (nullable BugsnagEvent *)generateEventForLastLaunchWithError:(BugsnagError *)e session:session]; event.context = stateDict[BSGKeyClient][BSGKeyContext]; - - id featureFlags = stateDict[BSGKeyClient][BSGKeyFeatureFlags]; - event.featureFlagStore = BSGFeatureFlagStoreFromJSON(featureFlags); + event.featureFlagStore = BSGFeatureFlagStoreWithFlags([self.featureFlags allFlags]); return event; } diff --git a/Bugsnag/Configuration/BugsnagConfiguration+Private.h b/Bugsnag/Configuration/BugsnagConfiguration+Private.h index e7c3ec76a..dda99f2e9 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration+Private.h +++ b/Bugsnag/Configuration/BugsnagConfiguration+Private.h @@ -24,7 +24,7 @@ BSG_OBJC_DIRECT_MEMBERS @property (readonly, nonatomic) NSDictionary *dictionaryRepresentation; -@property (nonatomic) BSGFeatureFlagStore *featureFlagStore; +@property (nonatomic) BSGMemoryFeatureFlagStore *featureFlagStore; @property (copy, nonatomic) BugsnagMetadata *metadata; diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index edab5f2a5..17d0e9ca2 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -28,7 +28,7 @@ #import "BSGConfigurationBuilder.h" #import "BSGDefines.h" -#import "BSGFeatureFlagStore.h" +#import "BSGMemoryFeatureFlagStore.h" #import "BSGKeys.h" #import "BugsnagApiClient.h" #import "BugsnagEndpointConfiguration.h" @@ -166,7 +166,7 @@ - (instancetype)initWithApiKey:(NSString *)apiKey { if (apiKey) { [self setApiKey:apiKey]; } - _featureFlagStore = [[BSGFeatureFlagStore alloc] init]; + _featureFlagStore = [[BSGMemoryFeatureFlagStore alloc] init]; _metadata = [[BugsnagMetadata alloc] init]; _endpoints = [BugsnagEndpointConfiguration new]; _autoDetectErrors = YES; @@ -241,7 +241,7 @@ - (instancetype)initWithDictionaryRepresentation:(NSDictionary * _bundleVersion = dictionaryRepresentation[BSGKeyBundleVersion]; _context = dictionaryRepresentation[BSGKeyContext]; _enabledReleaseStages = dictionaryRepresentation[BSGKeyEnabledReleaseStages]; - _featureFlagStore = [[BSGFeatureFlagStore alloc] init]; + _featureFlagStore = [[BSGMemoryFeatureFlagStore alloc] init]; _releaseStage = dictionaryRepresentation[BSGKeyReleaseStage]; return self; } diff --git a/Bugsnag/Delivery/BSGEventUploadKSCrashReportOperation.m b/Bugsnag/Delivery/BSGEventUploadKSCrashReportOperation.m index ca3d5302d..109e38fd0 100644 --- a/Bugsnag/Delivery/BSGEventUploadKSCrashReportOperation.m +++ b/Bugsnag/Delivery/BSGEventUploadKSCrashReportOperation.m @@ -37,7 +37,7 @@ return nil; } NSMutableArray *keys = [NSMutableArray array]; - NSString *pattern = @"\"(report|process|system|system_atcrash|binary_images|crash|threads|error|user_atcrash|config|metaData|state|breadcrumbs)\":"; + NSString *pattern = @"\"(report|process|system|system_atcrash|binary_images|crash|threads|error|user_atcrash|config|metaData|state|breadcrumbs|featureFlags)\":"; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; for (NSTextCheckingResult *result in [regex matchesInString:string options:0 range:NSMakeRange(0, string.length)]) { if ([result numberOfRanges] == 2) { diff --git a/Bugsnag/FeatureFlags/BSGMemoryFeatureFlagStore.h b/Bugsnag/FeatureFlags/BSGMemoryFeatureFlagStore.h new file mode 100644 index 000000000..51bd0910e --- /dev/null +++ b/Bugsnag/FeatureFlags/BSGMemoryFeatureFlagStore.h @@ -0,0 +1,56 @@ +// +// BSGMemoryFeatureFlagStore.h +// Bugsnag +// +// Created by Nick Dowell on 11/11/2021. +// Copyright © 2021 Bugsnag Inc. All rights reserved. +// + +#import "BugsnagInternals.h" +#import "BSGDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +void BSGFeatureFlagStoreAddFeatureFlag(BSGMemoryFeatureFlagStore *store, NSString *name, NSString *_Nullable variant); + +void BSGFeatureFlagStoreAddFeatureFlags(BSGMemoryFeatureFlagStore *store, NSArray *featureFlags); + +void BSGFeatureFlagStoreClear(BSGMemoryFeatureFlagStore *store, NSString *_Nullable name); + +NSArray * BSGFeatureFlagStoreToJSON(BSGMemoryFeatureFlagStore *store); + +BSGMemoryFeatureFlagStore * BSGFeatureFlagStoreFromJSON(id _Nullable json); +BSGMemoryFeatureFlagStore * BSGFeatureFlagStoreWithFlags(NSArray *); + + +BSG_OBJC_DIRECT_MEMBERS +@interface BSGMemoryFeatureFlagStore () + +@property(nonatomic,nonnull,readonly) NSArray * allFlags; +@property(nonatomic) BOOL isMainStore; + ++ (nonnull BSGMemoryFeatureFlagStore *) fromJSON:(nonnull id)json; ++ (nonnull BSGMemoryFeatureFlagStore *)withFlags:(NSArray *)flags; + +- (NSUInteger) count; + +- (void) addFeatureFlag:(nonnull NSString *)name withVariant:(nullable NSString *)variant; + +- (void) addFeatureFlags:(nonnull NSArray *)featureFlags; + +- (void) clear:(nullable NSString *)name; + +- (nonnull NSArray *) toJSON; + +@end + +#pragma mark - + +/** + * Inserts the current feature flags into a crash report. + * + */ +void BugsnagFeatureFlagsWriteCrashReport(const BSG_KSCrashReportWriter * _Nonnull writer, + bool requiresAsyncSafety); + +NS_ASSUME_NONNULL_END diff --git a/Bugsnag/FeatureFlags/BSGMemoryFeatureFlagStore.m b/Bugsnag/FeatureFlags/BSGMemoryFeatureFlagStore.m new file mode 100644 index 000000000..49849ed9a --- /dev/null +++ b/Bugsnag/FeatureFlags/BSGMemoryFeatureFlagStore.m @@ -0,0 +1,319 @@ +// +// BSGMemoryFeatureFlagStore.m +// Bugsnag +// +// Created by Nick Dowell on 11/11/2021. +// Copyright © 2021 Bugsnag Inc. All rights reserved. +// + +#import "BSGMemoryFeatureFlagStore.h" + +#import "BSGKeys.h" +#import "BugsnagFeatureFlag.h" +#import "BSGJSONSerialization.h" +#import "BugsnagLogger.h" + +#import + +void BSGFeatureFlagStoreAddFeatureFlag(BSGMemoryFeatureFlagStore *store, NSString *name, NSString *_Nullable variant) { + [store addFeatureFlag:name withVariant:variant]; +} + +void BSGFeatureFlagStoreAddFeatureFlags(BSGMemoryFeatureFlagStore *store, NSArray *featureFlags) { + [store addFeatureFlags:featureFlags]; +} + +void BSGFeatureFlagStoreClear(BSGMemoryFeatureFlagStore *store, NSString *_Nullable name) { + [store clear:name]; +} + +NSArray * BSGFeatureFlagStoreToJSON(BSGMemoryFeatureFlagStore *store) { + return [store toJSON]; +} + +BSGMemoryFeatureFlagStore * BSGFeatureFlagStoreFromJSON(id json) { + return [BSGMemoryFeatureFlagStore fromJSON:json]; +} + +BSGMemoryFeatureFlagStore * BSGFeatureFlagStoreWithFlags(NSArray *flags) { + return [BSGMemoryFeatureFlagStore withFlags:flags]; +} + +struct bsg_feature_flag_list_item { + struct bsg_feature_flag_list_item *next; + struct bsg_feature_flag_list_item *previous; + char jsonData[]; // MUST be null terminated +}; + +static _Atomic(struct bsg_feature_flag_list_item *) g_feature_flags_head; +static _Atomic(struct bsg_feature_flag_list_item *) g_feature_flags_tail; +static atomic_bool g_writing_crash_report; +static NSMutableDictionary *nameToFlag; + +/** + * Stores feature flags as a dictionary containing the flag name as a key, with the + * value being the index into an array containing the complete feature flag. + * + * Removals leave holes in the array, which gets rebuilt on clear once there are too many holes. + * + * This gives the access speed of a dictionary while keeping ordering intact. + */ +BSG_OBJC_DIRECT_MEMBERS +@interface BSGMemoryFeatureFlagStore () + +@property(nonatomic, readwrite) NSMutableArray *flags; +@property(nonatomic, readwrite) NSMutableDictionary *indices; + +@end + +static const int REBUILD_AT_HOLE_COUNT = 1000; + +BSG_OBJC_DIRECT_MEMBERS +@implementation BSGMemoryFeatureFlagStore + ++ (nonnull BSGMemoryFeatureFlagStore *) fromJSON:(nonnull id)json { + BSGMemoryFeatureFlagStore *store = [BSGMemoryFeatureFlagStore new]; + if ([json isKindOfClass:[NSArray class]]) { + for (id item in json) { + if ([item isKindOfClass:[NSDictionary class]]) { + NSString *featureFlag = item[BSGKeyFeatureFlag]; + if ([featureFlag isKindOfClass:[NSString class]]) { + id variant = item[BSGKeyVariant]; + if (![variant isKindOfClass:[NSString class]]) { + variant = nil; + } + [store addFeatureFlag:featureFlag withVariant:variant]; + } + } + } + } + return store; +} + ++ (nonnull BSGMemoryFeatureFlagStore *) withFlags:(NSArray *)flags { + BSGMemoryFeatureFlagStore *store = [BSGMemoryFeatureFlagStore new]; + + for (BugsnagFeatureFlag *flag in flags) { + [store addFeatureFlag:flag.name withVariant:flag.variant]; + } + + return store; +} + +- (nonnull instancetype) init { + if ((self = [super init]) != nil) { + _flags = [NSMutableArray new]; + _indices = [NSMutableDictionary new]; + _isMainStore = NO; + } + return self; +} + +static inline int getIndexFromDict(NSDictionary *dict, NSString *name) { + NSNumber *boxedIndex = dict[name]; + if (boxedIndex == nil) { + return -1; + } + return boxedIndex.intValue; +} + +- (NSUInteger) count { + return self.indices.count; +} + +- (nonnull NSArray *) allFlags { + NSMutableArray *flags = [NSMutableArray arrayWithCapacity:self.indices.count]; + for (BugsnagFeatureFlag *flag in self.flags) { + if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) { + [flags addObject:flag]; + } + } + return flags; +} + +- (void)rebuildIfTooManyHoles { + int holeCount = (int)self.flags.count - (int)self.indices.count; + if (holeCount < REBUILD_AT_HOLE_COUNT) { + return; + } + + NSMutableArray *newFlags = [NSMutableArray arrayWithCapacity:self.indices.count]; + NSMutableDictionary *newIndices = [NSMutableDictionary new]; + for (BugsnagFeatureFlag *flag in self.flags) { + if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) { + [newFlags addObject:flag]; + } + } + + for (NSUInteger i = 0; i < newFlags.count; i++) { + BugsnagFeatureFlag *flag = newFlags[i]; + newIndices[flag.name] = @(i); + } + self.flags = newFlags; + self.indices = newIndices; +} + +- (void) addFeatureFlag:(nonnull NSString *)name withVariant:(nullable NSString *)variant { + BugsnagFeatureFlag *flag = [BugsnagFeatureFlag flagWithName:name variant:variant]; + + int index = getIndexFromDict(self.indices, name); + if (index >= 0) { + self.flags[(unsigned)index] = flag; + } else { + index = (int)self.flags.count; + [self.flags addObject:flag]; + self.indices[name] = @(index); + } + [self removeAtomicFeatureFlagWithName:name]; + [self addAtomicFeatureFlag: flag]; +} + +- (void) addFeatureFlags:(nonnull NSArray *)featureFlags { + for (BugsnagFeatureFlag *flag in featureFlags) { + [self addFeatureFlag:flag.name withVariant:flag.variant]; + } +} + +- (void) clear:(nullable NSString *)name { + if (name != nil) { + int index = getIndexFromDict(self.indices, name); + if (index >= 0) { + self.flags[(unsigned)index] = [NSNull null]; + [self.indices removeObjectForKey:(id)name]; + [self rebuildIfTooManyHoles]; + } + [self removeAtomicFeatureFlagWithName:name]; + } else { + [self.indices removeAllObjects]; + [self.flags removeAllObjects]; + [self clearAllAtomicFeatureFlags]; + } +} + +- (nonnull NSArray *) toJSON { + NSMutableArray *result = [NSMutableArray array]; + + for (BugsnagFeatureFlag *flag in self.flags) { + if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) { + if (flag.variant) { + [result addObject:@{BSGKeyFeatureFlag:flag.name, BSGKeyVariant:(NSString *_Nonnull)flag.variant}]; + } else { + [result addObject:@{BSGKeyFeatureFlag:flag.name}]; + } + } + } + return result; +} + +- (id)copyWithZone:(NSZone *)zone { + BSGMemoryFeatureFlagStore *store = [[BSGMemoryFeatureFlagStore allocWithZone:zone] init]; + store.flags = [self.flags mutableCopy]; + store.indices = [self.indices mutableCopy]; + return store; +} + +#pragma mark - Crash reporting + +- (void)awaitAtomicFeatureFlagsLockIfNeeded { + if (!self.isMainStore) { + return; + } + while (atomic_load(&g_writing_crash_report)) { continue; } +} + +- (void)addAtomicFeatureFlag:(BugsnagFeatureFlag *)flag { + @synchronized (self) { + if (!self.isMainStore) { + return; + } + [self awaitAtomicFeatureFlagsLockIfNeeded]; + NSData *data = [self dataForFeatureFlag:flag]; + struct bsg_feature_flag_list_item *head = atomic_load(&g_feature_flags_head); + struct bsg_feature_flag_list_item *tail = atomic_load(&g_feature_flags_tail); + struct bsg_feature_flag_list_item *newItem = calloc(1, sizeof(struct bsg_feature_flag_list_item) + data.length + 1); + if (!newItem) { + return; + } + [data getBytes:newItem->jsonData length:data.length]; + + if (head == NULL) { + atomic_store(&g_feature_flags_head, newItem); + } + if (tail) { + tail->next = newItem; + } + newItem->previous = tail; + atomic_store(&g_feature_flags_tail, newItem); + } +} + +- (NSData *)dataForFeatureFlag:(BugsnagFeatureFlag *)flag { + NSData *data = nil; + NSError *error = nil; + NSMutableDictionary *json = [NSMutableDictionary new]; + json[BSGKeyFeatureFlag] = flag.name; + if (flag.variant) { + json[BSGKeyVariant] = flag.variant; + } + if (!json || !(data = BSGJSONDataFromDictionary(json, &error))) { + bsg_log_err(@"Unable to serialize feature flag: %@", error); + } + return data; +} + +- (void)removeAtomicFeatureFlagWithName:(NSString *)name { + @synchronized (self) { + if (!self.isMainStore) { + return; + } + [self awaitAtomicFeatureFlagsLockIfNeeded]; + struct bsg_feature_flag_list_item *flag = [nameToFlag[name] pointerValue]; + if (flag) { + if (flag->previous) { + flag->previous->next = flag->next; + } + if (flag == atomic_load(&g_feature_flags_head)) { + atomic_store(&g_feature_flags_head, flag->next); + } + if (flag == atomic_load(&g_feature_flags_tail)) { + atomic_store(&g_feature_flags_tail, flag->previous); + } + free(flag); + } + } +} + +- (void)clearAllAtomicFeatureFlags { + @synchronized (self) { + if (!self.isMainStore) { + return; + } + [self awaitAtomicFeatureFlagsLockIfNeeded]; + struct bsg_feature_flag_list_item *item = atomic_exchange(&g_feature_flags_head, NULL); + while (item) { + struct bsg_feature_flag_list_item *next = item->next; + free(item); + item = next; + } + atomic_store(&g_feature_flags_tail, NULL); + } +} + +@end + +void BugsnagFeatureFlagsWriteCrashReport(const BSG_KSCrashReportWriter *writer, + bool __unused requiresAsyncSafety) { + atomic_store(&g_writing_crash_report, true); + + writer->beginArray(writer, "featureFlags"); + + struct bsg_feature_flag_list_item *item = atomic_load(&g_feature_flags_head); + while (item) { + writer->addJSONElement(writer, NULL, item->jsonData); + item = item->next; + } + + writer->endContainer(writer); + + atomic_store(&g_writing_crash_report, false); +} diff --git a/Bugsnag/FeatureFlags/BSGPersistentFeatureFlagStore.h b/Bugsnag/FeatureFlags/BSGPersistentFeatureFlagStore.h new file mode 100644 index 000000000..ff9d64907 --- /dev/null +++ b/Bugsnag/FeatureFlags/BSGPersistentFeatureFlagStore.h @@ -0,0 +1,32 @@ +// +// BSGPersistentFeatureFlagStore.h +// Bugsnag +// +// Created by Robert B on 25/11/2024. +// Copyright © 2024 Bugsnag. All rights reserved. +// + +#import "BugsnagInternals.h" +#import "BSGDefines.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +BSG_OBJC_DIRECT_MEMBERS +@interface BSGPersistentFeatureFlagStore: NSObject + +@property(nonatomic, nonnull, readonly) NSArray *allFlags; + +- (instancetype)initWithStorageDirectory:(NSString *)directory; + +- (void)addFeatureFlag:(nonnull NSString *)name withVariant:(nullable NSString *)variant; + +- (void)addFeatureFlags:(nonnull NSArray *)featureFlags; + +- (void)clear:(nullable NSString *)name; + +- (void)clear; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Bugsnag/FeatureFlags/BSGPersistentFeatureFlagStore.m b/Bugsnag/FeatureFlags/BSGPersistentFeatureFlagStore.m new file mode 100644 index 000000000..17d2c27f1 --- /dev/null +++ b/Bugsnag/FeatureFlags/BSGPersistentFeatureFlagStore.m @@ -0,0 +1,135 @@ +// +// BSGPersistentFeatureFlagStore.m +// Bugsnag +// +// Created by Robert B on 25/11/2024. +// Copyright © 2024 Bugsnag. All rights reserved. +// + +#import "BSGPersistentFeatureFlagStore.h" +#import "BugsnagInternals.h" +#import "BugsnagStoredFeatureFlag.h" +#import "BugsnagLogger.h" +#import + +BSG_OBJC_DIRECT_MEMBERS +@interface BSGPersistentFeatureFlagStore () + +@property(nonatomic, readwrite) uint64_t currentIndex; +@property(nonatomic, strong) NSString *directoryPath; + +@end + +BSG_OBJC_DIRECT_MEMBERS +@implementation BSGPersistentFeatureFlagStore + +- (nonnull instancetype) initWithStorageDirectory:(NSString *)directory { + if ((self = [super init]) != nil) { + _currentIndex = 0; + _directoryPath = directory; + } + return self; +} + +- (nonnull NSArray *)allFlags { + NSMutableArray *storedFlags = [NSMutableArray new]; + for (NSString *path in [self pathsForFlags]) { + NSData *data = [NSData dataWithContentsOfFile:path]; + NSError *error = nil; + NSDictionary *content = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; + if (error == nil) { + [storedFlags addObject:[BugsnagStoredFeatureFlag fromJSON:content]]; + } else { + bsg_log_err(@"Unable to decode feature flag: %@", error); + } + } + + [storedFlags sortUsingComparator:^NSComparisonResult(BugsnagStoredFeatureFlag * _Nonnull obj1, BugsnagStoredFeatureFlag * _Nonnull obj2) { + if (obj1.index == obj2.index) { + return NSOrderedSame; + } + return obj1.index < obj2.index ? NSOrderedAscending : NSOrderedDescending; + }]; + NSMutableArray *flags = [NSMutableArray new]; + for (BugsnagStoredFeatureFlag *storedFeatureFlag in storedFlags) { + BugsnagFeatureFlag *featureFlag = [BugsnagFeatureFlag flagWithName:storedFeatureFlag.name variant:storedFeatureFlag.variant]; + [flags addObject:featureFlag]; + } + return flags; +} + +- (void)addFeatureFlag:(nonnull NSString *)name withVariant:(nullable NSString *)variant { + @synchronized (self) { + NSString *path = [self pathForFlagWithName:name]; + BugsnagStoredFeatureFlag *flag = [[BugsnagStoredFeatureFlag alloc] initWithName:name + variant:variant + index:self.currentIndex]; + self.currentIndex += 1; + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:[flag toJson] options:kNilOptions error:&error]; + if (error == nil) { + [data writeToFile:path options:0 error:&error]; + if (error != nil) { + bsg_log_err(@"Unable to save feature flag: %@", error); + } + } else { + bsg_log_err(@"Unable to encode feature flag: %@", error); + } + } +} + +- (void)addFeatureFlags:(nonnull NSArray *)featureFlags { + for (BugsnagFeatureFlag *flag in featureFlags) { + [self addFeatureFlag:flag.name withVariant:flag.variant]; + } +} + +- (void)clear:(nullable NSString *)name { + @synchronized (self) { + [self deleteFile:[self pathForFlagWithName:name]]; + } +} + +- (void)clear { + @synchronized (self) { + NSArray *paths = [self pathsForFlags]; + for (NSString *path in paths) { + [self deleteFile:path]; + } + } +} + +- (NSArray *)pathsForFlags { + NSError *error = nil; + NSArray *paths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.directoryPath error:&error]; + if (error == nil) { + NSMutableArray *result = [NSMutableArray new]; + for (NSString *path in paths) { + [result addObject:[self.directoryPath stringByAppendingPathComponent:path]]; + } + return result; + } else { + bsg_log_err(@"Unable to get paths for feature flags: %@", error); + return @[]; + } +} + +- (NSString *)pathForFlagWithName:(NSString *)name { + return [self.directoryPath stringByAppendingPathComponent: [NSString stringWithFormat:@"%@.json", name]]; +} + +- (void)deleteFile:(NSString *)path { + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; + if (error != nil) { + bsg_log_err(@"Unable to delete a feature flag file: %@", error); + } +} + +- (id)copyWithZone:(NSZone *)zone { + BSGPersistentFeatureFlagStore *flags = [[BSGPersistentFeatureFlagStore allocWithZone:zone] initWithStorageDirectory:self.directoryPath]; + flags.currentIndex = self.currentIndex; + return flags; +} + +@end diff --git a/Bugsnag/FeatureFlags/BugsnagStoredFeatureFlag.h b/Bugsnag/FeatureFlags/BugsnagStoredFeatureFlag.h new file mode 100644 index 000000000..5c9672b4f --- /dev/null +++ b/Bugsnag/FeatureFlags/BugsnagStoredFeatureFlag.h @@ -0,0 +1,27 @@ +// +// BugsnagStoredFeatureFlag.h +// Bugsnag +// +// Created by Robert B on 25/11/2024. +// Copyright © 2024 Bugsnag. All rights reserved. +// + +#import +#import "BSGDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +BSG_OBJC_DIRECT_MEMBERS +@interface BugsnagStoredFeatureFlag: NSObject + +@property (nonatomic, strong) NSString *name; +@property (nonatomic, nullable, strong) NSString *variant; +@property (nonatomic) uint64_t index; + ++ (instancetype)fromJSON:(NSDictionary *)json; +- (instancetype)initWithName:(NSString *)name variant:(nullable NSString *)variant index:(uint64_t)index; +- (NSDictionary *)toJson; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Bugsnag/FeatureFlags/BugsnagStoredFeatureFlag.m b/Bugsnag/FeatureFlags/BugsnagStoredFeatureFlag.m new file mode 100644 index 000000000..ae7ee083d --- /dev/null +++ b/Bugsnag/FeatureFlags/BugsnagStoredFeatureFlag.m @@ -0,0 +1,43 @@ +// +// BugsnagStoredFeatureFlag.m +// Bugsnag +// +// Created by Robert B on 25/11/2024. +// Copyright © 2024 Bugsnag. All rights reserved. +// + +#import "BugsnagStoredFeatureFlag.h" + +@implementation BugsnagStoredFeatureFlag + ++ (instancetype)fromJSON:(NSDictionary *)json { + BugsnagStoredFeatureFlag *result = [self new]; + result.name = json[@"name"] ?: @""; + result.variant = json[@"variant"]; + result.index = [json[@"index"] unsignedLongLongValue]; + + return result; +} + +- (instancetype)initWithName:(NSString *)name variant:(nullable NSString *)variant index:(uint64_t)index +{ + self = [super init]; + if (self) { + self.name = name; + self.variant = variant; + self.index = index; + } + return self; +} + +- (NSDictionary *)toJson { + NSMutableDictionary *result = [NSMutableDictionary new]; + result[@"name"] = self.name; + result[@"index"] = @(self.index); + if (self.variant) { + result[@"variant"] = self.variant; + } + return result; +} + +@end diff --git a/Bugsnag/Helpers/BSGFeatureFlagStore.h b/Bugsnag/Helpers/BSGFeatureFlagStore.h deleted file mode 100644 index fbaf3a650..000000000 --- a/Bugsnag/Helpers/BSGFeatureFlagStore.h +++ /dev/null @@ -1,44 +0,0 @@ -// -// BSGFeatureFlagStore.h -// Bugsnag -// -// Created by Nick Dowell on 11/11/2021. -// Copyright © 2021 Bugsnag Inc. All rights reserved. -// - -#import "BugsnagInternals.h" -#import "BSGDefines.h" - -NS_ASSUME_NONNULL_BEGIN - -void BSGFeatureFlagStoreAddFeatureFlag(BSGFeatureFlagStore *store, NSString *name, NSString *_Nullable variant); - -void BSGFeatureFlagStoreAddFeatureFlags(BSGFeatureFlagStore *store, NSArray *featureFlags); - -void BSGFeatureFlagStoreClear(BSGFeatureFlagStore *store, NSString *_Nullable name); - -NSArray * BSGFeatureFlagStoreToJSON(BSGFeatureFlagStore *store); - -BSGFeatureFlagStore * BSGFeatureFlagStoreFromJSON(id _Nullable json); - - -BSG_OBJC_DIRECT_MEMBERS -@interface BSGFeatureFlagStore () - -@property(nonatomic,nonnull,readonly) NSArray * allFlags; - -+ (nonnull BSGFeatureFlagStore *) fromJSON:(nonnull id)json; - -- (NSUInteger) count; - -- (void) addFeatureFlag:(nonnull NSString *)name withVariant:(nullable NSString *)variant; - -- (void) addFeatureFlags:(nonnull NSArray *)featureFlags; - -- (void) clear:(nullable NSString *)name; - -- (nonnull NSArray *) toJSON; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Bugsnag/Helpers/BSGFeatureFlagStore.m b/Bugsnag/Helpers/BSGFeatureFlagStore.m deleted file mode 100644 index fd2ecaa77..000000000 --- a/Bugsnag/Helpers/BSGFeatureFlagStore.m +++ /dev/null @@ -1,182 +0,0 @@ -// -// BSGFeatureFlagStore.m -// Bugsnag -// -// Created by Nick Dowell on 11/11/2021. -// Copyright © 2021 Bugsnag Inc. All rights reserved. -// - -#import "BSGFeatureFlagStore.h" - -#import "BSGKeys.h" -#import "BugsnagFeatureFlag.h" - -void BSGFeatureFlagStoreAddFeatureFlag(BSGFeatureFlagStore *store, NSString *name, NSString *_Nullable variant) { - [store addFeatureFlag:name withVariant:variant]; -} - -void BSGFeatureFlagStoreAddFeatureFlags(BSGFeatureFlagStore *store, NSArray *featureFlags) { - [store addFeatureFlags:featureFlags]; -} - -void BSGFeatureFlagStoreClear(BSGFeatureFlagStore *store, NSString *_Nullable name) { - [store clear:name]; -} - -NSArray * BSGFeatureFlagStoreToJSON(BSGFeatureFlagStore *store) { - return [store toJSON]; -} - -BSGFeatureFlagStore * BSGFeatureFlagStoreFromJSON(id json) { - return [BSGFeatureFlagStore fromJSON:json]; -} - - -/** - * Stores feature flags as a dictionary containing the flag name as a key, with the - * value being the index into an array containing the complete feature flag. - * - * Removals leave holes in the array, which gets rebuilt on clear once there are too many holes. - * - * This gives the access speed of a dictionary while keeping ordering intact. - */ -BSG_OBJC_DIRECT_MEMBERS -@interface BSGFeatureFlagStore () - -@property(nonatomic, readwrite) NSMutableArray *flags; -@property(nonatomic, readwrite) NSMutableDictionary *indices; - -@end - -static const int REBUILD_AT_HOLE_COUNT = 1000; - -BSG_OBJC_DIRECT_MEMBERS -@implementation BSGFeatureFlagStore - -+ (nonnull BSGFeatureFlagStore *) fromJSON:(nonnull id)json { - BSGFeatureFlagStore *store = [BSGFeatureFlagStore new]; - if ([json isKindOfClass:[NSArray class]]) { - for (id item in json) { - if ([item isKindOfClass:[NSDictionary class]]) { - NSString *featureFlag = item[BSGKeyFeatureFlag]; - if ([featureFlag isKindOfClass:[NSString class]]) { - id variant = item[BSGKeyVariant]; - if (![variant isKindOfClass:[NSString class]]) { - variant = nil; - } - [store addFeatureFlag:featureFlag withVariant:variant]; - } - } - } - } - return store; -} - -- (nonnull instancetype) init { - if ((self = [super init]) != nil) { - _flags = [NSMutableArray new]; - _indices = [NSMutableDictionary new]; - } - return self; -} - -static inline int getIndexFromDict(NSDictionary *dict, NSString *name) { - NSNumber *boxedIndex = dict[name]; - if (boxedIndex == nil) { - return -1; - } - return boxedIndex.intValue; -} - -- (NSUInteger) count { - return self.indices.count; -} - -- (nonnull NSArray *) allFlags { - NSMutableArray *flags = [NSMutableArray arrayWithCapacity:self.indices.count]; - for (BugsnagFeatureFlag *flag in self.flags) { - if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) { - [flags addObject:flag]; - } - } - return flags; -} - -- (void)rebuildIfTooManyHoles { - int holeCount = (int)self.flags.count - (int)self.indices.count; - if (holeCount < REBUILD_AT_HOLE_COUNT) { - return; - } - - NSMutableArray *newFlags = [NSMutableArray arrayWithCapacity:self.indices.count]; - NSMutableDictionary *newIndices = [NSMutableDictionary new]; - for (BugsnagFeatureFlag *flag in self.flags) { - if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) { - [newFlags addObject:flag]; - } - } - - for (NSUInteger i = 0; i < newFlags.count; i++) { - BugsnagFeatureFlag *flag = newFlags[i]; - newIndices[flag.name] = @(i); - } - self.flags = newFlags; - self.indices = newIndices; -} - -- (void) addFeatureFlag:(nonnull NSString *)name withVariant:(nullable NSString *)variant { - BugsnagFeatureFlag *flag = [BugsnagFeatureFlag flagWithName:name variant:variant]; - - int index = getIndexFromDict(self.indices, name); - if (index >= 0) { - self.flags[(unsigned)index] = flag; - } else { - index = (int)self.flags.count; - [self.flags addObject:flag]; - self.indices[name] = @(index); - } -} - -- (void) addFeatureFlags:(nonnull NSArray *)featureFlags { - for (BugsnagFeatureFlag *flag in featureFlags) { - [self addFeatureFlag:flag.name withVariant:flag.variant]; - } -} - -- (void) clear:(nullable NSString *)name { - if (name != nil) { - int index = getIndexFromDict(self.indices, name); - if (index >= 0) { - self.flags[(unsigned)index] = [NSNull null]; - [self.indices removeObjectForKey:(id)name]; - [self rebuildIfTooManyHoles]; - } - } else { - [self.indices removeAllObjects]; - [self.flags removeAllObjects]; - } -} - -- (nonnull NSArray *) toJSON { - NSMutableArray *result = [NSMutableArray array]; - - for (BugsnagFeatureFlag *flag in self.flags) { - if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) { - if (flag.variant) { - [result addObject:@{BSGKeyFeatureFlag:flag.name, BSGKeyVariant:(NSString *_Nonnull)flag.variant}]; - } else { - [result addObject:@{BSGKeyFeatureFlag:flag.name}]; - } - } - } - return result; -} - -- (id)copyWithZone:(NSZone *)zone { - BSGFeatureFlagStore *store = [[BSGFeatureFlagStore allocWithZone:zone] init]; - store.flags = [self.flags mutableCopy]; - store.indices = [self.indices mutableCopy]; - return store; -} - -@end diff --git a/Bugsnag/Payload/BugsnagEvent+Private.h b/Bugsnag/Payload/BugsnagEvent+Private.h index 72d066094..cbe934dc2 100644 --- a/Bugsnag/Payload/BugsnagEvent+Private.h +++ b/Bugsnag/Payload/BugsnagEvent+Private.h @@ -7,7 +7,7 @@ // #import "BSGDefines.h" -#import "BSGFeatureFlagStore.h" +#import "BSGMemoryFeatureFlagStore.h" #import "BugsnagInternals.h" #import "BugsnagCorrelation.h" diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index 52dc3aacb..d77298452 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -9,7 +9,7 @@ #import "BugsnagEvent+Private.h" #import "BSGDefines.h" -#import "BSGFeatureFlagStore.h" +#import "BSGMemoryFeatureFlagStore.h" #import "BSGJSONSerialization.h" #import "BSGKeys.h" #import "BSGSerialization.h" @@ -33,6 +33,7 @@ #import "BugsnagStacktrace.h" #import "BugsnagThread+Private.h" #import "BugsnagUser+Private.h" +#import "BSGFileLocations.h" static NSString * const RedactedMetadataValue = @"[REDACTED]"; @@ -103,6 +104,20 @@ id BSGLoadConfigValue(NSDictionary *report, NSString *valueName) { return breadcrumbs; } +BSGMemoryFeatureFlagStore *BSGParseFeatureFlags(NSDictionary *report) { + // default to overwritten featureFlags from callback + NSArray *cache = [report valueForKeyPath:@"user.overrides.featureFlags"] + // then cached featureFlags from an OOM event + ?: [report valueForKeyPath:@"user.state.oom.featureFlags"] + // then cached featureFlags from a regular event + // KSCrashReports from earlier versions of the notifier used this + ?: [report valueForKeyPath:@"user.state.crash.featureFlags"] + // featureFlags added to a KSCrashReport by BSSerializeDataCrashHandler + ?: [report valueForKeyPath:@"user.featureFlags"]; + + return BSGFeatureFlagStoreFromJSON(cache); +} + NSString *BSGParseReleaseStage(NSDictionary *report) { return [report valueForKeyPath:@"user.overrides.releaseStage"] ?: BSGLoadConfigValue(report, @"releaseStage"); @@ -167,7 +182,7 @@ - (instancetype)initWithApp:(BugsnagAppWithState *)app _metadata = metadata; _breadcrumbs = breadcrumbs; _errors = errors; - _featureFlagStore = [[BSGFeatureFlagStore alloc] init]; + _featureFlagStore = [[BSGMemoryFeatureFlagStore alloc] init]; _threads = threads; _session = [session copy]; } @@ -400,7 +415,7 @@ - (instancetype)initWithKSCrashReport:(NSDictionary *)event { obj.enabledReleaseStages = BSGLoadConfigValue(event, BSGKeyEnabledReleaseStages); obj.releaseStage = BSGParseReleaseStage(event); obj.deviceAppHash = deviceAppHash; - obj.featureFlagStore = BSGFeatureFlagStoreFromJSON([event valueForKeyPath:@"user.state.client.featureFlags"]); + obj.featureFlagStore = BSGParseFeatureFlags(event); obj.context = [event valueForKeyPath:@"user.state.client.context"]; obj.customException = BSGParseCustomException(event, [errors[0].errorClass copy], [errors[0].errorMessage copy]); obj.depth = depth; @@ -429,7 +444,7 @@ - (instancetype)initWithUserData:(NSDictionary *)crashReport { } _apiKey = BSGDeserializeString(json[BSGKeyApiKey]); _context = BSGDeserializeString(json[BSGKeyContext]); - _featureFlagStore = [[BSGFeatureFlagStore alloc] init]; + _featureFlagStore = [[BSGMemoryFeatureFlagStore alloc] init]; _groupingHash = BSGDeserializeString(json[BSGKeyGroupingHash]); if (_errors.count) { diff --git a/Bugsnag/Storage/BSGFileLocations.h b/Bugsnag/Storage/BSGFileLocations.h index 10920b7ed..f61ce1a60 100644 --- a/Bugsnag/Storage/BSGFileLocations.h +++ b/Bugsnag/Storage/BSGFileLocations.h @@ -19,6 +19,7 @@ BSG_OBJC_DIRECT_MEMBERS @property (readonly, nonatomic) NSString *events; @property (readonly, nonatomic) NSString *kscrashReports; @property (readonly, nonatomic) NSString *sessions; +@property (readonly, nonatomic) NSString *featureFlags; /** * File containing details of the current app hang (if the app is hung) @@ -40,7 +41,6 @@ BSG_OBJC_DIRECT_MEMBERS * General per-launch metadata */ @property (readonly, nonatomic) NSString *metadata; - /** * BSGRunContext */ diff --git a/Bugsnag/Storage/BSGFileLocations.m b/Bugsnag/Storage/BSGFileLocations.m index 3b49bcfa4..3b2eb48a6 100644 --- a/Bugsnag/Storage/BSGFileLocations.m +++ b/Bugsnag/Storage/BSGFileLocations.m @@ -119,6 +119,7 @@ - (instancetype)initWithVersion1 { _sessions = getAndCreateSubdir(root, @"sessions"); _breadcrumbs = getAndCreateSubdir(root, @"breadcrumbs"); _kscrashReports = getAndCreateSubdir(root, @"KSCrashReports"); + _featureFlags = getAndCreateSubdir(root, @"featureFlags"); _appHangEvent = [root stringByAppendingPathComponent:@"app_hang.json"]; _flagHandledCrash = [root stringByAppendingPathComponent:@"bugsnag_handled_crash.txt"]; _configuration = [root stringByAppendingPathComponent:@"config.json"]; diff --git a/Tests/BugsnagTests/BSGFeatureFlagStoreTests.m b/Tests/BugsnagTests/BSGFeatureFlagStoreTests.m index 296f8cfd5..6efce450c 100644 --- a/Tests/BugsnagTests/BSGFeatureFlagStoreTests.m +++ b/Tests/BugsnagTests/BSGFeatureFlagStoreTests.m @@ -8,7 +8,7 @@ #import "BSGTestCase.h" -#import "BSGFeatureFlagStore.h" +#import "BSGMemoryFeatureFlagStore.h" @interface BSGFeatureFlagStoreTests : BSGTestCase @@ -17,7 +17,7 @@ @interface BSGFeatureFlagStoreTests : BSGTestCase @implementation BSGFeatureFlagStoreTests - (void)test { - BSGFeatureFlagStore *store = [[BSGFeatureFlagStore alloc] init]; + BSGMemoryFeatureFlagStore *store = [[BSGMemoryFeatureFlagStore alloc] init]; XCTAssertEqualObjects(BSGFeatureFlagStoreToJSON(store), @[]); BSGFeatureFlagStoreAddFeatureFlag(store, @"featureC", @"checked"); @@ -65,7 +65,7 @@ - (void)test { - (void)testAddRemoveMany { // Tests that rebuildIfTooManyHoles works as expected - BSGFeatureFlagStore *store = [[BSGFeatureFlagStore alloc] init]; + BSGMemoryFeatureFlagStore *store = [[BSGMemoryFeatureFlagStore alloc] init]; BSGFeatureFlagStoreAddFeatureFlag(store, @"blah", @"testing"); for (int j = 0; j < 10; j++) { @@ -95,7 +95,7 @@ - (void)testAddRemoveMany { } - (void)testAddFeatureFlagPerformance { - BSGFeatureFlagStore *store = [[BSGFeatureFlagStore alloc] init]; + BSGMemoryFeatureFlagStore *store = [[BSGMemoryFeatureFlagStore alloc] init]; __auto_type block = ^{ for (int i = 0; i < 1000; i++) { diff --git a/Tests/BugsnagTests/BugsnagClientMirrorTest.m b/Tests/BugsnagTests/BugsnagClientMirrorTest.m index cd5b592e5..5eac593d9 100644 --- a/Tests/BugsnagTests/BugsnagClientMirrorTest.m +++ b/Tests/BugsnagTests/BugsnagClientMirrorTest.m @@ -133,6 +133,8 @@ - (void)setUp { @"thermalStateDidChange: v24@0:8@16", @"updateSession: v24@0:8@?16", @"notifyErrorOrException:stackStripDepth:block: v40@0:8@16Q24@?32", + @"setFeatureFlags: v24@0:8@16", + @"featureFlags @16@0:8" ]]; // the following methods are implemented on Bugsnag but do not need to diff --git a/features/fixtures/shared/scenarios/OOMSessionlessScenario.m b/features/fixtures/shared/scenarios/OOMSessionlessScenario.m index 8b4377404..1f71f34d5 100644 --- a/features/fixtures/shared/scenarios/OOMSessionlessScenario.m +++ b/features/fixtures/shared/scenarios/OOMSessionlessScenario.m @@ -22,8 +22,11 @@ - (void)configure { } - (void)run { + [Bugsnag addFeatureFlagWithName:@"Feature Flag1" variant: @"Variant1"]; // Allow time for state metadata to be flushed to disk dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [Bugsnag addFeatureFlagWithName:@"Feature Flag2" variant: @"Variant2"]; + [Bugsnag addFeatureFlagWithName:@"Feature Flag3"]; // Fake an OOM kill(getpid(), SIGKILL); }); diff --git a/features/release/out_of_memory.feature b/features/release/out_of_memory.feature index c9505f883..09cc9a7a2 100644 --- a/features/release/out_of_memory.feature +++ b/features/release/out_of_memory.feature @@ -104,6 +104,13 @@ Feature: Out of memory errors And I wait to receive an error Then the error is an OOM event And the event "user.id" is not null + And the error payload field "events.0.featureFlags" is an array with 3 elements + And the error payload field "events.0.featureFlags.0.featureFlag" equals "Feature Flag1" + And the error payload field "events.0.featureFlags.0.variant" equals "Variant1" + And the error payload field "events.0.featureFlags.1.featureFlag" equals "Feature Flag2" + And the error payload field "events.0.featureFlags.1.variant" equals "Variant2" + And the error payload field "events.0.featureFlags.2.featureFlag" equals "Feature Flag3" + And the error payload field "events.0.featureFlags.2.variant" is null Scenario: Out of memory errors are not reported from UIApplicationStateInactive Given I run "OOMInactiveScenario" and relaunch the crashed app