Skip to content

Commit a2ae764

Browse files
antonisclaude
andcommitted
fix(ios): Resolve getNewScreenTimeToDisplay Promise with number not array
`getTimeToDisplay:` typed its block as `RCTResponseSenderBlock` (the callback convention that wraps results in an `NSArray`), but it is only invoked via the Promise-based `getNewScreenTimeToDisplay` bridge method, whose `RCTPromiseResolveBlock` expects a raw value. The Promise therefore resolved with a single-element array instead of a timestamp number. The array survived the duration arithmetic via coercion but was assigned directly as the TTID span's `timestamp`, producing a spurious `deadline_exceeded` status and an inflated duration. Retype the block to `RCTPromiseResolveBlock` and resolve with the raw timestamp (and `nil` on non-iOS instead of an empty array). Adds a native regression test asserting the resolve value is a scalar `NSNumber`. Fixes #6433 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 26843eb commit a2ae764

4 files changed

Lines changed: 32 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
- Add `Sentry.reportFullyDisplayed()` imperative API for signaling Time to Full Display ([#6419](https://github.com/getsentry/sentry-react-native/pull/6419))
1414

15+
### Fixes
16+
17+
- Fix iOS time-to-initial-display fallback spans reporting a spurious `deadline_exceeded` status and inflated duration ([#6433](https://github.com/getsentry/sentry-react-native/issues/6433))
18+
1519
## 8.18.0
1620

1721
### Features

packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTimeToDisplayTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,29 @@ final class RNSentryTimeToDisplayTests: XCTestCase {
3030
XCTAssertNotNil(newestEntry)
3131
}
3232

33+
func testGetTimeToDisplayResolvesWithNumberNotArray() {
34+
let sut = RNSentryTimeToDisplay()
35+
let expectation = self.expectation(description: "resolve block is called")
36+
var resolvedValue: Any?
37+
38+
// `getTimeToDisplay:` is invoked with an `RCTPromiseResolveBlock` from the
39+
// Promise-based `getNewScreenTimeToDisplay` bridge method, so it must resolve
40+
// with a raw timestamp value and never wrap it in an array.
41+
sut.getTimeToDisplay { value in
42+
resolvedValue = value
43+
expectation.fulfill()
44+
}
45+
46+
waitForExpectations(timeout: 5)
47+
48+
XCTAssertFalse(resolvedValue is [Any],
49+
"getTimeToDisplay must resolve with a scalar timestamp, not an array")
50+
let number = try? XCTUnwrap(resolvedValue as? NSNumber,
51+
"getTimeToDisplay must resolve with an NSNumber timestamp")
52+
XCTAssertNotNil(number)
53+
XCTAssertGreaterThan(number?.doubleValue ?? 0, 0)
54+
}
55+
3356
func testHandlesEarlyPoppedValues() {
3457
let maxSize = TIME_TO_DISPLAY_ENTRIES_MAX_SIZE + 1
3558
for i in 1...maxSize {

packages/core/ios/RNSentryTimeToDisplay.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ static const int TIME_TO_DISPLAY_ENTRIES_MAX_SIZE = 50;
99
+ (void)setActiveSpanId:(NSString *)spanId;
1010
+ (void)putTimeToInitialDisplayForActiveSpan:(NSNumber *)timestampSeconds;
1111

12-
- (void)getTimeToDisplay:(RCTResponseSenderBlock)callback;
12+
- (void)getTimeToDisplay:(RCTPromiseResolveBlock)callback;
1313

1414
@end

packages/core/ios/RNSentryTimeToDisplay.m

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// React Native bridge / JS thread (setActiveSpanId, pop). Synchronize every access.
77
@implementation RNSentryTimeToDisplay {
88
CADisplayLink *displayLink;
9-
RCTResponseSenderBlock resolveBlock;
9+
RCTPromiseResolveBlock resolveBlock;
1010
}
1111

1212
static NSMutableDictionary<NSString *, NSNumber *> *screenIdToRenderDuration;
@@ -92,7 +92,7 @@ + (void)putTimeToDisplayFor:(NSString *)screenId value:(NSNumber *)value
9292
}
9393

9494
// Rename requestAnimationFrame to getTimeToDisplay
95-
- (void)getTimeToDisplay:(RCTResponseSenderBlock)callback
95+
- (void)getTimeToDisplay:(RCTPromiseResolveBlock)callback
9696
{
9797
// Store the resolve block to use in the callback.
9898
resolveBlock = callback;
@@ -102,7 +102,7 @@ - (void)getTimeToDisplay:(RCTResponseSenderBlock)callback
102102
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
103103
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
104104
#else
105-
resolveBlock(@[]); // Return nothing if not iOS.
105+
resolveBlock(nil); // Return nothing if not iOS.
106106
#endif
107107
}
108108

@@ -114,7 +114,7 @@ - (void)handleDisplayLink:(CADisplayLink *)link
114114

115115
// Ensure the callback is valid and pass the current time back
116116
if (resolveBlock) {
117-
resolveBlock(@[ @(currentTime) ]); // Call the callback with the current time
117+
resolveBlock(@(currentTime)); // Resolve the promise with the current time
118118
resolveBlock = nil; // Clear the block after it's called
119119
}
120120

0 commit comments

Comments
 (0)