Skip to content

Commit

Permalink
feat: Add time-to-initial-display and time-to-full-display measuremen…
Browse files Browse the repository at this point in the history
…ts to ViewController transactions (#2843)

Added time-to-initial-display and time-to-full-display measurements to ViewController transactions

Co-authored-by: Philipp Hofmann <philipp.hofmann@sentry.io>
Co-authored-by: Andrew McKnight <andrew.mcknight@sentry.io>
  • Loading branch information
3 people authored Mar 31, 2023
1 parent cf724da commit 98a8c16
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Time to initial and full display (#2724)
- Add time-to-initial-display and time-to-full-display measurements to ViewController transactions (#2843)
- Add `name` and `geo` to User (#2710)

### Fixes
Expand Down
1 change: 0 additions & 1 deletion Sources/Sentry/SentryMeasurementValue.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ - (instancetype)initWithValue:(NSNumber *)value unit:(SentryMeasurementUnit *)un

- (NSDictionary<NSString *, id> *)serialize
{

if (self.unit != nil) {
return @{ @"value" : _value, @"unit" : _unit.unit };
} else {
Expand Down
28 changes: 25 additions & 3 deletions Sources/Sentry/SentryTimeToDisplayTracker.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "SentryTimeToDisplayTracker.h"
#import "SentryCurrentDate.h"
#import "SentryFramesTracker.h"
#import "SentryMeasurementValue.h"
#import "SentrySpan.h"
#import "SentrySpanContext.h"
#import "SentrySpanId.h"
Expand Down Expand Up @@ -74,22 +75,43 @@ - (void)reportFullyDisplayed
{
_fullyDisplayedReported = YES;
if (self.waitForFullDisplay && _isReadyToDisplay) {
// We need the timestamp to be able to calculate the duration
// but we can't finish first and add measure later because
// finishing the span may trigger the tracer finishInternal.
self.fullDisplaySpan.timestamp = [SentryCurrentDate date];
[self addTimeToDisplayMeasurement:self.fullDisplaySpan name:@"time_to_full_display"];
[self.fullDisplaySpan finish];
}
}

- (void)addTimeToDisplayMeasurement:(SentrySpan *)span name:(NSString *)name
{
NSTimeInterval duration = [span.timestamp timeIntervalSinceDate:span.startTimestamp] * 1000;
[span setMeasurement:name value:@(duration) unit:SentryMeasurementUnitDuration.millisecond];
}

- (void)framesTrackerHasNewFrame
{
NSDate *finishTime = [SentryCurrentDate date];

// The purpose of TTID and TTFD is to measure how long
// takes to the content of the screen to change.
// Thats why we need to wait for the next frame to be drawn.
if (_waitForFullDisplay && _fullyDisplayedReported && self.fullDisplaySpan.isFinished == NO) {
[self.fullDisplaySpan finish];
}
if (_isReadyToDisplay && self.initialDisplaySpan.isFinished == NO) {
self.initialDisplaySpan.timestamp = finishTime;

[self addTimeToDisplayMeasurement:self.initialDisplaySpan name:@"time_to_initial_display"];

[self.initialDisplaySpan finish];
[_frameTracker removeListener:self];
}
if (_waitForFullDisplay && _fullyDisplayedReported && self.fullDisplaySpan.isFinished == NO) {
self.fullDisplaySpan.timestamp = finishTime;

[self addTimeToDisplayMeasurement:self.initialDisplaySpan name:@"time_to_full_display"];

[self.fullDisplaySpan finish];
}
}

- (void)trimTTFDIdNecessaryForTracer:(SentryTracer *)tracer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class SentryTimeToDisplayTrackerTest: XCTestCase {
XCTAssertEqual(ttidSpan.spanDescription, "UIViewController initial display")
XCTAssertEqual(ttidSpan.operation, SentrySpanOperationUILoadInitialDisplay)

assertMeasurement(tracer: tracer, name: "time_to_initial_display", duration: 2_000)

XCTAssertEqual(Dynamic(fixture.framesTracker).listeners.count, 0)
}

Expand Down Expand Up @@ -110,6 +112,9 @@ class SentryTimeToDisplayTrackerTest: XCTestCase {
fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 11))
sut.reportFullyDisplayed()

assertMeasurement(tracer: tracer, name: "time_to_initial_display", duration: 2_000)
assertMeasurement(tracer: tracer, name: "time_to_full_display", duration: 4_000)

XCTAssertEqual(ttidSpan?.timestamp, Date(timeIntervalSince1970: 9))
XCTAssertTrue(ttidSpan?.isFinished ?? false)
XCTAssertEqual(tracer.children.count, 2)
Expand Down Expand Up @@ -209,10 +214,11 @@ class SentryTimeToDisplayTrackerTest: XCTestCase {
}

func testFullDisplay_reportedBefore_initialDisplay() {
fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 7))

let sut = fixture.getSut(for: UIViewController(), waitForFullDisplay: true)
let tracer = fixture.tracer
sut.start(for: tracer)
fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 7))

fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 9))
sut.reportFullyDisplayed()
Expand All @@ -221,9 +227,47 @@ class SentryTimeToDisplayTrackerTest: XCTestCase {
sut.reportReadyToDisplay()
fixture.displayLinkWrapper.normalFrame()

assertMeasurement(tracer: tracer, name: "time_to_initial_display", duration: 4_000)
assertMeasurement(tracer: tracer, name: "time_to_full_display", duration: 4_000)

XCTAssertEqual(sut.initialDisplaySpan?.timestamp, fixture.dateProvider.date())
XCTAssertEqual(sut.fullDisplaySpan?.timestamp, sut.initialDisplaySpan?.timestamp)
}

