From 572f1dd985ccc6e10b3eb233bcc0336f0ab555a3 Mon Sep 17 00:00:00 2001 From: Karl Stenerud Date: Fri, 22 Dec 2023 13:01:38 +0100 Subject: [PATCH] Add support for deferring view load span end --- Example/Example/SomeView.swift | 49 +++++++- Gemfile.lock | 12 +- ...nagPerformanceSwiftUIInstrumentation.swift | 109 ++++++++++++---- features/default/automatic_spans.feature | 117 +++++++++++++++++- .../ios/Fixture.xcodeproj/project.pbxproj | 4 + ...utoInstrumentSwiftUIDeferredScenario.swift | 72 +++++++++++ .../AutoInstrumentSwiftUIScenario.swift | 38 ++++-- features/steps/app_steps.rb | 6 + 8 files changed, 362 insertions(+), 45 deletions(-) create mode 100644 features/fixtures/ios/Scenarios/AutoInstrumentSwiftUIDeferredScenario.swift diff --git a/Example/Example/SomeView.swift b/Example/Example/SomeView.swift index 2858afa0..86f3ef9d 100644 --- a/Example/Example/SomeView.swift +++ b/Example/Example/SomeView.swift @@ -11,13 +11,50 @@ import SwiftUI @available(iOS 13.0.0, *) struct SomeView: View { + @State var data: Data? + @State var deferringViewLoadSpan = true + var body: some View { - let span = BugsnagPerformance.startViewLoadSpan( - name: "SomeView", viewType: .swiftUI) - Text("Hello from SwiftUI 🙃") - .onAppear { - span.end() - }.bugsnagTraced("My text view") + + // Fake a background data load. + // On iOS 15+ you'd use a .task() + defer { + DispatchQueue.global().async { + data = Data() + } + } + + return VStack { + if data == nil { + // .bugsnagDeferEndUntilViewDisappears() will hold the current + // view load span open until this Text view disappears. + // + // When this Text view disappears, its defer is resolved. + // That would in theory leave the view load span free to end, + // but there are still other defers to resolve! + Text("SwiftUI is loading") + .bugsnagTraced("loading") + .bugsnagDeferEndUntilViewDisappears() + } else { + Text("Hello from SwiftUI 🙃") + .bugsnagTraced("loaded") + + if deferringViewLoadSpan { + // Defer the view load span end until this button disappears. + Button("Stop deferring the view load span") { + deferringViewLoadSpan = false + } + .bugsnagDeferEndUntilViewDisappears() + } + } + } + .bugsnagDeferEndUntil { + // Defer the view load span end until this function returns a true value. + // + // This is technically redundant since the above button is deferring the + // view load span based on the same @State value (deferringViewLoadSpan). + return !deferringViewLoadSpan + } } } diff --git a/Gemfile.lock b/Gemfile.lock index ea4b7e14..7a74b3ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -80,19 +80,19 @@ GEM uri_template (~> 0.7) mime-types (3.5.1) mime-types-data (~> 3.2015) - mime-types-data (3.2023.1003) + mime-types-data (3.2023.1205) multi_test (0.1.2) - nokogiri (1.15.4-arm64-darwin) + nokogiri (1.15.5-arm64-darwin) racc (~> 1.4) - nokogiri (1.15.4-x86_64-darwin) + nokogiri (1.15.5-x86_64-darwin) racc (~> 1.4) optimist (3.0.1) os (1.0.1) power_assert (2.0.3) - racc (1.7.1) + racc (1.7.3) rack (2.2.8) rake (12.3.3) - regexp_parser (2.8.2) + regexp_parser (2.8.3) rexml (3.2.6) rubyzip (2.3.2) selenium-webdriver (4.5.0) @@ -109,7 +109,7 @@ GEM tomlrb (2.0.3) unf (0.1.4) unf_ext - unf_ext (0.0.8.2) + unf_ext (0.0.9.1) uri_template (0.7.0) webrick (1.7.0) websocket (1.2.10) diff --git a/Sources/BugsnagPerformanceSwiftUI/BugsnagPerformanceSwiftUIInstrumentation.swift b/Sources/BugsnagPerformanceSwiftUI/BugsnagPerformanceSwiftUIInstrumentation.swift index 08bab7f1..e4d77b17 100644 --- a/Sources/BugsnagPerformanceSwiftUI/BugsnagPerformanceSwiftUIInstrumentation.swift +++ b/Sources/BugsnagPerformanceSwiftUI/BugsnagPerformanceSwiftUIInstrumentation.swift @@ -11,7 +11,9 @@ import BugsnagPerformance @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6.0, *) final class BugsnagViewContext: ObservableObject { - public var firstViewLoadSpan: BugsnagPerformanceSpan? = nil + public var isFirstBodyThisCycle = true + public var parentViewLoadSpan: BugsnagPerformanceSpan? = nil + public var unresolvedDeferCount = 0 } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6.0, *) @@ -36,53 +38,110 @@ extension View { @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6.0, *) public extension View { + + /** + * Trace this view through Bugsnag. + * If viewName is not specified, one will be generated based on the struct's class. + */ func bugsnagTraced(_ viewName: String? = nil) -> some View { return BugsnagTracedView(viewName) { return self } } + + /** + * Defer ending the overarching view load span until the supplied function returns true during a body build cycle. + * The view load span will not be ended until ALL deferred-end conditions are true. + */ + func bugsnagDeferEndUntil(deferUntil: @escaping ()->(Bool)) -> some View { + return BugsnagDeferredTraceEndView(deferUntil: deferUntil) {self} + } + + /** + * Defer ending the overarching view load span until this view disappears from the view hierarchy. + * The view load span will not be ended until ALL deferred-end conditions are true. + */ + func bugsnagDeferEndUntilViewDisappears() -> some View { + return self.bugsnagDeferEndUntil { + return false + } + } +} + +private func generateNameFromContent(content: Any) -> String { + let viewName = String(describing: content) + if let angleBracketIndex = viewName.firstIndex(of: "<") { + return String(viewName[viewName.startIndex ..< angleBracketIndex]) + } + return viewName } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6.0, *) -public struct BugsnagTracedView: View { +public struct BugsnagDeferredTraceEndView: View { @Environment(\.keyBSGViewContext) private var bsgViewContext: BugsnagViewContext - let content: () -> Content - let name: String + private let content: () -> Content + private let deferUntilCondition: ()->(Bool) - public init(_ viewName: String? = nil, content: @escaping () -> Content) { + public init(deferUntil: @escaping ()->(Bool), content: @escaping () -> Content) { self.content = content - self.name = viewName ?? BugsnagTracedView.getViewName(content: Content.self) + self.deferUntilCondition = deferUntil } - private static func getViewName(content: Any) -> String { - let viewName = String(describing: content) - if let angleBracketIndex = viewName.firstIndex(of: "<") { - return String(viewName[viewName.startIndex ..< angleBracketIndex]) + public var body: some View { + if !deferUntilCondition() { + bsgViewContext.unresolvedDeferCount += 1 } - return viewName + + // We're not generating our own content; merely passing through the content + // of the body we wrapped. The rendered scene will not contain any Bugsnag views. + return content() + } +} + +@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6.0, *) +public struct BugsnagTracedView: View { + @Environment(\.keyBSGViewContext) private var bsgViewContext: BugsnagViewContext + + private let content: () -> Content + private let name: String + + public init(_ viewName: String? = nil, content: @escaping () -> Content) { + self.content = content + self.name = viewName ?? generateNameFromContent(content: Content.self) } public var body: some View { - var firstViewLoadSpan = bsgViewContext.firstViewLoadSpan - - if firstViewLoadSpan == nil { - let opts = BugsnagPerformanceSpanOptions() - opts.setParentContext(nil) - let thisViewLoadSpan = BugsnagPerformance.startViewLoadSpan(name: name, viewType: BugsnagPerformanceViewType.swiftUI, options: opts) - firstViewLoadSpan = thisViewLoadSpan - bsgViewContext.firstViewLoadSpan = thisViewLoadSpan + var parentViewLoadSpan = bsgViewContext.parentViewLoadSpan + + if bsgViewContext.isFirstBodyThisCycle { + bsgViewContext.isFirstBodyThisCycle = false + + if parentViewLoadSpan == nil { + let opts = BugsnagPerformanceSpanOptions() + opts.setParentContext(nil) + let thisViewLoadSpan = BugsnagPerformance.startViewLoadSpan(name: name, viewType: BugsnagPerformanceViewType.swiftUI, options: opts) + parentViewLoadSpan = thisViewLoadSpan + bsgViewContext.parentViewLoadSpan = thisViewLoadSpan + } + DispatchQueue.main.async { - thisViewLoadSpan.end() + if bsgViewContext.unresolvedDeferCount == 0 { + bsgViewContext.parentViewLoadSpan?.end() + bsgViewContext.parentViewLoadSpan = nil + } + bsgViewContext.unresolvedDeferCount = 0 + bsgViewContext.isFirstBodyThisCycle = true } } let opts = BugsnagPerformanceSpanOptions() - opts.setParentContext(firstViewLoadSpan) - let thisViewLoadSpan = BugsnagPerformance.startViewLoadPhaseSpan(name: name, phase: "body", parentContext: firstViewLoadSpan!) - defer { - thisViewLoadSpan.end() - } + opts.setParentContext(parentViewLoadSpan) + // We are actually recording a point here, not measuring a duration. + BugsnagPerformance.startViewLoadPhaseSpan(name: name, phase: "body", parentContext: parentViewLoadSpan!).end() + + // We're not generating our own content; merely passing through the content + // of the body we wrapped. The rendered scene will not contain any Bugsnag views. return content() } } diff --git a/features/default/automatic_spans.feature b/features/default/automatic_spans.feature index 1b3ea9ba..35d524f9 100644 --- a/features/default/automatic_spans.feature +++ b/features/default/automatic_spans.feature @@ -248,7 +248,7 @@ Feature: Automatic instrumentation spans * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.name" equals "bugsnag.performance.cocoa" * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.version" matches the regex "[0-9]\.[0-9]\.[0-9]" - Scenario: AutoInstrumentSwiftUIScenario + Scenario: AutoInstrumentSwiftUIScenario no change Given I run "AutoInstrumentSwiftUIScenario" And I wait for 3 spans Then the trace "Content-Type" header equals "application/json" @@ -272,6 +272,121 @@ Feature: Automatic instrumentation spans * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.name" equals "bugsnag.performance.cocoa" * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.version" matches the regex "[0-9]\.[0-9]\.[0-9]" + Scenario: AutoInstrumentSwiftUIScenario with change + Given I run "AutoInstrumentSwiftUIScenario" + And I wait for 3 spans + Then the trace "Content-Type" header equals "application/json" + * the trace "Bugsnag-Sent-At" header matches the regex "^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ$" + * a span field "name" equals "[ViewLoad/SwiftUI]/My VStack view" + * a span field "name" equals "[ViewLoadPhase/body]/My VStack view" + * a span field "name" equals "[ViewLoadPhase/body]/My Image view" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "kind" equals 1 + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * a span string attribute "bugsnag.span.category" equals "view_load" + * a span string attribute "bugsnag.span.category" equals "view_load_phase" + * a span string attribute "bugsnag.view.name" equals "My VStack view" + * a span string attribute "bugsnag.view.name" equals "My Image view" + * a span bool attribute "bugsnag.span.first_class" is true + * a span string attribute "bugsnag.view.type" equals "SwiftUI" + * the trace payload field "resourceSpans.0.resource" string attribute "service.name" equals "com.bugsnag.fixtures.PerformanceFixture" + * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.name" equals "bugsnag.performance.cocoa" + * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.version" matches the regex "[0-9]\.[0-9]\.[0-9]" + And I discard every trace + And I invoke "switchView" + Then I wait for 2 spans + * a span field "name" equals "[ViewLoad/SwiftUI]/Text" + * a span field "name" equals "[ViewLoadPhase/body]/Text" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "kind" equals 1 + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * a span string attribute "bugsnag.span.category" equals "view_load" + * a span string attribute "bugsnag.span.category" equals "view_load_phase" + * every span string attribute "bugsnag.view.name" equals "Text" + * a span bool attribute "bugsnag.span.first_class" is true + * a span string attribute "bugsnag.view.type" equals "SwiftUI" + + Scenario: AutoInstrumentSwiftUIDeferredScenario toggleEndSpanDefer + Given I run "AutoInstrumentSwiftUIDeferredScenario" + And I wait for 2 spans + Then the trace "Content-Type" header equals "application/json" + * the trace "Bugsnag-Sent-At" header matches the regex "^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ$" + * a span field "name" equals "[ViewLoadPhase/body]/vstack1" + * a span field "name" equals "[ViewLoadPhase/body]/text1" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "kind" equals 1 + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * every span string attribute "bugsnag.span.category" equals "view_load_phase" + * a span string attribute "bugsnag.view.name" equals "vstack1" + * a span string attribute "bugsnag.view.name" equals "text1" + * the trace payload field "resourceSpans.0.resource" string attribute "service.name" equals "com.bugsnag.fixtures.PerformanceFixture" + * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.name" equals "bugsnag.performance.cocoa" + * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.version" matches the regex "[0-9]\.[0-9]\.[0-9]" + Then I discard every trace + And I invoke "toggleEndSpanDefer" + And I wait for 2 spans + * a span field "name" equals "[ViewLoadPhase/body]/vstack1" + * a span field "name" equals "[ViewLoadPhase/body]/text1" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "kind" equals 1 + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * every span string attribute "bugsnag.span.category" equals "view_load_phase" + * a span string attribute "bugsnag.view.name" equals "vstack1" + * a span string attribute "bugsnag.view.name" equals "text1" + + Scenario: AutoInstrumentSwiftUIDeferredScenario toggle everything + Given I run "AutoInstrumentSwiftUIDeferredScenario" + And I wait for 2 spans + Then the trace "Content-Type" header equals "application/json" + * the trace "Bugsnag-Sent-At" header matches the regex "^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ$" + * a span field "name" equals "[ViewLoadPhase/body]/vstack1" + * a span field "name" equals "[ViewLoadPhase/body]/text1" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "kind" equals 1 + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * every span string attribute "bugsnag.span.category" equals "view_load_phase" + * a span string attribute "bugsnag.view.name" equals "vstack1" + * a span string attribute "bugsnag.view.name" equals "text1" + * the trace payload field "resourceSpans.0.resource" string attribute "service.name" equals "com.bugsnag.fixtures.PerformanceFixture" + * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.name" equals "bugsnag.performance.cocoa" + * the trace payload field "resourceSpans.0.resource" string attribute "telemetry.sdk.version" matches the regex "[0-9]\.[0-9]\.[0-9]" + Then I discard every trace + And I invoke "toggleEndSpanDefer" + And I wait for 2 spans + * a span field "name" equals "[ViewLoadPhase/body]/vstack1" + * a span field "name" equals "[ViewLoadPhase/body]/text1" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "kind" equals 1 + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * every span string attribute "bugsnag.span.category" equals "view_load_phase" + * a span string attribute "bugsnag.view.name" equals "vstack1" + * a span string attribute "bugsnag.view.name" equals "text1" + Then I discard every trace + And I invoke "toggleHideText1" + And I wait for 2 spans + * a span field "name" equals "[ViewLoad/SwiftUI]/vstack1" + * a span field "name" equals "[ViewLoadPhase/body]/vstack1" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "kind" equals 1 + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * a span string attribute "bugsnag.span.category" equals "view_load" + * a span string attribute "bugsnag.span.category" equals "view_load_phase" + * every span string attribute "bugsnag.view.name" equals "vstack1" + Scenario: Automatically start a network span that has a parent Given I run "AutoInstrumentNetworkWithParentScenario" And I wait for 2 seconds diff --git a/features/fixtures/ios/Fixture.xcodeproj/project.pbxproj b/features/fixtures/ios/Fixture.xcodeproj/project.pbxproj index d82988a8..26fad82d 100644 --- a/features/fixtures/ios/Fixture.xcodeproj/project.pbxproj +++ b/features/fixtures/ios/Fixture.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 09637A412B060E7C00F4F776 /* CommandReaderThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09637A402B060E7C00F4F776 /* CommandReaderThread.swift */; }; 09637A432B0617FE00F4F776 /* MazeRunnerCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09637A422B0617FE00F4F776 /* MazeRunnerCommand.swift */; }; 09637A452B0B883B00F4F776 /* FixtureConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09637A442B0B883B00F4F776 /* FixtureConfig.swift */; }; + 096EE5EF2B3C2B3E006059CE /* AutoInstrumentSwiftUIDeferredScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096EE5EE2B3C2B3E006059CE /* AutoInstrumentSwiftUIDeferredScenario.swift */; }; 0983A1792B14B20C00DDF4FF /* AutoInstrumentSwiftUIScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0983A1782B14B20C00DDF4FF /* AutoInstrumentSwiftUIScenario.swift */; }; 0983A17B2B14BB2000DDF4FF /* BugsnagPerformanceSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0983A17A2B14BB2000DDF4FF /* BugsnagPerformanceSwiftUI */; }; 098808E02B10A6E400DC1677 /* ManualViewLoadPhaseScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 098808DF2B10A6E400DC1677 /* ManualViewLoadPhaseScenario.swift */; }; @@ -82,6 +83,7 @@ 09637A402B060E7C00F4F776 /* CommandReaderThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandReaderThread.swift; sourceTree = ""; }; 09637A422B0617FE00F4F776 /* MazeRunnerCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MazeRunnerCommand.swift; sourceTree = ""; }; 09637A442B0B883B00F4F776 /* FixtureConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixtureConfig.swift; sourceTree = ""; }; + 096EE5EE2B3C2B3E006059CE /* AutoInstrumentSwiftUIDeferredScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoInstrumentSwiftUIDeferredScenario.swift; sourceTree = ""; }; 0983A1782B14B20C00DDF4FF /* AutoInstrumentSwiftUIScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoInstrumentSwiftUIScenario.swift; sourceTree = ""; }; 098808DF2B10A6E400DC1677 /* ManualViewLoadPhaseScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualViewLoadPhaseScenario.swift; sourceTree = ""; }; 09DA59A42A6E866B00A06EEE /* ManualNetworkCallbackScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualNetworkCallbackScenario.swift; sourceTree = ""; }; @@ -193,6 +195,7 @@ CBE0872A29F81BBB007455F2 /* AutoInstrumentNetworkNoParentScenario.swift */, CB3477172901481F0033759C /* AutoInstrumentNetworkWithParentScenario.swift */, CBC90CE329D1BDE400280884 /* AutoInstrumentSubViewLoadScenario.swift */, + 096EE5EE2B3C2B3E006059CE /* AutoInstrumentSwiftUIDeferredScenario.swift */, 0983A1782B14B20C00DDF4FF /* AutoInstrumentSwiftUIScenario.swift */, 9657A8982A3CF75B001CEF5D /* AutoInstrumentTabViewLoadScenario.swift */, 0185C47128F6C983006F9BDC /* AutoInstrumentViewLoadScenario.swift */, @@ -319,6 +322,7 @@ 01FE4DAD28E1AEBD00D1F239 /* ViewController.swift in Sources */, CBEC89232A458BA70088A3CE /* AutoInstrumentNetworkMultiple.swift in Sources */, 01FE4DA928E1AEBD00D1F239 /* AppDelegate.swift in Sources */, + 096EE5EF2B3C2B3E006059CE /* AutoInstrumentSwiftUIDeferredScenario.swift in Sources */, CBAAE2592912601D006D4AA0 /* BatchingScenario.swift in Sources */, 09637A3C2B0607F300F4F776 /* Logging.swift in Sources */, 9657A89B2A3D06EB001CEF5D /* AutoInstrumentNavigationViewLoadScenario.swift in Sources */, diff --git a/features/fixtures/ios/Scenarios/AutoInstrumentSwiftUIDeferredScenario.swift b/features/fixtures/ios/Scenarios/AutoInstrumentSwiftUIDeferredScenario.swift new file mode 100644 index 00000000..3d0b44ca --- /dev/null +++ b/features/fixtures/ios/Scenarios/AutoInstrumentSwiftUIDeferredScenario.swift @@ -0,0 +1,72 @@ +// +// AutoInstrumentSwiftUIDeferredScenario.swift +// Fixture +// +// Created by Karl Stenerud on 27.12.23. +// + +import SwiftUI +import BugsnagPerformance +import BugsnagPerformanceSwiftUI + +@objcMembers +class AutoInstrumentSwiftUIDeferredScenario: Scenario { + var view = AutoInstrumentSwiftUIDeferredScenario_View(model: AutoInstrumentSwiftUIDeferredModel()) + + override func run() { + if #available(iOS 13.0.0, *) { + UIApplication.shared.windows[0].rootViewController!.present( + UIHostingController(rootView: view), animated: true) + } else { + fatalError("SwiftUI is not available on this version of iOS") + } + } + + func toggleHideText1() { + self.view.toggleHideText1() + } + + func toggleEndSpanDefer() { + self.view.toggleEndSpanDefer() + } +} + +class AutoInstrumentSwiftUIDeferredModel: ObservableObject { + @Published var shouldShowText1: Bool = true + @Published var shouldDeferEndSpan: Bool = true + + func toggleHideText1() { + shouldShowText1.toggle() + } + + func toggleEndSpanDefer() { + shouldDeferEndSpan.toggle() + } +} + +@available(iOS 13.0.0, *) +struct AutoInstrumentSwiftUIDeferredScenario_View: View { + @ObservedObject var model : AutoInstrumentSwiftUIDeferredModel + + func toggleHideText1() { + DispatchQueue.main.async { self.model.toggleHideText1() } + } + + func toggleEndSpanDefer() { + DispatchQueue.main.async { self.model.toggleEndSpanDefer() } + } + + var body: some View { + VStack { + if model.shouldShowText1 { + Text("Text 1") + .bugsnagTraced("text1") + .bugsnagDeferEndUntilViewDisappears() + } + } + .bugsnagTraced("vstack1") + .bugsnagDeferEndUntil { + return !model.shouldDeferEndSpan + } + } +} diff --git a/features/fixtures/ios/Scenarios/AutoInstrumentSwiftUIScenario.swift b/features/fixtures/ios/Scenarios/AutoInstrumentSwiftUIScenario.swift index 852c1da6..47479d97 100644 --- a/features/fixtures/ios/Scenarios/AutoInstrumentSwiftUIScenario.swift +++ b/features/fixtures/ios/Scenarios/AutoInstrumentSwiftUIScenario.swift @@ -11,25 +11,49 @@ import BugsnagPerformanceSwiftUI @objcMembers class AutoInstrumentSwiftUIScenario: Scenario { + var view = AutoInstrumentSwiftUIScenario_View(model: AutoInstrumentSwiftUIModel()) + override func run() { if #available(iOS 13.0.0, *) { UIApplication.shared.windows[0].rootViewController!.present( - UIHostingController(rootView: AutoInstrumentSwiftUIScenario_View()), animated: true) + UIHostingController(rootView: view), animated: true) } else { fatalError("SwiftUI is not available on this version of iOS") } } + + func switchView() { + self.view.switchView() + } +} + +class AutoInstrumentSwiftUIModel: ObservableObject { + @Published var shouldSwitchViews: Bool = false + + func switchViews() { + shouldSwitchViews.toggle() + } } @available(iOS 13.0.0, *) struct AutoInstrumentSwiftUIScenario_View: View { + @ObservedObject var model : AutoInstrumentSwiftUIModel + + func switchView() { + DispatchQueue.main.async { self.model.switchViews() } + } + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .bugsnagTraced("My Image view") + if !model.shouldSwitchViews { + return AnyView(VStack { + Image(systemName: "globe") + .imageScale(.large) + .bugsnagTraced("My Image view") + } + .bugsnagTraced("My VStack view") + .padding()) + } else { + return AnyView(Text("Switched").bugsnagTraced("Text")) } - .bugsnagTraced("My VStack view") - .padding() } } diff --git a/features/steps/app_steps.rb b/features/steps/app_steps.rb index d2ddd26e..f4cbbe43 100644 --- a/features/steps/app_steps.rb +++ b/features/steps/app_steps.rb @@ -20,6 +20,12 @@ def skip_between(os, version_lo, version_hi) skip_above('ios', 14.99) end +Then('I discard every {request_type}') do |request_type| + until Maze::Server.list_for(request_type).current.nil? + Maze::Server.list_for(request_type).next + end +end + When('I run {string}') do |scenario_name| Maze::Server.commands.add({ action: "run_scenario", args: [scenario_name] }) # Ensure fixture has read the command