Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
12 changes: 10 additions & 2 deletions BugsnagPerformance.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -660,6 +662,8 @@
96F129302DCD325E00A6FB2B /* BugsnagPerformanceSpanTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BugsnagPerformanceSpanTests.mm; sourceTree = "<group>"; };
96F417122E3B8E7000EABD8E /* BugsnagPerformanceNamedSpans.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceNamedSpans.h; sourceTree = "<group>"; };
96F417132E3B8E7000EABD8E /* BugsnagPerformanceNamedSpans.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = BugsnagPerformanceNamedSpans.docc; sourceTree = "<group>"; };
96F9010E2EE8175B0026F5B9 /* NSTimer+MainThread.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSTimer+MainThread.h"; sourceTree = "<group>"; };
96F901102EE817690026F5B9 /* NSTimer+MainThread.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSTimer+MainThread.m"; sourceTree = "<group>"; };
CB04969529150D860097E526 /* OtlpPackage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OtlpPackage.h; sourceTree = "<group>"; };
CB04969629150D860097E526 /* OtlpPackage.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OtlpPackage.mm; sourceTree = "<group>"; };
CB0496992915194E0097E526 /* OtlpUploader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OtlpUploader.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -896,8 +900,6 @@
0122C21F29019770002D243C /* Private */ = {
isa = PBXGroup;
children = (
1C7852A32E5F697D00BB8E2D /* SpanContext.mm */,
1C7852A12E5F5B2E00BB8E2D /* SpanContext.h */,
CB572EA829BB783200FD7A2A /* AppStateTracker.h */,
CB572EA929BB783200FD7A2A /* AppStateTracker.m */,
CBE8EA1C294B5E1500702950 /* Batch.h */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#import "ConditionTimeoutExecutor.h"
#import "BugsnagPerformanceSpan+Private.h"
#import "BugsnagPerformanceAppStartTypePlugin.h"
#import "NSTimer+MainThread.h"

using namespace bugsnag;

Expand Down Expand Up @@ -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();
}];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#import <Foundation/Foundation.h>
#import "BugsnagPerformanceSpanCondition+Private.h"
#import "NSTimer+MainThread.h"
#import <map>
#import <mutex>

Expand All @@ -21,7 +22,7 @@ class ConditionTimeoutExecutor {

void scheduleTimeout(BugsnagPerformanceSpanCondition *condition, NSTimeInterval timeout) noexcept {
std::lock_guard<std::mutex> 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];
}];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
}];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
19 changes: 19 additions & 0 deletions Sources/BugsnagPerformance/Private/NSTimer+MainThread.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// NSTimer+MainThread.h
// BugsnagPerformance
//
// Created by Robert Bartoszewski on 09/12/2025.
// Copyright © 2025 Bugsnag. All rights reserved.
//

#import <Foundation/Foundation.h>

@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
27 changes: 27 additions & 0 deletions Sources/BugsnagPerformance/Private/NSTimer+MainThread.m
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@
}

if (span != nil && span.state == SpanStateEnded) {
callOnSpanEndCallbacks(span);
[span forceMutate:^{
callOnSpanEndCallbacks(span);
}];
if (span.state == SpanStateAborted) {
return;
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/BugsnagPerformance/Private/SpanStackingHandler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ static inline os_activity_id_t currentActivityId() {
SpanStackingHandler::currentSpan() {
std::lock_guard<std::mutex> guard(mutex_);
std::shared_ptr<SpanActivityState> 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
Expand Down
14 changes: 14 additions & 0 deletions features/default/automatic/automatic_network.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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 ".*"
Expand Down
3 changes: 1 addition & 2 deletions features/default/callbacks.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
31 changes: 29 additions & 2 deletions features/default/manual_spans.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Loading