func testReportFullyDisplayed_afterFinishingTracer_withWaitForChildren() {
fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 9))

let hub = TestHub(client: SentryClient(options: Options()), andScope: nil)
let tracer = SentryTracer(transactionContext: TransactionContext(operation: "Test Operation"), hub: hub, waitForChildren: true)
let sut = fixture.getSut(for: UIViewController(), waitForFullDisplay: true)

sut.start(for: tracer)

fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 10))
sut.reportReadyToDisplay()
fixture.displayLinkWrapper.normalFrame()

fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 11))

tracer.finish()

fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 12))
sut.reportFullyDisplayed()

let transaction = hub.capturedTransactionsWithScope.first?.transaction
let measurements = transaction?["measurements"] as? [String: Any]
let ttid = measurements?["time_to_initial_display"] as? [String: Any]
let ttfd = measurements?["time_to_full_display"] as? [String: Any]

XCTAssertEqual(ttid?["value"] as? Int, 1_000)
XCTAssertEqual(ttfd?["value"] as? Int, 3_000)
}

func assertMeasurement(tracer: SentryTracer, name: String, duration: TimeInterval) {
XCTAssertEqual(tracer.measurements[name]?.value, NSNumber(value: duration))
XCTAssertEqual(tracer.measurements[name]?.unit?.unit, "millisecond")

}
}

#endif
1 change: 1 addition & 0 deletions Tests/SentryTests/SentryTests-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
#import "URLSessionTaskMock.h"
@import SentryPrivate;
#import "SentryEnvelopeAttachmentHeader.h"
#import "SentryMeasurementValue.h"
#import "SentryNSProcessInfoWrapper.h"
#import "SentryPerformanceTracker+Testing.h"
#import "SentrySpanOperations.h"
Expand Down
6 changes: 6 additions & 0 deletions Tests/SentryTests/State/TestHub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ class TestHub: SentryHub {

return event.eventId
}

var capturedTransactionsWithScope: [(transaction: [String: Any], scope: Scope)] = []
override func capture(_ transaction: Transaction, with scope: Scope) -> SentryId {
capturedTransactionsWithScope.append((transaction.serialize(), scope))
return super.capture(transaction, with: scope)
}
}

0 comments on commit 98a8c16

Please sign in to comment.