diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 367d18b1..ff8e1a6e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,13 +10,13 @@ jobs: runs-on: macos-latest steps: - name: Checkout target branch - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.base_ref }} - name: Build run: xcodebuild -scheme BugsnagPerformance-iOS -destination generic/platform=iOS -configuration Release -quiet -derivedDataPath $PWD/DerivedData.old VALID_ARCHS=arm64 - name: Checkout pull request merge branch - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: clean: false fetch-depth: 100 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index eb9453b9..f3f533eb 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -32,7 +32,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 + uses: github/codeql-action/upload-sarif@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 with: sarif_file: results.sarif diff --git a/BugsnagPerformance.xcodeproj/project.pbxproj b/BugsnagPerformance.xcodeproj/project.pbxproj index 5e16f01d..38101e98 100644 --- a/BugsnagPerformance.xcodeproj/project.pbxproj +++ b/BugsnagPerformance.xcodeproj/project.pbxproj @@ -236,6 +236,8 @@ 96F129312DCD325E00A6FB2B /* BugsnagPerformanceSpanTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96F129302DCD325E00A6FB2B /* BugsnagPerformanceSpanTests.mm */; }; 96F417152E3B8E7000EABD8E /* BugsnagPerformanceNamedSpans.h in Headers */ = {isa = PBXBuildFile; fileRef = 96F417122E3B8E7000EABD8E /* BugsnagPerformanceNamedSpans.h */; }; 96F417162E3B8E7000EABD8E /* BugsnagPerformanceNamedSpans.docc in Sources */ = {isa = PBXBuildFile; fileRef = 96F417132E3B8E7000EABD8E /* BugsnagPerformanceNamedSpans.docc */; }; + 96F9010F2EE817620026F5B9 /* NSTimer+MainThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 96F9010E2EE8175B0026F5B9 /* NSTimer+MainThread.h */; }; + 96F901112EE817700026F5B9 /* NSTimer+MainThread.m in Sources */ = {isa = PBXBuildFile; fileRef = 96F901102EE817690026F5B9 /* NSTimer+MainThread.m */; }; CB04969729150D860097E526 /* OtlpPackage.h in Headers */ = {isa = PBXBuildFile; fileRef = CB04969529150D860097E526 /* OtlpPackage.h */; }; CB04969829150D860097E526 /* OtlpPackage.mm in Sources */ = {isa = PBXBuildFile; fileRef = CB04969629150D860097E526 /* OtlpPackage.mm */; }; CB04969B2915194E0097E526 /* OtlpUploader.h in Headers */ = {isa = PBXBuildFile; fileRef = CB0496992915194E0097E526 /* OtlpUploader.h */; }; @@ -660,6 +662,8 @@ 96F129302DCD325E00A6FB2B /* BugsnagPerformanceSpanTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BugsnagPerformanceSpanTests.mm; sourceTree = ""; }; 96F417122E3B8E7000EABD8E /* BugsnagPerformanceNamedSpans.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceNamedSpans.h; sourceTree = ""; }; 96F417132E3B8E7000EABD8E /* BugsnagPerformanceNamedSpans.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = BugsnagPerformanceNamedSpans.docc; sourceTree = ""; }; + 96F9010E2EE8175B0026F5B9 /* NSTimer+MainThread.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSTimer+MainThread.h"; sourceTree = ""; }; + 96F901102EE817690026F5B9 /* NSTimer+MainThread.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSTimer+MainThread.m"; sourceTree = ""; }; CB04969529150D860097E526 /* OtlpPackage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OtlpPackage.h; sourceTree = ""; }; CB04969629150D860097E526 /* OtlpPackage.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OtlpPackage.mm; sourceTree = ""; }; CB0496992915194E0097E526 /* OtlpUploader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OtlpUploader.h; sourceTree = ""; }; @@ -896,8 +900,6 @@ 0122C21F29019770002D243C /* Private */ = { isa = PBXGroup; children = ( - 1C7852A32E5F697D00BB8E2D /* SpanContext.mm */, - 1C7852A12E5F5B2E00BB8E2D /* SpanContext.h */, CB572EA829BB783200FD7A2A /* AppStateTracker.h */, CB572EA929BB783200FD7A2A /* AppStateTracker.m */, CBE8EA1C294B5E1500702950 /* Batch.h */, @@ -945,6 +947,8 @@ CBEC51BF296DB311009C0CE3 /* JSON.mm */, 965FBD142DF24D3100D6BACB /* Logging.h */, 098FC8772D3E8D43001B627D /* Metrics.h */, + 96F9010E2EE8175B0026F5B9 /* NSTimer+MainThread.h */, + 96F901102EE817690026F5B9 /* NSTimer+MainThread.m */, CBB48A3A295EE1E10044E9AC /* ObjCUtils.h */, CBB48A3B295EE1E10044E9AC /* ObjCUtils.mm */, CB04969529150D860097E526 /* OtlpPackage.h */, @@ -972,6 +976,8 @@ 01A414CC2913C0F0003152A4 /* SpanAttributes.mm */, 96D4160D29F276FE00AEE435 /* SpanAttributesProvider.h */, 96D4160B29F276E400AEE435 /* SpanAttributesProvider.mm */, + 1C7852A12E5F5B2E00BB8E2D /* SpanContext.h */, + 1C7852A32E5F697D00BB8E2D /* SpanContext.mm */, 963726C42DEAB14D00C739E6 /* SpanControl */, 969EE0EE2E7872A600600F63 /* SpanFactory */, 0122C22329019770002D243C /* SpanKind.h */, @@ -1645,6 +1651,7 @@ CBEBE59329F2783C00BF0B4F /* Swizzle.h in Headers */, 962CE8082E651A0100380522 /* NetworkInstrumentationStateRepository.h in Headers */, 969EE0FE2E794A0700600F63 /* ViewLoadSpanFactoryCallbacks.h in Headers */, + 96F9010F2EE817620026F5B9 /* NSTimer+MainThread.h in Headers */, CBEC51BC296D9EEE009C0CE3 /* PersistentState.h in Headers */, 0122C23829019770002D243C /* BugsnagPerformanceSpan.h in Headers */, 962CE8212E66D24200380522 /* NetworkInstrumentationSystemUtilsImpl.h in Headers */, @@ -2121,6 +2128,7 @@ CBE8EA1B294B5AB800702950 /* Worker.mm in Sources */, CB78819C29E587CE00A58906 /* BugsnagPerformanceLibrary.mm in Sources */, 963726E02DF0B4AD00C739E6 /* BugsnagPerformancePluginContext.m in Sources */, + 96F901112EE817700026F5B9 /* NSTimer+MainThread.m in Sources */, 098FC8552D37A08D001B627D /* SystemInfoSampler.mm in Sources */, 1C68DBA12E535D06002621D1 /* BugsnagPerformanceAppStartSpanQuery.mm in Sources */, 963726C82DEAB1FC00C739E6 /* BugsnagPerformancePriority.m in Sources */, diff --git a/Sources/BugsnagPerformance/Private/BugsnagPerformanceImpl.mm b/Sources/BugsnagPerformance/Private/BugsnagPerformanceImpl.mm index 91494bd4..61662bb3 100644 --- a/Sources/BugsnagPerformance/Private/BugsnagPerformanceImpl.mm +++ b/Sources/BugsnagPerformance/Private/BugsnagPerformanceImpl.mm @@ -19,6 +19,7 @@ #import "ConditionTimeoutExecutor.h" #import "BugsnagPerformanceSpan+Private.h" #import "BugsnagPerformanceAppStartTypePlugin.h" +#import "NSTimer+MainThread.h" using namespace bugsnag; @@ -262,9 +263,9 @@ [worker_ start]; [frameMetricsCollector_ start]; - workerTimer_ = [NSTimer scheduledTimerWithTimeInterval:performWorkInterval_ - repeats:YES - block:^(__unused NSTimer * _Nonnull timer) { + workerTimer_ = [NSTimer mainThreadTimerWithTimeInterval:performWorkInterval_ + repeats:YES + block:^(__unused NSTimer * _Nonnull timer) { blockThis->onWorkInterval(); }]; diff --git a/Sources/BugsnagPerformance/Private/ConditionTimeoutExecutor.h b/Sources/BugsnagPerformance/Private/ConditionTimeoutExecutor.h index a20f199b..e2d35c9d 100644 --- a/Sources/BugsnagPerformance/Private/ConditionTimeoutExecutor.h +++ b/Sources/BugsnagPerformance/Private/ConditionTimeoutExecutor.h @@ -10,6 +10,7 @@ #import #import "BugsnagPerformanceSpanCondition+Private.h" +#import "NSTimer+MainThread.h" #import #import @@ -21,7 +22,7 @@ class ConditionTimeoutExecutor { void scheduleTimeout(BugsnagPerformanceSpanCondition *condition, NSTimeInterval timeout) noexcept { std::lock_guard guard(mutex_); - this->conditionIdToTimer_[condition.conditionId] = [NSTimer scheduledTimerWithTimeInterval:timeout repeats:NO block:^(NSTimer *) { + this->conditionIdToTimer_[condition.conditionId] = [NSTimer mainThreadTimerWithTimeInterval:timeout repeats:NO block:^(NSTimer *) { [condition didTimeout]; }]; } diff --git a/Sources/BugsnagPerformance/Private/Instrumentation/NetworkInstrumentation/Lifecycle/NetworkEarlyPhaseHandlerImpl.mm b/Sources/BugsnagPerformance/Private/Instrumentation/NetworkInstrumentation/Lifecycle/NetworkEarlyPhaseHandlerImpl.mm index 349ecf70..a3978a33 100644 --- a/Sources/BugsnagPerformance/Private/Instrumentation/NetworkInstrumentation/Lifecycle/NetworkEarlyPhaseHandlerImpl.mm +++ b/Sources/BugsnagPerformance/Private/Instrumentation/NetworkInstrumentation/Lifecycle/NetworkEarlyPhaseHandlerImpl.mm @@ -47,7 +47,10 @@ callback(state); if (state.hasBeenVetoed) { [state.overallSpan cancel]; + continue; } - [state.overallSpan internalSetMultipleAttributes:spanAttributesProvider_->networkSpanUrlAttributes(state.url, nil)]; + [state.overallSpan forceMutate:^{ + [state.overallSpan internalSetMultipleAttributes:spanAttributesProvider_->networkSpanUrlAttributes(state.url, nil)]; + }]; } } diff --git a/Sources/BugsnagPerformance/Private/Instrumentation/ViewLoadInstrumentation/Lifecycle/ViewLoadLifecycleHandlerImpl.mm b/Sources/BugsnagPerformance/Private/Instrumentation/ViewLoadInstrumentation/Lifecycle/ViewLoadLifecycleHandlerImpl.mm index 1989bbc6..b33b495a 100644 --- a/Sources/BugsnagPerformance/Private/Instrumentation/ViewLoadInstrumentation/Lifecycle/ViewLoadLifecycleHandlerImpl.mm +++ b/Sources/BugsnagPerformance/Private/Instrumentation/ViewLoadInstrumentation/Lifecycle/ViewLoadLifecycleHandlerImpl.mm @@ -71,7 +71,9 @@ originalImplementation(); return; } - adjustSpanIfPreloaded(overallSpan, state, [NSDate new], viewController); + [overallSpan forceMutate:^{ + adjustSpanIfPreloaded(overallSpan, state, [NSDate new], viewController); + }]; state.viewWillAppearSpan = spanFactory_->startViewWillAppearSpan(viewController, state.overallSpan); originalImplementation(); diff --git a/Sources/BugsnagPerformance/Private/NSTimer+MainThread.h b/Sources/BugsnagPerformance/Private/NSTimer+MainThread.h new file mode 100644 index 00000000..7d5fa989 --- /dev/null +++ b/Sources/BugsnagPerformance/Private/NSTimer+MainThread.h @@ -0,0 +1,19 @@ +// +// NSTimer+MainThread.h +// BugsnagPerformance +// +// Created by Robert Bartoszewski on 09/12/2025. +// Copyright © 2025 Bugsnag. All rights reserved. +// + +#import + +@interface NSTimer (MainThread) + +/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the main run loop in the default mode. +/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead +/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires. +/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references ++ (NSTimer *)mainThreadTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (NS_SWIFT_SENDABLE ^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + +@end diff --git a/Sources/BugsnagPerformance/Private/NSTimer+MainThread.m b/Sources/BugsnagPerformance/Private/NSTimer+MainThread.m new file mode 100644 index 00000000..62eebc7b --- /dev/null +++ b/Sources/BugsnagPerformance/Private/NSTimer+MainThread.m @@ -0,0 +1,27 @@ +// +// NSTimer+MainThread.m +// BugsnagPerformance +// +// Created by Robert Bartoszewski on 09/12/2025. +// Copyright © 2025 Bugsnag. All rights reserved. +// + +#import "NSTimer+MainThread.h" + +@implementation NSTimer (MainThread) + ++ (NSTimer *)mainThreadTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (NS_SWIFT_SENDABLE ^)(NSTimer *timer))block { + NSTimer *timer = [self timerWithTimeInterval:interval repeats:repeats block:block]; + if ([NSThread isMainThread]) { + [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([timer isValid]) { + [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; + } + }); + } + return timer; +} + +@end diff --git a/Sources/BugsnagPerformance/Private/SpanLifecycle/SpanLifecycleHandlerImpl.mm b/Sources/BugsnagPerformance/Private/SpanLifecycle/SpanLifecycleHandlerImpl.mm index 333117a5..d83a5014 100644 --- a/Sources/BugsnagPerformance/Private/SpanLifecycle/SpanLifecycleHandlerImpl.mm +++ b/Sources/BugsnagPerformance/Private/SpanLifecycle/SpanLifecycleHandlerImpl.mm @@ -246,7 +246,9 @@ } if (span != nil && span.state == SpanStateEnded) { - callOnSpanEndCallbacks(span); + [span forceMutate:^{ + callOnSpanEndCallbacks(span); + }]; if (span.state == SpanStateAborted) { return; } diff --git a/Sources/BugsnagPerformance/Private/SpanStackingHandler.mm b/Sources/BugsnagPerformance/Private/SpanStackingHandler.mm index e6565499..b847634d 100644 --- a/Sources/BugsnagPerformance/Private/SpanStackingHandler.mm +++ b/Sources/BugsnagPerformance/Private/SpanStackingHandler.mm @@ -52,13 +52,13 @@ static inline os_activity_id_t currentActivityId() { SpanStackingHandler::currentSpan() { std::lock_guard guard(mutex_); std::shared_ptr state = spanStateForActivity(currentActivityId()); - if (state == nullptr) { - return nullptr; - } - if (!(state->span.state == SpanStateOpen)) { - return nullptr; + while (state != nullptr) { + if (state->span.state == SpanStateOpen) { + return state->span; + } + state = spanStateForActivity(state->parentActivityId); } - return state->span; + return nullptr; } void diff --git a/features/default/automatic/automatic_network.feature b/features/default/automatic/automatic_network.feature index 3148ae46..2799ab5e 100644 --- a/features/default/automatic/automatic_network.feature +++ b/features/default/automatic/automatic_network.feature @@ -164,6 +164,20 @@ Feature: Automatic network instrumentation spans * every span field "kind" equals 3 * a span string attribute "http.url" equals "https://bugsnag.com" + Scenario: AutoInstrumentNetworkEarlyCallbackScenario + Given I run "AutoInstrumentNetworkEarlyCallbackScenario" + And I wait to receive 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$" + * every span field "name" equals "[HTTP/GET]" + * 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 3 + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * a span string attribute "http.url" equals "https://bugsnag.com" + * a span string attribute "http.url" equals "https://bugsnag.com/changed" + Scenario: AutoInstrumentNetworkTracePropagationScenario: Allow All Given I load scenario "AutoInstrumentNetworkTracePropagationScenario" And I configure bugsnag "propagateTraceParentToUrlsMatching" to ".*" diff --git a/features/default/callbacks.feature b/features/default/callbacks.feature index bb31ce72..b60d616d 100644 --- a/features/default/callbacks.feature +++ b/features/default/callbacks.feature @@ -10,6 +10,5 @@ Feature: Setting callbacks Scenario: Set OnEnd Given I run "OnEndCallbackScenario" - And I wait for exactly 1 span - * the trace "Bugsnag-Span-Sampling" header equals "1:1" + And I wait for exactly 3 spans * a span field "name" equals "OnEndCallbackScenario" \ No newline at end of file diff --git a/features/default/manual_spans.feature b/features/default/manual_spans.feature index c2a870e9..db8fe1ce 100644 --- a/features/default/manual_spans.feature +++ b/features/default/manual_spans.feature @@ -314,9 +314,13 @@ Feature: Manual creation of spans Scenario: Set OnEnd Given I run "OnEndCallbackScenario" - And I wait to receive 1 span - * the trace "Bugsnag-Span-Sampling" header equals "1:1" + And I wait to receive 3 spans + * 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 "OnEndCallbackScenario" + * a span field "name" equals "OnEndCallbackScenarioEarlySpan" + * a span field "name" equals "OnEndCallbackScenarioBlockedSpan" + * every span string attribute "OnSpanEndAttribute" equals "OnEndCallbackScenarioValue" + * every span float attribute "bugsnag.span.callbacks_duration" is greater than 1.0 Scenario: Frame metrics - no slow frames Given I run "FrameMetricsNoSlowFramesScenario" @@ -447,3 +451,26 @@ Feature: Manual creation of spans * the trace payload field "resourceSpans.0.scopeSpans.0.spans.0.parentSpanId" is null * the trace payload field "resourceSpans.0.scopeSpans.0.spans.1.parentSpanId" matches the regex "^[A-Fa-f0-9]{16}$" * the trace payload field "resourceSpans.0.scopeSpans.0.spans.2.parentSpanId" is null + + Scenario: Manually start and end spans after starting BugsnagPerformance on a background thread + Given I run "BackgroundThreadStartScenario" + And I wait to receive at least 2 spans + Then the trace "Content-Type" header equals "application/json" + * the trace "Bugsnag-Integrity" header matches the regex "^sha1 [A-Fa-f0-9]{40}$" + * 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 "BackgroundThreadStartScenarioEarlySpan" + * a span field "name" equals "BackgroundThreadStartScenarioSpan" + + Scenario: Parent context should be calculated despite blocked spans on the stack + Given I run "ManualParentBlockedSpanScenario" + And I wait to receive at least 4 spans + Then the trace "Content-Type" header equals "application/json" + * the trace "Bugsnag-Integrity" header matches the regex "^sha1 [A-Fa-f0-9]{40}$" + * 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 "ManualParentBlockedSpanScenarioParent" + * a span field "name" equals "ManualParentBlockedSpanScenarioBlocked1" + * a span field "name" equals "ManualParentBlockedSpanScenarioBlocked2" + * a span field "name" equals "ManualParentBlockedSpanScenarioChild" + * a span named "ManualParentBlockedSpanScenarioBlocked1" is a child of span named "ManualParentBlockedSpanScenarioParent" + * a span named "ManualParentBlockedSpanScenarioBlocked2" is a child of span named "ManualParentBlockedSpanScenarioBlocked1" + * a span named "ManualParentBlockedSpanScenarioChild" is a child of span named "ManualParentBlockedSpanScenarioParent" diff --git a/features/fixtures/ios/Fixture.xcodeproj/project.pbxproj b/features/fixtures/ios/Fixture.xcodeproj/project.pbxproj index 83588429..c2b575ce 100644 --- a/features/fixtures/ios/Fixture.xcodeproj/project.pbxproj +++ b/features/fixtures/ios/Fixture.xcodeproj/project.pbxproj @@ -95,6 +95,9 @@ 96EB8B502EB26B4400DDBF86 /* StartupEnabledMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96EB8B4F2EB26B3C00DDBF86 /* StartupEnabledMetrics.swift */; }; 96F129352DCE0CFE00A6FB2B /* ManualSpanWithRemoteContextParentScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F129342DCE0CFE00A6FB2B /* ManualSpanWithRemoteContextParentScenario.swift */; }; 96F5268C2C259E4E0095D600 /* ManualNetworkSpanCallbackSetToNilScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F5268B2C259E4E0095D600 /* ManualNetworkSpanCallbackSetToNilScenario.swift */; }; + 96F901132EE81E580026F5B9 /* BackgroundThreadStartScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F901122EE81E580026F5B9 /* BackgroundThreadStartScenario.swift */; }; + 96F901172EE836CC0026F5B9 /* ManualParentBlockedSpanScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F901162EE836CC0026F5B9 /* ManualParentBlockedSpanScenario.swift */; }; + 96F901092EE7B7FF0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F901082EE7B7FF0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift */; }; CB0496942913CA300097E526 /* BatchingWithTimeoutScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0496932913CA300097E526 /* BatchingWithTimeoutScenario.swift */; }; CB0AD76E2965BBDA002A3FB6 /* InitialPScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0AD76D2965BBDA002A3FB6 /* InitialPScenario.swift */; }; CB2B8A9D2A0CCEF90054FBBE /* AutoInstrumentFileURLRequestScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2B8A9C2A0CCEF90054FBBE /* AutoInstrumentFileURLRequestScenario.swift */; }; @@ -214,6 +217,9 @@ 96EB8B4F2EB26B3C00DDBF86 /* StartupEnabledMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupEnabledMetrics.swift; sourceTree = ""; }; 96F129342DCE0CFE00A6FB2B /* ManualSpanWithRemoteContextParentScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualSpanWithRemoteContextParentScenario.swift; sourceTree = ""; }; 96F5268B2C259E4E0095D600 /* ManualNetworkSpanCallbackSetToNilScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualNetworkSpanCallbackSetToNilScenario.swift; sourceTree = ""; }; + 96F901122EE81E580026F5B9 /* BackgroundThreadStartScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundThreadStartScenario.swift; sourceTree = ""; }; + 96F901162EE836CC0026F5B9 /* ManualParentBlockedSpanScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualParentBlockedSpanScenario.swift; sourceTree = ""; }; + 96F901082EE7B7FF0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoInstrumentNetworkEarlyCallbackScenario.swift; sourceTree = ""; }; CB0496932913CA300097E526 /* BatchingWithTimeoutScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchingWithTimeoutScenario.swift; sourceTree = ""; }; CB0AD76D2965BBDA002A3FB6 /* InitialPScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialPScenario.swift; sourceTree = ""; }; CB211D0629EEB615008F748D /* BugsnagPerformanceConfiguration+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "BugsnagPerformanceConfiguration+Private.h"; path = "../../../../Sources/BugsnagPerformance/Private/BugsnagPerformanceConfiguration+Private.h"; sourceTree = ""; }; @@ -335,6 +341,7 @@ 9657A89A2A3D06EB001CEF5D /* AutoInstrumentNavigationViewLoadScenario.swift */, CB2B8A9E2A0E80B80054FBBE /* AutoInstrumentNetworkBadAddressScenario.swift */, 0921F02D2A69262300C764EB /* AutoInstrumentNetworkCallbackScenario.swift */, + 96F901082EE7B7FF0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift */, CBEC89222A458BA70088A3CE /* AutoInstrumentNetworkMultiple.swift */, CBE0872A29F81BBB007455F2 /* AutoInstrumentNetworkNoParentScenario.swift */, 09F025082BA08817007D9F73 /* AutoInstrumentNetworkNullURLScenario.swift */, @@ -351,6 +358,7 @@ 9657A8982A3CF75B001CEF5D /* AutoInstrumentTabViewLoadScenario.swift */, 0185C47128F6C983006F9BDC /* AutoInstrumentViewLoadScenario.swift */, CB572EAC29BB829800FD7A2A /* BackgroundForegroundScenario.swift */, + 96F901122EE81E580026F5B9 /* BackgroundThreadStartScenario.swift */, CBAAE2582912601D006D4AA0 /* BatchingScenario.swift */, CB0496932913CA300097E526 /* BatchingWithTimeoutScenario.swift */, 09301DC02B63A65A000A7C12 /* ComplexViewScenario.swift */, @@ -381,6 +389,7 @@ 96F5268B2C259E4E0095D600 /* ManualNetworkSpanCallbackSetToNilScenario.swift */, CBE6B66A28FD66B400D1CF78 /* ManualNetworkSpanScenario.swift */, 09D59E162BDFA23600199E1B /* ManualNetworkTracePropagationScenario.swift */, + 96F901162EE836CC0026F5B9 /* ManualParentBlockedSpanScenario.swift */, 093EE63C2C32E5B900632B30 /* ManualParentSpanScenario.swift */, 01E7918928EC7B5E00855993 /* ManualSpanBeforeStartScenario.swift */, 96DADF502EAFCB1100B56CE6 /* ManualSpanEndOnDestroyScenario.swift */, @@ -524,6 +533,7 @@ 09F3F5302D6F17B300BAA0A3 /* RenderingMetricsScenario.swift in Sources */, 966634E22C9DE648004A934D /* FrameMetricsNoSlowFramesScenario.swift in Sources */, 9691A9DF2CA5E62800707CDF /* FrameMetricsSpanInstrumentRenderingOffScenario.swift in Sources */, + 96F901172EE836CC0026F5B9 /* ManualParentBlockedSpanScenario.swift in Sources */, 09F025092BA08817007D9F73 /* AutoInstrumentNetworkNullURLScenario.swift in Sources */, 0185C47228F6C983006F9BDC /* AutoInstrumentViewLoadScenario.swift in Sources */, 969EE0E22E784E6400600F63 /* ConditionsBasicScenario.swift in Sources */, @@ -581,10 +591,12 @@ 9657A8992A3CF75B001CEF5D /* AutoInstrumentTabViewLoadScenario.swift in Sources */, CBEC89452A4ED0590088A3CE /* MaxPayloadSizeScenario.swift in Sources */, 964735CB2CCF137A00759ED9 /* AutoInstrumentNetworkSharedSessionInvalidateScenario.swift in Sources */, + 96F901092EE7B7FF0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift in Sources */, CB572EAD29BB829800FD7A2A /* BackgroundForegroundScenario.swift in Sources */, 96EB8B502EB26B4400DDBF86 /* StartupEnabledMetrics.swift in Sources */, 966634E02C9DE384004A934D /* FrameMetricsSlowFramesScenario.swift in Sources */, CB2B8A9F2A0E80B80054FBBE /* AutoInstrumentNetworkBadAddressScenario.swift in Sources */, + 96F901132EE81E580026F5B9 /* BackgroundThreadStartScenario.swift in Sources */, DA58B7D62DF87EC500CB80A4 /* PluginInstallErrorScenario.swift in Sources */, 09F025152BAC50EC007D9F73 /* ViewDidLoadDoesntTriggerScenario.swift in Sources */, 0988B5372CAD32C500D131B1 /* InfraCheckNoBugsnagScenario.swift in Sources */, diff --git a/features/fixtures/ios/FixtureXcFramework.xcodeproj/project.pbxproj b/features/fixtures/ios/FixtureXcFramework.xcodeproj/project.pbxproj index 0022c797..7c692e76 100644 --- a/features/fixtures/ios/FixtureXcFramework.xcodeproj/project.pbxproj +++ b/features/fixtures/ios/FixtureXcFramework.xcodeproj/project.pbxproj @@ -96,6 +96,9 @@ 96EB8B522EB277CE00DDBF86 /* StartupEnabledMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96EB8B512EB277CE00DDBF86 /* StartupEnabledMetrics.swift */; }; 96F129332DCE0CDD00A6FB2B /* RenderingMetricsScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F129322DCE0CDD00A6FB2B /* RenderingMetricsScenario.swift */; }; 96F5268C2C259E4E0095D600 /* ManualNetworkSpanCallbackSetToNilScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F5268B2C259E4E0095D600 /* ManualNetworkSpanCallbackSetToNilScenario.swift */; }; + 96F901152EE81E6D0026F5B9 /* BackgroundThreadStartScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F901142EE81E6D0026F5B9 /* BackgroundThreadStartScenario.swift */; }; + 96F901192EE836E10026F5B9 /* ManualParentBlockedSpanScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F901182EE836E10026F5B9 /* ManualParentBlockedSpanScenario.swift */; }; + 96F9010B2EE7B81F0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F9010A2EE7B81F0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift */; }; CB0496942913CA300097E526 /* BatchingWithTimeoutScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0496932913CA300097E526 /* BatchingWithTimeoutScenario.swift */; }; CB0AD76E2965BBDA002A3FB6 /* InitialPScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0AD76D2965BBDA002A3FB6 /* InitialPScenario.swift */; }; CB2B8A9D2A0CCEF90054FBBE /* AutoInstrumentFileURLRequestScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2B8A9C2A0CCEF90054FBBE /* AutoInstrumentFileURLRequestScenario.swift */; }; @@ -233,6 +236,9 @@ 96EB8B512EB277CE00DDBF86 /* StartupEnabledMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupEnabledMetrics.swift; sourceTree = ""; }; 96F129322DCE0CDD00A6FB2B /* RenderingMetricsScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingMetricsScenario.swift; sourceTree = ""; }; 96F5268B2C259E4E0095D600 /* ManualNetworkSpanCallbackSetToNilScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualNetworkSpanCallbackSetToNilScenario.swift; sourceTree = ""; }; + 96F901142EE81E6D0026F5B9 /* BackgroundThreadStartScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundThreadStartScenario.swift; sourceTree = ""; }; + 96F901182EE836E10026F5B9 /* ManualParentBlockedSpanScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualParentBlockedSpanScenario.swift; sourceTree = ""; }; + 96F9010A2EE7B81F0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoInstrumentNetworkEarlyCallbackScenario.swift; sourceTree = ""; }; CB0496932913CA300097E526 /* BatchingWithTimeoutScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchingWithTimeoutScenario.swift; sourceTree = ""; }; CB0AD76D2965BBDA002A3FB6 /* InitialPScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialPScenario.swift; sourceTree = ""; }; CB211D0629EEB615008F748D /* BugsnagPerformanceConfiguration+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "BugsnagPerformanceConfiguration+Private.h"; path = "../../../../Sources/BugsnagPerformance/Private/BugsnagPerformanceConfiguration+Private.h"; sourceTree = ""; }; @@ -358,6 +364,7 @@ 9657A89A2A3D06EB001CEF5D /* AutoInstrumentNavigationViewLoadScenario.swift */, CB2B8A9E2A0E80B80054FBBE /* AutoInstrumentNetworkBadAddressScenario.swift */, 0921F02D2A69262300C764EB /* AutoInstrumentNetworkCallbackScenario.swift */, + 96F9010A2EE7B81F0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift */, CBEC89222A458BA70088A3CE /* AutoInstrumentNetworkMultiple.swift */, CBE0872A29F81BBB007455F2 /* AutoInstrumentNetworkNoParentScenario.swift */, 09F025082BA08817007D9F73 /* AutoInstrumentNetworkNullURLScenario.swift */, @@ -374,6 +381,7 @@ 9657A8982A3CF75B001CEF5D /* AutoInstrumentTabViewLoadScenario.swift */, 0185C47128F6C983006F9BDC /* AutoInstrumentViewLoadScenario.swift */, CB572EAC29BB829800FD7A2A /* BackgroundForegroundScenario.swift */, + 96F901142EE81E6D0026F5B9 /* BackgroundThreadStartScenario.swift */, CBAAE2582912601D006D4AA0 /* BatchingScenario.swift */, CB0496932913CA300097E526 /* BatchingWithTimeoutScenario.swift */, 09301DC02B63A65A000A7C12 /* ComplexViewScenario.swift */, @@ -404,6 +412,7 @@ 96F5268B2C259E4E0095D600 /* ManualNetworkSpanCallbackSetToNilScenario.swift */, CBE6B66A28FD66B400D1CF78 /* ManualNetworkSpanScenario.swift */, 09D59E162BDFA23600199E1B /* ManualNetworkTracePropagationScenario.swift */, + 96F901182EE836E10026F5B9 /* ManualParentBlockedSpanScenario.swift */, 093EE63C2C32E5B900632B30 /* ManualParentSpanScenario.swift */, 01E7918928EC7B5E00855993 /* ManualSpanBeforeStartScenario.swift */, 96DADF532EAFCB6300B56CE6 /* ManualSpanEndOnDestroyScenario.swift */, @@ -548,6 +557,7 @@ 96D528D02C77F38400FEA2E2 /* FixedSamplingProbabilityZeroScenario.swift in Sources */, 09D59E1D2BE105F700199E1B /* AutoInstrumentNetworkTracePropagationScenario.swift in Sources */, 09F3F52A2D6C72B300BAA0A3 /* CPUMetricsScenario.swift in Sources */, + 96F9010B2EE7B81F0026F5B9 /* AutoInstrumentNetworkEarlyCallbackScenario.swift in Sources */, 01FE4DAD28E1AEBD00D1F239 /* ViewController.swift in Sources */, CBEC89232A458BA70088A3CE /* AutoInstrumentNetworkMultiple.swift in Sources */, 093EE63D2C32E5B900632B30 /* ManualParentSpanScenario.swift in Sources */, @@ -579,8 +589,10 @@ 0983A1792B14B20C00DDF4FF /* AutoInstrumentSwiftUIScenario.swift in Sources */, CBF62109291A4F47004BEE0B /* RetryScenario.swift in Sources */, CB7FD92B299BB4E300499E13 /* ManualUIViewLoadScenario.swift in Sources */, + 96F901192EE836E10026F5B9 /* ManualParentBlockedSpanScenario.swift in Sources */, 9691A9E12CA7588700707CDF /* FrameMetricsNonFirstClassSpanInstrumentRenderingOnScenario.swift in Sources */, 09F3F52C2D6C72BD00BAA0A3 /* MemoryMetricsScenario.swift in Sources */, + 96F901152EE81E6D0026F5B9 /* BackgroundThreadStartScenario.swift in Sources */, 09637A3F2B06082200F4F776 /* Logging.m in Sources */, CB3477182901481F0033759C /* AutoInstrumentNetworkWithParentScenario.swift in Sources */, 091B95742CA18F66007DC8A9 /* AutoInstrumentNetworkPreStartDisabledScenario.swift in Sources */, diff --git a/features/fixtures/ios/Scenarios/AutoInstrumentNetworkEarlyCallbackScenario.swift b/features/fixtures/ios/Scenarios/AutoInstrumentNetworkEarlyCallbackScenario.swift new file mode 100644 index 00000000..91ee65dd --- /dev/null +++ b/features/fixtures/ios/Scenarios/AutoInstrumentNetworkEarlyCallbackScenario.swift @@ -0,0 +1,55 @@ +// +// AutoInstrumentNetworkEarlyCallbackScenario.swift +// Fixture +// +// Created by Robert Bartoszewski on 09/12/2025. +// + +import Foundation + +@objcMembers +class AutoInstrumentNetworkEarlyCallbackScenario: Scenario { + + override func postLoad() { + super.postLoad() + query(url: URL(string: "https://bugsnag.com")!) + query(url: URL(string: "https://bugsnag.com/changeme")!) + query(url: URL(string: "https://google.com")!) + + // Wait for the query to finish before starting bugsnag + Thread.sleep(forTimeInterval: 2.0) + } + + override func setInitialBugsnagConfiguration() { + super.setInitialBugsnagConfiguration() + bugsnagPerfConfig.autoInstrumentNetworkRequests = true + bugsnagPerfConfig.networkRequestCallback = { (origInfo: BugsnagPerformanceNetworkRequestInfo) -> BugsnagPerformanceNetworkRequestInfo in + let info = self.filterAdminMazeRunnerNetRequests(info: origInfo) + + let testUrl = info.url + if (testUrl == nil) { + return info + } + + let url = testUrl! + + if url.absoluteString == "https://google.com" { + info.url = nil + } else if url.lastPathComponent == "changeme" { + info.url = URL(string:"changed", relativeTo:url.deletingLastPathComponent()) + } + + return info + } + } + + func query(url: URL) { + let task = URLSession.shared.dataTask(with: url) {(data, response, error) in + } + task.resume() + + } + + override func run() { + } +} diff --git a/features/fixtures/ios/Scenarios/BackgroundThreadStartScenario.swift b/features/fixtures/ios/Scenarios/BackgroundThreadStartScenario.swift new file mode 100644 index 00000000..138592a2 --- /dev/null +++ b/features/fixtures/ios/Scenarios/BackgroundThreadStartScenario.swift @@ -0,0 +1,35 @@ +// +// BackgroundThreadStartScenario.swift +// Fixture +// +// Created by Robert Bartoszewski on 09/12/2025. +// + +import BugsnagPerformance + +@objcMembers +class BackgroundThreadStartScenario: Scenario { + + override func setInitialBugsnagConfiguration() { + super.setInitialBugsnagConfiguration() + + // Ensure the batch doesn't get full, as we want the spans to be delivered due to reaching work interval + bugsnagPerfConfig.internal.autoTriggerExportOnBatchSize = 100 + bugsnagPerfConfig.internal.performWorkInterval = 5.0 + } + + override func startBugsnag() { + BugsnagPerformance.startSpan(name: "BackgroundThreadStartScenarioEarlySpan").end() + DispatchQueue + .global(qos: .background) + .asyncAfter(deadline: .now() + 2.0) { + super.startBugsnag() + } + } + + override func run() { + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + BugsnagPerformance.startSpan(name: "BackgroundThreadStartScenarioSpan").end() + } + } +} diff --git a/features/fixtures/ios/Scenarios/ManualParentBlockedSpanScenario.swift b/features/fixtures/ios/Scenarios/ManualParentBlockedSpanScenario.swift new file mode 100644 index 00000000..3addee44 --- /dev/null +++ b/features/fixtures/ios/Scenarios/ManualParentBlockedSpanScenario.swift @@ -0,0 +1,35 @@ +// +// ManualParentBlockedSpanScenario.swift +// Fixture +// +// Created by Robert Bartoszewski on 09/12/2025. +// + +import BugsnagPerformance + +@objcMembers +class ManualParentBlockedSpanScenario: Scenario { + + override func setInitialBugsnagConfiguration() { + super.setInitialBugsnagConfiguration() + bugsnagPerfConfig.internal.autoTriggerExportOnBatchSize = 4; + } + + override func run() { + let parentSpan = BugsnagPerformance.startSpan(name: "ManualParentBlockedSpanScenarioParent") + let blockedSpan1 = BugsnagPerformance.startSpan(name: "ManualParentBlockedSpanScenarioBlocked1") + let blockedSpan2 = BugsnagPerformance.startSpan(name: "ManualParentBlockedSpanScenarioBlocked2") + let condition1 = blockedSpan1.block(timeout: 1.0) + condition1?.upgrade() + blockedSpan1.end() + let condition2 = blockedSpan2.block(timeout: 1.0) + condition2?.upgrade() + blockedSpan2.end() + + BugsnagPerformance.startSpan(name: "ManualParentBlockedSpanScenarioChild").end() + parentSpan.end() + + condition1?.cancel() + condition2?.cancel() + } +} diff --git a/features/fixtures/ios/Scenarios/OnEndCallbackScenario.swift b/features/fixtures/ios/Scenarios/OnEndCallbackScenario.swift index 4fe0001e..451198f9 100644 --- a/features/fixtures/ios/Scenarios/OnEndCallbackScenario.swift +++ b/features/fixtures/ios/Scenarios/OnEndCallbackScenario.swift @@ -15,6 +15,8 @@ class OnEndCallbackScenario: Scenario { override func setInitialBugsnagConfiguration() { super.setInitialBugsnagConfiguration() + + BugsnagPerformance.startSpan(name: "OnEndCallbackScenarioEarlySpan").end() bugsnagPerfConfig.add(onSpanEndCallback: { (span: BugsnagPerformanceSpan) -> Bool in return true @@ -36,11 +38,21 @@ class OnEndCallbackScenario: Scenario { bugsnagPerfConfig.add(onSpanEndCallback: { (span: BugsnagPerformanceSpan) -> Bool in return span.name != "drop_me_too" }) + bugsnagPerfConfig.add(onSpanEndCallback: { (span: BugsnagPerformanceSpan) -> Bool in + Thread.sleep(forTimeInterval: 1.2) + span.setAttribute("OnSpanEndAttribute", withValue: "OnEndCallbackScenarioValue") + return true + }) } override func run() { BugsnagPerformance.startSpan(name: "OnEndCallbackScenario").end() BugsnagPerformance.startSpan(name: "drop_me").end() BugsnagPerformance.startSpan(name: "drop_me_too").end() + let blockedSpan = BugsnagPerformance.startSpan(name: "OnEndCallbackScenarioBlockedSpan") + let condition = blockedSpan.block(timeout: 0.7) + condition?.upgrade() + blockedSpan.end() + condition?.close(endTime: Date()) } } diff --git a/features/steps/app_steps.rb b/features/steps/app_steps.rb index 6356e0d0..43c2c2a6 100644 --- a/features/steps/app_steps.rb +++ b/features/steps/app_steps.rb @@ -231,6 +231,13 @@ def run_command(action, args) Maze.check.false(attribute_values.empty?) end +Then('every span float attribute {string} is greater than {float}') do |attribute, expected| + spans = spans_from_request_list(Maze::Server.list_for('traces')) + selected_attributes = spans.map { |span| span['attributes'].find { |a| a['key'].eql?(attribute) && a['value'].has_key?('doubleValue') } }.compact + attribute_values = selected_attributes.map { |a| a['value']['doubleValue'].to_f > expected } + Maze.check.not_includes attribute_values, false +end + Then('a span float attribute {string} equals {float}') do |attribute, expected| spans = spans_from_request_list(Maze::Server.list_for('traces')) selected_attributes = spans.map { |span| span['attributes'].find { |a| a['key'].eql?(attribute) && a['value'].has_key?('doubleValue') } }.compact